解码 Cobalt Strike:理解有效负载 – Avast

介绍

是一种威胁模拟软件,被许多政府、组织和公司的信息安全团队广泛使用,成为事实上的标准闭源/付费工具。它在许多网络犯罪团伙中也非常流行,这些团伙通常会滥用破解或泄露版本的
Cobalt Strike。

Cobalt Strike拥有多个独特功能,安全通信,且完全模块化和可定制,因此正确检测和归因可能会非常复杂。这是我们在过去几年中几乎每个重大网络安全事件或大规模泄露中都能看到
Cobalt Strike 被使用的主要原因。

关于反向工程 Cobalt Strike软件的文章很多,尤其是其信标模块,因为这是整个链中的最重要部分。其他模块和有效载荷常常被忽视,但这些部分对于恶意软件研究者、取证分析师或调查员也包含了有价值的信息。

本系列的第一部分专注于所有原始有效载荷类型的正确识别,以及如何解码和解析它们。我们还分享了基于这些发现的有用解析器、脚本和 YARA 规则,回馈给

原始有效载荷

Cobalt Strike 的有效载荷基于 Meterpreter Shellcodes,具有许多相似之处,例如 API 哈希
(

版本) 或在 http/https 有效载荷中使用的 URL 查询

算法,这使得识别更加困难。这一特定的 checksum8 算法也用于其他框架,例如

让我们分别描述每个有效载荷的有趣部分。

有效载荷头 x86 版本

默认的 32 位原始有效载荷的入口点以典型指令 CLD (0xFC) 开始,随后是 CALL 指令和 PUSHA (0x60),作为 API哈希算法的第一个指令。

x86 有效载荷

有效载荷头 x64 版本

标准的 64 位变体同样以 CLD 指令开始,随后是 AND RSP,-10hCALL 指令。

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 | dnsapi.dll_DnsQuery_A | | 0x528796c6 | kernel32.dll_CloseHandle | | 0xe27d6f28 | kernel32.dll_ConnectNamedPipe | | 0xd4df7045 | kernel32.dll_CreateNamedPipeA | | 0xfcddfac0 | kernel32.dll_DisconnectNamedPipe | | 0x56a2b5f0 | kernel32.dll_ExitProcess | | 0x5de2c5aa | kernel32.dll_GetLastError | | 0x0726774c | kernel32.dll_LoadLibraryA | | 0xcc8e00f4 | kernel32.dll_lstrlenA | | 0xe035f044 | kernel32.dll_Sleep | | 0xbb5f9ead | kernel32.dll_ReadFile | | 0xe553a458 | kernel32.dll_VirtualAlloc | | 0x315e2145 | user32.dll_GetDesktopWindow | | 0x3b2e55eb | wininet.dll_HttpOpenRequestA | | 0x7b18062d | wininet.dll_HttpSendRequestA | | 0xc69f8957 | wininet.dll_InternetConnectA | | 0x0be057b7 | wininet.dll_InternetErrorDlg | | 0xa779563a | wininet.dll_InternetOpenA | | 0xe2899612 | wininet.dll_InternetReadFile | | 0x869e4675 | wininet.dll_InternetSetOptionA | | 0xe13bec74 | ws2_32.dll_accept | | 0x6737dbc2 | ws2_32.dll_bind | | 0x614d6e75 | ws2_32.dll_closesocket | | 0x6174a599 | ws2_32.dll_connect | | 0xff38e9b7 | ws2_32.dll_listen | | 0x5fc8d902 | ws2_32.dll_recv | | 0xe0df0fea | ws2_32.dll_WSASocketA | | 0x006b8029 | ws2_32.dll_WSAStartup |

完整的 Windows 10 系统 DLL 的 API 哈希列表可以在
找到。

客户 ID / 水印

根据官方网页提供的信息,客户 ID 是一个与 Cobalt Strike 许可密钥关联的 4 字节数字,并从第 3.9版本开始嵌入到有效载荷和信标配置中。如果存在,客户 ID 位于有效载荷的末尾。客户 ID 可用于特定威胁作者的识别或归因,但许多客户 ID来自破解或泄露的版本,因此在查找可能的归因时请考虑这一点。

DNS 启动器 x86

典型的有效载荷大小为 515 字节,或包含客户 ID 值时为 519 字节。DNS 查询名称字符串从 0x0140的偏移量开始(从有效载荷入口点计算),空字节和最大字符串大小为 63 字节。如果 DNS查询名称字符串较短,则以空字节终止,其余的字符串空间填充为垃圾字节。

DnsQuery_A API 函数以两个默认参数调用:

| 参数 | 值 | 常量 | |———————————————–|————|——————————-| | (wType) | 0x0010 | DNS_TYPE_TEXT | | (Options) | 0x0248 | DNS_QUERY_BYPASS_CACHE | | | | DNS_QUERY_NO_HOSTS_FILE | | | | DNS_QUERY_RETURN_MESSAGE |

除默认值外的任何内容都是可疑的,可能表明自定义有效载荷。

Python 解析:

默认 DNS 有效载荷 API 哈希:

| 偏移量 | 哈希值 | API 名称 | |——–|————–|———————————–| | 0x00a3 | 0xe553a458 | kernel32.dll_VirtualAlloc | | 0x00bd | 0x0726774c | kernel32.dll_LoadLibraryA | | 0x012f | 0xc99cc96a | dnsapi.dll_DnsQuery_A | | 0x0198 | 0x56a2b5f0 | kernel32.dll_ExitProcess | | 0x01a4 | 0xe035f044 | kernel32.dll_Sleep | | 0x01e4 | 0xcc8e00f4 | kernel32.dll_lstrlenA |

YARA 规则用于 DNS 启动器:

SMB 启动器 x86

默认有效载荷大小为 346 字节,加上以空字节终止的管道名称字符串的长度和客户 ID 的长度(如果存在)。管道名称字符串位于有效载荷代码之后的 0x015A偏移处,采用明文格式。

API 函数以 3 个默认参数调用:

| 参数 | 值 | 常量 | |———————|————|—————————-| | 打开模式(dwOpenMode) | 0x0003 | PIPE_ACCESS_DUPLEX | | 管道模式(dwPipeMode) | 0x0006 | PIPE_TYPE_MESSAGE, PIPE_READMODE_MESSAGE | | 最大实例数(nMaxInstances) | 0x0001 | |

Python 解析:

默认 SMB 有效载荷 API 哈希:

| 偏移量 | 哈希值 | API 名称 | |——–|————–|———————————–| | 0x00a1 | 0xe553a458 | kernel32.dll_VirtualAlloc | | 0x00c4 | 0xd4df7045 | kernel32.dll_CreateNamedPipeA | | 0x00d2 | 0xe27d6f28 | kernel32.dll_ConnectNamedPipe | | 0x00f8 | 0xbb5f9ead | kernel32.dll_ReadFile | | 0x010d | 0xbb5f9ead | kernel32.dll_ReadFile | | 0x0131 | 0xfcddfac0 | kernel32.dll_DisconnectNamedPipe | | 0x0139 | 0x528796c6 | kernel32.dll_CloseHandle | | 0x014b | 0x56a2b5f0 | kernel32.dll_ExitProcess |

YARA 规则用于 SMB 启动器:

TCP Bind 启动器 x86

有效载荷大小为 332 字节,加上客户 ID 的长度(如果存在)。Bind API 函数的参数存储在 SOCKADDR_IN 结构中,作为两个
dword 推送硬编码。第一个 PUSH 中的 sin_addr 值位于 0x00C4 的偏移量。第二个 PUSH 包含 sin_port 和
sin_family 值,位于 0x00C9 的偏移量。默认的 sin_family 值为 AF_INET (0x02)。

Python 解析:

默认 TCP Bind x86 有效载荷 API 哈希:

| 偏移量 | 哈希值 | API 名称 | |——–|————–|———————————–| | 0x009c | 0x0726774c | kernel32.dll_LoadLibraryA | | 0x00ac | 0x006b8029 | ws2_32.dll_WSAStartup | | 0x00bb | 0xe0df0fea | ws2_32.dll_WSASocketA | | 0x00d5 | 0x6737dbc2 | ws2_32.dll_bind | | 0x00de | 0xff38e9b7 | ws2_32.dll_listen | | 0x00e8 | 0xe13bec74 | ws2_32.dll_accept | | 0x00f1 | 0x614d6e75 | ws2_32.dll_closesocket | | 0x00fa | 0x56a2b5f0 | kernel32.dll_ExitProcess | | 0x0107 | 0x5fc8d902 | ws2_32.dll_recv | | 0x011a | 0xe553a458 | kernel32.dll_VirtualAlloc | | 0x0128 | 0x5fc8d902 | ws2_32.dll_recv | | 0x013d | 0x614d6e75 | ws2_32.dll_closesocket |

YARA 规则用于 TCP Bind x86 启动器:

TCP Bind 启动器 x64

有效载荷大小为 510 字节,加上客户 ID 的长度(如果存在)。SOCKADDR_IN 结构硬编码在 MOV 指令中,作为一个
qword,其中包含整个结构。MOV 指令的偏移量为 0x00EC。

Python 解析:

默认 TCP Bind x64 有效载荷 API 哈希:

| 偏移量 | 哈希值 | API 名称 | |——–|————–|———————————–| | 0x0100 | 0x0726774c | kernel32.dll_LoadLibraryA | | 0x0111 | 0x006b8029 | ws2_32.dll_WSAStartup | | 0x012d | 0xe0df0fea | ws2_32.dll_WSASocketA | | 0x0142 | 0x6737dbc2 | ws2_32.dll_bind | | 0x0150 | 0xff38e9b7 | ws2_32.dll_listen | | 0x0161 | 0xe13bec74 | ws2_32.dll_accept | | 0x016f | 0x614d6e75 | ws2_32.dll_closesocket | | 0x0198 | 0x5fc8d902 | ws2_32.dll_recv | | 0x01b8 | 0xe553a458 | kernel32.dll_VirtualAlloc | | 0x01d2 | 0x5fc8d902 | ws2_32.dll_recv | | 0x01ee | 0x614d6e75 | ws2_32.dll_closesocket |

YARA 规则用于 TCP Bind x64 启动器:

TCP 反向启动器 x86

有效载荷大小为 290 字节,加上客户 ID 的长度(如果存在)。这个有效载荷与 TCP Bind x86 非常相似,SOCKADDR_IN
结构在相同的偏移量中以相同的双推送指令硬编码,因此我们可以重复使用 TCP Bind x86 有效载荷的 Python 解析代码。

默认 TCP 反向 x86 有效载荷 API 哈希:

| 偏移量 | 哈希值 | API 名称 | |——–|————–|———————————–| | 0x009c | 0x0726774c | kernel32.dll_LoadLibraryA | | 0x00ac | 0x006b8029 | ws2_32.dll_WSAStartup | | 0x00bb | 0xe0df0fea | ws2_32.dll_WSASocketA | | 0x00d5 | 0x6174a599 | ws2_32.dll_connect | | 0x00e5 | 0x56a2b5f0 | kernel32.dll_ExitProcess | | 0x00f2 | 0x5fc8d902 | ws2_32.dll_recv | | 0x0105 | 0xe553a458 | kernel32.dll_VirtualAlloc | | 0x0113 | 0x5fc8d902 | ws2_32.dll_recv |

YARA 规则用于 TCP 反向 x86 启动器:

TCP 反向启动器 x64

默认有效载荷大小为 465 字节,加上客户 ID 的长度(如果存在)。有效载荷具有与 TCP Bind x64 启动器相同的 SOCKADDR_IN
结构位置,因此我们可以再次重复使用解析代码。

默认 TCP 反向 x64 有效载荷 API 哈希:

| 偏移量 | 哈希值 | API 名称 | |——–|————–|———————————–| | 0x0100 | 0x0726774c | kernel32.dll_LoadLibraryA | | 0x0111 | 0x006b8029 | ws2_32.dll_WSAStartup | | 0x012d | 0xe0df0fea | ws2_32.dll_WSASocketA | | 0x0142 | 0x6174a599 | ws2_32.dll_connect | | 0x016b | 0x5fc8d902 | ws2_32.dll_recv | | 0x018b | 0xe553a458 | kernel32.dll_VirtualAlloc | | 0x01a5 | 0x5fc8d902 | ws2_32.dll_recv | | 0x01c1 | 0x614d6e75 | ws2_32.dll_closesocket |

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 启动器的典型请求头值为 User-Agent。Cobalt Strike web 服务器禁止以 lynx、curl 或 wget开头的用户代理,并在发现这些字符串时返回 404 响应代码。

API 函数 HttpOpenRequestA 被调用,使用以下 dwFlags(0x84600200):

Python 解析:

默认 HTTP x86 有效载荷 API 哈希:

| 偏移量 | 哈希值 | API 名称 | |——–|————–|———————————–| | 0x009c | 0x0726774c | kernel32.dll_LoadLibraryA | | 0x00aa | 0xa779563a | wininet.dll_InternetOpenA | | 0x00c6 | 0xc69f8957 | wininet.dll_InternetConnectA | | 0x00de | 0x3b2e55eb | wininet.dll_HttpOpenRequestA | | 0x00f2 | 0x7b18062d | wininet.dll_HttpSendRequestA | | 0x010b | 0x5de2c5aa | kernel32.dll_GetLastError | | 0x0114 | 0x315e2145 | user32.dll_GetDesktopWindow | | 0x0123 | 0x0be057b7 | wininet.dll_InternetErrorDlg | | 0x02c4 | 0x56a2b5f0 | kernel32.dll_ExitProcess | | 0x02d8 | 0xe553a458 | kernel32.dll_VirtualAlloc | | 0x02f3 | 0xe2899612 | wininet.dll_InternetReadFile |

默认 HTTP x64 有效载荷 API 哈希:

| 偏移量 | 哈希值 | API 名称 | |——–|————–|———————————–| | 0x00e9 | 0x0726774c | kernel32.dll_LoadLibraryA | | 0x0101 | 0xa779563a | wininet.dll_InternetOpenA | | 0x0120 | 0xc69f8957 | wininet.dll_InternetConnectA | | 0x013f | 0x3b2e55eb | wininet.dll_HttpOpenRequestA | | 0x0163 | 0x7b18062d | wininet.dll_HttpSendRequestA | | 0x0308 | 0x56a2b5f0 | kernel32.dll_ExitProcess | | 0x0324 | 0xe553a458 | kernel32.dll_VirtualAlloc | | 0x0342 | 0xe2899612 | wininet.dll_InternetReadFile |

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 被调用,使用以下 dwFlags(0x84A03200):

InternetSetOptionA API 函数以以下参数调用:

Python 解析:

默认 HTTPS x86 有效载荷 API 哈希:

| 偏移量 | 哈希值 | API 名称 | |——–|————–|———————————–| | 0x009c | 0x0726774c | kernel32.dll_LoadLibraryA | | 0x00af | 0xa779563a | wininet.dll_InternetOpenA | | 0x00cb | 0xc69f8957 | wininet.dll_InternetConnectA | | 0x00e7 | 0x3b2e55eb | wininet.dll_HttpOpenRequestA | | 0x0100 | 0x869e4675 | wininet.dll_InternetSetOptionA | | 0x0110 | 0x7b18062d | wininet.dll_HttpSendRequestA | | 0x0129 | 0x5de2c5aa | kernel32.dll_GetLastError | | 0x0132 | 0x315e2145 | user32.dll_GetDesktopWindow | | 0x0141 | 0x0be057b7 | wininet.dll_InternetErrorDlg | | 0x02e9 | 0x56a2b5f0 | kernel32.dll_ExitProcess | | 0x02fd | 0xe553a458 | kernel32.dll_VirtualAlloc | | 0x0318 | 0xe2899612 | wininet.dll_InternetReadFile |

默认 HTTPS x64 有效载荷 API 哈希:

| 偏移量 | 哈希值 | API 名称 | |——–|————–|———————————–| | 0x00e9 | 0x0726774c | kernel32.dll_LoadLibraryA | | 0x0101 | 0xa779563a | wininet.dll_InternetOpenA | | 0x0123 | 0xc69f8957 | wininet.dll_InternetConnectA | | 0x0142 | 0x3b2e55eb | wininet.dll_HttpOpenRequestA | | 0x016c | 0x869e4675 | wininet.dll_InternetSetOptionA | | 0x0186 | 0x7b18062d | wininet.dll_HttpSendRequestA | | 0x032b | 0x56a2b5f0 | kernel32.dll_ExitProcess | | 0x0347 | 0xe553a458 | kernel32.dll_VirtualAlloc | | 0x0365 | 0xe2899612 | wininet.dll_InternetReadFile |

YARA 规则用于 HTTPS x86 和 x64 启动器:

下一个阶段或信标可以通过 curl 或 wget 工具轻松下载:

您可以在我们的
中找到原始有效载荷的解析器及所有相关的 YARA 规则。

原始有效载荷编码

Cobalt Strike 还包含一个有效载荷生成器,用于导出原始启动器和有效载荷,以多种编码格式进行。编码格式支持 UTF-8 和 UTF-16le。

以下是最常见的编码类型及其用法和示例的表格:

| 编码 | 用途 | 示例 | |————————-|—————————-|—————————| | Hex | VBS, HTA | 4d5a9000.. | | Hex Array | PS1 | 0x4d, 0x5a, 0x90, 0x00.. | | Hex Veil | PY | \x4d\x5a\x90\x00.. | | Decimal Array | VBA | -4,-24,-119,0.. | | Char Array | VBS, HTA | Chr(-4)&”H”&Chr(-125).. | | Base64 | PS1 | 38uqIyMjQ6.. | | gzip / deflate 压缩 | PS1 | | | XOR | PS1, 原始有效载荷, 信标 | |

大多数格式的解码都比较简单,但有几点需要注意。

  • Decimal 和 Char Array 中的值通过“换行”分隔,用“\s_\n”表示(\x20\x5F\x0A)。
  • PowerShell 脚本中使用的常见压缩算法是 GzipStream 和原始的 DeflateStream。

Python 解压实现:

XOR 编码

XOR 算法在三种不同的情况下使用。第一种情况是在 PS1 脚本中使用一个字节的 XOR,默认值为 35 (0x23)。

第二种用法是用于 PE 启动器二进制文件中编码原始有效载荷或信标的 dword 密钥 XOR。特定的 xored 数据头长 16字节,包括起始偏移量、xored 数据大小、XOR 密钥和四个 0x61 垃圾/填充字节。

Python 头部解析:

我们可以基于头部的 XOR 密钥和编码数据的第一个 dword 创建 YARA 规则,以验证假定的值:

第三种情况是使用循环的 dword 密钥进行 XOR 编码,仅用于解码下载的信标。已编码的数据块紧随 XOR 算法代码之后,没有任何填充。已编码的数据以初始
XOR 密钥(dword)和数据大小(dword 与初始密钥异或)开头。

有 x86 和 x64 的 XOR 算法实现。Cobalt Strike 资源包括 xor.bin 和 xor64.bin 文件,其中包含已编译的 XOR算法代码。

默认的 x86 编译代码长度为 52 和 56 字节(具体取决于使用的寄存器),加上垃圾字节的长度。x86 实现允许使用不同的寄存器集,因此 xor.bin文件包括超过 800 个不同的已编译代码变体。

YARA 规则用于覆盖所有 x86 变体的 XOR 验证:

预编译的 x64 代码长度为 63 字节,没有垃圾字节。此外,只有一个预编译的代码变体。

YARA 规则用于带有 XOR 验证的 x64 变体:

您可以在
找到我们的原始有效载荷解码器和提取器,支持最常见的编码。这使用了前一章中的解析器,能够为您节省时间和人工工作。我们还提供了一个 IDAPython脚本,便于原始有效载荷分析。

结论

随着我们越来越多地看到威胁行为者滥用 Cobalt Strike,理解如何解码其使用方式对于恶意软件分析至关重要。

在这篇博客中,我们专注于理解威胁行为者如何使用 Cobalt Strike 有效载荷,以及您如何能够分析它们。

本系列的下一部分将专注于 Cobalt Strike 信标及其配置结构的解析。

Leave a Reply

Your email address will not be published. Required fields are marked *