第一题 Tunnel Traffic
WireGuard 流量解密
我给的提示词:
请分析文件夹中的file.pcapng流量包中的WireGuard流量,可以memdump.lime获取用于解密的信息,最后拿到flag
1. 题目目标
分析当前目录中的 file.pcapng(流量包)与 memdump.lime(内存镜像),提取可用于 WireGuard 解密的信息,最终拿到 flag。
2. 样本与环境
当前目录关键文件:
file.pcapngmemdump.lime
工具环境(实测):
tshark4.6.3python(含cryptography库)tar(Windows 内置 bsdtar)- PowerShell
3. 整体思路
- 在
pcapng里先确认 WireGuard 会话与握手信息,拿到接收端公钥(wg.receiver_pubkey)。 - 从
memdump.lime中提取可能的 X25519 私钥,筛选出公钥能匹配握手公钥的私钥。 - 生成 WireGuard keylog 文件,验证私钥确实对应抓包中的两个 WireGuard 会话。
- 从解密后的内层 HTTP 数据里重组上传文件
backup.tar.gz。 - 解包后读取敏感文件,获取真实 flag。
4. 第一步:识别 WireGuard 流量
4.1 看 UDP 会话,定位可疑隧道端口
powershell
& 'C:\Program Files\Wireshark\tshark.exe' -r .\file.pcapng -q -z conv,udp
可观察到典型 WireGuard 端口会话(例如 192.168.1.100 <-> 10.10.0.1:51820),以及伪装在 443/udp 的可疑会话(203.0.113.50 <-> 198.51.100.10:443)。
4.2 强制按 WireGuard 解码并查看消息类型
powershell
& 'C:\Program Files\Wireshark\tshark.exe' `
-r .\file.pcapng `
-d udp.port==51820,wg `
-Y "wg" `
-T fields -e frame.number -e ip.src -e ip.dst -e wg.type
wg.type 含义:
1= Handshake Initiation2= Handshake Response4= Transport Data
5. 第二步:从握手提取目标公钥
从握手 initiation 包中提取 wg.receiver_pubkey(接收端静态公钥):
powershell
& 'C:\Program Files\Wireshark\tshark.exe' `
-r .\file.pcapng `
-d udp.port==51820,wg `
-Y "wg.type==1" `
-T fields -e frame.number -e ip.src -e ip.dst -e wg.receiver_pubkey
得到两个关键公钥(后续用于匹配内存里的私钥):
YYr6pRKCo/6rb6LaFxdHlhhSZQ8dOGZpk0tLjUkHbG4=fz+M3RfABjYGC25QMMdInlixOPbcC9Lpy0ZxdjPwtGA=
6. 第三步:从 memdump.lime 扫描并匹配 WireGuard 私钥
下面脚本思路:
- 遍历内存中所有长度 32 字节窗口;
- 用 X25519 私钥 clamp 规则进行快速过滤;
- 将候选私钥推导公钥;
- 与抓包中的两个
wg.receiver_pubkey做匹配; - 输出命中的私钥与偏移。
6.1 Python 脚本(可直接复制执行)
python
import base64
from pathlib import Path
from cryptography.hazmat.primitives.asymmetric import x25519
from cryptography.hazmat.primitives import serialization
targets = {
base64.b64decode("YYr6pRKCo/6rb6LaFxdHlhhSZQ8dOGZpk0tLjUkHbG4="),
base64.b64decode("fz+M3RfABjYGC25QMMdInlixOPbcC9Lpy0ZxdjPwtGA="),
}
data = Path("memdump.lime").read_bytes()
L = len(data)
found = []
checked = 0
for i in range(0, L - 32):
b0 = data[i]
b31 = data[i + 31]
# X25519 clamped private key condition:
# b0 & 7 == 0
# b31 & 128 == 0
# b31 & 64 == 64
if (b0 & 0x07) != 0:
continue
if (b31 & 0x80) != 0:
continue
if (b31 & 0x40) == 0:
continue
sk_bytes = data[i:i+32]
checked += 1
try:
sk = x25519.X25519PrivateKey.from_private_bytes(sk_bytes)
pk = sk.public_key().public_bytes(
encoding=serialization.Encoding.Raw,
format=serialization.PublicFormat.Raw
)
except Exception:
continue
if pk in targets:
found.append((
i,
base64.b64encode(sk_bytes).decode(),
base64.b64encode(pk).decode()
))
print("candidate_checked=", checked)
print("found=", len(found))
for off, sk, pk in found:
print(off, sk, "=>", pk)
6.2 运行命令
powershell
@'
<把上面的 Python 代码粘贴到这里>
'@ | python -
6.3 期望结果
命中 2 把私钥(示例结果):
- 偏移约
4252024:6BsXEpwOw1tzUl5nIXsPpsQsCXt27QKvqVlbmQ8qH2E= - 偏移约
4264069:GOgd5lIim6WkWh6tj3XDzMtlgdIueC4IMV7i2rKM4mo=
7. 第四步:验证私钥与公钥对应关系
python
from base64 import b64decode, b64encode
from cryptography.hazmat.primitives.asymmetric import x25519
keys = [
"6BsXEpwOw1tzUl5nIXsPpsQsCXt27QKvqVlbmQ8qH2E=",
"GOgd5lIim6WkWh6tj3XDzMtlgdIueC4IMV7i2rKM4mo=",
]
for k in keys:
sk = x25519.X25519PrivateKey.from_private_bytes(b64decode(k))
pk = sk.public_key().public_bytes_raw()
print(k, "->", b64encode(pk).decode())
输出应分别对应:
YYr6pRKCo/6rb6LaFxdHlhhSZQ8dOGZpk0tLjUkHbG4=fz+M3RfABjYGC25QMMdInlixOPbcC9Lpy0ZxdjPwtGA=
这一步证明了从内存提取到的私钥就是 WireGuard 会话私钥。
8. 第五步:生成 WireGuard keylog 并让 tshark 识别
创建 wg_keys.log:
powershell
Set-Content .\wg_keys.log @"
LOCAL_STATIC_PRIVATE_KEY = 6BsXEpwOw1tzUl5nIXsPpsQsCXt27QKvqVlbmQ8qH2E=
LOCAL_STATIC_PRIVATE_KEY = GOgd5lIim6WkWh6tj3XDzMtlgdIueC4IMV7i2rKM4mo=
"@
验证 tshark 已认出对应私钥(known_privkey=True):
powershell
& 'C:\Program Files\Wireshark\tshark.exe' `
-r .\file.pcapng `
-d udp.port==51820,wg `
-o "wg.keylog_file:.\wg_keys.log" `
-Y "frame.number==20 || frame.number==69" `
-T fields -e frame.number -e wg.receiver_pubkey -e wg.receiver_pubkey.known_privkey
说明:
- 在
tshark 4.6.3上会遇到 WireGuard dissector 的 assertion warning(已知兼容性问题),但不影响本题密钥匹配与后续证据链结论。
9. 第六步:从内层 HTTP 请求重组上传文件
在本题中,已可得到解密后的内层客户端请求数据(inner_c2s_reassembled.bin),内容是一个 multipart/form-data 的 POST /upload,其中包含 backup.tar.gz。
下面脚本用于从 HTTP multipart 中提取上传文件:
python
from pathlib import Path
import re
raw = Path("inner_c2s_reassembled.bin").read_bytes()
sep = b"\r\n\r\n"
idx = raw.find(sep)
if idx < 0:
raise SystemExit("No HTTP header/body separator found")
headers = raw[:idx].decode("latin1", errors="replace")
body = raw[idx + 4:]
m = re.search(r"boundary=([^\r\n;]+)", headers)
if not m:
raise SystemExit("No boundary found")
boundary = m.group(1).encode()
start_marker = b"--" + boundary
parts = body.split(start_marker)
file_bytes = None
for p in parts:
if b'filename="backup.tar.gz"' in p:
p2 = p.lstrip(b"\r\n")
i = p2.find(b"\r\n\r\n")
if i < 0:
continue
data = p2[i + 4:]
data = data.rstrip(b"\r\n")
if data.endswith(b"--"):
data = data[:-2].rstrip(b"\r\n")
file_bytes = data
break
if file_bytes is None:
raise SystemExit("backup.tar.gz not found in multipart")
Path("recovered_from_inner_c2s_backup.tar.gz").write_bytes(file_bytes)
print("Recovered bytes:", len(file_bytes))
运行后得到:
recovered_from_inner_c2s_backup.tar.gz
10. 第七步:解包并提取 flag
10.1 解压并查看内容
powershell
New-Item -ItemType Directory -Path .\tmp_recovered -Force | Out-Null
tar -xzf .\recovered_from_inner_c2s_backup.tar.gz -C .\tmp_recovered
Get-ChildItem -Recurse .\tmp_recovered
关键文件:
tmp_recovered\backup\.config\credentials.jsontmp_recovered\backup\README.md
10.2 读取潜在 flag
powershell
Get-Content -Raw .\tmp_recovered\backup\.config\credentials.json
Get-Content -Raw .\tmp_recovered\backup\README.md
可见:
credentials.json中 token 为:flag{ba00e1df-c9f3-4ac7-8cf5-a61b5936ce18}README.md中为:falg{a6bd8da6143c3f161d323f6415ba322}(拼写错误,疑似干扰项)
11. 结论与最终答案
真实 flag 为:
text
flag{ba00e1df-c9f3-4ac7-8cf5-a61b5936ce18}
理由:
- 该值位于解密后上传压缩包中的敏感凭据文件
credentials.json,语义完整且格式规范。 README.md中字符串为falg{...},明显为拼写错误的诱饵。
第二题:bash_dump
我给的提示词是:
你现在是一个应急响应专家,小赵遇到了一个linux勒索病毒,他拿着公司提供的dump工具登上机器,发现黑客正在执行内网横向,没有
发现勒索病毒进程,只发现了黑客的bash进程.所以他就将bash进程使用公司的dump工具dump下来,然后他找到了被勒索的文件,并根据勒索信特征在网络上找到了相关分析文章,向作者拿到了勒索病毒的hash:03d44cc44664aad7816fa73dc38ce8e2a67864911ceb75c49f90fabfdb821ab5,并在一个类似vt的平台上下载到了样本.请分析文件夹中的文件,最终要找到flag
1. 题目概述
题目给了三类关键证据:
- 攻击者 bash 进程内存 dump:
dumped_4053_*.bin - 勒索样本:
03d44cc44664aad7816fa73dc38ce8e2a67864911ceb75c49f90fabfdb821ab5 - 被加密文件:
flag.solarsec.locked
目标是恢复最终 flag。
2. 取证与线索提取
在 dumped_4053_[23d1000-253e000].bin 中提取字符串后,看到多条勒索程序执行痕迹:
text
./my_ransomware_23333 cae77ea5367e8d40bfa607179104 1155513bd65ceeeba70af9f06372
./my_ransomware_666666666 c2e77ea5367e8d40bfa607179104 1155513bd65ceeeba70af9f06372
./my_r@nsomw4re_lollollollol 0ae7dea53e7e8d40bfa907179104 1155513bd65ceeeba70af9f06a72
./my_r@nsomw4re_hahahahaha cae7dea53e7e8d40bfa607179104 1155513bd65ceeeba70af9f06a72
可判断勒索程序命令行需要两个 16 进制参数(疑似公钥坐标)。
3. 样本逆向结论
逆向样本主流程可得:
- 参数数量必须为 3(程序名 + x + y),否则打印 usage
- 固定读取文件:
flag.solarsec - 调用
ecc_enc(...)进行加密 - 输出
flag.solarsec.locked
输出文件格式:
text
"LOCKED"(6字节) + 原文长度(8字节小端) + 密文
ecc_enc 内置椭圆曲线参数(112-bit):
p = 0xe2e20e64347abef0fef83d1a5901n = 0xe2e20e64347abea8720594c31783a = 0x592a5290d72d5d58c73266dc96b2b = 0x630e734697aa226547d498f14e5bGx = 0x881fb0676400f362aac2b6ae22e6Gy = 0x5ceeaeea8ed10a70ee90737c9efb
从实现看,是 Crypto++ 的 ECIES 变体(KDF2(SHA1) + XOR + HMAC-SHA1)。
4. 关键突破点
从 dump 提取的 4 组候选坐标中,只有以下一组在曲线上:
Qx = cae7dea53e7e8d40bfa607179104Qy = 1155513bd65ceeeba70af9f06a72
并且 Q = dG 属于该子群。
同时 n 可分解为小因子乘积,适合用 Pohlig-Hellman 求离散对数恢复私钥 d。
5. 解题代码(可直接复制运行)
保存为
solve.py后执行:python solve.py
python
#!/usr/bin/env python3
import re
import math
import hmac
import hashlib
from pathlib import Path
# ========= Domain Parameters (from ransomware binary) =========
p = int("e2e20e64347abef0fef83d1a5901", 16)
n = int("e2e20e64347abea8720594c31783", 16)
a = int("592a5290d72d5d58c73266dc96b2", 16)
b = int("630e734697aa226547d498f14e5b", 16)
G = (
int("881fb0676400f362aac2b6ae22e6", 16),
int("5ceeaeea8ed10a70ee90737c9efb", 16),
)
# n factorization
FACTORS = [23, 347, 579079, 5412049, 3501919, 52536195991]
INF = None
def inv_mod(x, m):
return pow(x % m, -1, m)
def on_curve(P):
if P is INF:
return True
x, y = P
return (y * y - (x * x * x + a * x + b)) % p == 0
def point_neg(P):
if P is INF:
return INF
x, y = P
return (x, (-y) % p)
def point_add(P, Q):
if P is INF:
return Q
if Q is INF:
return P
x1, y1 = P
x2, y2 = Q
if x1 == x2:
if (y1 + y2) % p == 0:
return INF
lam = ((3 * x1 * x1 + a) * inv_mod(2 * y1, p)) % p
else:
lam = ((y2 - y1) * inv_mod(x2 - x1, p)) % p
x3 = (lam * lam - x1 - x2) % p
y3 = (lam * (x1 - x3) - y1) % p
return (x3, y3)
def scalar_mul(k, P):
if P is INF:
return INF
if k < 0:
return scalar_mul(-k, point_neg(P))
R = INF
A = P
while k:
if k & 1:
R = point_add(R, A)
A = point_add(A, A)
k >>= 1
return R
def point_key(P):
return ("INF",) if P is INF else (P[0], P[1])
def bsgs(base, target, order):
m = int(math.isqrt(order) + 1)
table = {}
R = INF
for j in range(m):
k = point_key(R)
if k not in table:
table[k] = j
R = point_add(R, base)
step = point_neg(scalar_mul(m, base))
gamma = target
for i in range(m + 1):
k = point_key(gamma)
if k in table:
return i * m + table[k]
gamma = point_add(gamma, step)
return None
def crt(congruences):
# congruences: [(a1,m1), (a2,m2), ...], pairwise coprime
M = 1
for _, mi in congruences:
M *= mi
x = 0
for ai, mi in congruences:
Mi = M // mi
inv = pow(Mi, -1, mi)
x = (x + ai * Mi * inv) % M
return x, M
def pohlig_hellman(G, Q, n, factors):
congruences = []
for q in factors:
h = n // q
Gi = scalar_mul(h, G)
Qi = scalar_mul(h, Q)
di = bsgs(Gi, Qi, q)
if di is None:
raise RuntimeError(f"BSGS failed on factor {q}")
di %= q
if scalar_mul(di, Gi) != Qi:
raise RuntimeError(f"Sub-check failed on factor {q}")
congruences.append((di, q))
d, mod = crt(congruences)
if mod != n:
raise RuntimeError("CRT modulus mismatch")
d %= n
if scalar_mul(d, G) != Q:
raise RuntimeError("Final d verification failed")
return d
def kdf2_sha1(z, out_len, shared_info=b""):
out = b""
counter = 1
while len(out) < out_len:
out += hashlib.sha1(z + counter.to_bytes(4, "big") + shared_info).digest()
counter += 1
return out[:out_len]
def extract_candidate_points_from_dump(dump_path):
data = Path(dump_path).read_bytes()
text = "".join(chr(c) if 32 <= c < 127 else " " for c in data)
pairs = re.findall(r"([0-9a-f]{28})\s+([0-9a-f]{28})", text)
uniq = []
for x, y in pairs:
t = (x, y)
if t not in uniq:
uniq.append(t)
return uniq
def choose_valid_pubkey(candidates):
valid = []
for xh, yh in candidates:
P = (int(xh, 16), int(yh, 16))
if on_curve(P) and scalar_mul(n, P) is INF:
valid.append(P)
if len(valid) != 1:
raise RuntimeError(f"Expected 1 valid pubkey, got {len(valid)}")
return valid[0]
def decrypt_locked(locked_path, d):
raw = Path(locked_path).read_bytes()
if raw[:6] != b"LOCKED":
raise RuntimeError("Bad magic")
orig_len = int.from_bytes(raw[6:14], "little")
ct = raw[14:]
field_len = (p.bit_length() + 7) // 8 # 14 bytes
if ct[0] != 0x04:
raise RuntimeError("Unexpected ephemeral point format")
ep_len = 1 + 2 * field_len
Rx = int.from_bytes(ct[1:1 + field_len], "big")
Ry = int.from_bytes(ct[1 + field_len:1 + 2 * field_len], "big")
R = (Rx, Ry)
if not on_curve(R):
raise RuntimeError("Ephemeral point not on curve")
C = ct[ep_len:-20]
T = ct[-20:]
S = scalar_mul(d, R)
if S is INF:
raise RuntimeError("Shared secret is INF")
xS = S[0].to_bytes(field_len, "big")
K = kdf2_sha1(xS, len(C) + 16, b"")
ks = K[:len(C)]
mk = K[len(C):]
tag = hmac.new(mk, C, hashlib.sha1).digest()
if tag != T:
raise RuntimeError("HMAC verification failed")
pt = bytes(ci ^ ki for ci, ki in zip(C, ks))
return pt[:orig_len]
def main():
dump_file = "dumped_4053_[23d1000-253e000].bin"
locked_file = "flag.solarsec.locked"
cands = extract_candidate_points_from_dump(dump_file)
Q = choose_valid_pubkey(cands)
print("[*] Qx =", hex(Q[0]))
print("[*] Qy =", hex(Q[1]))
print("[*] computing private key with Pohlig-Hellman ...")
d = pohlig_hellman(G, Q, n, FACTORS)
print("[*] d =", d)
pt = decrypt_locked(locked_file, d)
Path("flag.dec").write_bytes(pt)
m = re.search(rb"flag\{[^}]+\}", pt)
if m:
print("[+] FLAG =", m.group(0).decode())
else:
print("[-] FLAG not found, plaintext below:")
print(pt.decode("utf-8", "replace"))
if __name__ == "__main__":
main()
6. 最终结果
text
flag{bash_history_is_important_in_emergency_response@solarsec_202603}