尽管安全解决方案的新一代 AI 与机器学习组件持续强化基于行为的检测能力,但许多方案的核心仍依赖于基于特征码的检测。Cobalt Strike 自问世以来便是威胁参与者和红队广泛使用的热门命令与控制 (C2) 框架,因此安全解决方案对其建立了大量特征码识别。
为延续 Cobalt Strike 过去的实战应用,IBM® X-Force Red 对抗模拟团队投入大量研发资源,通过内部工具对 Cobalt Strike 进行定制化改造。我们部分针对 Cobalt Strike 的内部工具已推出公开版本,例如 “InlineExecute-Assembly”、“CredBandit” 和 “BokuLoader“。过去两年间,鉴于 Cobalt Strike 特征码被过度识别,我们仅将其用于模拟技术能力较低的威胁参与者;在执行更高级的红队演练时,则转向使用其他第三方及自研 C2 框架。
通过研发实践,我们在高级红队演练中取得了更好的实战成效,主要得益于以下方向:
然而,目前仍有大量威胁参与者使用盗版 Cobalt Strike,因此模拟这类威胁参与者仍具有重要意义。对于愿意投入研发资源的红队而言,在模拟此类对手时仍可能通过 Cobalt Strike 取得实战成果。此外,Cobalt Strike 作为优秀的学习工具,可通过红队培训课程帮助新手通过实战操作掌握 C2 框架的应用。
随着我们持续扩展 C2 框架能力,特此分享过去在 Cobalt Strike 框架基础上的开发经验,特别是通过开发定制反射式加载器的实践。本系列内容也旨在帮助防御者理解 Cobalt Strike 的工作原理,以构建更强大的检测方案。
通过 Think 时事通讯,了解有关 AI、自动化、数据等方面最重要且最有趣的行业趋势。请参阅 IBM 隐私声明。
本篇博客是系列教程的开篇指南,涵盖开发 Cobalt Strike 反射式加载器的基础知识。随着系列教程的推进,我们将在此基础上展开深入探讨并持续引用本文内容。
通过本系列学习,最终目标是创建一个能与 Cobalt Strike 现有规避功能集成,并通过当前工具尚未涵盖的高级技术进一步增强的反射式加载器。后续文章将深入探讨特定规避功能的开发方法,以及如何将其集成到我们的 Cobalt Strike 反射式加载器中。
作为系列开篇,本文将涵盖以下内容:
当我们以攻击性安全工具开发者的视角审视 Cobalt Strike 的反射式加载机制时,将重点关注检测与规避的潜在节点。部分开发细节将被省略或简化,建议通过调试现有反射式加载器项目、从头重建项目或参加专业培训来补充知识。
Cobalt Strike 的 C2 植入程序(称为“信标”)是一个 Windows 动态链接库 (DLL),而在 Cobalt Strike 中使用自定义 DLL 加载器的模块化功能被称为用户自定义反射式加载器 (UDRL)。
通常情况下,内置的 Windows DLL 加载器负责将 DLL 加载到进程的虚拟内存空间。Windows DLL 加载器主要存在于用户空间,但在从磁盘映射 DLL 时也会跨越到内核空间。
在对抗模拟中使用 Windows DLL 加载器存在以下缺陷:
因此,使用 Windows DLL 加载器加载信标 DLL 并非理想解决方案。为克服这些挑战,我们采用反射式加载器从内存加载信标 DLL。
反射式加载主要规避的三大检测点包括:
反射式加载可理解为直接从内存加载原始 DLL,而非从文件系统加载。
虽然反射式加载与内置 Windows DLL 加载器均实现将 DLL 从原始文件格式加载至进程虚拟内存空间的功能。但与 Windows DLL 加载器相比,反射式加载具备关键优势——无需 DLL 文件存在于文件系统。这种内存加载方式支持无限级联加载阶段,因为 C2 植入 DLL 可隐藏在进程内存的多层加密与编码之中。
加载 DLL 时需要理解的一个关键概念是,DLL 在磁盘上和内存中的格式化方式不同。原始文件格式与虚拟地址格式下 DLL 的主要区别在于:
原始文件格式:
虚拟地址格式:
通过 Aleksandra Doniec 开发的 PE-Bear 工具分析 HTTP 信标 DLL,可观察到 DLL 各节在原始格式与虚拟地址格式下的差异:
表格展示了信标 DLL 各节的原始地址与虚拟地址对比。
此 HTTP/S 信标 DLL 在加载到进程的虚拟内存空间时大小为
PE-Bear 工具以可视化形式呈现信标 DLL 在原始文件格式与虚拟地址空间格式下的对比:
原始格式与虚拟格式下信标 DLL 的可视化呈现(左侧为原始格式,右侧为虚拟格式)
虽然在对抗模拟中并非明智之举,但将未经混淆的原始信标 DLL 释放至磁盘并使用 Windows DLL 加载器加载,是解密信标与 DLL 加载机制的有效方式。本质上,信标仅是 DLL 文件。Windows DLL 加载器与反射式加载器都只是将 DLL 加载至进程的过程。
使用 Windows DLL 加载器加载信标 DLL 需执行以下步骤:
LoadLibrary
API 从磁盘加载信标 DLL。首先需禁用所有 可延展 PE 选项 ,这些选项会导致 Windows DLL 加载器无法加载信标 DLL。为此我们修改 可延展 C2 配置文件 ,在 stage 块中禁用可延展 PE 规避选项:
已修改的可延展 C2 配置文件 stage 块显示禁用了 Cobalt Strike 规避功能。
修改配置文件后,我们重启 Cobalt Strike Team Server,并将我们的
通过 Cobalt Strike 客户端连接团队服务器。然后创建一个
通过 Cobalt Strike 客户端创建“原始无阶段”信标 DLL 过程的屏幕截图
使用以下代码创建名为 的 C 程序
通过 Windows C 代码使用 Windows DLL 加载器从磁盘加载信标 DLL。
我们使用
在加载过程中,Windows DLL 加载器将通过调用其入口点并传入
当 Windows DLL 加载器将信标 DLL 加载并初始化到进程的虚拟内存空间后,我们需要再次调用虚拟信标 DLL 的入口点
程序必须知晓虚拟信标 DLL 的入口点才能执行虚拟信标 DLL。这可通过在程序中动态解析虚拟信标 DLL 头部获取入口点相对虚拟地址 (RVA) 实现,也可通过快速查看后硬编码该值。
为进行概念验证,将手动发现信标 DLL 入口点 RVA 并硬编码至程序中。使用 PE-Bear 工具发现信标入口点的 RVA 为
使用 PE-Bear 查找信标 DL 入口点 RVA 的屏幕截图
代码准备就绪后,将 C 程序编译为 Windows 可执行文件:
编译程序使用的命令。
通过将信标 DLL 与可执行信标加载器程序置于同一目录,Windows DLL 加载器能在执行加载例程时发现 DLL。
我们将两个
信标 DLL 与加载器程序存放于同一目录。
从 Windows 桌面双击 loadBeaconDLL.exe 程序,建立与团队服务器的活跃信标连接。
通过 Windows DLL 加载器加载的信标 DLL 成功连接 C2 团队服务器。
Cobalt Strike 使用的是 Stephen Fewer 反射式加载器 项目的修改版本。这款具有传奇色彩的内存 DLL 加载器已有十余年历史,曾被 Metasploit 及其他知名攻击性安全工具使用。
多年来,Cobalt Strike 反射式加载器已增强至能处理 Cobalt Strike 提供的所有可延展 PE 规避功能。使用自定义用户定义反射式加载器 (UDRL) 的主要劣势在于,可延展 PE 规避功能可能无法开箱即用。
使用 UDRL 时,部分规避功能已完全实现——这些功能会在生成信标载荷时由 Cobalt Strike 的可延展 PE 引擎通过补丁方式集成至信标 DLL。然而,目前像这类功能必须由 UDRL 处理,而其他如等功能 和 在正确集成 UDRL 的情况下,则可由信标处理。
原始反射式加载器项目要求将
随后另一项目需负责:
原始反射式加载器将 DLL 加载至虚拟内存的图示。
另一种方法是将反射式加载器预置到 DLL 前端。这种方式支持加载任何非托管 DLL,且无需从源代码编译 DLL。这是一种强大的反射加载方法,能够加载任何可移植可执行文件(EXE 或 DLL)。
预置反射式加载器的 DLL 加载至虚拟内存的图示。
Cobalt Strike 的反射式加载实现融合了上述两种方法。了解 Metasploit 的 Meterpreter 反射式加载机制的用户会对这种方法感到熟悉。
与原始反射式加载器方法类似,
当 UDRL 被载入 Cobalt Strike,且操作员从客户端生成信标载荷时,Cobalt Strike 的可延展 PE 引擎会在导出函数的原始文件偏移位置注入反射式加载器 shellcode
当可延展 PE 引擎完成对原始信标 DLL 的补丁处理后,原始信标 DLL 将以可执行 shellcode 的形式交付给操作者。
Cobalt Strike反射式加载器将信标DLL加载至虚拟内存的图示。
通过 PE-Bear 反汇编器查看初始字节,可见信标 DLL 本身具备可执行性:
调用反射式加载器桩显示为可执行的汇编操作码。
这些初始字节
在执行可选的预置
通过查看信标 DLL 的导出函数目录,我们确认
使用 PE-Bear 确定 ReflectiveLoader 导出函数原始文件偏移的屏幕截图。
由于存在于导出目录中,导出函数的地址
要发现
节的虚拟地址与原始地址
信标 DLL.text 节的信标 DLL 的节内。
两者差值为
我们可以在 PE-Bear 中通过右键点击
综上所述,Cobalt Strike 反射式加载流程如下:
图示展示 Cobalt Strike 执行信标 DLL 反射式加载的主要阶段。
由于反射式加载器在信标 DLL 加载之前执行,其代码需为纯 shellcode。
编写复杂 shellcode 最简单的方法是使用纯 C 语言(无外部依赖)。随后将 C 文件编译为目标文件。所有内容必须包含在目标文件的节中。最终提取.text
Cobalt Strike 的可延展 PE 引擎负责从反射式加载器目标文件中提取 shellcode,并将其注入原始信标 DLL 在导出函数的原始文件偏移位置
利用 Cobalt Strike 将反射式加载器 shellcode 写入原始信标 DLL 的 Aggressor 脚本。
我们的 UDRL Aggressor 脚本通过以下步骤指引 Cobalt Strike 写入反射式加载器 shellcode:
<a href="https://hstechdocs.helpsystems.com/manuals/cobaltstrike/current/userguide/content/topics_aggressor-scripts/as-resources_functions.htm#extract_reflective_loader">extract_reflective_loader</a>函数会从字节数组变量中解析 UDRL
<a href="https://hstechdocs.helpsystems.com/manuals/cobaltstrike/current/userguide/content/topics_aggressor-scripts/as-resources_functions.htm#setup_reflective_loader">setup_reflective_loader</a>Cobalt Strike Aggressor 函数将利用可延展 PE 引擎发现导出函数的原始文件
Cobalt Strike 已为我们完成了从反射式加载器目标文件提取 .text 节、注入反射式加载器 shellcode,以及通过信标 DLL 标头调用反射式加载器桩来触发加载器的工作。
以下是实现信标反射式加载必须开发的阶段:
我们可采用多种方法在内存中发现原始信标 DLL 的地址。一些方法包括:
使用回溯搜索方法时,首先需要获取当前线程指令指针的地址(
英特尔 x64 汇编代码:通过 RDI 寄存器获取原始信标 DLL 基地址。
原始反射式加载器项目采用回溯搜索 MZ 与 PE 标头的方法。但这些标头特征已成为检测点。为应对此问题,Cobalt Strike 增加了
Cobalt Strike 文档 说明
当配置
这些字节必须具有特定唯一性,否则反射式加载器将无法定位。同时 MZ 标头对应的字节必须是可执行的空操作指令。不能使用类似的值
发现这一潜在检测点后,我开发了一种不同但原理相似的方法来定位原始信标 DLL 基地址。该方法采用特征标记搜索技术,从当前地址向后回溯
选择地址
由于无法直接访问 Java 可延展 PE 引擎,
用于向原始信标 DLL 写入特征标记并在 Cobalt Strike 脚本控制台显示修改的 Aggressor 脚本。
UDRL 代码必须知晓由脚本写入原始信标 DLL 的特征标记值。获取特征标记值后,特征标记搜索器将向后搜索两个重复的特征标记实例,如下方代码所示:
用于向后搜索两个 64 位特征标记实例的英特尔 x64 汇编代码。
由于不再使用 MZ 和 PE 标头,我们可以在 UDRL 的 Aggressor 脚本中将其 置为 空操作指令:
用于掩蔽原始信标 DLL 标头中 MZ、PE 及未使用 DOS 标识字节的 Aggressor 脚本。
此外,还有一种专属于 Cobalt Strike 的方法可用于发现原始信标 DLL 的基地址。如上文所述,调用反射式加载器桩的初始字节会在调用反射式加载器之前,将原始信标 DLL 的基地址存储在
为了在调试器中进一步验证,生成一个信标载荷,在其前端插入断点(
x64dbg 单步调试调用反射式加载器桩的屏幕截图显示,在调用反射式加载器之前,原始信标 DLL 基地址已保存在 RDI 寄存器中。
以下是从调用反射式加载器桩获取原始信标 DLL 基地址的可行示例:
通过内联汇编 C 代码从 RDI 寄存器获取原始信标 DLL 基地址。
获得原始信标 DLL 的基地址后,我们现在可以获取将信标加载到进程虚拟地址空间所需的值。
下表列出了需要从原始信标 DLL 标头获取的值、它们的位置及其类型:。
表格列举了原始信标 DLL 标头中对加载信标 DLL 有用的值。
并非所有标头信息的内容都是加载信标 DLL 所必需的。必要值可重新打包或混淆处理。非必要值可删除或随机化。
一旦我们了解SizeOfImagef
为虚拟信标 DLL 分配内存可采用多种方法。不同方法会使用不同类型的内存。Cobalt Strike 默认反射式加载器支持的方法包括:
此处为 Cobalt Strike 虚拟信标 DLL 内存分配选项表格。
通过 UDRL 还可进一步扩展此过程。可改用这些函数的 NTAPI 版本。更进一步,还可通过直接或间接系统调用来调用 NTAPI 函数——这可能有助于增强规避功能,也可能并无帮助。
当分配器方法设置为
BokuLoader 项目中展示如何从进程中获取系统调用的代码示例。
既然我们已经为虚拟信标 DLL 分配了内存,现在需要将信标的各个段从其原始文件偏移位置(即它们在原始信标 DLL 中的位置)复制到已分配内存中对应的相对虚拟偏移位置。
如果我们分配内存READWRITE节及其大小。在调用虚拟信标 DLL 的入口点之前,我们需要更改内存保护属性节为可执行。
分配内存会使反射式加载过程更简单,但也会增加被安全解决方案检测到的可能性。
以下是 BokuLoader 项目中的一个简化代码示例,演示了这一过程:。
BokuLoader 项目中展示将段从原始信标 DLL 复制到虚拟信标 DLL 的代码示例。
关于加载节的一些规避检测特性包括:
在公开的 BokuLoader 项目中,信标 DLL 的标头不会从原始信标 DLL 复制到虚拟信标 DLL。目前第一个
另一个可能的规避机会是让 UDRL Aggressor 脚本加密各节。这些节可以在内存中由 UDRL 使用 UDRL 与 UDRL Aggressor 脚本之间共享的密钥进行解密。
x64 HTTP/S 信标的正常运行依赖于四个 DLL。如果这些 DLL 当前未加载到进程中,我们的反射式加载器将需要加载它们。
这四个 DLL 列在 HTTP/S 信标 DLL 的导入目录中:
PE-Bear 中列出的信标 DLL 导入目录中的 DLL 屏幕截图。
内置的 Cobalt Strike 反射式加载器使用 kernel32.LoadLibraryA API 进行 DLL 加载。
DLL 加载可以通过多种不同的方式实现,每种方式都有不同的操作安全考量。一些方法包括:
如果 DLL 已存在于进程中,上述 Windows API 仍可用于获取 DLL 基址,但这可能会触发不必要的检测告警。
或者,PEB 中保存有指向
<a title="https://learn.microsoft.com/cn-zh/windows/win32/api/winternl/ns-winternl-peb_ldr_data" href="https://learn.microsoft.com/cn-zh/windows/win32/api/winternl/ns-winternl-peb_ldr_data">_PEB_LDR_DATA</a>
struct 结构的指针。在该结构内部,有一个包含进程中所有已加载 DLL 及其相关信息的链表(
)。BokuLoader 利用此链表来发现 DLL 信息,从而避免不必要的 API 调用。
如果 DLL 不存在于
反射式嵌套加载不易用于加载 DLL 依赖项,因为反射式加载器通常不会将 DLL 注册到进程。DLL 外部的代码无法正确使用反射式加载的 DLL。 DarkLoadLibrary 项目或许能够在不触发内核镜像加载事件的情况下,正确地将 DLL 加载到内存中。
BokuLoader 项目中展示如何通过遍历 InMemoryOrderModuleList 来解析已加载 DLL 基址的代码示例。
在所需的 DLL 加载到进程中后,必须解析导入目录中列出的 API。随后,需要将 API 地址写入虚拟信标 DLL 的导入地址表 (IAT) 中。这样,信标在需要调用 API 时,就知道该跳转到哪个地址
导入条目需要通过序号或名称字符串进行解析。
在下图中,我们可以看到 Cobalt Strike 信标 DLL 的导入条目同时使用了序号和名称字符串:
PE-Bear 屏幕截图显示信标 DLL 的部分导入条目必须通过序号解析。
内置的 Cobalt Strike 反射式加载器使用
解析 API 地址的一些规避检测方法包括:
GetProcAddress
NTDLL.LdrGetProcedureAddress
BokuLoader 使用自定义代码实现GetProcAddress
该也能支持名称字符串和序号处理。如果导入条目返回的地址是转发到另一个 DLL 的转发器,BokuLoader 默认使用来解析该转发器。
在写入 IAT 时,可以通过写入我们已实现的挂钩函数的虚拟地址(而非目标 API 的虚拟地址)来实现挂钩。只要在调用 IAT 中的地址时,将预期输出返回给信标,我们就可以在返回到信标之前执行额外代码。未来的文章及公开的 BokuLoader 版本将展示如何利用 IAT 挂钩实现高级规避功能。
通过近期发布的版本,公开的 BokuLoader 项目通过自定义实现,支持了Cobalt Strike C2 配置文件中的可延展 PE 功能。通过修改 UDRL Aggressor 脚本中的BokuLoader.cna
关于操作安全,需要注意的是,模式匹配引擎能够暴力破解单字节 XOR 掩码。未来的文章将展示如何利用 Cobalt Strike 的 Aggressor 脚本功能创建我们自己的可延展 PE 引擎,以混淆信标从而应对模式匹配检测。
信标 DLL 具有许多重定位项,必须在执行前解析并写入虚拟信标 DLL 的基址重定位表。
在 PE-Bear 中,我们可以看到信标 DLL 默认的映像基址
PE-Bear 屏幕截图显示信标 DLL 的映像基址。
在开始写入重定位项之前,我们需要计算虚拟信标 DLL 的基址与硬编码基址之间的差值。
例如,假设虚拟信标 DLL 的基址为
接下来,为确定基址重定位表中每个重定位条目的虚拟地址,我们将基址差值加上硬编码的重定位条目地址,以确定其在虚拟信标 DLL 内的重定位位置。
在下图中,我们可以看到信标的重定位条目以小端序格式反向写入:
PE-Bear 屏幕截图显示部分重定位条目以小端序格式存在。
该重定位条目的硬编码地址为
我们将此地址与基址差值相加,得到该重定位在虚拟信标 DLL 中的虚拟地址:
对于每个重定位条目,我们需要检查其类型是否为
<a title="https://learn.microsoft.com/cn-zh/windows/win32/debug/pe-format" href="https://learn.microsoft.com/cn-zh/windows/win32/debug/pe-format">IMAGE_REL_BASED_DIR64 (0xA)</a>
。如果该条件不成立,我们将跳过该重定位项的写入。
一旦确定了重定位项在虚拟信标 DLL 内的虚拟地址,我们就将其写入存放硬编码重定位项地址的内存空间。
如果您有兴趣进一步了解如何进行 PE 重定位,请查看公开 BokuLoader 项目中的 doRelocations 函数代码。在发布此博客文章之前,我已将重定位代码从汇编语言改写成(希望是)易于理解的 C 代码,以帮助其他想了解此过程技术细节的人。
执行信标可分解为三个步骤:
如果我们为虚拟信标 DLL 分配的内存是
如果我们将虚拟信标内存分配为不可执行 (
在公开的 BokuLoader 项目中,内存保护属性的更改是通过直接系统调用
BokuLoader 项目中演示将虚拟信标 DLL 的 .text 节更改为可执行的代码示例。
来自
为了使虚拟信标 DLL 正常运行,必须首先通过调用虚拟信标 DLL 的入口点来对其进行初始化。第一个参数是虚拟信标 DLL 的基址。第二个参数是
BokuLoader 项目中初始化虚拟信标 DLL 的代码示例。
初始化虚拟信标 DLL 后,我们可以选择将虚拟信标的入口点返回给反射加载器存根调用,也可以在 UDRL 中调用虚拟信标 DLL 的入口点
与典型 DLL 不同(其第一个参数
<a href="https://learn.microsoft.com/cn-zh/windows/win32/dlls/dllmain">DLLMAIN</a>
应为虚拟 DLL 的基址),信标期望传入原始信标 DLL 的基址。若未提供此参数,部分可延展 PE 规避功能可能会失效。
BokuLoader 项目中展示两种不同执行虚拟信标 DLL 方式的代码示例。
希望这篇博文能帮助红队和蓝队更好地理解 Cobalt Strike 及其反射加载过程。通过反射加载仍可实现的规避手段还有很多。深入了解这些概念后,组织能更好地为成功防御网络威胁做好准备。
本系列的后续文章将聚焦于将 UDRL 与现有 Cobalt Strike 规避功能相集成,深入探讨已公开 BokuLoader 中存在的未公开规避功能,以及尚未公开发布的高级功能。敬请关注更多深度信息与技术,学习如何通过 UDRL 开发将您的 Cobalt Strike 运用能力提升到新水平!