众所周知,自64位Windows操作系统发布以来,微软引入了名为Kernel Patch Protection(KPP)的技术,旨在通过各种内核结构的完整性检查来确保系统的安全性。尽管存在争议,有人认为KPP确实是出于安全考虑,而另一些人则认为KPP连同驱动程序签名是实现数字版权管理(DRM)的便捷手段。本文不会深入探讨微软实施PatchGuard的真正原因,也不会讨论如何绕过PatchGuard(尽管这相对简单)。相反,微软已经明确表示,绕过PatchGuard的生产驱动程序最终将面临内核更新,这将导致所有用户遇到蓝屏错误,让看起来无能为力。
PatchGuard旨在确保以下结构的完整性:
在PatchGuard之前,最有趣的讨论之一是系统服务表。在x86 Windows中,这是一个函数指针数组,在x64中,这是一个偏移量数组。偏移量是从服务表基地址到函数的第一个字节的距离。以这种方式挂钩系统服务非常流行,从流行的rootkits到赛门铁克防病毒软件,甚至是索尼DRM软件。
本文将解释如何在不侵入性地保留其强大功能的同时,与PatchGuard一起工作来挂钩这些服务。这些功能包括在CPL 3(用户模式)下运行的代码可以在任何位置使用SYSCALL指令,而不仅仅是ntdll提供的存根。这种技术通常被称为手动系统调用。
上述技术在各种反调试和反篡改方案中很常见,也是恶意软件的动机。因此,当用户模式调试器不够用时,就需要内核级代码。还将看到这种方法不仅允许监控用户模式对系统调用的访问,还可以监控内核iret/sysret返回用户模式的所有实例。这使调试器能够对目标有更多的控制。这些实例包括:
将其付诸实践
旅程从KPROCESS结构开始,确切地说是:
InstrumentationCallback
在0x100处看到。已经在自己的调试器中使用这种方法一年多了,这个调试器使用驱动程序来实现这个功能。这是因为直到最近,才发现这个成员可以从用户模式使用NtSetInformationProcess设置(稍后会讲到有趣的部分)。可以想象,驱动程序是一个非常好的调试工具。只需对驱动程序进行简单的IOCTL,就可以为目标进程设置一个InstrumentationCallback,简单的用户模式调试器就变成了神模式调试器。更有创意的是,完全可以消除实际的debugport互斥体的需要,因为可以在到达KiUserExceptionDispatcher之前无形地处理异常。这可以绕过大量的反调试技术。
让分析它是如何工作的。每次内核遇到(如上所述的回调)返回到用户级代码的情况时,它会检查当前处理器执行的KPROCESS结构下的InstrumentationCallback成员。如果它不是NULL,并且假设它指向有效的内存,内核将交换陷阱帧上的RIP,并将其替换为InstrumentationCallback包含的值。
现在可能想知道,注入的调试器代码如何知道它来自哪个回调?答案在于r10。例如,如果发生异常,r10将包含KiUserExceptionDispatcher的线性地址,如果是用户APC,r10将包含KiUserApcDispatcher的线性地址。如果是系统调用(这意味着系统调用已经被分派),r10将包含返回地址。这是SYSCALL指令之后的地址。
需要注意的重要一点是,线程的Dr7值是否激活会影响InstrumentationCallback是否用于重定向某些转换的控制流。对于KiUserExceptionDispatcher和LdrInitializeThunk,Dr7是否激活或NULL并不重要。对KiUserExceptionDispatcher和LdrInitializeThunk的分派将始终被重定向到回调。然而,除非Dr7处于活动状态,否则SYSCALLS和其他剩余的内核到用户过渡回调将不会被重定向到instrumentationcallback。
也可以看到这如何有利于被调试者,作为一个非常有趣的反调试机制。如何在当前进程或目标调试者进程上设置这个?NtSetInformationProcess原型如下:
NTSTATUS NtSetInformationProcess
(
HANDLE hProcess,
ULONG ProcessInfoClass,
void *InputBuffer,
ULONG size
);
输入缓冲区必须是指向自己进程地址空间内的有效线性地址的指针,或者是目标的。还需要SeDebugPrivilege。由于只传递一个指针,而且这只适用于x64 Windows,大小当然是8字节。信息类是0x28。请记住,这个功能只在x64版本的Windows中添加。在WOW64 thunk层运行的32位进程仍然使用系统服务,但并不总是以直接的方式。
存在几种场景,其中InstrumentationCallback将无效。首先是NtTerminateProcess或NtTerminateThread(如果是对自身调用)。这是因为调用者不会从这些调用中返回。第二个是NtContinue。这个函数采用提供的上下文参数,并直接应用于当前的陷阱帧,然后执行IRET而不使用KeSystemServiceExit。第三个(但可以捕获)是NtRaiseException。像NtContinue一样,这个函数采用提供的上下文参数,并将其应用于当前的陷阱帧。然而,如果没有处理,KiUserExceptionDispatcher将被调用,给一个拦截的机会。