首页
社区
课程
招聘
[原创]第六题:酉时·书院迷局 WP
发表于: 1天前 389

[原创]第六题:酉时·书院迷局 WP

1天前
389

证据标记:

APK:

Java 输入要求:

JNI 将 50 字节按下标奇偶拆成两条 25-byte 通道:

题解中使用的 canonical 接受输入为:

对应:

原始 ARM64 JNI 在 clean 环境自然执行:

原 APK 真机也接受该输入。

canonical local94 中有 14 个后来被截掉或等价化的自由 bit,因此 canonical effective-state 分支精确对应:

个不同 100-hex 输入。

例如下面这个和 canonical 肉眼差别很大的输入,也已经在原 ARM64 JNI 上自然 return=1

所以本文会把上面的题解字符串称为 canonical accepted input,而不是数学意义上的唯一 flag。

需要说明边界:目前已经严格证明 canonical effective-state family 恰有 16384 个输入,但尚未穷尽全部 514 张 possible T-table × 64-bit effective state,因此不能声称“全程序总接受数恰好就是 16384”。

Java Activity 只承担三类工作:

Native JNI 入口:

最终 odd verifier 被 .eh_frame 覆盖为完整范围:

Ghidra/JADX 会因为计算跳转、tail block、假 CFG 把它切成多个函数,因此本题必须优先信任:

而不是 decompiler 自动函数边界。

ELF note / comment 还能确认工具链:

没有 .note.gnu.property 声明整库 BTI/PAC。.text 开头少量 BTI c 主要落在 CRT glue,0x13cbc 的 BTI 则属于 clear-cache helper;这些应与作者手写 anti-analysis 分开看。

ELF 中:

.kctfguard 的 96 字节看起来像合法 AArch64 小函数,还放了 SHA-256 IV 一类熟悉常量,但它从不被调用执行。[STATIC]

唯一相关 relocation 是把:

作为内存区间给 Native CRC helper。

因此它本质上是一个伪装成代码的 integrity anchor。

Java 对 section header 篡改还有 fallback:如果找不到 .kctfguard section,会在 .text 中逐字节寻找完整 96-byte guard signature;若 signature 也不存在,则退化为对整个 .text 做 CRC32。当前原始 APK 正常直接走 .kctfguard 路径。[STATIC]

Java 会解出:

但字符串解码函数的第二参数只改变控制状态,不影响真正的输出 byte;所以 classes.dex / META-INF / resources.arsc 只参与控制流烟雾,不进入 material hash。[STATIC]

真正的数据锚仍然是选中的 libkctf.so.kctfguard/.text

Java 使用 4 个 64-bit salt:

以及经典 PCG LCG:

每个 dword:

代入:

得到:

即:

这 16 字节只有 32 bit 真正自由度。因为:

任取 context 的一个 4-byte word,都能独立反解回同一个:

所以这不是 128-bit KDF,而是 CRC32 的四路 redundant commitment。[ALG]

Java 在跨 JNI 前再 XOR:

因此 Java share 前 16 字节为:

完整 32-byte share:

布局:

JADX 会把一个关键 goto 结构化错,导致看起来 'I' 分支永远不会真正保留。

按 smali 恢复后,状态机可写成:

canonical 结果:

Native 会逐语义重跑同一个状态机;canonical + 20 组随机 share 做过对拍,21/21 完全一致。[DYNAMIC]

Native 内存 guard helper 接收 Java share 的:

但实际只检查:

并对编译时固定的 [guard_start,guard_end) 做 CRC;offset 没有拿去比较真实内存地址。

因此只改 offset、同步重算 d(),原 JNI 仍可 return=1。[DYNAMIC]

任意非零 guard mismatch 经 e960 压成全 1 mask,再把:

XOR 到 context。

b979379e 正是 0x9e3779b9 的 little-endian 字节。

这不是不可逆 reject,而是可预测 fault transform。

如果能直接控制 Java share,可以先把 context 预补偿成:

让 Native poison 后重新恢复 canonical context,原 flag 仍可通过。[DYNAMIC]

但该 128-bit 预补偿 context 不属于正常 Java CRC32→context 的 32-bit image,所以只改 guard bytes、不 patch Java share 逻辑并不能得到它。[ALG]

我们实际构造了另一份 96-byte guard:内容不同,但通过 32×32 GF(2) CRC 补偿保持:

原 Java share、原 flag 不改:

所以 .kctfguard 是 CRC tripwire,不是密码学内容认证。[ALG+DYNAMIC]

Native 将 Java checksum mismatch 写入全局 0x16370;业务层只有 FUN_5fe8 读取它。

clean:

非零时:

因此除了 residual=0 外,只要四字节 XOR=0xa5,也会产生同样的 0x5a padding。

这样的非零 residual 有 2^24 个。

实际把 residual 改成:

原 ARM64 仍自然 JNI=1。[DYNAMIC]

精确验证后的基础 helper:

早期为了保守起见,2cbc/2d70/2e2c/10800 的全局写入被保留;深入分析后可以更强地证明:

只被这组 opaque helper 自己读写,对外返回仍恒 1/0/0/0,没有其它业务观察者。

所以整个 opaque state subsystem 可以整体删除。[STATIC+DYNAMIC]

按原 FDE 统计,仅这些已完全塌缩 helper 就超过:

约占 .text 的 6.5%;还没算 w2/f32c/G3 中后来消掉的大量 Mix→InvMix 与 carry 电路。

曾把 PLT 0x13f00 标成 sysconf,这是错误的。

ELF .rela.plt 明确证明:

两个调用点都:

也就是:

checker只关心:

AArch64 clean 设备低两位对应 FP / ASIMD,正常应满足。

旧 harness 默认 getauxval=0,所以稳定进入诱饵环境,loader key 变成:

修正:

后,loader 自然得到:

此外 dlsym("mprotect") 要返回一个成功的权限切换 stub。只补这两类真实环境语义,不注入任何算法中间值、不手改 PC,原 ARM64 JNI 即可完整 return=1。[DYNAMIC]

FUN_298c(0..9)

Outer loader 扫描:

前 6 条 AArch64 指令。

百余条 cmp/csel/ccmp 最终只是在生成 32-bit marker:

对 5 类语义代表的全部 5^6=15625 序列,以及 10000 组任意 32-bit 指令字,纯公式与原 ARM64 classifier 0 mismatch。[ALG]

总 marker 空间只有:

种。

环境位:

异常 XOR 常量:

64 个组合得到 64 把互不相同 runtime key。

context 先过一个 rank=128 的线性 byte-rotate 编码:

canonical:

再对 FUN_5fe8 开头四个 32-byte block 各做标准 CRC32:

得到:

clean 6-bit env expansion:

最终:

64 种环境全部和原 ARM64 对拍一致。

对完整 0x4c0 payload blob 逐字节验证:

那把曾经看起来很重要的 global static key:

在 clean/bad env 下都对 runtime key 和最终 payload 不产生可观察影响;改成全零、全 ff、随机值,最终代码页逐字节不变。[DYNAMIC]

它只是两遍复杂变换中最终抵消的 chaff material。

真实流程:

0x13cbc 的:

是标准/编译器 runtime 风格的 __clear_cache 实现;作者设计的是动态代码生命周期,不应把这段 cache-maintenance 本身误认成作者手写 anti-debug。

Outer key 的 self-code CRC 只覆盖:

FUN_5fe8 总大小约 908 bytes,而真正 SPECK-like 轮从:

才开始,完全不在 fingerprint 内。

因果实验:

反过来:

更进一步,CRC32 fingerprint 本身可碰撞:

再利用旁边不可达 4-byte slot 做 CRC 补偿,四个 32-byte CRC 全部保持原值;原 ARM64:

所以这层是 anti-analysis tripwire,不是安全意义上的 code integrity。

Inner payload 不再信任 libc:

四类异常分别 XOR:

随后只保留:

四个 basis:

它们在 GF(2) 下独立,因此 16 个 anomaly subset 对应 16 个不同 fp8。

payload return 值始终为:

异常并不靠 return 报错,而是静默污染 payload32。

payload 尾部 48-byte 表:

输出:

clean fp8=0

而第 0 字节的 anomaly delta:

本身就是可逆的;也就是说 payload 32 字节只是在冗余编码 1-byte anti-debug fingerprint。

早期 Frida 输出无法通过 C7。后续知道 maps anomaly 的 fp8 basis 正好是:

因此那次 trace 基本可以判定触发了 payload 内部的 frid/xpos maps 检测。

把全部 256 个理论 fp8 都代入 C7:

所以 payload return 虽永远为 0,但 C7 最终强制:

Inner anti-debug 不存在另一套 hidden flag world。

clean 下可以画成:

真正最终 hard gates:

五个 stack residual 都会 OR 进 master failure;C7 不是“完全无关的 smoke”。

输入被扩为:

clean Java checksum 下:

按 LE64 解释为 (A,B,C,D)

r 轮:

它本质是两条 SPECK128 round shape,再做可逆 cross-coupling。

逆轮:

完整 round 正逆与 XOF expansion 正逆均已做 20 万组随机双向 round-trip,0 mismatch。[ALG]

每 32-byte block 后:

同样是双射。

clean G1 直接固定的是:

不是“三段都直接比较”。

但因为 C/D expansion 可逆,可以从 O[80:96] 向前逆两次:

从而固定 post-12:

原 ARM64 直接得到:

canonical:

这三个字节全部属于由 G1/XOF 逆已经固定的 C/D 窗口,所以在开始解 even 之前:

已经是常量。

甚至可以直接从 payload 前16字节写出 R 的闭式,不需要 A/B。

所以 odd/even 表面上的双向耦合实际上可拓扑排序。

clean C7 可完全化为 8 个 byte residual:

success 要求:

扫描 256 个 seed:

只有两支。[ALG]

存在严格对称:

会保持所有 C7 residual 不变。

所以 C7 天生无法区分 seed bit7;后面的 generated8 / G3 / G4 才会继续破对称。

目标:

首先,入口状态严格化简为:

canonical:

256/256 seed exhaustive 验证。

对每轮 i

关键性质:[ALG]

这种 XOR–ADD–XOR permutation 家族。

因此 target y_i 固定后,给定 state 可以唯一反出 O[i]

进一步看状态依赖:

最终:

两支各唯一。

它只访问 payload[0:15],实际索引集合为 12 个字节;完全不读取 payload[16:31]

因此 payload32 的构造没有 fixed-point 自引用问题。

把 G2 扩到全部 256 个 seed-low 后,generated8 的解数分布为:

总计 248 个 (seed,A) preimage,均值接近 1;分布很接近 256 点伪随机 fixed-point map 的 Poisson(1) 形状。说明 57/d7 各恰好 1 个 A 并不是简单硬编码,而是该 byte-cycle 构造的自然结果。[ALG]

精确依赖分析得到:

w2_model 中 10 套:

全部严格恒等。

全部替换后 10 万组随机 exact 对拍 0 mismatch;同编译条件下主体从约:

再把 eb6c.low8、两段 ripple-carry 改成普通 + 后:

说明函数大半代码量只是视觉噪声。

canonical d7:

G4 使用的 X 由 sp280 经:

组成。

这些奇数乘法和 xor-shift 都可逆,因此固定 (seed,A,context,payload) 时:

是单射,甚至可从 X 直接反回 sp280。

canonical:

100000 组随机 full-width round-trip 零误差。[ALG]

FUN_5998 的后两个参数最终不影响返回,只依赖:

j=0..45 位:

所有下标 mod64。

目标:

每条方程只涉及 2~5 个 B 变量,并且是二次 ANF;46 条覆盖全部 64 bit,primal graph 是单一连通分量,greedy min-fill 宽度约 31。

因此最合适的方法不是把整函数 bit-blast,而是每位枚举局部真值表,生成 blocking clauses。

完整模型数:

为了避免单 solver 累积百万 blocking clause,按少量 B bit 切成 16 个互斥 cube 后可快速并行完整计数。

局部 CNF 的核心并不需要 bit-vector solver:

这样每个 clause 都有直接的局部语义,不会把 46 个小约束 bit-blast 成一个巨大黑盒。

G3 实际接口:

seed high24 不参与;A/B/sp280 全 bit活着。

8 个 InvMix 中:

5 个主体 round,每轮调用 3 次 f32c,domain tags:

f32c 自身也可缩成:

第 5 参数 low8 是真正 domain separator,高24死掉;9 套 AES 恒等和多段 carry 可以删除。

canonical target:

在 d7 的 1,130,274 个 G4 模型里:

就是 canonical:

seed57 的 156,466 个 G4 模型里:

更强的是,即便把 G3 里的 w2 强行固定成 C7 target,不让它读取每个 B 的真实 sp280,G3 仍只有 canonical B 命中,说明其唯一性来自直接 B-path,而不是借 C7 的力。[ALG]

C7 已给两支 (seed,sp280),G2 给每支唯一 A,G4 给 B 候选。

对 d7 的 1,130,274 个 G4 模型,我们完整枚举了三种独立检查:

三者唯一点完全相同:

seed57 三种 hit 都是 0。

所以 canonical odd 有至少三条彼此独立的收束路径。

这也解释了:

FUN_7400 两个 KAT、G5 都是真正 hard gate,但对“求出 odd 解”并不提供必需信息;便宜约束已经唯一化 A/B/C/D。它们更像冗余认证与 anti-analysis 负担。

拿到:

逆 12 轮唯一得到:

因此:

完整 O96:

clean work148 结构可以因式分解为:

四个 selector 恰好就是每个 O[64+r] 的四个 2-bit slice;所以 FUN_7400 看起来有 148-byte “key schedule”,实际大部分只是 O96 的重编码。

master guard 可完全化简为:

0x4c249726 恰好也是一个固定 work word q1.high32

看起来更“干净”的:

会强制:

但我们已经完整扫描其全部 2^32 low half,两个 seed 分支都无法恢复合法 5a*7 padding。

真正 canonical:

所以 deadbeef 不是错误码,而是 intended work key的一部分。

它有:

看起来像 AES/SPN,但 4 个矩阵中:

四个 circulant-style 首行分别为:

canonical 16 轮里,奇异矩阵出现在:

M1/M2 更精确地说是 near-MDS singular

canonical 16 轮中有 8 轮选到 M1/M2。

因此它不是 permutation。

round1 就选 singular matrix。

在 round1 Mix 前,对四列分别 XOR:

四个 t_i 独立,随后逆回前面的 S-box / ShiftRows / round0,即得到:

个不同初始 128-bit input,完整 16 轮输出全部相同。

例如:

都得到:

随机 5000 个 collision-family 成员全部验证成功。[ALG]

所以两个 KAT 更像 lossy keyed verifier,不是 block-cipher KAT。

canonical 两个固定 plaintext:

目标:

它再次扫描同一 5 个 API,并复用 Outer classifier 的同一语义。

对五个 iteration j=0..4,随机 3000 个 prologue 分类:

只是在 FUN6630 中再加 domain salt:

然后把 marker rotate/XOR 到:

线程名还检测:

异常时不会直接 return,而是改 work148。

单 API hook在完整 JNI上同时使:

失败,而 C7/G2/G3/G5 保持不变。

对常见 5 个 API hook + 4 个 thread marker,我们按新的 G1 target反推 C/D,再用固定 canonical A/B 逆12轮,9/9 都无法得到 uniform 7-byte pad。

更强:已完整枚举:

uniform-tail hit:

4/5 个 API 同时异常的全部组合没有穷尽,因此本文不把“所有可能异常世界无解”夸成全局证明。

旧分析把退出条件 17 误叫成 17 轮。

实际:

因此真正是:

外部可变依赖进一步追踪后只有:

所谓 tail_seed16 不是独立输入,而是由 FUN12650 固定块、seed 与 sp280在前置16次 byte generator里派生。

等 G5 执行时:

所以 G5 读到的全部变量已经唯一。

canonical:

对 320 个外部输入 bit做单 bit flip,318/320 会改变最终 low8;它扩散很广,但最后只保留 8 bit,本质是 checksum。

FUN12650 第 0 字节:

加上:

即:

而前面一大段“读取 FUN10598 机器码 + multiply/rotate”的结果随后被 x ^ x 无条件清零,是严格不可观察 chaff。

实际修改那些被读的 FUN10598 字节,G5/JNI完全不变;改真正保留下来的 0x140c:85→84,expected 立即变 d7,JNI失败。[DYNAMIC]

这个 helper 在全 SO 有 8 个 call site,是贯穿多个 gate 的固定常量骨架。

seed 由:

其中:

所以:

连续跑 4 次 Numerical Recipes LCG:

得到:

即:

在 JNI 主 verifier 的 0xb224 这一次调用里,第四个 word 0x480f6151 完全没有后续业务读者;单独改它仍 JNI=1。但 FUN12650 在其它 call site会整16字节消费,因此只能说“该调用点 P3 是死数据”,不能删除 helper 的第4 word。

它被用于:

于是:

拼起来:

f020 本身约 780 bytes,但可继续因式分解:

所以它主要是 target-obfuscation,不是额外独立密码 gate。

置换:

令:

第 i 步:

逆:

所以它没有增加数学难度,只是把连续的 state/config 字段打散到用户输入里。

全局:

四个 helper 都不是“比较失败立刻 reject”,而是:

这是分析 even 的核心。

四条件:

all-pass:

原代码并没有直接写“小常量2/5/0x40”,而是用一组看似代码地址的 rodata差值构造:

是很典型的“伪地址常量”混淆。

canonical 代表:

以:

生成 256-byte xorshift32 stream:

然后:

并检查:

85 26 27 也不是直接硬编码,而是:

all-pass canonical:

C4 + T-prefix 两组线性条件联合 rank=32,因此 canonical all-pass 中 L[9:13] 没有隐藏自由度。[ALG]

如果把所有 13008/1325c branch 都纳入,最终 retained T-table 也不是任意 256-byte 表,而只可能落在 514 张有限集合中:[ALG]

除 identity 外,513 张 retained non-identity table 的前三字节都满足 85 26 27T0=85。这把全局 even 问题从“任意表”缩成有限 table family;真正未完全穷尽的仍是每张表下 joint (D,LCG-seed) 64-bit ARX state。

先无条件:

生成:

32 个 word。

再跑三组 16-round XTEA-like KAT:

三组 exact known-pair target:

若三组不通过:

而不是 reject。

ELF .data 中 D 的初值就是:

并且结构明显是 TEA/XTEA。

13598 又会从 local94[13:17] 覆盖它,所以不能说“前序 hard gate 数学强制 delta=9e3779b9”。

正确表述:

8 lane AVX2 完整扫描:

唯一:

因为 T0=85

0x135f4 的不可达 magic instruction 恰好就是:

这是非常明显的 breadcrumb。[INFER]

固定 seed 扫全部 D:

固定 D 扫全部 seed:

最终 FUN126b8 16-byte gate 对 canonical D 的 2^32 seed 全扫也只有同一个 seed命中。

所以三组 13598 KAT 对 intended 分支求解并非唯一信息源;最终 early gate本身也独立指向同一 64-bit state。

它和 FUN5 用同一个 1000次短循环 + 200ms 阈值模板。

但 slow 时只是:

后续无观察者。

因此:

它扫描 FUN126b8 前:

只判断:

等价于识别:

原始代码 count=0。

canonical raw:

八个 config equation:

所以后六条只是重复确认/代码膨胀。

42 13 37 本身也明显是 breadcrumb。

把扫描区内真实执行:

换成语义相同:

结果:

而把一个永远不执行的 slot 改成 BRK #0

所以它检测的是“有没有软件断点类痕迹”,不是代码完整性。

caller固定输入:

内部先 XOR:

12轮两条 64-bit lane,使用同一 D 与 LCG key schedule;canonical:

这里要特别区分:

两者不是同一个东西。

每轮主要结构:

前 10 轮完成后、进入第 11 轮更新前:

这是唯一有意信息损失层。

canonical T 下 byte map:

256个输入只映到:

fiber:

canonical target 四个 low-byte fiber大小:

因为其它 round 都可逆,因此完整 FUN126b8 对固定 state、固定 target 精确有 8 个 128-bit plaintext preimage

其中 canonical 是 0^128;另一个例子:

也得到:

但 caller 不允许改变 p,所以这些 collision 不产生额外 flag;它们只证明该“大密码核心”本身是 lossy verifier。

Even target先用固定材料经 ec68 得 base,再 edcc(base,R,0xa1)

canonical:

edcc 内部:

0x3d 是奇数,在 mod256 可逆;x->x^(x>>3) 也可逆。

因此 任意一个 target byte 都单独一一编码 R

例如只用首字节:

16-byte target 的信息熵只有 8 bit,但要求 even 输出同时命中 16 个冗余一致字节,作为 equality gate 仍然很强。

KAT target 的 seed_low 也是完全相同的冗余编码:KAT1/KAT2 任意一个 target byte都能独立反出 d7

canonical representative:

即:

FUN126b8

通过 early gate。

seed fold:

得到:

selector=0x1058c 本身是 rodata 硬加载的 intended 常量,并不是 seed fold 方程的数学唯一解。固定 low8=0x8c 后枚举另外16 bit,共有 276 个 selector 也能 fold 到 d7;0x1058c 只是作者选择的一个代表,同时 low8=140 又承担 T-table shift 的职责。

逆 even map:

canonical local94 里,以下位不改变最终 effective state:

原因:

可把整族直接参数化:

因此:

个 local94 全部坍缩到同一个:

同一个 FUN126b8 output、同一个 seed d7。

local94 <-> even25 又是双射,因此正好对应 16384 个不同用户输入。[ALG]

这些输入在 even25 用户视图中被链式映射充分扩散:25个 even byte只有3个始终固定;绝大多数等价输入相对 canonical 会改变 22/25 个 even byte。

存在另一组代表:

它使 C4 fail,selector=0

但 xorshift table seed:

恰好满足:

于是:

第一处分歧恰好:

而 canonical FUN126b8 round10 实际查的四个索引:

最大:

正好全部落在共同前缀里。

因此两张表虽然后半完全不同,early gate看见的4个值完全相同。

这一 decoy family 同样有:

个 raw local94,全部:

但:

而 clean C7 256-seed全扫描只允许 57/d7,因此 e0 必失败。

原 ARM64端到端:

这个结构高度说明作者有意构造了一个“early gate假成功族”。[INFER]

本题 anti-analysis 不是统一 return false,而是不断把执行切到仍然能跑、但输入域无解的世界。

前序便宜 hard gates仍唯一钉死同一个 post-state (A,B,C,D)

逆8轮得到尾7字节:

不全相等。

而 Java residual无论什么值,FUN5输入尾部只能是:

所以 8-round world 对任何 padding byte都无解。[ALG]

256 个理论 fp8 中,C7 只有:

存在任何 (seed,sp280) 解;其它255全部无解。[ALG]

Native故意调用:

"%" 不是合法 JNI field descriptor。

真实 ART:返回 null + pending exception,代码清异常后仍走 clean path。

粗糙 emulator 若错误返回非空 fieldID,会固定把 checksum mirror初态:

最终 residual:

四字节 XOR=5a,因此非零 residual规则产生:

而 canonical post-state逆12轮要求 5a*7,所以该 world 也无解。

假 field 实际“读到什么值”无关;canary只检测非法 descriptor 是否被错误当成有效 field。[DYNAMIC]

后续深入分析可以从 intended odd/even state 不读取现成 target 反生成题目材料。

从 canonical odd:

然后:

因为 generated8只读 payload前16字节:

最后由 C7 residual=0 逐字节反解:

得到完整:

与真实 payload clean output 32/32 字节一致。[ALG]

因此没有 self-reference。

公式中:

是 0..31 的排列;每个 clean output byte 含一个独占 first32 table byte。

所以可以:

实际后16字节:

反解出的前32字节与 payload 完全一致。

ec68 对固定 seed:

(3+7i) mod16 是排列,因此给定 desired target:

再接可逆 edcc

我们从 canonical even/KAT output 反解出的三组 src 与 SO 中 rodata逐字节一致:

所以这些很像作者围绕 intended state 反向生成的 challenge vector,而不是独立秘密。[INFER]


传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!!

收藏
免费 2
打赏
分享
最新回复 (1)
雪    币: 112
活跃值: (9170)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
2
tql
12小时前
0
游客
登录 | 注册 方可回帖
返回