【逆向】Windows用户层调试器原理
开始调试
创建一个新的进程进行调试
1 BOOL WINAPI CreateProcess( 2 LPCTSTR lpApplicationName, 3 LPTSTR lpCommandLine, 4 LPSECURITY_ATTRIBUTES lpProcessAttributes, 5 LPSECURITY_ATTRIBUTES lpThreadAttributes, 6 BOOL bInheritHandles, 7 DWORD dwCreationFlags, //进程创建标识 8 LPVOID lpEnvironment, 9 LPCTSTR lpCurrentDirectory, 10 LPSTARTUPINFO lpStartupInfo, 11 LPPROCESS_INFORMATION lpProcessInformation 12 ); 13 //为 dwCreationFlags 参数设置 “DEBUG_ONLY_THIS_PROCESS | CREATE_NEW_CONSOLE”标识,就能以调试方式创建一个新的进程进行调试。
将调试器附加到一个正在运行的程序进行调试
1 BOOL DebugActiveProcess( 2 [in] DWORD dwProcessId //要调试进程的标识符 3 ); 4 //调试器被授予对进程的调试访问权限,就好像它使用DEBUG_ONLY_THIS_PROCESS标志创建了该进程一样。 5 //如果要停止调试进程,必须退出进程或调用 DebugActiveProcessStop 函数。 6 //使用 DebugSetProcessKillOnExit 函数可以退出调试而不结束目标进程。
调试循环
调试循环的作用类似于Windows的窗口过程函数(WndProc),通过一个 While() 循环来接收和处理被调试进程中发生的所有调试事件。
1 void DebugLoop() 2 { 3 //等待调试事件 4 DEBUG_EVENT de = {0}; 5 while( WaitForDebugEvent(&de, INFINITE) ) 6 { 7 //处理调试事件(共有9种调试类型) 8 switch (de->dwDebugEventCode) 9 { 10 case EXCEPTION_DEBUG_EVENT: 11 break; 12 case CREATE_THREAD_DEBUG_EVENT: 13 break; 14 case CREATE_PROCESS_DEBUG_EVENT: 15 break; 16 case EXIT_THREAD_DEBUG_EVENT: 17 break; 18 case EXIT_PROCESS_DEBUG_EVENT: 19 break; 20 case LOAD_DLL_DEBUG_EVENT: 21 break; 22 case UNLOAD_DLL_DEBUG_EVENT: 23 break; 24 case OUTPUT_DEBUG_STRING_EVENT: 25 break; 26 case RIP_EVENT: 27 break; 28 } 29 //恢复线程执行,等待下一个调试事件 30 ContinueDebugEvent(de.dwProcessId, de.dwThreadId, DBG_CONTINUE); 31 } 32 } 33 34 BOOL WINAPI WaitForDebugEvent( 35 LPDEBUG_EVENT lpDebugEvent, //指向接收调试事件信息的DEBUG_EVENT结构体的指针。 36 DWORD dwMilliseconds //等待时间 37 ); 38 39 BOOL WINAPI ContinueDebugEvent( 40 DWORD dwProcessId, //进程ID 41 DWORD dwThreadId, //线程ID 42 DWORD dwContinueStatus //正常处理:DBG_CONTINUE 43 //无法处理:DBG_EXCEPTION_NOT_HANDLED 将异常转交给被调试程序的SEH处理 44 );
调试事件
通过调试循环获取调试事件时,WaitForDebugEvent 函数会将发生的调试事件及相关信息保存到一个 DEBUG_EVENT 结构体中,调试器会根据这些信息作出相应的处理。
1 typedef struct _DEBUG_EVENT 2 { 3 DWORD dwDebugEventCode; //调试事件类型 4 DWORD dwProcessId; //发生调试事件的进程的标识符 5 DWORD dwThreadId; //发生调试事件的线程的标识符 6 union //联合体:与调试事件类型相关的附加信息(分别对应调试循环中提到的9种调试事件类型) 7 { 8 EXCEPTION_DEBUG_INFO Exception; //异常调试 9 CREATE_THREAD_DEBUG_INFO CreateThread; //创建线程 10 CREATE_PROCESS_DEBUG_INFO CreateProcessInfo; //创建进程 11 EXIT_THREAD_DEBUG_INFO ExitThread; //退出线程 12 EXIT_PROCESS_DEBUG_INFO ExitProcess; //退出进程 13 LOAD_DLL_DEBUG_INFO LoadDll; //加载 DLL 14 UNLOAD_DLL_DEBUG_INFO UnloadDll; //卸载 DLL 15 OUTPUT_DEBUG_STRING_INFO DebugString; //输出字符串 16 RIP_INFO RipInfo; //RIP 调试事件 17 } u; 18 } DEBUG_EVENT, *LPDEBUG_EVENT; 19 20 //更多信息可以参考MSDN: 21 https://docs.microsoft.com/zh-cn/windows/win32/api/minwinbase/ns-minwinbase-debug_event?redirectedfrom=MSDN
软件断点
1 //实现:INT 3(0xCC) 2 将目标地址首字节机器码修改为0xCC,当被调试器进程执行到 INT3 指令时触发异常,程序暂停执行, 3 调试器捕获异常后,对其进行处理,然后将首字节的0xCC恢复成修改前的数据,最后将 EIP-1 后继续执行。 4 软件断点理论上可以设置无限个,但是由于其实现原理,比较容易被反调试代码检测到。
硬件断点
1 //实现:DR寄存器(DR0-DR3、DR6、DR7) 2 使用 DR0-DR3 调试寄存器分别保存硬件断点地址:最多4个 3 使用 DR6 调试控制寄存器标识当前是哪个地址触发的异常:B0-B3 分别对应 DR0-DR3 4 使用 DR7 调试控制寄存器分别设置相应的标志位,用于控制每个断点的类型和状态:断点类型、断点长度、是否有效 5 L0-L3:局部有效,影响当前线程,每次中断后会被清零 6 G0-G3:全局有效,影响其它线程,每次中断后不被清零 7 LEN:控制断点长度 00=1字节、01=2字节、11=4字节 8 R/W:控制断点类型 00=执行断点、01=写入断点、11=访问断点 9 10 //注意: 11 以下两种方式都会触发单步异常: 12 1、设置硬件断点 13 2、设置单步执行(单步步入、单步步过) 14 在触发单步异常时,可以通过DR6寄存器的B0-B3标志位判断是硬件断点还是单步执行, 15 如果B0-B3标志位被置1,说明是硬件断点产生的单步异常,否则就是单步执行。

内存断点
//实现:修改内存读/写属性 BOOL VirtualProtectEx( [in] HANDLE hProcess, [in] LPVOID lpAddress, [in] SIZE_T dwSize, [in] DWORD flNewProtect, //修改属性 [out] PDWORD lpflOldProtect //原始属性 ); 内存访问断点:flNewProtect = PAGE_NOACCESS(禁用对页面提交区域的所有访问) 内存写入断点:flNewProtect = PAGE_EXECUTE_READ(启用对页面提交区域的执行或只读访问) //注意: 设置内存断点的时候实际修改的是整个内存页的内存属性,而不是某个字节,所以调试器在每次发生异常时都要对异常地址进行比较, 如果内存断点设置过多就会严重影响调试器的执行效率,自己编写调试器时一定对内存断点数量进行限制,在OllyDbg中只能同时使用一个内存断点。
单步执行
1 //单步步入:EFlags寄存器(TF标志位置1) 2 遇到CALL指令,会进入函数内部执行 3 4 //单步步过:EFlags寄存器(TF标志位置1) 5 遇到CALL指令,在CALL指令的下一条指令处设置软件断点或硬件断点,F8(实际是Go)运行到CALL指令下一条地址,不会进入函数内部。


浙公网安备 33010602011771号