Windows x64 系统服务挂钩和高级调试





5.00/5 (6投票s)
与 Windows KPP(patchguard)
背景
作者假定您对 Windows 驱动程序、Windows 调试器符号、Windows 内部原理、驱动程序 I/O、C 和汇编有扎实的了解。此处将详细解释所有内容,不提供可下载的示例程序。
引言
正如我们长期以来所了解的,随着 64 位 Windows 的发布以及自 Windows XP 以来,微软引入了一项名为内核补丁保护(Kernel Patch Protection)的技术,旨在对各种内核结构进行完整性检查,以确保系统的安全。我们不会深入探讨微软实施 PatchGuard 的真正原因,因为有些人认为这是真正的安全因素,而另一些人则坚持认为 KPP 以及驱动程序签名是更方便地实现 DRM 的步骤。
本文也 [不会] 涵盖如何绕过 PatchGuard 的方法(尽管它非常简单)。相反,微软表示,绕过 PatchGuard 的生产驱动程序最终将面临不断更新的内核补丁,最终会导致所有用户出现蓝屏死机,让您显得不称职。
PatchGuard,您可能知道或不知道,旨在确保以下结构的完整性:
- 中断描述符表
- 全局描述符表
- 某些 MSR(例如 LSTAR)
PsLoadedModuleList
PsActiveProcessHead
ntoskrnl
、hal
、kdcom
的代码和数据节(Windows 8 中已扩展)- 系统服务描述符表
在 PatchGuard 之前,最有趣也是我们讨论的重点是系统服务表。正如您可能已经知道的,在 x86 Windows 中,这是一个函数指针数组;而在 x64 中,这是一个偏移量数组。偏移量是从服务表的基地址到函数第一个字节的距离。以这种方式挂钩系统服务非常普遍,从流行的 Rootkit 到 Symantec 防病毒软件,甚至是 Sony DRM 软件。
本文将解释我们如何与 PatchGuard 协同工作,以一种侵入性较小的方式挂钩这些服务,但仍然保留其强大的功能。那就是 CPL 为 3(用户模式)的代码可以在任何位置使用 SYSCALL
指令,而不仅仅是 ntdll 提供的存根。
这项技术通常被称为手动系统调用。
上述技术在各种反调试和反篡改方案以及恶意软件中很常见。因此,当用户模式调试器不够用时,就需要内核级别的代码。
我们还将看到,这种方法不仅允许我们 **按进程** 监控 **用户模式** 的系统调用访问,还能监控所有内核 iret
/sysret
返回到用户模式的实例。从而使我们的调试器能够对目标拥有更多的控制权。这些实例包括:
LdrInitializeThunk
- 线程和初始进程线程创建的起点KiUserExceptionDispatcher
- 内核异常分派器将在两种情况之一时IRET
到此处。- 该进程没有调试端口。
- 该进程有调试端口,但调试器选择不处理该异常。
KiRaiseUserExceptionDispatcher
- 在某些系统服务实例中,控制流会转到这里,而不是返回一个错误状态码,它可以简单地调用用户异常链。例如:带有无效句柄值的CloseHandle()
。KiUserCallbackDispatcher
- 控制流会在此处转到 Win32K 窗口和基于线程消息的操作。然后它会调用进程 PEB 中包含的函数表。KiUserApcDispatcher
- 这是用户队列的 APC 被分派的地方。
应用
我们的旅程始于 KPROCESS
结构,确切地说是在偏移量 0x100 处的 InstrumentationCallback
。我已经在我的调试器中使用了这种方法一年多了,我的调试器使用驱动程序来实现此功能。因为直到最近,我才发现可以使用 NtSetInformationProcess
从 **用户模式** 设置此成员(稍后我们将讨论其中的乐趣)。您可以想象,驱动程序是一个出色的调试工具。只需向我们的驱动程序发送一个简单的 IOCTL,即可为目标进程设置一个仪器回调,然后一个简单的用户模式调试器就变成了上帝模式调试器。更有创意的是,您可以完全消除对实际调试端口互斥锁的需求,因为您可以在到达 KiUserExceptionDispatcher
之前就悄无声息地处理异常。这可以绕过大量的反调试技术。
+0x000 Header : _DISPATCHER_HEADER
+0x018 ProfileListHead : _LIST_ENTRY
+0x028 DirectoryTableBase : Uint8B
+0x030 ThreadListHead : _LIST_ENTRY
+0x040 ProcessLock : Uint8B
+0x048 Affinity : _KAFFINITY_EX
+0x070 ReadyListHead : _LIST_ENTRY
+0x080 SwapListEntry : _SINGLE_LIST_ENTRY
+0x088 ActiveProcessors : _KAFFINITY_EX
+0x0b0 AutoAlignment : Pos 0, 1 Bit
+0x0b0 DisableBoost : Pos 1, 1 Bit
+0x0b0 DisableQuantum : Pos 2, 1 Bit
+0x0b0 ActiveGroupsMask : Pos 3, 4 Bits
+0x0b0 ReservedFlags : Pos 7, 25 Bits
+0x0b0 ProcessFlags : Int4B
+0x0b4 BasePriority : Char
+0x0b5 QuantumReset : Char
+0x0b6 Visited : UChar
+0x0b7 Unused3 : UChar
+0x0b8 ThreadSeed : [4] Uint4B
+0x0c8 IdealNode : [4] Uint2B
+0x0d0 IdealGlobalNode : Uint2B
+0x0d2 Flags : _KEXECUTE_OPTIONS
+0x0d3 Unused1 : UChar
+0x0d4 Unused2 : Uint4B
+0x0d8 Unused4 : Uint4B
+0x0dc StackCount : _KSTACK_COUNT
+0x0e0 ProcessListEntry : _LIST_ENTRY
+0x0f0 CycleTime : Uint8B
+0x0f8 KernelTime : Uint4B
+0x0fc UserTime : Uint4B
+0x100 InstrumentationCallback : Ptr64 Void
+0x108 LdtSystemDescriptor : _KGDTENTRY64
+0x118 LdtBaseAddress : Ptr64 Void
+0x120 LdtProcessLock : _KGUARDED_MUTEX
+0x158 LdtFreeSelectorHint : Uint2B
+0x15a LdtTableLength : Uint2B
我们来分析一下
让我们看看它是如何工作的。
每次内核遇到(如上述回调中所述)返回到用户模式代码的情况时,它会检查处理器执行所在的当前 KPROCESS
结构中的 InstrumentationCallback
成员。如果它不是 NULL
并且指向有效内存,内核将从陷阱帧中弹出 RIP,并将其替换为 InstrumentationCallback
中包含的值。
您现在可能在想,我们的注入的调试器代码如何知道它起源于哪个回调?答案在于 r10
。例如,如果发生异常,r10
将包含 KiUserExceptionDispatcher
的线性地址;如果是用户 APC,r10
将包含 KiUserApcDispatcher
的线性地址。如果是 syscall(这意味着系统调用已经分派),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 模拟层的 32 位进程仍然使用系统服务,但并不总是以直接的方式。
冲突
在几种情况下,InstrumentationCallback
将不起作用。
第一种是 NtTerminateProcess
或 NtTerminateThread
(如果调用自身)。这是因为调用者不从这些调用返回。
第二种是 NtContinue
。此函数将提供的上下文参数直接应用于当前陷阱帧,然后执行 IRET,而不使用 KeSystemServiceExit
。
第三种(但可捕获)是 NtRaiseException
。与 NtContinue
一样,此函数将提供的上下文参数应用于当前陷阱帧。但是,如果未处理,KiUserExceptionDispatcher
将被调用,从而给我们一个拦截的机会。
结论
总而言之,我们看到,无论是否是手动调用,监控系统调用在 PatchGuard 之前都是一项出色的调试功能,现在仍然是。我们可以看到,Windows 留给我们这个漏洞,使我们能够继续与 PatchGuard 协同工作,以一种比 32 位 Windows 更好的方式来分析原生系统服务。
我希望您觉得这些信息很有用。我希望您能利用这些信息来扩展您自己的个人调试器或现有调试器的调试功能。
历史
- 2013 年 2 月 13 日:初版