S.l.e!ep.¢%

像打了激速一样,以四倍的速度运转,开心的工作
简单、开放、平等的公司文化;尊重个性、自由与个人价值;
posts - 1098, comments - 335, trackbacks - 0, articles - 1
  C++博客 :: 首页 :: 新随笔 :: 联系 :: 聚合  :: 管理
利用Win32 Debug API打造自己的Debugger
2006-10-21 23:43
很多朋友都梦想有自己的Debugger程序,今天我们就来自己制作一个。作为一个Debugger程序,其最基本的功能框架其实就是完成2件事情:
 启动目标程序。
 实时监控目标程序的运行,并做出相应的应对。
我们要打造自己的Debugger程序,实际上也只需要完成这两个功能就可以了。当然,要完成这两个特定的功能,我们不可能从头开始造轮子,要首先看看操作系统给我们提供了什么样的基础设施:
由于我们是在Windows平台上工作,自然离不开微软公司提供的文档大全——MSDN。翻开MSDN,定位到“Debugging and Error Handling”,一些最基本的Windows Debug信息都在这里面。不过与其他栏目相比,这个栏目的信息明显显得单薄许多——也许越是底层、强大的技术,微软越不想公开吧。
初步浏览之后,我们可以确定,对于我们的Debugger而言,最重要的Debug API有如下几个:
 CreateProcess —— 用于创建被调试进程
 WaitForDebugEvent —— Debug Loop(调试循环)的主要构成函数
 ContinueDebugEvent —— 用于构成Debug Loop
 GetThreadContext —— 得到被调试进程的寄存器信息
 SetThreadContext —— 设置被调试进程的寄存器信息
 ReadProcessMemory —— 得到被调试进程的内存内容
 WriteProcessMemory —— 设置被调试进程的内存内容
最重要的数据结构有如下几个:
 CONTEXT —— 寄存器结构
 STARTUPINFO —— Start信息
 PROCESS_INFORMATION —— 进程相关信息
 DEBUG_EVENT —— Debug Event(调试事件)结构
可以说,我们的Debugger程序就是利用这几个API函数结合下面的几个数据结构,完成我们指定的功能。那么下面就让我们先来看看这几个API和数据结构的具体含义:

 

Debug API解析
在这里,我们将对上面所述的几个Debug调试API做一个检阅式的考察,大概介绍一下每个API的应用领域。而将这些API应用到具体实践中去,将会在下一部分“实例解析”中,给出详细的说明。
1.CreateProcess。
函数原型:BOOL CreateProcess(
LPCTSTR lpApplicationName, // 要创建的进程模块名
LPTSTR lpCommandLine, // 命令行字符串
LPSECURITY_ATTRIBUTES lpProcessAttributes, // 进程安全属性
LPSECURITY_ATTRIBUTES lpThreadAttributes, // 线程安全属性
BOOL bInheritHandles, // 句柄继承选项
DWORD dwCreationFlags, // 进程创建选项
LPVOID lpEnvironment, // 进程环境块数据指针
LPCTSTR lpCurrentDirectory, // 当前目录名
LPSTARTUPINFO lpStartupInfo, // 启动信息
LPPROCESS_INFORMATION lpProcessInformation // 进程信息
);
函数解析:该函数是Windows平台提供的最基本的创建进程的函数。每当我们双击一个EXE可执行文件,Windows内核就会自动调用该函数创建我们双击的文件所对应的进程。该函数中,最重要的参数有三个:一个是进程模块名,指明了要创建哪个进程;一个是进程创建选项,指明了要如何创建目标进程;对于Debugger程序而言,最常用的创建选项就是:DEBUG_PROCESS|DEBUG_ONLY_THIS_PROCESS。最后还有一个就是进程信息,我们调用CreateProcess创建了进程以后,Windows会将新创建的进程的相关信息全部放到ProcessInfo信息块中,我们在Debug Loop调试循环中使用进程信息块中的数据与目标进程交互,监视和控制目标进程的动作。
2.WaitForDebugEvent。
函数原型:BOOL WaitForDebugEvent(
LPDEBUG_EVENT lpDebugEvent, // Debug Event(调试事件指针)
DWORD dwMilliseconds // 超时设置
);
函数解析:该函数构成了Debug Loop调试循环的主体,一个Debugger程序在创建出目标进程后,一般都会紧接着循环调用该函数等待目标进程的各种调试信息,这个循环调用WaitForDebugEvent的过程,我们就称之为Debug Loop调试循环。调试循环是所有Debugger程序的主体部分,Debugger几乎所有的监视、控制、调整的功能都是在调试循环内完成的。一般来说,此处的超时设置都设置为-1也就是无穷等待下去,该函数是非阻塞函数,在没有Debug Event发生,处于等待的过程中,仅消耗极其微小的系统资源。
3.ContinueDebugEvent。
函数原型:BOOL ContinueDebugEvent(
DWORD dwProcessId, // 目标进程ID
DWORD dwThreadId, // 目标线程ID
DWORD dwContinueStatus // 线程继续的标志
);
函数解析:该函数主要用于Debugger在Debug Loop调试循环中,处理完Debug Event,通知目标进/线程继续运作。通常情况下,目标进程ID和目标线程ID这两个参数,都是CreateProcess调用后,ProcessInfo结构中所包含的信息。该函数通过目标进程/线程ID来唯一标识目标进/线程,并且通过设置不同的ContinueStatus来通知目标进/线程继续运行的动作。最主要的ContinueStatus有两种选择:一个是DBG_CONTINUE,表明调试事件已经被Debugger处理完毕,目标进/线程可以照常继续运行;另一个是DBG_EXCEPTION_NOT_HANDLED,表明Debugger并未处理该调试事件,目标进程收到该标志位后,将会将调试事件沿着Windows异常调用链继续往下发送。直至该调试事件被处理完为止——当然,如果目标进程发出的Debug Event没有任何调试器能够处理,那最后Windows只有祭出自己的杀手锏:应用程序XXX异常,即将被关闭。
3.GetThreadContext & SetThreadContext。
函数原型:BOOL GetThreadContext(
HANDLE hThread, // 目标线程句柄
LPCONTEXT lpContext // CONTEXT结构
);
BOOL SetThreadContext(
HANDLE hThread, // 目标线程句柄
CONST CONTEXT *lpContext // CONTEXT结构
);
函数解析:这两个函数分别用来得到和设置目标线程的寄存器内容。请注意,在Windows操作系统中,操作系统调度的最小单位粒度是线程而不是进程,所以,笼统地说:设置某进程的寄存器内容是错误的,因为一个进程可能对应多个线程。因此在和寄存器打交道的时候,一定要指明是哪个线程所对应的寄存器。该函数参数中,由hThread参数也就是线程句柄参数指定目标线程,该参数的来源也通常是CreateProcess调用后所得到的ProcessInfomation中的hThread成员。而CONTEXT结构则是根据所在机器硬件平台的不同而有不同的定义。Windows操作系统在Intel、MIPS、Alpha和PowerPC平台上,各有不同的CONTEXT定义,每种定义都忠实而完整地反映出了目标CPU的寄存器分布情况。不过虽然CONTEXT结构在不同的CPU平台上有不同的表现形式,但是最基本的Intel X86架构在各个CPU上面的表现都是相同的,因此,只要Debugger代码未牵涉到各个CPU非常Specifc的细节,还是可以跨CPU平台使用的。
4.ReadProcessMemory & WriteProcessMemory。
函数原型:BOOL ReadProcessMemory(
HANDLE hProcess, // 进程句柄
LPCVOID lpBaseAddress, // 欲读取内存基地址
LPVOID lpBuffer, // 数据缓冲区指针
SIZE_T nSize, // 欲读取的内存内容长度
SIZE_T * lpNumberOfBytesRead // 实际读取的内存内容长度
);
BOOL WriteProcessMemory(
HANDLE hProcess, // 进程句柄
LPVOID lpBaseAddress, // 欲写入内存基地址
LPCVOID lpBuffer, // 数据缓冲区指针
SIZE_T nSize, // 欲写入的内存内容长度
SIZE_T * lpNumberOfBytesWritten // 实际写入的内存内容长度
);
函数解析:这两个函数分别用于“读取”和“写入”目标进程的内存地址空间。与寄存器操作不同,Windows对内存的分配粒度是以进程为单位的。由于自Intel 386以后,所有的Intel x86系列CPU都采用的保护模式,因此在保护模式下,Windows为每一个应用程序——也就是每一个进程都虚拟出一个拥有4GB大小内存的“虚拟机器”, 隶属于该进程的所有线程都共享这4GB的地址空间。因此,与上面寄存器操作不同,读取、写入内存操作时,我们需要的是进程的句柄——当然,该句柄也来源于CreateProcess后得到的ProcessInfomation结构。有了进程句柄以后,我们还需要一个基地址和一个长度参数来确定我们的Debugger程序所需要读取的内存范围。当然,此处的基地址数值上应该对应于Windows虚拟出来的那4GB的平坦地址空间内的地址——也就是经过了段选择和页选择过程后的地址数值。

DEBUG_EVENT全面剖析
在整个调试循环中,Debugger和目标进程之间调试信息的交互完全是通过调用WaitForDebugEvent时,传递的DEBUG_EVENT结构参数。该结构的定义初看起来似乎很简单:
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;
不过就是一个union联合域加上三个普通的信息数据。可细细推敲之下,这个结构所实现的功能却绝对不简单——让我们做一个最简单的思考,作为Debugger,至少应该能够收到:
 目标进程启动
 目标进程发生异常
 目标进程退出
这三个最基本的调试信息。而且每个信息所对应的信息类型应该都不太一样,比如:
 目标进程启动时,我们需要的是目标进程的模块名字,权限设置等启动信息
 目标进程异常时,我们应该能够知道异常的地址,异常的原因(这里面分类又很多),异常的严重程度
 目标进程退出时,我们应该能够知道进程的退出值,用来确定进程是否属于正常退出
上面所列出的,还只是一些最基本的要素而已,如果要构成整个Windows Debug API的数据交互层,那情况还要复杂得多。将这么多错综复杂的信息全部用一个结构表示,其难度可想而知。
微软公司在这里选择的方法是通过dwDebugEventCode标识最基本信息,然后通过union联合域将各种信息全部包罗进去的方法。这种做法的优点是在WaitForDebugEvent调用时,只需要传递一个统一的结构参数即可;缺点就是Debug_Event结构内部信息的异常复杂,给程序设计带来不小的麻烦。
我们在使用Debug_Event结构时,首先要得到dwDebugEventCode的值,从而确定union联合域内是什么内容。两者之间的对应关系如下表所示:

dwDebugEventCode的值
Union联合域类型 调试信息
EXCEPTION_DEBUG_EVENT EXCEPTION_DEBUG_INFO
应用程序发生异常
CREATE_THREAD_DEBUG_EVENT CREATE_THREAD_DEBUG_INFO
线程创建
CREATE_PROCESS_DEBUG_EVENT CREATE_PROCESS_DEBUG_INFO
进程创建
EXIT_THREAD_DEBUG_EVENT EXIT_THREAD_DEBUG_INFO
线程退出
EXIT_PROCESS_DEBUG_EVENT EXIT_PROCESS_DEBUG_INFO
进程退出
LOAD_DLL_DEBUG_EVENT LOAD_DLL_DEBUG_INFO
Dll载入
UNLOAD_DLL_DEBUG_EVENT UNLOAD_DLL_DEBUG_INFO
Dll卸载
OUTPUT_DEBUG_STRING_EVENT OUTPUT_DEBUG_STRING_INFO
输出Debug字符串
RIP_EVENT RIP_INFO 系统调试错误
上表仅仅是Debug_Event结构数据解析的第一层,当union联合域取得各个不同的值时,这些第二层的数据结构并不比这一层简单多少——不过幸亏大部分的时间我们解析Debug_Event结构只需要解析到第二层就可以了。
一般情况下,我们最感兴趣的DebugEventCode就是EXCEPTION_DEBUG_EVENT,而不巧的是,EXCEPTION_DEBUG_EVENT所对应的EXCEPTION_DEBUG_INFO结构也是所有union联合域结构中最复杂的一个,因此,有必要在此再对EXCEPTION_DEBUG_INFO这个二级数据结构作进一步的详细说明:
typedef struct _EXCEPTION_DEBUG_INFO {
EXCEPTION_RECORD ExceptionRecord;
DWORD dwFirstChance;
} EXCEPTION_DEBUG_INFO, *LPEXCEPTION_DEBUG_INFO;
 dwFirstChange:如果为0,表示是该异常从前未被处理过,也就是当前我们的Debugger程序处于Windows异常处理链条的头部。
 ExceptionRecord:该结构内部的信息是EXCEPTION_DEBUG_INFO结构的真实存储地点。因此,继续剖析该结构。
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;
首先:要注意到,该结构内部有一个类型为EXCEPTION_RECORD*的数据成员,这是C语言里经典的串联数据的方式:用一个指针域连接各个数据成员,可以构成经典的“链表”数据结构(英文里管这种做法叫做Chain)。
其次:ExceptionCode成员标识了该结构所代表的EXCEPTION_RECORD的类型,Windows内部定义了如下20种异常行为:

值 含义
EXCEPTION_ACCESS_VIOLATION 存取越界 
EXCEPTION_ARRAY_BOUNDS_EXCEEDED 由硬件监测到的数组访问越界 
EXCEPTION_BREAKPOINT 触发断点 
EXCEPTION_DATATYPE_MISALIGNMENT 数据未对齐
EXCEPTION_FLT_DENORMAL_OPERAND 浮点操作数范围越界(太小或太大)
EXCEPTION_FLT_DIVIDE_BY_ZERO 浮点操作除数为0
EXCEPTION_FLT_INEXACT_RESULT 浮点运算结果不能用小数正常表示.
EXCEPTION_FLT_INVALID_OPERATION 其他未知的浮点数错误
EXCEPTION_FLT_OVERFLOW 浮点操作太大溢出
EXCEPTION_FLT_STACK_CHECK 浮点栈溢出.
EXCEPTION_FLT_UNDERFLOW 浮点操作太小溢出
EXCEPTION_ILLEGAL_INSTRUCTION 执行非法指令
EXCEPTION_IN_PAGE_ERROR 存取未存在的内存页
EXCEPTION_INT_DIVIDE_BY_ZERO 除数为0
EXCEPTION_INT_OVERFLOW 整数操作,最高位溢出
EXCEPTION_INVALID_DISPOSITION 错误的异常处理程序地址
EXCEPTION_NONCONTINUABLE_EXCEPTION 遇上不能继续执行的异常
EXCEPTION_PRIV_INSTRUCTION 当前模式下,不能执行该指令
EXCEPTION_SINGLE_STEP 单步跟踪断点触发
EXCEPTION_STACK_OVERFLOW 线程的栈空间溢出
一般而言,我们的Debugger程序处理得最多的异常情况就是EXCEPTION_BREAKPOINT和EXCEPTION_SINGLE_STEP。例如:我们需要在目标进程运行到0x00400000地址的时候对目标进程进行一些操作,那么我们只要设法让目标程序运行到0x00400000的时候,向我们的Debugger程序发出异常信号,那么我们的Debugger程序就能收到该信号,并进而通过前面介绍的Set/GetThreadContext和Read/WriteProcessMemory函数对目标进程进行控制操作。

一个最初步的Debugger框架
既然上面所述的内容现在已经足够我们写出一个最基本的Debugger了,那么我们就来“实战演习”一番,先写出一个最基本的Debugger程序,该程序完成的功能异常简单:利用CONTEXT_FULL | CONTEXT_DEBUG_REGISTERS作为标志,创建一个进程,并让目标进程能够照常地运作起来。
首先跳入我们脑海的代码大概如下所示:
::CreateProcess (_T("Msg.exe"), NULL, NULL, NULL, NULL, DEBUG_PROCESS|DEBUG_ONLY_THIS_PROCESS,
NULL, NULL, &sif, &pi) ;
//这个下面就是大名鼎鼎的Debug框架!
do
{
::WaitForDebugEvent (&DBEvent, INFINITE) ;
dwState = DBG_EXCEPTION_NOT_HANDLED ;
case EXIT_PROCESS_DEBUG_EVENT :
{
STOP = TRUE ;
break ;
}
if (!STOP)
{
::ContinueDebugEvent(pi.dwProcessId, pi.dwThreadId, dwState) ;
}
} while (!STOP) ;
编译,双击,很不幸!Windows给了我们
为什么会出现上面这种错误情况?微软的文档里面并没有对Debugger具体应该如何工作做出详细的说明。不过通过跟踪上面的程序,我们发现每次Msg.exe目标应用程序启动前,都会向我们的Debugger程序发送一个EXCEPTION_BREAKPOINT的断点信号,而我们的Debug Loop并没有对该信号进行处理。现在我们加入对该信号的处理过程,使我们的Debugger不返回DBG_EXCEPTION_NOT_HANDLED而返回DBG_CONTINUE,具体到代码中,就是在:
case EXIT_PROCESS_DEBUG_EVENT :
这一句之前,加入:
case EXCEPTION_DEBUG_EVENT:
{
switch (DBEvent.u.Exception.ExceptionRecord.ExceptionCode)
{
case EXCEPTION_BREAKPOINT:
{
dwState = DBG_CONTINUE ;
break ;
}
}
break ;
}
编译、链接,试运行,一切OK。Msg.exe很正常地弹出了MessageBox,


从C到C++
从上面那个最简单的调试框架代码不难看出,每当我们要增加一个处理判断的异常类型,都要在日益庞大复杂的switch – case代码中增加新的选择路线,程序的代码很容易就变得臃肿不堪且难以维护。而实际上整个代码的工作骨架并没有发生实质性的改变,变化的仅仅是针对各种不同的异常情况我们所需要的不同处理子过程——这不禁让我们想起了Template设计模式——我们可以将在Debug Loop中分解Debug_Event的代码放进框架代码中,然后在框架代码中调用不同的hook虚函数,这样,我们要扩展自己的Debugger功能时,只需要从已有的debug_base基类继承,重写hook虚方法即可。
随本文附带的Debug_Base.h中有debug_base类,该类就实现了如上所述的Debugger Template模板。该类最简单的使用如下所示:
Debugger::debug_base debugger ;
debugger.run_debug_loop(std::tstring(TEXT("Msg.exe"))) ;
以上这两段代码,就实现了上一节所提到的那个最简单的调试器所完成的功能——载入被调试程序并且照常运行。
Debug_base提供有如下几个hook虚函数:
hook函数名 函数作用
handle_first_exception Windows内核首次发送EXCEPTION_BREAKPOINT时的处理子过程
handle_exception_breakpoint 调试过程中EXCEPTION_BREAKPOINT的处理子过程
handle_single_step 调试过程中EXCEPTION_SINGLE_STEP的处理子过程
handle_process_create 进程创建处理子过程
handle_process_exit 进程退出处理子过程
handle_thread_create 线程创建处理子过程
handle_thread_exit 线程推出处理子过程
handle_dll_load Dll载入时得到调用
handle_dll_unload Dll卸载的时候得到调用
handle_debug_wstring 被调试程序调用OutputDebugString得到调用
实际的编程过程中,我们只需要从debug_base派生出自己的新类,并重写其中需要的虚函数即可编译得到自己的Debugger!
例如:我们想让我们的Debugger具有如下功能:
 在程序入口点处中断,提示已经到达入口点
 截获被调试程序的Debug字符串,并输出
那么我们可以这样设计我们的新类:
class MyDebugger : public Debugger::debug_base
{
virtual void handle_first_exception(const PROCESS_INFORMATION& pi)
{
::MessageBox(NULL, TEXT("首次中断"), TEXT("Debugger Worked"), NULL) ;
return ;
}

virtual void handle_debug_wstring(std::wstring& debug_wstring)
{
::MessageBox(NULL, ATL::CW2T(debug_wstring.c_str()), TEXT("Debug String"), NULL) ;
return ;
}
} ;
而主程序并不需要做任何改变,该Debugger运行后,可以接受被调试程序的调试信息,并输出:


结束语
Windows借助于Debug API,为我们提供了一个功能全面的Ring 3级别的调试平台,但是由于文档资料、示例代码的缺乏,迄今为止,Debug API的应用面还很窄。其实如果能真正把握住动态调试的精髓,利用Debug API打造一个自己的专有调试器调试一些很特殊的程序是能收到非常好的效果的。可以说,任何一款脱壳机或者是内存注册机都只不过是Debug API的一个具体应用。
当然,由于篇幅所限,本文也不可能更加详尽深入地探索Windows Debugger的世界,我们的debug_base类还很初步——甚至还不具备在程序运行时增加调试断点的功能,不过只要理解了Debugger的工作原理,结合MicroSoft和Intel的一些底层的编程资料,各种功能都可以慢慢添加、完善。希望读者朋友在学习逆向工程的过程中,注意基础知识的积累、整理、消化,不断地提升自己对计算机系统的理解水平,最终才能修成一代宗师,打造出made in china的Soft-ice和OllyDbg!


只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理