WIN32汇编语言教程:第13章 进程控制 · 13.3 进程调试(7)
表13.1 发生不同调试事件时u字段中的结构
DwDebugEventCode | u字段中的结构 |
CREATE_PROCESS_DEBUG_EVENT | CREATE_PROCESS_DEBUG_INFO |
EXIT_PROCESS_DEBUG_EVENT | EXIT_PROCESS_DEBUG_INFO |
CREATE_THREAD_DEBUG_EVENT | CREATE_THREAD_DEBUG_INFO |
EXIT_THREAD_DEBUG_EVENT | EXIT_THREAD_DEBUG_EVENT |
LOAD_DLL_DEBUG_EVENT | LOAD_DLL_DEBUG_INFO |
UNLOAD_DLL_DEBUG_EVENT | UNLOAD_DLL_DEBUG_INFO |
EXCEPTION_DEBUG_EVENT | EXCEPTION_DEBUG_INFO |
OUTPUT_DEBUG_STRING_EVENT | OUTPUT_DEBUG_STRING_INFO |
RIP_EVENT | RIP_INFO |
当程序使用WaitForDebugEvent函数获取了一个事件并进行处理以后,被调试进程还处在挂起状态,调试事件处理完毕后让它恢复运行是调试器的责任,恢复被调试进程的运行可以使用ContinueDebugEvent函数:
invoke ContinueDebugEvent,dwProcessId,dwThreadId,dwContinueStatus
其中,dwProcessId和dwThreadId参数指定被恢复运行的进程ID和线程ID,在这里可以直接使用在DEBUG_EVENT结构中返回的同名字段。dwContinueStatus参数指定恢复运行的方式,一般指定为DBG_CONTINUE。
总之,使用下面的循环结构进行调试过程:
.while TRUE
invoke WaitForDebugEvent,addr DebugEvent,INFINITE
.break .if DebugEvent.dwDebugEventCode==EXIT_PROCESS_DEBUG_EVENT
.if DebugEvent.dwDebugEventCode==XXXXXXX
<处理调试事件1>
.elseif DebugEvent.dwDebugEventCode==YYYYYYY
<处理调试事件2>
...
.endif
invoke ContinueDebugEvent,DebugEvent.dwProcessId,\
DebugEvent.dwThreadId, DBG_CONTINUE
.endw
循环中首先用WaitForDebugEvent函数获取调试事件,然后用一个分支语句检测事件类型,当发现事件代码为EXIT_PROCESS_DEBUG_EVENT(进程退出)时,用 .break语句结束循环;在其他情况下,程序根据不同的事件码进行不同的处理。
例子程序中处理了CREATE_PROCESS_DEBUG_EVENT和EXCEPTION_DEBUG_EVENT事件。当发生CREATE_PROCESS_DEBUG_EVENT事件时,表示建立了被调试进程,这时例子程序在目标进程的入口代码处(地址为00405120h,原指令为pushad,机器码为60h)写入一个0cch(int 3的机器码),当目标进程开始执行时,Windows就会以EXCEPTION_DEBUG_EVENT(异常事件)通知程序。
由于已经在静态反汇编分析中知道了该地址原来的内容,所以将int 3指令写入之前不必使用ReadProcessMemory函数先保存原来的指令;如果不知道被覆盖的指令码是什么,那么在写入int 3之前就必须保存原来的指令码,因为以后还要将它恢复回去。
接下来就是等待这个int 3发生了,也就是EXCEPTION_DEBUG_EVENT事件的发生,对于EXCEPTION_DEBUG_EVENT事件,u字段被定义为EXCEPTION_DEBUG_INFO结构:
EXCEPTION_DEBUG_INFO STRUCT
pExceptionRecord EXCEPTION_RECORD <?,?,?,?,?,EXCEPTION_MAXIMUM_PARAMETERS
dup(?)>
dwFirstChance DWORD ?
EXCEPTION_DEBUG_INFO ENDS
结构中的pExceptionRecord字段又被定义为一个EXCEPTION_RECORD结构,在这个结构中,我们需要的信息才浮出水面:
EXCEPTION_RECORD STRUCT
ExceptionCode DWORD ? ;异常事件码
ExceptionFlags DWORD ? ;标志
pExceptionRecord DWORD ?
ExceptionAddress DWORD ?
NumberParameters DWORD ?
ExceptionInformation DWORD EXCEPTION_MAXIMUM_PARAMETERS dup(?)
EXCEPTION_RECORD ENDS
读者不要被嵌套得这么深的结构吓倒了,实际上对它们的引用很简单,只要使用“结构1.结构2.结构3.结构4.字段n”的格式就可以了。EXCEPTION_RECORD结构的ExceptionCode字段定义了异常事件的类型,异常事件的类型有很多,我们关心的是EXCEPTION_BREAKPOINT(断点中断)和EXCEPTION_SINGLE_STEP(单步中断)两种异常。好了,现在程序可以在异常事件的处理中再设置一个分支,并根据断点中断和单步中断两种情况做不同的处理。
在分析例子程序对这两种异常事件的处理代码之前,还需要了解一个新概念,即线程环境。
3. 线程环境
在第12章中已经提到过,Windows为不同的线程循环分配时间片,当挂起一个线程的时候,为了以后能够将它恢复执行,系统必须首先将线程的运行环境保存下来,当线程在下一个时间片恢复执行时,将运行环境恢复回去,线程就不会感觉到自己被打断过,这就像甲外出的时候把办公室交给乙管,不管乙把办公室搞成什么样子,只要在甲回来之前把所有东西恢复原状,甲就不会意识到甲出去的时候办公室被挪做它用了。
线程环境就是这个道理,Windows中将线程环境称为“Thread Context”(注意:没有进程Context,因为进程是不活动的),对一个线程来说,只要所有的寄存器没有改变,环境就没有改变,所以线程环境实际上就是寄存器的状态,它可以用一个CONTEXT结构来表示。
结构定义为:
CONTEXT STRUCT
ContextFlags DWORD ?
iDr0 DWORD ?
iDr1 DWORD ?
iDr2 DWORD ?
iDr3 DWORD ?
iDr6 DWORD ?
iDr7 DWORD ?
FloatSave FLOATING_SAVE_AREA <>
regGs DWORD ?
regFs DWORD ?
regEs DWORD ?
regDs DWORD ?
regEdi DWORD ?
regEsi DWORD ?
regEbx DWORD ?
regEdx DWORD ?
regEcx DWORD ?
regEax DWORD ?
regEbp DWORD ?
regEip DWORD ?
regCs DWORD ?
regFlag DWORD ?
regEsp DWORD ?
regSs DWORD ?
ExtendedRegisters db MAXIMUM_SUPPORTED_EXTENSION dup(?)
CONTEXT ENDS
结构中的字段包括80x86系列处理器中的全部寄存器,其中FloatSave字段用来保存浮点寄存器的内容,ExtendedRegisters字段用来保存扩展寄存器的内容(如MMX寄存器等),ContextFlags字段是供结构自己用的标志。
CONTEXT结构是Windows中惟一与硬件平台相关的结构,因为Windows设计成可以在不同的硬件平台上运行,当运行于MIPS,Alpha和PowerPC等平台上时,显然寄存器名称就和80x86系列的不同了,这时CONTEXT结构的定义也相应改变了。
在线程处于休眠状态的时候,它的线程环境由Windows保存,可以通过API获取它们并修改它们,当线程分配到时间片恢复运行时,Windows将修改过的线程环境恢复回去,而线程并不会意识到环境已经被修改了。用这种方法可以修改regEip字段,让某个线程转移到其他地方执行。
用于获取和重新设置线程环境的函数是GetThreadContext和GetThreadContext。有了这两个函数,调试手段中就多了一种利器,想一想,能够随意修改目标线程的内容,也可以随意修改它的运行环境,还有什么事情做不到呢?