-
-
[原创]Windows x64 系统调用链:从用户态到 NtCreateFile
-
发表于: 8小时前 153
-
整体调用链概览
用户态 App
│
▼
NtCreateFile (ntdll.dll) ← 用户态存根函数
│ MOV EAX, syscall_number
│ SYSCALL
▼
═══════════════════════════════════ Ring3 → Ring0 切换 ═══════════════════
│
│ CPU 读取 MSR_LSTAR (0xC0000082) 获取入口地址
▼
KiSystemCall64 (ntoskrnl.exe) ← 内核态系统调用入口
│
▼
KiSystemServiceRepeat ← 查 SSDT / KiServiceTable
│
▼
nt!NtCreateFile (ntoskrnl.exe) ← 真正的内核实现
1. 用户态:ntdll!NtCreateFile 存根
ntdll!NtCreateFile:
mov r10, rcx ; SYSCALL 会用 rcx 保存返回地址,所以先搬走第一个参数
mov eax, 0x55 ; 系统调用号(不同 Windows 版本不同)
test byte ptr [SharedUserData+0x308], 1
jne use_int2e ; 极少走这条路径
syscall ; 触发系统调用
ret
关键点:
EAX= 系统调用号(syscall number),是 SSDT 中的索引SYSCALL指令是 x64 唯一的快速系统调用机制(替代了 x86 的SYSENTER/INT 0x2E)
2. SYSCALL 指令与 MSR_LSTAR
MSR_LSTAR 的作用
| MSR 寄存器 | 地址 | 作用 |
|---|---|---|
| MSR_LSTAR | 0xC0000082 | 存储 SYSCALL 的 Ring0 入口点(RIP 目标) |
| MSR_STAR | 0xC0000081 | 存储 CS/SS 选择子(高32位=用户态, 低32位=内核态) |
| MSR_SFMASK | 0xC0000084 | SYSCALL 时对 RFLAGS 的掩码(关中断等) |
CPU 在执行 SYSCALL 时硬件自动完成:
1. RCX ← RIP(保存用户态返回地址)
2. R11 ← RFLAGS(保存用户态标志)
3. RFLAGS &= ~MSR_SFMASK(通常关闭 IF,即关中断)
4. CS ← MSR_STAR[47:32](内核代码段选择子)
5. SS ← MSR_STAR[47:32] + 8(内核栈段选择子)
6. RIP ← MSR_LSTAR(跳转到 KiSystemCall64)
注意: SYSCALL 不切换 RSP。栈的切换由 KiSystemCall64 代码自行完成。
内核初始化时设置 MSR_LSTAR
// 在 KiInitializeBootStructures 或类似函数中
wrmsr(MSR_LSTAR, (ULONG64)KiSystemCall64);
3. KiSystemCall64:内核入口处理
nt!KiSystemCall64:
swapgs ; GS 切换到内核 PCR(KPCR)
mov gs:[UserRsp], rsp ; 保存用户态 RSP
mov rsp, gs:[KernelStack] ; 切换到内核栈
; 构造 KTRAP_FRAME
push UmSS ; 用户态 SS
push gs:[UserRsp] ; 用户态 RSP
push r11 ; 用户态 RFLAGS
push UmCS ; 用户态 CS
push rcx ; 用户态 RIP(返回地址)
; ... 保存其余寄存器 ...
sti ; 重新开中断
; 参数复制:将用户态栈上的参数复制到内核栈
; (x64 前4个参数在寄存器,第5个起在用户态栈上)
; ── 进入分发逻辑 ──
; 落入 KiSystemServiceRepeat
4. SSDT 与 KiServiceTable(服务分发)
4.1 数据结构
// SSDT = System Service Descriptor Table
// 实际结构:KeServiceDescriptorTable
typedef struct _KSERVICE_TABLE_DESCRIPTOR {
PLONG Base; // KiServiceTable — 函数偏移数组
PULONG Count; // 调用计数(调试用,可能为 NULL)
ULONG Limit; // 表中最大服务号
PUCHAR Number; // 参数字节数表(KiArgumentTable)
} KSERVICE_TABLE_DESCRIPTOR;
// 系统中有两张表:
// [0] KeServiceDescriptorTable → nt 系统调用 (KiServiceTable)
// [1] KeServiceDescriptorTableShadow → 包含 win32k 的扩展表 (W32pServiceTable)
4.2 KiServiceTable 的编码方式(x64 特有)
在 x64 Windows 中,KiServiceTable 不是直接存储函数指针,而是存储 相对偏移(编码后的 4 字节值):
// KiServiceTable[i] 的编码:
// 高28位 = 函数相对于 KiServiceTable 基址的字节偏移(带符号右移4位)
// 低 4位 = 该系统调用在栈上的额外参数个数
offset = (INT32)KiServiceTable[syscall_number] >> 4;
target_function = (ULONG_PTR)KiServiceTable + offset;
stack_params = KiServiceTable[syscall_number] & 0xF;
这种编码节省空间(4字节 vs 8字节指针),并且可在单次查表中同时获得目标地址和参数信息。
4.3 分发逻辑(KiSystemServiceRepeat)
KiSystemServiceRepeat:
; EAX = 系统调用号
; 位[12] 区分是 nt 表(0) 还是 win32k 表(1)
mov edi, eax
shr edi, 7 ; 取出表索引位
and edi, 0x20 ; 0 或 0x20(选 nt 表或 shadow 表)
and eax, 0xFFF ; 低12位 = 表内索引
; 边界检查
lea r10, [KeServiceDescriptorTable + rdi]
cmp eax, [r10+LIMIT_OFFSET]
jae invalid_syscall
; 查 KiServiceTable
lea r11, [r10+BASE_OFFSET] ; r11 = KiServiceTable 基址
movsxd r11, dword ptr [r11 + rax*4] ; 取编码后的偏移
mov ecx, r11
shr ecx, (not implemented) ; 取参数数信息用于栈复制
sar r11, 4 ; 算术右移,得到真实偏移
add r11, [r10+BASE_OFFSET] ; 加基址 = 目标函数地址
; 复制用户态栈参数到内核栈(如果有超过4个参数)
; ...
call r11 ; 调用 nt!NtCreateFile
5. nt!NtCreateFile 执行
NTSTATUS NtCreateFile(
PHANDLE FileHandle, // rcx (实际已在r10)
ACCESS_MASK DesiredAccess, // rdx
POBJECT_ATTRIBUTES ObjectAttributes, // r8
PIO_STATUS_BLOCK IoStatusBlock, // r9
PLARGE_INTEGER AllocationSize, // 栈参数[0]
ULONG FileAttributes, // 栈参数[1]
ULONG ShareAccess, // 栈参数[2]
ULONG CreateDisposition, // 栈参数[3]
ULONG CreateOptions, // 栈参数[4]
PVOID EaBuffer, // 栈参数[5]
ULONG EaLength // 栈参数[6]
);
// → 调用 IoCreateFile → IopCreateFile → 对象管理器 → 文件系统驱动
6. 返回路径
; NtCreateFile 返回值在 RAX(NTSTATUS)
; 恢复寄存器,准备返回
mov rcx, [TrapFrame.Rip] ; 用户态返回地址 → RCX
mov r11, [TrapFrame.EFlags] ; 用户态 RFLAGS → R11
mov rsp, [TrapFrame.Rsp] ; 恢复用户态 RSP
swapgs ; GS 切回用户态 TEB
sysretq ; 返回 Ring3
; CPU: RIP←RCX, RFLAGS←R11, CS←STAR[63:48]+16, SS←STAR[63:48]+8
总结图
┌─────────────────────────────────────────────────────────────────────┐
│ User Mode │
│ │
│ CreateFileW (kernel32/kernelbase) │
│ ↓ │
│ NtCreateFile (ntdll.dll) │
│ │ mov eax, 0x55 ← syscall number │
│ │ syscall ← CPU reads MSR_LSTAR │
│ ↓ │
├═══════════════════════ RING 3 → RING 0 ════════════════════════════╡
│ Kernel Mode │
│ │
│ MSR_LSTAR (0xC0000082) → KiSystemCall64 地址 │
│ ↓ │
│ KiSystemCall64: │
│ • swapgs, 切换内核栈, 构建 TRAP_FRAME │
│ ↓ │
│ KiSystemServiceRepeat: │
│ • EAX[12] 选表 → SSDT (KeServiceDescriptorTable) │
│ • EAX[11:0] 作为索引 → KiServiceTable[index] │
│ • 解码偏移 → 得到 nt!NtCreateFile 地址 │
│ ↓ │
│ nt!NtCreateFile │
│ → IoCreateFile → 文件系统驱动 │
│ ↓ │
│ 返回 NTSTATUS → sysretq → 回到用户态 │
└─────────────────────────────────────────────────────────────────────┘
各组件核心职责
| 组件 | 职责 |
|---|---|
| MSR_LSTAR | 告诉 CPU "SYSCALL 应该跳到哪" — 硬件级别的入口地址寄存器 |
| KiSystemCall64 | 系统调用的软件入口:切栈、保存上下文、分发、返回 |
| SSDT | 服务描述符表,描述每张服务表的基址、上限、参数表 |
| KiServiceTable | SSDT 中"Base"指向的实际函数偏移数组,通过 syscall number 索引得到目标函数 |
赞赏
他的文章
赞赏
雪币:
留言: