edr绕过工具SysWhispers4源码分析系列(一)

admin 2026-03-12 22:43:56 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文档深入分析SysWhispers4源码,详解Windows系统调用机制与EDR绕过原理。内容涵盖API调用链结构、Win32与NativeAPI差异、以及从用户态到内核态的完整调用流程。重点剖析x64、x86及ARM64架构下syscall指令实现与SSN系统调用号的作用。结论指出直接调用NativeAPI可绕过用户态Hook,为安全研究人员提供了底层技术细节与规避检测的实战思路。 综合评分: 90 文章分类: 红队,免杀,安全工具,逆向分析


cover_image

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 的原因:

  1. 绕过用户态 Hook:EDR/AV 通常在 kernel32.dll 层 Hook Win32 API,但很少 Hook ntdll.dll
  2. 更细粒度控制:Native API 提供更底层的参数控制
  3. 避免中间层处理:Win32 API 会在内部调用多个 Native API,增加被检测风险
  4. 性能优化:减少不必要的封装层

代码示例(来自 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 指令执行流程:

  1. 保存用户态上下文
  • RIP → RCX (保存返回地址)
  • RFLAGS → R11 (保存标志寄存器)
  • CSSSRSP 自动保存
  1. 加载内核态上下文
  • 从 IA32_LSTAR MSR (0xC0000082) 加载内核入口地址
  • 从 IA32_FMASK MSR (0xC0000084) 加载 RFLAGS 掩码
  • 切换到内核栈(从 IA32_KERNEL_GSBASE MSR)
  1. 执行特权级转换
  • 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 的作用:

  1. 索引 SSDT 表:内核通过 SSN 查找对应的内核函数
  2. 版本识别:不同 Windows 版本的 SSN 可能不同
  3. 安全检查:某些安全产品监控特定 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&nbsp;=0;&nbsp;i&nbsp;<&nbsp;pExp->NumberOfNames;&nbsp;i++){
11. constchar*&nbsp;name&nbsp;=(char*)((BYTE*)pNtdll&nbsp;+&nbsp;pExp->AddressOfNames[i]);
12. if(strncmp(name,"Nt",2)==0){
13. exports[count].Address=(BYTE*)pNtdll&nbsp;+&nbsp;pExp->AddressOfFunctions[pExp->AddressOfNameOrdinals[i]];
14. exports[count].Name=&nbsp;name;
15. count++;
16. }
17. }

19. // 3. 按地址排序(关键!)
20. SortExports(exports,&nbsp;count);

22. // 4. 排序后的索引 = SSN
23. for(DWORD i&nbsp;=0;&nbsp;i&nbsp;<&nbsp;count;&nbsp;i++){
24. g_SsnTable[HashStr(exports[i].Name)]=&nbsp;i;
25. }
26. }

原理:Windows 内核按固定顺序排列系统调用,导出函数的内存地址顺序与 SSN 一致

优点:Hook 免疫,版本兼容性好 缺点:需要解析 PE 结构

方法 3:Hell’s Gate

1. // 直接读取 ntdll 存根的机器码
2. static&nbsp;DWORD&nbsp;HellsGateReadSSN(PVOID pFunction){
3. BYTE*&nbsp;pCode&nbsp;=(BYTE*)pFunction;

5. // 查找 "mov eax, imm32" 指令 (B8 xx xx xx xx)
6. if(pCode[0]==0xB8){
7. return*(DWORD*)(pCode&nbsp;+1);// SSN 在操作数中
8. }
9. return&nbsp;INVALID_SSN;
10. }

优点:简单直接 缺点:被 Hook 后失效

方法 4:Halo’s Gate(邻居扫描)

1. // Hell's Gate + 邻居扫描
2. static&nbsp;DWORD&nbsp;HalosGateReadSSN(PVOID pFunction){
3. DWORD ssn&nbsp;=HellsGateReadSSN(pFunction);
4. if(ssn&nbsp;!=&nbsp;INVALID_SSN)
5. return&nbsp;ssn;

7. // 向前后扫描邻近函数
8. for(int&nbsp;offset&nbsp;=1;&nbsp;offset&nbsp;<=8;&nbsp;offset++){
9. // 向上扫描
10. ssn&nbsp;=HellsGateReadSSN((BYTE*)pFunction&nbsp;-&nbsp;offset&nbsp;*0x20);
11. if(ssn&nbsp;!=&nbsp;INVALID_SSN)
12. return&nbsp;ssn&nbsp;-&nbsp;offset;// 推断原始 SSN

14. // 向下扫描
15. ssn&nbsp;=HellsGateReadSSN((BYTE*)pFunction&nbsp;+&nbsp;offset&nbsp;*0x20);
16. if(ssn&nbsp;!=&nbsp;INVALID_SSN)
17. return&nbsp;ssn&nbsp;+&nbsp;offset;
18. }
19. return&nbsp;INVALID_SSN;
20. }

优点:可应对部分 Hook 缺点:扫描范围有限

方法 5:Tartarus’Gate(增强版)

1. // Tartarus'Gate - 检测多种 Hook 类型
2. static&nbsp;BOOL&nbsp;IsHooked(BYTE*&nbsp;pFunction){
3. // 检测近跳转 (E9)
4. if(pFunction[0]==0xE9)
5. return&nbsp;TRUE;

7. // 检测远跳转 (FF 25)
8. if(pFunction[0]==0xFF&&&nbsp;pFunction[1]==0x25)
9. return&nbsp;TRUE;

11. // 检测断点 (CC)
12. if(pFunction[0]==0xCC)
13. return&nbsp;TRUE;

15. return&nbsp;FALSE;
16. }

扩展扫描范围:±16 个函数(相比 Halo’s Gate 的 ±8)

方法 6:SyscallsFromDisk(最彻底)

1. // 从 KnownDlls 加载干净的 ntdll
2. static&nbsp;PVOID&nbsp;LoadCleanNtdllFromDisk(){
3. HANDLE hSection;
4. UNICODE_STRING usPath&nbsp;=&nbsp;RTL_CONSTANT_STRING(L"\\KnownDlls\\ntdll.dll");

6. OBJECT_ATTRIBUTES oa&nbsp;={0};
7. InitializeObjectAttributes(&oa,&usPath,&nbsp;OBJ_CASE_INSENSITIVE,&nbsp;NULL,&nbsp;NULL);

9. // 打开 KnownDlls 中的 ntdll
10. NtOpenSection(&hSection,&nbsp;SECTION_MAP_READ,&oa);

12. // 映射到用户空间
13. PVOID pBase&nbsp;=&nbsp;NULL;
14. SIZE_T size&nbsp;=0;
15. NtMapViewOfSection(hSection,GetCurrentProcess(),&pBase,...);

17. return&nbsp;pBase;// 干净的 ntdll,无 Hook
18. }

优点:100% 绕过所有用户态 Hook 缺点:需要额外的系统调用

方法 7:RecycledGate(组合拳)

1. // FreshyCalls + opcode 验证
2. static&nbsp;DWORD&nbsp;RecycledGateResolve(PVOID pFunction){
3. // 1. 先用 FreshyCalls 获取候选 SSN
4. DWORD candidate&nbsp;=FreshyCallsGetSSN(pFunction);

6. // 2. 尝试读取 opcode 验证
7. DWORD opcode_ssn&nbsp;=HellsGateReadSSN(pFunction);

9. // 3. 如果一致,确认正确
10. if(candidate&nbsp;==&nbsp;opcode_ssn)
11. return&nbsp;candidate;

13. // 4. 如果不一致,信任 FreshyCalls(可能被 Hook)
14. return&nbsp;candidate;
15. }

优点:综合两种方法的优势 缺点:实现复杂

方法 8:HW Breakpoint(硬件断点)

1. // 使用调试寄存器提取 SSN
2. static&nbsp;DWORD g_CapturedSSN&nbsp;=0;

4. static&nbsp;LONG CALLBACK&nbsp;SsnBreakpointHandler(PEXCEPTION_POINTERS pEx){
5. if(pEx->ExceptionRecord->ExceptionCode==&nbsp;EXCEPTION_SINGLE_STEP){
6. // 在 syscall 指令处捕获 EAX
7. g_CapturedSSN&nbsp;=&nbsp;pEx->ContextRecord->Eax;

9. // 跳过 syscall 指令
10. pEx->ContextRecord->Rip+=2;// syscall 长度
11. return&nbsp;EXCEPTION_CONTINUE_EXECUTION;
12. }
13. return&nbsp;EXCEPTION_CONTINUE_SEARCH;
14. }

16. static&nbsp;DWORD&nbsp;HWBreakpointGetSSN(PVOID pFunction){
17. // 1. 设置 DR0 = syscall 指令地址
18. SetDr0((PBYTE)pFunction&nbsp;+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&nbsp;g_CapturedSSN;
31. }

优点:完全不依赖函数字节内容 缺点:需要硬件支持,速度慢

5.5 SSN 加密(SysWhispers4 新特性)

1. # 来自 obfuscator.py - XOR 加密 SSN
2. def&nbsp;generate_xor_key(self)->&nbsp;int:
3. """生成 32 位 XOR 密钥"""
4. return&nbsp;self._rng.randint(0x01010101,0xFEFEFEFE)

6. def&nbsp;xor_ssn(ssn:&nbsp;int,&nbsp;key:&nbsp;int)->&nbsp;int:
7. """XOR 加密单个 SSN"""
8. return&nbsp;ssn&nbsp;^&nbsp;key

生成的 C 代码:

1. // 加密的 SSN 表
2. #define&nbsp;SW4_XOR_KEY&nbsp;0xA3B7C9D2U
3. #define&nbsp;SW4_DECRYPT(v)((v)^&nbsp;SW4_XOR_KEY)

5. DWORD&nbsp;EncryptedSsnTable[]={
6. 0xA3B7C9F0U,// 24 ^ 0xA3B7C9D2
7. 0xA3B7C9E5U,// 58 ^ 0xA3B7C9D2
8. // ...
9. };

11. // 运行时解密
12. DWORD&nbsp;GetSSN(DWORD hash){
13. return&nbsp;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&nbsp;ProcessHandle,
2. ;&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;PVOID*BaseAddress,
3. ;&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;ULONG_PTR&nbsp;ZeroBits,
4. ;&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;PSIZE_T&nbsp;RegionSize,
5. ;&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;ULONG&nbsp;AllocationType,
6. ;&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;ULONG&nbsp;Protect)
7. NtAllocateVirtualMemory&nbsp;PROC
8. ;参数传递(调用者负责):
9. ;&nbsp;RCX&nbsp;=ProcessHandle
10. ;&nbsp;RDX&nbsp;=BaseAddress
11. ;&nbsp;R8 &nbsp;=ZeroBits
12. ;&nbsp;R9 &nbsp;=RegionSize
13. ;[RSP+28h]=AllocationType(第5个及以后参数在栈上)
14. ;[RSP+30h]=Protect

16. mov r10,&nbsp;rcx &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;;复制&nbsp;RCX&nbsp;→&nbsp;R10&nbsp;(syscall&nbsp;要求)
17. mov eax,0x18;&nbsp;SSN&nbsp;=24
18. syscall &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;;进入内核
19. ret &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;;返回,返回值在&nbsp;RAX
20. NtAllocateVirtualMemory&nbsp;ENDP

6.3 参数传递详细规则

前 4 个参数(寄存器):

1. NTSTATUS&nbsp;NtOpenProcess(
2. PHANDLE&nbsp;ProcessHandle,// RCX
3. ACCESS_MASK&nbsp;DesiredAccess,// RDX
4. POBJECT_ATTRIBUTES OA,// R8
5. PCLIENT_ID&nbsp;ClientId// R9
6. );

第 5 个及以后参数(栈):

1. NTSTATUS&nbsp;NtCreateThreadEx(
2. PHANDLE&nbsp;ThreadHandle,// RCX
3. ACCESS_MASK&nbsp;DesiredAccess,// RDX
4. POBJECT_ATTRIBUTES OA,// R8
5. HANDLE&nbsp;ProcessHandle,// R9
6. PVOID&nbsp;StartRoutine,// [RSP+28h]
7. PVOID&nbsp;Argument,// [RSP+30h]
8. ULONG&nbsp;CreateFlags,// [RSP+38h]
9. SIZE_T&nbsp;ZeroBits,// [RSP+40h]
10. SIZE_T&nbsp;StackSize,// [RSP+48h]
11. SIZE_T&nbsp;MaxStackSize,// [RSP+50h]
12. PPS_ATTRIBUTE_LIST&nbsp;AttrList// [RSP+58h]
13. );

栈布局图:

1. 调用前栈布局:
2. [RSP]=返回地址
3. [RSP+8]=第5个参数
4. [RSP+10h]=第6个参数
5. [RSP+18h]=第7个参数
6. ...

8. 调用后栈布局(被调用函数视角):
9. [RSP]=旧的&nbsp;RBP&nbsp;(如果建立栈帧)
10. [RSP+8]=返回地址
11. [RSP+10h]=第5个参数
12. [RSP+18h]=第6个参数
13. ...

6.4 返回值约定

NTSTATUS 返回值(RAX):

1. // 成功值
2. #define&nbsp;STATUS_SUCCESS &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;((NTSTATUS)0x00000000L)
3. #define&nbsp;STATUS_WAIT_0 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;((NTSTATUS)0x00000000L)
4. #define&nbsp;STATUS_WAIT_1 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;((NTSTATUS)0x00000001L)

6. // 常见错误值
7. #define&nbsp;STATUS_UNSUCCESSFUL &nbsp; &nbsp; &nbsp;((NTSTATUS)0xC0000001L)
8. #define&nbsp;STATUS_ACCESS_DENIED &nbsp; &nbsp;&nbsp;((NTSTATUS)0xC0000022L)
9. #define&nbsp;STATUS_INVALID_HANDLE &nbsp; &nbsp;((NTSTATUS)0xC0000008L)
10. #define&nbsp;STATUS_INFO_LENGTH_MISMATCH&nbsp;((NTSTATUS)0xC0000004L)

判断宏:

1. #define&nbsp;NT_SUCCESS(Status)(((NTSTATUS)(Status))>=0)
2. #define&nbsp;NT_INFORMATION(Status)(((ULONG)(Status)>>30)==1)
3. #define&nbsp;NT_WARNING(Status)(((ULONG)(Status)>>30)==2)
4. #define&nbsp;NT_ERROR(Status)(((ULONG)(Status)>>30)==3)

6.5 栈帧管理

基本栈帧(无局部变量):

1. SimpleFunction&nbsp;PROC
2. ;不需要建立栈帧,直接使用影子空间
3. sub&nbsp;rsp,20h;分配32字节影子空间
4. ;...函数体...
5. add rsp,20h;恢复栈
6. ret
7. SimpleFunction&nbsp;ENDP

带局部变量的栈帧:

1. ComplexFunction&nbsp;PROC
2. push rbp &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;;保存旧栈帧
3. mov rbp,&nbsp;rsp &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;;建立新栈帧
4. sub&nbsp;rsp,40h;分配局部变量+影子空间

6. ;局部变量访问:
7. ;[rbp-10h],[rbp-20h],...

9. ;调用其他函数:
10. mov ecx,123;第1个参数
11. mov edx,456;第2个参数
12. call&nbsp;SomeFunction

14. leave &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;;等价于&nbsp;mov rsp,&nbsp;rbp&nbsp;/&nbsp;pop rbp
15. ret
16. ComplexFunction&nbsp;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&nbsp;PROC
3. push rbx &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;;保存非易失性寄存器
4. push rdi
5. push rsi
6. push r12

8. ;使用临时寄存器
9. mov r11,[gadget_addr]
10. mov r10,&nbsp;rcx
11. call r11

13. pop r12 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;;恢复寄存器
14. pop rsi
15. pop rdi
16. pop rbx
17. ret
18. RandomizedSyscall&nbsp;ENDP

6.7 影子空间(Shadow Space)

影子空间定义:

调用者必须在栈上预留 32 字节(4×QWORD),供被调用函数临时存储 RCX、RDX、R8、R9:

1. [RSP]=返回地址
2. [RSP+8]=Shadowfor&nbsp;RCX
3. [RSP+10h]=Shadowfor&nbsp;RDX
4. [RSP+18h]=Shadowfor&nbsp;R8
5. [RSP+20h]=Shadowfor&nbsp;R9

使用示例:

1. Caller&nbsp;PROC
2. sub&nbsp;rsp,20h;分配影子空间

4. mov ecx,1;参数1
5. mov edx,2;参数2
6. mov r8d,3;参数3
7. mov r9d,4;参数4

9. call&nbsp;Callee;被调用者可随意使用[rsp+8..28h]

11. add rsp,20h;释放影子空间
12. ret
13. Caller&nbsp;ENDP

15. Callee&nbsp;PROC
16. ;可以直接写入影子空间(不需要额外分配栈)
17. mov&nbsp;[rcx],&nbsp;rdx &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;;保存参数
18. mov&nbsp;[rsp+8],&nbsp;r8 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;;使用影子空间
19. ;...
20. ret
21. Callee&nbsp;ENDP

6.8 SysWhispers4 中的调用约定应用

来自 generator.py 的 ASM 生成逻辑:

1. def&nbsp;_gen_asm_msvc(self)->&nbsp;str:
2. """生成 MASM 汇编存根"""
3. lines&nbsp;=[]

5. for&nbsp;proto&nbsp;in&nbsp;self._prototypes:
6. ssn&nbsp;=&nbsp;self._get_ssn(proto.name)

8. lines.append(f"{proto.name} PROC")
9. lines.append(f" &nbsp; &nbsp;mov r10, rcx");关键:RCX&nbsp;→&nbsp;R10
10. lines.append(f" &nbsp; &nbsp;mov eax, 0x{ssn:X}");&nbsp;SSN&nbsp;→&nbsp;EAX
11. lines.append(f" &nbsp; &nbsp;syscall");进入内核
12. lines.append(f" &nbsp; &nbsp;ret")
13. lines.append(f"{proto.name} ENDP")

15. return"\n".join(lines)

间接调用的寄存器处理:

1. def&nbsp;_gen_indirect_call(self)->&nbsp;str:
2. """生成间接 syscall 代码"""
3. return"""
4. mov r10, rcx &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;; 保存 RCX
5. mov r11, rdx &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;; 保存 RDX (重要!)
6. mov rax, [gadget_addr] &nbsp;; 加载 gadget 地址
7. call rax &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;; 间接调用
8. mov rdx, r11 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;; 恢复 RDX
9. ret
10. """

随机化方法的熵增处理:

1. def&nbsp;_gen_randomized_call(self)->&nbsp;str:
2. """生成随机化 syscall"""
3. return"""
4. mov r10, rcx
5. mov r11, rdx
6. rdtsc &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ; 获取时间戳计数器
7. and eax, GADGET_MASK &nbsp; &nbsp;; 取模得到索引
8. mov rax, [gadget_pool + rax*8]
9. call rax &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;; 调用随机 gadget
10. mov rdx, r11
11. ret
12. """

6.9 调用约定实战示例

完整示例:NtWriteVirtualMemory

1. // C 语言声明
2. NTSTATUS&nbsp;NtWriteVirtualMemory(
3. HANDLE&nbsp;ProcessHandle,// RCX
4. PVOID&nbsp;BaseAddress,// RDX
5. PVOID&nbsp;Buffer,// R8
6. SIZE_T&nbsp;NumberOfBytesToWrite,// R9
7. PSIZE_T&nbsp;NumberOfBytesWritten// [RSP+28h]
8. );
1. ;汇编调用示例
2. INVOKE&nbsp;NtWriteVirtualMemory,&nbsp;\
3. hProcess,&nbsp;\
4. lpBaseAddress,&nbsp;\
5. lpBuffer,&nbsp;\
6. nSize,&nbsp;\
7. lpBytesWritten

9. ;展开后的汇编代码:
10. sub&nbsp;rsp,28h;对齐栈+影子空间
11. mov ecx,&nbsp;hProcess &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;;&nbsp;RCX&nbsp;=参数1
12. mov rdx,&nbsp;lpBaseAddress &nbsp; &nbsp; &nbsp;;&nbsp;RDX&nbsp;=参数2
13. mov r8,&nbsp;lpBuffer &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;;&nbsp;R8&nbsp;=参数3
14. mov r9,&nbsp;nSize &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;;&nbsp;R9&nbsp;=参数4
15. lea rax,&nbsp;lpBytesWritten
16. mov&nbsp;[rsp+28h],&nbsp;rax &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;;参数5放栈上
17. call&nbsp;NtWriteVirtualMemory
18. add rsp,28h

返回值处理:

1. NTSTATUS status&nbsp;=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",&nbsp;status);
8. }

总结

通过深入分析 SysWhispers4 项目源码,我们全面了解了 Windows 系统调用的核心机制:

  1. API 调用链:kernel32.dll → ntdll.dll → syscall → ntoskrnl.exe
  2. Win32 API vs Native API:文档化程度、稳定性、EDR 监控差异
  3. 完整调用流程:从用户态到内核态的每一步转换
  4. 系统调用机制:x64 使用 syscall,x86 使用 sysenter,ARM64 使用 SVC
  5. SSN 管理:8 种不同的 SSN 获取方法,应对各种 Hook 场景
  6. x64 调用约定:寄存器分配、参数传递、栈帧管理、影子空间

这些知识不仅有助于理解 Windows 操作系统 internals,也为开发高级安全工具(如 EDR bypass、恶意软件分析)奠定了坚实基础。

  • 公众号:安全狗的自我修养
  • vx:2207344074
  • http://gitee.com/haidragon
  • http://github.com/haidragon
  • bilibili:haidragonx

#


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:安全狗的自我修养 haidragon haidragon《edr绕过工具 SysWhispers4 源码分析系列(一)》

评论:0   参与:  0