介绍
Cobalt Strike 是一种威胁模拟软件,被许多政府、组织和公司的信息安全团队广泛使用,成为事实上的标准闭源/付费工具。它在许多网络犯罪团伙中也非常流行,这些团伙通常会滥用破解或泄露版本的 Cobalt Strike。
Cobalt Strike 拥有多个独特功能,安全通信,且完全模块化和可定制,因此正确检测和归因可能会非常复杂。这是我们在过去几年中几乎每个重大网络安全事件或大规模泄露中都能看到 Cobalt Strike 被使用的主要原因。
关于反向工程 Cobalt Strike 软件的文章很多,尤其是其信标模块,因为这是整个链中的最重要部分。其他模块和有效载荷常常被忽视,但这些部分对于恶意软件研究者、取证分析师或调查员也包含了有价值的信息。
本系列的第一部分专注于所有原始有效载荷类型的正确识别,以及如何解码和解析它们。我们还分享了基于这些发现的有用解析器、脚本和 YARA 规则,回馈给 社区。
原始有效载荷
Cobalt Strike 的有效载荷基于 Meterpreter Shellcodes,具有许多相似之处,例如 API 哈希 (x86 和 x64 版本) 或在 http/https 有效载荷中使用的 URL 查询 checksum8 算法,这使得识别更加困难。这一特定的 checksum8 算法也用于其他框架,例如 Empire。
让我们分别描述每个有效载荷的有趣部分。
有效载荷头 x86 版本默认的 32 位原始有效载荷的入口点以典型指令 CLD (0xFC) 开始,随后是 CALL 指令和 PUSHA (0x60),作为 API 哈希算法的第一个指令。
x86 有效载荷
有效载荷头 x64 版本标准的 64 位变体同样以 CLD 指令开始,随后是 AND RSP10h 和 CALL 指令。
x64 有效载荷
我们可以使用这些模式来定位有效载荷的入口点并从该位置计算其他固定偏移量。
默认 API 哈希原始有效载荷具有预定义的结构和二进制格式,针对每个可定制值如 DNS 查询、HTTP 头或 C2 IP 地址有特定的占位符。这些占位符的偏移量处于固定位置,与硬编码的 API 哈希值相同。哈希算法为 ROR13,最终哈希是从 API 函数名称和 DLL 名称中计算出来的。整个算法在 Metasploit 仓库的汇编代码中进行了详细注释。
Python 实现的 API 哈希算法
我们可以使用以下正则表达式模式搜索硬编码的 API 哈希:
我们可以利用已知的 API 哈希列表和 API 哈希的已知固定位置来进行有效载荷类型的识别,以通过 YARA 规则进行更准确的检测。
通过已知 API 哈希进行有效载荷识别
完整的 Cobalt Strike API 哈希列表:
API 哈希 DLL 和 API 名称 0xc99cc96a dnsapidllDnsQueryA 0x528796c6 kernel32dllCloseHandle 0xe27d6f28 kernel32dllConnectNamedPipe 0xd4df7045 kernel32dllCreateNamedPipeA 0xfcddfac0 kernel32dllDisconnectNamedPipe 0x56a2b5f0 kernel32dllExitProcess 0x5de2c5aa kernel32dllGetLastError 0x0726774c kernel32dllLoadLibraryA 0xcc8e00f4 kernel32dlllstrlenA 0xe035f044 kernel32dllSleep 0xbb5f9ead kernel32dllReadFile 0xe553a458 kernel32dllVirtualAlloc 0x315e2145 user32dllGetDesktopWindow 0x3b2e55eb wininetdllHttpOpenRequestA 0x7b18062d wininetdllHttpSendRequestA 0xc69f8957 wininetdllInternetConnectA 0x0be057b7 wininetdllInternetErrorDlg 0xa779563a wininetdllInternetOpenA 0xe2899612 wininetdllInternetReadFile 0x869e4675 wininetdllInternetSetOptionA 0xe13bec74 ws232dllaccept 0x6737dbc2 ws232dllbind 0x614d6e75 ws232dllclosesocket 0x6174a599 ws232dllconnect 0xff38e9b7 ws232dlllisten 0x5fc8d902 ws232dllrecv 0xe0df0fea ws232dllWSASocketA 0x006b8029 ws232dllWSAStartup
完整的 Windows 10 系统 DLL 的 API 哈希列表可以在 这里 找到。
客户 ID / 水印根据官方网页提供的信息,客户 ID 是一个与 Cobalt Strike 许可密钥关联的 4 字节数字,并从第 39 版本开始嵌入到有效载荷和信标配置中。如果存在,客户 ID 位于有效载荷的末尾。客户 ID 可用于特定威胁作者的识别或归因,但许多客户 ID 来自破解或泄露的版本,因此在查找可能的归因时请考虑这一点。
DNS 启动器 x86
典型的有效载荷大小为 515 字节,或包含客户 ID 值时为 519 字节。DNS 查询名称字符串从 0x0140 的偏移量开始从有效载荷入口点计算,空字节和最大字符串大小为 63 字节。如果 DNS 查询名称字符串较短,则以空字节终止,其余的字符串空间填充为垃圾字节。
DnsQueryA API 函数以两个默认参数调用:
参数 值 常量 DNS 记录类型 (wType) 0x0010 DNSTYPETEXT DNS 查询选项 (Options) 0x0248 DNSQUERYBYPASSCACHE DNSQUERYNOHOSTSFILE DNSQUERYRETURNMESSAGE
除默认值外的任何内容都是可疑的,可能表明自定义有效载荷。
Python 解析:
默认 DNS 有效载荷 API 哈希:
偏移量 哈希值 API 名称 0x00a3 0xe553a458 kernel32dllVirtualAlloc 0x00bd 0x0726774c kernel32dllLoadLibraryA 0x012f 0xc99cc96a dnsapidllDnsQueryA 0x0198 0x56a2b5f0 kernel32dllExitProcess 0x01a4 0xe035f044 kernel32dllSleep 0x01e4 0xcc8e00f4 kernel32dlllstrlenA
YARA 规则用于 DNS 启动器:
SMB 启动器 x86
默认有效载荷大小为 346 字节,加上以空字节终止的管道名称字符串的长度和客户 ID 的长度如果存在。管道名称字符串位于有效载荷代码之后的 0x015A 偏移处,采用明文格式。
CreateNamedPipeA API 函数以 3 个默认参数调用:
参数 值 常量 打开模式(dwOpenMode) 0x0003 PIPEACCESSDUPLEX 管道模式(dwPipeMode) 0x0006 PIPETYPEMESSAGE PIPEREADMODEMESSAGE 最大实例数(nMaxInstances) 0x0001
Python 解析:
默认 SMB 有效载荷 API 哈希:
偏移量 哈希值 API 名称 0x00a1 0xe553a458 kernel32dllVirtualAlloc 0x00c4 0xd4df7045 kernel32dllCreateNamedPipeA 0x00d2 0xe27d6f28 kernel32dllConnectNamedPipe 0x00f8 0xbb5f9ead kernel32dllReadFile 0x010d 0xbb5f9ead kernel32dllReadFile 0x0131 0xfcddfac0 kernel32dllDisconnectNamedPipe 0x0139 0x528796c6 kernel32dllCloseHandle 0x014b 0x56a2b5f0 kernel32dllExitProcess
YARA 规则用于 SMB 启动器:
TCP Bind 启动器 x86
有效载荷大小为 332 字节,加上客户 ID 的长度如果存在。Bind API 函数的参数存储在 SOCKADDRIN 结构中,作为两个 dword 推送硬编码。第一个 PUSH 中的 sinaddr 值位于 0x00C4 的偏移量。第二个 PUSH 包含 sinport 和 sinfamily 值,位于 0x00C9 的偏移量。默认的 sinfamily 值为 AFINET (0x02)。
Python 解析:
默认 TCP Bind x86 有效载荷 API 哈希:
偏移量 哈希值 API 名称 0x009c 0x0726774c kernel32dllLoadLibraryA 0x00ac 0x006b8029 ws232dllWSAStartup 0x00bb 0xe0df0fea ws232dllWSASocketA 0x00d5 0x6737dbc2 ws232dllbind 0x00de 0xff38e9b7 ws232dlllisten 0x00e8 0xe13bec74 ws232dllaccept 0x00f1 0x614d6e75 ws232dllclosesocket 0x00fa 0x56a2b5f0 kernel32dllExitProcess 0x0107 0x5fc8d902 ws232dllrecv 0x011a 0xe553a458 kernel32dllVirtualAlloc 0x0128 0x5fc8d902 ws232dllrecv 0x013d 0x614d6e75 ws232dllclosesocket
YARA 规则用于 TCP Bind x86 启动器:
TCP Bind 启动器 x64
有效载荷大小为 510 字节,加上客户 ID 的长度如果存在。SOCKADDRIN 结构硬编码在 MOV 指令中,作为一个 qword,其中包含整个结构。MOV 指令的偏移量为 0x00EC。
Python 解析:
默认 TCP Bind x64 有效载荷 API 哈希:
偏移量 哈希值 API 名称 0x0100 0x0726774c kernel32dllLoadLibraryA 0x0111 0x006b8029 ws232dllWSAStartup 0x012d 0xe0df0fea ws232dllWSASocketA 0x0142 0x6737dbc2 ws232dllbind 0x0150 0xff38e9b7 ws232dlllisten 0x0161 0xe13bec74 ws232dllaccept 0x016f 0x614d6e75 ws232dllclosesocket 0x0198 0x5fc8d902 ws232dllrecv 0x01b8 0xe553a458 kernel32dllVirtualAlloc 0x01d2 0x5fc8d902 ws232dllrecv 0x01ee 0x614d6e75 ws232dllclosesocket
YARA 规则用于 TCP Bind x64 启动器:
TCP 反向启动器 x86
有效载荷大小为 290 字节,加上客户 ID 的长度如果存在。这个有效载荷与 TCP Bind x86 非常相似,SOCKADDRIN 结构在相同的偏移量中以相同的双推送指令硬编码,因此我们可以重复使用 TCP Bind x86 有效载荷的 Python 解析代码。
默认 TCP 反向 x86 有效载荷 API 哈希:
偏移量 哈希值 API 名称 0x009c 0x0726774c kernel32dllLoadLibraryA 0x00ac 0x006b8029 ws232dllWSAStartup 0x00bb 0xe0df0fea ws232dllWSASocketA 0x00d5 0x6174a599 ws232dllconnect 0x00e5 0x56a2b5f0 kernel32dllExitProcess 0x00f2 0x5fc8d902 ws232dllrecv 0x0105 0xe553a458 kernel32dllVirtualAlloc 0x0113 0x5fc8d902 ws232dllrecv
YARA 规则用于 TCP 反向 x86 启动器:
TCP 反向启动器 x64
默认有效载荷大小为 465 字节,加上客户 ID 的长度如果存在。有效载荷具有与 TCP Bind x64 启动器相同的 SOCKADDRIN 结构位置,因此我们可以再次重复使用解析代码。
默认 TCP 反向 x64 有效载荷 API 哈希:
偏移量 哈希值 API 名称 0x0100 0x0726774c kernel32dllLoadLibraryA 0x0111 0x006b8029 ws232dllWSAStartup 0x012d 0xe0df0fea ws232dllWSASocketA 0x0142 0x6174a599 ws232dllconnect 0x016b 0x5fc8d902 ws232dllrecv 0x018b 0xe553a458 kernel32dllVirtualAlloc 0x01a5 0x5fc8d902 ws232dllrecv 0x01c1 0x614d6e75 ws232dllclosesocket
YARA 规则用于 TCP 反向 x64 启动器:
HTTP 启动器 x86 和 x64
默认的 x86 有效载荷大小为 780 字节,而 x64 版本大小为 874 字节,外加请求地址字符串的大小和客户 ID 的大小如果存在。有效载荷包含存储在多个占位符中的完整请求信息。
请求地址请求地址是以空字节终止的明文字符串,位于最后一条有效载荷指令后面,没有任何填充。x86 版本的偏移量为 0x030C,x64 版本为 0x036A。典型格式为 IPv4。
请求端口对于 x86 版本,请求端口值硬编码在 PUSH 指令中,作为一个 dword。x64 版本的端口值在 0x010D 偏移量的 MOV r8d dword 指令中存储。
请求查询请求查询的占位符最大大小为 80 字节,值为以空字节终止的明文字符串。如果请求查询字符串较短,其余的字符串空间将填充为垃圾字节。x86 版本的占位符偏移量为 0x0143,x64 版本为 0x0186。
Cobalt Strike 和其他工具如 Metasploit使用简单的 checksum8 算法来区分 x86 和 x64 有效载荷或信标。
根据泄露的 Java web 服务器源代码, Cobalt Strike 仅使用两个 checksum 值,x86 有效载荷为 0x5C (92),x64 版本为 0x5D。还有严格启动器变体的实现,其中请求查询字符串必须为 5 个字符长包括斜杠。请求查询的 checksum 特性不是强制性的。
Python 实现的 checksum8 算法:
Metasploit 服务器使用类似的值:
您可以在 这里 找到 Cobalt Strike 的 x86 和 x64 严格请求查询的完整列表。
请求头请求头占位符的大小为 304 字节,值同样以空字节终止的明文字符串。请求头占位符紧接在请求查询占位符后面。x86 版本的偏移量为 0x0193,x64 版本为 0x01D6。
HTTP/HTTPS 启动器的典型请求头值为 UserAgent。Cobalt Strike web 服务器禁止以 lynx、curl 或 wget 开头的用户代理,并在发现这些字符串时返回 404 响应代码。
API 函数 HttpOpenRequestA 被调用,使用以下 dwFlags0x84600200:
Python 解析:
默认 HTTP x86 有效载荷 API 哈希:
偏移量 哈希值 API 名称 0x009c 0x0726774c kernel32dllLoadLibraryA 0x00aa 0xa779563a wininetdllInternetOpenA 0x00c6 0xc69f8957 wininetdllInternetConnectA 0x00de 0x3b2e55eb wininetdllHttpOpenRequestA 0x00f2 0x7b18062d wininetdllHttpSendRequestA 0x010b 0x5de2c5aa kernel32dllGetLastError 0x0114 0x315e2145 user32dllGetDesktopWindow 0x0123 0x0be057b7 wininetdllInternetErrorDlg 0x02c4 0x56a2b5f0 kernel32dllExitProcess 0x02d8 0xe553a458 kernel32dllVirtualAlloc 0x02f3 0xe2899612 wininetdllInternetReadFile
默认 HTTP x64 有效载荷 API 哈希:
偏移量 哈希值 API 名称 0x00e9 0x0726774c kernel32dllLoadLibraryA 0x0101 0xa779563a wininetdllInternetOpenA 0x0120 0xc69f8957 wininetdllInternetConnectA 0x013f 0x3b2e55eb wininetdllHttpOpenRequestA 0x0163 0x7b18062d wininetdllHttpSendRequestA 0x0308 0x56a2b5f0 kernel32dllExitProcess 0x0324 0xe553a458 kernel32dllVirtualAlloc 0x0342 0xe2899612 wininetdllInternetReadFile
YARA 规则用于 HTTP x86 和 x64 启动器:
HTTPS 启动器 x86 和 x64
有效载荷结构和占位符与 HTTP 启动器几乎相同,唯一的差别在于有效载荷大小、占位符偏移量、使用 InternetSetOptionA API 函数API 哈希 0x869e4675以及调用 HttpOpenRequestA API 函数时不同的 dwFlags。
默认的 x86 有效载荷大小为 817 字节,默认的 x64 版本为 909 字节,加上请求地址字符串的大小和客户 ID 的大小如果存在。
请求地址x86 版本的占位符偏移量为 0x0331,x64 版本为 0x038D。典型格式为 IPv4。
请求端口硬编码的请求端口格式与 HTTP 相同。x86 版本的 PUSH 偏移量为 0x00C3,x64 版本的 MOV 指令的偏移量为 0x0110。
请求查询请求查询的占位符格式和长度与 HTTP 版本相同。x86 版本的占位符偏移量为 0x0168,x64 版本为 0x01A9。
请求头请求头的大小和长度与 HTTP 版本相同。x86 版本的偏移量为 0x01B8,x64 版本为 0x01F9。
API 函数 HttpOpenRequestA 被调用,使用以下 dwFlags0x84A03200:
InternetSetOptionA API 函数以以下参数调用:
Python 解析:
默认 HTTPS x86 有效载荷 API 哈希:
偏移量 哈希值 API 名称 0x009c 0x0726774c kernel32dllLoadLibraryA 0x00af 0xa779563a wininetdllInternetOpenA 0x00cb 0xc69f8957 wininetdllInternetConnectA 0x00e7 0x3b2e55eb wininetdllHttpOpenRequestA 0x0100 0x869e4675 wininetdllInternetSetOptionA 0x0110 0x7b18062d wininetdllHttpSendRequestA 0x0129 0x5de2c5aa kernel32dllGetLastError 0x0132 0x315e2145 user32dllGetDesktopWindow 0x0141 0x0be057b7 wininetdllInternetErrorDlg 0x02e9 0x56a2b5f0 kernel32dllExitProcess 0x02fd 0xe553a458 kernel32dllVirtualAlloc 0x0318 0xe2899612 wininetdllInternetReadFile
默认 HTTPS x64 有效载荷 API 哈希:
偏移量 哈希值 API 名称 0x00e9 0x0726774c kernel32dllLoadLibraryA 0x0101 0xa779563a wininetdllInternetOpenA 0x0123 0xc69f8957 wininetdllInternetConnectA 0x0142 0x3b2e55eb wininetdllHttpOpenRequestA 0x016c 0x869e4675 wininetdllInternetSetOptionA 0x0186 0x7b18062d wininetdllHttpSendRequestA 0x032b 0x56a2b5f0 kernel32dllExitProcess 0x0347 0xe553a458 kernel32dllVirtualAlloc 0x0365 0xe2899612 wininetdllInternetReadFile
YARA 规则用于 HTTPS x86 和 x64 启动器:
下一个阶段或信标可以通过 curl 或 wget 工具轻松下载:
您可以在我们的 IoC 仓库 中找到原始有效载荷的解析器及所有相关的 YARA 规则。
原始有效载荷编码
Cobalt Strike 还包含一个有效载荷生成器,用于导出原始启动器和有效载荷,以多种编码格式进行。编码格式支持 UTF8 和 UTF16le。
以下是最常见的编码类型及其用法和示例的表格:
编码 用途 示例 Hex VBS HTA 4d5a9000 Hex Array PS1 0x4d 0x5a 0x90 0x00 Hex Veil PY x4dx5ax90x00 Decimal Array VBA 4241190 Char Array VBS HTA Chr(4)amp”H”ampChr(125) Base64 PS1 38uqIyMjQ6 gzip / deflate 压缩 PS1 XOR PS1 原始有效载荷 信标
大多数格式的解码都比较简单,但有几点需要注意。
Decimal 和 Char Array 中的值通过“换行”分隔,用“sn”表示x20x5Fx0A。PowerShell 脚本中使用的常见压缩算法是 GzipStream 和原始的 DeflateStream。Python 解压实现:
XOR 编码
XOR 算法在三种不同的情况下使用。第一种情况是在 PS1 脚本中使用一个字节的 XOR,默认值为 35 (0x23)。
第二种用法是用于 PE 启动器二进制文件中编码原始有效载荷或信标的 dword 密钥 XOR。特定的 xored 数据头长 16 字节,包括起始偏移量、xored 数据大小、XOR 密钥和四个 0x61 垃圾/填充字节。
Python 头部解析:
我们可以基于头部的 XOR 密钥和编码数据的第一个 dword 创建 YARA 规则,以验证假定的值:
免费外网npv加速器第三种情况是使用循环的 dword 密钥进行 XOR 编码,仅用于解码下载的信标。已编码的数据块紧随 XOR 算法代码之后,没有任何填充。已编码的数据以初始 XOR 密钥dword和数据大小dword 与初始密钥异或开头。
有 x86 和 x64 的 XOR 算法实现。Cobalt Strike 资源包括 xorbin 和 xor64bin 文件,其中包含已编译的 XOR 算法代码。
默认的 x86 编译代码长度为 52 和 56 字节具体取决于使用的寄存器,加上垃圾字节的长度。x86 实现允许使用不同的寄存器集,因此 xorbin 文件包括超过 800 个不同的已编译代码变体。
YARA 规则用于覆盖所有 x86 变体的 XOR 验证:
预编译的 x64 代码长度为 63 字节,没有垃圾字节。此外,只有一个预编译的代码变体。
YARA 规则用于带有 XOR 验证的 x64 变体:
您可以在 这里 找到我们的原始有效载荷解码器和提取器,支持最常见的编码。这使用了前一章中的解析器,能够为您节省时间和人工工作。我们还提供了一个 IDAPython 脚本,便于原始有效载荷分析。
结论
随着我们越来越多地看到威胁行为者滥用 Cobalt Strike,理解如何解码其使用方式对于恶意软件分析至关重要。
在这篇博客中,我们专注于理解威胁行为者如何使用 Cobalt Strike 有效载荷,以及您如何能够分析它们。
本系列的下一部分将专注于 Cobalt Strike 信标及其配置结构的解析。