首页
社区
课程
招聘
[原创]VT调试器原理揭秘--DebugPort转移与重建调试体系
发表于: 15小时前 468

[原创]VT调试器原理揭秘--DebugPort转移与重建调试体系

15小时前
468

目前市面上主要纯在两种基于虚拟化的调试器:

当我们回忆一下windows的调试体系,其实上相当精简,内核真正置位的只有几个地方,一个是peb->BeingDebugged,这个是老生常谈的,很好处理,你把他改回去就行了。另一个就是Process->DebugPort,这是连接调试器与被调试进程的地方,所有的通信都在这里发生,这里不可能隐藏,也许你会说我hook NtQueryInformationProcess不就行了,但是如果我直接获取结构体偏移,去查Process->DebugPort,你怎么办。所以,只要是在windows调试体系之下,我们是绝对无可能隐藏的了Process->DebugPort
但是如果我们不走windows调试体系,自建一个呢?


ept hook,是启动利用启动虚拟化之后增加的额外页表的功能,对于Guest机来说,是完全无法察觉到还存在另一层页表的,开启EPT后,真正的物理内存就被隐藏起来了,Guest中的所有人都访问不到,只有Host能访问到,同时我们还可以设置额外页表的属性,衍生出来几种方法:
摘自:7d3K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6I4K9e0c8D9i4K6u0W2k6$3W2@1K9s2g2T1i4K6u0W2K9h3!0Q4x3V1j5J5x3o6t1@1i4K6u0r3x3o6S2Q4x3V1j5H3y4q4)9J5c8W2)9J5y4f1f1$3i4K6t1#2b7U0g2Q4x3U0f1^5y4g2)9J5y4f1f1^5i4K6t1#2b7U0m8Q4x3U0f1^5z5p5g2b7g2q4)9J5y4f1f1$3i4K6t1#2z5e0N6Q4x3U0g2m8x3q4)9J5y4f1f1%4i4K6t1#2z5e0N6Q4x3U0f1&6y4f1S2a6e0@1E0Q4x3U0g2q4y4#2)9J5y4e0W2m8i4K6t1#2z5o6c8Q4x3U0g2q4y4W2)9J5y4e0V1$3i4K6t1#2b7U0W2Q4x3U0g2q4y4W2)9J5y4f1t1K6i4K6t1#2z5e0g2Q4x3V1j5`.

这样就会无限MTF,不断的切换不可读写,可读写,导致CPU卡死

仿内存执行断点的无痕Hook
设置页面HOOK,全程可读写,不可执行
这样有可能就会某一次到要HOOK的地方,但是页可能不到
如果没到,而是其他地址,恢复可执行,并设置MTF,在MTF Handler中恢复不可执行
直到遇到要Hook的地方,HOST中直接VMWRITE,修改GUEST_RIP
缺点:
遇到4kb页面访问次数多的,会巨卡无比。

MTF置位全程可执行的无痕Hook
设置EPT页级Hook,使整读和写页无效,而只留下可执行属性
分配一个新的物理内存(P2),EPT中的EPT Pml1Entry的PhyFrameNumber(原来的P1)替换成新分配的
修改P2,使其变成一个绝对跳转,跳转到Hook的地方去执行
页面受到读写访问,EPT Violation,此时将此页面可读/写,并设置MTF位
MTF VM Exit,判断是否是EPT Hook导致的,是则设置不可读写,并替换回P2物理内存。
缺点:
缺点不太明显,适合Hook内核函数,而对于高频CRC校验的函数,不太适合,总的来说是最适合的EPT无痕Hook了。

总之就是,实现读与执行分离,读出来的是正常的,执行的却是另一套代码,这样就可以规避内存检测,实现绕过patchguard或者其他内存crc检测
至于npt,就是amd-v技术下的拓展页表,也有着一套相似的流程
有了npt/ept hook,我们就可以放心大胆的hook这些系统函数了

请先食用:https://bbs.kanxue.com/thread-292227.htm
以下代码大部分来此某佬的仓库9ddK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6^5P5h3c8V1L8X3I4B7P5h3c8V1i4K6u0r3N6Y4c8Q4x3X3c8d9k6h3I4G2j5h3c8p5j5X3M7`.
我们首先思考,不通过debug_port,被调试进程怎么找到属于他自己的DEBUG_OBJECT
我们在全局维护起一个g_Debuginfo双向循环链表

通过pid查到到属于自己的DebugObject

将DEBUG_OBJECT挂到被调试进程的是调试器,所以我们要在第一个入口点hook,也就是NtDebugActiveProcess,在这里我们插入一个独立的g_Debuginfo

这里为了调试,日志写多了一点,反正这个函数的调用不是很高频
来到DbgkpPostFakeThreadMessages

这个函数会在两个地方用到,
比如

这时:
TargetDebugObject已经是正确对象,不需要再从:Process->DebugPort读取。
因此当前代码:
DebugObject = TargetDebugObject;

但是不是所有事件调用路径都一定传入有效的 TargetDebugObject
有些路径可能是:
TargetDebugObject == NULL
或者原版逻辑本来就是在函数内部读取Process->DebugPort,比如被调试进程触发异常时像调试器假消息,此时因为我们是自建debugport:
Process->DebugPort == NULL
于是两种来源都没有:
显式 TargetDebugObject 为空
Process->DebugPort 也为空
这时需要一个替代关系:
目标进程 PID -> DEBUG_OBJECT
g_Debuginfo 就承担了这个作用:

DbgkpSetProcessDebugObject是把调试对象挂到被调试进程的地方,同时也是置位peb->BeingDebugged,我们肯定要hook,只要不让他干这些事就行了

对于被调试调试器来说,真正要hook的只有KiDispatchException,和DbgkForwardException,因为只有这里会用上debugport,向调试器发生异常,比如int3
当触发异常时会来到KiDispatchException,此处有

假如异常发生且Process->DebugPort有值,才会发给调试器,但是显然我们没有,所以这里需要寻找, 没找到跳回原来的KiDispatchException即可

然后来到DbgkForwardException,这里还是从g_Debuginfo里面找DEBUG_OBJECT,同时把检测ThreadHideFromDebugger的地方删掉就行了

此时附加调试就hook完毕

请先食用:e4eK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6I4L8h3g2A6L8h3g2A6x3e0l9H3z5o6k6Q4x3X3g2Y4K9i4c8Z5N6h3u0Q4x3X3g2A6L8#2)9J5c8U0t1H3x3U0k6Q4x3V1j5H3z5q4)9J5c8U0p5@1i4K6u0r3N6$3W2F1i4K6t1#2c8e0g2Q4x3U0f1^5y4W2)9J5y4e0R3#2i4K6t1#2c8e0k6Q4x3U0g2m8x3q4)9J5y4f1t1^5i4K6t1#2c8e0S2Q4x3U0g2n7x3q4)9J5y4e0R3K6i4K6t1#2c8e0S2Q4x3U0g2m8c8W2)9J5y4e0V1#2i4K6t1#2c8e0g2Q4x3U0f1^5c8g2)9J5y4e0W2r3i4K6t1#2c8e0N6Q4x3U0f1&6x3q4)9J5y4e0R3$3i4K6t1#2c8e0k6Q4x3U0f1^5c8W2)9J5y4f1q4p5i4K6t1#2c8e0N6Q4x3U0g2m8y4#2)9J5y4e0V1^5y4g2)9J5k6q4)9J5y4f1f1#2i4K6t1#2z5o6S2Q4x3U0f1&6b7W2)9J5y4f1f1#2i4K6t1#2b7V1u0Q4x3U0g2n7b7g2)9J5y4f1f1^5i4K6t1#2b7U0m8Q4x3U0f1^5x3#2)9J5y4f1f1^5i4K6t1#2b7f1k6Q4x3U0f1&6y4g2)9J5c8R3`.`.
注意:创建调试hook的都是高频使用函数,请不要使用dbgprint了,而且有概率会在hook时就被pg发现从而蓝屏
创建调试顾名思义会创建进程,最终会走到NtCreateUserProcess
核心流程如下:
因为太长了,所以我们先完全调用原版创建流程。
status = OrignalNtCreateUserProcess(...);
然后检查当前进程是否存在调试记录:

这里的含义是:
当前进程 PID == 调试器 PID
如果成立,就认为这是“由当前调试器创建的进程”。
接着根据返回的进程句柄获取新进程对象:

然后执行以下额外逻辑。

它把调试器 PID -> 新创建进程 PID 写入g_Debuginfo
因此后续事件可以通过:TargetProcessId -> DebugObject -> 找到调试对象。
2. 清除新进程的 DebugPort

这一步是当前 hook 最重要的行为:
原版创建流程可能已经设置(如果在creatprocess里携带了相应信息):

当前 hook 随后改回:

也就是说,原版流程先建立真实调试关联,你的 hook 在返回前把它隐藏掉,再依赖自己的 g_Debuginfo 路由后续事件。
3. 更新 PEB 的 BeingDebugged
DbgkpMarkProcessPeb(temp_process)可能会置位Peb->BeingDebugged,我们直接让PEB.BeingDebugged = FALSE
4. 清除 NO_DEBUG_INHERIT

这一步清除:
PS_PROCESS_FLAGS_NO_DEBUG_INHERIT
目的是让目标进程后续创建的子进程继续继承调试关系,或者避免该标志阻止调试事件继承
最终代码

这几个函数可能会在加载dll比如loadlibraryA之类的地方被调用,如果检测到有调试对象,就发送模块加载的信息
但是显然我们没有调试对象,因此逻辑如下
通过当前进程 PID
-> g_Debuginfo.TargetProcessId
-> DebugObject
如果查不到:
if (!DebugObject)
return;
也就是直接丢弃模块加载事件。

通过前面创建调试的知识,我们知道创建好的进程第一次被调度会执行PspUserThreadStartup,进而来到DbgkCreateThread,这里就是第一次向调试器发送事件的地方,也是断下来的地方,这里我们的处理也很简单,还是通过g_Debuginfo找到调试对象,然后发送调试事件,然后直接在此调用原版的DbgkCreateThread,因为没有debugport,就不会再发一次,而且会帮我们完成剩下的工作

因为我的电脑是amd的,而且目前市面上开源svm调试器比较少,所以基于npt hook写了一个
效果:

这是普通调试器

经过测试可以调试全版本启动了反调试的vmp
项目地址:617K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6c8L8h3g2A6L8h3g2A6x3e0l9H3z5o6k6Q4x3V1k6K6N6X3#2Q4x3X3c8V1j5X3M7`.
希望留下你的star,本项目会持续开发哦~


传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!!

最后于 13小时前 被Qmeimei10086编辑 ,原因:
收藏
免费 37
打赏
分享
最新回复 (9)
雪    币: 6
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
2
感谢分享
12小时前
0
雪    币: 190
活跃值: (1501)
能力值: ( LV3,RANK:26 )
在线值:
发帖
回帖
粉丝
3

期待更多优质内容的分享,论坛有你更精彩!
12小时前
0
雪    币: 11781
活跃值: (7729)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
4
多谢分享
6小时前
0
雪    币: 2472
活跃值: (1865)
能力值: ( LV8,RANK:120 )
在线值:
发帖
回帖
粉丝
5
6
4小时前
0
雪    币: 4525
活跃值: (5368)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
6
感谢分享
3小时前
0
雪    币: 588
活跃值: (3653)
能力值: ( LV4,RANK:42 )
在线值:
发帖
回帖
粉丝
7
谢谢分析
2小时前
0
雪    币: 33
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
8
1
1小时前
0
雪    币: 158
活跃值: (5516)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
9
多谢分享 !!!
31分钟前
0
雪    币: 632
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
10
先学习
9分钟前
0
游客
登录 | 注册 方可回帖
返回