深入理解Windows内核保护机制PatchGuard

众所周知,自64位Windows操作系统发布以来,微软引入了名为Kernel Patch Protection(KPP)的技术,旨在通过各种内核结构的完整性检查来确保系统的安全性。尽管存在争议,有人认为KPP确实是出于安全考虑,而另一些人则认为KPP连同驱动程序签名是实现数字版权管理(DRM)的便捷手段。本文不会深入探讨微软实施PatchGuard的真正原因,也不会讨论如何绕过PatchGuard(尽管这相对简单)。相反,微软已经明确表示,绕过PatchGuard的生产驱动程序最终将面临内核更新,这将导致所有用户遇到蓝屏错误,让看起来无能为力。

PatchGuard旨在确保以下结构的完整性:

  • 中断描述符表(IDT)
  • 全局描述符表(GDT)
  • 某些MSR(例如LSTAR)
  • PsLoadedModuleList
  • PsActiveProcessHead
  • ntoskrnl、hal、kdcom(在Windows 8中扩展)的代码和数据段
  • 系统服务描述符表

在PatchGuard之前,最有趣的讨论之一是系统服务表。在x86 Windows中,这是一个函数指针数组,在x64中,这是一个偏移量数组。偏移量是从服务表基地址到函数的第一个字节的距离。以这种方式挂钩系统服务非常流行,从流行的rootkits到赛门铁克防病毒软件,甚至是索尼DRM软件。

本文将解释如何在不侵入性地保留其强大功能的同时,与PatchGuard一起工作来挂钩这些服务。这些功能包括在CPL 3(用户模式)下运行的代码可以在任何位置使用SYSCALL指令,而不仅仅是ntdll提供的存根。这种技术通常被称为手动系统调用。

上述技术在各种反调试和反篡改方案中很常见,也是恶意软件的动机。因此,当用户模式调试器不够用时,就需要内核级代码。还将看到这种方法不仅允许监控用户模式对系统调用的访问,还可以监控内核iret/sysret返回用户模式的所有实例。这使调试器能够对目标有更多的控制。这些实例包括:

  • LdrInitializeThunk - 线程和初始进程线程创建的起点
  • KiUserExceptionDispatcher - 内核异常调度程序将在以下两种条件之一时返回这里:
    • 进程没有调试端口
    • 进程有调试端口,但调试器选择不处理异常
  • KiRaiseUserExceptionDispatcher - 在某些情况下,系统服务期间的控制流将在这里着陆,而不是返回错误状态代码,它可以直接调用用户异常链。例如:
    • 使用无效的句柄值关闭句柄(CloseHandle)
  • KiUserCallbackDispatcher - 控制流将在这里着陆,用于Win32K窗口和基于线程的消息操作。然后调用进程PEB中包含的函数表。
  • KiUserApcDispatcher - 这是用户排队的APC被分派的地方。

将其付诸实践

旅程从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将被调用,给一个拦截的机会。

沪ICP备2024098111号-1
上海秋旦网络科技中心:上海市奉贤区金大公路8218号1幢 联系电话:17898875485