环境准备

  1. 在WIN_XP系统虚拟机下操作;
  2. 准备TitanHide驱动和插件;

过反调试过程

启动TitanHide

TitanHide是一个开源的Windows内核模式驱动程序,是一个调试辅助工具,用于绕过目标程序的反调试机制,能针对VMP的大部分反调试检测点。

在ntdll.KiFastSystemCallRet下硬件断点

之前对VMP-3.5.1泄露的源码分析,我们知道VMP调用NT API,是自己构建执行加载服务号 + SYSENTER(在32位程序下),直接进入Ring 0执行内核服务。
当内核服务执行完毕,需要返回用户模式时,它会精确地返回到KiFastSystemCall函数末尾的ret指令处,这条指令所在的地址标签就是KiFastSystemCallRet

简单来说,KiFastSystemCallRet是所有系统调用从内核返回到用户空间的“必经之路”和“枢纽站”。对任何一次系统调用,我们都能在它的返回点精准地拦截到。
在进入VMP保护的程序部分,在每一次暂停在KiFastSystemCallRet时,追踪记录栈数据和返回值、系统调用号。

ZwopenFile

这个VMP第一个反调试点,也是VMP-3.5.1泄露的源码中没有的检测点,是后面新添加的。

首先我们在堆栈窗口跟踪EBP,这个栈地址的内容是0x00000074,通过在ntdll.dll中定位函数,跳转到其汇编代码,函数的开头通常就是mov eax, 系统调用号
这个是当前版本下的NTopenFile的系统调研号(不同的系统版本系统调用号映射的函数也不一样),

追踪第二个参数的内核地址,发现\??\TitanHide字符串。
\??\TitanHide正是TitanHide驱动在用户模式下可见的设备符号链接
通过分析我们得出,VMP通过ZwopenFile去打开TitanHide这个设备,如果调用成功,判定系统内核中存在TitanHide驱动,推断可能有调试器正在隐藏,触发反调试机制。
绕过手段:
将其设备名和符号链接修改为自定义的名称,创建内核驱动服务时修改服务名sc create 自定义服务名 binPath= "C:\path\to\TitanHide.sys" type= kernel start= auto
然后要修改插件的源码中的设备名和符号链接,

ZwQueryInformationProcess

服务号:0000009A

同样跟踪EBP,获取系统调研号0x0000009A ——> ZwQueryInformationProcess,返回值:EAX:0x00000000,记录分析栈数据。
vmp调用NtQueryInformationProcessProcessInformationClass= ProcessDebugPort= 0x7,查询调试端口号。

绕过手段:
修改返回值为调用失败,
或者修改[out] ProcessInformation第三个参数(接收返回的调试端口指针)所指向的内容为0。

DebugView有类似日志说明过饭调试成功:
[TITANHIDE] ProcessDebugPort by 1776


系统调研号0x0000009A ——> ZwQueryInformationProcess,返回值:EAX:0xC0000353[STATUS_PORT_CONNECTION_REFUSED],调用失败,记录分析栈数据。
vmp调用NtQueryInformationProcessProcessInformationClass= ProcessDebugObjectHandle = 0x1E,查询调试对象句柄。


返回值:EAX:0xC0000005[STATUS_ACCESS_VIOLATION],调用失败。


返回值:EAX:0x80000002[EXCEPTION_DATATYPE_MISALIGNMENT],调用失败。


返回值:EAX:0xC0000005[STATUS_ACCESS_VIOLATION],调用失败。

连续调用了四次NtQueryInformationProcess(ProcessDebugObjectHandle)
如果调用成功或者[out] ProcessInformation指向的内容为0,就会被检测为存在调试器。

绕过手段:
修改返回值为调用失败,并且[out] ProcessInformation第三个参数(接收返回的调试端口指针)所指向的内容不为0。

ZwCreateDebugObject

服务号:0x00000021 ——> ZwCreateDebugObject,返回值:0x00000000[STATUS_SUCCESS],调用成功。

在这里vmp试图创建一个调试对象,正常情况下程序没有权限是创建调试对象成功,如果创建成功就间接说明程序可能处在调试中。

后续调用一些API去提取信息;

服务号:0x000000A3 ——> NtQueryObject,返回值:0xC0000004[STATUS_INFO_LENGTH_MISMATCH],调用失败,通常是因为参数四指定的缓冲区太小,装不下要返回的数据。
参数一(00000058)就是刚刚创建好的内核调试对象句柄。
第一次调用为了获取正确的缓冲区大小,参数五 (0x0012EEB0) 指向的变量此时会被内核填入正确的缓冲区大小。


返回值:0x00000000[STATUS_SUCCESS],调用成功。
这次NtQueryObject调用成功,成功查询到了句柄0x58对应的对象类型信息。


返回值:0xC0000005[STATUS_ACCESS_VIOLATION],调用失败。


服务号:0x00000019 ——> NtClose,返回值:0x00000000[STATUS_SUCCESS],调用成功。
关闭创建的内核调试对象。

vmp试图创建一个内核调试对象,如果创建成功并且能够获取到这个调试对象的信息,说明程序在调试环境中。但是这个监测点检测完后没有触发报错弹出而是在后几个调试点后,绕过这个监测点或者不做修改进过完这个监测点都会到下一个监测点。

绕过手段:
修改返回值为调用失败(0xC000000D),并且将调试对象句柄清零,使NtCreateDebugObject调用失败。

ZwSetInformationThread

服务号:0x000000E5 ——> NtSetInformationThread,返回值:0x00000000[STATUS_SUCCESS],调用成功。

vmp调用NtSetInformationThread(ThreadHideFromDebugger(0x11))让当前线程脱离调试器,就是不让你的调试器调试VMP保护的代码区块。

服务号:0x0000009B ——> NtQueryInformationThread,返回值:0x80000002[EXCEPTION_DATATYPE_MISALIGNMENT],调用失败。

vmp检测当前线程是否已成功脱离调试器。

绕过手段:

  1. TitanHide中直接返回成功(STATUS_SUCCESS),不执行真正的ThreadHideFromDebugger操作。
  2. 修改参数将0x110即可。

ZwQuerySystemInformation

服务号:0x000000AD ——> NtQuerySystemInformation,返回值:0x00000000[STATUS_SUCCESS],调用成功。

通过查询KernelDebuggerEnabledKernelDebuggerNotPresent两个标志位来检查内核调试器。

绕过手段:

  1. 修改存放结果的缓冲区中这两个标志位的查询结果。
  2. 直接将缓冲区置0。



再次调用NtQueryInformationProcessNtSetInformationThread

CRC校验

服务号:0x00000074 ——> NtOpenFile,返回值:0x00000000[STATUS_SUCCESS],调用成功。

分析第三个参数ObjectAttributes可以发现vmp要打开存在要保护代码的可执行文件。

服务号:0x00000032 ——> NtCreateSection,返回值:0x00000000[STATUS_SUCCESS],调用成功。

给刚刚打开的文件创建可执行映像节区。

服务号:0x0000006C ——> NtMapViewOfSection,返回值:0x00000000[STATUS_SUCCESS],调用成功。

将之前创建的文件节区映射到了进程的内存空间。

服务号:0x0000010B ——> NtUnmapViewOfSection,返回值:0x00000000[STATUS_SUCCESS],调用成功。

卸载了之前映射到进程内存中的可执行文件节区。

服务号:0x00000019 ——> NtClose,返回值:0x00000000[STATUS_SUCCESS],调用成功。

关闭节区句柄。

服务号:0x00000019 ——> NtClose,返回值:0x00000000[STATUS_SUCCESS],调用成功。

关闭可执行文件句柄。

vmp这里一系列操作是为了CRC完整性校验和检测0xCC断点。

绕过手段:
不对vmp保护代码部分进行修改,调试过程中不要在保护代码内下0xCC断点,使用硬件断点替代。

ZwProtectVirtualMemory

服务号:0x00000089 ——> NtProtectVirtualMemory,返回值:0x00000000[STATUS_SUCCESS],调用成功。

多次调用,这里vmp遍历保护的所有节区(Section),并为每个节区设置正确的内存权限。
同时检测第五个输出参数(OldProtection)是否是PAGE_GUARD

绕过手段:
修改第五个输出参数(OldProtection)的返回结果。

关闭异常句柄

当调试到这个检测点时,vmp就已经执行完成解包,将之前压缩的原始exe基本信息释放出来了。
服务号:0x00000019 ——> NtClose,返回值:0xC0000008[STATUS_INVALID_HANDLE],调用失败。

vmp故意关闭无效句柄,调试器附加后,异常处理会和正常程序异常处理不同,通过这个不同点去检测(具体细节去看vmp泄露的源码)。

绕过手段:
修改返回值为0xC0000235[STATUS_HANDLE_NOT_CLOSABLE],将异常码修改为错误码。

TrapFlag与检测硬件断点

在断在关闭异常句柄这个检测点时:
设置调试器忽略所有异常(主要是单步中断异常),将硬件断点替换为0xCC断点(不能留有硬件断点),后续没有对软件断点的检测了。


过了两个KiFastSystemCallRet断点就成功过vmp反调试部分啦。