寻找 SSDT






4.71/5 (18投票s)
备受关注的内核数据结构
引言
与寻找圣杯不同,这次追寻一定会成功,但过程会有点棘手。
你听说过那些作者经常发布代码却没有任何相关解释的软件仓库吗?你当然听过,但在这里我们不能这样做。
我有义务以一种易于理解的方式,在单页文章的篇幅内,提供有关一个困难主题的相关信息,希望能最终让每个人都明白。
这可能不容易,但让我们通过 5 个步骤继续,并希望一切顺利。
- 要求和警告
- 什么是 SSDT 及其位置
- SSDT 的使用方式
- SSDT 为何如此重要?
- 我们查找 SSDT 的 32 位和 64 位代码
要求和警告
- 了解 C 语言的一些知识对于理解我们的代码很重要——如果你不了解,仍然可以继续阅读,并试着从中获得一些关于我们正在书写内容的见解(如果你还没有的话)。
- 对于内核模式的侦测,了解如何使用
Windbg
或其命令行版本 KD 是有益的。适当的内核调试需要两台计算机,目标计算机可以放在虚拟机中(除非我们正在测试某些硬件驱动程序,这种情况并非如此),这样您就不必费劲连接电缆来建立连接。 - 我们已经在所有 intended 的操作系统上测试了内核驱动程序(即,32 位 Windows 7 和 Windows 10 直到 RedStone 2 或版本 1703,64 位 Windows 7、8、8.1 和 10 直到 Redstone 2 的版本,以及使用相同代码库的 64 位服务器版本,特别是 2008 R2、2012 和 2016)。尽管测试成功,您只能在您专门用于测试的计算机(或虚拟机)上测试这些驱动程序以及任何一般驱动程序。
什么是 SSDT 及其位置
SSDT 隐藏在某些结构中。在我们最终定位并解释 SSDT 之前,我们必须先解释和定位它们。
服务描述符表 (Service Descriptor Table)
服务描述符表是一个内核结构,如下图所示,其中包含 4 个系统服务表 (SST) 条目。
typedef struct tagSERVICE_DESCRIPTOR_TABLE {
SYSTEM_SERVICE_TABLE nt;
SYSTEM_SERVICE_TABLE win32k;
SYSTEM_SERVICE_TABLE sst3;
SYSTEM_SERVICE_TABLE sst4;
} SERVICE_DESCRIPTOR_TABLE;
系统中存在两个服务描述符表,分别是 nt!KeServiceDescriptorTable
和 nt!KeServiceDescriptorTableShadow
(我们将使用在结构和函数名称前加上模块名称的表示法,这里 nt!
表示来自 ntoskrnl.exe 或 ntkrnlmp.exe)。
系统服务表 (System Service Table - SST)
SST 结构(如下图所示)包含 ServiceTable
字段,它是一个指向内核例程数组第一个元素的指针,在 32 位操作系统的情况下;或者在 64 位操作系统的情况下,是指向一个地址数组(以及一些关于参数数量的额外信息)的指针,该数组相对于它指向的基地址。
typedef struct tagSYSTEM_SERVICE_TABLE {
PULONG ServiceTable;
PULONG_PTR CounterTable;
ULONG_PTR ServiceLimit;
PBYTE ArgumentTable;
} SYSTEM_SERVICE_TABLE;
现在,让我们使用 Windbg
来查看一个 32 位的服务描述符表,在本例中是 nt!KeServiceDescriptorTable
,以及它所包含的 SST 结构。
kd> dps nt!KeServiceDescriptorTable L10
81a2a180 8190f20c nt!KiServiceTable
81a2a184 00000000
81a2a188 000001c8
81a2a18c 8190f930 nt!KiArgumentTable
81a2a190 00000000
81a2a194 00000000
81a2a198 00000000
81a2a19c 00000000
81a2a1a0 00000000
81a2a1a4 00000000
81a2a1a8 25c10ad3
81a2a1ac 01d2e256
81a2a1b0 00000000
81a2a1b4 8913bb40
81a2a1b8 89248580
81a2a1bc 890284f0
现在,让我们看看另一个服务描述符表,nt!KeServiceDescriptorTableShadow
。
kd> dps nt!KeServiceDescriptorTableShadow L10
81a2a140 8190f20c nt!KiServiceTable
81a2a144 00000000
81a2a148 000001c8
81a2a14c 8190f930 nt!KiArgumentTable
81a2a150 98f00000 win32k!W32pServiceTable
81a2a154 00000000
81a2a158 0000046c
81a2a15c 98f01628 win32k!W32pArgumentTable
81a2a160 a0cf3fff
81a2a164 00000001
81a2a168 ffffffff
81a2a16c 819245b2 nt!FinalExceptionHandlerPad58
81a2a170 8959869c
81a2a174 00000000
81a2a178 00000001
81a2a17c 00000000
SSDT
SSDT 是 System Service Dispatch Table(系统服务分派表)的缩写。
如上所述,存在一个指针数组(在 32 位情况下)或相对地址数组(加上额外信息)(在 64 位情况下),由 SST(系统服务表)的 ServiceTable
字段指向——这个数组就是 SSDT。
查看上面的 Windbg
输出,我们可以看到在 nt!KeServiceDescriptorTable
的情况下有一个明确标识的 SST 表,在 nt!KeServiceDescriptorTableShadow
的情况下有两个。虽然我们看到了更多数据,但根据现有信息,我们只能说,在可能的 4 个 SST 条目中,nt!KeServiceDescriptorTable
只使用了第一个——它描述了由 ntoskrnl.exe 导出的 Windows Native APIs 的 SSDT。nt!KeServiceDescriptorTableShadow
使用 2 个 SST 条目,第一个是 nt!KeServiceDescriptorTable
的 nt!KiServiceTable
的副本,第二个是 win32k!W32pServiceTable
,描述了由 win32k.sys 导出的用户和 GDI 例程的 SSDT。
SSDT 的使用方式
SSDT 为用户模式应用程序(当然是间接的)和内核模式驱动程序都提供了功能。
我们将看看用户模式的情况。
当用户模式应用程序直接或间接调用某些 Windows API 函数时,许多时候会在此过程中调用一个或多个内核例程。内核模式通过 Sysenter(或 64 位使用 Syscall)汇编指令从用户模式进入(过去是中断 0x2E——它仍然存在,尽管可能不常使用)。确切的内核服务例程将由一个称为分派 ID 的数字指定,该数字在执行 Sysenter
/Syscall
指令之前放入 EAX 寄存器。
分派 ID 的前 12 位是 SSDT 的索引。第 12 位和第 13 位指定了哪个 SSDT。这意味着分派 ID 高达 0xFFF 将由 nt!KiServiceTable
SSDT 处理,而分派 ID 在 0x1000 和 0x1FFF 之间将由 win32k!W32pServiceTable
SSDT 处理。
此时,如果这一切对您来说都是新事物,您可能会感到有些迷茫——这个主题确实很密集且棘手,但请坚持下去。
32 位示例
为了更清楚地说明,让我们再次使用 Windbg
,并设想一个用户模式调用 ntdll!NtCreateFile
(ntdll.dll 是一个用户模式 DLL,包含用户模式到内核模式系统服务的调用存根等)。
kd> u ntdll!NtCreateFile
ntdll!NtCreateFile:
77ab3250 b875010000 mov eax,175h
77ab3255 e803000000 call ntdll!NtCreateFile+0xd (77ab325d)
77ab325a c22c00 ret 2Ch
77ab325d 8bd4 mov edx,esp
77ab325f 0f34 sysenter
正如您所见,分派 ID 是 0x175。它将在 nt!KiServiceTable
SSDT 中解析到第 0x176 个(列表从零开始)条目,即 nt!NtCreateFile
。
kd> dps nt!KiServiceTable L176
8190f20c 818c573a nt!NtAccessCheck
8190f210 818cbfd8 nt!NtWorkerFactoryWorkerReady
8190f214 81b033b8 nt!NtAcceptConnectPort
.....
190f7d8 81ad7b18 nt!NtCreateTimer2
8190f7dc 81af323a nt!NtCreateIoCompletion
8190f7e0 81a66958 nt!NtCreateFile
64 位示例
再次使用 Windbg
,让我们设想一个用户模式调用 ntdll!NtCreateFile
。
kde> u ntdll!NtCreateFile
ntdll!ZwCreateFile:
00000000`777ac080 4c8bd1 mov r10,rcx
00000000`777ac083 b852000000 mov eax,52h
00000000`777ac088 0f05 syscall
00000000`777ac08a c3 ret
在这里,我们看到 ntdll!NtCreateFile
(在用户模式下与 ntdll!ZwCreateFile
相同)使用的分派 ID 是 0x52。
此分派 ID 对应于 nt!KiServiceTable
中的第 0x53 个条目。
kd> dd /c 1 nt!KiServiceTable L53
fffff800`02a78000 04170500
fffff800`02a78004 02f80700
......
fffff800`02a78140 02d4b140
fffff800`02a78144 046f9202
fffff800`02a78148 030b4fc7 <- This is our entry
0x030b4fc7 的第 4-31 位对应于到 nt!KiServiceTable
基址的相对地址。第 0-3 位与参数数量有关,此处不使用。
要获取函数地址,我们将值向右移 4 位,然后将结果添加到表基址线性地址。
所以,0x030b4fc7 >> 4 = 0x30b4fc
。
现在将其添加到表线性基址。
0xfffff80002a78000 + 0x30b4fc = 0xfffff80002d834fc
让我们确认一下。
kd> u nt!NtCreateFile
nt!NtCreateFile:
fffff800`02d834fc 4c8bdc mov r11,rsp
fffff800`02d834ff 4881ec88000000 sub rsp,88h
fffff800`02d83506 33c0 xor eax,eax
...
是的,成功了!
SSDT 为何如此重要?
系统中存在许多重要的数据结构,它们都至关重要,并有助于整个系统的预期功能。
尽管如此,SSDT 在公众中的关注度和重要性却高于其他结构。
到目前为止,您可能已经对原因有所感觉了。在 32 位时代(其实并不久远),互联网黑暗角落的恶意分子会制造(实际上仍然在制造,尽管如今 32 位操作系统不幸地需求量很小)运行在内核模式的恶意软件(所谓的 Rootkits),它们会修改 nt!KiServiceTable
或 win32k!W32pServiceTable
中的条目,将系统调用重定向到自己的代码,从而制造麻烦。不仅恶意分子热衷于玩弄 SSDT,许多安全产品,特别是杀毒软件,也曾钩住 SSDT 以便在病毒攻击时立即收到警报。
然而,从 Windows XP 64 位和 Windows Server 2003 SP1 开始,64 位 Windows 版本引入了一个强大的保护功能,称为内核补丁保护(通常称为 PatchGuard)。PatchGuard 会定期检查以确保一定数量的关键系统结构(包括 SSDT)在此期间未被修改。安全软件,特别是杀毒软件,被迫寻找效率较低的替代方案。Rootkit 作者遭受了剧烈的挫折,但并未完全失败——他们时不时地会想出新的但短暂的(至少我们希望如此)绕过 PatchGuard 的方法——当 Microsoft 得知后,它会发布新的 PatchGuard 补丁并发布新的安全更新。
除了 PatchGuard,强制使用选定认证机构的 Class 3 证书进行驱动程序签名,也为系统安全做出了巨大贡献。
我们查找 SSDT 的 32 位和 64 位代码
我们的代码目的不是钩住 SSDT 中的任何系统函数,实际上,在可行的情况下(PatchGuard 使事情变得不那么可行),这样做相对容易——关于这个主题有大量的文献,甚至包括畅销书籍,并且已经举行了数百场由专家主持的关于此主题的会议。
令人惊讶的是,这一切中最棘手的部分在于找出 SSDT 的真正位置。特别是,nt!KeServiceDescriptorTab1eShadow
的第二个 SST 条目指向的 win32k!W32pServiceTable
SSDT 确实被“隐藏”了。另一方面,定位 32 位操作系统的 nt!KiServiceTable
SSDT 是直接的,符号是导出的,可以链接——只需读取其值。然而,在 64 位 Windows 中,nt!KiServiceTable
的符号未被导出。
在我们的代码中,我们将定位 nt!KeServiceDescriptorTab1eShadow
。如果我们成功了(当然我们会),我们将知道 nt!KiServiceTable
和 win32k!W32pServiceTable
的位置——一石二鸟。
我们使用单个 Visual Studio 2015 解决方案构建了内核模式驱动程序和用户模式应用程序(该应用程序还负责加载和最终卸载驱动程序)——一个项目用于驱动程序(32 位和 64 位),另一个项目用于应用程序(32 位和 64 位)。
要在计算机上编译我们的项目而无需修改,必须安装 Windows Driver Kit 版本 10。
用户模式应用程序在编译为 32 位或 64 位时行为略有不同。
因此,在测试时,您必须将 32 位驱动程序与 32 位应用程序配对,将 64 位驱动程序与 64 位应用程序配对。
驱动程序的代码使用了未公开的(或如微软所说的“不透明”)结构和系统函数。这些未公开的元素在我们测试驱动程序的操作系统范围内(如上所述)都能正常工作。
内核驱动程序
驱动程序按需加载,不会在启动时运行——这意味着即使系统因 Bugcheck 崩溃,您也不会陷入无休止的灾难性循环。
完成工作后,驱动程序将被卸载,注册表项将被自动清理。此外,如果是 64 位驱动程序,它将被从 %SYSTEM32%\Drivers 文件夹(它被复制到那里)中删除。32 位驱动程序将从启动器应用程序所在的文件夹启动,当然不会被删除。
如果您想构建 64 位驱动程序并在没有有效 Class 3 证书(测试证书在此无效)的情况下进行测试,请注意,您需要将机器设置为测试模式。这是通过从提升的命令提示符运行 bcdedit.exe 并发出以下命令来完成的:
bcdedit.exe -set TESTSIGNING ON
然后重启。
测试完成后,再次启动 bcdedit
,使用 OFF
而不是 ON
发出相同的命令,然后重启。
为了完成其任务,驱动程序在 32 位和 64 位下使用不同的方法。
在 32 位下
- 枚举所有正在运行的进程,寻找用户模式发送的进程 PID(默认情况下,它将是 csrss.exe 的
PID
,这是一个始终存在的进程)。 - 找到后,它会枚举 csrss.exe 的所有线程,寻找一个 GUI 线程——它们在
KTHREAD
(ETHREAD
——Executive Thread Block 的一部分)中有Win32Thread
条目,而不是NULL
。 - GUI 线程保证其
KTHREAD
中的ServiceTable
条目指向nt!KeServiceDescriptorTab1eShadow
。
确实非常简单,然而 Win32Thread
和 ServiceTable
条目在 KTHREAD
结构中的位置可能因 Windows 版本而异(它们是“不透明”结构)。
让我们使用 Windbg
看看 KTHREAD
结构在 Windows 10 直到 Redstone2 中的样子。
kd> dt _KTHREAD
nt!_KTHREAD
+0x000 Header : _DISPATCHER_HEADER
+0x010 SListFaultAddress : Ptr32 Void
+0x018 QuantumTarget : Uint8B
+0x020 InitialStack : Ptr32 Void
+0x024 StackLimit : Ptr32 Void
+0x028 StackBase : Ptr32 Void
+0x02c ThreadLock : Uint4B
+0x030 CycleTime : Uint8B
+0x038 HighCycleTime : Uint4B
+0x03c ServiceTable : Ptr32 Void <------
....
+0x0e0 WaitBlockFill10 : [68] UChar
+0x124 Win32Thread : Ptr32 Void <------
在 64 位下
在这里,我们使用了一种更简单的方法。
- 读取 MSR CPU 寄存器,使用值
IA32_LSTAR
(1)(0xC0000082)
。这将返回 64 位服务调用分派器(nt!kiSystemCall64
)的地址。在距离它 512 字节以内,有一个函数nt!KiSystemServiceRepeat
。当我们反汇编它时,我们会发现对nt!KeServiceDescriptorTableShadow
的引用。kd> u nt!KiSystemServiceRepeat nt!KiSystemServiceRepeat: fffff803`f28060a4 4c8d15d5e72700 lea r10,[nt!KeServiceDescriptorTable (fffff803`f2a84880)] fffff803`f28060ab 4c8d1d4eb42600 lea r11,[nt!KeServiceDescriptorTableShadow (fffff803`f2a71500)] fffff803`f28060b2 f7437840000000 test dword ptr [rbx+78h],40h
- 我们所要做的就是搜索这个模式,提取 RIP 相对地址,然后计算
nt!KeServiceDescriptorTableShadow
的线性地址。
用户模式应用程序
用户模式应用程序将执行以下任务:
- 加载内核驱动程序。
- 发出 IOCTL 命令,要求它查找
nt!ServiceDescriptorTableShadow
、nt!KiServiceTable
和win32k!W32pServiceTable
的地址。 - 收集答案并在屏幕上显示。
- 最后,卸载内核驱动程序,保持系统干净。
此应用程序必须以管理员身份运行。
参考文献
(1) Intel® 64 and IA-32 Architectures Software Developer’s Manual
更新
2017 年 7 月 20 日
- 代码已更新,可以处理 32 位 Windows 8.0 和 8.1。现在涵盖了从 Windows 7 到 Windows 10 Redstone 2 的所有 Windows 版本,包括 32 位和 64 位。
- 修正了文本中的一些句子。