原理
我们在前面介绍了一些反调试的手段,基本上都是通过对内核的某个标志进行修改来达到反调试的效果,但是这里有一个问题就是,如果分析人员对我们的样本的API进行了hook,那么我们的反调试手段都将作废,也就是说我们还是要深入底层去探究调试器与被调试器直接究竟是如何建立连接的,只有了解了原理,我们才能更加得心应手的进行对抗
在windows里面,每个程序的低2G是独立使用的,高2G(内核)区域是共用的。那么我们假设一个场景,我们的3环调试器要想和被调试程序之间建立通信肯定就需要涉及到进程间的通信以及数据的交换,如果这个过程放在3环完成,不停的进程通信会很繁琐,所以windows选择将这个过程放在0环进行。调试器与被调试程序之间只能通过CreateProcess
和DebugActiveProcess
这两个API建立起连接,这里我们探究DebugActiveProcess
是如何建立起两者之间的连接的
与调试器建立连接
首先看一下DebugActiveProcess
调用ntdll.dll
的DbgUiConnectToDbg
再调用ZwCreateDebugObject
通过调用号进入0环
进入0环创建DEBUG_OBJECT
结构体
然后到ntoskrnl
里面看一下NtCreateDebugObject
然后调用了ObInsertObject
创建DebugObject
结构返回句柄
再回到ntdll.dll
,当前线程回0环创建了一个DebugObject
结构,返回句柄到3环存放在了TEB的0xF24
偏移处
也就是说,遍历TEB的0xF24
偏移的地方,如果有值则一定是调试器
与被调试程序建立连接
还是回到kernel32.dll
的DebugActiveProcess
,获取句柄之后调用了DbgUiDebugActiveProcess
调用ntdll.dll
的DbgUiDebugActiveProcess
跟到ntdll.dll
里面的DbgUiDebugActiveProcess
,传入两个参数,分别为调试器的句柄和被调试进程的句柄
通过调用号进0环
来到0环的NtDebugActiveProcess
, 第一个参数为被调试对象的句柄,第二个参数为调试器的句柄
执行ObReferenceObjectByHandle
,把被调试进程的句柄放到第五个参数里面,这里eax本来存储的是调试器的EPROCESS
,执行完之后eax存储的就是被调试进程的EPROCESS
这里判断调试器打开的进程是否是自己,如果是自己则直接退出
也不能调试系统初始化的进程
然后获取调试对象的地址,之前是句柄,但是句柄在0环里面是无效的,这里就要找真正的地址
获取到调试对象的地址之后还是存到ebp+Process
的地方,这里之前是被调试对象的地址,现在存储的是调试对象的地址
将调试进程和被调试的PEPROCESS
传入_DbgkpSetProcessDebugObject
,将调试对象和被调试进程关联起来
跟进函数,发现有判断DebugPort
是否为0的操作,ebx为0,edi为被调试进程的EPROCESS,那么edi+0bc
就是调试端口
然后再把调试对象的句柄放到被调试对象的DebugPort
里面
调试事件
调试事件是在调试过程中导致系统通知调试器的事件,调试事件包括创建进程、创建线程、加载动态链接库 (DLL) 、卸载 DLL、发送输出字符串和生成异常。我们知道调试器和被调试程序的通信主要依赖于0环的_DEBUG_OBJECT
结构,然后被调试程序在进行操作时(如:创建进程线程、加载dll)等,就会将一个调试事件写入到_DEBUG_OBJECT
结构里面,调试器获得句柄即可进行调试
这里就会使用调试事件采集函数来对调试事件进行写入,调试器在被调试进程进行特定操作的必经之路中调试事件采集函数,必经之路分为5条,其中以Dbgk开头的函数为调试事件采集函数,所有调试事件采集函数最终都会调用DbgkpSendApiMessage
将封装好的调试事件结构体写入到链表当中。DbgkSendApiMessage
有两个参数,第一个参数为调试事件类型,第二个参数为是否挂起其它线程,有时需要,比如0xCC,有时不需要,比如模块加载。
例如创建进程、线程必经之路:
退出线程、进程必经之路:
我们跟一下PspExitThread
,首先在PspExitThread
里面判断Debugport
是否为0,如果为0则不处于调试状态就不会跳转,如果不为0则为调试状态则跳转到loc_52E3AC
然后这里有个判断,如果当前线程是最后一个线程则跳转到DbgkExitProcess
退出进程,如果不是最后一个线程则跳转到DbgkExitThread
退出当前线程
这里我们跟DbgkExitProcess
,这里调用了DbgkpSendApiMessage
,这个函数的作用就是将被调试程序发送过来的调试事件挂入_DEBUG_OBJECT
这里继续跟进去,如果不挂起进程则用DbgkpQueueMessage
收集调试事件的有关信息
跟进DbgkpQueueMessage
可以看到最终通过DebugPort
得到被调试程序的_DEBUG_OBJECT
的地址
这里继续往下走,通过链表操作添加调试事件信息
总结
首先调试器进程先创建一个_DEBUG_OBJECT
结构放到TEB
的0xF24
处,存储的是_DEBUG_OBJECT
结构的句柄,然后在被调试程序EPROCESS
的DebugPort
的地方存储_DEBUG_OBJECT
结构的地址,然后通过调用DbgkpSendApiMessage
进行调试事件的采集,把句柄放到_DEBUG_OBJECT
结构,供调试器使用
那么了解了调试器和被调试器之间如何建立连接,我们就可以通过以下几种方式来进行反调试
1.不断将EPROCESS
的DebugPort
清零,使调试器不能够和被调试程序建立连接
2.遍历每个线程TEB
的0xF24
,如果存在值就证明有调试器
3.要想建立连接必须要在0环创建_DEBUG_OBJECT
结构,使用NtCreateDebugObject
,直接hook NtCreateDebugObject
达到监控的效果
4.被调试进程必须通过DbgkpSendApiMessage
将调试事件放入_DEBUG_OBJECT
结构供调试器使用,那么这里就可以直接hook 对应的调试事件,如创建进程就hook DbgkCreateThread
,也可以直接hook DbgkpSendApiMessage
达到监控所有调试事件的效果