首页
社区
课程
招聘
[翻译]通过滥用一个Windows中的糟糕假设来检测调试器
发表于: 2017-9-23 17:23 9436

[翻译]通过滥用一个Windows中的糟糕假设来检测调试器

2017-9-23 17:23
9436

翻译 by 银雁冰

原文链接

这篇博客将会审视一个微软在十多年前处理软件断点时所用的假设,利用这一点可以检测到大部分(市面上所有的?)用户态和内核态调试器。

x86架构可以用多种方式编码一条特定的汇编指令。举个例子,将两个寄存器eaxebx累加,并且将结果存储到eax中可采用如下助记符:add eax, ebx。这可以被编码成字节序列0x03 0xC30x01 0xD8.这两组机器码代表同一个汇编操作。

如果你只关心反调试技巧(并不想结合上下文理解它的原理),请将进度条滚动到这篇博客的最下面。对于那些想要勇敢地读完整篇文章的人,请系好安全带(老司机要开车了)。

一个int 3指令可以被编码成一个单字节指令0xCC,也可以通过不常见的方式被编码成多指令序列0xCD 0x03:

来自Intel指令手册(第2卷第3章第3.2节)

所以,如果Windows遇到一个多字节的int 3指令会发生什么呢?我们写一个简单的C++程序来看一下:

在你运行这个程序之后,你应该看到与之类似的输出:

一个单字节的int 3(0xCC)和预期的一致。代码块开始处位于0x000001BE94B90000。当这段代码被执行后,异常处理例程启动,我们看到_EXCEPTION_RECORD.ExceptionAddress_CONTEXT.Rip都位于0x000001BE94B90000,这就是int 3指令的开始处。完美!

多字节的int 3(0xCD 0x03)的代码块开始处位于0x000001BE94B90002。当这段代码被执行后,异常处理例程认为_EXCEPTION_RECORD.ExceptionAddress_CONTEXT.Rip都位于0x000001BE94B90003。这是int 3指令的中间位置。为什么?哪里出问题了?

注意:从现在开始,所有的汇编代码和伪代码都从Windows x64 10.0.15063(创意者更新版本)提供的系统文件中重新构建。如果你想要跟着做下去,请确保你使用和我相同的操作系统版本!

微软假设所有的int 3都源自单字节的情况。

这个假设发生在中断处理过程很前面的时候。顾名思义,当一个中断发生的时候,例如当一个int 3被处理器执行时,控制流被CPU重定向到一个注册在IDT(中断控制描述符表)的例程。在Windows操作系统中,软件中断对应的例程可以在nt!KiBreakpointTrap的符号中找到:

nt!KiBreakpointTrap做的第一件事是在栈上生成一个用来传递给后续例程的陷阱帧(_KTRAP_FRAME)。这个结构体的其中一个定义如下:

这个结构体的部分成员在中断发生时自动被CPU填写,具体地说,是 +0x160 (_KTRAP_FRAME.ErrorCode)+0x188 (_KTRAP_FRAME.SegSs)这一区间内的成员。

来自Intel指令手册(第3卷第6章第6.12节)

_KTRAP_FRAME事实上是被CPU存储在栈上的成员的一个扩展。它的目的是提供一个地方,去存储那些易失性寄存器,这些寄存器在调用C语言编译生成的函数时会被破坏。

需要指出的一件非常重要的事是,被CPU保存在栈上的指令寄存器(eip) (_KTRAP_FRAME.Rip)会被设为引发异常指令的下一句指令。在我们的场景中,这意味着_KTRAP_FRAME.Rip成员会被设为紧跟int 3的下一条指令,在上面的例子中,这个指令是ret(0xC3)

在易失性寄存器的值被保存后,nt!KiBreakpointTrap执行一次快速的检查,以判断中断是由用户态(ring3)还是内核态(ring0)发生。如果执行流来自ring3,一个swapgs语句需要被执行,同时填写一些另外的调试寄存器。

最终,控制流会恢复,然后易失性浮点寄存器也会被保存到_KTRAP_FRAME。在进入更多的异常处理逻辑前,指令指针会被从_KTRAP_FRAME.Rip(在前面进入nt!KiBreakpointTrap时被CPU所保存)取出,并且减1,然后作为一个参数被传入nt!KiExceptionDispatch。另外,异常代码,EXCEPTION_BREAKPOINT(0x80000003),也会被传入。nt!KiExceptionDispatch的声明如下:

需要指出的是nt!KiExceptionDispatch (和nt!KiBreakpointTrap一样)是用手工汇编写成。它假设ecx包含异常代码,edx是异常参数的数量(最多3个),r8包含异常发生的地址,r9是第一个异常参数(如果有存在),r10是第二个异常参数(如果存在),r11是第三个异常参数(如果存在),rbp指向_KTRAP_FRAME结构体中的一个段(位于偏移+0x80处)

nt!KiExceptionDispatch的入口处,第一件发生的事情是生成一个_KEXCEPTION_FRAME_KTRAP_FRAME用来存储易失性寄存器,_KEXCEPTION_FRAME则提供一个地方用来存储所有非易失性寄存器:

nt!KiExceptionDispatch还会在栈上创建一个_EXCEPTION_RECORD.aspx)结构体。如果你写过任何Windows上的错误处理例程(可以是用户模式或内核模式),你会很熟悉这个数据结构,因为它包含一个子结构体_EXCEPTION_POINTERS.aspx)。我们在上面的例子里面同时使用了这两个结构体。

更进一步,这解释了我们谜团中的第一部分,顾名思义,为什么_EXCEPTION_RECORD.ExceptionAddress是不正确的。回想一下_EXCEPTION_RECORD.ExceptionAddress是从给nt!KiExceptionDispatch的r8寄存器参数中传递过来的,而这个值来自nt!KiBreakpointTrap。这个值正是被减1后的_KTRAP_FRAME.Rip成员的一份拷贝。

为了找出_CONTEXT.Rip成员是如何被填充的,我们需要将兔子洞再挖得深一点。

nt!KiExceptionDispatch会调用nt!KiDispatchException(是的,这两个单词的顺序被有意翻转),同时传入刚创建的_EXCEPTION_RECORD_KEXCEPTION_FRAME

这个函数会在_KTRAP_FRAME和_KEXCEPTION_FRAME之外构造一个_CONTEXT:通过调用辅助例程KeContextFromKFrame。在_CONTEXT被创建后,会对_EXCEPTION_RECORD.ExceptionCode做一次检查(作为nt!KiExceptionDispatch的一个参数被接收),以判断它是否是STATUS_BREAKPOINT (0x80000003)。如果是,_CONTEXT.Rip成员会被减1:

这解释了最后一个谜团,从而导致_CONTEXT.Rip的值也被破坏。

知道了Windows如何处理不同的int 3类型后,就可以利用这一差异来检测反调试吗?答案是肯定的。

调试器会在异常发生的时候显示程序的状态。既然Windows会不正确地假设我们的int 3异常来自单字节的情况,完全可以迷惑调试器,让它读取“额外”内存。我们利用这种不一致来进行一趟到“守护页”的旅行。

正如我们在我们的第一个例子里看到的那样(见本文的开始),当多字节的int 3出现时,_EXCEPTION_RECORD.ExceptionAddress_CONTEXT.Rip会位于我们多字节指令的中间而不是开始。这意味着调试器将会不正确地认为抛出软件断点的那条指令开始于操作码0xC3。当我们引用靠谱的Intel指令手册时,我们可以看到这个操作码代表一个2字节add指令:

来自Intel指令手册(第2卷第3章第3.2节)

如果我们将我们的多字节int 3指令放在一个内存页的最后会发生什么?

当操作系统提示(我们附加的)调试器一个断点异常发生时,指令指针会被指向(被操作系统)错误解释为一个add(0x03)指令开始处的内存地址。这会导致调试器去反汇编相邻页的数据(既然这是一个2字节长度的指令),然后有效阅读一个我们“合法”内存外的字节。

我们的技巧依赖于Windows上的一个事实:作为一种优化手段,Windows并不会将虚拟内存提交到物理内存,除非它必须需要它。也就是说大多数内存,尤其在用户态,是被分页的。当不在物理内存中的内存需要被使用时,一个缺页异常会发生。想要了解更多关于内存管理的知识,可以阅读我们网站上的下列文章:Introduction to IA-32e hardware pagingExploring Windows virtual memory management.

所以,我们可以通过调用QueryWorkingSetEx.aspx) 检测到读取相邻页的内存,因为这个过程会插入对应的PTE(页表入口)。如果相邻页位于我们进程的工作集中(例如,被调试器映射到我们的进程中),_PSAPI_WORKING_SET_EX_BLOCK.aspx) 中的有效位就会被设定。


[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

收藏
免费 1
支持
分享
最新回复 (9)
雪    币: 8835
活跃值: (2404)
能力值: ( LV12,RANK:760 )
在线值:
发帖
回帖
粉丝
2
X32下并不能检测OD+SOD。
X64下如果PEB那个Flag位不标记,也不会有问题。
拉闸


2017-9-23 17:51
0
雪    币: 9662
活跃值: (4588)
能力值: ( LV15,RANK:800 )
在线值:
发帖
回帖
粉丝
3
cvcvxk X32下并不能检测OD+SOD。X64下如果PEB那个Flag位不标记,也不会有问题。拉闸
多谢v校指点,学习了
2017-9-23 17:56
0
雪    币: 1176
活跃值: (1264)
能力值: ( LV12,RANK:380 )
在线值:
发帖
回帖
粉丝
4
记得我看这篇文章的时候  x64dbg的作者在下面回复:已修复。。。
2017-9-24 09:45
1
雪    币: 272
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
5
Tennn 记得我看这篇文章的时候  x64dbg的作者在下面回复:已修复。。。
这TM就尴尬了
2017-9-24 18:07
0
雪    币: 6818
活跃值: (153)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
6
感谢分享!!
2017-9-24 19:01
0
雪    币: 9662
活跃值: (4588)
能力值: ( LV15,RANK:800 )
在线值:
发帖
回帖
粉丝
7
Tennn 记得我看这篇文章的时候  x64dbg的作者在下面回复:已修复。。。
我去看了一下原文评论,还真有,x64dbg的作者速度真快。。。
2017-9-25 10:10
0
雪    币: 2291
活跃值: (938)
能力值: ( LV8,RANK:130 )
在线值:
发帖
回帖
粉丝
8
思路挺不错的
2017-9-25 10:51
0
雪    币: 1462
活跃值: (12)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
9
好久没来看看了。
2017-9-28 13:09
0
雪    币: 106
活跃值: (276)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
10
好文章,感谢分享......
2017-10-19 18:21
0
游客
登录 | 注册 方可回帖
返回
//