找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

3925

积分

0

好友

539

主题
发表于 昨天 09:29 | 查看: 7| 回复: 0

睡眠混淆是用于对抗内存检测的一项技术。它的核心思路是:在特定的触发器触发或定时执行内存加解密、改变权限,以此来规避安全软件(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 );
}

我们来梳理一下流程:

  1. 初始化:首先创建一个事件(hEvent)和一个 TimerQueue。
  2. 获取关键函数:获取 NtContinue(用于切换线程上下文)和 SystemFunction032(用于 RC4 加密/解密)。
  3. 捕获上下文:第一个 CreateTimerQueueTimer 的回调是 RtlCaptureContext,它会捕获当前线程的上下文并填充到 CtxThread 结构体中。
  4. 构造 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 ); (设置事件,唤醒主线程)
  5. 调度执行:随后,通过多个 CreateTimerQueueTimer 调用,将构造好的上下文结构体分别作为参数,以 NtContinue 为回调进行排队。这些定时器设置了不同的延迟(100, 200, 300...毫秒),从而按顺序执行上述操作。
  6. 等待与恢复:主线程调用 WaitForSingleObject( hEvent, INFINITE ) 进入等待。当最后一个“设置事件”的定时器任务执行完毕,事件被触发,主线程恢复执行。

整个过程实现了:修改内存属性为 RW -> 加密内存 -> 睡眠等待 -> 解密内存 -> 修改内存属性回 RWX。在睡眠期间,内存处于加密状态,可以躲避基于内存的扫描。

EKKO项目运行状态截图:左侧控制台日志,中间任务管理器,右侧十六进制编辑器

另一个运行状态截图

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

TppETWTimerSet函数内部代码片段

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

显示SharedData为空的调试信息

Cronos 项目分析

GitHub 项目地址:https://github.com/Idov31/Cronos

Cronos 是另一个实现睡眠混淆的开源项目。其思路类似,同样是创建可等待的定时器,并通过 RtlCaptureContext 获取线程上下文。

创建多个可等待定时器的代码

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

通过findGadget函数搜索ROP gadget的代码

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

设置定时器并执行代码的片段

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

QuadSleep函数的汇编实现

描述ROP栈布局的示意图

其工作流本质上也是:修改内存属性为 RW -> 加密 -> 睡眠 -> 解密 -> 修改内存属性为 RWX

检测技术

了解了攻击技术,我们来看看如何检测。这里介绍两个相关的开源检测项目。

Hunt-Sleeping-Beacons

这个项目旨在检测使用此类技术的“沉睡”的 C2 信标。要理解其检测逻辑,需要一些 Windows 线程池和 Worker Factory 的背景知识,相关深入分析可参考 A Deep Dive into Exploiting Windows Thread Pools

Hunt-Sleeping-Beacons检测到可疑定时器的控制台输出

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

定义扫描类型的代码,包括suspicious_timer, blocking_apc等

下面我们逐一解析这些检测点:

suspicious_timer (可疑定时器)

  1. 通过 NtQuerySystemInformation 查询进程句柄信息,筛选出类型为 TpWorkerFactory 的句柄。
    通过NtQueryObject筛选WorkerFactory句柄的代码
  2. 每个 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;
  3. 遍历 TimerQueue 中的定时器,获取其回调函数地址(FinalizationCallback)。
    读取WorkerFactory信息并获取定时器回调的代码
  4. 将回调地址与一个预定义的“可疑回调函数列表”进行比对。如果匹配,则标记为“可疑定时器”。
    将定时器回调与可疑列表比对的代码
    这个可疑列表包含了常用于 安全/渗透/逆向 技术的函数,例如 NtContinueRtlCaptureContext 等。
    初始化可疑回调函数列表的代码

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

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

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

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

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

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

non_executable_memory (不可执行内存)
检查调用堆栈中地址所在内存页的权限。如果该页不具备任何执行权限(即没有 PAGE_EXECUTEPAGE_EXECUTE_READPAGE_EXECUTE_READWRITEPAGE_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 关键字,来监控本地进程的内存保护权限变化。

Microsoft-Windows-Threat-Intelligence 提供者的GUID和关键字描述

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

处理内存保护变更事件的ETW回调函数代码

简而言之,它的检测阈值是1:一旦某内存页完成了从“可变”到“预期不可变”的第一次状态切换,后续任何对其权限的修改都会被标记为可疑。

总结

睡眠混淆是一种有效的 逆向工程 与对抗检测的技术,它利用 Windows 的线程池定时器机制,在睡眠期间加密自身内存。针对这种技术的检测可以从多个维度入手,包括分析线程池和定时器回调、检查线程阻塞状态、监控内存权限的异常波动等。攻防双方在 网络/系统 底层细节上的较量仍在持续。对于安全研究人员和开发者而言,深入了解这些底层机制,无论是为了加固防御还是进行安全测试,都至关重要。

本文中分析的技术细节和开源项目,对于希望深入 计算机基础 与系统安全领域的学习者很有参考价值。如果你对这类话题感兴趣,欢迎在技术社区继续交流探讨。想了解更多前沿技术分析与实践分享,可以关注 云栈社区 的更新。




上一篇:OpenClaw与Claude重塑SaaS格局:AI Agent时代,软件价值与商业模式转型分析
下一篇:高盛研报解析:Claude Cowork成代理工作流落地标杆,OpenClaw预示AI交互与算力新趋势
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-3-10 11:12 , Processed in 0.612340 second(s), 43 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表