windows-syscall篇
字数 1646 2025-08-06 12:20:48
Windows Syscall 技术详解
1. Syscall 基础概念
1.1 什么是 Syscall
Syscall(系统调用)是用户态程序与内核态交互的接口,允许用户程序请求操作系统内核提供的服务。在Windows中,系统调用是通过NTDLL.dll中的函数实现的。
1.2 为什么使用 Syscall
- 绕过EDR在用户层的Hook:EDR通常会Hook用户层的Windows API调用以监控进程行为
- 直接与内核交互:避免通过被Hook的API,直接从NTDLL中读取系统调用号进行系统调用
- 提高隐蔽性:减少被安全产品检测到的风险
2. Windows 进程结构
2.1 PEB(进程环境块)
PEB是内核分配给每个进程的用户模式结构,包含进程的重要信息:
typedef struct _PEB {
BOOLEAN InheritedAddressSpace;
BOOLEAN ReadImageFileExecOptions;
BOOLEAN BeingDebugged;
BOOLEAN Spare;
HANDLE Mutant;
PVOID ImageBase;
PPEB_LDR_DATA LoaderData; // 重要:指向加载模块信息
// ... 其他字段
} PEB, *PPEB;
2.2 PEB_LDR_DATA 结构
包含进程加载模块的信息:
typedef struct _PEB_LDR_DATA {
ULONG Length;
ULONG Initialized;
PVOID SsHandle;
LIST_ENTRY InLoadOrderModuleList; // 按加载顺序的模块链表
LIST_ENTRY InMemoryOrderModuleList; // 按内存顺序的模块链表
LIST_ENTRY InInitializationOrderModuleList; // 按初始化顺序的链表
} PEB_LDR_DATA, *PPEB_LDR_DATA;
2.3 LDR_DATA_TABLE_ENTRY 结构
存储模块的具体信息:
struct _LDR_DATA_TABLE_ENTRY {
LIST_ENTRY InLoadOrderLinks; // 0x0
LIST_ENTRY InMemoryOrderLinks; // 0x8
LIST_ENTRY InInitializationOrderLinks; // 0x10
VOID* DllBase; // 0x18 模块基址
VOID* EntryPoint; // 0x1c
ULONG SizeOfImage; // 0x20
UNICODE_STRING FullDllName; // 0x24 完整路径+名称
UNICODE_STRING BaseDllName; // 0x2c 模块名称
// ... 其他字段
};
3. 获取模块信息
3.1 获取PEB地址
x64系统:
PPEB Peb = (PPEB)__readgsqword(0x60);
x86系统:
PPEB Peb = (PPEB)__readfsdword(0x30);
3.2 遍历加载模块
PLDR_MODULE pFirstLoadModule = (PLDR_MODULE)((PBYTE)Peb->LoaderData->InMemoryOrderModuleList.Flink - 0x10);
PLDR_MODULE pLoadModule = pFirstLoadModule;
do {
printf("Module Name:%ws\r\nModule Base Address:%p\r\n\r\n",
pLoadModule->FullDllName.Buffer, pLoadModule->BaseAddress);
pLoadModule = (PLDR_MODULE)((PBYTE)pLoadModule->InMemoryOrderModuleList.Flink - 0x10);
} while (pLoadModule != pFirstLoadModule);
4. PE文件解析
4.1 PE文件头结构
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature; // PE文件头标志 "PE\0\0"
IMAGE_FILE_HEADER FileHeader; // 标准PE头
IMAGE_OPTIONAL_HEADER32 OptionalHeader; // 扩展PE头
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
4.2 导出地址表(EAT)获取
- 获取DOS头并验证签名
- 获取NT头
- 从可选头中获取导出表RVA
- 转换为导出表结构
PIMAGE_DOS_HEADER dos_header = (PIMAGE_DOS_HEADER)base;
if (dos_header->e_magic != IMAGE_DOS_SIGNATURE) return;
PIMAGE_NT_HEADERS nt_headers = (PIMAGE_NT_HEADERS)((PBYTE)base + dos_header->e_lfanew);
PIMAGE_EXPORT_DIRECTORY export_table = (PIMAGE_EXPORT_DIRECTORY)(
(PBYTE)base + nt_headers->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
4.3 遍历导出函数
PDWORD AddressOfNames = (PDWORD)((PBYTE)base + export_table->AddressOfNames);
PWORD AddressOfNameOrdinals = (PWORD)((PBYTE)base + export_table->AddressOfNameOrdinals);
PDWORD AddressOfFunctions = (PDWORD)((PBYTE)base + export_table->AddressOfFunctions);
for (DWORD i = 0; i < export_table->NumberOfNames; i++) {
PCHAR functionName = (PCHAR)((PBYTE)base + AddressOfNames[i]);
PVOID functionAddress = (PBYTE)base + AddressOfFunctions[AddressOfNameOrdinals[i]];
printf("%s: %p\n", functionName, functionAddress);
}
5. Syscall 技术实现
5.1 地狱之门(Hell's Gate)技术
原理:通过读取NTDLL,解析导出表,根据函数名Hash找到函数地址,动态获取系统调用号。
关键结构:
typedef struct _VX_TABLE_ENTRY {
PVOID pAddress; // 函数地址指针
DWORD64 dwHash; // 函数哈希
WORD wSystemCall; // 系统调用号
} VX_TABLE_ENTRY, *PVX_TABLE_ENTRY;
获取系统调用号:
// 查找mov eax, SSN指令(操作码0xB8)
if (*((PBYTE)pFunctionAddress + cw) == 0x4c &&
*((PBYTE)pFunctionAddress + 1 + cw) == 0x8b &&
*((PBYTE)pFunctionAddress + 2 + cw) == 0xd1 &&
*((PBYTE)pFunctionAddress + 3 + cw) == 0xb8) {
BYTE high = *((PBYTE)pFunctionAddress + 5 + cw);
BYTE low = *((PBYTE)pFunctionAddress + 4 + cw);
pVxTableEntry->wSystemCall = (high << 8) | low;
break;
}
5.2 光环之门(Halo's Gate)技术
改进地狱之门,当目标函数被Hook时,通过邻函数推断系统调用号。
原理:
- 检查目标函数是否被Hook
- 如果被Hook,查找相邻的Nt/Zw函数
- 根据邻函数的系统调用号推断目标函数的系统调用号
5.3 GetSSN技术
通过遍历所有Zw函数,按地址排序后,系统调用号即为顺序索引。
实现步骤:
- 遍历NTDLL导出表中所有Zw开头的函数
- 记录函数名和地址
- 按地址升序排序
- 系统调用号即为排序后的索引
std::map<int, string> Nt_Table;
// ...填充Zw函数到map中(自动按地址排序)
int index = 0;
for (auto iter = Nt_Table.begin(); iter != Nt_Table.end(); ++iter) {
cout << "index:" << index << ' ' << iter->second << endl;
index++;
}
6. SysWhispers 工具
6.1 SysWhispers2
特点:
- 不依赖特定Windows版本的系统调用表
- 使用"按系统调用地址排序"技术
- 减少系统调用存根大小
关键函数:
SW2_PopulateSyscallList: 解析ntdll的EAT,定位Zw函数并排序SW2_GetSyscallNumber: 通过Hash获取系统调用号SW2_GetRandomSyscallAddress: 通过特征码定位随机Native API的syscall指令地址
6.2 SysWhispers3
新增技术:
- embedded: 传统syscall方式
- jumper & jumper_randomized: 隐藏syscall指令
- egg_hunter: 用垃圾指令代替syscall,运行时替换
egg_hunter示例:
NtAllocateVirtualMemory PROC
mov r10, rcx
mov eax, wSystemCall
DB 77h, 0h, 0h, 74h, 77h, 0h, 0h, 74h ; "w00tw00t"占位符
ret
NtAllocateVirtualMemory ENDP
运行时替换:
unsigned char egg[] = {0x77, 0x00, 0x00, 0x74, 0x77, 0x00, 0x00, 0x74};
unsigned char replace[] = {0x0f, 0x05, 0x90, 0x90, 0xC3, 0x90, 0xCC, 0xCC};
FindAndReplace(egg, replace);
7. 防御措施与绕过
7.1 EDR检测方式
- 检测非NTDLL模块中的syscall指令
- 检测syscall指令的上下文
- 监控系统调用号的异常使用
7.2 绕过技术
- 从NTDLL中动态获取syscall指令地址
- 使用间接跳转(jmp)而非直接syscall
- 运行时修改代码(egg_hunter)
- 使用合法的系统调用号
8. 实际应用示例
8.1 Rust实现模块遍历
unsafe {
let peb = __readgsqword(0x60) as *mut PEB;
let p_first_load_module = ((*peb).Ldr.as_ref().unwrap().InMemoryOrderModuleList.Flink as PBYTE).offset(-0x10) as *const LDR_MODULE;
let mut p_load_module = p_first_load_module;
loop {
let module_base = p_load_module as *const LDR_DATA_TABLE_ENTRY;
let slice = slice::from_raw_parts((*module_base).FullDllName.Buffer, (*module_base).FullDllName.Length as usize / 2);
let string = String::from_utf16_lossy(slice);
println!("Module Name: {:?}", string.trim());
println!("Module Base Address: {:?}\r\n", module_base);
p_load_module = ((*p_load_module).InMemoryOrderModuleList.Flink as PBYTE).offset(-0x10) as *const LDR_MODULE;
if p_first_load_module == ((*p_load_module).InMemoryOrderModuleList.Flink as PBYTE).offset(-0x10) as *const LDR_MODULE {
break;
}
}
}
8.2 系统调用实现
NtAllocateVirtualMemory PROC
mov [rsp +8], rcx ; 保存寄存器
mov [rsp+16], rdx
mov [rsp+24], r8
mov [rsp+32], r9
sub rsp, 28h
mov ecx, 003970B07h ; 函数Hash
call SW2_GetSyscallNumber ; 获取系统调用号
add rsp, 28h
mov rcx, [rsp +8] ; 恢复寄存器
mov rdx, [rsp+16]
mov r8, [rsp+24]
mov r9, [rsp+32]
mov r10, rcx
syscall ; 执行系统调用
ret
NtAllocateVirtualMemory ENDP
9. 总结
Windows系统调用技术是绕过EDR监控的重要手段,核心要点包括:
- 理解PEB结构和模块加载机制
- 掌握PE文件解析和导出表遍历
- 熟悉各种系统调用技术(地狱之门、光环之门、GetSSN)
- 了解SysWhispers工具的实现原理和使用方法
- 注意防御措施的规避和隐蔽性处理
通过合理组合这些技术,可以有效绕过用户层的Hook,直接与内核交互,提高恶意代码的隐蔽性和存活能力。