首页
社区
课程
招聘
[旧帖] [转帖]Win32调试API教程 0.00雪花
发表于: 2007-5-28 19:15 6455

[旧帖] [转帖]Win32调试API教程 0.00雪花

2007-5-28 19:15
6455
本贴原(共三篇)载于如下网址:
http://www.newasp.net/tech/program/21108.html
他让我对调试器的工作原理有了一定的了解,因觉得写得好,特整理在一块,便于自己查阅。如果版主觉得我转贴在此不合适,请给删去。
我们可以用本文所介绍的方法写软件写内存补丁或内存注册机等。


********************************************************************************
*                                   Win32调试API 第一部分                      *
********************************************************************************

在本教程中,我们将学习Win32提供给开发者的用于调试的原理. 在教程的结尾,我们将学习如何调试一个进程..

理论:
Win32有一些供程序员使用的API,它们提供相当于调试器的功能. 他们被称作Win32调试API(或原语).利用这些API,我们可以:

  加载一个程序或捆绑到一个正在运行的程序上以供调试 
  获得被调试的程序的低层信息,例如进程ID,进入地址,映像基址等. 
  当发生与调试有关的事件时被通知,例如进程/线程的开始/结束, DLL的加载/释放等. 
  修改被调试的进程或线程 
简而言之,我们可以用这些API写一个简单的调试器.由于这个题目有些过大,我把它分为几部分,而本教程就是它的第一部分.在本教程中,我将讲解一些基本概念及Win32调试API的大致框架.
使用Win32调试API的步骤如下:

创建一个进程或捆绑到一个运行中的进程上. 这是使用Win32调试API的第一步.由于我们的程序要扮演调试器的角色,我们要找一个供调试的程序.一个被调试的程序被称为debuggee.可以通过以下两种方式获得debuggee: 
通过CreateProcess创建debuggee进程.为了创建被调试的进程,必须指定DEBUG_PROCESS标志.这一标志告诉Windows我们要调试该进程. 当debuggee中发生重要的与调试有关的事件(调试事件)时,Windows 会向我们的程序发送通知.debuggee会立即挂起以等待我们的程序准备好.如果debuggee还创建了子进程,Windows还会为每个子进程中的调试事件向我们的程序发送通知.这一特性通常是不必要的.我们可以通过指定DEBUG_ONLY_THIS_PROCESS与 DEBUG_PROCESS的组合标志来禁止它. 
我们也可以用 DebugActiveProcess标志捆绑到一个运行中的进程上. 
等待调试事件. 在获得了一个debuggee进程后,debuggee的主线程被挂起,这种状况将持续到我们的程序调用WaitForDebugEvent为止.这个函数和其他的WaitForXXX函数相似,比如说,它阻塞调用线程直到等待的事件发生.对这个函数来说, 它等待由Windows发送的调试事件.下面是它的定义: 
WaitForDebugEvent proto lpDebugEvent:DWORD, dwMilliseconds:DWORD

lpDebugEvent is the address of a DEBUG_EVENT这个结构将被填入关于debuggee中发生的调试事件的信息.

dwMilliseconds 该函数等待调试事件的时间,以毫秒为单位.如果这段时间没有调试事件发生, WaitForDebugEvent返回调用者.另一方面,如果将该参数指定为 INFINITE 常数,函数将一直等待直到调试事件发生.

现在我们看一下DEBUG_EVENT 结构.

DEBUG_EVENT STRUCT 
   dwDebugEventCode dd ? 
   dwProcessId dd ? 
   dwThreadId dd ? 
   u DEBUGSTRUCT <> 
DEBUG_EVENT ENDS 

dwDebugEventCode 该值指定了等待发生的调试事件的类型.因为有很多种类型的事件发生,我们的程序要检查该值,知道要发生事件的类型并做出响应. 该值可能的取值如下:

        取值                         含义 
CREATE_PROCESS_DEBUG_EVENT      进程被创建.当debuggee进程刚被创建(还未运行) 或我们的程序刚以
                                DebugActiveProcess被捆绑到一个运行中的进程时事件发生. 这是我
                                们的程序应该获得的第一个事件. 
EXIT_PROCESS_DEBUG_EVENT        进程退出. 
CREATE_THEAD_DEBUG_EVENT        当一个新线程在deuggee进程中创建或我们的程序首次捆绑到运行中的
                                进程时事件发生.要注意的是当debugge的主线程被创建时不会收到该
                                通知.  
EXIT_THREAD_DEBUG_EVENT         debuggee中的线程退出时事件发生.debugee的主线程退出时不会收到
                                该通知.我们可以认为debuggee的主线程与debugge进程是同义词. 因
                                此, 当我们的程序看到CREATE_PROCESS_DEBUG_EVENT标志时,对主线程
                                来说,就是CREATE_THREAD_DEBUG_EVENT标志. 
LOAD_DLL_DEBUG_EVENT            debuggee装入一个DLL.当PE装载器第一次分解指向DLL的链接时,我们
                                将收到这一事件. (当调用CreateProcess装入 debuggee时)并且当
                                debuggee调用LoadLibrary时也会发生. 
UNLOAD_DLL_DEBUG_EVENT          一个DLL从debuggee中卸载时事件发生.  
EXCEPTION_DEBUG_EVENT           在debuggee中发生异常时事件发生. 注意: 该事件仅在debuggee开始
                                它的第一条指令之前发生一次.异常实际上是一个调试中断(int 3h).
                                如果想恢复debuggee事,以 DBG_CONTINUE 标志调用ContinueDebugEvent
                                函数. 不要使用DBG_EXCEPTION_NOT_HANDLED 标志否则debuggee会在NT
                                下拒绝运行(Win98下运行得很好). 
OUTPUT_DEBUG_STRING_EVENT       当debuggee调用DebugOutputString函数向我们的程序发送消息字符串时
                                该事件发生.  
RIP_EVENT                       系统调试发生错误 

dwProcessId 和dwThreadId发生调试事件的进程和线程Id.我们可以用这些值作为我们感兴趣的进程或线程的标志符.记住如果我们使用CreateProcess来装载debuggee,我们仍可在PROCESS_INFO结构中获得debuggee的进程和线程.我们可以用这些值来区别调试事件是发生在debuggee中还是它的子进程中(当没有指定 DEBUG_ONLY_THIS_PROCESS 标志时).

u 是一个联合,包含了调试事件的更多信息.根据上面dwDebugEventCode的不同,它可以是以下结构:

   dwDebugEventCode              u的解释 
CREATE_PROCESS_DEBUG_EVENT     名为CreateProcessInfo的CREATE_PROCESS_DEBUG_INFO结构 
EXIT_PROCESS_DEBUG_EVENT       名为ExitProcess的EXIT_PROCESS_DEBUG_INFO结构 
CREATE_THREAD_DEBUG_EVENT      名为CreateThread的CREATE_THREAD_DEBUG_INFO结构 
EXIT_THREAD_DEBUG_EVENT        名为ExitThread的EXIT_THREAD_DEBUG_EVENT 结构 
LOAD_DLL_DEBUG_EVENT           名为LoadDll的LOAD_DLL_DEBUG_INFO 结构 
UNLOAD_DLL_DEBUG_EVENT         名为UnloadDll的UNLOAD_DLL_DEBUG_INFO结构 
EXCEPTION_DEBUG_EVENT          名为Exception的EXCEPTION_DEBUG_INFO结构 
OUTPUT_DEBUG_STRING_EVENT      名为DebugString的OUTPUT_DEBUG_STRING_INFO 结构 
RIP_EVENT                      名为RipInfo的RIP_INFO 结构 

我不会在这一个教程里讲所有这些结构的细节,这里只详细讲一下CREATE_PROCESS_DEBUG_INFO 结构. 
假设我们的程序调用了WaitForDebugEvent函数并返回,我们要做的第一件事就是检查dwDebugEventCode中的值来看debuggee进程中发生了那种类型的调试事件.比如说,如果dwDebugEventCode的值为 CREATE_PROCESS_DEBUG_EVENT,就可认为u的成员为CreateProcessInfo 并用u.CreateProcessInfo来访问. 

在我们的程序中做对调试事件的响应. 当WaitForDebugEvent 返回时,这意味着在debuggee进程中发生了调试事件或者发生了超时.所以我们的程序要检查dwDebugEventCode 来作出适当的反应.这里有些象处理Windows消息:由用户来选择和忽略消息. 
继续运行debuggee. 当调试事件发生时, Windows挂起了debuggee,所以当我们处理完调试事件,还要让debuggee继续运行.调用ContinueDebugEvent 函数来完成这一过程. 
ContinueDebugEvent proto dwProcessId:DWORD, dwThreadId:DWORD, dwContinueStatus:DWORD

该函数恢复由于调试事件而挂起的线程.
dwProcessId和dwThreadId是要恢复的线程的进程ID和线程ID,通常这两个值从 DEBUG_EVENT结构的dwProcessId 和dwThreadId成员获得.
dwContinueStatus显示了如何继续报告调试事件的线程.可能的取值有两个: DBG_CONTINUE 和DBG_EXCEPTION_NOT_HANDLED. 对大多数调试事件,这两个值都一样:恢复线程.唯一的例外是EXCEPTION_DEBUG_EVENT,如果线程报告发生了一个异常调试事件,这意味着在debuggee的线程中发生了一个异常.如果指定了DBG_CONTINUE,线程将忽略它自己的异常处理部分并继续执行.在这种情况下,我们的程序必须在以DBG_CONTINUE恢复线程之前检查并处理异常,否则异常将生生不息地不断发生....如果我们指定了 DBG_EXCEPTION_NOT_HANDLED值,就是告诉Windows我们的程序并不处理异常:Windows将使用debuggee的默认异常处理函数来处理异常. 
总而言之,如果我们的程序没有考虑异常,而调试事件又指向debuggee进程中的一个异常的话,就应调用含DBG_CONTINUE标志的ContinueDebugEvent函数.否则,我们的程序就必须以DBG_EXCEPTION_NOT_HANDLED调用 ContinueDebugEvent.但在下面这种情况下必须使用DBG_CONTINUE标志:第一个在ExceptionCode成员中有值EXCEPTION_BREAKPOINT的 EXCEPTION_DEBUG_EVENT事件.当debuggee开始执行它的第一条指令时,我们的函数将接受到异常调试事件.它事实上是一个调试中断(int 3h).如果我们以DBG_EXCEPTION_NOT_HANDLED调用ContinueDebugEvent 来响应调试事件, Windows NT会拒绝执行debuggee(因为它没有异常处理).所以在这种情况下,要用DBG_CONTINUE标志告诉Windows我们希望该线程继续执行.

继续上面的步骤循环直到debuggee进程退出. 我们的程序必须在一个很象消息循环的无限循环中直到debuggee结束.该循环大体如下: 
.while TRUE
    invoke WaitForDebugEvent, addr DebugEvent, INFINITE
   .break .if DebugEvent.dwDebugEventCode==EXIT_PROCESS_DEBUG_EVENT
   <调试事件处理>
   invoke ContinueDebugEvent, DebugEvent.dwProcessId, DebugEvent.dwThreadId, DBG_EXCEPTION_NOT_HANDLED 
.endw 

就是说,当开始调试程序时,我们的程序不能和debuggee分开直到它结束.

我们再来总结一下这些步骤:

创建一个进程或捆绑我们的程序到运行中的进程上. 
等待调试事件 
响应调试事件. 
继续执行debuggee. 
继续这一无尽循环直到debuggee进程结束 
例子:
这个例子调试一个win32程序并显示诸如进程句柄,进程Id,映象基址等.

.386 
.model flat,stdcall 
option casemap:none 
include masm32includewindows.inc 
include masm32includekernel32.inc 
include masm32includecomdlg32.inc 
include masm32includeuser32.inc 
includelib masm32libkernel32.lib 
includelib masm32libcomdlg32.lib 
includelib masm32libuser32.lib 
.data 
AppName db "Win32 Debug Example no.1",0 
ofn OPENFILENAME <> 
FilterString db "Executable Files",0,"*.exe",0 
             db "All Files",0,"*.*",0,0 
ExitProc db "The debuggee exits",0 
NewThread db "A new thread is created",0 
EndThread db "A thread is destroyed",0 
ProcessInfo db "File Handle: %lx ",0dh,0Ah 
            db "Process Handle: %lx",0Dh,0Ah 
            db "Thread Handle: %lx",0Dh,0Ah 
            db "Image Base: %lx",0Dh,0Ah 
            db "Start Address: %lx",0 
.data? 
buffer db 512 dup(?) 
startinfo STARTUPINFO <> 
pi PROCESS_INFORMATION <> 
DBEvent DEBUG_EVENT <> 
.code 
start: 
mov ofn.lStructSize,sizeof ofn 
mov ofn.lpstrFilter, offset FilterString 
mov ofn.lpstrFile, offset buffer 
mov ofn.nMaxFile,512 
mov ofn.Flags, OFN_FILEMUSTEXIST or OFN_PATHMUSTEXIST or OFN_LONGNAMES or OFN_EXPLORER or OFN_HIDEREADONLY 
invoke GetOpenFileName, ADDR ofn 
.if eax==TRUE 
invoke GetStartupInfo,addr startinfo 
invoke CreateProcess, addr buffer, NULL, NULL,NULL, FALSE, DEBUG_PROCESS+ DEBUG_ONLY_THIS_PROCESS, NULL, NULL, addr startinfo,addrpi
.whileTRUE 
   invoke WaitForDebugEvent, addr DBEvent, INFINITE 
   .if DBEvent.dwDebugEventCode==EXIT_PROCESS_DEBUG_EVENT 
       invoke MessageBox, 0, addr ExitProc, addr AppName, MB_OK+MB_ICONINFORMATION 
       .break 
   .elseif DBEvent.dwDebugEventCode==CREATE_PROCESS_DEBUG_EVENT 
       invoke wsprintf, addr buffer, addr ProcessInfo, DBEvent.u.CreateProcessInfo.hFile, DBEvent.u.CreateProcessInfo.hProcess, DBEvent.u.CreateProcessInfo.hThread, DBEvent.u.CreateProcessInfo.lpBaseOfImage, DBEvent.u.CreateProcessInfo.lpStartAddress 
       invoke MessageBox,0, addr buffer, 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 
   .elseif DBEvent.dwDebugEventCode==CREATE_THREAD_DEBUG_EVENT 
       invoke MessageBox,0, addr NewThread, addr AppName, MB_OK+MB_ICONINFORMATION 
   .elseif DBEvent.dwDebugEventCode==EXIT_THREAD_DEBUG_EVENT 
       invoke MessageBox,0, addr EndThread, addr AppName, MB_OK+MB_ICONINFORMATION 
   .endif 
   invoke ContinueDebugEvent, DBEvent.dwProcessId, DBEvent.dwThreadId, DBG_EXCEPTION_NOT_HANDLED 
.endw 
invoke CloseHandle,pi.hProcess 
invoke CloseHandle,pi.hThread 
.endif 
invoke ExitProcess, 0 
end start 

分析:
程序首先填充OPENFILENAME结构,调用GetOpenFileName让用户选择要调试的程序.

invoke GetStartupInfo,addr startinfo 
invoke CreateProcess, addr buffer, NULL, NULL, NULL, FALSE, DEBUG_PROCESS+ DEBUG_ONLY_THIS_PROCESS, NULL, NULL, addr startinfo, addr pi 

当接收用户选择后,调用CreateProcess装载程序.并调用GetStartupInfo以默认值填充STARTUPINFO结构.注意我们将DEBUG_PROCESS标志与DEBUG_ONLY_THIS_PROCESS标志组合来仅调试这个程序,不包括子进程.

.while TRUE 
   invoke WaitForDebugEvent, addr DBEvent, INFINITE 

在debuggee被装入后,我们调用WaitForDebugEvent进入无尽的调试循环,WaitForDebugEvent在debuggee中发生调试事件时返回,因为我们指定了INFINITE作为第二个参数.当调试事件发生时, WaitForDebugEvent 返回并填充DBEvent结构.

   .if DBEvent.dwDebugEventCode==EXIT_PROCESS_DEBUG_EVENT 
       invoke MessageBox, 0, addr ExitProc, addr AppName, MB_OK+MB_ICONINFORMATION 
       .break 

我们要先检查dwDebugEventCode的值, 如果是EXIT_PROCESS_DEBUG_EVENT,用一个消息框显示"The debuggee exits" 并退出调试循环.

   .elseif DBEvent.dwDebugEventCode==CREATE_PROCESS_DEBUG_EVENT 
       invoke wsprintf, addr buffer, addr ProcessInfo, DBEvent.u.CreateProcessInfo.hFile, DBEvent.u.CreateProcessInfo.hProcess, DBEvent.u.CreateProcessInfo.hThread, DBEvent.u.CreateProcessInfo.lpBaseOfImage, DBEvent.u.CreateProcessInfo.lpStartAddress 
       invoke MessageBox,0, addr buffer, addr AppName, MB_OK+MB_ICONINFORMATION    

如果dwDebugEventCode 的值为CREATE_PROCESS_DEBUG_EVENT,我们就在消息框中显示一些感兴趣的底层信息.这些信息从u.CreateProcessInfo获得. CreateProcessInfo是一个CREATE_PROCESS_DEBUG_INFO类型的结构体.你可以查阅Win32 API获得它的更多信息e. 

   .elseif DBEvent.dwDebugEventCode==EXCEPTION_DEBUG_EVENT 
       .if DBEvent.u.Exception.pExceptionRecord.ExceptionCode==EXCEPTION_BREAKPOINT 
          invoke ContinueDebugEvent, DBEvent.dwProcessId, DBEvent.dwThreadId, DBG_CONTINUE 
         .continue 
       .endif 

如果dwDebugEventCode 的值为EXCEPTION_DEBUG_EVENT,我们就要更进一步检查异常类型.它是一大堆的结构嵌套,但我们可以从ExceptionCode成员获得异常类型.如果ExceptionCode的值为 EXCEPTION_BREAKPOINT并且是第一次发生(或者我们已知道deuggee中没有int 3h指令),我们可以安全地假定在debuggee要执行第一条指令时发生这一异常.在我们完成这些处理后,就可以用 DBG_CONTINUE调用ContinueDebugEvent来继续执行debuggee.接着我们继续等待下一个调试事件的发生.

   .elseif DBEvent.dwDebugEventCode==CREATE_THREAD_DEBUG_EVENT 
       invoke MessageBox,0, addr NewThread, addr AppName, MB_OK+MB_ICONINFORMATION 
   .elseif DBEvent.dwDebugEventCode==EXIT_THREAD_DEBUG_EVENT 
       invoke MessageBox,0, addr EndThread, addr AppName, MB_OK+MB_ICONINFORMATION 
   .endif 

如果dwDebugEventCode 的值为CREATE_THREAD_DEBUG_EVENT或EXIT_THREAD_DEBUG_EVENT, 我们的程序显示一个消息框.

   invoke ContinueDebugEvent, DBEvent.dwProcessId, DBEvent.dwThreadId, DBG_EXCEPTION_NOT_HANDLED 
.endw 

除了上面讨论过的 EXCEPTION_DEBUG_EVENT,用DBG_EXCEPTION_NOT_HANDLED标志调用ContinueDebugEvent函数恢复debuggee的执行.

invoke CloseHandle,pi.hProcess 
invoke CloseHandle,pi.hThread 

当debuggee结束时,我们就跳出了调试循环,这时要关闭 debuggee的线程和进程句柄.关闭这些句柄并不意味着要关闭这些进程和线程.只是说不再用这些句柄罢了

********************************************************************************
*                          Win32调试API 第二部分                               *
********************************************************************************

我们继续Win32调试API的话题。在本章中,我们将要学习如何修改被调试程序。

理论:
在前面一章中,我们学会了如何装载被调试的进程以及如何处理进程中发生的事件。为了有实际用途,我们的程序应具有修改被调试程序的能力。有好几个API函数用于这一目的。

ReadProcessMemory该函数允许你去读指定的进程的内存。函数原型如下: 
ReadProcessMemory proto hProcess:DWORD, lpBaseAddress:DWORD, lpBuffer:DWORD, nSize:DWORD, lpNumberOfBytesRead:DWORD

hProcess 待读进程的句柄.
lpBaseAddress 目标进程中待读内存起始地址。例如,如果你想要读目标 进程中从地址401000h开始的4个字节,该参数值应置为401000h。
lpBuffer 接收缓冲区地址
nSize 想要读的字节数。
lpNumberOfBytesRead 记录实际读取的字节数的变量地址。如果对这个值 不关心,填入NULL即可。

WriteProcessMemory 是对应于ReadProcessMemory的函数,通过它 可以写目标进程的内存。其参数和ReadProcessMemory 相同。 
理解接下去的两个函数需要一些进程上下文的有关背景知识。在象Windows这样的 多任务操作系统中,同一时间里可能运行着几个程序。Windows分配给每个线程一个 时间片,当时间片结束后,Windows将冻结当前线程并切换到下一具有最高优先级的 线程。在切换之前,Windows将保存当前进程的寄存器的 内容,这样当在该线程再 次恢复运行时,Windows可以恢复最近一次线程运行的*环境*。保存的寄存器内容总 称为进程上下文。
现在回到我们的主题。当一个调试事件发生时,Windows暂停被调试进程,并保存其 进程上下文。由于进程被暂停运行,我们可以确信其进程上下文内容将保持不变。 可以用GetThreadContext来获取进程上下文内容,并且也可以用SetThreadContext 来修改进程上下文内容。
这两个函数威力非凡。有了他们,对被调试进程你就具有象VxD的能力: 如改变其寄 存器内容,而在被调试程序恢复运行前,这些值将会写回寄存器中。在进程上下文中 所做的任何改动,将都会反映到被调试程序中。想象一下: 甚至可以改变eip寄存器 的内容,这样你可以让程序运行到你想要的任何地方! 在正常情况下是不可能做到这 一点的。

GetThreadContext proto hThread:DWORD, lpContext:DWORD 

hThread 你想要获得上下文的线程句柄
lpContext 函数成功返回时用来保存上下文内容的结构指针。

SetThreadContext 参数相同。让我们来看看上下文的结构: 

CONTEXT STRUCT 

ContextFlags dd ? 
;----------------------------------------------------------------------------------------------------------
;当ContextFlags包含CONTEXT_DEBUG_REGISTERS,返回本部分 
;-----------------------------------------------------------------------------------------------------------
iDr0 dd ? 
iDr1 dd ? 
iDr2 dd ? 
iDr3 dd ? 
iDr6 dd ? 
iDr7 dd ? 

;----------------------------------------------------------------------------------------------------------
;当ContextFlags包含CONTEXT_FLOATING_POINT,返回本部分 
;-----------------------------------------------------------------------------------------------------------

FloatSave FLOATING_SAVE_AREA <> 

;----------------------------------------------------------------------------------------------------------
;当ContextFlags包含CONTEXT_SEGMENTS,返回本部分 
;----------------------------------------------------------------------------------------------------------- 
regGs dd ? 
regFs dd ? 
regEs dd ? 
regDs dd ? 

;----------------------------------------------------------------------------------------------------------
;当ContextFlags包含CONTEXT_INTEGER,返回本部分 
;----------------------------------------------------------------------------------------------------------- 
regEdi dd ? 
regEsi dd ? 
regEbx dd ? 
regEdx dd ? 
regEcx dd ? 
regEax dd ? 

;----------------------------------------------------------------------------------------------------------
;当ContextFlags包含CONTEXT_CONTROL,返回本部分 
;----------------------------------------------------------------------------------------------------------- 
regEbp dd ? 
regEip dd ? 
regCs dd ? 
regFlag dd ? 
regEsp dd ? 
regSs dd ? 

;----------------------------------------------------------------------------------------------------------
;当ContextFlags包含CONTEXT_EXTENDED_REGISTERS,返回本部分 
;----------------------------------------------------------------------------------------------------------- 
ExtendedRegisters db MAXIMUM_SUPPORTED_EXTENSION dup(?) CONTEXT ENDS

可以看出,该结构中的成员是对实际处理器的寄存器的模仿。在使用该结构之前 要在ContextFlags 中指定哪些寄存器组用来读写。如要访问所有的寄存器, 你可以置ContextFlags 为CONTEXT_FULL 。或者只访问regEbp, regEip, regCs, regFlag, regEsp 或 regSs, 应置ContextFlags 为 CONTEXT_CONTROL 。

在使用结构CONTEXT 时还应记住: 它必须是双字对齐的,否则在NT下将得 到奇怪的结果。可以在定义前加上"align dword"。例如: 

align dword
MyContext CONTEXT <>

例:
第一个例子演示DebugActiveProcess的使用。首先,需要在Windows显示在屏幕上以前运行一个待调试程序win.exe,该程序将处于无限循环运行状态中。然后你运行例子程序,它将把自己与win.exe连接起来,并且修改win.exe的代码,这样win.exe将退出无限循环状态而显示自己的窗口。

.386 
.model flat,stdcall 
option casemap:none 
include masm32includewindows.inc 
include masm32includekernel32.inc 
include masm32includecomdlg32.inc 
include masm32includeuser32.inc 
includelib masm32libkernel32.lib 
includelib masm32libcomdlg32.lib 
includelib masm32libuser32.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

.data? 
DBEvent DEBUG_EVENT <> 
ProcessId dd ? 
ThreadId dd ? 
align dword 
context CONTEXT <> 

.code 
start: 
invoke FindWindow, addr ClassName, NULL 
.if eax!=NULL 
    invoke GetWindowThreadProcessId, eaxaddr 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 ,addrbuffer,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 

;--------------------------------------------------------------------
; The partial source code of win.asm, our debuggee. It's actually
; the simple window example in tutorial 2 with an infinite loop inserted
; just before it enters the message loop.
;----------------------------------------------------------------------

......
mov wc.hIconSm,eax 
invoke LoadCursor,NULL,IDC_ARROW 
mov wc.hCursor,eax 
invoke RegisterClassEx, addr wc 
INVOKE CreateWindowEx,NULL,ADDR ClassName,ADDR AppName, WS_OVERLAPPEDWINDOW,CW_USEDEFAULT, CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL, hInst,NULL 
mov hwnd,eax 
jmp $ <---- Here's our infinite loop. It assembles to EB FE
invoke ShowWindow, hwnd,SW_SHOWNORMAL 
invoke UpdateWindow, hwnd 
.while TRUE 
   invoke GetMessage, ADDR msg,NULL,0,0 
   .break .if (!eax
   invoke TranslateMessage, ADDR msg 
   invoke DispatchMessage, ADDR msg 
.endw 
mov eax,msg.wParam 
ret 
WinMain endp 

分析:
invoke FindWindow, addr ClassName, NULL 

我们的程序需要用DebugActiveProcess将自己绑定到被调试程序,这需要知道被调试程序的进程Id。用GetWindowThreadProcessId 可以得到该Id,该函数需要窗口句柄作为参数,因此首先需要知道窗口句柄。
用FindWindow, 我们先指定窗口类的名称,返回的是该类创建的窗口句柄。如 果返回NULL,则表明当前没有该类的窗口。

.if eax!=NULL 
    invoke GetWindowThreadProcessId, eaxaddr ProcessId 
    mov ThreadId, eax 
    invoke DebugActiveProcess, ProcessId 

得到进程Id后,我们调用DebugActiveProcess。这样就进入等待调试事件的循环中。

       .if DBEvent.dwDebugEventCode==CREATE_PROCESS_DEBUG_EVENT 
          mov context.ContextFlags, CONTEXT_CONTROL 
          invoke GetThreadContext,DBEvent.u.CreateProcessInfo.hThread, addr context           

当得到 CREATE_PROCESS_DEBUG_INFO, 这意味着被调试进程已经被暂停运行了。 我们就可以对该进程动手术了。本例中,我们将用NOPs ( 90h 90h)覆盖被调试进程中的无 限循环指令(0EBh 0FEh) 。 
首先,需要得到该指令的地址。由于在我们的程序绑定到被调试程序时,被调试程序已经 处于循环语句中了,eip总是指向该指令。我们所要做的是得到eip的值。我们将使用 GetThreadContext来达到此目的。将上下文结构成员中ContextFlags设置 为CONTEXT_CONTROL ,这样告诉GetThreadContext我们需要它去填充上下 文结构的成员中的"控制"寄存器。 

          invoke WriteProcessMemory, DBEvent.u.CreateProcessInfo.hProcess, context.regEip ,addrbuffer,2,NULL

得到eip的值以后,可以调用WriteProcessMemory来用NOPs覆盖"jmp $" 指令,这样将使被调试程序退出无限循环。在向用户显示了信息之后,调用ContinueDebugEvent 来恢复被调试程序的运行。由于指令"jmp $"已被Nops覆盖,被调试程序将继续 显示窗口,并进入消息循环。证据是我们在屏幕上观察到了次窗口。 

另一个例子与此稍有不同,它是将被调试程序从无限循环中中断。 

.......
.......
.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 
.......
....... 

这里仍调用GetThreadContext来获取eip值,但没有去覆盖"jmp $" 指令,而是将 regEip加2,从而"跳过"该指令。结果是当被调试程序 重新获得控制权时,将恢复执行在"jmp $"后的指令。

现在你可以体会到Get/SetThreadContext的威力了。你也可以修改其他寄存器映象,这些值将直接反映到被调试程序中。甚至你可以把int 3h指令插入到被调试进程中。产生断点

********************************************************************************
*                           Win32调试API 第三部分                              *
********************************************************************************

在本章中,我们将继续探讨win32调试api。特别地,我们将学习如何去跟踪被调试程序..
理论:
如果你以前使用过调试器,那么你应对跟踪比较熟悉。当"跟踪"一个程序时,程序在每执行一条指令后将会停止,这使你有机会去检查寄存器/内存中的值。这种单步运行的官方定义为跟踪(tracing)。
单步运行的特色是由CPU本身提供的。标志寄存器的第8位称为陷阱标志trap flag。如果该位设置,则CPU运行于单步模式。CPU将在每条指令后产生一个debug异常。当debug 异常产生后,陷阱标志自动清除。利用win32调试api,我们也可以单步运行被调试程序。方法如下:

调用GetThreadContext, 指定 ContextFlags为CONTEXT_CONTROL, 来获得标志寄存器的值 
设置CONTEXT结构成员标志寄存器regFlag中的陷阱标志位 
调用 SetThreadContext 
等待调式事件。被调试程序将按单步模式执行,在每执行一条指令后,我们将得到调试 事件,u.Exception.pExceptionRecord.ExceptionCode值为EXCEPTION_SINGLE_STEP 
如果要跟踪下一条指令,需要再次设置陷阱标志位。 
例:
.386
.model flat,stdcall 
option casemap:none 
include masm32includewindows.inc 
include masm32includekernel32.inc 
include masm32includecomdlg32.inc 
include masm32includeuser32.inc 
includelib masm32libkernel32.lib 
includelib masm32libcomdlg32.lib 
includelib masm32libuser32.lib 

.data 
AppName db "Win32 Debug Example no.4",0 
ofn OPENFILENAME <> 
FilterString db "Executable Files",0,"*.exe",0 
             db "All Files",0,"*.*",0,0 
ExitProc db "The debuggee exits",0Dh,0Ah 
         db "Total Instructions executed : %lu",0 
TotalInstruction dd 0

.data? 
buffer db 512 dup(?) 
startinfo STARTUPINFO <> 
pi PROCESS_INFORMATION <> 
DBEvent DEBUG_EVENT <> 
context CONTEXT <> 

.code 
start: 
mov ofn.lStructSize,SIZEOF ofn 
mov ofn.lpstrFilter, OFFSET FilterString 
mov ofn.lpstrFile, OFFSET buffer 
mov ofn.nMaxFile,512 
mov ofn.Flags, OFN_FILEMUSTEXIST or OFN_PATHMUSTEXIST or OFN_LONGNAMES or OFN_EXPLORER or OFN_HIDEREADONLY 
invoke GetOpenFileName, ADDR ofn 
.if eax==TRUE 
    invoke GetStartupInfo,addr startinfo 
    invoke CreateProcess, addr buffer, NULL, NULL, NULL, FALSE, DEBUG_PROCESS+ DEBUG_ONLY_THIS_PROCESS, NULL, NULL, addr startinfo, addr pi 
    .while TRUE 
       invoke WaitForDebugEvent, addr DBEvent, INFINITE 
       .if DBEvent.dwDebugEventCode==EXIT_PROCESS_DEBUG_EVENT 
          invoke wsprintf, addr buffer, addr ExitProc, TotalInstruction 
          invoke MessageBox, 0, addr buffer, addr AppName, MB_OK+MB_ICONINFORMATION 
          .break 
       .elseif DBEvent.dwDebugEventCode==EXCEPTION_DEBUG_EVENT
           .if DBEvent.u.Exception.pExceptionRecord.ExceptionCode==EXCEPTION_BREAKPOINT 
             mov context.ContextFlags, CONTEXT_CONTROL 
             invoke GetThreadContext, pi.hThread, addr context 
             or context.regFlag,100h 
             invoke SetThreadContext,pi.hThread, addr context 
             invoke ContinueDebugEvent, DBEvent.dwProcessId, DBEvent.dwThreadId, DBG_CONTINUE 
             .continue 
          .elseif DBEvent.u.Exception.pExceptionRecord.ExceptionCode==EXCEPTION_SINGLE_STEP 
             inc TotalInstruction 
             invoke GetThreadContext,pi.hThread,addr context
       or context.regFlag,100h 
             invoke SetThreadContext,pi.hThread, addr context 
             invoke ContinueDebugEvent, DBEvent.dwProcessId, DBEvent.dwThreadId,DBG_CONTINUE 
             .continue 
          .endif 
       .endif 
       invoke ContinueDebugEvent, DBEvent.dwProcessId, DBEvent.dwThreadId, DBG_EXCEPTION_NOT_HANDLED 
    .endw 
.endif 
invoke CloseHandle,pi.hProcess 
invoke CloseHandle,pi.hThread 
invoke ExitProcess, 0 
end start 

分析:
该程序先显示一个打开文件对话框,当用户选择了一个可执行文件,它将单步执行该程序,并记录执行的指令数,直到被调试程序退出运行。 

       .elseif DBEvent.dwDebugEventCode==EXCEPTION_DEBUG_EVENT
           .if DBEvent.u.Exception.pExceptionRecord.ExceptionCode==EXCEPTION_BREAKPOINT 

利用该机会来设置被调试程序为单步运行模式。记住,在执行被调试程序的第一条指令前 windows将发送一个EXCEPTION_BREAKPOINT消息。 

             mov context.ContextFlags, CONTEXT_CONTROL 
             invoke GetThreadContext, pi.hThread, addr context 

调用GetThreadContext,以被调试程序的当前寄存器内容来填充CONTEXT 结构 特别地,我们需要标志寄存器的当前值。

             or context.regFlag,100h 

设置标志寄存器映象的陷阱位(第8位)

             invoke SetThreadContext,pi.hThread, addr context 
             invoke ContinueDebugEvent, DBEvent.dwProcessId, DBEvent.dwThreadId, DBG_CONTINUE 
             .continue 

然后调用SetThreadContext去覆盖CONTEXT的值。再以DBG_CONTINUE调用 ContinueDebugEvent 来恢复被调试程序的运行。

          .elseif DBEvent.u.Exception.pExceptionRecord.ExceptionCode==EXCEPTION_SINGLE_STEP 
             inc TotalInstruction 

当调试程序中一条指令执行后,我们将接收到EXCEPTION_DEBUG_EVENT的调试事件, 必须要检查u.Exception.pExceptionRecord.ExceptionCode的值。如果该值为 EXCEPTION_SINGLE_STEP,那么,该调试事件是单步运行模式造成的。在这种情况 下,TotalInstruction加一,因为我们确切地知道此时被调试程序执行了一条指令。

             invoke GetThreadContext,pi.hThread,addr context or context.regFlag,100h 
             invoke SetThreadContext,pi.hThread, addr context 
             invoke ContinueDebugEvent, DBEvent.dwProcessId, DBEvent.dwThreadId,DBG_CONTINUE 
             .continue 

由于陷阱标志在debug异常后自动清除了,如果我们需要继续保持单步运行模式,则必须设置陷阱标志位。
警告: 不要用本教程中的此例子来调试大程序: 跟踪是很慢的。你或许需要等待10 多分钟才能关闭被调试程序。

[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!

收藏
免费 0
支持
分享
最新回复 (8)
雪    币: 209
活跃值: (14)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
2
好文章呀!!!
感谢!感谢!
2007-5-28 20:12
0
雪    币: 370
活跃值: (15)
能力值: ( LV9,RANK:170 )
在线值:
发帖
回帖
粉丝
3
辛苦,值得学习,收藏
对于DEBUG原理只懂皮毛,谢谢
2007-5-28 22:53
0
雪    币: 1969
活跃值: (46)
能力值: (RANK:550 )
在线值:
发帖
回帖
粉丝
4
好文章,学习!
2007-5-29 09:24
0
雪    币: 424
活跃值: (10)
能力值: ( LV9,RANK:850 )
在线值:
发帖
回帖
粉丝
5
```好哇`

以后慢慢学习`
2007-5-29 09:28
0
雪    币: 201
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
6
好,不错.....
2007-5-29 10:41
0
雪    币: 226
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
7
多年没上来过了,学习中
2007-5-29 19:05
0
雪    币: 314
活跃值: (15)
能力值: ( LV12,RANK:410 )
在线值:
发帖
回帖
粉丝
8
阁下这样的牛人还用慢慢学习吗?
2007-5-30 20:38
0
雪    币: 243
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
9
很详细哟,进步了,3Q!
2007-5-31 10:09
0
游客
登录 | 注册 方可回帖
返回
//