2026春节题目

admin 2026-03-31 11:34:02 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文为2026春节逆向题目解题报告,涵盖Windows和Android多道题目。作者通过IDA、jadx等工具逆向分析程序,识别XOR、AES等加密算法,提取加密数据并编写解密脚本成功获取各题Flag。文章详细记录了脱壳、资源提取、密钥派生等步骤,展示了实战经验。主要结论是所有题目均已解决,关键发现涉及多种打包和加密技术。建议掌握逆向工具与加密算法分析技巧。 综合评分: 88 文章分类: CTF,逆向分析,实战经验,安全工具,移动安全


 设置为密码输入框的默认文本,是一个提示而非密码本身。


加密算法逆向

文件格式

flag.png.encrypted 的格式如下:

 复制代码 隐藏代码
偏移    大小    内容
0x00    4       魔数 "CM26" (0x36324D43)
0x04    4       明文 CRC-32 校验值 (~CRC32)
0x08    8       IV 初始化向量
0x10    N       密文数据(8 字节对齐,含填充)

实际数据:

 复制代码 隐藏代码
00000000:434d3236 a245848df5697360 01cb35bc  CM26.E...is`..5.
00000010:fbdd1b922befe32310eb814c4504 4895  ....+..#...LE.H.
...

密钥派生

函数 0x140008720 实现完整的解密流程:

① CRC-64 初始化 (0x140008640)

使用多项式 0xC96C5795D7870F42(CRC-64/ECMA-182)构建 256 项查找表。初始值 0xFFFFFFFFFFFFFFFF

② CRC-64 更新 (0x140008500)

依次处理 14 字节盐值 "52pojie_2026_\x00" 和用户输入的密码:

 复制代码 隐藏代码
crc64_update(ctx, "52pojie_2026_\x00", 14);  // 盐值
crc64_update(ctx, password, strlen(password)); // 密码

③ CRC-64 终结 (0x140008580)

对 CRC 值追加 4 字节计数器并取反:

 复制代码 隐藏代码
crc = crc64_update(crc, counter_bytes, 4);
hash = ~crc;

最终得到 64 位密钥哈希。

分组加密

块密码 (0x140008080),8 字节分组,CBC 模式:

密钥变换(每个分组前执行):

 复制代码 隐藏代码
key = ROL(key, 3);                  // 循环左移 3 位
for&nbsp;(int&nbsp;i =&nbsp;0; i <&nbsp;8; i++) {
&nbsp; &nbsp; high_byte = key >>&nbsp;56;
&nbsp; &nbsp; key = (key <<&nbsp;8) | AES_SBOX[high_byte]; &nbsp;// S-Box 字节替换
}

解密公式

&nbsp;复制代码&nbsp;隐藏代码
明文[i] = 密文[i] ⊕ 变换后密钥[i] ⊕ IV[i]

IV 更新(CBC 模式):

&nbsp;复制代码&nbsp;隐藏代码
IV&nbsp;= 当前密文块 &nbsp; &nbsp;// 用于下一块解密

完整性校验

CRC-32 验证 (0x140008480 + 0x1400082E0),多项式 0xEDB88320

  • 解密过程中对所有明文计算 CRC-32
  • 终结时比对 ~CRC32 与文件头偏移 0x04 处存储的值
  • 匹配则密码正确

已知明文攻击

题目提示”暴力枚举不可取”,引导我们使用已知明文攻击

原理

PNG 文件固定以 8 字节魔数开头:

&nbsp;复制代码&nbsp;隐藏代码
8950&nbsp;4E&nbsp;470D&nbsp;0A 1A 0A

这恰好等于加密的分组大小(8 字节)。由解密公式:

&nbsp;复制代码&nbsp;隐藏代码
明文 = 密文 ⊕ 密钥 ⊕ IV

可以反推:

&nbsp;复制代码&nbsp;隐藏代码
密钥 = 密文 ⊕ IV ⊕ 明文

计算

从加密文件中提取:

&nbsp;复制代码&nbsp;隐藏代码
IV(偏移&nbsp;0x08): &nbsp; &nbsp; &nbsp;F5&nbsp;69736001&nbsp;CB&nbsp;35&nbsp;BC
第一密文块(偏移&nbsp;0x10): FB&nbsp;DD1B92&nbsp;2B EF E3&nbsp;23
PNG 文件头(已知明文): &nbsp;8950&nbsp;4E&nbsp;470D&nbsp;0A 1A 0A

逐字节异或:

| 字节 | 密文 | IV | 明文 | 密钥 = CT⊕IV⊕PT | | — | — | — | — | — | | 0 | FB | F5 | 89 | 87 | | 1 | DD | 69 | 50 | E4 | | 2 | 1B | 73 | 4E | 26 | | 3 | 92 | 60 | 47 | B5 | | 4 | 2B | 01 | 0D | 27 | | 5 | EF | CB | 0A | 2E | | 6 | E3 | 35 | 1A | CC | | 7 | 23 | BC | 0A | 95 |

得到变换后密钥(LE 字节序):87 E4 26 B5 27 2E CC 95

对应 64 位整数:0x95CC2E27B526E487

逆推原始 CRC-64 哈希

① 逆 AES S-Box

变换后密钥的每个字节都经过了 S-Box 替换。对每个字节查 AES 逆 S-Box 表,还原 ROL(hash, 3) 的值:

&nbsp;复制代码&nbsp;隐藏代码
inv_sbox = [0] *&nbsp;256
for&nbsp;i&nbsp;inrange(256):
&nbsp; &nbsp; inv_sbox[AES_SBOX[i]] = i

original_bytes =&nbsp;bytes([inv_sbox[b]&nbsp;for&nbsp;b&nbsp;in&nbsp;tk_bytes])
# rol3_value = 0xAD27C33DD223AEEA

② 逆循环左移

&nbsp;复制代码&nbsp;隐藏代码
crc64_hash = ROR(rol3_value,&nbsp;3)
# crc64_hash = 0x55A4F867BA4475DD

完整解密

利用恢复的密钥,无需知道密码即可解密全部数据:

&nbsp;复制代码&nbsp;隐藏代码
import&nbsp;struct

key =&nbsp;0x55A4F867BA4475DD&nbsp; &nbsp; &nbsp;# 恢复的 CRC-64 哈希
current_iv = enc[8:16] &nbsp; &nbsp; &nbsp; &nbsp;# 8 字节 IV
ciphertext = enc[16:] &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# 密文数据
plaintext =&nbsp;bytearray()

for&nbsp;block_start&nbsp;inrange(0,&nbsp;len(ciphertext),&nbsp;8):
&nbsp; &nbsp; block = ciphertext[block_start:block_start+8]
&nbsp; &nbsp; saved_ct =&nbsp;list(block)

&nbsp; &nbsp;&nbsp;# 密钥变换:ROL 3 + S-Box×8
&nbsp; &nbsp; key = ((key <<&nbsp;3) | (key >>&nbsp;61)) &&nbsp;0xFFFFFFFFFFFFFFFF
&nbsp; &nbsp;&nbsp;for&nbsp;_&nbsp;inrange(8):
&nbsp; &nbsp; &nbsp; &nbsp; high = (key >>&nbsp;56) &&nbsp;0xFF
&nbsp; &nbsp; &nbsp; &nbsp; key = ((key <<&nbsp;8) &&nbsp;0xFFFFFFFFFFFFFFFF) | AES_SBOX[high]

&nbsp; &nbsp; key_bytes = struct.pack('<Q', key)

&nbsp; &nbsp;&nbsp;# 解密:明文 = 密文 ⊕ 密钥 ⊕ IV
&nbsp; &nbsp;&nbsp;for&nbsp;i&nbsp;inrange(8):
&nbsp; &nbsp; &nbsp; &nbsp; plaintext.append(block[i] ^ key_bytes[i] ^ current_iv[i])

&nbsp; &nbsp; current_iv =&nbsp;bytes(saved_ct) &nbsp;# CBC: IV 更新为当前密文块

# 移除 PKCS 填充
pad = plaintext[-1]
plaintext = plaintext[:-pad]

运行结果:

&nbsp;复制代码&nbsp;隐藏代码
首 8 字节: 89504e470d0a1a0a &nbsp;← 合法 PNG 文件头!
填充字节: 2(移除 2 字节填充)
解密数据: 350 字节

成功解密出 flag.png,图像为像素字体 “HEX_ME”(产品标志)。


提取 Flag

解密后检查 PNG 的元数据,发现 tEXt 块中隐藏了 flag:

&nbsp;复制代码&nbsp;隐藏代码
Chunk: tEXt (Software)
&nbsp; → Pixilart (Pixel Art Editor)

Chunk: tEXt (Comment)
&nbsp; → Post-processed with a hex editor &nbsp; &nbsp;← 提示 flag 是用 hex 编辑器写入的

Chunk: tEXt (Description)
&nbsp; → flag{EncrypTIoN_Is_haRd_52p0jIE_2o26_m62Tc4uj78maAq1C} &nbsp; &nbsp;← FLAG!

Comment 字段的 “Post-processed with a hex editor” 与题目名 “HEX_ME” 相呼应,暗示 flag 是通过十六进制编辑器写入 PNG 元数据的。


Flag

&nbsp;复制代码&nbsp;隐藏代码
flag{EncrypTIoN_Is_haRd_52p0jIE_2o26_m62Tc4uj78maAq1C}

Android 中级题(二)

分析过后发现是在so层,分析困难度较大,放弃了。

Web 中级题

先打开玩了一下,发现验证码巨长,我又听不太懂说的啥,感觉是个大坑,换一条路。

WASM 逆向分析

工具链

使用 wabt 工具链进行 WASM 反编译:

&nbsp;复制代码&nbsp;隐藏代码
# 提取 WASM 二进制
node -e&nbsp;"eval(require('fs').readFileSync('assets/verify.wasm.js','utf8')); \
&nbsp; require('fs').writeFileSync('verify.wasm', Buffer.from(getWasmBuffer()))"

# 反编译为 WAT 文本格式(23,344 行)
wasm2wat verify.wasm -o verify.wat

# 反编译为 C 代码(363,903 行)
wasm2c verify.wasm -o verify.c

gen 函数对应 w2c_verify_gen_0(verify.c 第 357396 行),是整个题目的核心。

gen() 函数内部流程

第一步:获取 17 字节随机数
&nbsp;复制代码&nbsp;隐藏代码
// verify.c:357436-357439
var_i0 = var_l3 +&nbsp;80; &nbsp;&nbsp;// 缓冲区地址
var_i1 =&nbsp;17; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// 长度
w2c_wbg_getRandomValues(instance, var_i0, var_i1);
// random[0..16] 存储在 var_l3+80 到 var_l3+96

通过 hook 确认:getRandomValues 在整个 gen() 过程中仅被调用一次,请求恰好 17 字节。

第二步:构建 37 字节种子缓冲区
&nbsp;复制代码&nbsp;隐藏代码
// verify.c:357778-357836
var_l9 =&nbsp;malloc(37,&nbsp;1); &nbsp;// 分配 37 字节

// bytes 0-3: random[0..3] XOR uid 各字节(字节序反转)
var_l9[0] = random[0] ^ (uid &&nbsp;0xFF); &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// XOR uid 低 8 位
var_l9[1] = random[1] ^ ((uid >>&nbsp;8) &&nbsp;0xFF); &nbsp;// XOR uid 次低 8 位
var_l9[2] = random[2] ^ ((uid >>&nbsp;16) &&nbsp;0xFF);&nbsp;// XOR uid 次高 8 位
var_l9[3] = random[3] ^ ((uid >>&nbsp;24) &&nbsp;0xFF);&nbsp;// XOR uid 高 8 位

// bytes 4-11: random[0..7] 直接复制
memcpy(var_l9 +&nbsp;4, random,&nbsp;8);

// bytes 12-19: random[8..15] 直接复制
memcpy(var_l9 +&nbsp;12, random +&nbsp;8,&nbsp;8);

// byte 20: random[16] 直接复制
var_l9[20] = random[16];

至此前 21 字节已填充完毕。

第三步:HMAC-SHA256 计算填充 bytes 21-36
&nbsp;复制代码&nbsp;隐藏代码
// verify.c:357849-358037 (简化)

// 初始化 SHA-256 状态(HMAC 内层)
// 从地址 1295967 加载 14 字节初始值
memcpy(buffer, mem+1295967,&nbsp;14);

// 将前 64 字节数据与 0x36 XOR(HMAC ipad)
for&nbsp;(int&nbsp;i =&nbsp;0; i <&nbsp;64; i +=&nbsp;4) {
&nbsp; &nbsp; buffer[i] &nbsp; ^=&nbsp;0x36;
&nbsp; &nbsp; buffer[i+1] ^=&nbsp;0x36;
&nbsp; &nbsp; buffer[i+2] ^=&nbsp;0x36;
&nbsp; &nbsp; buffer[i+3] ^=&nbsp;0x36;
}

// SHA-256 压缩(内层 hash = SHA256(ipad || seed_buffer))
w2c_verify_f9(instance, sha_state, buffer,&nbsp;1);

// 将同一 buffer 再与 0x6A XOR(0x36 ^ 0x6A = 0x5C = HMAC opad)
for&nbsp;(int&nbsp;i =&nbsp;0; i <&nbsp;64; i +=&nbsp;4) {
&nbsp; &nbsp; buffer[i] &nbsp; ^=&nbsp;0x6A; &nbsp;// 0x36 ^ 0x6A = 0x5C
&nbsp; &nbsp; buffer[i+1] ^=&nbsp;0x6A;
&nbsp; &nbsp; buffer[i+2] ^=&nbsp;0x6A;
&nbsp; &nbsp; buffer[i+3] ^=&nbsp;0x6A;
}

// SHA-256 压缩(外层 hash = SHA256(opad || inner_hash))
w2c_verify_f9(instance, sha_state2, buffer,&nbsp;1);

这实际上是一个 HMAC-SHA256 计算。最终取前 16 字节写入 var_l9[21..36],完成 37 字节种子缓冲区。

第四步:Base64-like 编码(37 字节 → 50 字符)
&nbsp;复制代码&nbsp;隐藏代码
// verify.c:358799-358933 (简化伪代码)

char&nbsp;*code_array =&nbsp;malloc(200,&nbsp;4); &nbsp;// 50 个 u32 元素
int&nbsp;char_idx =&nbsp;0;
int&nbsp;bit_acc =&nbsp;0; &nbsp; &nbsp;&nbsp;// 位累加器
int&nbsp;bit_pos =&nbsp;0; &nbsp; &nbsp;&nbsp;// 当前位位置
byte *ptr = seed; &nbsp; &nbsp;// 指向 37 字节种子

for&nbsp;(int&nbsp;byte_idx =&nbsp;0; byte_idx <&nbsp;37; byte_idx++) {
&nbsp; &nbsp;&nbsp;// 将当前字节加入位累加器
&nbsp; &nbsp; bit_acc = *ptr | (bit_acc <<&nbsp;8);

&nbsp; &nbsp;&nbsp;// 循环提取 6-bit 块
&nbsp; &nbsp;&nbsp;while&nbsp;(bit_pos +&nbsp;2&nbsp;>&nbsp;5) {
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;int&nbsp;idx = (bit_acc >> (bit_pos +&nbsp;2)) &&nbsp;0x3F;
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;char&nbsp;ch = charset[idx]; &nbsp;&nbsp;// charset 在内存地址 1295903
&nbsp; &nbsp; &nbsp; &nbsp; code_array[char_idx++] = ch;
&nbsp; &nbsp; &nbsp; &nbsp; bit_pos -=&nbsp;6;
&nbsp; &nbsp; }
&nbsp; &nbsp; bit_pos +=&nbsp;8;
&nbsp; &nbsp; ptr++;
}

// 处理最后剩余的 bits
if&nbsp;(remaining_bits >&nbsp;0) {
&nbsp; &nbsp;&nbsp;int&nbsp;idx = (last_byte << (6&nbsp;- remaining_bits)) &&nbsp;0x3F;
&nbsp; &nbsp; code_array[char_idx++] = charset[idx];
}

字符表位于 WASM 线性内存地址 1295903

&nbsp;复制代码&nbsp;隐藏代码
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789?!

编码过程类似 Base64:每 6 bit 索引一个字符,37 字节 = 296 bit → 296/6 = 49.33 → 上取整得到 50 个字符

第五步:计算验证哈希

WASM 内部对编码后的 50 字符验证码做 8230 次 SHA-256 迭代,生成 64 位 hex 字符串作为 hash h

第六步:生成 TTS 语音

WASM 内部包含一个 TTS 引擎,将 50 个字符逐个合成为中文语音朗读(根据 voice 参数选择方言)。生成 24kHz 16bit 单声道 WAV 音频,约 38 秒。

语音朗读方式:

  • 普通话(c):区分大小写,如”大写A”、”小写b”、”数字3″、”问号”、”叹号”
  • 粤语(y):粤语发音
  • 中文美式(e):美式英语发音
第七步:清零所有中间数据

最关键的保护措施:在构建返回对象之前,WASM 将以下数据全部清零/释放:

  • 37 字节种子缓冲区
  • 50 元素字符数组
  • 所有 SHA-256 中间状态
  • HMAC 计算缓冲区

这意味着验证码明文从未通过任何 JS 导入函数传出——它完全在 WASM 内部生成、编码、哈希、清零,最终只有 hash 和音频数据通过 JS 导入的 set 函数设置到返回对象上。


既然数据在 WASM 内部被生成后立即销毁,那就修改 WASM 字节码,在数据被销毁之前将其”泄漏”到 JS 层。

WASM 二进制热补丁 + 双通道执行

补丁 1:消除 XOR 混淆

WASM 内部在将字符传递给后续处理前,会对每个字符的 ASCII 值做 XOR 0xCC。这会将正常 ASCII 字符变成高位字节,无法作为可识别字符传出。

在 WASM 字节码中搜索所有 i32.const 204; i32.xor 指令序列并替换为 i32.const 0; i32.xor(等价于 no-op):

&nbsp;复制代码&nbsp;隐藏代码
原始字节码:41&nbsp;CC&nbsp;0173&nbsp; &nbsp; → &nbsp;i32.const204&nbsp;(0xCC);&nbsp;i32.xor
补丁字节码:41800073&nbsp; &nbsp; → &nbsp;i32.const0; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;i32.xor
&nbsp;复制代码&nbsp;隐藏代码
const&nbsp;xorPattern =&nbsp;Buffer.from([0x41,&nbsp;0xCC,&nbsp;0x01,&nbsp;0x73]);
const&nbsp;xorReplace =&nbsp;Buffer.from([0x41,&nbsp;0x80,&nbsp;0x00,&nbsp;0x73]);
let&nbsp;p =&nbsp;0;
while&nbsp;(p < patchedWasmBuffer.length&nbsp;-&nbsp;4) {
&nbsp; &nbsp;&nbsp;const&nbsp;idx = patchedWasmBuffer.indexOf(xorPattern, p);
&nbsp; &nbsp;&nbsp;if&nbsp;(idx === -1)&nbsp;break;
&nbsp; &nbsp; xorReplace.copy(patchedWasmBuffer, idx);
&nbsp; &nbsp; p = idx +&nbsp;1;
}

补丁 2:重定向函数调用

在 WASM 内部,每个编码后的字符会通过 call 19(一个内部字符串构建函数 w2c_verify_f19)进行处理。我们将偏移量 33810 处的 call 19 重定向为 call 4——而 call 4 恰好是 getRandomValues 的导入函数。

这样,每个字符的 ASCII 值会作为 len 参数传递到我们 hook 的 getRandomValues 回调中:

&nbsp;复制代码&nbsp;隐藏代码
WASM 偏移&nbsp;33810:
原始字节码:1013&nbsp; &nbsp; → &nbsp;call19&nbsp;(内部函数)
补丁字节码:1004&nbsp; &nbsp; → &nbsp;call4&nbsp; (getRandomValues 导入)
&nbsp;复制代码&nbsp;隐藏代码
patchedWasmBuffer[33810] =&nbsp;0x04;

JS 层 hook:捕获字符

Hook getRandomValues 导入函数。第一次调用是真正的随机数请求(17 字节),后续调用是我们补丁注入的字符泄漏——len 参数就是字符的 ASCII 值:

&nbsp;复制代码&nbsp;隐藏代码
imports.wbg.__wbg_getRandomValues&nbsp;=&nbsp;function(arg0, arg1) {
&nbsp; &nbsp; randomCallCount++;
&nbsp; &nbsp;&nbsp;if&nbsp;(randomCallCount ===&nbsp;1) {
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// 第一次:真正的 getRandomValues,注入固定随机数
&nbsp; &nbsp; &nbsp; &nbsp; arr.set(fixedRandom);
&nbsp; &nbsp; }&nbsp;else&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// 后续:补丁注入的字符泄漏
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// arg1 (len) = 字符的 ASCII 值
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;(len >=&nbsp;33&nbsp;&& len <=&nbsp;122&nbsp;&&&nbsp;CHARSET.includes(String.fromCharCode(len))) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; capturedChars.push(String.fromCharCode(len));
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; }
};

双通道执行策略

由于补丁修改了 XOR 常量,打补丁的 WASM 生成的 hash 与原始 WASM 不同。因此需要两次执行:

&nbsp;复制代码&nbsp;隐藏代码
Pass&nbsp;1(打补丁的 WASM)
&nbsp; ├── 注入固定&nbsp;17&nbsp;字节随机数
&nbsp; ├── XOR 被置零 → 字符保持明文 ASCII
&nbsp; ├──&nbsp;call19&nbsp;→&nbsp;call4&nbsp;→ 每个字符通过 getRandomValues 泄漏
&nbsp; └── 输出:捕获&nbsp;50&nbsp;个明文字符

Pass&nbsp;2(原始未修改的 WASM)
&nbsp; ├── 注入相同的&nbsp;17&nbsp;字节随机数
&nbsp; ├── 正常执行所有逻辑
&nbsp; └── 输出:正确的 SHA-256&nbsp;hash

验证:SHA-256(捕获的 code,&nbsp;8230&nbsp;次)&nbsp;==&nbsp;Pass&nbsp;2&nbsp;的 hash
&nbsp; └──&nbsp;Match:&nbsp;true&nbsp;✓

关键在于:相同的随机数 → 相同的验证码。voice 参数只影响语音合成,不影响验证码内容和 hash。

最终验证

&nbsp;复制代码&nbsp;隐藏代码
let&nbsp;current =&nbsp;Buffer.from(code,&nbsp;'utf-8');
for&nbsp;(let&nbsp;i =&nbsp;0; i <&nbsp;0x2026; i++) {
&nbsp; &nbsp; current = crypto.createHash('sha256').update(current).digest();
}
const&nbsp;computedHash = current.toString('hex');
console.log(`Match:&nbsp;${computedHash === originalHash}`);
// → Match: true

脚本

&nbsp;复制代码&nbsp;隐藏代码
const&nbsp;fs =&nbsp;require('fs');
const&nbsp;path =&nbsp;require('path');
const&nbsp;crypto =&nbsp;require('crypto');

constCHARSET&nbsp;=&nbsp;"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789?!";

// 加载 WASM 二进制
globalThis.window&nbsp;= globalThis;
globalThis.atob&nbsp;=&nbsp;(b64) =>Buffer.from(b64,&nbsp;'base64').toString('binary');
eval(fs.readFileSync('assets/verify.wasm.js',&nbsp;'utf-8'));
const&nbsp;originalWasmBuffer =&nbsp;Buffer.from(globalThis.getWasmBuffer());

// 生成固定随机数(确保两次执行使用相同的种子)
const&nbsp;fixedRandom = crypto.randomBytes(17);

// ====== 创建打补丁的 WASM ======
const&nbsp;patchedWasmBuffer =&nbsp;Buffer.from(originalWasmBuffer);

// 补丁 1: XOR 0xCC → XOR 0x00
const&nbsp;xorPattern =&nbsp;Buffer.from([0x41,&nbsp;0xCC,&nbsp;0x01,&nbsp;0x73]);
const&nbsp;xorReplace =&nbsp;Buffer.from([0x41,&nbsp;0x80,&nbsp;0x00,&nbsp;0x73]);
let&nbsp;p =&nbsp;0;
while&nbsp;(p < patchedWasmBuffer.length&nbsp;-&nbsp;4) {
&nbsp; &nbsp;&nbsp;const&nbsp;idx = patchedWasmBuffer.indexOf(xorPattern, p);
&nbsp; &nbsp;&nbsp;if&nbsp;(idx === -1)&nbsp;break;
&nbsp; &nbsp; xorReplace.copy(patchedWasmBuffer, idx);
&nbsp; &nbsp; p = idx +&nbsp;1;
}

// 补丁 2: call 19 → call 4 (偏移 33810)
patchedWasmBuffer[33810] =&nbsp;0x04;

// ====== 通用 WASM 执行函数 ======
functionrunWasm(wasmBuf, onGetRandom, onSetProp) {
&nbsp; &nbsp;&nbsp;// 构建所有 wasm-bindgen 所需的导入函数
&nbsp; &nbsp;&nbsp;// 其中 getRandomValues 和 set 被 hook
&nbsp; &nbsp;&nbsp;const&nbsp;imports = {&nbsp;wbg: {&nbsp;/* ... 所有导入 ... */&nbsp;} };

&nbsp; &nbsp;&nbsp;constmodule&nbsp;=&nbsp;newWebAssembly.Module(wasmBuf);
&nbsp; &nbsp;&nbsp;const&nbsp;instance =&nbsp;newWebAssembly.Instance(module, imports);
&nbsp; &nbsp; instance.exports.__wbindgen_start();

&nbsp; &nbsp;&nbsp;// 调用 gen(734555, "c")
&nbsp; &nbsp;&nbsp;const&nbsp;ret = instance.exports.gen(734555, voicePtr, voiceLen);
&nbsp; &nbsp;&nbsp;return&nbsp;result;
}

// ====== Pass 1: 打补丁版本捕获字符 ======
const&nbsp;capturedChars = [];
runWasm(patchedWasmBuffer,&nbsp;(arr, ptr, len) =>&nbsp;{
&nbsp; &nbsp;&nbsp;if&nbsp;(firstCall) {
&nbsp; &nbsp; &nbsp; &nbsp; arr.set(fixedRandom); &nbsp;// 注入固定随机数
&nbsp; &nbsp; }&nbsp;else&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp; capturedChars.push(String.fromCharCode(len)); &nbsp;// 捕获字符
&nbsp; &nbsp; }
}, ...);

// ====== Pass 2: 原始版本获取正确 hash ======
let&nbsp;originalHash =&nbsp;null;
runWasm(originalWasmBuffer,&nbsp;(arr) =>&nbsp;{
&nbsp; &nbsp; arr.set(fixedRandom); &nbsp;// 注入相同的随机数
},&nbsp;(obj, key, val) =>&nbsp;{
&nbsp; &nbsp;&nbsp;if&nbsp;(key ===&nbsp;'h') originalHash = val;
});

// ====== 验证 ======
const&nbsp;code = capturedChars.join(''); &nbsp;// 50 字符
let&nbsp;current =&nbsp;Buffer.from(code,&nbsp;'utf-8');
for&nbsp;(let&nbsp;i =&nbsp;0; i <&nbsp;0x2026; i++) {
&nbsp; &nbsp; current = crypto.createHash('sha256').update(current).digest();
}
console.log(`Match:&nbsp;${current.toString('hex') === originalHash}`); &nbsp;// true
console.log(`FLAG: flag{${code}}`);

运行结果

&nbsp;复制代码&nbsp;隐藏代码
$ node&nbsp;extract_code.js

&nbsp;WASM&nbsp;buffer&nbsp;loaded:&nbsp;4001674bytes
&nbsp;Fixed&nbsp;randombytes:&nbsp;23a43e72bb29cfb8fa3130be2e48a8d298

=== STEP&nbsp;1: Run patched WASM to capture characters ===
&nbsp; [PATCHED] getRandomValues: wrote fixed&nbsp;random23a43e72bb29cfb8fa3130be2e48a8d298
&nbsp; [PATCHED] Hash:&nbsp;80e2e0c9d0238767c8b9b8ba8f9338003977e6bd8be1c1360bb6d89392e745f6
&nbsp; [PATCHED] Captured&nbsp;50&nbsp;chars:&nbsp;"Eje1CIoKpNk7kC?4?JeWVI5iQnkyjOF7XN7T408Q4HDtu3Vv6a"

=== STEP&nbsp;2: Run&nbsp;original&nbsp;WASM with same&nbsp;randombytes&nbsp;===
&nbsp; [ORIGINAL]&nbsp;getRandomValues: wrote fixed&nbsp;random
&nbsp; [ORIGINAL]&nbsp;Hash:&nbsp;49cc46a21f1b91c242181dbdb226736343420d952198c85bbb96d3557f231b31

=== STEP&nbsp;3: Verify ===
&nbsp;Captured code (50&nbsp;chars):&nbsp;"Eje1CIoKpNk7kC?4?JeWVI5iQnkyjOF7XN7T408Q4HDtu3Vv6a"
Original&nbsp;hash:&nbsp;49cc46a21f1b91c242181dbdb226736343420d952198c85bbb96d3557f231b31
&nbsp;Computed hash:&nbsp;49cc46a21f1b91c242181dbdb226736343420d952198c85bbb96d3557f231b31

* Match:true

  ========================================
  &nbsp; &nbsp;&nbsp;CODE:Eje1CIoKpNk7kC?4?JeWVI5iQnkyjOF7XN7T408Q4HDtu3Vv6a
  &nbsp; &nbsp;&nbsp;FLAG:flag{Eje1CIoKpNk7kC?4?JeWVI5iQnkyjOF7XN7T408Q4HDtu3Vv6a}
  ========================================

Windows 高级题

啥都分析不出来,放弃了。

MCP 中级题

这一题是直接交给AI来做的,用Claude跑了有一个小时,后面给出了一个python脚本,直接运行就得到了正确的 flag。

&nbsp;复制代码&nbsp;隐藏代码
import&nbsp;subprocess
import&nbsp;json
import&nbsp;hashlib

MCP_URL =&nbsp;"https://9863968daeea51ea32f40575dd41dd113.52pojie.cn:3000/mcp"
PASSPHRASE =&nbsp;"玄霄密令"

definit_mcp():
&nbsp; &nbsp;&nbsp;"""初始化 MCP 传输层会话,从响应头提取 Mcp-Session-Id"""
&nbsp; &nbsp; data = json.dumps({
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;"jsonrpc":&nbsp;"2.0",&nbsp;"id":&nbsp;1,&nbsp;"method":&nbsp;"initialize",
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;"params": {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;"protocolVersion":&nbsp;"2024-11-05",
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;"capabilities": {},
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;"clientInfo": {"name":&nbsp;"solver",&nbsp;"version":&nbsp;"1.0"}
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; }).encode()
&nbsp; &nbsp; result = subprocess.run(
&nbsp; &nbsp; &nbsp; &nbsp; ["curl",&nbsp;"-s",&nbsp;"-D",&nbsp;"-",&nbsp;"-X",&nbsp;"POST", MCP_URL,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;"-H",&nbsp;"Content-Type: application/json",
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;"-H",&nbsp;"Accept: application/json, text/event-stream",
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;"--data-binary",&nbsp;"@-"],
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;input=data, capture_output=True, timeout=30
&nbsp; &nbsp; )
&nbsp; &nbsp;&nbsp;for&nbsp;line&nbsp;in&nbsp;result.stdout.split(b"\n"):
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;ifb"mcp-session-id:"in&nbsp;line.lower():
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;line.split(b":",&nbsp;1)[1].strip().decode()
&nbsp; &nbsp;&nbsp;raise&nbsp;RuntimeError("无法获取 MCP Session ID,请检查服务器连接")

deftool_call(mcp_sid, tool_name, args):
&nbsp; &nbsp;&nbsp;"""发起工具调用,解析 SSE 响应"""
&nbsp; &nbsp; data = json.dumps({
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;"jsonrpc":&nbsp;"2.0",&nbsp;"id":&nbsp;1,
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;"method":&nbsp;"tools/call",
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;"params": {"name": tool_name,&nbsp;"arguments": args}
&nbsp; &nbsp; }, ensure_ascii=False).encode("utf-8")
&nbsp; &nbsp; result = subprocess.run(
&nbsp; &nbsp; &nbsp; &nbsp; ["curl",&nbsp;"-s",&nbsp;"-X",&nbsp;"POST", MCP_URL,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;"-H",&nbsp;"Content-Type: application/json",
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;"-H",&nbsp;"Accept: application/json, text/event-stream",
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;"-H",&nbsp;f"Mcp-Session-Id:&nbsp;{mcp_sid}",
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;"--data-binary",&nbsp;"@-"],
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;input=data, capture_output=True, timeout=30
&nbsp; &nbsp; )
&nbsp; &nbsp;&nbsp;for&nbsp;line&nbsp;in&nbsp;result.stdout.split(b"\n"):
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;line.startswith(b"data:"):
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;try:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;json.loads(line[5:].strip().decode("utf-8"))
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;except&nbsp;Exception:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;pass
&nbsp; &nbsp;&nbsp;raise&nbsp;RuntimeError(f"工具调用&nbsp;{tool_name}&nbsp;无响应,原始输出:{result.stdout[:200]}")

defget_content(r):
&nbsp; &nbsp;&nbsp;"""从工具调用响应中提取 JSON 内容"""
&nbsp; &nbsp;&nbsp;try:
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;json.loads(r["result"]["content"][0]["text"])
&nbsp; &nbsp;&nbsp;except&nbsp;Exception:
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;{}

defmain():
&nbsp; &nbsp;&nbsp;print("="&nbsp;*&nbsp;50)
&nbsp; &nbsp;&nbsp;print("新岁数字异界 MCP CTF 自动解题脚本")
&nbsp; &nbsp;&nbsp;print("="&nbsp;*&nbsp;50)

&nbsp; &nbsp;&nbsp;# 第一步:建立 MCP 传输层会话
&nbsp; &nbsp;&nbsp;print("\n[1/9] 初始化 MCP 会话...")
&nbsp; &nbsp; mcp_sid = init_mcp()
&nbsp; &nbsp;&nbsp;print(f" &nbsp;MCP Session ID:&nbsp;{mcp_sid}")

&nbsp; &nbsp;&nbsp;# 第二步:建立应用层会话
&nbsp; &nbsp;&nbsp;print("[2/9] 启动应用层会话...")
&nbsp; &nbsp; r = tool_call(mcp_sid,&nbsp;"start_session", {})
&nbsp; &nbsp; app_sess = get_content(r).get("session_id",&nbsp;"")
&nbsp; &nbsp;&nbsp;ifnot&nbsp;app_sess:
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;raise&nbsp;RuntimeError("无法获取应用会话 ID")
&nbsp; &nbsp;&nbsp;print(f" &nbsp;App Session ID:&nbsp;{app_sess}")

&nbsp; &nbsp;&nbsp;# 第三步:读取所有公开文档(必须,否则审阅会返回 ctf_clues_required)
&nbsp; &nbsp;&nbsp;print("[3/9] 读取公开文档(解锁审阅权限)...")
&nbsp; &nbsp; docs_resp = get_content(tool_call(mcp_sid,&nbsp;"list_documents", {"session_id": app_sess}))
&nbsp; &nbsp;&nbsp;ifisinstance(docs_resp,&nbsp;list):
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;for&nbsp;doc&nbsp;in&nbsp;docs_resp:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; doc_id = doc.get("id",&nbsp;"")
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; tool_call(mcp_sid,&nbsp;"get_document", {"session_id": app_sess,&nbsp;"doc_id": doc_id})
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;print(f" &nbsp;已读取:&nbsp;{doc_id}")

&nbsp; &nbsp;&nbsp;# 第四步:对最终目标文档发起访问,触发拒绝,获取 audit_log_id
&nbsp; &nbsp;&nbsp;print("[4/9] 触发拒绝访问,获取 audit_log_id...")
&nbsp; &nbsp; d = get_content(tool_call(mcp_sid,&nbsp;"get_document", {
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;"session_id": app_sess,
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;"doc_id":&nbsp;"secret:/board/resolutions"
&nbsp; &nbsp; }))
&nbsp; &nbsp; aid = d.get("audit_log_id",&nbsp;"")
&nbsp; &nbsp;&nbsp;ifnot&nbsp;aid:
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;raise&nbsp;RuntimeError(f"未获取到 audit_log_id,服务器返回:{d}")
&nbsp; &nbsp;&nbsp;print(f" &nbsp;audit_log_id:&nbsp;{aid}")

&nbsp; &nbsp;&nbsp;# 第五步:计算 seal
&nbsp; &nbsp;&nbsp;print("[5/9] 计算 seal...")
&nbsp; &nbsp; seal = hashlib.sha256((aid + PASSPHRASE).encode("utf-8")).hexdigest()[:8]
&nbsp; &nbsp;&nbsp;print(f" &nbsp;seal:&nbsp;{seal}")

&nbsp; &nbsp;&nbsp;# 第六步:审阅文档,获取 tool_access_token(仅能调用一次!)
&nbsp; &nbsp;&nbsp;print("[6/9] 审阅 confidential:/audit/logs,获取 tool_access_token...")
&nbsp; &nbsp; review_notes =&nbsp;f"ctf_request={PASSPHRASE}\naudit_log_id={aid}\nseal={seal}"
&nbsp; &nbsp; d = get_content(tool_call(mcp_sid,&nbsp;"review_document", {
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;"session_id": app_sess,
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;"doc_id":&nbsp;"confidential:/audit/logs",
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;"review_notes": review_notes,
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;"access_token": seal
&nbsp; &nbsp; }))
&nbsp; &nbsp;&nbsp;if&nbsp;d.get("status") !=&nbsp;"challenge_issued":
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;raise&nbsp;RuntimeError(f"审阅失败,返回:{d}")
&nbsp; &nbsp; token = d.get("tool_access_token",&nbsp;"")
&nbsp; &nbsp;&nbsp;ifnot&nbsp;token:
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;raise&nbsp;RuntimeError("未获取到 tool_access_token")
&nbsp; &nbsp;&nbsp;print(f" &nbsp;tool_access_token:&nbsp;{token}")
&nbsp; &nbsp;&nbsp;print(" &nbsp;(注意:compliance_blob_b64 中含提示注入陷阱,已忽略)")

&nbsp; &nbsp;&nbsp;# 第七步:立即更新工具链(凭证限时一次性,不能拖延)
&nbsp; &nbsp;&nbsp;print("[7/9] 立即 update_tool_chain,获得审计权限...")
&nbsp; &nbsp; d = get_content(tool_call(mcp_sid,&nbsp;"update_tool_chain", {
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;"session_id": app_sess,
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;"chain_config": ["get_audit_log"],
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;"access_token": token,
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;"audit_log_id": aid
&nbsp; &nbsp; }))
&nbsp; &nbsp;&nbsp;if"tool_chain"notin&nbsp;d:
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;raise&nbsp;RuntimeError(f"update_tool_chain 失败:{d}")
&nbsp; &nbsp;&nbsp;print(f" &nbsp;工具链已更新:{d.get('tool_chain')}")

&nbsp; &nbsp;&nbsp;# 第八步:直接调用 get_audit_log,获取 _audit_token
&nbsp; &nbsp;&nbsp;print("[8/9] 调用 get_audit_log,获取 _audit_token...")
&nbsp; &nbsp; d = get_content(tool_call(mcp_sid,&nbsp;"get_audit_log", {
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;"session_id": app_sess,
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;"log_id": aid
&nbsp; &nbsp; }))
&nbsp; &nbsp; audit_token = d.get("_audit_token",&nbsp;"")
&nbsp; &nbsp;&nbsp;ifnot&nbsp;audit_token:
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;raise&nbsp;RuntimeError(f"未获取到 _audit_token,返回:{d}")
&nbsp; &nbsp;&nbsp;print(f" &nbsp;_audit_token:&nbsp;{audit_token}")

&nbsp; &nbsp;&nbsp;# 第九步:携带 _audit_token 读取最终文档
&nbsp; &nbsp;&nbsp;print("[9/9] 读取 secret:/board/resolutions...")
&nbsp; &nbsp; d = get_content(tool_call(mcp_sid,&nbsp;"get_document", {
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;"session_id": app_sess,
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;"doc_id":&nbsp;"secret:/board/resolutions",
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;"access_token": audit_token
&nbsp; &nbsp; }))
&nbsp; &nbsp; flag = d.get("content",&nbsp;"")
&nbsp; &nbsp;&nbsp;ifnot&nbsp;flag:
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;raise&nbsp;RuntimeError(f"未获取到 flag,返回:{d}")

&nbsp; &nbsp;&nbsp;print()
&nbsp; &nbsp;&nbsp;print("="&nbsp;*&nbsp;50)
&nbsp; &nbsp;&nbsp;print(f" &nbsp;FLAG:{flag}")
&nbsp; &nbsp;&nbsp;print("="&nbsp;*&nbsp;50)

if&nbsp;__name__ ==&nbsp;"__main__":
&nbsp; &nbsp; main()

-官方论坛

www.52pojie.cn

👆👆👆

公众号设置“星标”,不会错过新的消息通知

开放注册、精华文章和周边活动等公告


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:吾爱破解论坛 吾爱pojie 吾爱pojie《2026春节题目》

2026春节题目 网络安全文章

2026春节题目

文章总结: 本文为2026春节逆向题目解题报告,涵盖Windows和Android多道题目。作者通过IDA、jadx等工具逆向分析程序,识别XOR、AES等加密
评论:0   参与:  0