承接上一篇的除錯事件迴圈,本文將探討如何「動態修改」被除錯程序:讀寫其虛擬記憶體、取得/設定執行緒上下文(暫存器值)。這是實現斷點、修補程式碼、動態分析的核心能力。
ReadProcessMemory proto hProcess:DWORD, lpBaseAddress:DWORD, lpBuffer:DWORD, nSize:DWORD, lpNumberOfBytesRead:DWORD
WriteProcessMemory proto hProcess:DWORD, lpBaseAddress:DWORD, lpBuffer:DWORD, nSize:DWORD, lpNumberOfBytesWritten:DWORD
hProcess:目標程序控制代碼(需 PROCESS_VM_READ/WRITE 權限,除錯時自動擁有)。lpBaseAddress:目標程序虛擬位址空間中的起始位址。lpBuffer:本地緩衝區位址。nSize:讀寫位元組數。lpNumberOfBytesRead/Written:實際傳輸量指標,可為 NULL。這兩個 API 讓除錯器能像操作自身記憶體一樣存取目標程序的位址空間,是掃描記憶體、修改資料、植入斷點指令(0xCC)的基礎。
GetThreadContext proto hThread:DWORD, lpContext:DWORD
SetThreadContext proto hThread:DWORD, lpContext:DWORD
當除錯事件發生時,Windows 暫停目標執行緒並保存其上下文(所有暫存器值)。透過這兩個 API,除錯器可:
CONTEXT STRUCT
ContextFlags dd ?
; CONTEXT_DEBUG_REGISTERS
iDr0 dd ?; iDr1 dd ?; iDr2 dd ?; iDr3 dd ?; iDr6 dd ?; iDr7 dd ?
; CONTEXT_FLOATING_POINT
FloatSave FLOATING_SAVE_AREA <>
; CONTEXT_SEGMENTS
regGs dd ?; regFs dd ?; regEs dd ?; regDs dd ?
; CONTEXT_INTEGER
regEdi dd ?; regEsi dd ?; regEbx dd ?; regEdx dd ?; regEcx dd ?; regEax dd ?
; CONTEXT_CONTROL
regEbp dd ?; regEip dd ?; regCs dd ?; regFlag dd ?; regEsp dd ?; regSs dd ?
; CONTEXT_EXTENDED_REGISTERS
ExtendedRegisters db MAXIMUM_SUPPORTED_EXTENSION dup(?)
CONTEXT ENDS
ContextFlags 決定要存取哪些暫存器組:
CONTEXT_CONTROL:EIP, EFLAGS, ESP, EBP, CS, SS(最常用)CONTEXT_INTEGER:通用整數暫存器CONTEXT_FULL:CONTEXT_CONTROL | CONTEXT_INTEGER | CONTEXT_SEGMENTSContextFlags,且 CONTEXT 結構必須 雙字對齊(建議前加 align dword),否則在 NT 核心下會失敗。目標程式 win.exe 在建立視窗前故意放置 jmp $ (EB FE) 無限迴圈。除錯器附著後,在 CREATE_PROCESS_DEBUG_EVENT 時讀取 EIP(指向該迴圈),用 WriteProcessMemory 將兩個位元組覆寫為 NOP (90 90),目標程式即可跳出迴圈正常顯示視窗。
.if DBEvent.dwDebugEventCode == CREATE_PROCESS_DEBUG_EVENT
mov context.ContextFlags, CONTEXT_CONTROL
invoke GetThreadContext, DBEvent.u.CreateProcessInfo.hThread, addr context
invoke WriteProcessMemory, DBEvent.u.CreateProcessInfo.hProcess, \
context.regEip, addr buffer, 2, NULL ; buffer = 9090h
invoke MessageBox, 0, addr TargetPatched, addr AppName, MB_OK+MB_ICONINFORMATION
不修改記憶體,改直接操作上下文:add context.regEip, 2 後呼叫 SetThreadContext,執行緒恢復時 EIP 已指向 jmp $ 之後的指令,效果相同但不動程式碼區。
.if DBEvent.dwDebugEventCode == CREATE_PROCESS_DEBUG_EVENT
mov context.ContextFlags, CONTEXT_CONTROL
invoke GetThreadContext, DBEvent.u.CreateProcessInfo.hThread, addr context
add context.regEip, 2
invoke SetThreadContext, DBEvent.u.CreateProcessInfo.hThread, addr context
invoke MessageBox, 0, addr LoopSkipped, addr AppName, MB_OK+MB_ICONINFORMATION
0xCC (INT 3),觸發 EXCEPTION_BREAKPOINT。需保存原位元組以便恢復。.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
include \masm32\include\comdlg32.inc
include \masm32\include\user32.inc
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\comdlg32.lib
includelib \masm32\lib\user32.lib
.data
AppName db "Win32 Debug Example no.2",0
ClassName db "SimpleWinClass",0
SearchFail db "Cannot find the target process",0
TargetPatched db "Target patched!",0
buffer dw 9090h ; 兩個 NOP
.data?
DBEvent DEBUG_EVENT <>
ProcessId dd ?
ThreadId dd ?
align dword
context CONTEXT <>
.code
start:
invoke FindWindow, addr ClassName, NULL
.if eax != NULL
invoke GetWindowThreadProcessId, eax, addr ProcessId
mov ThreadId, eax
invoke DebugActiveProcess, ProcessId
.while TRUE
invoke WaitForDebugEvent, addr DBEvent, INFINITE
.break .if DBEvent.dwDebugEventCode == EXIT_PROCESS_DEBUG_EVENT
.if DBEvent.dwDebugEventCode == CREATE_PROCESS_DEBUG_EVENT
mov context.ContextFlags, CONTEXT_CONTROL
invoke GetThreadContext, DBEvent.u.CreateProcessInfo.hThread, addr context
invoke WriteProcessMemory, DBEvent.u.CreateProcessInfo.hProcess, \
context.regEip, addr buffer, 2, NULL
invoke MessageBox, 0, addr TargetPatched, addr AppName, MB_OK+MB_ICONINFORMATION
.elseif DBEvent.dwDebugEventCode == EXCEPTION_DEBUG_EVENT
.if DBEvent.u.Exception.pExceptionRecord.ExceptionCode == EXCEPTION_BREAKPOINT
invoke ContinueDebugEvent, DBEvent.dwProcessId, DBEvent.dwThreadId, DBG_CONTINUE
.continue
.endif
.endif
invoke ContinueDebugEvent, DBEvent.dwProcessId, DBEvent.dwThreadId, DBG_EXCEPTION_NOT_HANDLED
.endw
.else
invoke MessageBox, 0, addr SearchFail, addr AppName, MB_OK+MB_ICONERROR
.endif
invoke ExitProcess, 0
end start
Read/WriteProcessMemory 與 Get/SetThreadContext 賦予除錯器「上帝視角」:任意讀寫虛擬記憶體、完全掌控暫存器狀態。配合除錯事件迴圈,即可實現斷點管理、程式碼修補、執行流程劫持、動態分析等核心功能。下一篇將實作單步追蹤與指令計數器。