调试器学习与编写

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

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

分析函数

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的函数改变了返回值,导致程序跑飞

文章目录
  1. 1. 分析函数
    1. 1.1. DebugActiveProcess
    2. 1.2. 总结
  2. 2. 调试事件的处理
  3. 3. 断点
    1. 3.1. 软件断点
    2. 3.2. 内存断点
    3. 3.3. 硬件断点
  4. 4. 单步异常
  5. 5. 单步步过
|