浅析VMP内核反调试

逆向分析 Jan 31, 2023

上次发的文章分析了一个APT的Rootkit,因为使用了VMP进行了保护处理,所以在开启双机调试的时候Rootkit是无法成功加载的。

当时文章里只是简单的提了一下处理方法,没有说的很详细,有朋友看到后问具体怎么处理,也正好借此机会水一篇文章。

上篇文章里说到,VMP中的内核反调试方法如下:

  • KdDebuggerEnabled
  • KdDebuggerNotPresent

对于KdDebuggerEnabled来说,其本质上是共享内存区域KUSER_SHARED_DATA里的一个标志位,在开启内核调试时会被置位。通过查询KUSER_SHARED_DATA中的标志位可以检测到是否有内核调试器,或者通过NtQuerySystemInformation的class 0x23(35)来检测,该class返回两个值,分别对应KdDebuggerEnabled和KdDebuggerNotPresent。

接下来只需要对Rootkit进行分析,看看具体使用了哪种方法然后进行处置即可。

现在QEMU/Unicorn之类的模拟执行大行其道,所以分析rootkit有很多好用的工具。我们可以借用这些工具来协助分析rootkit的行为,我这里使用的是hzqst的unicorn_pe,对于内核模块来说只需要加上`-k`参数即可,具体命令如下:

unicorn_pe xxx.sys -k

输出如上图

可以看到rootkit加载时,执行了NtQuerySystemInformation函数,但是class是11,也就是查询模块相关的信息,并不是我们预计的35,查看其他的函数调用也没有发现和调试相关的。

是因为模拟器未模拟全还是因为VMP使用了其他我们未知的反调试方法?

分析的时候已经知道该Rootkit是使用VMP2.x的版本保护的,当前流行的是VMP3.x的版本,是否2.x和3.x的保护使用的方法不一样呢?

于是我找了一个使用3.x保护的rootkit进行分析,发现3.x调用了NtQuerySystemInformation,class 35,符合我们的预期。2.x没有使用该方法,会不会2.x是直接访问KUSER_SHARED_DATA进行查询的呢?因为很多模拟执行并不监控对数据的访问,所以如果是直接对数据进行读取的话,这些工具可能无法监测到相关的操作。

告诉朋友后,朋友对他的模拟执行插件进行了更新,添加了监测数据访问的操作,经过测试发现rootkit确实有访问KdDebuggerEnabled的操作

根据上面我们的分析,可以简单的总结一下,VMP2.x和3.x内核反调试是有一些差异的,2.x是通过KdDebuggerEnabled变量来检测调试器,3.x则是通过NtQuerySystemInformation的class 35来检测的。

知道了检测方法,下面就是如何绕过检测,因为这些标志位都是在内核中的,所以需要编写内核模块对这些标志位进行处理,在双机调试中,根据不同的调试方法会使用不同的模块,如kdusb.dll或者kdcom.dll分别对应着USB和COM调试。而KdDebuggerEnabled变量就是双机调试时在这些模块中设置的。

kdcom中设置KdDebuggerEnabled的操作

所以一种绕过KdDebuggerEnabled检测的思路就是修改kdcom中对KdDebuggerEnabled进行操作的地方,将其置零或者使用其他的变量替换。

我使用的方法是:

  • 编写一个内核模块,其中有一个全局变量,FakeSharedKdDebuggerEnabled,值为0
  • 遍历内核模块列表,获取到kdcom.dll或者kdusb.dll的基址
  • 对模块进行扫描,查找有0xFFFFF780000002D4(KUSER_SHARED_DATA+0x2D4)相关操作的地方
  • 将0xFFFFF780000002D4替换为FakeSharedKdDebuggerEnabled的地址

对于KdDebuggerEnabled和KdDebuggerNotPresent也使用类似的方法,首先在这两个变量附近找到一片空内存,创建两个全局变量FakeKdDebuggerEnabled和FakeKdDebuggerNotPresent并设置对应的值,然后将ntoskrnl导出表中的KdDebuggerEnabled和KdDebuggerNotPresent替换为FakeKdDebuggerEnabled和FakeKdDebuggerNotPresent。

至此,对于使用VMP保护的rootkit,不管是2.x的还是3.x的都可以绕过内核反调试进行分析了。

标签