Drunkmars's Blog

windows环境下的调试器探究

字数统计: 4.6k阅读时长: 21 min
2022/04/01

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

软件断点

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

image-20220401163633170

被调试进程

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

首先找到IDT表的3号中断

image-20220401173115746

调用CommonDispatchException

image-20220401173221221

通过KiDispatchException分发异常

image-20220401173322672

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

image-20220401181028062

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

image-20220401181427103

然后调用调试事件

image-20220401181849466

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

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

image-20220401182810735

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

image-20220401210427370

如果是异常事件则调用ExceptionHandler

image-20220401210528898

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

image-20220401210654430

image-20220401212239579

image-20220401212317494

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

image-20220401212500170

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

image-20220401212831739

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

image-20220401213324112

然后调用处理的函数

image-20220401213713782

image-20220401213725277

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

image-20220401213954580

实现代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
// Debug4.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"
#include <stdio.h>
#include <windows.h>
#include <tlhelp32.h>

#define DEBUGGEE "C:\\ipmsg.exe"

//被调试进程ID,进程句柄,OEP
DWORD dwDebuggeePID = 0;

//被调试线程句柄
HANDLE hDebuggeeThread = NULL;
HANDLE hDebuggeeProcess = NULL;

//系统断点
BOOL bIsSystemInt3 = TRUE;

//被INT 3覆盖的数据
CHAR OriginalCode = 0;

//线程上下文
CONTEXT Context;

typedef HANDLE (__stdcall *FnOpenThread) (DWORD, BOOL, DWORD);

VOID InitDebuggeeInfo(DWORD dwPID, HANDLE hProcess)
{
dwDebuggeePID = dwPID;
hDebuggeeProcess = hProcess;
}

DWORD GetProcessId(LPTSTR lpProcessName)
{
HANDLE hProcessSnap = NULL;
PROCESSENTRY32 pe32 = {0};

hProcessSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if(hProcessSnap == (HANDLE)-1)
{
return 0;
}

pe32.dwSize = sizeof(PROCESSENTRY32);

if(Process32First(hProcessSnap, &pe32))
{
do
{
if(!strcmp(lpProcessName, pe32.szExeFile))
return (int)pe32.th32ProcessID;
} while (Process32Next(hProcessSnap, &pe32));
}
else
{
CloseHandle(hProcessSnap);
}

return 0;
}

BOOL WaitForUserCommand()
{
BOOL bRet = FALSE;
CHAR command;

printf("COMMAND > ");

command = getchar();

switch(command)
{
// into
case 't':
bRet = TRUE;
break;
// pass
case 'p':
bRet = TRUE;
break;
// go
case 'g':
bRet = TRUE;
break;
}

getchar();
return bRet;
}

BOOL Int3ExceptionProc(EXCEPTION_DEBUG_INFO *pExceptionInfo)
{
BOOL bRet = FALSE;

//1. 将INT 3修复为原来的数据(如果是系统断点,不用修复)

if(bIsSystemInt3)
{
bIsSystemInt3 = FALSE;
return TRUE;
}

else
{
WriteProcessMemory(hDebuggeeProcess, pExceptionInfo->ExceptionRecord.ExceptionAddress, &OriginalCode, 1, NULL);
}

//2. 显示断点位置
printf("Int 3断点 : 0x%p \r\n", pExceptionInfo->ExceptionRecord.ExceptionAddress);

//3. 获取线程上下文
Context.ContextFlags = CONTEXT_FULL | CONTEXT_DEBUG_REGISTERS;
GetThreadContext(hDebuggeeThread, &Context);

//4. 修正EIP
//printf("Eip : %x\n",Context.Eip);
Context.Eip--;
SetThreadContext(hDebuggeeThread, &Context);

//5. 显示反汇编代码、寄存器等

//6. 等待用户命令
while(bRet == FALSE)
{
bRet = WaitForUserCommand();
}

return bRet;
}

BOOL AccessExceptionProc(EXCEPTION_DEBUG_INFO *pExceptionInfo)
{
BOOL bRet = TRUE;

return bRet;
}

BOOL SingleStepExceptionProc(EXCEPTION_DEBUG_INFO *pExceptionInfo)
{
BOOL bRet = TRUE;

return bRet;
}

BOOL ExceptionHandler(DEBUG_EVENT *pDebugEvent)
{
BOOL bRet = TRUE;
EXCEPTION_DEBUG_INFO *pExceptionInfo = NULL;
pExceptionInfo = &pDebugEvent->u.Exception;

//得到线程句柄,后面要用
FnOpenThread MyOpenThread = (FnOpenThread)GetProcAddress(LoadLibrary("kernel32.dll"), "OpenThread");
hDebuggeeThread = MyOpenThread(THREAD_ALL_ACCESS, FALSE, pDebugEvent->dwThreadId);

switch(pExceptionInfo->ExceptionRecord.ExceptionCode)
{
//INT 3异常
case EXCEPTION_BREAKPOINT:
bRet = Int3ExceptionProc(pExceptionInfo);
break;

//访问异常
case EXCEPTION_ACCESS_VIOLATION:
bRet = AccessExceptionProc(pExceptionInfo);
break;

//单步执行
case EXCEPTION_SINGLE_STEP:
bRet = SingleStepExceptionProc(pExceptionInfo);
break;
}

return bRet;
}

void SetInt3BreakPoint(LPVOID addr)
{
ReadProcessMemory(hDebuggeeProcess, addr, &OriginalCode, 1, NULL);

BYTE int3[1] = { 0xcc };

WriteProcessMemory(hDebuggeeProcess, addr, int3, 1, NULL);
}

BOOL ExceptionTest()
{
BOOL nIsContinue = TRUE;
DEBUG_EVENT debugEvent = {0};
BOOL bRet = TRUE;
DWORD dwContinue = DBG_CONTINUE;

//1.创建调试进程
STARTUPINFO startupInfo = {0};
PROCESS_INFORMATION pInfo = {0};
GetStartupInfo(&startupInfo);

bRet = CreateProcess(DEBUGGEE, NULL, NULL, NULL, TRUE, DEBUG_PROCESS || DEBUG_ONLY_THIS_PROCESS, NULL, NULL, &startupInfo, &pInfo);

if(!bRet)
{
printf("CreateProcess error: %d \n", GetLastError());
return 0;
}

hDebuggeeProcess = pInfo.hProcess;

//2.调试循环
while(nIsContinue)
{
bRet = WaitForDebugEvent(&debugEvent, INFINITE);

if(!bRet)
{
printf("WaitForDebugEvent error: %d \n", GetLastError());
return 0;
}

switch(debugEvent.dwDebugEventCode)
{
//1.异常
case EXCEPTION_DEBUG_EVENT:
bRet = ExceptionHandler(&debugEvent);
if(!bRet)
dwContinue = DBG_EXCEPTION_NOT_HANDLED;
break;
//2.
case CREATE_THREAD_DEBUG_EVENT:
break;
//3.创建进程
case CREATE_PROCESS_DEBUG_EVENT:
SetInt3BreakPoint((PCHAR)debugEvent.u.CreateProcessInfo.lpStartAddress);
break;
//4.
case EXIT_THREAD_DEBUG_EVENT:
break;
//5.
case EXIT_PROCESS_DEBUG_EVENT:
break;
//6.
case LOAD_DLL_DEBUG_EVENT:
break;
//7.
case UNLOAD_DLL_DEBUG_EVENT:
break;
//8.
case OUTPUT_DEBUG_STRING_EVENT:
break;
}

bRet = ContinueDebugEvent(debugEvent.dwProcessId, debugEvent.dwThreadId, DBG_CONTINUE);
}

return 0;
}

int main(int argc, char* argv[])
{
ExceptionTest();

return 0;
}

实现效果

image-20220401214231280

内存断点

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

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

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

原理:VirtualProtectEx

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

flNewProtect

内存访问:将指定内存的属性修改为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

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

image-20220402154028491

我们首先看一下EXCEPTION_DEBUG_INFO结构

image-20220402154800949

然后再看ExceptionRecord

image-20220402154814655

定位到_EXCEPTION_RECORD

image-20220402154853465

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

image-20220402164207943

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

image-20220402164349679

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

image-20220402164529085

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

image-20220402165412874

image-20220402165450796

实现代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
// Debug4.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"
#include <stdio.h>
#include <windows.h>
#include <tlhelp32.h>

#define DEBUGGEE "C:\\ipmsg.exe"

//被调试进程ID,进程句柄,OEP
DWORD dwDebuggeePID = 0;

//被调试线程句柄
HANDLE hDebuggeeThread = NULL;
HANDLE hDebuggeeProcess = NULL;

//系统断点
BOOL bIsSystemInt3 = TRUE;

//被INT 3覆盖的数据
CHAR OriginalCode = 0;

//原始内存属性
DWORD dwOriginalProtect;

//线程上下文
CONTEXT Context;

typedef HANDLE (__stdcall *FnOpenThread) (DWORD, BOOL, DWORD);

VOID InitDebuggeeInfo(DWORD dwPID, HANDLE hProcess)
{
dwDebuggeePID = dwPID;
hDebuggeeProcess = hProcess;
}

DWORD GetProcessId(LPTSTR lpProcessName)
{
HANDLE hProcessSnap = NULL;
PROCESSENTRY32 pe32 = {0};

hProcessSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if(hProcessSnap == (HANDLE)-1)
{
return 0;
}

pe32.dwSize = sizeof(PROCESSENTRY32);

if(Process32First(hProcessSnap, &pe32))
{
do
{
if(!strcmp(lpProcessName, pe32.szExeFile))
return (int)pe32.th32ProcessID;
} while (Process32Next(hProcessSnap, &pe32));
}
else
{
CloseHandle(hProcessSnap);
}

return 0;
}

BOOL WaitForUserCommand()
{
BOOL bRet = FALSE;
CHAR command;

printf("COMMAND>");

command = getchar();

switch(command)
{
case 't':
bRet = TRUE;
break;
case 'p':
bRet = TRUE;
break;
case 'g':
bRet = TRUE;
break;
}

getchar();
return bRet;
}

BOOL Int3ExceptionProc(EXCEPTION_DEBUG_INFO *pExceptionInfo)
{
BOOL bRet = FALSE;

//1. 将INT 3修复为原来的数据(如果是系统断点,不用修复)
if(bIsSystemInt3)
{
bIsSystemInt3 = FALSE;
return TRUE;
}
else
{
WriteProcessMemory(hDebuggeeProcess, pExceptionInfo->ExceptionRecord.ExceptionAddress, &OriginalCode, 1, NULL);
}

//2. 显示断点位置
printf("Int 3断点 : 0x%p \r\n", pExceptionInfo->ExceptionRecord.ExceptionAddress);

//3. 获取线程上下文
Context.ContextFlags = CONTEXT_FULL | CONTEXT_DEBUG_REGISTERS;
GetThreadContext(hDebuggeeThread, &Context);

//4. 修正EIP
Context.Eip--;
SetThreadContext(hDebuggeeThread, &Context);

//5. 显示反汇编代码、寄存器等

//6. 等待用户命令
while(bRet == FALSE)
{
bRet = WaitForUserCommand();
}

return bRet;
}

BOOL AccessExceptionProc(EXCEPTION_DEBUG_INFO *pExceptionInfo)
{
BOOL bRet = FALSE;
DWORD dwAccessFlag; //访问类型 0为读 1为写
DWORD dwAccessAddr; //访问地址
DWORD dwProtect; //内存属性

//1. 获取异常信息,修改内存属性
dwAccessFlag = pExceptionInfo->ExceptionRecord.ExceptionInformation[0];
dwAccessAddr = pExceptionInfo->ExceptionRecord.ExceptionInformation[1];
printf("内存断点 : dwAccessFlag - %x dwAccessAddr - %x \n", dwAccessFlag, dwAccessAddr);
VirtualProtectEx(hDebuggeeProcess, (VOID*)dwAccessAddr, 1, dwOriginalProtect, &dwProtect);

//2. 获取线程上下文
Context.ContextFlags = CONTEXT_FULL | CONTEXT_DEBUG_REGISTERS;
GetThreadContext(hDebuggeeThread, &Context);
//3. 修正EIP(内存访问异常,不需要修正EIP)
printf("Eip: 0x%p \n", Context.Eip);
//4. 显示汇编/寄存器等信息
//5. 等待用户命令
while(bRet == FALSE)
{
bRet = WaitForUserCommand();
}

return bRet;
}

BOOL SingleStepExceptionProc(EXCEPTION_DEBUG_INFO *pExceptionInfo)
{
BOOL bRet = TRUE;

return bRet;
}

BOOL ExceptionHandler(DEBUG_EVENT *pDebugEvent)
{
BOOL bRet = TRUE;
EXCEPTION_DEBUG_INFO *pExceptionInfo = NULL;

pExceptionInfo = &pDebugEvent->u.Exception;

//得到线程句柄,后面要用
FnOpenThread MyOpenThread = (FnOpenThread)GetProcAddress(LoadLibrary("kernel32.dll"), "OpenThread");
hDebuggeeThread = MyOpenThread(THREAD_ALL_ACCESS, FALSE, pDebugEvent->dwThreadId);

switch(pExceptionInfo->ExceptionRecord.ExceptionCode)
{
//INT 3异常
case EXCEPTION_BREAKPOINT:
{
bRet = Int3ExceptionProc(pExceptionInfo);
break;
}
//访问异常
case EXCEPTION_ACCESS_VIOLATION:
bRet = AccessExceptionProc(pExceptionInfo);
break;
//单步执行
case EXCEPTION_SINGLE_STEP:
bRet = SingleStepExceptionProc(pExceptionInfo);
break;
}

return bRet;
}

VOID SetInt3BreakPoint(LPVOID addr)
{
CHAR int3 = 0xCC;

//1. 备份
ReadProcessMemory(hDebuggeeProcess, addr, &OriginalCode, 1, NULL);
//2. 修改
WriteProcessMemory(hDebuggeeProcess, addr, &int3, 1, NULL);
}

VOID SetMemBreakPoint(PCHAR pAddress)
{
//1. 访问断点
VirtualProtectEx(hDebuggeeProcess, pAddress, 1, PAGE_NOACCESS, &dwOriginalProtect); //PTE P=0
//2. 写入断点
//VirtualProtectEx(hDebuggeeProcess, pAddress, 1, PAGE_EXECUTE_READ, &dwOriginalProtect); //PTE R/W=0
}

int main(int argc, char* argv[])
{
BOOL nIsContinue = TRUE;
DEBUG_EVENT debugEvent = {0};
BOOL bRet = TRUE;
DWORD dwContinue = DBG_CONTINUE;

//1.创建调试进程
STARTUPINFO startupInfo = {0};
PROCESS_INFORMATION pInfo = {0};
GetStartupInfo(&startupInfo);

bRet = CreateProcess(DEBUGGEE, NULL, NULL, NULL, TRUE, DEBUG_PROCESS || DEBUG_ONLY_THIS_PROCESS, NULL, NULL, &startupInfo, &pInfo);
if(!bRet)
{
printf("CreateProcess error: %d \n", GetLastError());
return 0;
}

hDebuggeeProcess = pInfo.hProcess;

//2.调试循环
while(nIsContinue)
{
bRet = WaitForDebugEvent(&debugEvent, INFINITE);
if(!bRet)
{
printf("WaitForDebugEvent error: %d \n", GetLastError());
return 0;
}

switch(debugEvent.dwDebugEventCode)
{
//1.异常
case EXCEPTION_DEBUG_EVENT:
bRet = ExceptionHandler(&debugEvent);
if(!bRet)
dwContinue = DBG_EXCEPTION_NOT_HANDLED;
break;
//2.
case CREATE_THREAD_DEBUG_EVENT:
break;
//3.创建进程
case CREATE_PROCESS_DEBUG_EVENT:
//int3 断点
//SetInt3BreakPoint((PCHAR)debugEvent.u.CreateProcessInfo.lpStartAddress);
//内存断点
SetMemBreakPoint((PCHAR)debugEvent.u.CreateProcessInfo.lpStartAddress);
break;
//4.
case EXIT_THREAD_DEBUG_EVENT:
break;
//5.
case EXIT_PROCESS_DEBUG_EVENT:
break;
//6.
case LOAD_DLL_DEBUG_EVENT:
break;
//7.
case UNLOAD_DLL_DEBUG_EVENT:
break;
//8.
case OUTPUT_DEBUG_STRING_EVENT:
break;
}

bRet = ContinueDebugEvent(debugEvent.dwProcessId, debugEvent.dwThreadId, DBG_CONTINUE);
}

return 0;
}

实现效果如下

image-20220402165639018

硬件断点

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

image-20220402185424231

假如在Dr0寄存器中写入线性地址,是否所有线程都会受影响?其实不会,每个线程都拥有一份独立的寄存器,切换线程时,寄存器的值也会被切换。

设置硬件断点

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

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

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

image-20220402213248841

断点长度(LENx):00(1字节)、01(2字节)、11(4字节)

通过DR7的LEN控制

image-20220402213410805

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

image-20220402213439641

流程

被调试进程:

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-20220402212910783

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

首先看一下异常处理函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
BOOL SingleStepExceptionProc(EXCEPTION_DEBUG_INFO *pExceptionInfo)
{
BOOL bRet = FALSE;

//1. 获取线程上下文
Context.ContextFlags = CONTEXT_FULL | CONTEXT_DEBUG_REGISTERS;
GetThreadContext(hDebuggeeThread, &Context);
//2. 判断是否是硬件断点导致的异常
if(Context.Dr6 & 0xF) //B0~B3不为空 硬件断点
{
//2.1 显示断点信息
printf("硬件断点:%x 0x%p \n", Context.Dr7&0x00030000, Context.Dr0);
//2.2 将断点去除
Context.Dr0 = 0;
Context.Dr7 &= 0xfffffffe;
}
else //单步异常
{
//2.1 显示断点信息
printf("单步:0x%p \n", Context.Eip);
//2.2 将断点去除
Context.Dr7 &= 0xfffffeff;
}

SetThreadContext(hDebuggeeThread, &Context);

// 等待用户命令
while(bRet == FALSE)
{
bRet = WaitForUserCommand();
}

return bRet;
}

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

image-20220402214947584

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

image-20220402215101783

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

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

完整代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
// Debug4.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"
#include <stdio.h>
#include <windows.h>
#include <tlhelp32.h>

#define DEBUGGEE "C:\\ipmsg.exe"

//被调试进程ID,进程句柄,OEP
DWORD dwDebuggeePID = 0;

//被调试线程句柄
HANDLE hDebuggeeThread = NULL;
HANDLE hDebuggeeProcess = NULL;

//系统断点
BOOL bIsSystemInt3 = TRUE;

//被INT 3覆盖的数据
CHAR OriginalCode = 0;

//原始内存属性
DWORD dwOriginalProtect;

//线程上下文
CONTEXT Context;

typedef HANDLE (__stdcall *FnOpenThread) (DWORD, BOOL, DWORD);

VOID InitDebuggeeInfo(DWORD dwPID, HANDLE hProcess)
{
dwDebuggeePID = dwPID;
hDebuggeeProcess = hProcess;
}

DWORD GetProcessId(LPTSTR lpProcessName)
{
HANDLE hProcessSnap = NULL;
PROCESSENTRY32 pe32 = {0};

hProcessSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if(hProcessSnap == (HANDLE)-1)
{
return 0;
}

pe32.dwSize = sizeof(PROCESSENTRY32);

if(Process32First(hProcessSnap, &pe32))
{
do
{
if(!strcmp(lpProcessName, pe32.szExeFile))
return (int)pe32.th32ProcessID;
} while (Process32Next(hProcessSnap, &pe32));
}
else
{
CloseHandle(hProcessSnap);
}

return 0;
}

BOOL WaitForUserCommand()
{
BOOL bRet = FALSE;
CHAR command;

printf("COMMAND>");

command = getchar();

switch(command)
{
case 't':
bRet = TRUE;
break;
case 'p':
bRet = TRUE;
break;
case 'g':
bRet = TRUE;
break;
}

getchar();
return bRet;
}

VOID SetHardBreakPoint(PVOID pAddress)
{
//1. 获取线程上下文
Context.ContextFlags = CONTEXT_FULL | CONTEXT_DEBUG_REGISTERS;
GetThreadContext(hDebuggeeThread, &Context);
//2. 设置断点位置
Context.Dr0 = (DWORD)pAddress;
Context.Dr7 |= 1;
//3. 设置断点长度和类型
Context.Dr7 &= 0xfff0ffff; //执行断点(16、17位 置0) 1字节(18、19位 置0)
//5. 设置线程上下文
SetThreadContext(hDebuggeeThread, &Context);
}

BOOL Int3ExceptionProc(EXCEPTION_DEBUG_INFO *pExceptionInfo)
{
BOOL bRet = FALSE;

//1. 将INT 3修复为原来的数据(如果是系统断点,不用修复)
if(bIsSystemInt3)
{
bIsSystemInt3 = FALSE;
return TRUE;
}
else
{
WriteProcessMemory(hDebuggeeProcess, pExceptionInfo->ExceptionRecord.ExceptionAddress, &OriginalCode, 1, NULL);
}

//2. 显示断点位置
printf("Int 3断点:0x%p \r\n", pExceptionInfo->ExceptionRecord.ExceptionAddress);

//3. 获取线程上下文
Context.ContextFlags = CONTEXT_FULL | CONTEXT_DEBUG_REGISTERS;
GetThreadContext(hDebuggeeThread, &Context);

//4. 修正EIP
Context.Eip--;
SetThreadContext(hDebuggeeThread, &Context);

//5. 显示反汇编代码、寄存器等

/*
硬件断点需要设置在被调试进程的的线程上下文中。
因此当被调试程序触发调试器设置的INT 3断点时,此时设置硬件断点较为合理。
*/
SetHardBreakPoint((PVOID)((DWORD)pExceptionInfo->ExceptionRecord.ExceptionAddress+1));

//6. 等待用户命令
while(bRet == FALSE)
{
bRet = WaitForUserCommand();
}

return bRet;
}

BOOL AccessExceptionProc(EXCEPTION_DEBUG_INFO *pExceptionInfo)
{
BOOL bRet = FALSE;
DWORD dwAccessFlag; //访问类型 0为读 1为写
DWORD dwAccessAddr; //访问地址
DWORD dwProtect; //内存属性

//1. 获取异常信息,修改内存属性
dwAccessFlag = pExceptionInfo->ExceptionRecord.ExceptionInformation[0];
dwAccessAddr = pExceptionInfo->ExceptionRecord.ExceptionInformation[1];
printf("内存断点 : dwAccessFlag - %x dwAccessAddr - %x \n", dwAccessFlag, dwAccessAddr);
VirtualProtectEx(hDebuggeeProcess, (VOID*)dwAccessAddr, 1, dwOriginalProtect, &dwProtect);

//2. 获取线程上下文
Context.ContextFlags = CONTEXT_FULL | CONTEXT_DEBUG_REGISTERS;
GetThreadContext(hDebuggeeThread, &Context);
//3. 修正EIP(内存访问异常,不需要修正EIP)
printf("Eip: 0x%p \n", Context.Eip);
//4. 显示汇编/寄存器等信息
//5. 等待用户命令
while(bRet == FALSE)
{
bRet = WaitForUserCommand();
}

return bRet;
}

BOOL SingleStepExceptionProc(EXCEPTION_DEBUG_INFO *pExceptionInfo)
{
BOOL bRet = FALSE;

//1. 获取线程上下文
Context.ContextFlags = CONTEXT_FULL | CONTEXT_DEBUG_REGISTERS;
GetThreadContext(hDebuggeeThread, &Context);
//2. 判断是否是硬件断点导致的异常
if(Context.Dr6 & 0xF) //B0~B3不为空 硬件断点
{
//2.1 显示断点信息
printf("硬件断点:%x 0x%p \n", Context.Dr7&0x00030000, Context.Dr0);
//2.2 将断点去除
Context.Dr0 = 0;
Context.Dr7 &= 0xfffffffe;
}
else //单步异常
{
//2.1 显示断点信息
printf("单步:0x%p \n", Context.Eip);
//2.2 将断点去除
Context.Dr7 &= 0xfffffeff;
}

SetThreadContext(hDebuggeeThread, &Context);

//6. 等待用户命令
while(bRet == FALSE)
{
bRet = WaitForUserCommand();
}

return bRet;
}

BOOL ExceptionHandler(DEBUG_EVENT *pDebugEvent)
{
BOOL bRet = TRUE;
EXCEPTION_DEBUG_INFO *pExceptionInfo = NULL;
pExceptionInfo = &pDebugEvent->u.Exception;
//得到线程句柄,后面要用
FnOpenThread MyOpenThread = (FnOpenThread)GetProcAddress(LoadLibrary("kernel32.dll"), "OpenThread");
hDebuggeeThread = MyOpenThread(THREAD_ALL_ACCESS, FALSE, pDebugEvent->dwThreadId);

switch(pExceptionInfo->ExceptionRecord.ExceptionCode)
{
//INT 3异常
case EXCEPTION_BREAKPOINT:
bRet = Int3ExceptionProc(pExceptionInfo);
break;
//访问异常
case EXCEPTION_ACCESS_VIOLATION:
bRet = AccessExceptionProc(pExceptionInfo);
break;
//单步执行
case EXCEPTION_SINGLE_STEP:
bRet = SingleStepExceptionProc(pExceptionInfo);
break;
}

return bRet;
}

VOID SetInt3BreakPoint(LPVOID addr)
{
CHAR int3 = 0xCC;

//1. 备份
ReadProcessMemory(hDebuggeeProcess, addr, &OriginalCode, 1, NULL);
//2. 修改
WriteProcessMemory(hDebuggeeProcess, addr, &int3, 1, NULL);
}

VOID SetMemBreakPoint(PCHAR pAddress)
{
//1. 访问断点
VirtualProtectEx(hDebuggeeProcess, pAddress, 1, PAGE_NOACCESS, &dwOriginalProtect); //PTE P=0
//2. 写入断点
//VirtualProtectEx(hDebuggeeProcess, pAddress, 1, PAGE_EXECUTE_READ, &dwOriginalProtect); //PTE R/W=0
}

int main(int argc, char* argv[])
{
BOOL nIsContinue = TRUE;
DEBUG_EVENT debugEvent = {0};
BOOL bRet = TRUE;
DWORD dwContinue = DBG_CONTINUE;

//1.创建调试进程
STARTUPINFO startupInfo = {0};
PROCESS_INFORMATION pInfo = {0};
GetStartupInfo(&startupInfo);

bRet = CreateProcess(DEBUGGEE, NULL, NULL, NULL, TRUE, DEBUG_PROCESS || DEBUG_ONLY_THIS_PROCESS, NULL, NULL, &startupInfo, &pInfo);
if(!bRet)
{
printf("CreateProcess error: %d \n", GetLastError());
return 0;
}

hDebuggeeProcess = pInfo.hProcess;

//2.调试循环
while(nIsContinue)
{
bRet = WaitForDebugEvent(&debugEvent, INFINITE);
if(!bRet)
{
printf("WaitForDebugEvent error: %d \n", GetLastError());
return 0;
}

switch(debugEvent.dwDebugEventCode)
{
//1.异常
case EXCEPTION_DEBUG_EVENT:
bRet = ExceptionHandler(&debugEvent);
if(!bRet)
dwContinue = DBG_EXCEPTION_NOT_HANDLED;
break;
//2.
case CREATE_THREAD_DEBUG_EVENT:
break;
//3.创建进程
case CREATE_PROCESS_DEBUG_EVENT:
//int3 断点
SetInt3BreakPoint((PCHAR)debugEvent.u.CreateProcessInfo.lpStartAddress);
//内存断点
//SetMemBreakPoint((PCHAR)debugEvent.u.CreateProcessInfo.lpStartAddress);
break;
//4.
case EXIT_THREAD_DEBUG_EVENT:
break;
//5.
case EXIT_PROCESS_DEBUG_EVENT:
break;
//6.
case LOAD_DLL_DEBUG_EVENT:
break;
//7.
case UNLOAD_DLL_DEBUG_EVENT:
break;
//8.
case OUTPUT_DEBUG_STRING_EVENT:
break;
}

bRet = ContinueDebugEvent(debugEvent.dwProcessId, debugEvent.dwThreadId, DBG_CONTINUE);
}

return 0;
}

实现效果如下

image-20220402215902212

CATALOG
  1. 1. 软件断点
  2. 2. 内存断点
    1. 2.1. 流程
  3. 3. 硬件断点
    1. 3.1. 设置硬件断点
    2. 3.2. 流程
    3. 3.3. 处理硬件断点