Tutorial 29: Win32 Debug API Part 2
第二十九课:win32调试API第二部分
________________________________________
We continue with the subject of win32 debug API. In this tutorial, we will learn how to modify the debuggee process.
我们继续win32 调试 API这个话题。在这一课中,我们将学习如何修改被调试的进程;
Download the example  (见附件) 
Theory:
In the previous tutorial, we know how to load the debuggee and handle debug events that occur in its process. In order to be useful, our program must be able to modify the debuggee process. There are several APIs just for this purpose.
在上一课中,我们懂得了如何装载一个被调试的进程,还有当调试事件在它所处的进程中发生时,我们该如何处理。为了有用,我们的程序必须能修改被调试进程。这里有几个API函数仅用于这个目的;;
ReadProcessMemory This function allows you to read memory in the specified process. The function prototype is as follows: 
ReadProcessMemory 这个函数允许你在指定的进程中读内存数据。函数原型如下: 
ReadProcessMemory proto hProcess:DWORD, lpBaseAddress:DWORD, lpBuffer:DWORD, nSize:DWORD, lpNumberOfBytesRead:DWORD
hProcess is the handle to the process you want to read.
hProcess 是你想读的进程句柄;
lpBaseAddress is the address in the target process you want to start reading. For example, if you want to read 4 bytes from the debuggee process starting at 401000h, the value in this parameter must be 401000h.
lpBaseAddress 目标进程中你想读的内存起始地址。例如,如果你想从被调试进程的401000H处读取4字节,那么这个参数的值必须是401000h。
lpBuffer is the address of the buffer to receive the bytes read from the process. 
LpBuffer,存放从进程中读取数据的缓冲区地址;
nSize is the number of bytes you want to read
nSize 你想读的字节数;
lpNumberOfBytesRead is the address of the variable of dword size that receives the number of bytes actually read. If you don't care about it, you can use NULL.
LpNumberofBytesRead  接受实际读取字节数的一dword类型变量的地址。 如果你对于它漠不关心,你可以用NULL值;
  WriteProcessMemory is the counterpart of ReadProcessMemory. It enables you to write memory of the target process. Its parameters are exactly the same as those of ReadProcessMemory 
WriteProcessMemory 是ReadProcessMemory的对应物。它能让你改写目标进程的内存空间;它的参数和ReadProcessMemory相同;
The next two API functions need a little background on context. Under a multitasking OS like Windows, there can be several programs running at the same time. Windows gives each thread a timeslice. When that timeslice expires, Windows freezes the present thread and switches to the next thread that has the highest priority. Just before switching to the other thread, Windows saves values in registers of the present thread so that when the time comes to resume the thread, Windows can restore the last *environment* of that thread. The saved values of the registers are collectively called a context. 
下面两个函数需要你具备少许在线程执行环境上的背景知识。在一个像windows这样的多任务OS中,同一时间可以有几个应用程序同时运行。Windows给每一线程一时间片。当时间片用完时,windows冻结当前线程并切换到下一个有高优先级的线程。(线程调度) 只不过在切换到其它线程中执行之前,windows保存当前线程使用的寄存器值以至于在线程时间到来时继续执行这个线程,windows能恢复最近一次线程运行的环境;这些被保存的寄存器的值和起来就是一个线程执行环境;
Back to our subject. When a debug event occurs, Windows suspends the debuggee. The debuggee's context is saved. Since the debuggee is suspended, we can be sure that the values in the context will remain unchanged . We can get the values in the context with GetThreadContext and we can change them with SetThreadContext.
These two APIs are very powerful. With them, you have at your fingertips the VxD-like power over the debuggee: you can alter the saved register values and just before the debuggee resumes execution, the values in the context will be written back into the registers. Any change you made to the context is reflected back to the debuggee. Think about it: you can even alter the value of the eip register and divert the flow of execution to anywhere you like! You won't be able to do that under normal circumstance.
回到我们的主题,当一个调试事件发生时,windows将被调试的进程挂起。被调试进程的执行环境被保存。因为被调试的进程处于挂起态,我们可以确信在进程执行环境中的值将不会被改变。我们能用GetThreadContext这个函数来获取这些值并且我们可以用SetThreadContext函数来改变这些值。这两个API函数是非常有用的。用它们,在对待被调试进程上,你可以做到和VXD一样的有力:你能改变这些被保存的寄存器值并且只能够在被调试进程恢复执行之前改变它们,在进程上下文环境中的值将被写回到寄存器中。任何对进程执行环境中的值的改变都被反射给被了调试进程;想一下:你能改变EiP寄存器的值并且让执行流转移到你想要的任何地方!而在正常情况下你并不能这样做;
GetThreadContext proto hThread:DWORD, lpContext:DWORD 
hThread is the handle to the thread that you want to obtain the context from 
hThread 是你想获得的线程执行环境的线程句柄;
lpContext is the address of the CONTEXT structure that will be filled when the function returns successfully.
LpContext 是CONTEXT结构的地址,当函数成功返回时,这个CONTEXT结构将被填充;
SetThreadContext has exactly the same parameters. Let's see what a CONTEXT structure looks like:
SetThreadContext 参数完全和GetThreadContext一样;让我们看看CONTEXT结构的成员都有哪些: 
  CONTEXT STRUCT 
  ContextFlags dd ? 
;----------------------------------------------------------------------------------------------------------
; This section is returned if ContextFlags contains the value CONTEXT_DEBUG_REGISTERS 
如果ContextFlags包含值CONTEXT_DEBUG_REGISTERS,这个节区被返回
  ;-----------------------------------------------------------------------------------------------------------
iDr0 dd ? 
iDr1 dd ? 
iDr2 dd ? 
iDr3 dd ? 
iDr6 dd ? 
iDr7 dd ? 
  ;----------------------------------------------------------------------------------------------------------
; This section is returned if ContextFlags contains the value CONTEXT_FLOATING_POINT 
如果ContextFlags包含值CONTEXT_FLOATING_POINT,这个节区被返回;
  ;-----------------------------------------------------------------------------------------------------------
  FloatSave FLOATING_SAVE_AREA <> 
  ;----------------------------------------------------------------------------------------------------------
; This section is returned if ContextFlags contains the value CONTEXT_SEGMENTS 
  如果ContextFlags包含值CONTEXT_SEGMENTS,这个节区被返回;

  ;----------------------------------------------------------------------------------------------------------- 
  regGs dd ? 
regFs dd ? 
regEs dd ? 
regDs dd ? 
  ;----------------------------------------------------------------------------------------------------------
; This section is returned if ContextFlags contains the value CONTEXT_INTEGER 
  如果ContextFlags包含值CONTEXT_INTEGER,这个节区被返回;

  ;----------------------------------------------------------------------------------------------------------- 
  regEdi dd ? 
regEsi dd ? 
regEbx dd ? 
regEdx dd ? 
regEcx dd ? 
regEax dd ? 
  ;----------------------------------------------------------------------------------------------------------
; This section is returned if ContextFlags contains the value CONTEXT_CONTROL 
如果ContextFlags包含值CONTEXT_CONTROL,这个节区被返回;
  ;----------------------------------------------------------------------------------------------------------- 
  regEbp dd ? 
regEip dd ? 
regCs dd ? 
regFlag dd ? 
regEsp dd ? 
regSs dd ? 
  ;----------------------------------------------------------------------------------------------------------
; This section is returned if ContextFlags contains the value CONTEXT_EXTENDED_REGISTERS 
  如果ContextFlags包含值CONTEXT_EXTENDED_REGISTERS,这个节区被返回;

  ;----------------------------------------------------------------------------------------------------------- 
  ExtendedRegisters db MAXIMUM_SUPPORTED_EXTENSION dup(?) CONTEXT ENDS 
As you can observe, the members of this structures are mimics of the real processor's registers. Before you can use this structure, you need to specify which groups of registers you want to read/write in ContextFlags member. For example, if you want to read/write all registers, you must specify CONTEXT_FULL in ContextFlags. If you want only to read/write regEbp, regEip, regCs, regFlag, regEsp or regSs, you must specify CONTEXT_CONTROL in ContextFlags.
你能看出,这个结构的成员是对真实处理器的寄存器的伪造。在你用这个结构之前,你需要在ContextFlags成员变量中指定你想读或写的寄存器组。例如,如果你想读或写所有的寄存器,你必须指定CONTEXT_FULL标志在ContextFlags中。如果你仅对regEbp, regEip, regCs, regFlag, regEsp or regSs 寄存器读或写,你必须在ContextFlags中指定CONTEXT_CONTROL标志;
One thing you must remember when using the CONTEXT structure: it must be aligned on dword boundary else you'd get strange results under NT. You must put "align dword" just above the line that declares it, like this: 
在用到CONTEXT结构时有一件事你必须记住:它必须按双子对齐,否则在NT中你将得到一些奇怪的结果。你必须放置“align dword”在声明语句之前,像这样:
align dword
MyContext CONTEXT <>
Example:
The first example demonstrates the use of DebugActiveProcess. First, you need to run a target named win.exe which goes in an infinite loop just before the window is shown on the screen. Then you run the example, it will attach itself to win.exe and modify the code of win.exe such that win.exe exits the infinite loop and shows its own window.
第一个例子示范了DebugActiveProcess的使用。首先,你需要运行一个目标进程win.exe,在它的窗口显示在屏幕上之前,win.exe进入死循环。 然后,你运行这个例子,它将附加它自己在win。Exe上并且修改win.exe的代码。让win.exe退出死循环并将窗口显示在屏幕上;
.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

.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 
;--------------------------------------------------------------------
; 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 
Analysis:
invoke FindWindow, addr ClassName, NULL 
Our program needs to attach itself to the debuggee with DebugActiveProcess which requires the process Id of the debuggee. We can obtain the process Id by calling GetWindowThreadProcessId which in turn needs the window handle as its parameter. So we need to obtain the window handle first. 
With FindWindow, we can specify the name of the window class we need. It returns the handle to the window created by that window class. If it returns NULL, no window of that class is present.
我们的程序需要用DebugActiveProcess函数附加它自己在被调试程序上,DebugActiveProcess函数需要被调试程序的进程ID号。我们通过调用GetWindowThreadProcessId函数来获得进程ID号,这个函数需要窗口句柄作为它的参数。所以,我们首先就要获得窗口句柄,使用FindWindow函数,首先,我们指定我们需要的窗口类的名称。它返回这个窗口类创建的窗口句柄。如果返回值为NULL,表明当前没有这个依于这个类的窗口;
.if eax!=NULL 
    invoke GetWindowThreadProcessId, eax, addr ProcessId 
    mov ThreadId, eax 
    invoke DebugActiveProcess, ProcessId 
After we obtain the process Id, we can call DebugActiveProcess. Then we enter the debug loop waiting for the debug events.
在我们获取了进程Id后,我们就能调用DebugActiveProcess了。然后进入调试循环等待调试事件;
       .if DBEvent.dwDebugEventCode==CREATE_PROCESS_DEBUG_EVENT 
          mov context.ContextFlags, CONTEXT_CONTROL 
          invoke GetThreadContext,DBEvent.u.CreateProcessInfo.hThread, addr context           
When we get CREATE_PROCESS_DEBUG_INFO, it means the debuggee is suspended, ready for us to do surgery upon its process. In this example, we will overwrite the infinite loop instruction in the debuggee (0EBh 0FEh) with NOPs ( 90h 90h). 
当我们得到CREATE_PROCESS_DEBUG_INFO时,它意味着被调试进程已经挂起,我们可以对该进程调试了。在这个例子中,我们将用NOP( 90h 90h)指令改写在被调试进程中的无限循环的指令(0EBh 0FEh)

First, we need to obtain the address of the instruction. Since the debuggee is already in the loop by the time our program attached to it, eip will always point to the instruction. All we need to do is obtain the value of eip. We use GetThreadContext to achieve that goal. We set the ContextFlags member to CONTEXT_CONTROL so as to tell GetThreadContext that we want it to fill the "control" register members of the CONTEXT structure.
首先,我们需要获取指令的地址。因为在我们的程序附加到被调试进程上时,它就已经在死循环中了,eip将永远指向无限循环的指令。我们要做的就是得到eip的值。我们用GetThreadContext来达到这个目标。我们设置ContextFlags成员的值为CONTEXT_CONTROL,这样就告诉GetThreadContext函数我们想让它填充CONTEXT结构的“control”寄存器成员;
          invoke WriteProcessMemory, DBEvent.u.CreateProcessInfo.hProcess, context.regEip ,addr buffer, 2, NULL
Now that we get the value of eip, we can call WriteProcessMemory to overwrite the "jmp $" instruction with NOPs, thus effectively help the debuggee exit the infinite loop. After that we display the message to the user and then call ContinueDebugEvent to resume the debuggee. Since the "jmp $" instruction is overwritten by NOPs, the debuggee will be able to continue with showing its window and enter the message loop. The evidence is we will see its window on screen.
现在,我们得到了eip的值,我们能调用WriteProcessMemory来用NOP指令覆盖Jmp $ 指令了,因而有效的帮助被调试进程退出无限循环。在向用户显示了消息后,我们调用ContinueDebugEvent来恢复被调试进程;因为JMP $ 指令被NOP指令覆盖了,被调试进程能继续显示它的窗口并进入消息循环了。事实是我们将在屏幕上看到它的窗口; 
The other example uses a slightly different approach to break the debuggee out of the infinite loop.
另一个例子与此稍有不同,它可以看成是将被调试程序从无限循环中中断。
.......
.......
.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 
.......
....... 
It still calls GetThreadContext to obtain the current value of eip but instead of overwriting the "jmp $" instruction, it increments the value of regEip by 2 to "skip over" the instruction. The result is that when the debuggee regains control , it resumes execution at the next instruction after "jmp $". 
它仍然调用GetThreadContext函数来获得eip的当前值,但是它并不覆盖JMP $指令,它将eip寄存器的值加上2来跳过这条指令。结果是当被调试进程重新得到控制权时,将执行在jmp $ 后的指令。
Now you can see the power of Get/SetThreadContext. You can also modify the other register images as well and their values will be reflected back to the debuggee. You can even insert int 3h instruction to put breakpoints in the debuggee process. 
现在你能看到Get/SetThreadContext的力量了吧。你也可以修改其他寄存器映象,这 些值将直接反射到被调试程序中。你甚至能将int 3h指令插入到被调试进程中来放置一个断点;
________________________________________
This article come from Iczelion's asm page
风向改变 翻译于 2008-5-1

上传的附件 tut29.zip