Files
Cesarus85--ringfall-mjolnir-vr/tools/c2_live_debug.py
T

227 lines
8.9 KiB
Python

# -*- coding: utf-8 -*-
"""c2.dll live-debug harness (64-bit host / 32-bit WOW64 debuggee).
C2.Dll loads in-process in CL.Exe at its fixed preferred base 0x10700000
(verified: no relocation), so static VAs are literal live addresses.
Usage: import and call run(bp_vas, on_hit) where bp_vas is a list of live
c2 VAs and on_hit(ctx, mem, va, hitno) is a callback receiving the WOW64
register context and a memory reader. The harness plants INT3, and on each
hit reads the 32-bit context, invokes the callback, then transparently
restores the byte, single-steps over, and re-arms.
"""
import ctypes as C
from ctypes import wintypes as W
import json
k32 = C.WinDLL('kernel32', use_last_error=True)
DEBUG_ONLY_THIS_PROCESS = 0x00000002
CREATE_NEW_CONSOLE = 0x00000010
INFINITE = 0xFFFFFFFF
DBG_CONTINUE = 0x00010002
DBG_EXCEPTION_NOT_HANDLED = 0x80010001
EXCEPTION_DEBUG_EVENT = 1
CREATE_PROCESS_DEBUG_EVENT = 3
EXIT_PROCESS_DEBUG_EVENT = 5
LOAD_DLL_DEBUG_EVENT = 6
EXC_BREAKPOINT = 0x80000003
EXC_SINGLE_STEP = 0x80000004
LPVOID = C.c_void_p
ULPTR = C.c_uint64
C2_BASE = 0x10700000
# WOW64_CONTEXT (x86 CONTEXT) — flags
WOW64_CONTEXT_i386 = 0x00010000
CONTEXT_CONTROL = WOW64_CONTEXT_i386 | 0x1
CONTEXT_INTEGER = WOW64_CONTEXT_i386 | 0x2
CONTEXT_SEG = WOW64_CONTEXT_i386 | 0x4
CONTEXT_FLOAT = WOW64_CONTEXT_i386 | 0x8
CONTEXT_FULL = CONTEXT_CONTROL | CONTEXT_INTEGER | CONTEXT_SEG
CONTEXT_ALL = CONTEXT_FULL | CONTEXT_FLOAT
class FLOATING_SAVE_AREA(C.Structure):
_fields_ = [("ControlWord", W.DWORD), ("StatusWord", W.DWORD),
("TagWord", W.DWORD), ("ErrorOffset", W.DWORD),
("ErrorSelector", W.DWORD), ("DataOffset", W.DWORD),
("DataSelector", W.DWORD), ("RegisterArea", C.c_ubyte * 80),
("Cr0NpxState", W.DWORD)]
class WOW64_CONTEXT(C.Structure):
_fields_ = [("ContextFlags", W.DWORD),
("Dr0", W.DWORD), ("Dr1", W.DWORD), ("Dr2", W.DWORD),
("Dr3", W.DWORD), ("Dr6", W.DWORD), ("Dr7", W.DWORD),
("FloatSave", FLOATING_SAVE_AREA),
("SegGs", W.DWORD), ("SegFs", W.DWORD), ("SegEs", W.DWORD),
("SegDs", W.DWORD), ("Edi", W.DWORD), ("Esi", W.DWORD),
("Ebx", W.DWORD), ("Edx", W.DWORD), ("Ecx", W.DWORD),
("Eax", W.DWORD), ("Ebp", W.DWORD), ("Eip", W.DWORD),
("SegCs", W.DWORD), ("EFlags", W.DWORD), ("Esp", W.DWORD),
("SegSs", W.DWORD), ("ExtendedRegisters", C.c_ubyte * 512)]
class STARTUPINFOW(C.Structure):
_fields_ = [("cb", W.DWORD), ("r0", W.LPWSTR), ("r1", W.LPWSTR),
("r2", W.LPWSTR), ("dwX", W.DWORD), ("dwY", W.DWORD),
("dwXSize", W.DWORD), ("dwYSize", W.DWORD), ("dwXCC", W.DWORD),
("dwYCC", W.DWORD), ("dwFill", W.DWORD), ("dwFlags", W.DWORD),
("wShow", W.WORD), ("cbR2", W.WORD), ("lpR2", LPVOID),
("hIn", W.HANDLE), ("hOut", W.HANDLE), ("hErr", W.HANDLE)]
class PROCESS_INFORMATION(C.Structure):
_fields_ = [("hProcess", W.HANDLE), ("hThread", W.HANDLE),
("dwPid", W.DWORD), ("dwTid", W.DWORD)]
class EXCEPTION_RECORD(C.Structure):
_fields_ = [("Code", W.DWORD), ("Flags", W.DWORD), ("Rec", ULPTR),
("Addr", ULPTR), ("nParams", W.DWORD), ("Info", ULPTR * 15)]
class EXCEPTION_DEBUG_INFO(C.Structure):
_fields_ = [("ExceptionRecord", EXCEPTION_RECORD), ("dwFirstChance", W.DWORD)]
class CREATE_PROCESS_DEBUG_INFO(C.Structure):
_fields_ = [("hFile", W.HANDLE), ("hProcess", W.HANDLE), ("hThread", W.HANDLE),
("lpBaseOfImage", ULPTR), ("off", W.DWORD), ("sz", W.DWORD),
("tlb", ULPTR), ("start", ULPTR), ("name", ULPTR), ("uni", W.WORD)]
class LOAD_DLL_DEBUG_INFO(C.Structure):
_fields_ = [("hFile", W.HANDLE), ("lpBaseOfDll", ULPTR), ("off", W.DWORD),
("sz", W.DWORD), ("name", ULPTR), ("uni", W.WORD)]
class EXIT_PROCESS_DEBUG_INFO(C.Structure):
_fields_ = [("dwExitCode", W.DWORD)]
class DBG_U(C.Union):
_fields_ = [("Exception", EXCEPTION_DEBUG_INFO),
("CreateProcessInfo", CREATE_PROCESS_DEBUG_INFO),
("LoadDll", LOAD_DLL_DEBUG_INFO),
("ExitProcess", EXIT_PROCESS_DEBUG_INFO),
("_raw", C.c_ubyte * 160)]
class DEBUG_EVENT(C.Structure):
_fields_ = [("code", W.DWORD), ("pid", W.DWORD), ("tid", W.DWORD), ("u", DBG_U)]
k32.OpenThread.restype = W.HANDLE
k32.OpenThread.argtypes = [W.DWORD, W.BOOL, W.DWORD]
k32.Wow64GetThreadContext.argtypes = [W.HANDLE, C.POINTER(WOW64_CONTEXT)]
k32.Wow64SetThreadContext.argtypes = [W.HANDLE, C.POINTER(WOW64_CONTEXT)]
def _rpm(hproc, addr, n):
buf = (C.c_ubyte * n)()
got = C.c_size_t(0)
k32.ReadProcessMemory(hproc, LPVOID(addr), buf, n, C.byref(got))
return bytes(buf[:got.value])
PAGE_EXECUTE_READWRITE = 0x40
k32.VirtualProtectEx.argtypes = [W.HANDLE, LPVOID, C.c_size_t, W.DWORD, C.POINTER(W.DWORD)]
def _wpm(hproc, addr, data):
n = len(data)
buf = (C.c_ubyte * n).from_buffer_copy(data)
wrote = C.c_size_t(0)
old = W.DWORD(0)
k32.VirtualProtectEx(hproc, LPVOID(addr), n, PAGE_EXECUTE_READWRITE, C.byref(old))
ok = k32.WriteProcessMemory(hproc, LPVOID(addr), buf, n, C.byref(wrote))
k32.VirtualProtectEx(hproc, LPVOID(addr), n, old.value, C.byref(old))
k32.FlushInstructionCache(hproc, LPVOID(addr), n)
return ok and wrote.value == n
def run(bp_vas, on_hit, max_hits_per_bp=3, timeout_events=200000):
cmd = json.load(open('dbg_cmd.json'))
argv = cmd['argv']; root = cmd['root']
cmdline = ' '.join(('"%s"' % a if (' ' in a and not a.startswith('"')) else a)
for a in argv)
si = STARTUPINFOW(); si.cb = C.sizeof(si)
pi = PROCESS_INFORMATION()
if not k32.CreateProcessW(argv[0], C.create_unicode_buffer(cmdline), None, None,
False, DEBUG_ONLY_THIS_PROCESS | CREATE_NEW_CONSOLE,
None, root, C.byref(si), C.byref(pi)):
raise OSError('CreateProcessW %d' % C.get_last_error())
hproc = None
orig = {} # va -> original byte
planted = set()
pending = {} # tid -> va to re-arm after single-step
hits = {va: 0 for va in bp_vas}
de = DEBUG_EVENT()
n = 0
while n < timeout_events:
n += 1
if not k32.WaitForDebugEvent(C.byref(de), INFINITE):
break
status = DBG_CONTINUE
code = de.code
if code == CREATE_PROCESS_DEBUG_EVENT:
hproc = de.u.CreateProcessInfo.hProcess
elif code == LOAD_DLL_DEBUG_EVENT:
if de.u.LoadDll.lpBaseOfDll == C2_BASE:
for va in bp_vas:
orig[va] = _rpm(hproc, va, 1)
_wpm(hproc, va, b'\xcc')
planted.add(va)
elif code == EXCEPTION_DEBUG_EVENT:
exc = de.u.Exception.ExceptionRecord
addr = exc.Addr & 0xffffffff
if exc.Code == EXC_BREAKPOINT and addr in planted:
hth = k32.OpenThread(0x1FFFFF, False, de.tid)
ctx = WOW64_CONTEXT(); ctx.ContextFlags = CONTEXT_ALL
k32.Wow64GetThreadContext(hth, C.byref(ctx))
hits[addr] += 1
try:
on_hit(ctx, lambda a, l: _rpm(hproc, a, l), addr, hits[addr])
except Exception as e:
print('on_hit error:', e)
# restore original, back up EIP, single-step to re-arm
_wpm(hproc, addr, orig[addr])
ctx.Eip = addr
if hits[addr] < max_hits_per_bp:
ctx.EFlags |= 0x100 # TF
pending[de.tid] = addr
ctx.ContextFlags = CONTEXT_ALL
k32.Wow64SetThreadContext(hth, C.byref(ctx))
k32.CloseHandle(hth)
elif exc.Code == EXC_SINGLE_STEP and de.tid in pending:
va = pending.pop(de.tid)
_wpm(hproc, va, b'\xcc') # re-arm
elif exc.Code in (EXC_BREAKPOINT, EXC_SINGLE_STEP):
pass # loader breakpoint etc.
else:
status = DBG_EXCEPTION_NOT_HANDLED
elif code == EXIT_PROCESS_DEBUG_EVENT:
k32.ContinueDebugEvent(de.pid, de.tid, status)
if de.pid == pi.dwPid:
break
continue
k32.ContinueDebugEvent(de.pid, de.tid, status)
return hits
if __name__ == '__main__':
INVOKE = 0x107b6938 # _InvokeCompilerPass@12 (backend entry cl.exe calls)
def dump(ctx, mem, va, hitno):
# args are at [esp+4],[esp+8],[esp+0xc] (stdcall @12 = 3 dwords)
stk = mem(ctx.Esp, 16)
a = struct_unpack(stk)
print('HIT InvokeCompilerPass #%d eip=%#x esp=%#x ret=%#x args=(%#x,%#x,%#x)'
% (hitno, ctx.Eip, ctx.Esp, a[0], a[1], a[2], a[3]))
h = run([INVOKE], dump, max_hits_per_bp=5)
print('hit counts:', {hex(k): v for k, v in h.items()})
def struct_unpack(b):
import struct
return struct.unpack_from('<4I', b + b'\0' * 16)