整体调用链概览
用户态 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
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 数据结构
typedef struct _KSERVICE_TABLE_DESCRIPTOR {
PLONG Base;
PULONG Count;
ULONG Limit;
PUCHAR Number;
} KSERVICE_TABLE_DESCRIPTOR;
4.2 KiServiceTable 的编码方式(x64 特有)
在 x64 Windows 中,KiServiceTable 不是直接存储函数指针,而是存储 相对偏移(编码后的 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,
ACCESS_MASK DesiredAccess,
POBJECT_ATTRIBUTES ObjectAttributes,
PIO_STATUS_BLOCK IoStatusBlock,
PLARGE_INTEGER AllocationSize,
ULONG FileAttributes,
ULONG ShareAccess,
ULONG CreateDisposition,
ULONG CreateOptions,
PVOID EaBuffer,
ULONG EaLength
);
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 索引得到目标函数 |
[招生]科锐逆向工程师培训(2026年7月3日实地,远程教学同时开班, 第56期)!