0x03. 系统调用
一、系统调用路径
类似于 Linux 下的系统调用,Windows 下由内核向用户态进程提供的服务称为系统服务(system service),所有对系统服务的调用都通过 ntdll.dll 来完成进入内核的过程:

在
ntdll.dll中以Nt开头的以及以Zw开头的函数皆为系统调用,本质上没有差别
以 NtDeleteFile 这一系统服务为例,若支持 syscall 指令则通过 syscall 指令进入内核,否则通过 int 0x2E 软中断走中断门进入内核:

0x7FFE0000这块区域为一块内核与用户进程共享的内存(只有内核有写权限),为一个KUSER_SHARED_DATA结构体,由内核向用户态提供一些信息在偏移
0x308处存放的便是一个布尔值,用来标识该架构是否支持syscall指令
Windows 系统调用内核层的入口被设置为 KiSystemCall64() 函数,当进行 syscall 指令/ 0x2E 软中断时,CPU 会切换到 ring0 并执行该函数作为统一的系统调用入口点,该函数定义于 ntoskrnl.exe 中,根据 rax 寄存器指定的系统调用号来调用 SSDT 中对应的内核函数

注:有的时候系统调用入口为
kiSystemCall64Shadow()函数,不过这个函数最终会跳转到kiSystemCall64()中的KiSystemServiceUser标签
二、系统服务描述符表
SSDT 基本结构
逆向分析 ntoskrnl.exe 中的 KiSystemCall64() 函数,注意到其会引用到 KeServiceDescriptorTable 和 KeServiceDescriptorTable 这两个变量,这便是系统服务描述符表(System Service Descriptor Table,简称 SSDT ),用来存储不同的系统调用号对应的系统调用函数:

其本质上是一个 tag_SERVICE_DESCRIPTOR_TABLE 结构体,为由四张子表组成的 SYSTEM_SERVICE_TABLE 结构体数组:
typedef struct tag_SERVICE_DESCRIPTOR_TABLE {
SYSTEM_SERVICE_TABLE nt; // KeServiceDescriptorTable 只有这张表
SYSTEM_SERVICE_TABLE win32k; // KeServiceDescriptorTableShadow 额外多用一个这张表
SYSTEM_SERVICE_TABLE sst3;
SYSTEM_SERVICE_TABLE sst4;
} SERVICE_DESCRIPTOR_TABLE;
SYSTEM_SERVICE_TABLE 结构体定义如下,其中的 ServiceTable 成员为实质上的系统调用表:
typedef struct tag_SYSTEM_SERVICE_TABLE {
PULONG ServiceTable; // 实际的系统调用表
PULONG CounterTable; //
ULONG ServiceLimit; // 系统调用的数量
PCHAR ArgumentTable; // 参数数量表
} SYSTEM_SERVICE_TABLE;

ServiceTable 成员 实际上为一个指向【目标函数到系统调用表偏移】的数组的指针,每一项为 4 字节,但是实际上计算时会 将该值先右移四位 后再与 系统调用表起始地址 相加获取系统调用实际的地址,即:
$$
Addr_{syscall} = Addr_{ServiceTable} + (ServiceTable[syscall_nr] >> 4)
$$
由此我们得到 SSDT 的基本结构如下图所示(以 NtOpenFile() 系统调用为例):

在 Windbg 中验证:

这里为什么要进行左右移 4 位的神秘操作有以下原因:
- 出于安全性考虑,这样使得通过 SSDT 调用到的系统调用范围被限制在 SSDT 附近 4G 偏移处,驱动地址空间之间间隔至少 4GB,而 SSDT 地址又是写死在代码里的,这样可以很好地防止对 SSDT 的劫持,加大攻击难度
- 存储栈上参数数量,NT kernel 很多系统调用参数都远大于 6 个,因此需要使用栈进行传参,此时这 4 bit 便用于记录不同系统调用使用栈进行传递的参数数量
Shadow SSDT:GUI程序的 SSDT
影子系统服务描述符表(Shadow System Service Descriptor Table,简称 Shadow SSDT )是GUI 程序所专用的一张 SSDT,相比起 SSDT 而言其额外多了一张负责图形相关系统调用的子表 W32pServiceTable ,位于 win32k.sys 即 图形子系统 中:
SSDT 和 Shadow SSDT 第一张子表都是
KiServiceTable,对应 NT kernel 的常规系统调用

当应用程式在 Windows 下进行系统调用时,线程控制块(KTHREAD )中对应的标志位决定了调用哪一张大表,eax 寄存器的 13 ~ 12 位被用来指定调用哪一张子表,低 12 位则被用来指定表中具体的系统调用:

扩展阅读:KeServiceDescriptorTableFilter
自 Windows 10 开始引入了这张表,当 KTHREAD 中设置了
RestrictedGuiThread标志位时会用该表替代 Shadow SSDT,不过目前该表相关的资料暂时比较少(🕊)