edr绕过工具 SysWhispers4 源码分析系列(六)
官网:http://securitytech.cc
六、系统调用汇编实现深度分析
本文档从汇编层面深入剖析 Windows x64 系统调用的完整实现机制,包括 syscall stub 结构、寄存器传递、特权级转换、内核分发等核心流程。通过本文档,你将完全理解从用户态到内核态的每一个机器指令级别细节。
1. syscall stub 汇编结构
1.1 标准 syscall stub 格式
SysWhispers4 生成的典型 stub:
; SW4Syscalls.asm- MASM 语法.codeSW4_NtAllocateVirtualMemory PROCmov r10, rcx ;第1参数 RCX → R10mov eax,18h; SSN =24(Windows1021H2)syscall ;进入内核模式ret ;返回到调用者SW4_NtAllocateVirtualMemory ENDPSW4_NtCreateThreadEx PROCmov r10, rcxmov eax,0C9h; SSN =201syscallretSW4_NtCreateThreadEx ENDPEND
1.2 指令编码分析
机器码反汇编:
00007FF7`12345678 4C8BD9 mov r10,rcx ; 3 bytes00007FF7`1234567B B818000000 mov eax,18h;5 bytes00007FF7`12345680 0F05 syscall ; 2 bytes00007FF7`12345682 C3 ret ;1byte;总计:11 bytes
字节码详解:
|
|
|
|
|
|---|---|---|---|
|
|
4C8BD9 |
mov r10,rcx |
|
|
|
B818000000 |
mov eax,18h |
|
|
|
0F05 |
syscall |
|
|
|
C3 |
ret |
|
1.3 不同架构的 stub 对比
x64 架构(syscall)
; x64 -使用 syscall 指令NtAllocateVirtualMemory PROCmov r10, rcxmov eax,18hsyscallretNtAllocateVirtualMemory ENDP
特点:
- 使用
syscall指令(最快) - RCX → R10 传递约定
- EAX 存放 SSN
x86 架构(sysenter)
; x86 -使用 sysenter 指令_NtAllocateVirtualMemory@24 PROCmov eax,46h; SSN =70(Windows7)mov edx,7FFE0300h; SYSENTER_RETURN_ADDRESSsysenter ;进入内核;注意:sysenter 不自动返回_NtAllocateVirtualMemory@24 ENDP
特点:
- 使用
sysenter指令 - 需要特殊处理返回
- 参数通过栈传递
ARM64 架构(SVC)
特点:
- 使用
svc#0指令 - X16 存放 SSN
- 异常机制进入内核
1.4 不同调用方式的 stub 变体
Embedded(直接式)
SW4_NtAllocateVirtualMemory PROCmov r10, rcxmov eax,18hsyscall ;直接 syscallretSW4_NtAllocateVirtualMemory ENDP
机器码: 4C8BD9 B8180000000F05C3
特征:
- 包含明显的
0F05(syscall) - 静态可检测
- 最简单快速
Indirect(间接式)
EXTERN syscall_gadget:QWORDSW4_NtAllocateVirtualMemory PROCmov r10, rcxmov eax,18hjmp qword ptr [syscall_gadget];间接跳转SW4_NtAllocateVirtualMemory ENDP
机器码: 4C8BD9 B818000000FF25XX XX XX XX
特征:
- 使用
FF25(远跳转) - RIP 显示在 ntdll.dll
- 绕过基于 RIP 的检测
Randomized(随机化)
SW4_NtAllocateVirtualMemory PROCmov r10, rcxpush rdx ;保存寄存器rdtsc ;获取时间戳and eax,3Fh;取模64shl eax,3;*8(每个 gadget 8字节)lea rax,[gadget_pool]mov rax,[rax + rax];随机选择 gadgetcall rax ;调用 gadgetpop rdx ;恢复寄存器retSW4_NtAllocateVirtualMemory ENDP
特征:
- 包含
RDTSC指令 - 随机选择 gadget
- 每次调用路径不同
Egg(标记式)
SW4_NtAllocateVirtualMemory PROCmov r10, rcx; egg marker (8字节占位符)DB 41h,42h,43h,44h,45h,46h,47h,48hretSW4_NtAllocateVirtualMemory ENDP
特征:
- 无 syscall 指令
- 运行时替换为 syscall
- 需要调用
HatchEggs()
2. mov r10,rcx 原理深度解析
2.1 x64 调用约定要求
Microsoft x64 调用约定规则:
整数参数传递:-第1个参数:RCX-第2个参数:RDX-第3个参数:R8-第4个参数:R9-第5个及以后:栈上浮点参数传递:-第1-4个:XMM0-XMM3返回值:-整数:RAX-浮点:XMM0
syscall 的特殊要求:
syscall 指令使用前必须:1. RCX → R10(第1参数)2. SSN → EAX(系统服务号)3.其他参数保持不变
2.2 为什么需要 RCX → R10?
历史原因和技术需求:
x64 syscall 设计规范:┌─────────────────────────────────────┐│ syscall 指令会自动覆盖 RCX 寄存器││用于保存返回地址(类似 CALL 指令)│└─────────────────────────────────────┘
syscall 执行时的硬件行为:
执行 syscall 瞬间,CPU 自动完成:1. RCX ← RIP +2;保存返回地址2. R11 ← RFLAGS ;保存标志寄存器3. RIP ← IA32_LSTAR ;跳转到内核入口4. CS/SS ←内核选择子;切换到内核段5. RSP ←内核栈;切换到内核栈
如果不复制 RCX 会怎样?
// 错误示例(没有 mov r10, rcx)NtAllocateVirtualMemory PROCmov eax,18h; SSNsyscall ;❌ RCX 被覆盖!第1参数丢失retNtAllocateVirtualMemory ENDP// 正确示例NtAllocateVirtualMemory PROCmov r10, rcx ;✓先复制 RCX 到 R10mov eax,18hsyscall ; RCX 被覆盖也没关系了retNtAllocateVirtualMemory ENDP
2.3 参数传递完整流程
6 参数函数的参数传递示例:
NTSTATUS NtAllocateVirtualMemory(HANDLE ProcessHandle,// [1] RCX → R10PVOID*BaseAddress,// [2] RDXULONG_PTR ZeroBits,// [3] R8PSIZE_T RegionSize,// [4] R9ULONG AllocationType,// [5] [RSP+28h]ULONG Protect// [6] [RSP+30h]);
汇编实现:
SW4_NtAllocateVirtualMemory PROC;寄存器参数(前4个); RCX 已经在 RCX →复制到 R10mov r10, rcx ;第1参数; RDX 已经在 RDX ;第2参数; R8 已经在 R8 ;第3参数; R9 已经在 R9 ;第4参数;栈上参数(第5个及以后);[RSP+28h]=第5参数(AllocationType);[RSP+30h]=第6参数(Protect);设置 SSNmov eax,18h;执行 syscallsyscall;返回值在 RAX (NTSTATUS)retSW4_NtAllocateVirtualMemory ENDP
调用者的栈布局:
调用前栈布局:[RSP]=返回地址(到调用者)[RSP+8]=影子空间(ShadowSpacefor RCX)[RSP+10h]=影子空间(ShadowSpacefor RDX)[RSP+18h]=影子空间(ShadowSpacefor R8)[RSP+20h]=影子空间(ShadowSpacefor R9)[RSP+28h]=第5参数(AllocationType)[RSP+30h]=第6参数(Protect)syscall 后栈布局(内核视角):[RSP]=内核栈帧...[原 RSP+28h]=第5参数(仍可通过原偏移访问)[原 RSP+30h]=第6参数
2.4 影子空间(Shadow Space)
什么是影子空间?
影子空间定义:调用者必须在栈上预留32字节(4×8字节)供被调用函数临时存储 RCX、RDX、R8、R9位置:[RSP+8]=Shadowfor RCX[RSP+10h]=Shadowfor RDX[RSP+18h]=Shadowfor R8[RSP+20h]=Shadowfor R9
为什么需要影子空间?
;被调用函数可以随意使用影子空间CalleeFunction PROC;可以把寄存器值暂存到影子空间mov [rcx], rdx ;保存参数mov [rsp+8], r8 ;使用影子空间;...retCalleeFunction ENDP
syscall stub 中的影子空间:
;SysWhispers4生成的 stub 隐式依赖影子空间SW4_NtAllocateVirtualMemory PROCmov r10, rcx ; RCX → R10;调用者负责维护影子空间mov eax,18hsyscallret ;不调用其他函数,无需额外栈SW4_NtAllocateVirtualMemory ENDP
3. syscall 指令执行流程
3.1 syscall 指令编码
指令格式:
syscall 指令├─操作码:0F05(2字节)├─特权级:只能在Ring3执行└─作用:快速系统调用
执行特权检查:
CPU 执行 syscall 前检查:1. CPL (CurrentPrivilegeLevel)=3?✓用户态2. IA32_LSTAR MSR 已设置?✓内核入口3. IA32_FMASK MSR 已设置?✓ RFLAGS 掩码如果检查失败:→#GP(General Protection Fault) 异常
3.2 syscall 执行的微观流程
阶段 1:保存用户态上下文
执行动作(硬件自动):┌──────────────────────────────────────┐│1. RCX ← RIP +2││保存下一条指令地址(返回地址)││││2. R11 ← RFLAGS ││保存标志寄存器状态││││3. IA32_KERNEL_GSBASE → GS ││切换到内核 GS 基址││││4. RSP ← IA32_PGTBL_ADDR ││切换到内核页表│└──────────────────────────────────────┘
阶段 2:加载内核态上下文
执行动作(硬件自动):┌──────────────────────────────────────┐│5. IA32_LSTAR → RIP ││跳转到内核 syscall 入口││(通常是KiSystemCall64)││││6. IA32_STAR → CS/SS ││切换到内核代码段/数据段││(Ring0)││││7. RFLAGS &= IA32_FMASK ││应用 RFLAGS 掩码││││8. IF ←0││禁用中断│└──────────────────────────────────────┘
阶段 3:执行内核入口代码
; nt!KiSystemCall64(简化版)KiSystemCall64:;1.交换 GS 基址(用户↔内核)swapgs;2.保存用户栈指针mov qword ptr gs:0x10, rsp;3.加载内核栈mov rsp, gs:0x18;4.保存非易失性寄存器push rbppush rbxpush rdipush rsipush r12push r13push r14push r15;5.根据 EAX 查找 SSDTlea r10,[KeServiceDescriptorTable]movzx rax, al ; SSNshl rax,4;每个表项16字节add r10, rax;6.调用对应的内核函数mov rax,[r10];获取函数地址call rax ;调用Nt*函数;7.恢复寄存器pop r15pop r14pop r13pop r12pop r11pop r10pop r9pop r8pop rdxpop rcxpop rax;8.返回用户态swapgso64 sysretl
3.3 SSDT 查找机制
SSDT 表结构:
typedefstruct _KSERVICE_TABLE_DESCRIPTOR {PULONG_PTR Base;// 系统服务函数地址表PULONG Count;// 服务数量ULONG Limit;// 表大小PUCHAR Number;// 参数长度表} KSERVICE_TABLE_DESCRIPTOR,*PKSERVICE_TABLE_DESCRIPTOR;
查找算法:
;假设 EAX =0x18(SSN =24)lea r10,[KeServiceDescriptorTable]; r10 = SSDT 基址movzx rax, al ; rax =0x18shl rax,4; rax =0x180(每个表项16字节)add r10, rax ; r10 = SSDT[24]mov rax,[r10]; rax =NtAllocateVirtualMemory地址call rax ;调用内核函数
内存布局图:
KeServiceDescriptorTable(SSDT)+---------------------------+|Base→[函数地址表]│|Count→0x00000123│|Limit→0x00000200│|Number→[参数长度表]│+---------------------------+函数地址表(Base):[0x000]→NtAcceptConnectPort[0x010]→NtAccessCheck[0x020]→NtAccessCheckByType...[0x180]→NtAllocateVirtualMemory← SSN 24...[0x200]→NtWriteVirtualMemory
4. 用户态到内核态切换
4.1 特权级概念
Intel 特权级模型:
Ring0(最高特权级)├─内核代码(ntoskrnl.exe)├─内核驱动(*.sys)└─访问所有内存和指令Ring1(未使用)Ring2(未使用)Ring3(最低特权级)├─用户应用程序├─Win32子系统(csrss.exe)└─受限访问
Windows 的特权级使用:
Windows只使用两个特权级:-Ring0:内核模式(KernelMode)-Ring3:用户模式(UserMode)特权级转换只能通过特定指令:- syscall / sysenter (Ring3→Ring0)- sysret / sysexit (Ring0→Ring3)- iret (通用返回)
4.2 完整的特权级转换流程
用户态 → 内核态转换步骤:
步骤1:用户程序执行 syscall 指令↓步骤2: CPU 硬件自动保存上下文├─ RCX ← RIP +2(返回地址)├─ R11 ← RFLAGS (标志寄存器)├─ GS ← IA32_KERNEL_GSBASE (GS 基址)└─禁用中断(IF=0)↓步骤3: CPU 加载内核上下文├─ RIP ← IA32_LSTAR (内核入口地址)├─ CS ←KernelCodeSegment├─ SS ←KernelDataSegment└─ CPL ←0(Ring0)↓步骤4:开始执行内核代码(KiSystemCall64)├─ swapgs (切换 GS 基址)├─保存用户栈指针├─加载内核栈└─执行内核函数
寄存器变化对比:
syscall 前(用户态Ring3):RIP =00007FF7`12345680 (用户代码)CS = 0033 (用户代码段)SS = 002B (用户数据段)CPL = 3 (Ring 3)RSP = 000000E1`23456780(用户栈)GS =用户 GS 基址syscall 后(内核态Ring0):RIP = FFFFF800`12345678 (内核代码 KiSystemCall64)CS = 0010 (内核代码段)SS = 0018 (内核数据段)CPL = 0 (Ring 0)RSP = FFFF8A00`12345000(内核栈)GS =内核 GS 基址
4.3 MSR 寄存器配置
关键 MSR 寄存器:
|
|
|
|
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
读取 MSR 值(WinDbg):
0: kd> rdmsr 0xC0000082msr[c0000082]= fffff800`12345678 ; IA32_LSTAR (syscall 入口)0: kd> rdmsr 0xC0000084msr[c0000084] = 00000000`00050202; IA32_FMASK0: kd> rdmsr 0xC0000101msr[c0000101]= fffff800`56789ABC ; IA32_KERNEL_GSBASE
IA32_LSTAR 设置过程(内核启动时):
// 内核初始化代码(伪代码)VOID InitializeSyscallEntry(){// 设置 syscall 入口点为 KiSystemCall64__writemsr(0xC0000082,(ULONG64)KiSystemCall64);// 设置内核段选择子__writemsr(0xC0000081,(KERNEL_CS <<16)|(USER_CS <<32));// 设置 RFLAGS 掩码(清除中断和陷阱标志)__writemsr(0xC0000084,0x50202);}
4.4 栈切换机制
为什么要切换栈?
用户栈 vs 内核栈:┌─────────────────────────────────────┐│用户栈(Ring3)││-位于用户空间(0x0000000000000000││到0x00007FFFFFFFFFFF)││-可能被攻击者控制││-不可信│└─────────────────────────────────────┘┌─────────────────────────────────────┐│内核栈(Ring0)││-位于内核空间(0xFFFF000000000000││到0xFFFFFFFFFFFFFFFF)││-受内核保护││-可信│└─────────────────────────────────────┘
栈切换过程:
; syscall 硬件自动切换执行 syscall:1.保存当前 RSP 到某处2. RSP ← IA32_PGTBL_ADDR (内核页表)3.或者 RSP ← TSS.RSP0 (任务状态段);KiSystemCall64手动切换KiSystemCall64:swapgs ;切换到内核 GSmov gs:0x10, rsp ;保存用户 RSPmov rsp, gs:0x18;加载内核 RSP;现在在内核栈上了
内核栈结构:
内核栈(从高地址向低地址增长)高地址+---------------------------+|内核栈底部│| THREAD_STACK_SIZE =12KB│+---------------------------+|局部变量│|保存的寄存器│|函数参数│+---------------------------+| TRAP_FRAME 结构│← gs:0x10指向这里|-保存的用户态寄存器│|- RAX, RCX, RDX,...│+---------------------------+| KTRAP_FRAME 结构│+---------------------------+低地址(RSP 当前位置)
5. syscall 返回流程
5.1 sysret 指令
sysret 指令格式:
sysret 指令├─操作码:0F07(2字节)├─特权级:只能在Ring0执行└─作用:快速返回用户态
sysret 执行流程:
执行动作(硬件自动):┌──────────────────────────────────────┐│1. RIP ← RCX ││恢复到用户态返回地址││││2. RFLAGS ← R11 ││恢复标志寄存器││││3. CS/SS ←用户段选择子││从 IA32_STAR 加载││││4. CPL ←3││切换到用户特权级││││5. RSP ←用户栈指针││从 TSS 或保存的位置恢复││││6.启用中断(IF=1)│└──────────────────────────────────────┘
5.2 完整的返回路径
内核函数返回流程:
;内核函数执行完毕NtAllocateVirtualMemory:;...执行内存分配逻辑mov eax,0;返回 STATUS_SUCCESSret ;返回到KiSystemCall64;返回到 syscall 分发器KiSystemCall64:;此时 RAX =返回值(NTSTATUS);恢复保存的寄存器pop r15pop r14pop r13pop r12pop r11pop r10pop r9pop r8pop rdxpop rcxpop rax;准备返回用户态swapgs ;切换回用户 GS; sysret 返回sysret ;等价于:; RIP ← RCX; RFLAGS ← R11; CS/SS ←用户段; CPL ←3
5.3 返回值传递
NTSTATUS 返回值:
返回值传递链:
内核函数返回值(RAX)↓KiSystemCall64(保持 RAX 不变)↓sysret (RAX 仍然是返回值)↓用户态 stub (RAX = NTSTATUS)↓C 语言调用者(接收返回值)
汇编示例:
;用户态 stubSW4_NtAllocateVirtualMemory PROCmov r10, rcxmov eax,18hsyscall ;执行后 RAX = NTSTATUSret ;返回值保持在 RAXSW4_NtAllocateVirtualMemory ENDP; C 语言调用NTSTATUS status = SW4_NtAllocateVirtualMemory(...);// status 的值就是 RAX
5.4 错误处理
NT_SUCCESS 宏:
#define NT_SUCCESS(Status)(((NTSTATUS)(Status))>=0)// 判断逻辑if(status >=0){// 成功 (0x00000000 - 0x7FFFFFFF)}else{// 失败 (0x80000000 - 0xFFFFFFFF)}
错误码分类:
严重程度位(bits 30-31):00=Success(成功)01=Informational(信息)10=Warning(警告)11=Error(错误)示例:0x00000000= STATUS_SUCCESS (成功)0x40000000= STATUS_PENDING (等待中)0x80000005= STATUS_BUFFER_OVERFLOW (缓冲区溢出-警告)0xC0000022= STATUS_ACCESS_DENIED (访问拒绝-错误)
错误处理示例:
NTSTATUS status = SW4_NtAllocateVirtualMemory(...);if(NT_SUCCESS(status)){printf("Success!\n");}else{switch(status){case STATUS_ACCESS_DENIED:printf("Access denied\n");break;case STATUS_INVALID_HANDLE:printf("Invalid handle\n");break;case STATUS_NO_MEMORY:printf("Out of memory\n");break;default:printf("Error: 0x%08X\n", status);}}
5.5 异常处理
syscall 可能触发的异常:
异常处理流程:
发生异常↓CPU 自动保存现场↓查找 IDT (中断描述符表)↓调用对应的异常处理程序↓KiTrapXX处理例程↓分析异常原因↓可能终止进程或恢复执行
WinDbg 调试异常:
0:000> g(1234.5678):Access violation - code c0000005 (!!! second chance)ntdll!NtAllocateVirtualMemory+0x18:00007ff8`12345690 488b01 mov rax,qword ptr [rcx] ds:00000000`00000000=????????????????0:000> rrax=0000000000000000 rbx=0000000000000000rcx=0000000000000000; NULL 指针!rdx=00000000000000000:000> k# Child-SP RetAddr00000000e1`23456780 00007ff7`12345678 ntdll!NtAllocateVirtualMemory+0x1801000000e1`23456788 00007ff7`12345680 myapp!main02000000e1`23456790 00007ff8`56789abc kernel32!BaseThreadInitThunk03000000e1`23456798 00007ff8`6789abcd ntdll!RtlUserThreadStart
总结
通过对 syscall 汇编实现的深度分析,我们理解了:
syscall stub 结构
- 标准格式:
mov r10,rcx;mov eax,SSN;syscall;ret - 机器码编码:
4C8BD9 B8 xx xx xx xx0F05C3 - 4 种调用方式的变体
mov r10, rcx 原理
- x64 调用约定要求
- syscall 会覆盖 RCX
- 参数传递完整流程
- 影子空间的作用
syscall 执行流程
- 保存用户态上下文(RCX、R11)
- 加载内核态上下文(RIP、CS、SS)
- SSDT 查找机制
- 内核入口代码
特权级切换
- Ring 3 → Ring 0 转换
- MSR 寄存器配置
- 栈切换机制
- GS 基址切换
syscall 返回流程
- sysret 指令执行
- 返回值传递(RAX = NTSTATUS)
- 错误处理机制
- 异常处理流程
这些知识使我们能够从汇编层面完全理解和控制 Windows 系统调用,为开发高级安全工具和研究底层机制奠定了坚实基础。
-
公众号:安全狗的自我修养
-
vx:2207344074
-
http://gitee.com/haidragon
-
http://github.com/haidragon
-
bilibili:haidragonx
-


夜雨聆风
