好久没写文章了,开始今年的第一篇,话不多说,正文开始,今天主要来聊聊Cobalt Strike中的Sleep Mask
一、什么是Sleep Mask
Sleep Mask 是 Cobalt Strike 在内存中屏蔽和解除自身屏蔽的功能。主要目的是避免内存检测,但是Sleep Mask本身会被作为检测的一部分。
二、从 Sleep Mask 进化史看现代EDR规避
1、Cobalt Strike 4.4 首次加入Kit
2021年8月,Sleep Mask Kit 最初在 Cobalt Strike 4.4 中推出,提供自定义Sleepmask代码的能力,规避针对sleepmask本身的内存签名规则,默认大小为289字节
2、Cobalt Strike 4.5 Sleep Mask Update
2021年12月,Cobalt Strike 4.5 对Sleep mask进行了更新,Sleepmask能够加密Beacon使用的堆内存,从而规避BeaconEyes等针对堆内存特征的检测
-
从 289 字节增加到 769 字节,如果超过这个大小,将使用默认的Sleep Mask
-
堆内存加密
-
添加了新的 HEAP_RECORDS 数据结构
-
添加了指向现有 SLEEPMASKP 结构的 HEAP_RECORDS 数据结构的指针
-
添加了新的循环,用于屏蔽和取消屏蔽由 HEAP_RECORDS 结构标识的堆内存
源代码如下:
/******** 不要修改此文件开始 ********/
/*
* ptr - 指向已分配内存的基地址指针。
* size - 为ptr分配的字节数。
*/
typedef struct {
char * ptr;
size_t size;
} HEAP_RECORD;
/*
* beacon_ptr - 指向Beacon基地址的指针
* sections - Beacon想要掩盖的内存区段列表。
* 每个区段由起始和结束索引位置的对表示。
* 列表以起始和结束位置均为0的对结束。
* heap_records - Beacon想要掩盖的堆内存地址列表。
* 列表以HEAP_RECORD.ptr为NULL结束。
* mask - Beacon随机生成用于掩盖的掩码
*/
typedef struct {
char * beacon_ptr;
DWORD * sections;
HEAP_RECORD * heap_records;
char mask[MASK_SIZE];
} SLEEPMASKP;
voidsleep_mask(SLEEPMASKP * parms, void(__stdcall *pSleep)(DWORD), DWORD time){
/******** 不要修改此文件结束 ********/
/******** 修改文件开始 ********/
DWORD * index;
DWORD a, b;
/* 遍历区段并进行掩盖 */
index = parms->sections;
while (TRUE) {
a = *index; b = *(index + 1);
index += 2;
if (a == 0 && b == 0)
break;
while (a < b) {
*(parms->beacon_ptr + a) ^= parms->mask[a % MASK_SIZE];
a++;
}
}
/* 掩盖堆内存记录 */
a = 0;
while (parms->heap_records[a].ptr != NULL) {
for (b = 0; b < parms->heap_records[a].size; b++) {
parms->heap_records[a].ptr[b] ^= parms->mask[b % MASK_SIZE];
}
a++;
}
pSleep(time);
/* 取消掩盖堆内存记录 */
a = 0;
while (parms->heap_records[a].ptr != NULL) {
for (b = 0; b < parms->heap_records[a].size; b++) {
parms->heap_records[a].ptr[b] ^= parms->mask[b % MASK_SIZE];
}
a++;
}
/* 遍历区段并取消掩盖 */
index = parms->sections;
while (TRUE) {
a = *index; b = *(index + 1);
index += 2;
if (a == 0 && b == 0)
break;
while (a < b) {
*(parms->beacon_ptr + a) ^= parms->mask[a % MASK_SIZE];
a++;
}
}
/******** 修改文件结束 ********/
}
注:仅在睡眠时间超过 2 秒时进行屏蔽和取消屏蔽
来自官方的效果图,但是依旧会被卡巴斯基扫描到
3、Cobalt Strike 4.7-4.9 Sleep Mask Refactoring
2022年8月,Sleep Mask以BOF形式重构,不再存放在beacon的.text部分,而是存放在bof使用内存。同年6月,基于线程池的睡眠混淆技术Ekko发布,这使得在完成对Beacon相关内存加密后,再对Sleep Mask本身进行加密成为可能,这项技术后续被加入到 Sleep Mask Kit中
整个文件目录如下
-
beacon.h:定义可用的内部beacon api
-
bofdefs.h:定义 Windows API 的动态函数解析原型
-
cfg.c:CFG保护绕过,仅限x64,使用evasive_sleep和evasive_sleep_stack_spoof弃用
-
common_mask.c:常见屏蔽函数
-
evasive_sleep.c:使用 CreateTimerQueueTimer 混淆Sleep Mask自身,仅限X64
-
evasive_sleep_stack_spoof.c:添加了堆栈欺骗的evasive_sleep,仅限X64
-
log_sleepmask_parms.c:将信息记录到beacon console
-
mask_text_section.c:睡眠之前屏蔽.text部分
-
sleepmask.c:定义默认Sleep Mask类型的Sleep Mask函数
-
sleepmask_pivot.c:定义pivot Sleep Mask类型的Sleep Mask函数
-
syscall.h:syscall相关头文件
-
syscalls_embedded.c:使用嵌入式方法实现syscall
-
syscalls_indirect.c:使用间接syscall
-
syscalls_indirect_randomized.c:使用indirect_randomedical方法实现syscall
更详细的以及一些注意事项,参考Sleep Mask Kit中的README.md,这里就详细分析关键代码
(1)加密内存区段、堆内存、Sleepmask bof 自身
首先是sleepmask.c,代码中存在一个sleep_mask函数,该函数根据用户的配置以及编译选项,来加密beacon的内存区段、堆内存以及是否加密sleep mask bof 自身。
可以修改此处的异或加密方式,例如
voidmask_section(SLEEPMASKP * parms, DWORD a, DWORD b) {
char key[] = "fjsihwisf349saf0";
size_t key_lenght = sizeof(key) - 1;
while (a < b) {
*(parms->beacon_ptr + a) ^= key[a % key_lenght];
a++;
}
}
mask_heap函数对分配的堆内存进行加解密操作
(2)修改.text属性
首先调用setup_text_section函数确定Beacon中.text的位置,然后根据Profile文件配置中stage.userwx设置来决定是否掩码.text 接下来使用
mask_text_section
和unmask_text_section
函数来修改.text
的属性,根据用户选项,如果使用syscall则用NtProtectVirtualMemory
函数修改内存属性,否则使用VirtualProtect
函数修改内存属性(3)利用CreateTimerQueueTimer 混淆Sleep Mask自身
在
sleepmask.c
中,将EVASIVE_SLEEP
设置为1
,将使用Ekko混淆Sleep Mask自身在下面的EVASIVE_SLEEP部分,我们可以选择是用默认的EVASIVE_SLEEP还是使用添加堆栈欺骗的EVASIVE_SLEEP
需要注意此处的#define CFG_BYPASS 0的配置,默认情况下,使用EVASIVE_SLEEP功能会禁用CFG_BYPASS,如果需要进程注入,注入到受CFG保护的进程中,那么就需要把#define CFG_BYPASS设置为1,
evasive_sleep
函数,主要通过创建一系列的计时器,计时器在触发时使用NtContinue
函数来将控制权转移到先前准备好的CONTEXT
结构中指定的地址,NtContinue
函数,其主要功能是恢复线程的上下文,其参数指向一个CONTEXT
结构体的指针,该结构体包含线程的寄存器和其他信息其中加密使用
SystemFunction032
函数,该函数实现了RC4加密算法,可同时用于加密和解密,接收数据和密钥结构作为参数,配合VirtualProtect
来实现对sleepmask代码进行加解密再次回到CFG的问题,为什么会触发CFG保护呢,先来看一下微软的介绍,再次回到我们的代码中CreateTimerQueueTimer 指向的回调例程是 NtContinue,它位于启用 CFG 时无法间接调用的特殊函数列表中。间接函数调用是从程序调用堆栈之外的另一个函数启动的调用。
CFG中不允许的函数调用,此处是ntdll中
那么此处的CFG_BYPASS,就是使用 markCFGValid_nt 函数调用 NtSetInformationVirtualMemory,将 NtContinue 添加到 CFG 允许列表从而绕过CFG保护
不同的操作系统,tVmInformation 数据结构的大小不同,在某些环境下会导致崩溃,因此官方给出了一个默认注释的代码,利用 NtSetInformationVirtualMemory 函数实现了一个循环,直到找到合适的大小。
(4)堆栈欺骗
写累了,涉及的知识点比较多,等下一篇再详细介绍吧
4、Mutator Kit(Sleep Mask mutated )
之前的方法是运行时对Sleep Mask自身进行混淆(evasive sleep mask),也就是Ekko Sleep Obfuscation,这会大大增加我们被检测的概率,在某些环境下,用了Ekko反正直接被检测。除此之外还要考虑CFG(Control Flow Guard)保护问题,在某些环境下会直接崩溃。我本人非常不喜欢Ekko,有多远踢多远。
所以后面官方推出了Mutator Kit,主要目的是通过LLVM的代码变异技术,动态生成每次都不同的sleep mask,从而避免被基于静态特征的YARA签名检测到。
Mutator Kit利用LLVM的中间表示(IR)代码,应用多种变异passes(包括替换等价运算符、插入虚假控制流块、代码扁平化、基本块拆分等)对sleep mask的代码结构和机器码进行变异。这样生成的sleep mask在功能上等价,但机器码层面差异巨大,难以被静态签名准确识别
具体作用包括:
-
生成每次都唯一的sleep mask,避免YARA等静态签名检测。
-
通过多种LLVM混淆通道,增强代码多样性和混淆度。
-
提供命令行和Cobalt Strike客户端集成的使用方式,方便自动化应用变异。
-
支持对其他Beacon Object Files(BOFs)进行类似变异,提高整体操作安全性(OPSEC)。
默认的sleep mask Kit每次编译后的的.text段完全一致,YARA签名可以直接匹配其特定的机器码指令序列,从而检测Sleepmask,
// [1] Build the sleepmask.
$ ./build.sh 49 WaitForSingleObject
true
none /tmp/dist
[ ... ]
[ ] [*] Compile sleepmask.x64.o
[ ... ]
// [2] Use objdump to find the text section size.
$ objdump -h sleepmask.x64.o
sleepmask.x64.o: file format pe-x86-64
Sections:
Idx Name Size VMA LMA File off Algn
0 .text 00000200 0000000000000000 0000000000000000 00000104 2**4
CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
// [3] Extract the .text section.
// NB skip is the offset('File off') of the .text section
// from objdmp and count is the size of the section
// e.g. python -c 'print(int("104", 16))' == 260.
$ dd
if
=sleepmask.x64.o of=sleepmask1.bin skip=260 count=512 bs=1
// [4] Calculate shasum.
$ shasum sleepmask1.bin
4f7813a6aae018a4cf6a78040d9c20024b5a83da sleepmask1.bin
// [5] Repeat the steps again to build another sleep
// mask and extract/hash the .text section - the hash is identical!
$ shasum sleepmask2.bin
4f7813a6aae018a4cf6a78040d9c20024b5a83da sleepmask2.bin
比如说 Elastic 针对sleepm mask的 YARA扫描规则,Windows_Trojan_CobaltStrike_b54b94ac,它在默认Sleep mask中查找以下操作码模式:0x4C 0x8B 0x53 0x08
mov r10, [rbx+0x08]
mov r9d, [r10]
mov r11d, [r10+0x04]
lea r10, [r10+0x08]
test r9d, r9d
jnz 0x0000000000000007
test r11d, r11d
jz 0x0000000000000035
cmp r9d, r11d
jnb 0xFFFFFFFFFFFFFFE8
mov rdi, r9
mov r8, [rbx]
Windows_Trojan_CobaltStrike_b54b94ac规则:
rule Windows_Trojan_CobaltStrike_b54b94ac {
meta:
author = "Elastic Security"
id = "b54b94ac-6ef8-4ee9-a8a6-f7324c1974ca"
fingerprint = "2344dd7820656f18cfb774a89d89f5ab65d46cc7761c1f16b7e768df66aa41c8"
creation_date = "2021-10-21"
last_modified = "2022-01-13"
description = "Rule for beacon sleep obfuscation routine"
threat_name = "Windows.Trojan.CobaltStrike"
reference_sample = "36d32b1ed967f07a4bd19f5e671294d5359009c04835601f2cc40fb8b54f6a2a"
severity = 100
arch_context = "x86"
scan_context = "file, memory"
license = "Elastic License v2"
os = "windows"
strings:
$a_x64 = { 4C 8B 53 08 45 8B 0A 45 8B 5A 04 4D 8D 52 08 45 85 C9 75 05 45 85 DB 74 33 45 3B CB 73 E6 49 8B F9 4C 8B 03 }
$a_x64_smbtcp = { 4C 8B 07 B8 4F EC C4 4E 41 F7 E1 41 8B C1 C1 EA 02 41 FF C1 6B D2 0D 2B C2 8A 4C 38 10 42 30 0C 06 48 }
$a_x86 = { 8B 46 04 8B 08 8B 50 04 83 C0 08 89 55 08 89 45 0C 85 C9 75 04 85 D2 74 23 3B CA 73 E6 8B 06 8D 3C 08 33 D2 }
$a_x86_2 = { 8B 06 8D 3C 08 33 D2 6A 0D 8B C1 5B F7 F3 8A 44 32 08 30 07 41 3B 4D 08 72 E6 8B 45 FC EB C7 }
$a_x86_smbtcp = { 8B 07 8D 34 08 33 D2 6A 0D 8B C1 5B F7 F3 8A 44 3A 08 30 06 41 3B 4D 08 72 E6 8B 45 FC EB }
condition:
any of them
}
Beacon 休眠时,sleep mask在内存中可见
使用 Mutator Kit 生成Sleep mask,来自官方的默认 sleep mask 和mutated sleep mask 中相同函数的调用对比图
详细的配置和说明参考官方文章:
https://www.cobaltstrike.com/blog/introducing-the-mutator-kit-creating-object-file-monstrosities-with-sleep-mask-and-llvm
5、Cobalt Strike 4.10 BeaconGate with Custom Sleepmask BOFs
2024年7月,Sleepmask-vs发布,支持添加堆栈欺骗等等,配合beacongate使的规避性更强
在过去几年中,异常 API 调用的检测逻辑急剧增加。例如syscall-detect、MalMemDetect、Hunt-Sleeping-Beacons和pe-sieve等开源项目都展示了从未支持的内存中搜寻可疑 API 的调用。此外,Elastic 凭借 异常的调用堆栈检测逻辑 推动了防御行业的向前发展,这些都给红队操作带来了巨大挑战。之前Cobalt Strike难以应对这些检测,特别是无法基于Beacon的系统调用实现细粒度控制,只能通过复杂的IAT hooking实现。
因此,BeaconGate正式诞生,先来看下官方的定义,从高层来看,睡眠掩码在概念上类似于远程过程调用(RPC),尽管在相同的进程地址空间内。例如,当Beacon睡眠时,它将调用Sleepmask BOF、mask和sleep。Beacon在这里充当“客户端”,Sleepmask是代表Beacon执行Sleep调用的“服务器”。在Cobalt Strike 4.10中,我们已经将这一想法推到了合乎逻辑的结论,Sleepmask现在支持执行任意函数。因此,现在可以配置Beacon转发其Windows API调用,以便通过Sleepmask(又名Beacon Gate)执行
长话短说,官方说了那么多其实总结下来就是,BeaconGate 使用户能够自定义 Beacon 调用 WinAPI 函数的方式。 配置 并启用BeaconGate 后,Beacon 将代理其 Windows API 调用,以通过 Sleepmask 执行(即类似于远程过程调用)
BeaconGate 工作原理的高级示意图
目前beacongate支持的的调用API如下:
可以通过Profile选项来配置 BeaconGate
stage {
beacon_gate {
All;
}
}
简单解读下代码,
当 API 调用代理到 Sleep Mask 时,相关数据将保存在结构中。
FUNCTION_CALL
BeaconGate 目前最多支持 10 个参数
那么怎么添加堆栈欺骗已经显而易见了
如果想要使用自定义睡眠函数,只需要修改sleep.cpp即可
例如,此处的MySleep() 的实现依赖于对 IO 对象实现等待 ,那么也可以自己塞个Ekko或者Foliage,个人感觉意义不大。
如果拿BeaconGate和Brute Ratel C4的自定义堆栈帧做对比,我们会发现完全是2个极端:
-
Cobalt Strike是越来越透明支持自定义,让操作员可以做到完全自定义可控,这也是为什么cobalt strike 明明有这么多检测规则,依然无法做到完全检测,水平高的操作员依旧可以利用Cobalt Strike提供的自定义插件规避顶级EDR
-
Brute Ratel C4在引入自定义堆栈帧后,表面上自带开箱即用的规避,但是需要用户自行去自定义,那么就意味着你要自己测试,再次又把问题再次抛给了用户,违背了一开始的开箱即用的规避的理念,只能说脚本小子有福了
这几天官方又发布了新文章,对Sleepmask-vs进行了更新,使用clang来编译内敛汇编,彻底提供了傻瓜式 自带开箱即用规避的 Sleep Mask,官方文章
https://www.cobaltstrike.com/blog/instrumenting-beacon-with-beacongate-for-call-stack-spoofing
来自官方的配置图,根据用户选项应用不同的sleepmask,包括默认 Sleep Mask、间接syscall的Sleep Mask、带返回地址欺骗的Sleep Mask(执行X86)、带合成帧的Sleep Mask
至于具体的效果如何,那不得而知
6、Cobalt Strike 4.11 New default Sleep Mask
2025年3月,在Cobalt Strike 4.11版本中,官方添加了一个全新的 Sleep Mask,在之前的版本中,Cobalt Strike 的"evasive sleep"功能已包含在 Arsenal Kit 中,它基于经过修改的开源技术(Ekko),因此需要进行大量定制修改才能与 Beacon 配合使用,全新的Sleep mask将对 Beacon、其堆分配 以及 自身 进行混淆,这意味着 Beacon 在运行时能够抵御静态签名,开箱即用无需任何额外配置。
总结下来就是 Cobalt Strike 4.11提供了一个全新的默认Sleep mask,如果你不具备任何开发能力,那么可以直接使用这个全新的默认Slee pmask,如果不想使用这个默认Sleep mask,那么也可以继续使用Arsenal Kit 中提供的Sleep mask Kit 或者Sleep mask-vs进行自定义开发。
阅读官方的文章,总结如下:
-
针对不具备开发能力的操作员,提供了开箱即用的规避,降低了使用门槛
-
针对不想用官网提供的规避的操作员,依旧可以使用插件定制开发属于自己的Cobalt Strike
至于其他的就是将Beacon 的默认反射加载器移植到一个新的 prepend/SRDI 样式加载器中,并添加了几个新的规避功能:
-
EAF 绕过选项,stage.set eaf_bypass “true”
-
支持间接系统调用,stage.set rdll_use_syscalls “true”
-
支持自动将复杂的混淆程序应用于 Beacon,stage.transform-obfuscate {}
其中 transform-obfuscate 是最大亮点,可以直接混淆Beacon,也就是说现在生成的shellcode是可以直接无视静态查杀的
例如下面的选项将压缩 Beacon ,使用随机的 64 位密钥对其进行rc4 加密,并使用随机的 32 位密钥对其进行 xor 运算,最后再对其进行 base64 编码
stage {
transform-obfuscate {
lznt1;
rc4 “64”; # NB The max supported rc4 key size is 128
xor “32”; # NB The max supported xor key size is 2048
base64;
}
}
至于很详细的还是直接看官方文章(https://www.cobaltstrike.com/blog/cobalt-strike-411-shh-beacon-is-sleeping)吧,至于什么时候能用上4.11,4.10都还没泄露呢,洗洗睡吧,梦里什么都有。
三、最终回(闲聊几句)
让我们再次回到Cobalt Strike 4.91,那么根据上面的这些知识,我们可以实现在不启用 evasive sleep mask 情况下实现堆栈欺骗,让我们的堆栈看起来合法
如图:
默认的sleepmask 堆栈,会出现内存地址
修改后的sleepmask 堆栈
Windows 10 效果
Windows 11(evasive_sleep_stack_spoof中自带的堆栈欺骗,在windows 11上不生效)效果
那么如何修改,相信大家看图就知道了,这里就不给代码了,拒绝伸手党,正所谓自己动手丰衣足食
写在最后
如果上个线就是免杀了,那么人均都是免杀大师,这个吊圈子就这样,上个线就是过了,演示嘎嘎过,实战落地没。
对抗终是末路,检测才是王道。
比如说某检测规则,可以做到通杀国内95%以上只会写Loader的脚本小子以及他们用的所谓的二开CS。如图,是不是感受到来自最新天擎的恐惧了
至于怎么检测Sleep Mask,相信聪明的你已经想到了
原文始发于微信公众号(哈拉少安全小队):从Sleep Mask到Beacon Gate看现代EDR规避技术
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论