【逆向】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指令下一条地址,不会进入函数内部。

posted @ 2022-03-23 23:01  SunsetR  阅读(174)  评论(0)    收藏  举报