-
-
[原创]第六题:酉时·书院迷局 从双分支耦合到代数逆推
-
发表于: 2天前 270
-
题目给出一个 Android APK。界面上只有一个输入框和一个 VERIFY 按钮,目标是找到
一组合法输入,使未修改的原版程序提示:
这题真正麻烦的地方不是某一个算法特别复杂,而是 native 层同时布置了多套状态机、
opaque helper、完整性检查和真假分支。输入又被拆成奇偶两组:odd 分支决定 even 的
目标,even 分支反过来决定 odd 的 salt。只看任意一边,都会得到一个无法独立验证的
“半成品答案”。
最终没有修改 APK,也没有绕过返回值。整个解法是先确定程序的真实正向数据流,再从
odd 的终端约束逆出 odd25,随后求出 even 的状态参数,最后把两组字节重新交织回原始
50 字节输入。
求解顺序可以概括为:
入口在 com.autorun.kctf.MainActivity。Java 层先规范化输入,然后调用 JNI:
因此输入有三个直接约束:
JNI 校验对象是十六进制字符串解码得到的 50 个字节,不要求这些字节本身可打印。
native 会通过 Java 方法读取 APK/ELF 中的 .kctfguard 元数据。Java 先用nativeShareMask 遮罩前 16 字节,native 再用相同掩码恢复。
对未修改 APK,真实运行时材料为:
后面的 guard 校验值、地址和长度只是完整性材料,不是隐藏 flag。
50 字节输入通过三次 ARM64 VLD2 按下标奇偶拆成两个 25 字节数组:
两支不是独立的:odd 先决定 even 的摘要目标,even 再决定传给 odd 最终校验器的
salt。
下面这张图只描述 MainActivity.nativeProcessInput(byte[50]) 内部的主校验数据流。
even25 完成可逆预处理后会写入一个 25 字节中间缓冲区,后续四个 stage 都从这里
读取状态。

看起来像环形依赖,但方向是确定的:
native 中有大量 opaque helper 和伪分支。通过静态化简、Unicorn/angr 与真机动态
回归,可归纳为:
这一步很重要:如果保留所有伪分支直接符号执行,状态空间会迅速爆炸;化简后真正的
根返回门只有 even_ok 和 odd_ok。
odd[0:25] 后补 7 个 0x5a,组成 32 字节并解释成四个小端 64 位字。sub_5FE8 的单轮可写成:
跑满 12 轮后得到 96 字节派生状态 O。用于 even 目标变换的 selector 是:
odd 最终校验器的调用形式为:
尾部虽然很长,但返回 1 实际要求以下检查同时成立:
其中一组 sub_7400 tag 的目标变换对 salt 可逆,已经足以唯一确定:
另一组给出同一结果,是冗余校验。固定 salt=0xd7 后,tag 与 mask 约束可用
Z3/PySAT 反解 A,B,再逆 12 轮 ARX 得到 odd25。
最终 odd 候选为:
具体回归值:
该 odd 候选在真机上强制 salt=d7 时,sub_8F90 返回 1。
对该 odd25:
native 先生成基础目标:
再执行 sub_EDCC:
得到最终 even 目标:
even25 先经过一个严格可逆的逐字节置换,生成 25 字节中间缓冲区。下面简记为buf[25]:
这层只是双射,不参与成功判定。因此可以先求出中间缓冲区,再按相反顺序逐字节逆回
even25。
Stage 1~4 的局部检查 不是最终返回门:
所以不能从控制流上断言“四个 stage 必须全过”。正确做法是把全通过路径作为一个候选
构造,求解后再检查最终摘要和 salt。事实证明本题的预期解确实位于这条自然路径上。
Stage 1 的自然通过前缀为:
Stage 2 以 0xe57305c7 为 xorshift32 seed,生成并散布 256 字节流。自然路径的
S-box 前 16 字节为:
因此 sbox[0] = 0x85。
Stage 3 从中间缓冲区读取两个 32 位参数:
LCG 为:
连续生成 32 个 key,再对三组明文做 16 轮 TEA-like ARX:
自然路径取标准 TEA delta:
此时只剩 32 位 seed0。通用 64 位 QF_BV 求解很慢,因此使用 CUDA 完整枚举2^32 个 seed,只用第一组向量筛选,再用另外两组独立回验:
枚举命中结果为:
于是:
0xdeadc0de 也印证这是预期构造值,而不是偶然碰撞。
Stage 4 的 8 个等式可直接用 Z3 求出:
对应中间缓冲区尾部:
核心初始四个 32 位字固定为:
共执行 12 轮,两条 lane 分别使用偶数和奇数 key。单条 lane 的轮函数为:
完成前 10 轮后、进入第 11 轮前,四个 word 各执行:
代入求出的参数:
得到:
与 odd selector 产生的 expectedDigest 完全一致。
even 还必须自然产生 salt=0xd7,否则已解出的 odd25 仍不能通过。
代入:
得到:
至此 even 摘要和 odd salt 两条耦合约束同时闭合。
完整的 25 字节中间缓冲区为:
按第 6 节的字节关系逆变换,恢复出 even25:
按 even[0], odd[0], even[1], odd[1], ... 交织:
最终得到:
在未修改 APK 中被动观测摘要、salt 与 odd 返回值,同时直接调用MainActivity.nativeProcessInput(byte[50])。
成功结果:
停止 Frida,彻底冷启动应用,输入 100 字符并点击真实 VERIFY 按钮,Toast 显示:
这排除了“模型正确但真机路径不一致”、hook 修改寄存器、APK 被修改等情况。

题目 APK 与设备安装包的 SHA-256 均为:
这题的难点不在某一个轮函数,而在于 odd 和 even 两条分支之间存在双向依赖,同时
四个 stage 的局部检查又只负责选择后续状态,并不直接决定 JNI 返回值。把 opaque
helper 和伪分支化简后,真正的成功条件可以收束为两项:even 生成的 16 字节摘要必须
等于 odd 决定的目标摘要,odd25 还必须在 even 自然生成的 salt 下通过 sub_8F90。
冰与火的战歌:Windows内核攻防实战高级班!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。