Cobalt Strike 十周年纪念,Cobalt Strike 4.7: The 10th Anniversary Edition[1]
Raphael also cautioned against adding cutting edge, out of the box evasion techniques to Cobalt Strike. The obvious danger is that once they’re inevitably fingerprinted, we’d get stuck in an endless loop of fixing those issues rather than working on new features. Cobalt Strike’s defaults are easily fingerprinted and that’s by design. The idea is that you bring your own tools and techniques to Cobalt Strike and use those. That’s what makes it unique.
Cobalt Strike 的观点是:
1.CS 的基本原则是稳定性和可扩展性。2.CS 不会在核心中添加任何开箱即用的、前沿的 evasion 技术。3.CS 在未来会继续增加灵活性并保持稳定。积极开发 evasion 技术,并通过扩展的 Arsenal Kit 提供。
观点是很正确的,作为一个使用极广的 C2,如果在核心模块中提供高级的 evasion 技术:一方面会增加特征,然后陷入无休止的对抗,这会给某些不需要该功能的用户造成麻烦(What you don't use, you don't pay for.);另一方面会影响稳定性和兼容性。
相比 Socks4a,多了认证和 UDP 代理支持。现在的内置代理应该没有多少人用,CS 计划未来将 beacon 和代理分离,应该会通过反射 DLL 注入来实现。
增加三个 BOF 配置项:
•bof_allocator
,可选 VirtualAlloc
、MapViewOfFile
和 HeapAlloc
。•bof_reuse_memory
,设为 true 则会在执行完 BOF 后清空内存并复用。•userwx
,BOF 会受该通用配置项影响。
CS 说通过添加内存相关的配置项解决了两个问题:
Adding flexibility around how BOFs live in memory provided us with the means to address another item that we had on our backlog. We have added support for additional relocation types for BOFs, specifically the .xdata, .pdata, and .bss sections. This change firstly means that an issue has been resolved whereby BOFs sometimes wouldn’t run because the address offset was greater than 4GB. Secondly, the number of available dynamic functions has been increased from 32 to 64.
反正我读了几遍也没读懂这之间的联系,导致我有点怀疑写这篇文章的人是非核心开发人员。抛开这些没逻辑的话,以作者曾开发 BOF loader 的经验来讲 CS 实现上的问题。
开发过稍复杂 BOF 的读者应该都踩过一些坑,其中部分坑是 CS 的错误实现导致的。为什么这么说,因为我的 BOF loader 中不会存在这些问题。没有开发过 BOF 的读者可以看 TrustedSec 的 A Developer's Introduction to Beacon Object Files[2]。
出现该报错的原因是处理重定位的方式有误。
举个例子,对于相对寻址 call QWORD PTR [rip+offset]
,COFF 中会生成重定位项 IMAGE_REL_AMD64_REL32
,在处理时需要(其它 relocation 的处理可参考 lld source code[3])
*(uint32_t*)(addr_of_reloc) += (uint32_t)(addr_of_symbol - addr_of_reloc) - 4
CS 的报错发生在 addr_of_symbol - addr_of_reloc > 0xffffffff
的情况,那么什么时候会出现这种情况?分为两种情况考虑:
1) 重定位的符号非外部符号,这种情况下只要将所有节分配在一整块内存中,是不会出现地址差大于 4 GB 的,除非加载一个大于 4 GB 的 BOF。
没有看过 CS 的实现,结合上文
We have added support for additional relocation types for BOFs, specifically the .xdata, .pdata, and .bss sections
猜一下,CS 的实现中应该只分配了 .text
、.data
等节,才会导致处理重定位时出现地址差大于 4 GB。但这和添加的这几个配置项有什么关系呢?
2) 重定位的符号是外部符号,这种情况下只要将自行构造的符号表和节数据连续分配在同一块内存中,也不会出现大于 4 GB 的情况。
不清楚 BOF 这里的实现是怎样的,但我找到 GitHub 上一个 BOF loader 项目 CoffeLdr[4] 存在这个问题。
if ( Coffee->Reloc->Type == IMAGE_REL_AMD64_REL32 && FuncPtr != NULL )
{
// 校验 function map 项地址与 relocation 项地址差值大于 4 GB
if ( ( ( Coffee->FunMap + ( FuncCount * 8 ) ) - ( Coffee->SecMap[ SectionCnt ].Ptr + Coffee->Reloc->VirtualAddress + 4 ) ) > 0xffffffff )
return FALSE;
...
}
// 符号表和节数据没有连续分配
Coffee.SecMap = LocalAlloc( LPTR, Coffee.Header->NumberOfSections * sizeof( SECTION_MAP ) );
Coffee.FunMap = VirtualAlloc( NULL, 2048, MEM_COMMIT | MEM_RESERVE | MEM_TOP_DOWN, PAGE_READWRITE );
当函数栈大于 4 KB 时,编译器会插入该函数的调用,该函数是用来扩栈的。
在 BOF loader 解析外部符号时可以将该函数符号绑定到自实现的 chkstk
函数。
CS BOF 最蠢的一点就是限制了只能有 32 个动态导入函数(而且在文档中根本不提),这就导致了稍复杂的 BOF 中基本都得自己动态解析函数,例如 PPLDump_BOF[5] 项目中:
BOOL populateDataStructure() {
// Initialize "global" handles to the two DLLs in question.
// By default, both are loaded into the address space of a beacon, but this is to help use them elsewhere.
hmKernel32ModuleHandle = KERNEL32$GetModuleHandleW(L"kernel32");
hmAdvapi32ModuleHandle = KERNEL32$GetModuleHandleW(L"advapi32");
if (hmKernel32ModuleHandle == NULL) {
BeaconPrintf(CALLBACK_ERROR, "Kernel32 module is valid.\n");
return FALSE;
}
if (hmAdvapi32ModuleHandle == NULL) {
BeaconPrintf(CALLBACK_ERROR, "Advapi32 module is invalid.\n");
return FALSE;
}
sFunctionPointerStruct.StructGetProcAddress = (_GetProcAddress)KERNEL32$GetProcAddress(hmKernel32ModuleHandle, "GetProcAddress");
if (sFunctionPointerStruct.StructGetProcAddress == NULL) {
BeaconPrintf(CALLBACK_ERROR, "Error assigning function pointer to StructGeProcAddress\n");
return FALSE;
}
sFunctionPointerStruct.StructGetModuleHandleW = (_GetModuleHandleW)sFunctionPointerStruct.StructGetProcAddress(hmKernel32ModuleHandle, "GetModuleHandleW");
if (sFunctionPointerStruct.StructGetModuleHandleW == NULL) {
BeaconPrintf(CALLBACK_ERROR, "Error assigning function pointer to StructGetModuleHandleW\n");
return FALSE;
}
...
CS 的这个限制没有任何意义,节约内存 or 减少文件大小 or 加快加载速度
都解释不通。我需要用到 100 个 API,不可能你限制只帮我绑定 32 个,剩下的我就不用了吧,结果只能是我自己做动态解析。
我猜测 CS 做该限制的原因是其静态分配了一个 void* func_map[32]
,谁知道呢。
Secondly, the number of available dynamic functions has been increased from 32 to 64.
上文中 CS 说通过更新的内存配置项,将 32 个 slots 的限制提升到了 64 ...(???)
Cobalt Strike expects that your BOFs are single-threaded programs that run for a short period of time. BOFs will block other Beacon tasks and functionality from executing. There is no BOF pattern for asynchronous or long-running tasks. If you want to build a long-running capability, consider a Reflective DLL that runs inside of a sacrificial process.
BOF 执行时会阻塞 Beacon 中其它所有任务,应该是因为 BOF 被放在主线程中串行执行。这个设计并不好,并无必要的理由要这样做,完全可以新开线程去执行。
4.6 以前的自定义 sleep mask 是用类似 UDRL 的方式,直接将编译后 .text 段的代码取出来覆盖到 Beacon 中。4.7 的更新将其转换为了真正的 BOF 实现,现在可以在里面调用 BOF 的动态函数 LIBRARY$function
和 Beacon API,将限制大小提高到 4 KB,并且 sleep mask 现在不处于 Beacon 的 .text 段。
CS 的 steal_token
打开令牌时一直传递的是 TOKEN_ALL_ACCESS
,现在允许通过 profile 中 steal_token_access_mask
来配置,也可以窃取令牌时在 GUI 的界面里勾选。
我实践中最小化权限窃取令牌的流程是,以 PROCESS_QUERY_LIMITED_INFORMATION
打开进程,以 TOKEN_DUPLICATE
打开令牌,然后以需要的权限复制新令牌(我一般用 MAXIMUM_ALLOWED
)。
另外分享关于令牌权限的一个坑,在某些情况下用 TOKEN_ALL_ACCESS
打开的令牌不具备全部权限
#define TOKEN_ALL_ACCESS_P (STANDARD_RIGHTS_REQUIRED |\
TOKEN_ASSIGN_PRIMARY |\
TOKEN_DUPLICATE |\
TOKEN_IMPERSONATE |\
TOKEN_QUERY |\
TOKEN_QUERY_SOURCE |\
TOKEN_ADJUST_PRIVILEGES |\
TOKEN_ADJUST_GROUPS |\
TOKEN_ADJUST_DEFAULT )
#if ((defined(_WIN32_WINNT) && (_WIN32_WINNT > 0x0400)) || (!defined(_WIN32_WINNT)))
#define TOKEN_ALL_ACCESS (TOKEN_ALL_ACCESS_P |\
TOKEN_ADJUST_SESSIONID )
#else
#define TOKEN_ALL_ACCESS (TOKEN_ALL_ACCESS_P)
#endif
Windows 定义中当 WINNT > 0x400 时,TOKEN_ALL_ACCESS
是 TOKEN_ALL_ACCESS_P | TOKEN_ADJUST_SESSIONID
,此时包含全部权限。
直接使用这个头文件不会有问题,但用其它语言绑定时,例如很多语言用来生成定义的 microsoft/win32metadata[6],其定义只有 WINNT <= 0x400
的情况。
再比如 Golang 的 issue #25775 - syscall: TOKEN_ALL_ACCESS inconsistency[7]。
令牌不含 TOKEN_ADJUST_SESSIONID
导致的后果是无法以该令牌权限新进程,例如 CreateProcessWithToken
。所以如果发现正常打开的 TOKEN_ALL_ACCESS
令牌无法用来创建新进程,不妨看看是不是这个原因。
这个功能没怎么见人用过,它从 3.11 开始被引入,见 Cobalt Strike 3.11 – The snake that eats its tail[8]。
Module Stomping 用来防止使用可执行的私有内存,所以它加载一个 DLL,将 Beacon 放置在 DLL 的 Image 内存中。通过设置 profile 的 module_x64 / module_x86
,CS 会使用指定的 DLL 作内存。这个 DLL 大小需要大于 Beacon 所需的大小,还需要保证该 DLL 之后不会被加载。
Based on user feedback, a small change has been made to module stomping. In some cases, although the module was loaded, the actual stomping failed because Beacon remained in virtual memory. This was because unless the exported function had an ordinal value between 1 and 15, Beacon would default to using VirtualAlloc. This limitation has now been addressed by adding optional syntax to the setting to specify the starting ordinal when searching for exported functions.
上文是 4.7 的改进,又是一段让人看不懂的解释,这跟导出符号的序数有什么关系 ...
可以通过 clipboard 命令或 cna 的 bclipboard 获取剪切板里最多 204800 字节的内容。
Stageless 的 payload 可以设置退出时退出进程或线程。
暗黑模式之类的更新,不详写了。
[1]
Cobalt Strike 4.7: The 10th Anniversary Edition: https://www.cobaltstrike.com/blog/cobalt-strike-4-7-the-10th-anniversary-edition/[2]
A Developer's Introduction to Beacon Object Files: https://www.trustedsec.com/blog/a-developers-introduction-to-beacon-object-files/[3]
lld source code: https://android.googlesource.com/toolchain/lld/+/refs/heads/master/COFF/Chunks.cpp#103[4]
CoffeLdr: https://github.com/Cracked5pider/CoffeeLdr/blob/main/Source/CoffeeLdr.c#L254[5]
PPLDump_BOF: https://github.com/EspressoCake/PPLDump_BOF/blob/master/src/main.c#L37[6]
microsoft/win32metadata: https://github.com/microsoft/win32metadata[7]
issue #25775 - syscall: TOKEN_ALL_ACCESS inconsistency: https://github.com/golang/go/issues/25775[8]
Cobalt Strike 3.11 – The snake that eats its tail: https://www.cobaltstrike.com/blog/cobalt-strike-3-11-the-snake-that-eats-its-tail/