Bugku 逆向 MatrixHunter:黑盒 oracle + GF(2) 消元出唯一解

一道看起来是"矩阵运算"的逆向题,真正的难点不在算法本身,而在你那套复刻出来的算法到底对不对。 本文记录从「复刻 5 轮全部失败」到「黑盒测量 + 线性代数一次出解」的完整过程。


0. 题目与结果

项 值
附件 MatrixHunter.exe
大小 92,672 字节
类型 x86_64 PE
ImageBase 0x140000000
输入长度 29 字节
正确输入 bugku{M4tr1x_0v3r_GF2_1s_fun}

先给结论:程序把 29 字节输入 变换成 32 字节输出 ,再与两个 16 字节常量逐字节比较,全等才打印 Correct!。

关键发现是:这个变换对输入是「仿射」的 (线性 + 常数偏移)。一旦确认这一点,就不需要理解 M 表、perm、splitmix64 RNG 的任何细节------只需要测 232 次,就能完整刻画整个映射,然后用高斯消元解出来。


1. 静态分析:程序到底在做什么

用 radare2 定位 main(0x140001420),先把几个关键地址记下来:

1.1 反调试

序言里有两处:

asm 复制代码
call IsDebuggerPresent      ; 非 0 直接 return
call GetTickCount
cmp  eax, 0xfff             ; <= 0xfff(即启动后 4 秒内)直接 return
jbe  exit

这两处决定了后面做动态仿真时必须给它们返回值 (IsDebuggerPresent → 0、GetTickCount → 0x20000)。

1.2 输入与长度校验

asm 复制代码
lea  rcx, "Enter the flag: "
call printf
lea  rcx, [rbp-0x60]
mov  edx, 0x40
mov  r8,  stdin
call fgets
; 循环剥掉尾部 \r \n 并清零
call strlen
cmp  eax, 0x1d              ; 必须等于 29
jne  wrong

长度不等于 29 就直接 Wrong!,所以未知量恰好是 29 字节 = 232 比特。

1.3 输入装配与置换

asm 复制代码
movaps xmm0, [rbp-0x60]     ; input[0..15]
movaps [rbp+0x00], xmm0
movdqu xmm0, [rbp-0x53]     ; input[13..28]
movdqu [rbp+0x0d], xmm0
mov  byte [rbp+0x1f], 0
mov  word [rbp+0x1d], 0

得到 32 字节工作缓冲:

复制代码
W = input[0..28] || 0x00 0x00 0x00

之后从 0x1400019e0 开始的一段是纯字节 gather (rax 从 3 步进 4 到 0x23):

复制代码
V[i] = W[perm[i]]
X    = V            ; 仍是 32 字节

注意这里没有任何位运算,置换只重排字节顺序。这一点很重要:它意味着后面的"位级"结构全部来自轮函数,而不是置换。

1.4 轮密钥与 256 轮变换

asm 复制代码
; K0 = dword[0x1400070a0 .. 0x1400070af]   (8 个 dword)
; K1 = dword[0x1400070b0 .. 0x1400070bf]   (8 个 dword)
u0 = X[0:16]  ^ K0
u1 = X[16:32] ^ K1

然后 0x140001ab0 起是 256 轮 ,每轮处理 M 表的一行 32 字节(8 个 u32):

复制代码
t   = (M[j] & u0) | (M[j+4] & u1)      ; 4 个 dword
t  ^= ...                              ; 折叠
t >>= 16; t >>= 8; t >>= 4; t >>= 2; t >>= 1
bit = t & 1                            ; 取奇偶位
out_bit[j] = bit                       ; shl edx, cl 写入 32 字节输出

即:每一轮把 32 字节压缩成 1 个奇偶位,256 轮得到 256 位 = 32 字节。

整条数据流如下(静态示意图):

同一份数据流的交互版(Archify 渲染,可在线路图里逐节点查看):

1.5 比较

asm 复制代码
pcmpeqb xmm1, [0x140004100]   ; out[16:32] vs 3d785f47a59be7f301e23017ca912d2c
pcmpeqb xmm0, [0x140004110]   ; out[0:16]  vs 200cd90738d03bea348b4337a81b3ae1
pand    xmm0, xmm1
pmovmskb eax, xmm0
cmp     eax, 0xffff

32 字节全等才 Correct!。

目标向量拼起来就是:

复制代码
target = 200cd90738d03bea348b4337a81b3ae13d785f47a59be7f301e23017ca912d2c

2. 为什么"复刻算法"这条路走不通

一开始的直觉是:既然是矩阵运算,那把 M 表、perm、splitmix64 RNG、round_key 全部复刻出来,正向算一遍,然后对 GF(2) 求逆就行了。

结果连续 5 轮全部失败:

轮次 做法 结果
1 复刻 M 表 + perm 正向与真机 3 次逐字节一致,但求逆后不可打印
2 修正 perm 推导顺序 方程不一致(rank < 232)
3 用 frida dump 真机中间量校准 中间量对齐了,最终仍不可打印
4 更换求逆库(sympy / 自写) 同上
5 开始怀疑题目本身 判定 target_not_reachable_from_29char_input(其实是自己模型错了)

矛盾点很刺眼:正向能对上,反向解不出 。这说明复刻的模型在某个细节(perm 的某处、或字节装配的某一步)有偏差,而"正向巧合对上"并不能排除这种偏差------因为正向只验证了 3 个点。

教训:当"模型正向一致、反向无解"时,不要继续修模型,要换一个不依赖模型的判据。


3. 换思路:用 Unicorn 把校验函数变成黑盒 oracle

既然模型不可信,那就直接执行真实机器码。用 Unicorn 铺一个最小 PE 运行环境:

python 复制代码
# 按 PE 节表铺映像到 0x140000000(14 个节)
# 45 个 IAT 槽全部指向 0x30000000 stub 页(每槽一个 0xC3 = ret)
# 用 UC_HOOK_CODE 拦截,按函数名实现最小语义
HOOKS = {
    'printf':            lambda: 0,
    'fflush':            lambda: 0,
    '__acrt_iob_func':   lambda: FAKE_FILE,
    'fgets':             lambda buf, n, fp: write(buf, INPUT + b'\n\x00'),
    'strlen':            real_strlen,
    'puts':              lambda: 0,
    'IsDebuggerPresent': lambda: 0,
    'GetTickCount':      lambda: 0x20000,     # > 0xfff,绕过 4s 反调试
}

从 main(0x140001420)一直跑到 0x140001b3d(cmp eax, 0xffff)停下,读 [rbp-0x20, rbp) 的 32 字节和 eax。

整个求解过程中五方的交互时序(Archify 时序图):

3.1 先自证 oracle 等价真机

这一步不能省。用两个固定输入和历史上的 frida 真机 dump 对比:

复制代码
input = b'B'*29  ->  1545226b789928c236fd8fbed5f2f3320f12460c6296d1b97d80e076401e95bb
input = b'A'*29  ->  8933b2d92643d3fc732c25ed0468eaa69a2de5d2ba56bcfcb4514efe8a772d66

两份结果与真机 dump 逐字节一致 ⇒ oracle 与真机等价。


4. 仿射性 ⇒ 232 次测量刻画整个映射

4.1 先假设是仿射的

如果这个映射 F: {0,1}^232 -> {0,1}^256 满足

复制代码
F(x) = A·x ⊕ b

那么只要测出 b = F(0) 和 A 的 232 个列向量,整个函数就完全确定了:

复制代码
col[k][b] = F(base ^ (1 << (k*8 + b)))  ^  F(base)

取 base = b'B'*29(全 'B',避免 0 字节带来的边界情况),一共需要 232 次 oracle 调用(实测缓存下来约 2 分钟)。

4.2 再验证仿射性成立

用 3 个随机的 29 字节输入:

python 复制代码
pred = out0
for k in range(29):
    for b in range(8):
        if (x[k] >> b) & 1:
            pred ^= col[k][b]
assert pred == oracle(x)     # 3/3 通过

3/3 全部通过 ⇒ 仿射性成立,模型可以放心用。

这一步是整个解法里最关键的一步 。它把"要不要相信自己的复刻"这个问题彻底绕开了:无论作者用了什么 RNG、什么表、什么置换,只要这个函数是仿射的,232 次测量就是完备的。


5. GF(2) 高斯消元

现在把问题写成方程组。设未知位向量为 x(232 位),则

复制代码
A·x = target ⊕ out0

其中 A 是 256×232 的 0/1 矩阵,每列就是测出来的 col[k][b]。

矩阵的列权重分布(每列 1 的个数)也在正常范围内,既不稀疏也不稠密,符合随机线性映射的特征:

5.1 消元

标准的 GF(2) 高斯消元(XOR 代替加减):

python 复制代码
rows = [ (int.from_bytes(target ^ out0, 'big') << 232) | ((1 << c)) for ... ]
# 按位主元消元
rank = 0
for col in range(232):
    piv = next((r for r in range(rank, 256) if (rows[r] >> (231 - col)) & 1), None)
    if piv is None:
        continue
    rows[rank], rows[piv] = rows[piv], rows[rank]
    for r in range(256):
        if r != rank and (rows[r] >> (231 - col)) & 1:
            rows[r] ^= rows[rank]
    rank += 1

结果:

复制代码
rank = 232 / 232
自由变量 = 0          ← 唯一解

由于秩满,解是唯一的,不需要任何猜测。


6. 结果

复制代码
[+] 唯一解输入 = b'bugku{M4tr1x_0v3r_GF2_1s_fun}'
[+] 全可打印    = True
[*] eax=0xffff? True
[*] 真机输出 = 'Enter the flag: Correct!'
[+] FLAG = bugku{M4tr1x_0v3r_GF2_1s_fun}

回代 oracle:eax == 0xffff(32 字节全等)✓ 真机运行 MatrixHunter.exe 并喂入该输入:输出 Enter the flag: Correct! ✓

FLAG:bugku{M4tr1x_0v3r_GF2_1s_fun}

注意这道题用 bugku{...} 前缀,不是常见的 flag{...}。


7. 踩坑清单

# 坑 现象 解决
1 复刻 RNG/表去求逆 正向对上、反向不可打印 改用黑盒测量,不建模
2 只验证 3 个点就认为模型对 正向一致但反向无解 必须做线性自检(随机输入交叉验证)
3 Unicorn 只铺代码节 IAT 调用直接崩 把 45 个 IAT 槽改写成 ret,用 hook 接管
4 忘了反调试 运行 4 秒内直接 return,拿不到输出 GetTickCount hook 返回 0x20000
5 用 fgets 时没加 \n\0 输入长度不对,直接 Wrong! hook 里写入 INPUT + b'\n\x00'
6 以为置换是位运算 推导方向偏 反汇编确认是 movzx 字节 gather
7 假设平台是 flag{} 提交失败 该题为 bugku{}

8. 一句话总结

逆向题里最贵的成本不是"看不懂算法",而是"以为看懂了算法 "。 当正向能对上、反向解不出时,正确做法是把函数当成黑盒,用足够多的测量点去刻画它------ 本题只要 232 次测量,就把一个绕了 5 轮的迷宫变成了一道线性代数填空题。


9. 图形附件(Archify 交互图)

两张交互图已就近嵌入正文(截图形式,见 §1.4 与 §3);可离线打开的独立 HTML 原件:

图 文件 说明
数据流(输入→输出) matrixhunter-dataflow.html(交互图,CSDN 已省略 HTML 版) 5 段(Input→Assemble→Permute→Key Mix→Parity+Compare)节点与流向,标注每步为何保持 GF(2) 线性
求解时序 matrixhunter-sequence.html(交互图,CSDN 已省略 HTML 版) 分析者 / MatrixHunter.exe / Unicorn 仿真 / cols.json / GF(2) 消元 五方的 13 条交互与三段阶段划分

生成、校验与截图:

text 复制代码
archify finalize dataflow ... --quality showcase --json   -> ok=true, status=pass
archify finalize sequence ... --quality showcase --json   -> ok=true, status=pass
archify check 两张图                                       -> issues: []
archify visual-check <html> --out-dir wp_figs/archify_shots
  -> checks: containment/readability/viewerChrome/themeStates/captures 全部 pass

CSDN 不支持托管 HTML,故正文嵌入的是 visual-check 的真实渲染截图(2048×1320,深色主题), 交互原件保留在交付目录内。


本文所有数据均来自对附件的本地分析与自写脚本实测,未引用任何外部题解。

相关推荐
菩提小狗19 小时前
每日安全情报报告 · 2026-11-07
网络安全·漏洞·cve·安全情报·每日安全
不庸.1 天前
theHarvester 入门教程:从装好到看懂输出
安全·web安全·网络安全·开源·安全性测试
kingx31 天前
Bugku 3122 谷歌小恐龙题解:纯离线逆向 Vault 的 AES-GCM 加密资源(附 Python 解密器)
java·逆向·ctf·bugku·aes-gcm
liferecords2 天前
OpenAI 把「Critical」级挖洞模型交付出去了:GPT-6 Cyber 正式发布,越界率压到 1% 以下
网络安全·openai·ai安全·模型发布
kingx32 天前
Bugku 326 Baby-heap1 题解:glibc 2.23 堆溢出到 FSOP 劫持 _IO_list_all(附完整 exp)
pwn·ctf·glibc·堆溢出·fsop
FungLeo2 天前
成为全栈·基础补充·Web 安全基础:XSS、CSRF 与 SQL 注入
web安全·网络安全·xss·csrf·sql注入·成为全栈
hengdonghui2 天前
Writeup 4 0xGame 2021 · command (WEB)
web·ctf·rce(远程代码执行)
菩提小狗3 天前
每日安全情报报告 · 2026-11-05
网络安全·漏洞·cve·安全情报·每日安全
QZ166560951593 天前
政务泛监测体系能力排行:AI 重塑监测边界,治理的颗粒度革命
网络·人工智能·安全·网络安全·政务