身为一名逆向人员,ida,x64dbg几乎是我们无法离开的工具,这些调试器的背后的原理是什么
所谓各路五花八门的反调试,调试器检测,无痕hook,线程逃逸又是什么
接下来的几篇博客,我将带大家深入内核,探究windows内核里的调试体系
我们来看一个最简的调试器代码
这边就涉及3个重要函数:
DebugActiveProcess WaitForDebugEvent ContinueDebugEvent
接下来这些函数就是我的分析重点
我这里不卖关子,直接告诉你这几个函数的功能
这里我们不难想到第一个问题,调试器是如何与被调试对象关联的呢?
当我们执行DebugActiveProcess,就会将创建一个调试对象,他既挂在被调试进程上,也挂在调试器上

什么是调试对象?我们直接从wrk里拿定义
EventsPresent就是调试器监听的信号量,当调试事件来临,被调试进程就会给信号量置位,唤起调试器
EventList就是时一个win内核最喜欢的双向循环链表头,他串联着整个调试事件

每当发生调试事件,被调试进程就会进行如下操作
1.检测是否被调试
2.创建调试事件(DEBUG_EVENT),并且挂入链表
3.置位调试对象的EventsPresent,调试器会被激活
4.开始死等ContinueEvent的信号量
5.收到来自调试器的ContinueEvent信号量,恢复执行,同时reset调试对象的EventsPresent(也有可能不会)
调试器流程:
1.我用WaitForDebugEvent死等
2.我的EventsPresent被激活,我检测EventList串起来的一大串链表是否有东西
3.发现有东西,开始处理,处理完成一个就发送ContinueEvent信号量,然后摘除这个节点
4.处理到链表为空,reset调试对象的EventsPresent
5.WaitForDebugEvent继续死等
DEBUG_EVENT定义
从图结合定义我们可用看到DEBUG_EVENT和DEBUG_OBJECT都有EventList,像一条链子一样把DEBUG_OBJECT和一堆DEBUG_EVENT串起来
当然你可能现在云里雾里,我们结合代码分析
DebugActiveProcess为kernelBase.dll的导出函数,在ida里是
DbgUiConnectToDbg的功能就是初始化调试对象,并且先将调试对象挂到调试器上
我们来到ntdll
emmm,很简单的中转一下直接进入内核
注意看一下&NtCurrentTeb()->DbgSsReserved[1]的汇编
16A8的偏移应该刻进dna,这就是调试器该被挂入调试对象的地方,放的是个句柄
我们来看ntoskrnl
代码不难
主要就是创建调试对象,初始化一下,然后挂上去,注意时先挂调试器
当然里面的结构体我都是导入的,你们看起来肯定没这么好看,之后我会把我自己用的idb文件放重来供大家学习
创建好了就要激活
我们来到ntdll
NtDebugActiveProcess就是将调试对象与被调试进程关联的地方,在ntoskrnl里比较复杂,我们先看DbgUiIssueRemoteBreakin
嗯,很简单,就是创建一个远程线程,然后这个线程有个int3会把程序中断下来
解释了为什么我们附加会让程序暂停下来
当然这也是一个反调试点,如果我们让DbgUiRemoteBreakin没有int3,不就断不下来了?
接下来时最复杂的
NtCurrentTeb()->DbgSsReserved[1]依旧16A8
我们来到ntoskrnl
进行了前面一些无关紧要的操作,什么增加引用计数,system和psp保护的进程不让你调试,然后获取目标进程rundown保护锁,最关键两个操作是
DbgkpPostFakeProcessCreateMessages是发假消息,就是每次你用ida附加,不是底下会刷屏一大堆信息,什么创建了几个线程,加载了什么dll,exe镜像在哪,就是这个发的
DbgkpSetProcessDebugObject就是将被调试进程也挂上调试对象的函数
有发送线程假消息和和模块假消息
模块假消息不难,发的主要是一些加载了哪些dll啦之类的,但是我们要拿这些信息需要被调试进程的上下文,所以KiStackAttachProcess一下
DbgkpPostModuleMessages不难,我们重点看DbgkpPostFakeThreadMessages
首先我们看到多了一个结构体也就是DBGKM_APIMSG,这个是debugevent的成员,记录着关键的调试信息
我们可用看到有个大循环,一开始传进来的StartThread是0
然后一直PsGetNextProcessThread,得到每一个线程的一些关键信息,并且填入DBGKM_APIMSG
最后调用最关键的函数DbgkpQueueMessage

我们可用看到交叉应用,他被很多关键函数调用,比如DbgkpSendApiMessage,这就是异常啦之类的发送调试事件必须用到的函数
这个函数的作用就是,创建一个调试事件,然后把收到的DBGKM_APIMSG写道调试事件的ApiMsg成员里,最后挂入调试对象的双向循环链表里,然后置位调试器信号量,激活调试器
当然他的功能很多,我们发假消息用的flag是
也就是走的NOWAIT这条路,只会干复制apimsg并且挂到循环链表上面去的活
最后DbgkpPostFakeThreadMessages还会传出处理后的最后一个线程,接下来有用
show code
注意看
我们传入的LastThread,他又拿去PsGetNextProcessThread,如果拿到了,那说明这期间你又偷偷创建线程了,拿去重新发假消息
终于在这一步,我们将调试对象与被调试进程关联了
经过一些杂七杂八的检查,比如检查链表完整性
然后就会激活调试器,让他去收那些假消息
然后轮询链表,摘下那些没用的,已读的事件,放入clear_link,让DbgkpWakeTarget处理
我们来看WaitForDebugEvent和ContinueDebugEvent
我们回忆一下r3也有一个debugevent,两个是不一样的,你总不能和r0一样一堆r0的地址吧
代码不难,关键点是
死等,然后拿到了个DbgUiWaitStateCange,然后转换为r3的DebugEvent,最后返回
大概流程是
然后转换一下,r3调试器就高高兴兴的跑去处理调试事件了
他会遍历链表摘除读过的事件,如果还有就恢复被前面KeWaitForSingleObject清空的信号量,给通过DebugEvent的信号置位通行,DbgkpWakeTarget放行线程然后返回
如果判断调试事件链表是否为空,
对于双向循环链表,前一个等于后一个等于自己就意味着空
如果为空就不置位debugobject的信号,直接放行
至此,整体框架构建完成
这次我们来看被调试进程是如何发送调试事件的,以及送大家一个有趣反调试
拿异常举例,当发生异常,如果检测到有调试器,会先发给调试器,调用这个函数
这里填写了根据异常apimsg的类型啦,一些异常记录,然后主动调用DbgkpSendApiMessage
可以看到调用了DbgkpQueueMessage,我们前面分析过了
计算最后传入DbgkpQueueMessage的flag是0,也就是要wait的需等待事件,与nowait走一些不一样的路,最终会走到这里
这时候这个线程就开死等直到调试器处理完成,然后还会传出ApiMsg供外面的函数分析使用
我们看DbgkForwardException的这里
传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!!