《C++ 黑客编程揭秘与防范(第2版)》—第6章6.6节调试API函数的使用-阿里云开发者社区

开发者社区> 开发与运维> 正文

《C++ 黑客编程揭秘与防范(第2版)》—第6章6.6节调试API函数的使用

简介:

本节书摘来自异步社区《C++ 黑客编程揭秘与防范(第2版)》一书中的第6章6.6节调试API函数的使用,作者冀云,更多章节内容可以访问云栖社区“异步社区”公众号查看。

6.6 调试API函数的使用
C++ 黑客编程揭秘与防范(第2版)
Windows中有些API函数是专门用来进行调试的,被称作Debug API,或者是调试API。利用这些函数可以进行调试器的开发,调试器通过创建有调试关系的父子进程来进行调试,被调试进程的底层信息、即时的寄存器、指令等信息都可以被获取,进而用来分析。

上面介绍的OllyDbg调试器的功能非常强大,虽然有众多的功能,但是其基础的实现就是依赖于调试API。调试API函数的个数虽然不多,但是合理使用会产生非常大的作用。调试器依赖于调试事件,调试事件有着非常复杂的结构体。调试器有着固定的流程,由于实时需要等待调试事件的发生,其过程是一个调试循环体,非常类似于SDK开发程序中的消息循环。无论是调试事件还是调试循环,对于调试或者说调试器来说,其最根本、最核心的部分是中断,或者说其最核心的部分是可以捕获中断。

6.6.1 常见的3种断点方法
在前面介绍OD的时候提到过,产生中断的方法是设置断点。常见的产生中断的断点方法有3种,分别是中断断点、内存断点和硬件断点。下面介绍这3种断点的不同。

中断断点,这里通常指的是汇编语言中的int 3指令,CPU执行该指令时会产生一个断点,因此也常称之为INT3断点。现在演示如何使用int 3来产生一个断点,代码如下:

int main(int argc, char* argv[])
{
  __asm int 3

  return 0;
}

代码中使用了__asm,在__asm后面可以使用汇编指令。如果想添加一段汇编指令,方法是__asm{}。通过__asm可以在C语言中进行内嵌汇编语言。在__asm后面直接使用的是int 3指令,这样会产生一个异常,称为断点中断异常。对这段简单的代码进行编译连接,并且运行。运行后出现错误对话框,如图6-62所示。


dea6a2744cf42cf2e9dbd982a923898e23fa5554

注:图6-62所示的异常对话框中通过链接“请单击此处”可以打开详细的异常报告。如果读者电脑与此处显示的对话框不同,请依次进行如下设置:在“我的电脑”上单击右键,在弹出的菜单中选择“属性”,打开“属性”对话框,选择“高级”选项卡,选择“错误报告”按钮,打开“错误汇报”界面,在该界面上选择“启用错误汇报”单选按钮,然后单击确定。通过这样的设置,就可以启动“异常对话框”了。对于分析程序的BUG、挖掘软件的漏洞,弹出异常对话框界面是非常有用的。

这个对话框可能常常见到,而且见到以后多半会很让人郁闷,通常情况是直接单击“不发送”按钮,然后关闭这个对话框。在这里,这个异常是通过int 3导致的,不要忙着关掉它。通常在写自己的软件时如果出现这样的错误,应该去寻找更多的帮助信息来修正错误。单击“请单击此处”链接,出现如图6-63所示的对话框。


2e22bcd8d8464c2b1f752ef995640cd68e0668e6

弹出“异常基本信息”对话框,因为这个对话框给出的信息实在太少了,继续单击“要查看关于错误报告的技术信息”后面的“请单击此处”链接,打开如图6-64所示的对话框。


1b8323a474efff4401f5d8f9efe36e5b714756da

通常情况下,在这个报告中只关心两个内容,一是Code,二是Address。在图6-64中,Code后面的值为0x80000003, Address后面的值为0x0000000000401028。Code的值为产生异常的异常代码,Address是产生异常的地址。在Winnt.h中定义了关于Code的值,在这里0x80000003的定义为STATUS_BREAKPOINT,也就是断点中断。在Winnt.h中的定义为:

define STATUS_BREAKPOINT        ((DWORD)0x80000003L)

可以看出,这里给的Address是一个VA(虚拟地址),用OD打开这个程序,直接按F9键运行,如图6-65和图6-66所示。


92babbb73ebb7fa9abda59254dca04d802682c99

从图6-65中可以看到,程序执行停在了00401029位置处。从图6-66看到,INT3命令位于00401028位置处。再看一下图6-64中Address后面的值,为00401028。这也就证明了在系统的错误报告中可以给出正确的出错地址(或产生异常的地址)。这样在以后写程序的过程中可以很容易地定位到自己程序中有错误的位置。

注:在OD中运行自己的int 3程序时,可能OD不会停在00401029地址处,也不会给出类似图6-65的提示。在实验这个例子的时候需要对OD进行设置,在菜单中选择“选项”->“调试设置”,打开“调试选项”对话框,选择“异常”选项卡,取消“INT3 中断”复选框的选中状态,这样就可以按照该例子进行测试了。

回到中断断点的话题上,中断断点是由int 3产生的,那么要如何通过调试器(调试进程)在被调试进程中设置中断断点呢?看图6-65中00401028地址处,在地址值的后面、反汇编代码的前面,中间那一列的内容是汇编指令对应的机器码。可以看出,INT3对应的机器码是0xCC。如果想通过调试器在被调试进程中设置INT3断点的话,那么只需要把要中断的位置的机器码改为0xCC即可。当调试器捕获到该断点异常时,修改为原来的值即可。

内存断点的方法同样是通过异常产生的。在Win32平台下,内存是按页进行划分的,每页的大小为4KB。每一页内存都有其各自的内存属性,常见的内存属性有只读、可读写、可执行、可共享等。内存断点的原理就是通过对内存属性的修改,本该允许进行的操作无法进行,这样便会引发异常。

在OD中关于内存断点有两种,一种是内存访问,另一种是内存写入。用OD随便打开一个应用程序,在其“转存窗口”(或者叫“数据窗口”)中随便选中一些数据点后单击右键,在弹出的菜单中选择“断点”命令,在“断点”子命令下会看到“内存访问”和“内存写入”两种断点,如图6-67所示。


9cdccd00e9a225b7c05603a53d8919966755a365

下面通过简单例子来看如何产生一个内存访问异常,代码如下:
#include <Windows.h>

#define MEMLEN 0x100

int main(int argc, char* argv[])
{
  PBYTE pByte = NULL;

  pByte = (PBYTE)malloc(MEMLEN);
  if ( pByte == NULL )
  {
    return -1;
  }

  DWORD dwProtect = 0;
  VirtualProtect(pByte, MEMLEN, PAGE_READONLY, &dwProtect);

  BYTE bByte = '\xCC';

  memcpy(pByte, (const char *)&bByte, MEMLEN);

  free(pByte);

  return 0;
}

这个程序中使用了VirtualProtect()函数,该函数与第3章中介绍的VirtualProtectEx()函数类似,不过VirtualProtect()是用来修改当前进程的内存属性的。读者如果不记得,可以参考MSDN。

对这个程序编译连接,并运行起来。熟悉的出错界面又出现在眼前,如图6-68所示。

按照前面介绍的步骤打开“错误报告内容”对话框,如图6-69所示。


400d5b05809fd6ed4b000b22d1920eeba530cec7

按照上面的分析方法来看一下Code和Address这两个值。Code后面的值为0xc0000005,这个值在Winnt.h中的定义如下:

#define STATUS_ACCESS_VIOLATION     ((DWORD)0xC0000005L)

这个值的意义表示访问违例。Address后面的值为0x0000000000403093,这个值是地址,但是这里的地址根据程序来考虑,值是用malloc()函数申请的,用于保存数据的堆地址,而不是用来保存代码的地址。这个地址就不进行测试了,因为是动态申请,很可能每次不同,因此读者了解就可以了。

硬件断点是由硬件进行支持的,它是硬件提供的调试寄存器组。通过这些硬件寄存器设置相应的值,然后让硬件断在需要下断点的地址。在CPU上有一组特殊的寄存器,被称作调试寄存器。该调试寄存器有8个,分别是DR0—DR7,用于设置和管理硬件断点。调试寄存器DR0—DR3用于存储所设置硬件断点的内存地址,由于只有4个调试寄存器可以用来存放地址,因此最多只能设置4个硬件断点。寄存器DR4和DR5是系统保留的,并没有公开其用处。调试寄存器DR6被称为调试状态寄存器,记录了上一次断点触发所产生的调试事件类型信息。调试寄存器DR7用于设置触发硬件断点的条件,比如硬件读断点、硬件访问断点或硬件执行断点。由于调试寄存器原理内容较多,这里就不具体进行介绍。

6.6.2 调试API函数及相关结构体介绍
通过前面的内容已经知道,调试器的根本是依靠中断,其核心也是中断。前面也演示了两个产生中断异常的例子。本小节的内容是介绍调试API函数及其相关的调试结构体。调试API函数的数量非常少,但是其结构体是非常少有的较为复杂的。虽然说是复杂,其实只是嵌套的层级比较多,只要了解了较为常见的,剩下的可以自己对照MSDN进行学习。在介绍完调试API函数及其结构体后,再来简单演示如何通过调试API捕获INT3断点和内存断点。

1.创建调试关系
既然是调试,那么必然存在调试和被调试。调试和被调试的这种调试关系是如何建立起来的,这是读者首先要了解的内容。要使调试和被调试创建调试关系,就会用到两个函数中的一个,分别是CreateProcess()和DebugActiveProcess()。其中CreateProcess()函数已经介绍过了,那么如何使用CreateProcess()函数来建立一个需要被调试的进程呢?回顾一下CreateProcess()函数,其定义如下:

BOOL CreateProcess(
 LPCTSTR lpApplicationName,             // name of executable module
 LPTSTR lpCommandLine,                // command line string
 LPSECURITY_ATTRIBUTES lpProcessAttributes,  // SD
 LPSECURITY_ATTRIBUTES lpThreadAttributes,   // SD
 BOOL bInheritHandles,                // handle inheritance option
 DWORD dwCreationFlags,               // creation flags
 LPVOID lpEnvironment,                // new environment block
 LPCTSTR lpCurrentDirectory,           // current directory name
 LPSTARTUPINFO lpStartupInfo,          // startup information
 LPPROCESS_INFORMATION lpProcessInformation  // process information
);

现在要做的是创建一个被调试进程。CreateProcess()函数有一个dwCreationFlags参数,其取值中有两个重要的常量,分别为DEBUG_PROCESS和DEBUG_ONLY_THIS_PROCESS。DEBUG_
PROCESS的作用是被创建的进程处于调试状态。如果一同指定了DEBUG_ONLY_ THIS_PROCESS的话,那么就只能调试被创建的进程,而不能调试被调试进程创建出来的进程。只要在使用CreateProcess()函数时指定这两个常量即可。

除了CreateProcess()函数以外,还有一种创建调试关系的方法,该方法用的函数如下:

BOOL DebugActiveProcess(
 DWORD dwProcessId  // process to be debugged
);

这个函数的功能是将调试进程附加到被调试的进程上。该函数的参数只有一个,该参数指定了被调试进程的进程ID号。从函数名与函数参数可以看出,这个函数是和一个已经被创建的进程来建立调试关系的,跟CreateProcess()的方法不一样。在OD中也同样有这个功能,打开OD,选择菜单中的“文件”->“挂接”(或者是“附加”)命令,就出现“选择要附加的进程”窗口,如图6-70所示。


3d39e6c74f70bac36d3ce75e04b4b67aa8f09acf

OD的这个功能是通过DebugActiveProcess()函数来完成的。

调试器与被调试的目标进程可以通过前两个函数建立调试关系,但是如何使调试器与被调试的目标进程断开调试关系呢?有一个很简单的方法:关闭调试器进程,这样调试器进程与被调试的目标进程会同时结束。也可以关闭被调试的目标进程,这样也可以断开调试关系。那如何让调试器与被调试的目标进程断开调试关系,又保持被调试目标进程的运行呢?这里介绍一个函数,函数名为DebugActiveProcessStop(),其定义如下:

WINBASEAPI
BOOL
WINAPI
DebugActiveProcessStop(
  __in DWORD dwProcessId
  );

该函数只有一个参数,就是被调试进程的进程ID号。使用该函数可以在不影响调试器进程和被调试进程的正常运行的情况下,将两者的关系解除。但是有一个前提,被调试进程需要处于运行状态,而不是中断状态。如果被调试进程处于中断状态时和调试进程解除调试关系,由于被调试进程无法运行而导致退出。

2.判断进程是否处于被调试状态
很多程序都要检测自己是否处于被调试状态,比如游戏、病毒,或者加壳后的程序。游戏为了防止被做出外挂而进行反调试,病毒为了给反病毒工程师增加分析难度而反调试。加壳程序是专门用来保护软件的,当然也会有反调试的功能(该功能仅限于加密壳,压缩壳一般没有反调试功能)。

本小节不是要介绍反调试,而是介绍一个简单的函数,这个函数是判断自身是否处于被调试状态,函数名为IsDebuggerPresent(),其定义如下:

BOOL IsDebuggerPresent(VOID);
该函数没有参数,根据返回值来判断是否处于被调试状态。这个函数也可以用来进行反调试。不过由于这个函数的实现过于简单,很容易就能够被分析者突破,因此现在也没有软件再使用该函数来进行反调试了。

下面通过一个简单的例子来演示IsDebuggerPresent()函数的使用,代码如下:

#include <Windows.h>
#include <stdio.h>

extern "C" BOOL WINAPI IsDebuggerPresent(VOID);

DWORD WINAPI ThreadProc(LPVOID lpParam)
{
  while ( TRUE )
  {
    //检测用ActiveDebugProcess()来创建调试关系
    if ( IsDebuggerPresent() == TRUE )
    {
      printf("thread func checked the debuggee \r\n");
      break;
    }
    Sleep(1000);
  }

  return 0;
}

int main(int argc, char* argv[])
{
  BOOL bRet = FALSE;

  //检测CreateProcess()创建调试关系
  bRet = IsDebuggerPresent();

  if ( bRet == TRUE )
  {
    printf("main func checked the debuggee \r\n");
    getchar();
    return 1;
  }

  HANDLE hThread = CreateThread(NULL, 0, ThreadProc, NULL, 0, NULL);
  if ( hThread == NULL )
  {
    return -1;
  }

  WaitForSingleObject(hThread, INFINITE);
  CloseHandle(hThread);

  getchar();

  return 0;
}

这个例子用来检测自身是否处于被调试状态。在进入主函数后,直接调用IsDebugger Present()函数,判断是否被调试器创建。在自定义线程函数中,一直循环检测是否被附加。只要发现自身处于被调试状态,那么就在控制台中进行输出提示。

现在用OD对这个程序进行测试。首先用OD直接打开这个程序,并按F9键运行,如图6-71所示。


80d4391868e62858592c647e13a956828cc054b7

按下F9键启动以后,控制台中输出“main func checked the debuggee”,也就是发现了调试器。再测试一下检测OD附加的效果。先运行这个程序,然后用OD去挂接它,看其提示,如图6-72所示。


224eac9aeee3cba64b7e2aec3621ae72c7feccc7

控制台中输出“thread func checked the debuggee”。可以看出,用OD进行附加也能够检测到自身处于被调试状态。

注意:
进行该测试时请选用原版OD。由于检测是否处于被调试的这种方法过于简单,因此任何其他修改版的OD都可以将其突破,从而使得测试失败。
3.断点异常函数
有时为了调试方便可能会在自己的代码中插入__asm int 3,这样当程序运行到这里时会产生一个断点,就可以用调试器进行调试了。其实微软提供了一个函数,使用该函数可以直接让程序运行到某处的时候产生INT3断点,该函数的定义如下:

VOID DebugBreak(VOID);
修改一下前面的程序,把__asm int 3替换为DebugBreak(),编译连接并运行。同样会因产生异常而出现“异常基本信息”对话框,查看它的“错误报告内容”,如图6-73所示。


62589f8aaa932118a7d974af982a193e26e52e92

Code后面的值为0x80000003,看到它就应该知道是EXCEPTION_BREAKPOINT。Address后面的值为0x000000007c92120e,可以看出该值在系统的DLL文件中,因为调用的是系统提供的函数。

4.调试事件
调试器在调试程序的过程中是通过用户不断地下断点、单步等来完成的,而断点的产生在前面的内容中提到过一部分。通过前面介绍的INT3断点、内存断点和硬件断点可以得知,调试器是在捕获目标进程产生的断点或异常从而做出响应。当然,对于所介绍的断点来说是这样的。不过对于调试器来说,除了对断点和异常做出响应以外,还会对其他的一些事件做出响应,断点和异常只是所有调试能进行响应事件的一部分。

调试器的工作方式主要是依赖在调试过程中不断产生的调试事件。调试事件在系统中被定义为一个结构体,也是到目前为止要接触的最为复杂的一个结构体,因为这个结构体的嵌套关系很多。这个结构体的定义如下:

typedef struct _DEBUG_EVENT { 
 DWORD dwDebugEventCode; 
 DWORD dwProcessId; 
 DWORD dwThreadId; 
 union { 
   EXCEPTION_DEBUG_INFO Exception; 
   CREATE_THREAD_DEBUG_INFO CreateThread; 
   CREATE_PROCESS_DEBUG_INFO CreateProcessInfo; 
   EXIT_THREAD_DEBUG_INFO ExitThread; 
   EXIT_PROCESS_DEBUG_INFO ExitProcess; 
   LOAD_DLL_DEBUG_INFO LoadDll; 
   UNLOAD_DLL_DEBUG_INFO UnloadDll; 
   OUTPUT_DEBUG_STRING_INFO DebugString; 
   RIP_INFO RipInfo; 
 } u; 
} DEBUG_EVENT, *LPDEBUG_EVENT;

这个结构体非常重要,这里有必要详细地介绍。

dwDebugEventCode:该字段指定了调试事件的类型编码。在调试过程中可能产生的调试事件非常多,因此要根据不同的类型码进行不同的响应处理。常见的调试事件如图6-74所示。


e4d89210b76545e6a21c23d2bf8d4eb9526394cb

dwProcessId:该字段指明了引发调试事件的进程ID号。

dwThreadId:该字段指明了引发调试事件的线程ID号。

u:该字段是一个联合体,其取值由dwDebugEventCode指定。该联合体包含很多个结构体,包括EXCEPTION_DEBUG_INFO、CREATE_THREAD_ DEBUG_INFO、CREATE_PRO CESS_DEBUG_
INFO、EXIT_THREAD_DEBUG_INFO、EXIT_PROCESS_DEBUG_INFO、LOAD_DLL_DEBUG_
INFO、UNLOAD_DLL_DEBUG_INFO和OUTPUT_DEBUG_STRING_INFO。

在以上众多的结构体中,特别要介绍一下EXCEPTION_DEBUG_INFO,因为这个结构体包含关于异常相关的信息;而其他几个结构体的使用比较简单,读者可以参考MSDN。

EXCEPTION_DEBUG_INFO的定义如下:

typedef struct _EXCEPTION_DEBUG_INFO { 
 EXCEPTION_RECORD ExceptionRecord; 
 DWORD dwFirstChance; 
} EXCEPTION_DEBUG_INFO, *LPEXCEPTION_DEBUG_INFO;

EXCEPTION_DEBUG_INFO包含的EXCEPTION_RECORD结构体中保存着真正的异常信息, dwFirstChance中保存着ExceptionRecord的个数。EXCEPTION_RECORD结构体的定义如下:

typedef struct _EXCEPTION_RECORD { 
 DWORD ExceptionCode; 
 DWORD ExceptionFlags; 
 struct _EXCEPTION_RECORD *ExceptionRecord; 
 PVOID ExceptionAddress; 
 DWORD NumberParameters; 
 ULONG_PTR ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];
} EXCEPTION_RECORD, *PEXCEPTION_RECORD;

ExceptionCode:异常码。该值在MSDN中的定义非常多,不过这里需要使用的值只有3个,分别是EXCEPTION_ACCESS_VIOLATION(访问违例)、EXCEPTION_ BREAKPOINT(断点异常)和EXCEPTION_SINGLE_STEP(单步异常)。这3个值中的前两个值对于读者来说应该是非常熟悉的,因为在前面已经介绍过了;最后一个单步异常想必读者也非常熟悉。使用OD快捷键的F7键、F8键时就是在使用单步功能,而单步异常就是由EXCEPTION_SINGLE _STEP来表示的。

ExceptionRecord:指向一个EXCEPTION_RECORD的指针,异常记录是一个链表,其中可能保存着很多异常信息。

ExceptionAddress:异常产生的地址。

调试事件这个结构体DEBUG_EVENT看似非常复杂,其实也只是嵌套得比较深而已。只要读者仔细体会每个结构体、每层嵌套的含义,自然就觉得它没有多么复杂。

5.调试循环
调试器不断地对被调试目标进程进行捕获调试信息,有点类似于Win32应用程序的消息循环,但是又有所不同。调试器在捕获到调试信息后进行相应的处理,然后恢复线程,使之继续运行。

用来等待捕获被调试进程调试事件的函数是WaitForDebugEvent(),其定义如下:

BOOL WaitForDebugEvent(
 LPDEBUG_EVENT lpDebugEvent,    // debug event information
 DWORD dwMilliseconds        // time-out value
);

lpDebugEvent:该参数用于接收保存调试事件;

dwMilliseconds:该参数用于指定超时的时间,无限制等待使用INFINITE。

调试器捕获到调试事件后,会对被调试的目标进程中产生调试事件的线程进行挂起。调试器对被调试目标进程进行相应的处理后,需要使用ContinueDebugEvent()对先前被挂起的线程进行恢复。ContinueDebugEvent()函数的定义如下:

BOOL ContinueDebugEvent(
 DWORD dwProcessId,      // process to continue
 DWORD dwThreadId,       // thread to continue
 DWORD dwContinueStatus    // continuation status
);

dwProcessId:该参数表示被调试进程的进程标识符。

dwThreadId:该参数表示准备恢复挂起线程的线程标识符。

dwContinueStatus:该参数指定了该线程以何种方式继续执行,其取值为DBG_EXCEPTI ON_NOT_HANDLED和DBG_CONTINUE。对于这两个值来说,通常情况下并没有什么差别。但是当遇到调试事件中的调试码为EXCEPTION_DEBUG_EVENT时,这两个常量就会有不同的动作。如果使用DBG_EXCEPTION_NOT_HANDLED,调试器进程将会忽略该异常,Windows会使用被调试进程的异常处理函数对异常进行处理;如果使用DBG_CONTINUE的话,那么需要调试器进程对异常进行处理,然后继续运行。

由上面两个函数配合调试事件结构体,就可以构成一个完整的调试循环。以下这段调试循环的代码摘自MSDN:

DEBUG_EVENT DebugEv;                 // debugging event information 
DWORD dwContinueStatus = DBG_CONTINUE;    // exception continuation 

for(;;) 
{ 

// Wait for a debugging event to occur. The second parameter indicates 
// that the function does not return until a debugging event occurs. 

  WaitForDebugEvent(&DebugEv, INFINITE); 

// Process the debugging event code. 

  switch (DebugEv.dwDebugEventCode) 
  { 
    case EXCEPTION_DEBUG_EVENT: 
    // Process the exception code. When handling 
    // exceptions, remember to set the continuation 
    // status parameter (dwContinueStatus). This value 
    // is used by the ContinueDebugEvent function. 

      switch (DebugEv.u.Exception.ExceptionRecord.ExceptionCode) 
      { 
        case EXCEPTION_ACCESS_VIOLATION: 
        // First chance: Pass this on to the system. 
        // Last chance: Display an appropriate error. 

        case EXCEPTION_BREAKPOINT: 
        // First chance: Display the current 
        // instruction and register values. 

        case EXCEPTION_DATATYPE_MISALIGNMENT: 
        // First chance: Pass this on to the system. 
        // Last chance: Display an appropriate error. 

        case EXCEPTION_SINGLE_STEP: 
        // First chance: Update the display of the 
        // current instruction and register values. 

        case DBG_CONTROL_C: 
        // First chance: Pass this on to the system. 
        // Last chance: Display an appropriate error. 

        // Handle other exceptions. 
      } 

    case CREATE_THREAD_DEBUG_EVENT: 
    // As needed, examine or change the thread's registers 
    // with the GetThreadContext and SetThreadContext functions; 
    // and suspend and resume thread execution with the 
    // SuspendThread and ResumeThread functions. 

    case CREATE_PROCESS_DEBUG_EVENT: 
    // As needed, examine or change the registers of the 
    // process's initial thread with the GetThreadContext and 
    // SetThreadContext functions; read from and write to the 
    // process's virtual memory with the ReadProcessMemory and 
    // WriteProcessMemory functions; and suspend and resume 
    // thread execution with the SuspendThread and ResumeThread 
    // functions. 

    case EXIT_THREAD_DEBUG_EVENT: 
    // Display the thread's exit code. 

    case EXIT_PROCESS_DEBUG_EVENT: 
    // Display the process's exit code. 

    case LOAD_DLL_DEBUG_EVENT: 
    // Read the debugging information included in the newly 
    // loaded DLL. 

    case UNLOAD_DLL_DEBUG_EVENT: 
    // Display a message that the DLL has been unloaded. 

    case OUTPUT_DEBUG_STRING_EVENT: 
    // Display the output debugging string. 

  } 

// Resume executing the thread that reported the debugging event. 

ContinueDebugEvent(DebugEv.dwProcessId, 
  DebugEv.dwThreadId, dwContinueStatus); 

}

以上就是一个完整的调试循环。不过有些调试事件对于读者来说可能是用不到的,那么就把不需要的调试事件所对应的case语句删除就可以了。

6.内存的操作
调试器进程通常要对被调试的目标进程进行内存的读取或写入。跨进程的内存读取和写入的函数其实在前面的章节已介绍过,就是ReadProcessMemory()和WriteProcessMemory()。

要对被调试的目标进程设置INT3断点,就需要使用WriteProcessMemory()函数对指定的位置写入0xCC。当INT3被执行后,要在原来的位置上把原来的机器码写回去,原来的机器码需要使用ReadProcessMemory()函数来进行读取。

内存操作除了以上两个函数以外,还有一个就是修改内存的页面属性的函数,即VirtualProtectEx()。这个函数在前面也介绍过了。

7.线程环境相关API及结构体
在前面的章节中介绍过,进程是用来向系统申请各种资源的,而真正被分配到CPU并执行代码的是线程。进程中的每个线程都共享进程的资源,但是每个线程都有不同的线程上下文或线程环境。Windows是一个多任务的操作系统,在Windows中为每一个线程分配一个时间片,当某个线程执行完其所属的时间片后,Windows会切换到另外的线程去执行。在进行线程切换以前有一步保存线程环境的工作,那就是保证在切换时线程的寄存器值、栈信息及描述符等相关的所有信息在切换回来后不变。只有把线程的上下文保存起来,下次该线程被CPU再次调度时才能正确地接着上次的工作继续进行。

在Windows系统下,将线程环境定义为CONTEXT结构体。该结构体需要在Winnt.h头文件中找到,在MSDN中并没有给出定义。CONTEXT结构体的定义如下:

//
// Context Frame
//
// This frame has a several purposes: 1) it is used as an argument to
// NtContinue, 2) is is used to constuct a call frame for APC delivery,
// and 3) it is used in the user level thread creation routines.
//
// The layout of the record conforms to a standard call frame.
//

typedef struct _CONTEXT {

  //
  // The flags values within this flag control the contents of
  // a CONTEXT record.
  //
  // If the context record is used as an input parameter, then
  // for each portion of the context record controlled by a flag
  // whose value is set, it is assumed that that portion of the
  // context record contains valid context. If the context record
  // is being used to modify a threads context, then only that
  // portion of the threads context will be modified.
  //
  // If the context record is used as an IN OUT parameter to capture
  // the context of a thread, then only those portions of the thread's
  // context corresponding to set flags will be returned.
  //
  // The context record is never used as an OUT only parameter.
  //

  DWORD ContextFlags;

  //
  // This section is specified/returned if CONTEXT_DEBUG_REGISTERS is
  // set in ContextFlags. Note that CONTEXT_DEBUG_REGISTERS is NOT
  // included in CONTEXT_FULL.
  //

  DWORD  Dr0;
  DWORD  Dr1;
  DWORD  Dr2;
  DWORD  Dr3;
  DWORD  Dr6;
  DWORD  Dr7;

  //
  // This section is specified/returned if the
  // ContextFlags word contians the flag CONTEXT_FLOATING_POINT.
  //

  FLOATING_SAVE_AREA FloatSave;

  //
  // This section is specified/returned if the
  // ContextFlags word contians the flag CONTEXT_SEGMENTS.
  //

  DWORD  SegGs;
  DWORD  SegFs;
  DWORD  SegEs;
  DWORD  SegDs;

  //
  // This section is specified/returned if the
  // ContextFlags word contians the flag CONTEXT_INTEGER.
  //

  DWORD  Edi;
  DWORD  Esi;
  DWORD  Ebx;
  DWORD  Edx;
  DWORD  Ecx;
  DWORD  Eax;

  //
  // This section is specified/returned if the
  // ContextFlags word contians the flag CONTEXT_CONTROL.
  //

  DWORD  Ebp;
  DWORD  Eip;
  DWORD  SegCs;       // MUST BE SANITIZED
  DWORD  EFlags;       // MUST BE SANITIZED
  DWORD  Esp;
  DWORD  SegSs;

  //
  // This section is specified/returned if the ContextFlags word
  // contains the flag CONTEXT_EXTENDED_REGISTERS.
  // The format and contexts are processor specific
  //

  BYTE  ExtendedRegisters[MAXIMUM_SUPPORTED_EXTENSION];
} CONTEXT;

这个结构体看似很大,只要了解汇编语言其实也并不大。前面章节中介绍了关于汇编语言的知识,对于结构体中的各个字段,读者应该非常熟悉。关于各个寄存器的介绍,这里就不重复了,这需要读者翻看前面的内容。这里只介绍ContextFlags字段的功能,该字段用于控制GetThreadContext()和SetThreadContext()能够获取或写入的环境信息。ContextFlags的取值也只能在Winnt.h头文件中找到,其取值如下:

#define CONTEXT_CONTROL   (CONTEXT_i386 | 0x00000001L)  // SS:SP, CS:IP, FLAGS, BP
#define CONTEXT_INTEGER   (CONTEXT_i386 | 0x00000002L)  // AX, BX, CX, DX, SI, DI
#define CONTEXT_SEGMENTS  (CONTEXT_i386 | 0x00000004L)  // DS, ES, FS, GS
#define CONTEXT_FLOATING_POINT    (CONTEXT_i386 | 0x00000008L)  // 387 state
#define CONTEXT_DEBUG_REGISTERS    (CONTEXT_i386 | 0x00000010L)  // DB 0-3,6,7
#define CONTEXT_EXTENDED_REGISTERS  (CONTEXT_i386|0x00000020L) //cpu specific exten- sions

#define CONTEXT_FULL (CONTEXT_CONTROL | CONTEXT_INTEGER |\
           CONTEXT_SEGMENTS)

#define CONTEXT_ALL (CONTEXT_CONTROL | CONTEXT_INTEGER | CONTEXT_SEGMENTS | CONTEXT_ FLOATING_POINT | CONTEXT_DEBUG_REGISTERS | CONTEXT_EXTENDED_REGISTERS)

从这些宏定义的注释能很清楚地知道这些宏可以控制GetThreadContext()和SetThread Context()进行何种操作。用户在真正使用时进行相应的赋值就可以了。

注意:
关于CONTEXT结构体可能会在Winnt.h头文件中找到多个定义,因为该结构体是与平台相关的。因此,在各种不同平台上,此结构体有所不同。
线程环境在Windows中定义了一个CONTEXT的结构体。要获取或设置线程环境的话,需要使用GetThreadContext()和SetThreadContext()。这两个函数的定义如下:

BOOL GetThreadContext(
 HANDLE hThread,          // handle to thread with context
 LPCONTEXT lpContext       // context structure
);
BOOL SetThreadContext(
 HANDLE hThread,          // handle to thread
 CONST CONTEXT *lpContext    // context structure
);

这两个函数的参数基本一样,hThread表示线程句柄,而lpContext表示指向CONTEXT的指针。所不同的是,GetThreadContext()是用来获取线程环境的,SetThreadContext()是用来设置线程环境的。需要注意的是,在获取或设置线程的上下文时,请将线程暂停后进行,以免发生“不明现象”。

本文仅用于学习和交流目的,不代表异步社区观点。非商业转载请注明作译者、出处,并保留本文的原始链接。

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

分享:
开发与运维
使用钉钉扫一扫加入圈子
+ 订阅

集结各类场景实战经验,助你开发运维畅行无忧

其他文章