windows环境下的调试器探究

在windows里面触发异常主要通过三种方式:软件断点、内存断点、硬件断点来实现,本文对这三种方式进行原理分析,通过自己构造代码来实现调试器的效果。

0x00 前言

在windows里面触发异常主要通过三种方式:软件断点、内存断点、硬件断点来实现,本文对这三种方式进行原理分析,通过自己构造代码来实现调试器的效果。

0x01 软件断点

当在调试器下一个断点,其实就是把这行汇编语句的硬编码改为CC,即int 3

image-20220401163633170.png

被调试进程

1.CPU检测到INT 3指令
2.查IDT表找到对应的函数
3.CommonDispatchException
4.KiDispatchException
5.DbgkForwardException收集并发送调试事件

首先找到IDT表的3号中断

image-20220401173115746.png

调用CommonDispatchException

image-20220401173221221.png

通过KiDispatchException分发异常

image-20220401173322672.png

首先用KeContextFromframes备份,若为用户调用则跳转

image-20220401181028062.png

进入函数如果没有内核调试器则跳转,也就是说如果有内核调试器的存在,3环调试器是接收不到异常的

image-20220401181427103.png

然后调用调试事件

image-20220401181849466.png

DbgkForwardException主要是通过DbgkpSendApiMessage来发送调试事件,第二个参数决定线程是否挂起,首先通过cmp判断,如果为0则直接跳转,如果不为0则调用DbgkpSuspendProcess将被调试进程挂起

也就是说如果要想调试进程,就必须要调用DbgkpSuspendProcess将调试进程挂起

image-20220401182810735.png

首先用调试模式创建进程,然后使用调试循环

image-20220401210427370.png

如果是异常事件则调用ExceptionHandler

image-20220401210528898.png

ExceptionHandler主要是通过判断ExcptionRecord结构里面的ExceptionCode来判断异常的类型,然后调用相应的函数,这里首先看软件断点,即int 3,调用Int3ExceptionProc

image-20220401210654430.png

image-20220401212239579.png

image-20220401212317494.png

下断点会把之前的指令修改为CC,如果不是系统断点,就把下断点的位置修改的指令写回去,然后获取int3断点的地址

image-20220401212500170.png

然后获取上下文,所有调试寄存器都存储在ContextFlags里面

image-20220401212831739.png

当我们下软件断点的时候,EIP并不会停留在断点的地方,而是会停留在断点+1的地方(这里不同的异常EIP停留的位置不同),所以这里需要进行EIP-1的操作

image-20220401213324112.png

然后调用处理的函数

image-20220401213713782.png

image-20220401213725277.png

当被调试进程收集并发送调试事件之后就会处于阻塞状态,根据异常处理的结果决定下一步的执行

image-20220401213954580.png

实现代码如下

  1. // Debug4.cpp : Defines the entry point for the console application.
  2. //
  3. #include "stdafx.h"
  4. #include <stdio.h>
  5. #include <windows.h>
  6. #include <tlhelp32.h>
  7. #define DEBUGGEE "C:\\\\ipmsg.exe"
  8. //被调试进程ID,进程句柄,OEP
  9. DWORD dwDebuggeePID \= 0;
  10. //被调试线程句柄
  11. HANDLE hDebuggeeThread \= NULL;
  12. HANDLE hDebuggeeProcess \= NULL;
  13. //系统断点
  14. BOOL bIsSystemInt3 \= TRUE;
  15. //被INT 3覆盖的数据
  16. CHAR OriginalCode \= 0;
  17. //线程上下文
  18. CONTEXT Context;
  19. typedef HANDLE (\_\_stdcall \*FnOpenThread) (DWORD, BOOL, DWORD);
  20. VOID InitDebuggeeInfo(DWORD dwPID, HANDLE hProcess)
  21. {
  22. dwDebuggeePID \= dwPID;
  23. hDebuggeeProcess \= hProcess;
  24. }
  25. DWORD GetProcessId(LPTSTR lpProcessName)
  26. {
  27. HANDLE hProcessSnap \= NULL;
  28. PROCESSENTRY32 pe32 \= {0};
  29. hProcessSnap \= CreateToolhelp32Snapshot(TH32CS\_SNAPPROCESS, 0);
  30. if(hProcessSnap \== (HANDLE)\-1)
  31. {
  32. return 0;
  33. }
  34. pe32.dwSize \= sizeof(PROCESSENTRY32);
  35. if(Process32First(hProcessSnap, &amp;pe32))
  36. {
  37. do
  38. {
  39. if(!strcmp(lpProcessName, pe32.szExeFile))
  40. return (int)pe32.th32ProcessID;
  41. } while (Process32Next(hProcessSnap, &amp;pe32));
  42. }
  43. else
  44. {
  45. CloseHandle(hProcessSnap);
  46. }
  47. return 0;
  48. }
  49. BOOL WaitForUserCommand()
  50. {
  51. BOOL bRet \= FALSE;
  52. CHAR command;
  53. printf("COMMAND > ");
  54. command \= getchar();
  55. switch(command)
  56. {
  57. // into
  58. case 't':
  59. bRet \= TRUE;
  60. break;
  61. // pass
  62. case 'p':
  63. bRet \= TRUE;
  64. break;
  65. // go
  66. case 'g':
  67. bRet \= TRUE;
  68. break;
  69. }
  70. getchar();
  71. return bRet;
  72. }
  73. BOOL Int3ExceptionProc(EXCEPTION\_DEBUG\_INFO \*pExceptionInfo)
  74. {
  75. BOOL bRet \= FALSE;
  76. //1. 将INT 3修复为原来的数据(如果是系统断点,不用修复)
  77. if(bIsSystemInt3)
  78. {
  79. bIsSystemInt3 \= FALSE;
  80. return TRUE;
  81. }
  82. else
  83. {
  84. WriteProcessMemory(hDebuggeeProcess, pExceptionInfo\->ExceptionRecord.ExceptionAddress, &amp;OriginalCode, 1, NULL);
  85. }
  86. //2. 显示断点位置
  87. printf("Int 3断点 : 0x%p \\r\\n", pExceptionInfo\->ExceptionRecord.ExceptionAddress);
  88. //3. 获取线程上下文
  89. Context.ContextFlags \= CONTEXT\_FULL | CONTEXT\_DEBUG\_REGISTERS;
  90. GetThreadContext(hDebuggeeThread, &amp;Context);
  91. //4. 修正EIP
  92. //printf("Eip : %x\\n",Context.Eip);
  93. Context.Eip\--;
  94. SetThreadContext(hDebuggeeThread, &amp;Context);
  95. //5. 显示反汇编代码、寄存器等
  96. //6. 等待用户命令
  97. while(bRet \== FALSE)
  98. {
  99. bRet \= WaitForUserCommand();
  100. }
  101. return bRet;
  102. }
  103. BOOL AccessExceptionProc(EXCEPTION\_DEBUG\_INFO \*pExceptionInfo)
  104. {
  105. BOOL bRet \= TRUE;
  106. return bRet;
  107. }
  108. BOOL SingleStepExceptionProc(EXCEPTION\_DEBUG\_INFO \*pExceptionInfo)
  109. {
  110. BOOL bRet \= TRUE;
  111. return bRet;
  112. }
  113. BOOL ExceptionHandler(DEBUG\_EVENT \*pDebugEvent)
  114. {
  115. BOOL bRet \= TRUE;
  116. EXCEPTION\_DEBUG\_INFO \*pExceptionInfo \= NULL;
  117. pExceptionInfo \= &amp;pDebugEvent\->u.Exception;
  118. //得到线程句柄,后面要用
  119. FnOpenThread MyOpenThread \= (FnOpenThread)GetProcAddress(LoadLibrary("kernel32.dll"), "OpenThread");
  120. hDebuggeeThread \= MyOpenThread(THREAD\_ALL\_ACCESS, FALSE, pDebugEvent\->dwThreadId);
  121. switch(pExceptionInfo\->ExceptionRecord.ExceptionCode)
  122. {
  123. //INT 3异常
  124. case EXCEPTION\_BREAKPOINT:
  125. bRet \= Int3ExceptionProc(pExceptionInfo);
  126. break;
  127. //访问异常
  128. case EXCEPTION\_ACCESS\_VIOLATION:
  129. bRet \= AccessExceptionProc(pExceptionInfo);
  130. break;
  131. //单步执行
  132. case EXCEPTION\_SINGLE\_STEP:
  133. bRet \= SingleStepExceptionProc(pExceptionInfo);
  134. break;
  135. }
  136. return bRet;
  137. }
  138. void SetInt3BreakPoint(LPVOID addr)
  139. {
  140. ReadProcessMemory(hDebuggeeProcess, addr, &amp;OriginalCode, 1, NULL);
  141. BYTE int3\[1\] \= { 0xcc };
  142. WriteProcessMemory(hDebuggeeProcess, addr, int3, 1, NULL);
  143. }
  144. BOOL ExceptionTest()
  145. {
  146. BOOL nIsContinue \= TRUE;
  147. DEBUG\_EVENT debugEvent \= {0};
  148. BOOL bRet \= TRUE;
  149. DWORD dwContinue \= DBG\_CONTINUE;
  150. //1.创建调试进程
  151. STARTUPINFO startupInfo \= {0};
  152. PROCESS\_INFORMATION pInfo \= {0};
  153. GetStartupInfo(&amp;startupInfo);
  154. bRet \= CreateProcess(DEBUGGEE, NULL, NULL, NULL, TRUE, DEBUG\_PROCESS || DEBUG\_ONLY\_THIS\_PROCESS, NULL, NULL, &amp;startupInfo, &amp;pInfo);
  155. if(!bRet)
  156. {
  157. printf("CreateProcess error: %d \\n", GetLastError());
  158. return 0;
  159. }
  160. hDebuggeeProcess \= pInfo.hProcess;
  161. //2.调试循环
  162. while(nIsContinue)
  163. {
  164. bRet \= WaitForDebugEvent(&amp;debugEvent, INFINITE);
  165. if(!bRet)
  166. {
  167. printf("WaitForDebugEvent error: %d \\n", GetLastError());
  168. return 0;
  169. }
  170. switch(debugEvent.dwDebugEventCode)
  171. {
  172. //1.异常
  173. case EXCEPTION\_DEBUG\_EVENT:
  174. bRet \= ExceptionHandler(&amp;debugEvent);
  175. if(!bRet)
  176. dwContinue \= DBG\_EXCEPTION\_NOT\_HANDLED;
  177. break;
  178. //2.
  179. case CREATE\_THREAD\_DEBUG\_EVENT:
  180. break;
  181. //3.创建进程
  182. case CREATE\_PROCESS\_DEBUG\_EVENT:
  183. SetInt3BreakPoint((PCHAR)debugEvent.u.CreateProcessInfo.lpStartAddress);
  184. break;
  185. //4.
  186. case EXIT\_THREAD\_DEBUG\_EVENT:
  187. break;
  188. //5.
  189. case EXIT\_PROCESS\_DEBUG\_EVENT:
  190. break;
  191. //6.
  192. case LOAD\_DLL\_DEBUG\_EVENT:
  193. break;
  194. //7.
  195. case UNLOAD\_DLL\_DEBUG\_EVENT:
  196. break;
  197. //8.
  198. case OUTPUT\_DEBUG\_STRING\_EVENT:
  199. break;
  200. }
  201. bRet \= ContinueDebugEvent(debugEvent.dwProcessId, debugEvent.dwThreadId, DBG\_CONTINUE);
  202. }
  203. return 0;
  204. }
  205. int main(int argc, char\* argv\[\])
  206. {
  207. ExceptionTest();
  208. return 0;
  209. }

实现效果

image-20220401214231280.png

0x02 内存断点

描述:当需要在某块内存被访问时产生中断,可以使用内存断点。

内存断点能够分为两种类型:

内存访问:内存被读写时产生中断。
内存写入:内存被写入时产生中断。

原理:VirtualProtectEx

  1. BOOL VirtualProtectEx(
  2. HANDLE hProcess, // handle to process
  3. LPVOID lpAddress, // region of committed pages
  4. SIZE\_T dwSize, // size of region
  5. DWORD flNewProtect, // desired access protection
  6. PDWORD lpflOldProtect // old protection
  7. );

内存访问:将指定内存的属性修改为PAGE_NOACCESS(修改后,PTE的P位等于0)

内存写入:将指定内存的属性修改为PAGE_EXECUTE_READ(修改后,PTE的P位等于1,R/W位等于0)

流程

被调试进程:

1)CPU访问错误的内存地址,触发页异常
2)查IDT表找到对应的中断处理函数(nt!_KiTrap0E
3)CommonDispatchException
4)KiDispatchException
5)DbgkForwardException收集并发送调试事件

最终调用DbgkpSendApiMessage(x, x) 第一个参数:消息类型,共有7种类型 第二个参数:是否挂起其它线程

调试器进程:

1)循环判断
2)取出调试事件
3)列出消息(寄存器/内存)
4)用户处理

在创建进程的地方使用内存断点

image-20220402154008317.png

通过修改PTE的P=0来设置页不可访问

image-20220402154028491.png

我们首先看一下EXCEPTION_DEBUG_INFO结构

image-20220402154800949.png

然后再看ExceptionRecord

image-20220402154814655.png

定位到_EXCEPTION_RECORD

image-20220402154853465.png

到msdn里面看一下EXCEPTION_RECORD,这里主要关注ExceptionInformation

image-20220402164207943.png

如果这个值为0有线程试图读这块内存,如果这个值为1则有线程试图写这块内存

image-20220402164349679.png

这里显示出异常的信息,打印异常类型和异常地址

image-20220402164529085.png

内存断点的EIP就是原EIP,不需要进行减的操作

image-20220402165412874.png

image-20220402165450796.png

实现代码如下

  1. // Debug4.cpp : Defines the entry point for the console application.
  2. //
  3. #include "stdafx.h"
  4. #include <stdio.h>
  5. #include <windows.h>
  6. #include <tlhelp32.h>
  7. #define DEBUGGEE "C:\\\\ipmsg.exe"
  8. //被调试进程ID,进程句柄,OEP
  9. DWORD dwDebuggeePID \= 0;
  10. //被调试线程句柄
  11. HANDLE hDebuggeeThread \= NULL;
  12. HANDLE hDebuggeeProcess \= NULL;
  13. //系统断点
  14. BOOL bIsSystemInt3 \= TRUE;
  15. //被INT 3覆盖的数据
  16. CHAR OriginalCode \= 0;
  17. //原始内存属性
  18. DWORD dwOriginalProtect;
  19. //线程上下文
  20. CONTEXT Context;
  21. typedef HANDLE (\_\_stdcall \*FnOpenThread) (DWORD, BOOL, DWORD);
  22. VOID InitDebuggeeInfo(DWORD dwPID, HANDLE hProcess)
  23. {
  24. dwDebuggeePID \= dwPID;
  25. hDebuggeeProcess \= hProcess;
  26. }
  27. DWORD GetProcessId(LPTSTR lpProcessName)
  28. {
  29. HANDLE hProcessSnap \= NULL;
  30. PROCESSENTRY32 pe32 \= {0};
  31. hProcessSnap \= CreateToolhelp32Snapshot(TH32CS\_SNAPPROCESS, 0);
  32. if(hProcessSnap \== (HANDLE)\-1)
  33. {
  34. return 0;
  35. }
  36. pe32.dwSize \= sizeof(PROCESSENTRY32);
  37. if(Process32First(hProcessSnap, &amp;pe32))
  38. {
  39. do
  40. {
  41. if(!strcmp(lpProcessName, pe32.szExeFile))
  42. return (int)pe32.th32ProcessID;
  43. } while (Process32Next(hProcessSnap, &amp;pe32));
  44. }
  45. else
  46. {
  47. CloseHandle(hProcessSnap);
  48. }
  49. return 0;
  50. }
  51. BOOL WaitForUserCommand()
  52. {
  53. BOOL bRet \= FALSE;
  54. CHAR command;
  55. printf("COMMAND>");
  56. command \= getchar();
  57. switch(command)
  58. {
  59. case 't':
  60. bRet \= TRUE;
  61. break;
  62. case 'p':
  63. bRet \= TRUE;
  64. break;
  65. case 'g':
  66. bRet \= TRUE;
  67. break;
  68. }
  69. getchar();
  70. return bRet;
  71. }
  72. BOOL Int3ExceptionProc(EXCEPTION\_DEBUG\_INFO \*pExceptionInfo)
  73. {
  74. BOOL bRet \= FALSE;
  75. //1. 将INT 3修复为原来的数据(如果是系统断点,不用修复)
  76. if(bIsSystemInt3)
  77. {
  78. bIsSystemInt3 \= FALSE;
  79. return TRUE;
  80. }
  81. else
  82. {
  83. WriteProcessMemory(hDebuggeeProcess, pExceptionInfo\->ExceptionRecord.ExceptionAddress, &amp;OriginalCode, 1, NULL);
  84. }
  85. //2. 显示断点位置
  86. printf("Int 3断点 : 0x%p \\r\\n", pExceptionInfo\->ExceptionRecord.ExceptionAddress);
  87. //3. 获取线程上下文
  88. Context.ContextFlags \= CONTEXT\_FULL | CONTEXT\_DEBUG\_REGISTERS;
  89. GetThreadContext(hDebuggeeThread, &amp;Context);
  90. //4. 修正EIP
  91. Context.Eip\--;
  92. SetThreadContext(hDebuggeeThread, &amp;Context);
  93. //5. 显示反汇编代码、寄存器等
  94. //6. 等待用户命令
  95. while(bRet \== FALSE)
  96. {
  97. bRet \= WaitForUserCommand();
  98. }
  99. return bRet;
  100. }
  101. BOOL AccessExceptionProc(EXCEPTION\_DEBUG\_INFO \*pExceptionInfo)
  102. {
  103. BOOL bRet \= FALSE;
  104. DWORD dwAccessFlag; //访问类型 0为读 1为写
  105. DWORD dwAccessAddr; //访问地址
  106. DWORD dwProtect; //内存属性
  107. //1. 获取异常信息,修改内存属性
  108. dwAccessFlag \= pExceptionInfo\->ExceptionRecord.ExceptionInformation\[0\];
  109. dwAccessAddr \= pExceptionInfo\->ExceptionRecord.ExceptionInformation\[1\];
  110. printf("内存断点 : dwAccessFlag - %x dwAccessAddr - %x \\n", dwAccessFlag, dwAccessAddr);
  111. VirtualProtectEx(hDebuggeeProcess, (VOID\*)dwAccessAddr, 1, dwOriginalProtect, &amp;dwProtect);
  112. //2. 获取线程上下文
  113. Context.ContextFlags \= CONTEXT\_FULL | CONTEXT\_DEBUG\_REGISTERS;
  114. GetThreadContext(hDebuggeeThread, &amp;Context);
  115. //3. 修正EIP(内存访问异常,不需要修正EIP)
  116. printf("Eip: 0x%p \\n", Context.Eip);
  117. //4. 显示汇编/寄存器等信息
  118. //5. 等待用户命令
  119. while(bRet \== FALSE)
  120. {
  121. bRet \= WaitForUserCommand();
  122. }
  123. return bRet;
  124. }
  125. BOOL SingleStepExceptionProc(EXCEPTION\_DEBUG\_INFO \*pExceptionInfo)
  126. {
  127. BOOL bRet \= TRUE;
  128. return bRet;
  129. }
  130. BOOL ExceptionHandler(DEBUG\_EVENT \*pDebugEvent)
  131. {
  132. BOOL bRet \= TRUE;
  133. EXCEPTION\_DEBUG\_INFO \*pExceptionInfo \= NULL;
  134. pExceptionInfo \= &amp;pDebugEvent\->u.Exception;
  135. //得到线程句柄,后面要用
  136. FnOpenThread MyOpenThread \= (FnOpenThread)GetProcAddress(LoadLibrary("kernel32.dll"), "OpenThread");
  137. hDebuggeeThread \= MyOpenThread(THREAD\_ALL\_ACCESS, FALSE, pDebugEvent\->dwThreadId);
  138. switch(pExceptionInfo\->ExceptionRecord.ExceptionCode)
  139. {
  140. //INT 3异常
  141. case EXCEPTION\_BREAKPOINT:
  142. {
  143. bRet \= Int3ExceptionProc(pExceptionInfo);
  144. break;
  145. }
  146. //访问异常
  147. case EXCEPTION\_ACCESS\_VIOLATION:
  148. bRet \= AccessExceptionProc(pExceptionInfo);
  149. break;
  150. //单步执行
  151. case EXCEPTION\_SINGLE\_STEP:
  152. bRet \= SingleStepExceptionProc(pExceptionInfo);
  153. break;
  154. }
  155. return bRet;
  156. }
  157. VOID SetInt3BreakPoint(LPVOID addr)
  158. {
  159. CHAR int3 \= 0xCC;
  160. //1. 备份
  161. ReadProcessMemory(hDebuggeeProcess, addr, &amp;OriginalCode, 1, NULL);
  162. //2. 修改
  163. WriteProcessMemory(hDebuggeeProcess, addr, &amp;int3, 1, NULL);
  164. }
  165. VOID SetMemBreakPoint(PCHAR pAddress)
  166. {
  167. //1. 访问断点
  168. VirtualProtectEx(hDebuggeeProcess, pAddress, 1, PAGE\_NOACCESS, &amp;dwOriginalProtect); //PTE P=0
  169. //2. 写入断点
  170. //VirtualProtectEx(hDebuggeeProcess, pAddress, 1, PAGE\_EXECUTE\_READ, &amp;dwOriginalProtect); //PTE R/W=0
  171. }
  172. int main(int argc, char\* argv\[\])
  173. {
  174. BOOL nIsContinue \= TRUE;
  175. DEBUG\_EVENT debugEvent \= {0};
  176. BOOL bRet \= TRUE;
  177. DWORD dwContinue \= DBG\_CONTINUE;
  178. //1.创建调试进程
  179. STARTUPINFO startupInfo \= {0};
  180. PROCESS\_INFORMATION pInfo \= {0};
  181. GetStartupInfo(&amp;startupInfo);
  182. bRet \= CreateProcess(DEBUGGEE, NULL, NULL, NULL, TRUE, DEBUG\_PROCESS || DEBUG\_ONLY\_THIS\_PROCESS, NULL, NULL, &amp;startupInfo, &amp;pInfo);
  183. if(!bRet)
  184. {
  185. printf("CreateProcess error: %d \\n", GetLastError());
  186. return 0;
  187. }
  188. hDebuggeeProcess \= pInfo.hProcess;
  189. //2.调试循环
  190. while(nIsContinue)
  191. {
  192. bRet \= WaitForDebugEvent(&amp;debugEvent, INFINITE);
  193. if(!bRet)
  194. {
  195. printf("WaitForDebugEvent error: %d \\n", GetLastError());
  196. return 0;
  197. }
  198. switch(debugEvent.dwDebugEventCode)
  199. {
  200. //1.异常
  201. case EXCEPTION\_DEBUG\_EVENT:
  202. bRet \= ExceptionHandler(&amp;debugEvent);
  203. if(!bRet)
  204. dwContinue \= DBG\_EXCEPTION\_NOT\_HANDLED;
  205. break;
  206. //2.
  207. case CREATE\_THREAD\_DEBUG\_EVENT:
  208. break;
  209. //3.创建进程
  210. case CREATE\_PROCESS\_DEBUG\_EVENT:
  211. //int3 断点
  212. //SetInt3BreakPoint((PCHAR)debugEvent.u.CreateProcessInfo.lpStartAddress);
  213. //内存断点
  214. SetMemBreakPoint((PCHAR)debugEvent.u.CreateProcessInfo.lpStartAddress);
  215. break;
  216. //4.
  217. case EXIT\_THREAD\_DEBUG\_EVENT:
  218. break;
  219. //5.
  220. case EXIT\_PROCESS\_DEBUG\_EVENT:
  221. break;
  222. //6.
  223. case LOAD\_DLL\_DEBUG\_EVENT:
  224. break;
  225. //7.
  226. case UNLOAD\_DLL\_DEBUG\_EVENT:
  227. break;
  228. //8.
  229. case OUTPUT\_DEBUG\_STRING\_EVENT:
  230. break;
  231. }
  232. bRet \= ContinueDebugEvent(debugEvent.dwProcessId, debugEvent.dwThreadId, DBG\_CONTINUE);
  233. }
  234. return 0;
  235. }

实现效果如下

image-20220402165639018.png

0x03 硬件断点

  1. 与软件断点与内存断点不同,硬件断点不依赖被调试程序,而是依赖于CPU中的调试寄存器
  2. 调试寄存器有7个,分别为Dr0~Dr7
  3. 用户最多能够设置4个硬件断点,这是由于只有Dr0~Dr3用于存储线性地址。
  4. 其中,Dr4和Dr5是保留的。

image-20220402185424231.png

那么假如在Dr0寄存器中写入线性地址,是否所有线程都会受影响?

实际上是不会的,每个线程都拥有一份独立的寄存器,切换线程时,寄存器的值也会被切换。

设置硬件断点

Dr0~Dr3用于设置硬件断点,由于只有4个断点寄存器,所以最多只能设置4个硬件调试断点,在7个寄存器中,Dr7是最重要的寄存器

L0/G0 ~ L3/G3:控制Dr0~Dr3是否有效,局部还是全局;每次异常后,Lx都被清零,Gx不清零。

若Dr0有效,L0=1则为局部,G0=1则为全局,以此类推

image-20220402213248841.png
断点长度(LENx):00(1字节)、01(2字节)、11(4字节)

通过DR7的LEN控制

image-20220402213410805.png

断点类型(R/Wx):00(执行断点)、01(写入断点)、11(访问断点)

image-20220402213439641.png

流程

被调试进程:

1)CPU执行时检测当前线性地址与调试寄存器(Dr0~Dr3)中的线性地址相等。 >2)查IDT表找到对应的中断处理函数(nt!_KiTrap01
3)CommonDispatchException
4)KiDispatchException
5)DbgkForwardException收集并发送调试事件

最终调用DbgkpSendApiMessage(x, x) 第一个参数:消息类型 第二个参数:是否挂起其它线程

调试器进程:

1)循环判断
2)取出调试事件
3)列出信息:寄存器、内存
4)用户处理

处理硬件断点

1)硬件调试断点产生的异常是 STATUS_SINGLE_STEP(单步异常)
2)检测Dr6寄存器的B0~B3:哪个寄存器触发的异常

这里硬件断点有两种情况,一种情况是dr0-dr3寄存器引发的异常,另外一种情况就是TF=1引发的异常

image-20220402215101783.png

这里如果是DR0寄存器引发的异常,那么B0=1,以此类推 如果是TF=1引发的异常,那么DR6的低4位为全0

首先看一下异常处理函数

  1. BOOL SingleStepExceptionProc(EXCEPTION\_DEBUG\_INFO \*pExceptionInfo)
  2. {
  3. BOOL bRet \= FALSE;
  4. //1. 获取线程上下文
  5. Context.ContextFlags \= CONTEXT\_FULL | CONTEXT\_DEBUG\_REGISTERS;
  6. GetThreadContext(hDebuggeeThread, &amp;Context);
  7. //2. 判断是否是硬件断点导致的异常
  8. if(Context.Dr6 &amp; 0xF) //B0~B3不为空 硬件断点
  9. {
  10. //2.1 显示断点信息
  11. printf("硬件断点:%x 0x%p \\n", Context.Dr7&amp;0x00030000, Context.Dr0);
  12. //2.2 将断点去除
  13. Context.Dr0 \= 0;
  14. Context.Dr7 &amp;= 0xfffffffe;
  15. }
  16. else //单步异常
  17. {
  18. //2.1 显示断点信息
  19. printf("单步:0x%p \\n", Context.Eip);
  20. //2.2 将断点去除
  21. Context.Dr7 &amp;= 0xfffffeff;
  22. }
  23. SetThreadContext(hDebuggeeThread, &amp;Context);
  24. // 等待用户命令
  25. while(bRet \== FALSE)
  26. {
  27. bRet \= WaitForUserCommand();
  28. }
  29. return bRet;
  30. }

之前我们是在创建进程的时候进行断点,但是因为硬件断点需要在线程创建完成之后,设置在被调试程序的上下文中

image-20220402214947584.png

因此当被调试程序触发调试器设置的INT 3断点时,此时设置硬件断点较为合理

image-20220402215101783.png

再就是硬件断点的代码,这里把Dr0寄存器置1,然后把16、17为置0为执行断点,异常长度为1字节(18、19位置0),地址的话就是int3断点的地址+1

  1. VOID SetHardBreakPoint(PVOID pAddress)
  2. {
  3. //1. 获取线程上下文
  4. Context.ContextFlags \= CONTEXT\_FULL | CONTEXT\_DEBUG\_REGISTERS;
  5. GetThreadContext(hDebuggeeThread, &amp;Context);
  6. //2. 设置断点位置
  7. Context.Dr0 \= (DWORD)pAddress;
  8. Context.Dr7 |= 1;
  9. //3. 设置断点长度和类型
  10. Context.Dr7 &amp;= 0xfff0ffff; //执行断点(16、17位 置0) 1字节(18、19位 置0)
  11. //5. 设置线程上下文
  12. SetThreadContext(hDebuggeeThread, &amp;Context);
  13. }

完整代码如下

  1. // Debug4.cpp : Defines the entry point for the console application.
  2. //
  3. #include "stdafx.h"
  4. #include <stdio.h>
  5. #include <windows.h>
  6. #include <tlhelp32.h>
  7. #define DEBUGGEE "C:\\\\ipmsg.exe"
  8. //被调试进程ID,进程句柄,OEP
  9. DWORD dwDebuggeePID \= 0;
  10. //被调试线程句柄
  11. HANDLE hDebuggeeThread \= NULL;
  12. HANDLE hDebuggeeProcess \= NULL;
  13. //系统断点
  14. BOOL bIsSystemInt3 \= TRUE;
  15. //被INT 3覆盖的数据
  16. CHAR OriginalCode \= 0;
  17. //原始内存属性
  18. DWORD dwOriginalProtect;
  19. //线程上下文
  20. CONTEXT Context;
  21. typedef HANDLE (\_\_stdcall \*FnOpenThread) (DWORD, BOOL, DWORD);
  22. VOID InitDebuggeeInfo(DWORD dwPID, HANDLE hProcess)
  23. {
  24. dwDebuggeePID \= dwPID;
  25. hDebuggeeProcess \= hProcess;
  26. }
  27. DWORD GetProcessId(LPTSTR lpProcessName)
  28. {
  29. HANDLE hProcessSnap \= NULL;
  30. PROCESSENTRY32 pe32 \= {0};
  31. hProcessSnap \= CreateToolhelp32Snapshot(TH32CS\_SNAPPROCESS, 0);
  32. if(hProcessSnap \== (HANDLE)\-1)
  33. {
  34. return 0;
  35. }
  36. pe32.dwSize \= sizeof(PROCESSENTRY32);
  37. if(Process32First(hProcessSnap, &amp;pe32))
  38. {
  39. do
  40. {
  41. if(!strcmp(lpProcessName, pe32.szExeFile))
  42. return (int)pe32.th32ProcessID;
  43. } while (Process32Next(hProcessSnap, &amp;pe32));
  44. }
  45. else
  46. {
  47. CloseHandle(hProcessSnap);
  48. }
  49. return 0;
  50. }
  51. BOOL WaitForUserCommand()
  52. {
  53. BOOL bRet \= FALSE;
  54. CHAR command;
  55. printf("COMMAND>");
  56. command \= getchar();
  57. switch(command)
  58. {
  59. case 't':
  60. bRet \= TRUE;
  61. break;
  62. case 'p':
  63. bRet \= TRUE;
  64. break;
  65. case 'g':
  66. bRet \= TRUE;
  67. break;
  68. }
  69. getchar();
  70. return bRet;
  71. }
  72. VOID SetHardBreakPoint(PVOID pAddress)
  73. {
  74. //1. 获取线程上下文
  75. Context.ContextFlags \= CONTEXT\_FULL | CONTEXT\_DEBUG\_REGISTERS;
  76. GetThreadContext(hDebuggeeThread, &amp;Context);
  77. //2. 设置断点位置
  78. Context.Dr0 \= (DWORD)pAddress;
  79. Context.Dr7 |= 1;
  80. //3. 设置断点长度和类型
  81. Context.Dr7 &amp;= 0xfff0ffff; //执行断点(16、17位 置0) 1字节(18、19位 置0)
  82. //5. 设置线程上下文
  83. SetThreadContext(hDebuggeeThread, &amp;Context);
  84. }
  85. BOOL Int3ExceptionProc(EXCEPTION\_DEBUG\_INFO \*pExceptionInfo)
  86. {
  87. BOOL bRet \= FALSE;
  88. //1. 将INT 3修复为原来的数据(如果是系统断点,不用修复)
  89. if(bIsSystemInt3)
  90. {
  91. bIsSystemInt3 \= FALSE;
  92. return TRUE;
  93. }
  94. else
  95. {
  96. WriteProcessMemory(hDebuggeeProcess, pExceptionInfo\->ExceptionRecord.ExceptionAddress, &amp;OriginalCode, 1, NULL);
  97. }
  98. //2. 显示断点位置
  99. printf("Int 3断点:0x%p \\r\\n", pExceptionInfo\->ExceptionRecord.ExceptionAddress);
  100. //3. 获取线程上下文
  101. Context.ContextFlags \= CONTEXT\_FULL | CONTEXT\_DEBUG\_REGISTERS;
  102. GetThreadContext(hDebuggeeThread, &amp;Context);
  103. //4. 修正EIP
  104. Context.Eip\--;
  105. SetThreadContext(hDebuggeeThread, &amp;Context);
  106. //5. 显示反汇编代码、寄存器等
  107. /\*
  108. 硬件断点需要设置在被调试进程的的线程上下文中。
  109. 因此当被调试程序触发调试器设置的INT 3断点时,此时设置硬件断点较为合理。
  110. \*/
  111. SetHardBreakPoint((PVOID)((DWORD)pExceptionInfo\->ExceptionRecord.ExceptionAddress+1));
  112. //6. 等待用户命令
  113. while(bRet \== FALSE)
  114. {
  115. bRet \= WaitForUserCommand();
  116. }
  117. return bRet;
  118. }
  119. BOOL AccessExceptionProc(EXCEPTION\_DEBUG\_INFO \*pExceptionInfo)
  120. {
  121. BOOL bRet \= FALSE;
  122. DWORD dwAccessFlag; //访问类型 0为读 1为写
  123. DWORD dwAccessAddr; //访问地址
  124. DWORD dwProtect; //内存属性
  125. //1. 获取异常信息,修改内存属性
  126. dwAccessFlag \= pExceptionInfo\->ExceptionRecord.ExceptionInformation\[0\];
  127. dwAccessAddr \= pExceptionInfo\->ExceptionRecord.ExceptionInformation\[1\];
  128. printf("内存断点 : dwAccessFlag - %x dwAccessAddr - %x \\n", dwAccessFlag, dwAccessAddr);
  129. VirtualProtectEx(hDebuggeeProcess, (VOID\*)dwAccessAddr, 1, dwOriginalProtect, &amp;dwProtect);
  130. //2. 获取线程上下文
  131. Context.ContextFlags \= CONTEXT\_FULL | CONTEXT\_DEBUG\_REGISTERS;
  132. GetThreadContext(hDebuggeeThread, &amp;Context);
  133. //3. 修正EIP(内存访问异常,不需要修正EIP)
  134. printf("Eip: 0x%p \\n", Context.Eip);
  135. //4. 显示汇编/寄存器等信息
  136. //5. 等待用户命令
  137. while(bRet \== FALSE)
  138. {
  139. bRet \= WaitForUserCommand();
  140. }
  141. return bRet;
  142. }
  143. BOOL SingleStepExceptionProc(EXCEPTION\_DEBUG\_INFO \*pExceptionInfo)
  144. {
  145. BOOL bRet \= FALSE;
  146. //1. 获取线程上下文
  147. Context.ContextFlags \= CONTEXT\_FULL | CONTEXT\_DEBUG\_REGISTERS;
  148. GetThreadContext(hDebuggeeThread, &amp;Context);
  149. //2. 判断是否是硬件断点导致的异常
  150. if(Context.Dr6 &amp; 0xF) //B0~B3不为空 硬件断点
  151. {
  152. //2.1 显示断点信息
  153. printf("硬件断点:%x 0x%p \\n", Context.Dr7&amp;0x00030000, Context.Dr0);
  154. //2.2 将断点去除
  155. Context.Dr0 \= 0;
  156. Context.Dr7 &amp;= 0xfffffffe;
  157. }
  158. else //单步异常
  159. {
  160. //2.1 显示断点信息
  161. printf("单步:0x%p \\n", Context.Eip);
  162. //2.2 将断点去除
  163. Context.Dr7 &amp;= 0xfffffeff;
  164. }
  165. SetThreadContext(hDebuggeeThread, &amp;Context);
  166. //6. 等待用户命令
  167. while(bRet \== FALSE)
  168. {
  169. bRet \= WaitForUserCommand();
  170. }
  171. return bRet;
  172. }
  173. BOOL ExceptionHandler(DEBUG\_EVENT \*pDebugEvent)
  174. {
  175. BOOL bRet \= TRUE;
  176. EXCEPTION\_DEBUG\_INFO \*pExceptionInfo \= NULL;
  177. pExceptionInfo \= &amp;pDebugEvent\->u.Exception;
  178. //得到线程句柄,后面要用
  179. FnOpenThread MyOpenThread \= (FnOpenThread)GetProcAddress(LoadLibrary("kernel32.dll"), "OpenThread");
  180. hDebuggeeThread \= MyOpenThread(THREAD\_ALL\_ACCESS, FALSE, pDebugEvent\->dwThreadId);
  181. switch(pExceptionInfo\->ExceptionRecord.ExceptionCode)
  182. {
  183. //INT 3异常
  184. case EXCEPTION\_BREAKPOINT:
  185. bRet \= Int3ExceptionProc(pExceptionInfo);
  186. break;
  187. //访问异常
  188. case EXCEPTION\_ACCESS\_VIOLATION:
  189. bRet \= AccessExceptionProc(pExceptionInfo);
  190. break;
  191. //单步执行
  192. case EXCEPTION\_SINGLE\_STEP:
  193. bRet \= SingleStepExceptionProc(pExceptionInfo);
  194. break;
  195. }
  196. return bRet;
  197. }
  198. VOID SetInt3BreakPoint(LPVOID addr)
  199. {
  200. CHAR int3 \= 0xCC;
  201. //1. 备份
  202. ReadProcessMemory(hDebuggeeProcess, addr, &amp;OriginalCode, 1, NULL);
  203. //2. 修改
  204. WriteProcessMemory(hDebuggeeProcess, addr, &amp;int3, 1, NULL);
  205. }
  206. VOID SetMemBreakPoint(PCHAR pAddress)
  207. {
  208. //1. 访问断点
  209. VirtualProtectEx(hDebuggeeProcess, pAddress, 1, PAGE\_NOACCESS, &amp;dwOriginalProtect); //PTE P=0
  210. //2. 写入断点
  211. //VirtualProtectEx(hDebuggeeProcess, pAddress, 1, PAGE\_EXECUTE\_READ, &amp;dwOriginalProtect); //PTE R/W=0
  212. }
  213. int main(int argc, char\* argv\[\])
  214. {
  215. BOOL nIsContinue \= TRUE;
  216. DEBUG\_EVENT debugEvent \= {0};
  217. BOOL bRet \= TRUE;
  218. DWORD dwContinue \= DBG\_CONTINUE;
  219. //1.创建调试进程
  220. STARTUPINFO startupInfo \= {0};
  221. PROCESS\_INFORMATION pInfo \= {0};
  222. GetStartupInfo(&amp;startupInfo);
  223. bRet \= CreateProcess(DEBUGGEE, NULL, NULL, NULL, TRUE, DEBUG\_PROCESS || DEBUG\_ONLY\_THIS\_PROCESS, NULL, NULL, &amp;startupInfo, &amp;pInfo);
  224. if(!bRet)
  225. {
  226. printf("CreateProcess error: %d \\n", GetLastError());
  227. return 0;
  228. }
  229. hDebuggeeProcess \= pInfo.hProcess;
  230. //2.调试循环
  231. while(nIsContinue)
  232. {
  233. bRet \= WaitForDebugEvent(&amp;debugEvent, INFINITE);
  234. if(!bRet)
  235. {
  236. printf("WaitForDebugEvent error: %d \\n", GetLastError());
  237. return 0;
  238. }
  239. switch(debugEvent.dwDebugEventCode)
  240. {
  241. //1.异常
  242. case EXCEPTION\_DEBUG\_EVENT:
  243. bRet \= ExceptionHandler(&amp;debugEvent);
  244. if(!bRet)
  245. dwContinue \= DBG\_EXCEPTION\_NOT\_HANDLED;
  246. break;
  247. //2.
  248. case CREATE\_THREAD\_DEBUG\_EVENT:
  249. break;
  250. //3.创建进程
  251. case CREATE\_PROCESS\_DEBUG\_EVENT:
  252. //int3 断点
  253. SetInt3BreakPoint((PCHAR)debugEvent.u.CreateProcessInfo.lpStartAddress);
  254. //内存断点
  255. //SetMemBreakPoint((PCHAR)debugEvent.u.CreateProcessInfo.lpStartAddress);
  256. break;
  257. //4.
  258. case EXIT\_THREAD\_DEBUG\_EVENT:
  259. break;
  260. //5.
  261. case EXIT\_PROCESS\_DEBUG\_EVENT:
  262. break;
  263. //6.
  264. case LOAD\_DLL\_DEBUG\_EVENT:
  265. break;
  266. //7.
  267. case UNLOAD\_DLL\_DEBUG\_EVENT:
  268. break;
  269. //8.
  270. case OUTPUT\_DEBUG\_STRING\_EVENT:
  271. break;
  272. }
  273. bRet \= ContinueDebugEvent(debugEvent.dwProcessId, debugEvent.dwThreadId, DBG\_CONTINUE);
  274. }
  275. return 0;
  276. }

实现效果如下

image-20220402215902212.png

  • 发表于 2022-04-12 09:40:45
  • 阅读 ( 6530 )
  • 分类:漏洞分析

0 条评论

szbuffer
szbuffer

30 篇文章

站长统计