"""
Breakpoint de HARDWARE (registradores de debug Dr0-Dr3/Dr7) via GetThreadContext/SetThreadContext,
SEM escrever nenhum byte no codigo executavel e SEM anexar como debugger de verdade
(sem DebugActiveProcess) -- duas limitacoes que ja mapeamos:

  1. patch_int3.py (escreve 0xCC no codigo) parece disparar um verificador de integridade de
     codigo do jogo depois de alguns segundos (fecha limpo, sem crash/dump) -- ver findings.md,
     secao "Achado critico (21/08)".
  2. Anexar um debugger de verdade (x32dbg, cdb invasivo) trava a janela DirectX imediatamente.

Essa tecnica usa os registradores de debug da CPU (Dr0=endereco, Dr7=habilita+condicao) via
SetThreadContext em CADA thread do processo alvo. Como e' estado da THREAD (nao memoria/codigo),
nao deveria disparar um checksum de integridade de codigo. E como nao usamos DebugActiveProcess,
o processo nao vira "debuggee" formalmente (WaitForDebugEvent nao e' necessario pra isso disparar
uma excecao STATUS_SINGLE_STEP/STATUS_BREAKPOINT quando o Dr7 esta habilitado) -- o LocalDumps
(WER) configurado deve pegar a excecao do mesmo jeito que pegou o INT3 antes.

Uso:
    python patch_hwbp.py <pid> <endereco_hex>

Restaura os Dr0/Dr7 originais NAO e' feito automaticamente (o processo deve ser encerrado ou
relancado depois do teste, igual ao patch_int3.py).
"""
import ctypes
import ctypes.wintypes as wintypes
import sys

PROCESS_ALL_ACCESS = 0x1F0FFF
THREAD_ALL_ACCESS = 0x1F03FF
TH32CS_SNAPTHREAD = 0x00000004
CONTEXT_DEBUG_REGISTERS = 0x00010000 | 0x00000010  # x86: CONTEXT_i386 | CONTEXT_DEBUG_REGISTERS


class THREADENTRY32(ctypes.Structure):
    _fields_ = [
        ("dwSize", wintypes.DWORD),
        ("cntUsage", wintypes.DWORD),
        ("th32ThreadID", wintypes.DWORD),
        ("th32OwnerProcessID", wintypes.DWORD),
        ("tpBasePri", ctypes.c_long),
        ("tpDeltaPri", ctypes.c_long),
        ("dwFlags", wintypes.DWORD),
    ]


class CONTEXT(ctypes.Structure):
    _fields_ = [
        ("ContextFlags", wintypes.DWORD),
        ("Dr0", wintypes.DWORD),
        ("Dr1", wintypes.DWORD),
        ("Dr2", wintypes.DWORD),
        ("Dr3", wintypes.DWORD),
        ("Dr6", wintypes.DWORD),
        ("Dr7", wintypes.DWORD),
        ("FloatSave", ctypes.c_byte * 112),
        ("SegGs", wintypes.DWORD),
        ("SegFs", wintypes.DWORD),
        ("SegEs", wintypes.DWORD),
        ("SegDs", wintypes.DWORD),
        ("Edi", wintypes.DWORD),
        ("Esi", wintypes.DWORD),
        ("Ebx", wintypes.DWORD),
        ("Edx", wintypes.DWORD),
        ("Ecx", wintypes.DWORD),
        ("Eax", wintypes.DWORD),
        ("Ebp", wintypes.DWORD),
        ("Eip", wintypes.DWORD),
        ("SegCs", wintypes.DWORD),
        ("EFlags", wintypes.DWORD),
        ("Esp", wintypes.DWORD),
        ("SegSs", wintypes.DWORD),
        ("ExtendedRegisters", ctypes.c_byte * 512),
    ]


def enum_threads(pid):
    kernel32 = ctypes.windll.kernel32
    snap = kernel32.CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0)
    if snap == -1:
        raise OSError("CreateToolhelp32Snapshot falhou")
    entry = THREADENTRY32()
    entry.dwSize = ctypes.sizeof(THREADENTRY32)
    tids = []
    if kernel32.Thread32First(snap, ctypes.byref(entry)):
        while True:
            if entry.th32OwnerProcessID == pid:
                tids.append(entry.th32ThreadID)
            if not kernel32.Thread32Next(snap, ctypes.byref(entry)):
                break
    kernel32.CloseHandle(snap)
    return tids


def main():
    pid = int(sys.argv[1])
    # ate 4 enderecos (Dr0-Dr3), separados por virgula: python patch_hwbp.py <pid> addr1,addr2,addr3
    addrs = [int(a, 16) for a in sys.argv[2].split(",")]
    assert 1 <= len(addrs) <= 4, "no maximo 4 enderecos (Dr0-Dr3)"

    kernel32 = ctypes.windll.kernel32
    tids = enum_threads(pid)
    print(f"threads encontradas no PID {pid}: {tids}")

    dr_fields = ["Dr0", "Dr1", "Dr2", "Dr3"]

    ok_count = 0
    for tid in tids:
        hThread = kernel32.OpenThread(THREAD_ALL_ACCESS, False, tid)
        if not hThread:
            print(f"  tid={tid}: OpenThread falhou ({ctypes.GetLastError()})")
            continue

        # IMPORTANTE: uma thread RODANDO nao aceita GetThreadContext/SetThreadContext de forma
        # confiavel (o estado "vivo" fica nos registradores da CPU do core que a executa, nao
        # so na estrutura salva do kernel) -- precisa suspender antes e retomar depois.
        suspend_count = kernel32.SuspendThread(hThread)
        if suspend_count == 0xFFFFFFFF:
            print(f"  tid={tid}: SuspendThread falhou ({ctypes.GetLastError()})")
            kernel32.CloseHandle(hThread)
            continue

        ctx = CONTEXT()
        ctx.ContextFlags = CONTEXT_DEBUG_REGISTERS
        if not kernel32.GetThreadContext(hThread, ctypes.byref(ctx)):
            print(f"  tid={tid}: GetThreadContext falhou ({ctypes.GetLastError()})")
            kernel32.ResumeThread(hThread)
            kernel32.CloseHandle(hThread)
            continue

        dr7 = ctx.Dr7 & ~0xFFFF0000  # zera bits de condicao/tamanho de todos os slots
        for i, addr in enumerate(addrs):
            setattr(ctx, dr_fields[i], addr)
            dr7 |= (1 << (i * 2))  # habilita Li (local enable) do slot i
        ctx.Dr7 = dr7
        ctx.ContextFlags = CONTEXT_DEBUG_REGISTERS

        if kernel32.SetThreadContext(hThread, ctypes.byref(ctx)):
            enderecos = ", ".join(f"Dr{i}={a:#x}" for i, a in enumerate(addrs))
            print(f"  tid={tid}: {enderecos} Dr7={ctx.Dr7:#x} setado com sucesso")
            ok_count += 1
        else:
            print(f"  tid={tid}: SetThreadContext falhou ({ctypes.GetLastError()})")

        kernel32.ResumeThread(hThread)
        kernel32.CloseHandle(hThread)

    print(f"total: {ok_count}/{len(tids)} threads com breakpoint(s) de hardware ativo(s)")


if __name__ == "__main__":
    sys.exit(main())
