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)获取

  1. 获取DOS头并验证签名
  2. 获取NT头
  3. 从可选头中获取导出表RVA
  4. 转换为导出表结构
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时,通过邻函数推断系统调用号。

原理:

  1. 检查目标函数是否被Hook
  2. 如果被Hook,查找相邻的Nt/Zw函数
  3. 根据邻函数的系统调用号推断目标函数的系统调用号

5.3 GetSSN技术

通过遍历所有Zw函数,按地址排序后,系统调用号即为顺序索引。

实现步骤:

  1. 遍历NTDLL导出表中所有Zw开头的函数
  2. 记录函数名和地址
  3. 按地址升序排序
  4. 系统调用号即为排序后的索引
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版本的系统调用表
  • 使用"按系统调用地址排序"技术
  • 减少系统调用存根大小

关键函数:

  1. SW2_PopulateSyscallList: 解析ntdll的EAT,定位Zw函数并排序
  2. SW2_GetSyscallNumber: 通过Hash获取系统调用号
  3. SW2_GetRandomSyscallAddress: 通过特征码定位随机Native API的syscall指令地址

6.2 SysWhispers3

新增技术:

  1. embedded: 传统syscall方式
  2. jumper & jumper_randomized: 隐藏syscall指令
  3. 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检测方式

  1. 检测非NTDLL模块中的syscall指令
  2. 检测syscall指令的上下文
  3. 监控系统调用号的异常使用

7.2 绕过技术

  1. 从NTDLL中动态获取syscall指令地址
  2. 使用间接跳转(jmp)而非直接syscall
  3. 运行时修改代码(egg_hunter)
  4. 使用合法的系统调用号

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监控的重要手段,核心要点包括:

  1. 理解PEB结构和模块加载机制
  2. 掌握PE文件解析和导出表遍历
  3. 熟悉各种系统调用技术(地狱之门、光环之门、GetSSN)
  4. 了解SysWhispers工具的实现原理和使用方法
  5. 注意防御措施的规避和隐蔽性处理

通过合理组合这些技术,可以有效绕过用户层的Hook,直接与内核交互,提高恶意代码的隐蔽性和存活能力。

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是内核分配给每个进程的用户模式结构,包含进程的重要信息: 2.2 PEB_ LDR_ DATA 结构 包含进程加载模块的信息: 2.3 LDR_ DATA_ TABLE_ ENTRY 结构 存储模块的具体信息: 3. 获取模块信息 3.1 获取PEB地址 x64系统: x86系统: 3.2 遍历加载模块 4. PE文件解析 4.1 PE文件头结构 4.2 导出地址表(EAT)获取 获取DOS头并验证签名 获取NT头 从可选头中获取导出表RVA 转换为导出表结构 4.3 遍历导出函数 5. Syscall 技术实现 5.1 地狱之门(Hell's Gate)技术 原理:通过读取NTDLL,解析导出表,根据函数名Hash找到函数地址,动态获取系统调用号。 关键结构: 获取系统调用号: 5.2 光环之门(Halo's Gate)技术 改进地狱之门,当目标函数被Hook时,通过邻函数推断系统调用号。 原理: 检查目标函数是否被Hook 如果被Hook,查找相邻的Nt/Zw函数 根据邻函数的系统调用号推断目标函数的系统调用号 5.3 GetSSN技术 通过遍历所有Zw函数,按地址排序后,系统调用号即为顺序索引。 实现步骤: 遍历NTDLL导出表中所有Zw开头的函数 记录函数名和地址 按地址升序排序 系统调用号即为排序后的索引 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示例: 运行时替换: 7. 防御措施与绕过 7.1 EDR检测方式 检测非NTDLL模块中的syscall指令 检测syscall指令的上下文 监控系统调用号的异常使用 7.2 绕过技术 从NTDLL中动态获取syscall指令地址 使用间接跳转(jmp)而非直接syscall 运行时修改代码(egg_ hunter) 使用合法的系统调用号 8. 实际应用示例 8.1 Rust实现模块遍历 8.2 系统调用实现 9. 总结 Windows系统调用技术是绕过EDR监控的重要手段,核心要点包括: 理解PEB结构和模块加载机制 掌握PE文件解析和导出表遍历 熟悉各种系统调用技术(地狱之门、光环之门、GetSSN) 了解SysWhispers工具的实现原理和使用方法 注意防御措施的规避和隐蔽性处理 通过合理组合这些技术,可以有效绕过用户层的Hook,直接与内核交互,提高恶意代码的隐蔽性和存活能力。