文章总结: 本文档深入分析SysWhispers4源码,详解Windows系统调用机制与EDR绕过原理。内容涵盖API调用链结构、Win32与NativeAPI差异、以及从用户态到内核态的完整调用流程。重点剖析x64、x86及ARM64架构下syscall指令实现与SSN系统调用号的作用。结论指出直接调用NativeAPI可绕过用户态Hook,为安全研究人员提供了底层技术细节与规避检测的实战思路。 综合评分: 90 文章分类: 红队,免杀,安全工具,逆向分析
edr绕过工具 SysWhispers4 源码分析系列(一)
原创
haidragon haidragon
安全狗的自我修养
2026年3月12日 12:33 湖南
官网:http://securitytech.cc
一、Windows 系统调用基础
本文档基于 SysWhispers4 项目源码分析,深入讲解 Windows 系统调用的核心机制。SysWhispers4 是一个强大的 NT 系统调用存根生成器,支持 Windows 7 到 Windows 11 24H2,涵盖 x64、x86、WoW64 和 ARM64 架构。
1. Windows API 调用链结构
1.1 三层架构模型
Windows API 调用链呈现清晰的三层架构:
1. 应用程序层(Application)
2. ↓
3. Win32 API 层(kernel32.dll, user32.dll, advapi32.dll)
4. ↓
5. Native API 层(ntdll.dll)
6. ↓
7. 内核层(ntoskrnl.exe, win32k.sys)
1.2 调用链详细流程
从源码分析可见,完整的调用流程如下:
1. CreateProcess()[Win32 API - kernel32.dll]
2. ↓
3. NtCreateUserProcess()[Native API - ntdll.dll]
4. ↓
5. syscall 指令[CPU 特权级转换]
6. ↓
7. NtCreateUserProcess实现[ntoskrnl.exe 内核]
8. ↓
9. 进程创建完成
代码实证(来自 generator.py):
在 SysWhispers4 的代码生成逻辑中,所有 NT 函数都遵循以下模式:
1. // 示例:NtAllocateVirtualMemory 调用链
2. VirtualAlloc()// kernel32.dll Win32 API
3. →NtAllocateVirtualMemory()// ntdll.dll Native API
4. → syscall // CPU 指令进入内核
5. →MmAllocateVirtualMemory()// ntoskrnl.exe 内核实现
1.3 DLL 加载与函数定位
SysWhispers4 展示了如何通过 PEB (Process Environment Block) 定位 ntdll.dll:
1. // x64 架构下获取 ntdll 基址(来自 generator.py)
2. static PVOID GetNtdllBase(VOID){
3. PPEB pPeb =(PPEB)__readgsqword(0x60);// GS:0x60 -> PEB 指针
4. PPEB_LDR_DATA pLdr = pPeb->Ldr;
5. PLIST_ENTRY pHead =&pLdr->InMemoryOrderModuleList;
6. PLIST_ENTRY pEntry = pHead->Flink;// exe 模块
7. pEntry = pEntry->Flink;// ntdll 总是第二个
8. PLDR_DATA_TABLE_ENTRY pMod =
9. CONTAINING_RECORD(pEntry, LDR_DATA_TABLE_ENTRY,InMemoryOrderLinks);
10. return pMod->DllBase;
11. }
2. Win32 API 与 Native API 区别
2.1 定义对比
| 特性 | Win32 API | Native API | | — | — | — | | 文档化 | 完全官方文档 | 未公开文档(Undocumented) | | 稳定性 | 保证向后兼容 | 可能随版本变化 | | 调用约定 | 标准调用约定 | NTAPI (__stdcall) | | 返回值 | BOOL/HANDLE | NTSTATUS | | 位置 | kernel32.dll, user32.dll 等 | ntdll.dll | | EDR 监控 | 重灾区(被大量 Hook) | 相对干净 |
2.2 函数对应关系示例
从 prototypes.json 中提取的对应关系:
1. Win32 API Native API
2. ─────────────────────────────────────────────
3. VirtualAlloc()→NtAllocateVirtualMemory()
4. VirtualFree()→NtFreeVirtualMemory()
5. WriteProcessMemory()→NtWriteVirtualMemory()
6. ReadProcessMemory()→NtReadVirtualMemory()
7. CreateThread()→NtCreateThreadEx()
8. OpenProcess()→NtOpenProcess()
9. TerminateProcess()→NtTerminateProcess()
10. Sleep()→NtDelayExecution()
2.3 为什么使用 Native API?
SysWhispers4 选择直接调用 Native API 的原因:
- 绕过用户态 Hook:EDR/AV 通常在 kernel32.dll 层 Hook Win32 API,但很少 Hook ntdll.dll
- 更细粒度控制:Native API 提供更底层的参数控制
- 避免中间层处理:Win32 API 会在内部调用多个 Native API,增加被检测风险
- 性能优化:减少不必要的封装层
代码示例(来自 models.py):
1. # SysWhispers4 支持的 Native API 函数原型
2. @dataclass
3. classSyscallPrototype:
4. name: str # 如 "NtAllocateVirtualMemory"
5. return_type: str # 返回 NTSTATUS
6. params:List[SyscallParam]# 参数列表
8. def c_signature(self, prefix: str ="")-> str:
9. """生成 C 函数签名"""
10. func_name = f"{prefix}{self.name}"if prefix else self.name
11. param_str =", ".join(p.c_declaration()for p in self.params)
12. return f"{self.return_type} NTAPI {func_name}({param_str})"
3. kernel32.dll → ntdll.dll → syscall → ntoskrnl 调用流程
3.1 完整调用路径详解
第一阶段:Win32 API 调用(kernel32.dll)
1. // 应用程序调用
2. HANDLE hProcess =OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
kernel32.dll 内部实现逻辑:
1. // kernel32!OpenProcess 伪代码
2. HANDLE WINAPI OpenProcess(DWORD dwDesiredAccess, BOOL bInheritHandle, DWORD dwProcessId){
3. CLIENT_ID ClientId={0};
4. OBJECT_ATTRIBUTES ObjectAttributes={0};
5. HANDLE hResult;
7. // 初始化对象属性
8. InitializeObjectAttributes(&ObjectAttributes,...);
9. ClientId.UniqueProcess=(HANDLE)dwProcessId;
11. // 调用 ntdll!NtOpenProcess
12. NTSTATUS status =NtOpenProcess(&hResult, dwDesiredAccess,
13. &ObjectAttributes,&ClientId);
15. if(!NT_SUCCESS(status)){
16. SetLastError(RtlNtStatusToDosError(status));
17. return NULL;
18. }
19. return hResult;
20. }
第二阶段:Native API 存根(ntdll.dll)
ntdll!NtOpenProcess 汇编存根(x64):
1. ;SysWhispers4生成的 ASM 存根(来自 generator.py)
2. PUBLIC NtOpenProcess
3. EXTERN SW4_SyscallStub@NtOpenProcess:PROC
5. NtOpenProcess PROC
6. ;设置 SSN (SystemServiceNumber)
7. mov eax,0x3A; SSN forNtOpenProcess(Windows1021H2)
9. ;跳转到 syscall 指令
10. jmp SW4_SyscallStub@NtOpenProcess
11. NtOpenProcess ENDP
syscall 存根实现:
1. ;直接 syscall 方式(Embedded模式)
2. SW4_SyscallStub@NtOpenProcess PROC
3. mov r10, rcx ; x64 调用约定:前4个参数通过 RCX, RDX, R8, R9 传递
4. syscall ;进入内核模式
5. ret ;返回到调用者
6. SW4_SyscallStub@NtOpenProcess ENDP
第三阶段:CPU 特权级转换(syscall 指令)
syscall 指令执行流程:
- 保存用户态上下文:
RIP→RCX(保存返回地址)RFLAGS→R11(保存标志寄存器)CS,SS,RSP自动保存
- 加载内核态上下文:
- 从
IA32_LSTARMSR (0xC0000082) 加载内核入口地址 - 从
IA32_FMASKMSR (0xC0000084) 加载 RFLAGS 掩码 - 切换到内核栈(从
IA32_KERNEL_GSBASEMSR)
- 执行特权级转换:
CPL(Current Privilege Level) 从 Ring 3 → Ring 0- 开始执行内核中的系统调用分发器
第四阶段:内核系统调用分发(ntoskrnl.exe)
内核 syscall 入口点(KiSystemCall64):
1. ; ntoskrnl!KiSystemCall64简化流程
2. KiSystemCall64:
3. ;1.保存用户态寄存器
4. swapgs ;切换到内核 GS 基址
5. mov qword ptr gs:0x10, rsp ;保存用户栈指针
6. mov rsp, gs:0x18;加载内核栈
8. ;2.根据 EAX 中的 SSN 查找 SSDT
9. lea r10,[KeServiceDescriptorTable]
10. mov eax, eax ; SSN
11. shl eax,4;每个表项16字节
12. add r10, rax
14. ;3.调用对应的内核函数
15. mov rax,[r10];获取函数地址
16. call rax ;调用NtOpenProcess内核实现
SSDT (System Service Descriptor Table) 结构:
1. // SSDT 表项结构(来自 syscalls_nt_x64.json 分析)
2. typedefstruct _KSERVICE_TABLE_DESCRIPTOR {
3. PULONG_PTR Base;// 系统服务函数地址表
4. PULONG Count;// 服务数量
5. ULONG Limit;// 表大小
6. PUCHAR Number;// 参数长度表
7. } KSERVICE_TABLE_DESCRIPTOR,*PKSERVICE_TABLE_DESCRIPTOR;
第五阶段:内核函数执行
NtOpenProcess 内核实现:
1. // ntoskrnl!NtOpenProcess 简化逻辑
2. NTSTATUS NTAPI NtOpenProcess(
3. PHANDLE ProcessHandle,
4. ACCESS_MASK DesiredAccess,
5. POBJECT_ATTRIBUTES ObjectAttributes,
6. PCLIENT_ID ClientId
7. ){
8. PEPROCESS TargetProcess;
9. NTSTATUS status;
11. // 1. 解析 ClientId 找到目标 EPROCESS
12. status =PsLookupProcessByProcessId(ClientId->UniqueProcess,&TargetProcess);
13. if(!NT_SUCCESS(status))
14. return status;
16. // 2. 检查访问权限
17. status =ObOpenObjectByPointer(TargetProcess,...);
18. if(!NT_SUCCESS(status)){
19. ObDereferenceObject(TargetProcess);
20. return status;
21. }
23. // 3. 返回进程句柄
24. *ProcessHandle= hProcess;
25. return STATUS_SUCCESS;
26. }
3.2 调用流程图
1. ┌─────────────────────────────────────────────────────────────┐
2. │用户模式(Ring3)│
3. ├─────────────────────────────────────────────────────────────┤
4. │应用程序│
5. │↓│
6. │ kernel32.dll (Win32 API)│
7. │↓封装/参数验证│
8. │ ntdll.dll (Native API)│
9. │↓设置 SSN (EAX 寄存器)│
10. │ syscall 指令│
11. └─────────────────────────────────────────────────────────────┘
12. ↓
13. 特权级转换
14. ↓
15. ┌─────────────────────────────────────────────────────────────┐
16. │内核模式(Ring0)│
17. ├─────────────────────────────────────────────────────────────┤
18. │KiSystemCall64(syscall 分发器)│
19. │↓查 SSDT 表│
20. │NtOpenProcess(内核实现)│
21. │↓访问内核对象│
22. │ ntoskrnl.exe / win32k.sys │
23. └─────────────────────────────────────────────────────────────┘
4. Windows 系统调用机制(syscall / sysenter)
4.1 x64 架构:syscall 指令
syscall 指令特点:
- 引入时间:Intel EM64T / AMD64 架构
- 指令编码:
0F05 - 速度:比 sysenter 更快(硬件优化)
- 寄存器使用:
EAX:SSN (System Service Number)RCX:返回地址(自动保存)R10:RCX 的副本(用于传参)R11:RFLAGS 的副本
syscall 指令序列(来自 generator.py 的 ASM 生成逻辑):
1. ;SysWhispers4生成的标准 syscall 存根(x64)
2. NtAllocateVirtualMemory PROC
3. mov eax,0x18; SSN =24(Windows1021H2)
4. mov r10, rcx ;第1个参数从 RCX 移到 R10
5. syscall ;进入内核
6. ret ;返回
7. NtAllocateVirtualMemory ENDP
间接调用方式(Indirect Method):
1. ;间接 syscall -通过 ntdll 中的 gadget
2. NtAllocateVirtualMemory PROC
3. mov eax,0x18; SSN
4. mov r10, rcx
5. jmp qword ptr [syscall_gadget];间接跳转
6. ; RIP 跟踪显示在 ntdll.dll 内
7. NtAllocateVirtualMemory ENDP
随机化间接调用(Randomized Method):
1. ;随机化 syscall -每次调用不同的 gadget
2. NtAllocateVirtualMemory PROC
3. mov eax,0x18
4. mov r10, rcx
5. mov r11, qword ptr [gadget_pool + rax*8];从64个 gadget 中随机选
6. call r11 ;调用随机 gadget
7. NtAllocateVirtualMemory ENDP
4.2 x86 架构:sysenter 指令
sysenter 指令特点:
- 引入时间:Pentium II / P6 微架构
- 指令编码:
0F34 - 限制:需要配合特定 MSR 配置
- 调用约定:参数通过栈传递
sysenter 调用序列(来自 generator.py x86 支持):
1. ; x86 sysenter 存根
2. _NtAllocateVirtualMemory@24 PROC
3. mov eax,0x18; SSN
4. mov edx,7FFE0300h; SYSENTER_RETURN_ADDRESS (MSR 更新)
5. sysenter ;进入内核
6. ;注意:sysenter 不自动返回,需要特殊处理
7. _NtAllocateVirtualMemory@24 ENDP
sysenter vs syscall 对比:
| 特性 | sysenter (x86) | syscall (x64) | | — | — | — | | 返回机制 | 需要特殊处理 | 自动保存到 RCX | | 速度 | 较慢 | 更快 | | 灵活性 | 低 | 高 | | 现代支持 | 已淘汰 | 主流 |
4.3 ARM64 架构:SVC 指令
SVC (Supervisor Call) 指令:
1. ; ARM64 syscall 存根(SysWhispers4新增支持)
2. NtAllocateVirtualMemory PROC
3. mov x16,#0x18 ; SSN 放入 X16
4. svc #0 ; 触发异常进入内核
5. ret
6. NtAllocateVirtualMemory ENDP
ARM64 调用约定:
- 前 8 个参数:X0-X7
- 返回值:X0
- SSN 存放:X16(类似 x64 的 EAX)
4.4 WoW64:Heaven’s Gate
WoW64 架构下的系统调用:
WoW64 (Windows 32-bit on Windows 64-bit) 使用特殊机制:
1. ;WoW64调用流程
2. push fs:[0xC0];保存 WOW32Reserved
3. mov eax,0x18; SSN
4. call dword ptr fs:[0xC0];通过Heaven's Gate 进入 x64 层
5. 系统调用号(SSN)与版本差异
5.1 SSN 定义与作用
SSN (System Service Number) 是系统调用的唯一标识符,存储在 EAX/RAX 寄存器中传递给内核。
SSN 的作用:
- 索引 SSDT 表:内核通过 SSN 查找对应的内核函数
- 版本识别:不同 Windows 版本的 SSN 可能不同
- 安全检查:某些安全产品监控特定 SSN
5.2 SSN 表结构(来自 syscallsntx64.json)
1. {
2. "NtAllocateVirtualMemory":{
3. "xp_sp1":70,
4. "xp_sp2":70,
5. "vista_sp0":70,
6. "7_sp0":70,
7. "7_sp1":70,
8. "8.0":71,
9. "8.1":23,
10. "10240":24,//Windows101507
11. "10586":24,//Windows101511
12. "14393":24,//Windows101607
13. "15063":24,//Windows101703
14. "16299":24,//Windows101709
15. "17134":24,//Windows101803
16. "17763":24,//Windows101809
17. "18362":24,//Windows101903
18. "19041":24,//Windows102004
19. "19045":24,//Windows1022H2
20. "22000":24,//Windows1121H2
21. "22621":24,//Windows1122H2
22. "26100":24//Windows1124H2
23. }
24. }
5.3 SSN 变化规律分析
从 SysWhispers4 的 SSN 表可以看出:
规律 1:大版本更新时 SSN 变化频繁
1. Windows7→Windows8:大量 SSN 重新编号
2. Windows8→Windows8.1:大规模重组(变化最大)
3. Windows10→Windows11:相对稳定
规律 2:小版本更新通常保持兼容
1. Windows101507→22H2:大部分 SSN 保持不变
2. Windows1121H2→24H2:几乎完全兼容
规律 3:新函数添加不影响旧函数
1. // 新增函数会分配新的 SSN,不会改变现有函数的 SSN
2. NtAlertResumeThread(Windows8新增)
3. NtCreateThreadEx(WindowsVista新增)
5.4 SSN 获取方法(SysWhispers4 核心技术)
方法 1:静态表(Static Resolution)
1. # 来自 utils.py - 直接从 JSON 加载 SSN 表
2. def load_ssn_table_x64()-> dict:
3. return load_json(DATA_DIR /"syscalls_nt_x64.json")
5. def get_current_build_from_table(ssn_table: dict, func_name: str):
6. """获取最新版本的 SSN"""
7. entry = ssn_table.get(func_name)
8. numeric_keys =[k for k in entry.keys()if k.isdigit()]
9. latest_build = max(numeric_keys, key=int)
10. return entry[latest_build]
优点:速度快,无需运行时解析 缺点:无法适应未知版本
方法 2:FreshyCalls(推荐)
1. // 来自 generator.py - FreshyCalls 实现
2. static VOID FreshyCallsResolve(PVOID pNtdll){
3. // 1. 获取 ntdll 导出表
4. PIMAGE_EXPORT_DIRECTORY pExp =GetExportDirectory(pNtdll);
6. // 2. 收集所有 Nt* 函数
7. EXPORT_ENTRY exports[MAX_EXPORTS];
8. DWORD count =0;
10. for(DWORD i =0; i < pExp->NumberOfNames; i++){
11. constchar* name =(char*)((BYTE*)pNtdll + pExp->AddressOfNames[i]);
12. if(strncmp(name,"Nt",2)==0){
13. exports[count].Address=(BYTE*)pNtdll + pExp->AddressOfFunctions[pExp->AddressOfNameOrdinals[i]];
14. exports[count].Name= name;
15. count++;
16. }
17. }
19. // 3. 按地址排序(关键!)
20. SortExports(exports, count);
22. // 4. 排序后的索引 = SSN
23. for(DWORD i =0; i < count; i++){
24. g_SsnTable[HashStr(exports[i].Name)]= i;
25. }
26. }
原理:Windows 内核按固定顺序排列系统调用,导出函数的内存地址顺序与 SSN 一致
优点:Hook 免疫,版本兼容性好 缺点:需要解析 PE 结构
方法 3:Hell’s Gate
1. // 直接读取 ntdll 存根的机器码
2. static DWORD HellsGateReadSSN(PVOID pFunction){
3. BYTE* pCode =(BYTE*)pFunction;
5. // 查找 "mov eax, imm32" 指令 (B8 xx xx xx xx)
6. if(pCode[0]==0xB8){
7. return*(DWORD*)(pCode +1);// SSN 在操作数中
8. }
9. return INVALID_SSN;
10. }
优点:简单直接 缺点:被 Hook 后失效
方法 4:Halo’s Gate(邻居扫描)
1. // Hell's Gate + 邻居扫描
2. static DWORD HalosGateReadSSN(PVOID pFunction){
3. DWORD ssn =HellsGateReadSSN(pFunction);
4. if(ssn != INVALID_SSN)
5. return ssn;
7. // 向前后扫描邻近函数
8. for(int offset =1; offset <=8; offset++){
9. // 向上扫描
10. ssn =HellsGateReadSSN((BYTE*)pFunction - offset *0x20);
11. if(ssn != INVALID_SSN)
12. return ssn - offset;// 推断原始 SSN
14. // 向下扫描
15. ssn =HellsGateReadSSN((BYTE*)pFunction + offset *0x20);
16. if(ssn != INVALID_SSN)
17. return ssn + offset;
18. }
19. return INVALID_SSN;
20. }
优点:可应对部分 Hook 缺点:扫描范围有限
方法 5:Tartarus’Gate(增强版)
1. // Tartarus'Gate - 检测多种 Hook 类型
2. static BOOL IsHooked(BYTE* pFunction){
3. // 检测近跳转 (E9)
4. if(pFunction[0]==0xE9)
5. return TRUE;
7. // 检测远跳转 (FF 25)
8. if(pFunction[0]==0xFF&& pFunction[1]==0x25)
9. return TRUE;
11. // 检测断点 (CC)
12. if(pFunction[0]==0xCC)
13. return TRUE;
15. return FALSE;
16. }
扩展扫描范围:±16 个函数(相比 Halo’s Gate 的 ±8)
方法 6:SyscallsFromDisk(最彻底)
1. // 从 KnownDlls 加载干净的 ntdll
2. static PVOID LoadCleanNtdllFromDisk(){
3. HANDLE hSection;
4. UNICODE_STRING usPath = RTL_CONSTANT_STRING(L"\\KnownDlls\\ntdll.dll");
6. OBJECT_ATTRIBUTES oa ={0};
7. InitializeObjectAttributes(&oa,&usPath, OBJ_CASE_INSENSITIVE, NULL, NULL);
9. // 打开 KnownDlls 中的 ntdll
10. NtOpenSection(&hSection, SECTION_MAP_READ,&oa);
12. // 映射到用户空间
13. PVOID pBase = NULL;
14. SIZE_T size =0;
15. NtMapViewOfSection(hSection,GetCurrentProcess(),&pBase,...);
17. return pBase;// 干净的 ntdll,无 Hook
18. }
优点:100% 绕过所有用户态 Hook 缺点:需要额外的系统调用
方法 7:RecycledGate(组合拳)
1. // FreshyCalls + opcode 验证
2. static DWORD RecycledGateResolve(PVOID pFunction){
3. // 1. 先用 FreshyCalls 获取候选 SSN
4. DWORD candidate =FreshyCallsGetSSN(pFunction);
6. // 2. 尝试读取 opcode 验证
7. DWORD opcode_ssn =HellsGateReadSSN(pFunction);
9. // 3. 如果一致,确认正确
10. if(candidate == opcode_ssn)
11. return candidate;
13. // 4. 如果不一致,信任 FreshyCalls(可能被 Hook)
14. return candidate;
15. }
优点:综合两种方法的优势 缺点:实现复杂
方法 8:HW Breakpoint(硬件断点)
1. // 使用调试寄存器提取 SSN
2. static DWORD g_CapturedSSN =0;
4. static LONG CALLBACK SsnBreakpointHandler(PEXCEPTION_POINTERS pEx){
5. if(pEx->ExceptionRecord->ExceptionCode== EXCEPTION_SINGLE_STEP){
6. // 在 syscall 指令处捕获 EAX
7. g_CapturedSSN = pEx->ContextRecord->Eax;
9. // 跳过 syscall 指令
10. pEx->ContextRecord->Rip+=2;// syscall 长度
11. return EXCEPTION_CONTINUE_EXECUTION;
12. }
13. return EXCEPTION_CONTINUE_SEARCH;
14. }
16. static DWORD HWBreakpointGetSSN(PVOID pFunction){
17. // 1. 设置 DR0 = syscall 指令地址
18. SetDr0((PBYTE)pFunction +2);// 跳过 "mov eax, XX"
20. // 2. 注册 VEH
21. AddVectoredExceptionHandler(TRUE,SsnBreakpointHandler);
23. // 3. 调用函数触发断点
24. ((FUNCTION_TYPE)pFunction)(...);
26. // 4. 清理
27. RemoveVectoredExceptionHandler(...);
28. ClearDr0();
30. return g_CapturedSSN;
31. }
优点:完全不依赖函数字节内容 缺点:需要硬件支持,速度慢
5.5 SSN 加密(SysWhispers4 新特性)
1. # 来自 obfuscator.py - XOR 加密 SSN
2. def generate_xor_key(self)-> int:
3. """生成 32 位 XOR 密钥"""
4. return self._rng.randint(0x01010101,0xFEFEFEFE)
6. def xor_ssn(ssn: int, key: int)-> int:
7. """XOR 加密单个 SSN"""
8. return ssn ^ key
生成的 C 代码:
1. // 加密的 SSN 表
2. #define SW4_XOR_KEY 0xA3B7C9D2U
3. #define SW4_DECRYPT(v)((v)^ SW4_XOR_KEY)
5. DWORD EncryptedSsnTable[]={
6. 0xA3B7C9F0U,// 24 ^ 0xA3B7C9D2
7. 0xA3B7C9E5U,// 58 ^ 0xA3B7C9D2
8. // ...
9. };
11. // 运行时解密
12. DWORD GetSSN(DWORD hash){
13. return SW4_DECRYPT(EncryptedSsnTable[hash]);
14. }
作用:防止静态分析工具识别 SSN 值
6. x64 系统调用调用约定
6.1 x64 调用约定概述
Windows x64 调用约定(Microsoft x64 calling convention):
寄存器分配规则:
| 寄存器 | 用途 | 说明 | | — | — | — | | RCX | 第 1 个整数参数 | 必须复制到 R10 | | RDX | 第 2 个整数参数 | – | | R8 | 第 3 个整数参数 | – | | R9 | 第 4 个整数参数 | – | | R10 | RCX 的副本 | syscall 前必需 | | XMM0 | 第 1 个浮点参数 | – | | XMM1 | 第 2 个浮点参数 | – | | XMM2 | 第 3 个浮点参数 | – | | XMM3 | 第 4 个浮点参数 | – | | RAX | 返回值 / SSN | syscall 编号 | | RSP | 栈指针 | 16 字节对齐 |
6.2 系统调用专用约定
SysWhispers4 生成的标准存根:
1. ;NtAllocateVirtualMemory(HANDLE ProcessHandle,
2. ; PVOID*BaseAddress,
3. ; ULONG_PTR ZeroBits,
4. ; PSIZE_T RegionSize,
5. ; ULONG AllocationType,
6. ; ULONG Protect)
7. NtAllocateVirtualMemory PROC
8. ;参数传递(调用者负责):
9. ; RCX =ProcessHandle
10. ; RDX =BaseAddress
11. ; R8 =ZeroBits
12. ; R9 =RegionSize
13. ;[RSP+28h]=AllocationType(第5个及以后参数在栈上)
14. ;[RSP+30h]=Protect
16. mov r10, rcx ;复制 RCX → R10 (syscall 要求)
17. mov eax,0x18; SSN =24
18. syscall ;进入内核
19. ret ;返回,返回值在 RAX
20. NtAllocateVirtualMemory ENDP
6.3 参数传递详细规则
前 4 个参数(寄存器):
1. NTSTATUS NtOpenProcess(
2. PHANDLE ProcessHandle,// RCX
3. ACCESS_MASK DesiredAccess,// RDX
4. POBJECT_ATTRIBUTES OA,// R8
5. PCLIENT_ID ClientId// R9
6. );
第 5 个及以后参数(栈):
1. NTSTATUS NtCreateThreadEx(
2. PHANDLE ThreadHandle,// RCX
3. ACCESS_MASK DesiredAccess,// RDX
4. POBJECT_ATTRIBUTES OA,// R8
5. HANDLE ProcessHandle,// R9
6. PVOID StartRoutine,// [RSP+28h]
7. PVOID Argument,// [RSP+30h]
8. ULONG CreateFlags,// [RSP+38h]
9. SIZE_T ZeroBits,// [RSP+40h]
10. SIZE_T StackSize,// [RSP+48h]
11. SIZE_T MaxStackSize,// [RSP+50h]
12. PPS_ATTRIBUTE_LIST AttrList// [RSP+58h]
13. );
栈布局图:
1. 调用前栈布局:
2. [RSP]=返回地址
3. [RSP+8]=第5个参数
4. [RSP+10h]=第6个参数
5. [RSP+18h]=第7个参数
6. ...
8. 调用后栈布局(被调用函数视角):
9. [RSP]=旧的 RBP (如果建立栈帧)
10. [RSP+8]=返回地址
11. [RSP+10h]=第5个参数
12. [RSP+18h]=第6个参数
13. ...
6.4 返回值约定
NTSTATUS 返回值(RAX):
1. // 成功值
2. #define STATUS_SUCCESS ((NTSTATUS)0x00000000L)
3. #define STATUS_WAIT_0 ((NTSTATUS)0x00000000L)
4. #define STATUS_WAIT_1 ((NTSTATUS)0x00000001L)
6. // 常见错误值
7. #define STATUS_UNSUCCESSFUL ((NTSTATUS)0xC0000001L)
8. #define STATUS_ACCESS_DENIED ((NTSTATUS)0xC0000022L)
9. #define STATUS_INVALID_HANDLE ((NTSTATUS)0xC0000008L)
10. #define STATUS_INFO_LENGTH_MISMATCH ((NTSTATUS)0xC0000004L)
判断宏:
1. #define NT_SUCCESS(Status)(((NTSTATUS)(Status))>=0)
2. #define NT_INFORMATION(Status)(((ULONG)(Status)>>30)==1)
3. #define NT_WARNING(Status)(((ULONG)(Status)>>30)==2)
4. #define NT_ERROR(Status)(((ULONG)(Status)>>30)==3)
6.5 栈帧管理
基本栈帧(无局部变量):
1. SimpleFunction PROC
2. ;不需要建立栈帧,直接使用影子空间
3. sub rsp,20h;分配32字节影子空间
4. ;...函数体...
5. add rsp,20h;恢复栈
6. ret
7. SimpleFunction ENDP
带局部变量的栈帧:
1. ComplexFunction PROC
2. push rbp ;保存旧栈帧
3. mov rbp, rsp ;建立新栈帧
4. sub rsp,40h;分配局部变量+影子空间
6. ;局部变量访问:
7. ;[rbp-10h],[rbp-20h],...
9. ;调用其他函数:
10. mov ecx,123;第1个参数
11. mov edx,456;第2个参数
12. call SomeFunction
14. leave ;等价于 mov rsp, rbp / pop rbp
15. ret
16. ComplexFunction ENDP
6.6 非易失性寄存器(Non-volatile Registers)
调用者保存(Volatile):
- RAX, RCX, RDX, R8, R9, R10, R11
- XMM0-XMM5
被调用者保存(Non-volatile):
- RBX, RBP, RDI, RSI, R12-R15
- XMM6-XMM15
SysWhispers4 中的寄存器保存:
1. ;随机化方法中的寄存器保护
2. RandomizedSyscall PROC
3. push rbx ;保存非易失性寄存器
4. push rdi
5. push rsi
6. push r12
8. ;使用临时寄存器
9. mov r11,[gadget_addr]
10. mov r10, rcx
11. call r11
13. pop r12 ;恢复寄存器
14. pop rsi
15. pop rdi
16. pop rbx
17. ret
18. RandomizedSyscall ENDP
6.7 影子空间(Shadow Space)
影子空间定义:
调用者必须在栈上预留 32 字节(4×QWORD),供被调用函数临时存储 RCX、RDX、R8、R9:
1. [RSP]=返回地址
2. [RSP+8]=Shadowfor RCX
3. [RSP+10h]=Shadowfor RDX
4. [RSP+18h]=Shadowfor R8
5. [RSP+20h]=Shadowfor R9
使用示例:
1. Caller PROC
2. sub rsp,20h;分配影子空间
4. mov ecx,1;参数1
5. mov edx,2;参数2
6. mov r8d,3;参数3
7. mov r9d,4;参数4
9. call Callee;被调用者可随意使用[rsp+8..28h]
11. add rsp,20h;释放影子空间
12. ret
13. Caller ENDP
15. Callee PROC
16. ;可以直接写入影子空间(不需要额外分配栈)
17. mov [rcx], rdx ;保存参数
18. mov [rsp+8], r8 ;使用影子空间
19. ;...
20. ret
21. Callee ENDP
6.8 SysWhispers4 中的调用约定应用
来自 generator.py 的 ASM 生成逻辑:
1. def _gen_asm_msvc(self)-> str:
2. """生成 MASM 汇编存根"""
3. lines =[]
5. for proto in self._prototypes:
6. ssn = self._get_ssn(proto.name)
8. lines.append(f"{proto.name} PROC")
9. lines.append(f" mov r10, rcx");关键:RCX → R10
10. lines.append(f" mov eax, 0x{ssn:X}"); SSN → EAX
11. lines.append(f" syscall");进入内核
12. lines.append(f" ret")
13. lines.append(f"{proto.name} ENDP")
15. return"\n".join(lines)
间接调用的寄存器处理:
1. def _gen_indirect_call(self)-> str:
2. """生成间接 syscall 代码"""
3. return"""
4. mov r10, rcx ; 保存 RCX
5. mov r11, rdx ; 保存 RDX (重要!)
6. mov rax, [gadget_addr] ; 加载 gadget 地址
7. call rax ; 间接调用
8. mov rdx, r11 ; 恢复 RDX
9. ret
10. """
随机化方法的熵增处理:
1. def _gen_randomized_call(self)-> str:
2. """生成随机化 syscall"""
3. return"""
4. mov r10, rcx
5. mov r11, rdx
6. rdtsc ; 获取时间戳计数器
7. and eax, GADGET_MASK ; 取模得到索引
8. mov rax, [gadget_pool + rax*8]
9. call rax ; 调用随机 gadget
10. mov rdx, r11
11. ret
12. """
6.9 调用约定实战示例
完整示例:NtWriteVirtualMemory
1. // C 语言声明
2. NTSTATUS NtWriteVirtualMemory(
3. HANDLE ProcessHandle,// RCX
4. PVOID BaseAddress,// RDX
5. PVOID Buffer,// R8
6. SIZE_T NumberOfBytesToWrite,// R9
7. PSIZE_T NumberOfBytesWritten// [RSP+28h]
8. );
1. ;汇编调用示例
2. INVOKE NtWriteVirtualMemory, \
3. hProcess, \
4. lpBaseAddress, \
5. lpBuffer, \
6. nSize, \
7. lpBytesWritten
9. ;展开后的汇编代码:
10. sub rsp,28h;对齐栈+影子空间
11. mov ecx, hProcess ; RCX =参数1
12. mov rdx, lpBaseAddress ; RDX =参数2
13. mov r8, lpBuffer ; R8 =参数3
14. mov r9, nSize ; R9 =参数4
15. lea rax, lpBytesWritten
16. mov [rsp+28h], rax ;参数5放栈上
17. call NtWriteVirtualMemory
18. add rsp,28h
返回值处理:
1. NTSTATUS status =NtWriteVirtualMemory(...);
2. if(NT_SUCCESS(status)){
3. // RAX >= 0 表示成功
4. printf("Write succeeded\n");
5. }else{
6. // RAX < 0 表示失败
7. printf("Write failed: 0x%08X\n", status);
8. }
总结
通过深入分析 SysWhispers4 项目源码,我们全面了解了 Windows 系统调用的核心机制:
- API 调用链:kernel32.dll → ntdll.dll → syscall → ntoskrnl.exe
- Win32 API vs Native API:文档化程度、稳定性、EDR 监控差异
- 完整调用流程:从用户态到内核态的每一步转换
- 系统调用机制:x64 使用 syscall,x86 使用 sysenter,ARM64 使用 SVC
- SSN 管理:8 种不同的 SSN 获取方法,应对各种 Hook 场景
- x64 调用约定:寄存器分配、参数传递、栈帧管理、影子空间
这些知识不仅有助于理解 Windows 操作系统 internals,也为开发高级安全工具(如 EDR bypass、恶意软件分析)奠定了坚实基础。
- 公众号:安全狗的自我修养
- vx:2207344074
- http://gitee.com/haidragon
- http://github.com/haidragon
- bilibili:haidragonx
#
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:安全狗的自我修养 haidragon haidragon《edr绕过工具 SysWhispers4 源码分析系列(一)》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。








评论