WIN32汇编语言教程:第13章 进程控制 · 13.3 进程调试(7)

表13.1 发生不同调试事件时u字段中的结构

DwDebugEventCodeu字段中的结构
CREATE_PROCESS_DEBUG_EVENTCREATE_PROCESS_DEBUG_INFO
EXIT_PROCESS_DEBUG_EVENTEXIT_PROCESS_DEBUG_INFO
CREATE_THREAD_DEBUG_EVENTCREATE_THREAD_DEBUG_INFO
EXIT_THREAD_DEBUG_EVENTEXIT_THREAD_DEBUG_EVENT
LOAD_DLL_DEBUG_EVENTLOAD_DLL_DEBUG_INFO
UNLOAD_DLL_DEBUG_EVENTUNLOAD_DLL_DEBUG_INFO
EXCEPTION_DEBUG_EVENTEXCEPTION_DEBUG_INFO
OUTPUT_DEBUG_STRING_EVENTOUTPUT_DEBUG_STRING_INFO
RIP_EVENTRIP_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。有了这两个函数,调试手段中就多了一种利器,想一想,能够随意修改目标线程的内容,也可以随意修改它的运行环境,还有什么事情做不到呢?

上页:第13章 进程控制 · 13.3 进程调试(6) 下页:第13章 进程控制 · 13.3 进程调试(8)

第13章 进程控制

版权所有 © 中山市飞娥软件工作室 证书:粤ICP备09170368号