调试器学习与编写

老早前写的东西。。。突然意识到忘记发了,成品代码不见了只有半成品
参考<<软件调试>>&&网上各大神的资料

在学习调试之前首先得明白调试得原理

分析函数

DebugActiveProcess

image-20201011141928644

跟如__imp_DbgUiConnectToDbg

image-20201011142133962

1
2
3
4
.text:00000001800CD037                 mov     rcx, gs:30h ; teb
.text:00000001800CD040 add rcx, 16A8h
.text:00000001800CD047 call NtCreateDebugObject
.text:00000001800CD04C mov ecx, eax

可以看到进内核创建调试对象然后放到了teb+0x16A8处

image-20201011142246830

反调试方法加1

image-20201011143315424

继续看

image-20201011150503549

跟进0环

image-20201011150519304

image-20201011151731430

image-20201011151858083

查看如何建立联系

image-20201011152527156

主要是判断debugport是否为0,然后吧对象写入

所以调试大致流程是,调试器进程创建调试对象句柄放入自身teb+16A8h

然后吧调试对象地址放入被调试进程eprocess的debugport处

总结

image-20201023102517762

图上不是win10得,数值上有些区别但是大致原理一样

调试事件的处理

主要是通过捕获调试消息,调试消息程序会发送给调试器DbgkpSendApiMessage

调试事件采集函数:
<1> 创建进程、线程必经之路:
PspUserThreadStartup
DbgkCreateThread
DbgkpSendApiMessage(x, x)
<2> 退出线程、进程必经之路:
PspExitThread
DbgkExitThread/DbgkExitProcess
DbgkpSendApiMessage(x, x)
<3> 加载模块的必经之路:
NtMapViewOfSection
DbgkMapViewOfSection
DbgkpSendApiMessage(x, x)
<4> 卸载模块的必经之路:
NtUnMapViewOfSection
DbgkUnMapViewOfSection
DbgkpSendApiMessage(x, x)
<5> 异常的必经之路:
KiDispatchException
DbgkForwardException
DbgkpSendApiMessage(x, x)

可以亲自验证下,以PspUserThreadStartup为例

image-20201021141049947

image-20201021141114787

image-20201021141124962

至于调试事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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

dwDebugEventCode描述了调试事件的类型,总共有9类调试事件:

CREATE_PROCESS_DEBUG_EVENT 创建进程之后发送此类调试事件,这是调试器收到的第一个调试事件。
CREATE_THREAD_DEBUG_EVENT 创建一个线程之后发送此类调试事件。
EXCEPTION_DEBUG_EVENT 发生异常时发送此类调试事件。
EXIT_PROCESS_DEBUG_EVENT 进程结束后发送此类调试事件。
EXIT_THREAD_DEBUG_EVENT 一个线程结束后发送此类调试事件。
LOAD_DLL_DEBUG_EVENT 装载一个DLL模块之后发送此类调试事件。
OUTPUT_DEBUG_STRING_EVENT 被调试进程调用OutputDebugString之类的函数时发送此类调试事件。
RIP_EVENT 发生系统调试错误时发送此类调试事件。
UNLOAD_DLL_DEBUG_EVENT 卸载一个DLL模块之后发送此类调试事件。

下面是调试事件

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
#define _CRT_SECURE_NO_DEPRECATE
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <Windows.h>

int main()
{
STARTUPINFO si = { 0 };
si.cb = sizeof(si);
PROCESS_INFORMATION pi = { 0 };
if (CreateProcess(L"C:\\Users\\A\\Desktop\\temp.exe", NULL, NULL, NULL, FALSE, DEBUG_ONLY_THIS_PROCESS | CREATE_NEW_CONSOLE, NULL, NULL, &si, &pi) == FALSE)
{
printf("create process failed\n");
exit(0);
}
BOOL waitEvent = TRUE;
DEBUG_EVENT debugEvent;
while (waitEvent == TRUE && WaitForDebugEvent(&debugEvent, INFINITE)) {

switch (debugEvent.dwDebugEventCode) {

case CREATE_PROCESS_DEBUG_EVENT:
printf("CREATE_PROCESS_DEBUG_EVENT\n");
break;

case CREATE_THREAD_DEBUG_EVENT:
printf("CREATE_THREAD_DEBUG_EVENT\n");
break;

case EXCEPTION_DEBUG_EVENT:
printf("EXCEPTION_DEBUG_EVENT\n");
break;

case EXIT_PROCESS_DEBUG_EVENT:
printf("EXIT_PROCESS_DEBUG_EVENT\n");
waitEvent = FALSE;
break;

case EXIT_THREAD_DEBUG_EVENT:
printf("EXIT_THREAD_DEBUG_EVENT\n");
break;

case LOAD_DLL_DEBUG_EVENT:
printf("LOAD_DLL_DEBUG_EVENT\n");
break;

case UNLOAD_DLL_DEBUG_EVENT:
printf("UNLOAD_DLL_DEBUG_EVENT\n");
break;

case OUTPUT_DEBUG_STRING_EVENT:
printf("OUTPUT_DEBUG_STRING_EVENT\n");
break;

case RIP_EVENT:
printf("RIP_EVENT\n");
break;

default:
printf("unknown debug event\n");
break;
}

if (waitEvent == TRUE) {
ContinueDebugEvent(debugEvent.dwProcessId, debugEvent.dwThreadId, DBG_CONTINUE);
}
else {
break;
}
}
CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
return 0;
}

断点

软件断点

这个比较简单,软件断点就是把目标机器码替换为0xcc int 3

这样就会触发异常发送给调试器,调试器进行处理。

这时一般都会输出当时ip多少还有各个寄存器的值,其实是利用GetThreadContext来获得

1
2
3
4
5
6
7
WINBASEAPI
BOOL
WINAPI
GetThreadContext(
_In_ HANDLE hThread,
_Inout_ LPCONTEXT lpContext
);

第一个参数是线程的句柄,第二个参数是指向CONTEXT结构的指针。要注意,调用该函数之前需要设置CONTEXT结构的ContextFlags字段,指明你想要获取哪部分寄存器的值。该字段的取值如下:

CONTEXT_CONTROL 获取EBP,EIP,CS,EFLAGS,ESP和SS寄存器的值。
CONTEXT_INTEGER 获取EAX,EBX,ECX,EDX,ESI和EDI寄存器的值。
CONTEXT_SEGMENTS 获取DS,ES,FS和GS寄存器的值。
CONTEXT_FLOATING_POINT 获取有关浮点数寄存器的值。
CONTEXT_DEBUG_REGISTERS 获取DR0,DR1,DR2,DR3,DR6,DR7寄存器的值。
CONTEXT_FULL 等于CONTEXT_CONTROL | CONTEXT_INTEGER | CONTEXT_SEGMENTS

当然在编写程序时还需要注意,断点断下后要让ip–

内存断点

内存断点和软件断点有些区别

但本质一样,都是通过触发异常然后发送给调试器来处理

主要是通过修改页属性,当访问该页时则会出现缺页异常然后发送调试事件,调试器接收到后进行处理

与软件断点不同的是,内存断点不需要ip–

硬件断点

先看下调试寄存器

image-20201023200313571

设置硬件断点:

1、Dr0~3用于设置硬件断点,由于只有4个断点寄存器,所以最多
只能设置4个硬件调试断点。

2、Dr7是最重要的寄存器:

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

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

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

处理硬件断点:

1、硬件调试断点产生的异常是 STATUS_SINGLE_STEP(单步异常)

2、B0~B3:哪个寄存器触发的异常

所以我们可以总结下

首先硬件断点有个数量限制那就是4个,还有就是与内存断点和软件断点不同的是,硬件断点是通过单步异常来触发

单步异常

之前硬件断点学习后对单步异常有个初步了解

首先单步异常的出现是因为有这个需求,调试的时候需要单步走查看变化,需要一直下断点触发异常,为了满足需求,现在只需要把TF置1

image-20201023203441916

这样每走一步就会触发单步异常

单步步过

问题:当出现call时,单步入和单步过的实现

单步过通过软件断点或者硬件断点来实现,通过计算当前指令长度然后下断点后直接跑

调试器一般都这么实现所以有时候没有跟进call,但是call的函数改变了返回值,导致程序跑飞


调试器学习与编写
http://www.psbazx.com/2020/10/16/调试器学习与编写/
Beitragsautor
皮三宝
Veröffentlicht am
October 16, 2020
Urheberrechtshinweis