2025ciscn初赛
依旧坐牢,但还是有收获的
Eternum
go程序结合流量分析来考察
先追踪TCP流,发现所有数据包均以 45 54 33 52 4E 55 4D 58 开头,对应ASCIL是 “ET3RNUMX”,猜测是自定义的C2通信协议, 结构为 Header(8) + Length(4) + EncryptedData
交叉引用发现go程序还是加壳了的,脱壳后在ida打开

函数名去除了符号,用GoReSym工具分析二进制文件,https://github.com/mandiant/GoReSym/releases,恢复部分运行时元数据,并生成 IDAPython 脚本导入 IDA。虽然混淆后的函数名无法完全还原语义,但标准库函数得到了恢复
以下是题目程序恢复符号后的main.main函数伪代码: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
void __fastcall main_main()
{
__int64 v0; // r14
__int64 v1; // rcx
__int64 v2; // rax
_QWORD *v3; // rax
__int64 v4; // rcx
__int64 v5; // rsi
_QWORD *v6; // r11
_QWORD *v7; // rax
_QWORD *v8; // rcx
_QWORD *v9; // r11
__int64 v10; // [rsp-20h] [rbp-80h]
__int64 v11; // [rsp+0h] [rbp-60h]
__int64 v12; // [rsp+8h] [rbp-58h]
_QWORD *v13; // [rsp+10h] [rbp-50h]
__int64 v14; // [rsp+18h] [rbp-48h]
__int64 v15; // [rsp+28h] [rbp-38h]
void *retaddr; // [rsp+60h] [rbp+0h] BYREF
if ( (unsigned __int64)&retaddr <= *(_QWORD *)(v0 + 16) )
goto LABEL_11;
if ( (unsigned __int64)qword_9A8A58 <= 1 )
{
runtime_panicIndex();
LABEL_11:
runtime_morestack_noctxt();
j_main_main();
return;
}
v11 = *(_QWORD *)(qword_9A8A50 + 24);
v14 = *(_QWORD *)(qword_9A8A50 + 16);
v10 = iGw9vplejnCj_FPKxH3Y();
v15 = v1;
v12 = v2;
v3 = (_QWORD *)runtime_newobject();
v3[3] = v11;
if ( dword_9CB880 )
{
v3 = (_QWORD *)runtime_gcWriteBarrier3();
v4 = v14;
*v6 = v14;
v6[1] = &qword_9CB440;
v5 = v15;
v6[2] = v15;
}
else
{
v4 = v14;
v5 = v15;
}
v13 = v3;
v3[2] = v4;
v3[4] = v12;
v3[5] = &qword_9CB440;
v3[6] = v5;
runtime_makechan(v10);
ktyrAE7wjsb_AmuaG1Qm280(2LL);
v7 = (_QWORD *)runtime_newobject();
*v7 = main_main_func1;
if ( dword_9CB880 )
{
v7 = (_QWORD *)runtime_gcWriteBarrier1();
v8 = v13;
*v9 = v13;
}
else
{
v8 = v13;
}
v7[1] = v8;
runtime_newproc();
runtime_chanrecv1();
iupHvc2q4__ptr_H1eV17y_Stop();
}
发现程序是一个 C2 Agent ,为了快速找到加密算法,直接去分析KeepAlive,路径追踪:Agent.Run -> gowrap2 -> KeepAlive -> SendHeartbeat,在 SendHeartbeat 的底层调用链中,定位到封包函数 iupHvc2q4_S1msIZMcWt03。 在iupHvc2q4_S1msIZMcWt03中发现了关键特征:
- 硬编码密钥: 加载了一个 32 字节的字符串 “xfqGcVjrOWp5tUGCPFQq448nPDjILTe7”。
- AES 特征: 密钥长度符合 AES-256。
- 封包逻辑:
○ 序列化 Payload。
○ 加密 Payload (AES-GCM,带 Nonce 和 Tag)。
○ 拼接包头:[Magic (8)] + [Total Length (4)] + [Nonce (12)] + [Ciphertext] + [Tag (16)]。
确定完整协议结构先批量解密1
2
3
4
5
6
7struct Packet {
char Magic[8]; // "ET3RNUMX"
uint32 Length; // Big-Endian, 包含后续所有数据的长度
byte Nonce[12]; // AES-GCM IV
byte CipherText[]; // 具体的密文
byte Tag[16]; // AES-GCM 校验位 (Go标准库通常附加在密文末尾)
};发现是 Protobuf 序列化数据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
91
92
93
94
95import re
import struct
from Crypto.Cipher import AES
# 1. 配置
KEY = b"xfqGcVjrOWp5tUGCPFQq448nPDjILTe7"
INPUT_FILE = "trace.txt" # 把你的 Hex 文本保存到这个文件
def decrypt_payload(ciphertext_with_nonce_tag):
try:
# Go AES-GCM: [Nonce (12)] [Ciphertext] [Tag (16)]
if len(ciphertext_with_nonce_tag) < 28:
return None, "Data too short"
nonce = ciphertext_with_nonce_tag[:12]
tag = ciphertext_with_nonce_tag[-16:]
ciphertext = ciphertext_with_nonce_tag[12:-16]
cipher = AES.new(KEY, AES.MODE_GCM, nonce=nonce)
decrypted = cipher.decrypt_and_verify(ciphertext, tag)
return decrypted, "Success"
except Exception as e:
return None, str(e)
def parse_trace(filename):
with open(filename, 'r', encoding='utf-8') as f:
content = f.read()
# 简单的清洗逻辑,移除行号和偏移量,只保留 Hex
# 这里假设输入格式比较杂乱,我们用正则提取所有 Hex 字节流
# 更好的方法是根据 Magic "45 54 33 52 4E 55 4D 58" 定位
# 将所有文本转换为类似于二进制流的大字符串(忽略非Hex字符)
clean_hex = re.sub(r'[^0-9A-Fa-f]', '', content)
# 转换成 bytes
try:
data = bytes.fromhex(clean_hex)
except:
print("[-] Error parsing hex from file. Check format.")
return
# 遍历寻找 Magic Header
magic = b"\x45\x54\x33\x52\x4E\x55\x4D\x58"
offset = 0
while True:
offset = data.find(magic, offset)
if offset == -1:
break
print(f"\n[+] Found Packet at offset {offset}")
# 读取长度 (Offset + 8, 4 bytes, Big Endian)
if offset + 12 > len(data):
print("[-] Truncated header")
break
payload_len = struct.unpack(">I", data[offset+8 : offset+12])[0]
print(f" Payload Length: {payload_len}")
# 提取完整密文段
# Packet End = Offset + 12 (Header+Len) + payload_len
if offset + 12 + payload_len > len(data):
print("[-] Truncated payload")
# 可能是因为 hex 数据不完整,尝试解密现有的部分
encrypted_data = data[offset+12:]
else:
encrypted_data = data[offset+12 : offset+12+payload_len]
# 解密
decrypted, status = decrypt_payload(encrypted_data)
if decrypted:
print(" [Decrypted HEX]: " + decrypted.hex()[:60] + "...")
try:
# 尝试打印 ASCII,过滤不可见字符
ascii_text = ''.join([chr(b) if 32 <= b <= 126 else '.' for b in decrypted])
print(f" [Decrypted ASCII]: {ascii_text}")
# *** 自动寻找 Flag ***
if "flag" in ascii_text.lower() or "gwht" in ascii_text.lower():
print("\n 🔥 CRITICAL: POTENTIAL FLAG FOUND! 🔥")
print(" " + ascii_text)
print("\n")
except:
pass
else:
print(f" [-] Decryption Failed: {status}")
offset += 1 # 继续搜下一个
if __name__ == "__main__":
# 如果你没有文件,可以直接把那一大段 hex 粘贴到这里作为 string 测试
# parse_trace("trace.txt")
print("Please save your hex data to trace.txt and run.")
● Field 1: OpCode (4 = Heartbeat)
● Field 2: Payload Data (嵌套结构)
● Field 5: TraceID
最后进行流量行为审计,攻击者执行了命令 base32 /var/opt/s*,在响应包中发现明显base32字符串’IMZWGCZ33MI3WGNJYG4YDALJSMIYDCLJUMRSDILJYGUZDMLLBGRQTIN3BGY2WCMLBHF6QU===’
解密发现不对,根据 Protobuf 结构分析,字符串开头的 ‘I’ (0x49) 实际上是 Protobuf 嵌套字段的一部分或数据流残留。去掉I后解密

babygame
玩了下游戏发现过不了,用https://github.com/GDRETools/gdsdecomp 来解包
解包以后定位到flag.gdc

不难发现是aes加密,可以找到key和加密后的密文,同时analyze_flag.py 将A替换成B,直接用cyberchef解密就好了

wasm-login
wasm逆向,用wabt工具来反编译拿到.wat
html源码
题目已经提示了时间
某人本想在2025年12月第三个周末爆肝一个web安全登录demo,结果不仅搞到周一凌晨,他自己还忘了成功登录时的时间戳了,你能帮他找回来吗?
Source Map 利⽤:目录下暴露了 release.wasm.map ,可以直接基于时间戳来爆破
解题思路主要是直接利用 release.js 导出的函数,并通过 Hook Date.now 来欺骗 WASM1
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
59import { authenticate } from './build/release.js';
import crypto from 'crypto';
// 目标前缀
const TARGET_PREFIX = "ccaf33e3512e31f3";
// 搜索范围:2025-12-22 00:00:00 (UTC+8) 附近
// 起始:1766332800000
// 结束:1766343600000
const START_MS = 1766332800000;
const END_MS = 1766343600000;
console.log(`[+] 开始极速爆破...`);
console.log(`[+] 范围: ${START_MS} -> ${END_MS} (共 ${(END_MS - START_MS)} 毫秒)`);
const startTime = performance.now();
for (let ms = START_MS; ms <= END_MS; ms++) {
// --- 核心 Hook 逻辑 ---
// 强制修改 Date.now,使得 release.js 内部调用 Date.now() 时获取到我们伪造的时间
Date.now = () => ms;
try {
// 直接调用 WASM 导出的函数
const authResult = authenticate("admin", "admin");
// 严格按照题目逻辑:JSON.parse -> JSON.stringify -> MD5
// (模拟前端的数据处理流程)
const authData = JSON.parse(authResult);
const jsonStr = JSON.stringify(authData);
const check = crypto.createHash('md5')
.update(jsonStr)
.digest('hex');
if (check.startsWith(TARGET_PREFIX)) {
console.log(`\n========== BINGO! ==========`);
console.log(`Timestamp : ${ms}`);
console.log(`Time (ISO): ${new Date(ms).toISOString()}`);
console.log(`Check Val : ${check}`);
console.log(`\nFlag: flag{${check}}`);
console.log(`============================`);
break; // 找到后退出
}
} catch (e) {
// 忽略 JSON 解析错误或其他 WASM 内部错误
continue;
}
// 每 5000 次打印一下进度(因为速度很快,可以适当减少打印频率)
if ((ms - START_MS) % 5000 === 0) {
process.stdout.write(`\rProgress: ${ms} ...`);
}
}
const endTime = performance.now();
console.log(`\n[+] 耗时: ${((endTime - startTime) / 1000).toFixed(2)} 秒`);
直接node solve.mjs
ECDSA
通过分析发现,题目中的ECDSA签名使用了可预测的nonce(k值),这使得私钥可以被恢复。关键问题在于ecdsa库默认使用SHA1作为hash函数,而不是SHA512。通过使用正确的hash函数(SHA1)和已知的nonce生成方式,成功恢复了私钥,并计算出其MD5值。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
77from ecdsa import VerifyingKey, NIST521p
from hashlib import sha512, sha1
from Crypto.Util.number import long_to_bytes, bytes_to_long
import binascii
import hashlib
# 从public.pem读取公钥
with open("public.pem", "rb") as f:
vk_pem = f.read()
vk = VerifyingKey.from_pem(vk_pem)
curve_order = NIST521p.order
# 读取签名文件
signatures = []
with open("signatures.txt", "r") as f:
for line in f:
if line.strip():
msg_hex, sig_hex = line.strip().split(":")
msg = binascii.unhexlify(msg_hex)
sig = binascii.unhexlify(sig_hex)
signatures.append((msg, sig))
# nonce函数(与task.py中相同)
def nonce(i):
seed = sha512(b"bias" + bytes([i])).digest()
k = int.from_bytes(seed, "big")
return k
# 选择第一个签名进行攻击
msg, sig = signatures[0]
msg_index = 0 # 因为msg是"message-\x00"
# 计算nonce k
k = nonce(msg_index)
# 解析ECDSA签名 (r, s)
# NIST521p的签名长度是132字节 (66字节r + 66字节s)
sig_len = len(sig)
half_len = sig_len // 2
r = bytes_to_long(sig[:half_len])
s = bytes_to_long(sig[half_len:])
# 计算消息的hash (ecdsa库默认使用SHA1)
msg_hash = bytes_to_long(sha1(msg).digest()) % curve_order
# 验证r和s是否有效
assert 1 <= r < curve_order
assert 1 <= s < curve_order
# 计算私钥 d = (s * k - hash(m)) * r^(-1) mod n
r_inv = pow(r, -1, curve_order)
d = ((s * k - msg_hash) * r_inv) % curve_order
print(f"Recovered private key: {d}")
print(f"Private key hex: {hex(d)}")
# 验证私钥是否正确
from ecdsa import SigningKey
# 使用与task.py相同的方式创建私钥
d_bytes_for_sk = long_to_bytes(d, 66)
sk_recovered = SigningKey.from_string(d_bytes_for_sk, curve=NIST521p)
vk_recovered = sk_recovered.verifying_key
# 验证公钥是否匹配
if vk.to_string() == vk_recovered.to_string():
print("Private key is correct!")
# 计算MD5 - 注意:MD5应该基于原始的私钥整数,而不是填充后的字节
# 但根据task.py,私钥是priv_int,所以我们应该对priv_int取MD5
original_d_bytes = long_to_bytes(d) # 不带长度参数,只包含必要的字节
md5_hash = hashlib.md5(original_d_bytes).hexdigest()
print(f"Flag: flag{{{md5_hash}}}")
else:
print("Private key recovery failed!")
print(f"Expected vk: {binascii.hexlify(vk.to_string()).decode()}")
print(f"Recovered vk: {binascii.hexlify(vk_recovered.to_string()).decode()}")
还原得到的key是11786190273906782566706300546504742629011900435269701041731697414027484824601255112180676531145294320443777235338538357924760601782873554458995940394745073
得到flag
EzFlag
逆向题目和密码的结合
main函数,比较输入内容与硬编码字符串 “V3ryStr0ngp@ssw0rd”,用’’-‘’隔断flag
f函数逻辑
斐波那契数列模 n 具有周期性(皮萨诺周期,Pisano Period)。 对于 n=16 (24),其周期是 24。 即:Fib(x)(mod16)==Fib(x(mod24))(mod16),并且k的值也知道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
39def solve():
# 1. 题目给出的映射表 K
k = "012ab9c3478d56ef"
# 2. 预计算模 16 的斐波那契序列(周期为 24)
# F[0]=0, F[1]=1, F[n] = (F[n-1] + F[n-2]) % 16
fib_mod = [0, 1]
for i in range(2, 24):
fib_mod.append((fib_mod[-1] + fib_mod[-2]) & 0xF)
def get_char(v11_val):
# 利用周期性:F[v11] % 16 == F[v11 % 24] % 16
idx = fib_mod[v11_val % 24]
return k[idx]
v11 = 1
flag_content = ""
print("正在计算 Flag...")
# 3. 循环 32 次
for i in range(32):
# 先取字符
char = get_char(v11)
flag_content += char
# 格式化:在特定的索引 i 之后添加横杠
# i=7 (第8个字符), i=12 (第13个字符)...
if i in [7, 12, 17, 22]:
flag_content += "-"
# 更新 v11
# 关键点:模拟 C++ unsigned long long (64位) 溢出行为
v11 = (v11 * 8 + i + 64) & 0xFFFFFFFFFFFFFFFF
print(f"最终 Flag: flag{{{flag_content}}}")
if __name__ == "__main__":
solve()
SnakeBackdoor-2
题目内容:
攻击者通过漏洞利用获取Flask应用的 SECRET_KEY 是什么,结果提交形式:flag{xxxxxxxxxx}
攻击者在利用那个复杂的 RCE(远程代码执行)Payload 之前,通常会先通过简单的 SSTI Payload 来探测环境和读取敏感配置,而 Flask 应用的 SECRET_KEY 就存储在全局对象 config 中。
在过滤器中搜索:http.request.method == “POST” && http.request.uri contains “/admin/preview”
有三个包,一个个地看, 这个数据包完美记录了攻击者通过 Payload 窃取 Flask 配置信息的过程
定位关键信息 在响应包的 div 标签中,可以看到如下内容(HTML 实体编码格式):
'SECRET_KEY': 'c6242af0-6891-4510-8432-e1cdf051f160'
解码 将 HTML 实体 ' 还原为单引号 ‘,内容即为: ‘SECRET_KEY’: ‘c6242af0-6891-4510-8432-e1cdf051f160’
得到flag:flag{c6242af0-6891-4510-8432-e1cdf051f160}
— END —