CS流量分析

本文最后更新于 2025年8月5日 中午

前言 原由是上周nepctf2025出了一道cs的流量题,刚好我没刷过类似题目,虽然写出来了,但是还是系统学习一下比较好的。

参考博客:

Cobalt Strike流量解密 - 1cePeak

(●´3`●)やれやれだぜ

基础前置

本人使用的cs4.7,同时teamserver放在本地kali上面

1
./teamserver [服务端ip] [密码]

比如说

1
./teamserver 192.168.71.3 123456

然后创建一个监听器,生成一个可执行程序然后在本地的虚拟机上线

2025-08-01111845

最后类似就是这样的内容

加密算法

Cobalt Strike(CS)在通信过程中采用多层加密机制,以实现 Beacon 和 TeamServer 间的加密通讯与流量伪装。其加密算法与版本、Profile 配置有关

AES-128-CBC(Beacon 加密 Payload 和通信数据)

  • 用途:对 Beacon 的任务、回传数据、Stage payload 等进行加密。
  • Key 生成方式:Beacon 在初始化时从 TeamServer 获取对称密钥,通常为预设或通过 RSA 握手生成。
  • IV:随机或固定,取决于 Malleable C2 配置。
  • 实现语言:Java(TeamServer)与 C(Beacon)

通常这种是默认的,也是最常见的加密算法,本文也是围绕这一块来谈

RSA-2048(Beacon 与 TeamServer 握手)

  • 用途:在 Beacon 初次上线时使用 RSA 公钥加密 AES 会话密钥。
  • 流程:
    • Beacon 内置 RSA 公钥(由 TeamServer 生成)。
    • Beacon 使用该公钥加密 AES 密钥并发送。
    • TeamServer 用私钥解密并获取 AES 会话密钥。
  • 密钥位置:
    • 存储在 .cobaltstrike.beacon_keys 或内嵌于 Payload。

2025-08-01113312

流量传递

Cobalt Strike 流量传递流程简述:

  1. 握手阶段
    Beacon 启动后,用 Team Server 公钥加密 AES 会话密钥发送给服务器,完成密钥交换。
  2. 加密通信
    后续数据用 AES-128-CBC 加密,数据再经 Base64 等编码,伪装成正常 HTTP/DNS/SMB 流量。
  3. 定时请求
    Beacon 按配置的 sleep 和 jitter 定时发起请求,获取任务并回传结果。
  4. Malleable C2
    通过配置 HTTP 头、URI、编码方式等,实现流量伪装,降低检测风险。

Cobalt Strike 的 Beacon 在初始通信中常通过 HTTP GET 请求伪装为正常浏览行为,访问如 /dpixel/__utm.gif/pixel.gif 等路径,并将包含 AES 会话密钥等元数据的信息使用 RSA 公钥加密后,通过 Base64 编码写入 Cookie 头中发送给 C2 服务器,从而完成安全信道的建立。

2025-08-01143054

这里抓了一下本地的流量,可以注意到伪装的ga.js(也可以伪装成其他的内容)然后一段长的cookie值

2025-08-01143409

下发指令的时候会请求 /submit.php?id=一串数字同时post一段一串 0000 开头的16进制数据,这是 cs 流量的发送任务数据,这里我执行的是getuid的命令

流量解密

方法一

这里假如CTF题目提供了.cobaltstrike.beacon_keys可以通过脚本来获取内容

cs-scripts/parse_beacon_keys.py at master · Slzdude/cs-scripts

参考这个脚本,我贴一下我微调的脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import base64
import javaobj

def to_pem(key_bytes: bytes, header: str) -> str:
pem_body = base64.encodebytes(key_bytes).decode().replace('\n', '')
lines = [f"-----BEGIN {header}-----"]
lines += [pem_body[i:i+64] for i in range(0, len(pem_body), 64)]
lines.append(f"-----END {header}-----")
return '\n'.join(lines)

with open(".cobaltstrike.beacon_keys", "rb") as f:
pobj = javaobj.load(f)

# 结构访问
key_entry = pobj.array.value

# 获取 JavaByteArray._data
priv_raw = key_entry.privateKey.encoded._data
pub_raw = key_entry.publicKey.encoded._data

# 转换为字节流
priv_bytes = bytes([b & 0xFF for b in priv_raw])
pub_bytes = bytes([b & 0xFF for b in pub_raw])

# 转 PEM
private_pem = to_pem(priv_bytes, "PRIVATE KEY")
public_pem = to_pem(pub_bytes, "PUBLIC KEY")

print(private_pem)
print(public_pem)

这样可以获得公钥和私钥

再拿这个项目WBGlIl/CS_Decrypt,这个代码不兼容,我也贴一下我微调的代码,把心跳的时候的cookie贴进去

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
from cryptography.hazmat.primitives import serialization, hashes
from cryptography.hazmat.primitives.asymmetric import padding
import base64
import hashlib
import hexdump


PRIVATE_KEY = b"""-----BEGIN PRIVATE KEY-----

-----END PRIVATE KEY-----
"""

# base64 编码密文(C2 返回的 Beacon metadata 字段)
# 请替换此变量
encode_data = "QGhgfFHXZOUsjC8YcgAdkB6fAgepIFJDM9iO7j39Q8aHelwnzqRILSMxM9gPUy3b+TfLpbNd33HdSXisl36A2Qj9qYFEaM8F3DTzAPFHpXs91tEaBUavdSRFLk13sUFAI7Cd41FlAc/ChNRidbGApbjB08JPpv0nvQE44gTuao0="

# 加载私钥
private_key = serialization.load_pem_private_key(PRIVATE_KEY, password=None)

# 解密(PKCS#1 v1.5)
ciphertext = private_key.decrypt(
base64.b64decode(encode_data),
padding=padding.PKCS1v15()
)

# ========== 以下为原始 Beacon metadata 字段结构解析 ==========

def isFlag(var, flag): return (var & flag) == flag

def toIP(var):
return ".".join([str((var >> (8 * i)) & 0xff) for i in reversed(range(4))])

def getName(var0):
mapping = {
936: "gbk", 950: "big5", 65001: "utf-8", 1252: "windows-1252", 0: "ISO8859-1"
}
return mapping.get(var0, "ISO8859-1")

if ciphertext[0:4] == b'\x00\x00\xBE\xEF':
raw_aes_keys = ciphertext[8:24]
var9 = int.from_bytes(ciphertext[24:26], "little")
var9_name = getName(var9)

beacon_id = int.from_bytes(ciphertext[28:32], "big")
pid = int.from_bytes(ciphertext[32:36], "big")
port = int.from_bytes(ciphertext[36:38], "big")
flag = int.from_bytes(ciphertext[38:39], "big")

arch = "x64" if isFlag(flag, 2) else "x86"
is64 = "1" if isFlag(flag, 4) else "0"
bypass_uac = "True" if isFlag(flag, 8) else "False"

win_major = ciphertext[39]
win_minor = ciphertext[40]
win_build = int.from_bytes(ciphertext[41:43], "big")

ip = toIP(int.from_bytes(ciphertext[55:59], "little"))
ip = ip if ip != "0.0.0.0" else "unknown"

# 解码字符串部分
try:
ddata = ciphertext[59:].decode(var9_name, errors="replace")
except:
ddata = ciphertext[59:].decode("ISO8859-1", errors="replace")

fields = ddata.split("\t")
computer = fields[0] if len(fields) > 0 else ""
username = fields[1] if len(fields) > 1 else ""
process = fields[2] if len(fields) > 2 else ""

print(f"Beacon ID: {beacon_id}")
print(f"PID: {pid}")
print(f"Port: {port}")
print(f"Arch: {arch}, is64: {is64}, BypassUAC: {bypass_uac}")
print(f"Windows version: {win_major}.{win_minor}.{win_build}")
print(f"Host IP: {ip}")
print(f"Computer: {computer}, Username: {username}, Process: {process}")

digest = hashlib.sha256(raw_aes_keys).digest()
aes_key = digest[:16]
hmac_key = digest[16:]
print(f"AES key: {aes_key.hex()}")
print(f"HMAC key: {hmac_key.hex()}")

print("\nFull metadata hexdump:")
print(hexdump.hexdump(ciphertext))

else:
print("Invalid Beacon metadata header")

2025-08-01151856

成功拿到被控主机信息和两个需要用到的 key

1
2
AES key: b3fbe5014705d989f005deb2f759fb80
HMAC key: af2bcf97ce60a201446fe44c3e7d2cbe

然后再解密CS流量,既然存在通信,那么必然有数据传输,所以直接查看存在data字段的数据包。

还是用上面的项目的CS_Task_AES_Decrypt.py代码

需要先fromhex再to base64

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
'''
cobaltstrike任务解密
'''
import hmac
import binascii
import base64
import struct

import hexdump
from Crypto.Cipher import AES

def compare_mac(mac, mac_verif):
if mac == mac_verif:
return True
if len(mac) != len(mac_verif):
print
"invalid MAC size"
return False

result = 0

for x, y in zip(mac, mac_verif):
result |= x ^ y

return result == 0


def decrypt(encrypted_data, iv_bytes, signature, shared_key, hmac_key):
if not compare_mac(hmac.new(hmac_key, encrypted_data, digestmod="sha256").digest()[0:16], signature):
print("message authentication failed")
return

cypher = AES.new(shared_key, AES.MODE_CBC, iv_bytes)
data = cypher.decrypt(encrypted_data)
return data


def readInt(buf):
return struct.unpack('>L', buf[0:4])[0]

#接收到的任务数据
shell_whoami="G2ZeKrJpjQiBVl8KiUhPm0Gcc8iWcq/mbiCtn0uPTqE/cD9OsYRO9J2riLj1CXrC"

if __name__ == "__main__":
#key源自Beacon_metadata_RSA_Decrypt.py
SHARED_KEY = binascii.unhexlify("b3fbe5014705d989f005deb2f759fb80")
HMAC_KEY = binascii.unhexlify("af2bcf97ce60a201446fe44c3e7d2cbe")

enc_data = base64.b64decode(shell_whoami)
print("数据总长度:{}".format(len(enc_data)))
signature = enc_data[-16:]
encrypted_data = enc_data[:-16]

iv_bytes = bytes("abcdefghijklmnop",'utf-8')

dec = decrypt(encrypted_data,iv_bytes,signature,SHARED_KEY,HMAC_KEY)

counter = readInt(dec)
print("时间戳:{}".format(counter))

decrypted_length = readInt(dec[4:])
print("任务数据包长度:{}".format(decrypted_length))

data = dec[8:len(dec)]
print("任务Data")
print(hexdump.hexdump(data))

# 任务标志
Task_Sign=data[0:4]
print("Task_Sign:{}".format(Task_Sign))

# 实际的任务数据长度
Task_file_len = int.from_bytes(data[4:8], byteorder='big', signed=False)
print("Task_file:{}".format(Task_file_len))

with open('data.bin', 'wb') as f:
f.write(data[8:Task_file_len])

print(hexdump.hexdump(data[Task_file_len:]))

2025-08-01152627

可以得到数据,然后?id=那个的数据包用Beacon_Task_return_AES_Decrypt.py来解密,也是先from hex再to base64,大体流程就是这样。

方法二

参考这个项目

minhangxiaohui/CSthing: somthing about Cobaltstrike

参考文章

Cobalt Strike:使用已知私钥解密流量 – 第 2 部分 – NVISO Labs — Cobalt Strike: Using Known Private Keys To Decrypt Traffic – Part 2 – NVISO Labs

这种情况一般是在没有提供密钥也就是没有提供.cobaltstrike.beacon_keys,可以尝试用这个方法

利用wireshark提取出来

2025-08-04211849

很明显这里不是已知的cskey库里的,所以没给出密钥,拿到信标依然没有作用

还是得拿到本地的 key 文件才能解密流量,这里没啥用

(或者这里可以根据n在factordb进行分解,可以得到p和q的话,或者使用rsactftool弱公钥解,那么可以构造私钥,进一步也能实现,也是概率问题,不一定能成功)

那在如果有的情况下,可以继续执行

1
python3 cs-decrypt-metadata.py -p 私钥 cookie

得到 Raw key、AES key 和 HMAC key

接下来用

1
python3 cs-parse-http-traffic.py -r [Raw key] xxx.pcapng

这样就能得到一定的数据,在不规定其它参数的情况下解密出流量包中的通信流量,解决的比较快,但是大概率在实际情况还是要配合其他方法一起食用。

方法三

通过进程转储(dump)直接从内存中提取加密密钥(AES key、HMAC key和RAW key),从而绕过传统的私钥解密过程。

这里需要用到的脚本

Beta/cs-extract-key.py at master · DidierStevens/Beta

3.x

在 Cobalt Strike 3.x 中,信标的关键元数据(如 AES Key、HMAC Key、IV)在内存中以明文形式存储,且以特征标志 0x0000BEEF 开头。通过对信标进程进行内存转储,可在早期阶段直接提取这三组未加密的密钥信息,适用于内存取证与样本分析等场景。

1
python3 cs-extract-key.py test.dmp

之后获得key之后大体的逻辑是差不多的

4.x

Cobalt Strike 3.x 中信标的元数据以明文形式存储于内存,易通过特征标志如 0x0000BEEF 提取关键密钥;而在 4.x 中,这些数据被加密并进行了结构混淆,内存中不再直接暴露明文 key,大幅提升了对抗分析与取证的难度,是从易识别向高隐匿演进的重要转变。

具体例题可以参考nepCTF2025的misc题客服小美,我没上传博客

首先先用上文提到的 cs-parse-http-traffic.py 脚本对抓取的流量包进行提取加密数据的操作

1
python cs-parse-http-traffic.py -k unknown  DESKTOP.pcapng

这时候可以获得数据,得到加密数据之后,再用动态信标的进程转储文件进行爆破解密,得到 key

然后执行

1
python3 cs-extract-key.py -t 加密data test.dmp

2025-08-05111255

差不多就是这样的例子,那么最后也可以照着前面的进行解密得到内容

比如利用 RAWkey 使用 cs-parse-http-traffic.py 对通信流量进行解密了,不同的是这里使用的是 SHA256 Raw Key,参数为 - k

1
python cs-parse-http-traffic.py -k [SHA256 Raw key] test.pcapng

当然也可以带入到CS_Task_AES_Decrypt.py这个脚本使用,具体问题具体分析。

后话

配合食用几个题目,大体逻辑应该就是了解了,这篇就水到这里了。


CS流量分析
https://0ran9ewww.github.io/2025/08/05/学习文章/CS流量分析/
作者
orange
发布于
2025年8月5日
更新于
2025年8月5日
许可协议