65.9K
CodeProject 正在变化。 阅读更多。
Home

寻找 SSDT

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.71/5 (18投票s)

2017 年 6 月 12 日

CPOL

11分钟阅读

viewsIcon

36996

downloadIcon

459

备受关注的内核数据结构

引言

与寻找圣杯不同,这次追寻一定会成功,但过程会有点棘手。
你听说过那些作者经常发布代码却没有任何相关解释的软件仓库吗?你当然听过,但在这里我们不能这样做。

我有义务以一种易于理解的方式,在单页文章的篇幅内,提供有关一个困难主题的相关信息,希望能最终让每个人都明白。
这可能不容易,但让我们通过 5 个步骤继续,并希望一切顺利。

  1. 要求和警告
  2. 什么是 SSDT 及其位置
  3. SSDT 的使用方式
  4. SSDT 为何如此重要?
  5. 我们查找 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!KeServiceDescriptorTablent!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!KeServiceDescriptorTablent!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!NtCreateFilentdll.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!KiServiceTablewin32k!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!KiServiceTablewin32k!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 位下

  1. 枚举所有正在运行的进程,寻找用户模式发送的进程 PID(默认情况下,它将是 csrss.exePID,这是一个始终存在的进程)。
  2. 找到后,它会枚举 csrss.exe 的所有线程,寻找一个 GUI 线程——它们在 KTHREADETHREAD——Executive Thread Block 的一部分)中有 Win32Thread 条目,而不是 NULL
  3. GUI 线程保证其 KTHREAD 中的 ServiceTable 条目指向 nt!KeServiceDescriptorTab1eShadow

确实非常简单,然而 Win32ThreadServiceTable 条目在 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 的线性地址。

用户模式应用程序

用户模式应用程序将执行以下任务:

  1. 加载内核驱动程序。
  2. 发出 IOCTL 命令,要求它查找 nt!ServiceDescriptorTableShadownt!KiServiceTablewin32k!W32pServiceTable 的地址。
  3. 收集答案并在屏幕上显示。
  4. 最后,卸载内核驱动程序,保持系统干净。

此应用程序必须以管理员身份运行。

参考文献

(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 位。
  • 修正了文本中的一些句子。
© . All rights reserved.