首页
课程
问答
CTF
社区
招聘
峰会
发现
排行榜
知识库
工具下载
看雪20年
看雪商城
证书查询
登录
注册
首页
社区
课程
招聘
发现
问答
CTF
排行榜
知识库
工具下载
峰会
看雪商城
证书查询
社区
CTF对抗
发新帖
1
4
[原创]第六题:酉时·书院迷局 WP
发表于: 2026-8-19 09:05
1500
[原创]第六题:酉时·书院迷局 WP
mxym_
2
2026-8-19 09:05
1500
# 第六题:酉时·书院迷局 > 证据标记: > > - **[STATIC]**:由 ELF / smali / ARM64 机器码直接得到; > - **[DYNAMIC]**:由原 `libkctf.so` 在 clean Unicorn / 真机上的因果实验验证; > - **[ALG]**:由精确模型、代数推导、穷举或模型计数证明; > - **[INFER]**:从结构高度支持的出题意图推断,但没有源码,不能冒充事实。 --- ## 0. 最终结论 APK: ```text SHA256 = 21f932aa6222a37ffc4183861017794c45c3e07a5eb88a7b9050e044256e763f package = com.autorun.kctf arch = arm64-v8a ``` Java 输入要求: ```text 100 个小写十六进制字符 ↓ 50 bytes ``` JNI 将 50 字节按下标奇偶拆成两条 25-byte 通道: ```python even = decoded[0::2] odd = decoded[1::2] ``` 题解中使用的 canonical 接受输入为: ```text a77a7ae3781bff94c8d2945636f87d0c1b41f5b7bb293f8eaa63b6a5e2dff410db4b00c87072533d3d96e80fb7e4345843ad ``` 对应: ```text odd25 = 7ae31b94d256f80c41b7298e63a5df104bc8723d960fe458ad even25 = a77a78ffc894367d1bf5bb3faab6e2f4db0070533de8b73443 ``` 原始 ARM64 JNI 在 clean 环境自然执行: ```text payload key = d23adbe6ea44b7f98bdbd44c0662ab4c payload ret = 0 even early gate = 0 seed_low = d7 final w19 = 0 JNI return = 1 ``` 原 APK 真机也接受该输入。 ### 0.1 一个重要的新结论:checker 并不只接受一个字符串 canonical `local94` 中有 14 个后来被截掉或等价化的自由 bit,因此 canonical effective-state 分支精确对应: ```text 2^14 = 16384 ``` 个不同 100-hex 输入。 例如下面这个和 canonical 肉眼差别很大的输入,也已经在原 ARM64 JNI 上自然 `return=1`: ```text 037a01e3781b999420d239562bf87d0ce74114b7e929ee8e0c6390a5b6df0810014b8ac8bb72533d6896190f76e4b55807ad ``` 所以本文会把上面的题解字符串称为 **canonical accepted input**,而不是数学意义上的唯一 flag。 需要说明边界:目前已经严格证明 canonical effective-state family 恰有 16384 个输入,但**尚未穷尽全部 514 张 possible T-table × 64-bit effective state**,因此不能声称“全程序总接受数恰好就是 16384”。 --- # 1. 入口、ELF 与真实函数边界 Java Activity 只承担三类工作: 1. 从 APK 中定位 `libkctf.so` 并计算 32-byte share; 2. 检查输入恰好为 100 个小写 hex 字符并 decode 成 50 字节; 3. 调用 `nativeProcessInput(byte[50])`,JNI 返回 1 时显示成功。 Native JNI 入口: ```text 0x8260 ``` 最终 odd verifier 被 `.eh_frame` 覆盖为完整范围: ```text 0x8f90 .. 0xe818 ``` Ghidra/JADX 会因为计算跳转、tail block、假 CFG 把它切成多个函数,因此本题必须优先信任: ```text FDE / .eh_frame ARM64 ABI 寄存器 机器码 动态 trace ``` 而不是 decompiler 自动函数边界。 ELF note / comment 还能确认工具链: ```text Android API level = 29 NDK = r27 clang = 18.0.1 LLD = 18.0.1 ``` 没有 `.note.gnu.property` 声明整库 BTI/PAC。`.text` 开头少量 `BTI c` 主要落在 CRT glue,`0x13cbc` 的 BTI 则属于 clear-cache helper;这些应与作者手写 anti-analysis 分开看。 --- # 2. Java 完整性材料:真正绑定了什么 ## 2.1 `.kctfguard` ELF 中: ```text .kctfguard offset = 0x13d60 .kctfguard size = 0x60 CRC32 = 0x3e0695ce ``` `.kctfguard` 的 96 字节看起来像合法 AArch64 小函数,还放了 SHA-256 IV 一类熟悉常量,但它**从不被调用执行**。[STATIC] 唯一相关 relocation 是把: ```text 0x13d60 = guard_start 0x13dc0 = guard_end ``` 作为内存区间给 Native CRC helper。 因此它本质上是一个伪装成代码的 integrity anchor。 Java 对 section header 篡改还有 fallback:如果找不到 `.kctfguard` section,会在 `.text` 中逐字节寻找完整 96-byte guard signature;若 signature 也不存在,则退化为对整个 `.text` 做 CRC32。当前原始 APK 正常直接走 `.kctfguard` 路径。[STATIC] ## 2.2 Java 字符串解码中的假依赖 Java 会解出: ```text "lib/" "/libkctf.so" "arm64-v8a" "armeabi-v7a" "x86_64" "x86" ".kctfguard" ".text" "classes.dex" "META-INF/" "resources.arsc" ``` 但字符串解码函数的第二参数只改变控制状态,不影响真正的输出 byte;所以 `classes.dex / META-INF / resources.arsc` 只参与控制流烟雾,不进入 material hash。[STATIC] 真正的数据锚仍然是选中的 `libkctf.so` 及 `.kctfguard/.text`。 ## 2.3 guard CRC → 16-byte context:不是 KDF,而是 32→128 冗余编码 Java 使用 4 个 64-bit salt: ```text a3f1b28c7d4e5f60 9c8b7a6d5e4f3021 1f2e3d4c5b6a7980 d0e1f2038495a6b7 ``` 以及经典 PCG LCG: ```text M = 0x5851f42d4c957f2d INC = 0x14057b7ef767814f ``` 每个 dword: ```python y_i = ((crc ^ salt_i) * M + INC) mod 2^64 ctx[4*i:4*i+4] = BE32(y_i & 0xffffffff) ``` 代入: ```text crc = 3e0695ce ``` 得到: ```text 870573e5 f5c63d52 862dbd05 ab3d9494 ``` 即: ```text context16 = 870573e5f5c63d52862dbd05ab3d9494 ``` 这 16 字节只有 32 bit 真正自由度。因为: ```text M.low32 = 0x4c957f2d inverse mod 2^32 = 0x329e28a5 ``` 任取 context 的一个 4-byte word,都能独立反解回同一个: ```text CRC32 = 0x3e0695ce ``` 所以这不是 128-bit KDF,而是 CRC32 的四路 redundant commitment。[ALG] Java 在跨 JNI 前再 XOR: ```text mask16 = 6db5d9492d0561c1ad8599c96d153151 ``` 因此 Java share 前 16 字节为: ```text eab0aaacd8c35c932ba824ccc628a5c5 ``` 完整 32-byte share: ```text eab0aaacd8c35c932ba824ccc628a5c5ce95063e603d010060000000c34224ac ``` 布局: ```text 00..0f masked context16 10..13 CRC32 = 3e0695ce 14..17 file offset= 00013d60 18..1b size = 00000060 1c..1f d(...) = ac2442c3 ``` ## 2.4 Java `d()`:必须按 smali 恢复,JADX 结构化错了 JADX 会把一个关键 `goto` 结构化错,导致看起来 `'I'` 分支永远不会真正保留。 按 smali 恢复后,状态机可写成: ```python state = 0x12 v = 0x5eed4a71 ^ 0x6b02c3a5 mix = crc ^ ROL32(offset + 0x27d4eb2f, 7) ^ ROR32(0xa0761d64 ^ size, 3) i = 0 while True: if state == 0x12: v ^= mix state = 0x34 elif state == 0x34: if i >= 16: state = 0x5d else: x = share[i] v = ROL32( 0x045d9f3b*i + ((v ^ (x << ((i&3)*8))) + 0x9e3779b9), (x & 7)+3 ) i += 1 state = 0x49 if ((v ^ i) & 3) == 0 else 0x34 elif state == 0x49: v ^= ROL32(0x5eed4a71 + i + 0x165667b1, i & 15) state = 0x34 else: x = ((v ^ (v>>16)) * 0x7feb352d) & 0xffffffff x = ((x ^ (x>>15)) * 0x846ca68b) & 0xffffffff return (x ^ (x>>16)) & 0xffffffff ``` canonical 结果: ```text d = ac2442c3 ``` Native 会逐语义重跑同一个状态机;canonical + 20 组随机 share 做过对拍,21/21 完全一致。[DYNAMIC] --- # 3. 完整性层的真实强度 ## 3.1 offset 并没有和真实 guard 地址绑定 Native 内存 guard helper 接收 Java share 的: ```text offset=13d60 size=60 ``` 但实际只检查: ```text size == guard_end - guard_start ``` 并对编译时固定的 `[guard_start,guard_end)` 做 CRC;`offset` 没有拿去比较真实内存地址。 因此只改 offset、同步重算 `d()`,原 JNI 仍可 `return=1`。[DYNAMIC] ## 3.2 CRC/size mismatch 不是 reject,而是 context poison 任意非零 guard mismatch 经 `e960` 压成全 1 mask,再把: ```text b979379e b979379e b979379e b979379e ``` XOR 到 context。 `b979379e` 正是 `0x9e3779b9` 的 little-endian 字节。 这不是不可逆 reject,而是可预测 fault transform。 如果能直接控制 Java share,可以先把 context 预补偿成: ```text canonical_ctx XOR b979379e*4 = 3e7c447b4cbf0acc3f548a9b1244a30a ``` 让 Native poison 后重新恢复 canonical context,原 flag 仍可通过。[DYNAMIC] 但该 128-bit 预补偿 context 不属于正常 Java `CRC32→context` 的 32-bit image,所以**只改 guard bytes、不 patch Java share 逻辑并不能得到它**。[ALG] ## 3.3 `.kctfguard` 只认证 CRC32,不认证内容 我们实际构造了另一份 96-byte guard:内容不同,但通过 32×32 GF(2) CRC 补偿保持: ```text CRC32 = 3e0695ce ``` 原 Java share、原 flag 不改: ```text Native memory CRC = 3e0695ce context = canonical payload key = canonical JNI return = 1 ``` 所以 `.kctfguard` 是 CRC tripwire,不是密码学内容认证。[ALG+DYNAMIC] ## 3.4 Java `d()` 的 32-bit residual 实际只剩 8-bit效果 Native 将 Java checksum mismatch 写入全局 `0x16370`;业务层只有 `FUN_5fe8` 读取它。 clean: ```text residual = 0 -> pad = 0x5a ``` 非零时: ```python pad = ~(r0 ^ r1 ^ r2 ^ r3) & 0xff ``` 因此除了 residual=0 外,只要四字节 XOR=`0xa5`,也会产生同样的 `0x5a` padding。 这样的非零 residual 有 `2^24` 个。 实际把 residual 改成: ```text 000000a5 a5000000 ``` 原 ARM64 仍自然 `JNI=1`。[DYNAMIC] --- # 4. Native 混淆 helper:大量代码只是语义膨胀 精确验证后的基础 helper: | 地址 | 净语义 | | --------- | ---------------------------------- | | `0x2cbc` | 恒 1 | | `0x2d70` | 恒 0 | | `0x2e2c` | 恒 0 | | `0x10800` | 恒 0 | | `0x6374` | XOR64 | | `0x6614` | ADD64 mod 2^64 | | `0xe818` | XOR32 | | `0xe960` | `0xffffffff if x!=0 else 0` | | `0xee90` | ADD32 | | `0xf004` | SUB32 | | `0x10414` | `(x >> (8*(i&3))) & 0xff` | | `0xe9f4` | AES InvMixColumns(word) | | `0x12448` | 返回 param1 low32,param2 完全抵消 | | `0x10598` | 恒 0,且没有业务调用 | 早期为了保守起见,`2cbc/2d70/2e2c/10800` 的全局写入被保留;深入分析后可以更强地证明: ```text 0x16360 / 0x16364 ``` 只被这组 opaque helper 自己读写,对外返回仍恒 `1/0/0/0`,没有其它业务观察者。 所以整个 opaque state subsystem 可以整体删除。[STATIC+DYNAMIC] 按原 FDE 统计,仅这些已完全塌缩 helper 就超过: ```text 4628 bytes ``` 约占 `.text` 的 6.5%;还没算 w2/f32c/G3 中后来消掉的大量 Mix→InvMix 与 carry 电路。 --- # 5. clean Unicorn 环境:一个关键纠错 曾把 PLT `0x13f00` 标成 `sysconf`,这是错误的。 ELF `.rela.plt` 明确证明: ```text 0x13f00 = getauxval@plt ``` 两个调用点都: ```asm mov w0, #16 bl 0x13f00 ``` 也就是: ```text getauxval(AT_HWCAP) ``` checker只关心: ```text HWCAP & 3 == 3 ``` AArch64 clean 设备低两位对应 FP / ASIMD,正常应满足。 旧 harness 默认 `getauxval=0`,所以稳定进入诱饵环境,loader key 变成: ```text ee7d15e448dbc967cb9c954e64ec75da ``` 修正: ```text getauxval(AT_HWCAP) -> 3 # 至少 low2=11b ``` 后,loader 自然得到: ```text d23adbe6ea44b7f98bdbd44c0662ab4c ``` 此外 `dlsym("mprotect")` 要返回一个成功的权限切换 stub。只补这两类真实环境语义,不注入任何算法中间值、不手改 PC,原 ARM64 JNI 即可完整 `return=1`。[DYNAMIC] --- # 6. Outer payload loader:从上千行化成 6-bit环境 + CRC + XOR ## 6.1 动态解析表 `FUN_298c(0..9)`: ```text 0 clock_gettime 1 fopen 2 fgets 3 fclose 4 open 5 read 6 close 7 mprotect 8 mmap 9 munmap ``` Outer loader 扫描: ```text clock_gettime / open / read / mprotect / mmap ``` 前 6 条 AArch64 指令。 ## 6.2 同一个 prologue classifier 的纯闭式 百余条 `cmp/csel/ccmp` 最终只是在生成 32-bit marker: ```python raw = 0 for p in range(6): if insn[p] is B/BL immediate: raw ^= 0x01010101 << (p & 3) if insn[p] is BRK: raw ^= 0x3d5a7c10 ^ (p ^ 1) for p in range(5): if insn[p].top_byte == 0x58 and insn[p+1] is BR/BLR-style: raw ^= 0x6b2d41ea + p ``` 对 5 类语义代表的全部 `5^6=15625` 序列,以及 10000 组任意 32-bit 指令字,纯公式与原 ARM64 classifier **0 mismatch**。[ALG] 总 marker 空间只有: ```text 375 ``` 种。 ## 6.3 Outer 环境只有 6 bit 环境位: ```text bit0 : HWCAP bad bit1..5 : 五个 API prologue suspicious ``` 异常 XOR 常量: ```text HWCAP 31415926 clock c00d6f7c open 0e3b0c54 read 50ebd4a6 mprotect a6c1f01c mmap 4c4c4051 ``` 64 个组合得到 64 把互不相同 runtime key。 ## 6.4 runtime key 可完全静态复算 context 先过一个 rank=128 的线性 byte-rotate 编码: ```python for i in range(8): L[i] = ctx[i] ^ ROL8(ctx[i+3],3) ^ ROL8(ctx[i+7],5) for i in range(8,16): L[i] = ctx[i] ^ ROL8(ctx[i-3],2) ^ ROL8(ctx[(i+3)&15],6) ``` canonical: ```text L(ctx)=e27ae0bbc787f32ddc33bb3a3a2ac1e6 ``` 再对 `FUN_5fe8` 开头四个 32-byte block 各做标准 CRC32: ```python K = [ 0xa618ae7d, 0x1672e4eb, 0x920a56a8, 0xd3d5c192, ] word[i] = CRC32(code[32*i:32*i+32]) ^ K[i] ^ LE32(L(ctx)[4*i:4*i+4]) ``` 得到: ```text code_mask = a8a3f7cc6eae6eaa21c9dd7c9f159a9a ``` clean 6-bit env expansion: ```text 7a992c2a84ead953aa120930997731d6 ``` 最终: ```text runtime_key = code_mask XOR env_expansion = d23adbe6ea44b7f98bdbd44c0662ab4c ``` 64 种环境全部和原 ARM64 对拍一致。 ## 6.5 所谓“两遍复杂解密”最终只是 repeating-key XOR 对完整 `0x4c0` payload blob 逐字节验证: ```python payload[i] = encrypted_blob[i] ^ runtime_key[i & 15] ``` 那把曾经看起来很重要的 global static key: ```text a37b4f1de85692c43d0ef168b527da8c ``` 在 clean/bad env 下都对 runtime key 和最终 payload 不产生可观察影响;改成全零、全 `ff`、随机值,最终代码页逐字节不变。[DYNAMIC] 它只是两遍复杂变换中最终抵消的 chaff material。 ## 6.6 payload 代码页生命周期 真实流程: ```text mmap RW → 写解密代码 → cache synchronization → mprotect RX → call payload → mprotect/清零 → munmap ``` `0x13cbc` 的: ```text BTI c CTR_EL0 dc cvau dsb ish ic ivau isb ``` 是标准/编译器 runtime 风格的 `__clear_cache` 实现;作者设计的是动态代码生命周期,不应把这段 cache-maintenance 本身误认成作者手写 anti-debug。 --- # 7. self-code fingerprint:是 tripwire,不是核心代码完整性 Outer key 的 self-code CRC 只覆盖: ```text FUN_5fe8[0:128] ``` `FUN_5fe8` 总大小约 908 bytes,而真正 SPECK-like 轮从: ```text 0x61e4 ``` 才开始,完全不在 fingerprint 内。 因果实验: ```text 把真实 round 的 ROR64(a,8) 改为 ROR64(a,9) → payload runtime key 完全不变 → odd算法当然失败 ``` 反过来: ```text 只把 fingerprint 范围内、正常路径永远不会执行的 deadbeef slot 改成 NOP → code CRC 变化 → runtime key变化 → payload损坏 ``` 更进一步,CRC32 fingerprint 本身可碰撞: ```text 真实执行 ldrb w8,[x0] → 换成语义等价 ldurb w8,[x0,#0] ``` 再利用旁边不可达 4-byte slot 做 CRC 补偿,四个 32-byte CRC 全部保持原值;原 ARM64: ```text runtime key unchanged R=88 early=0 final=0 JNI=1 ``` 所以这层是 **anti-analysis tripwire**,不是安全意义上的 code integrity。 --- # 8. Inner payload:第二层独立 anti-debug Inner payload 不再信任 libc: - `/proc/self/status`:直接 syscall,解析 `TracerPid`; - `/proc/self/maps`:直接 syscall,滑窗搜 `frid` / `xpos`; - `/proc/self/auxv`:直接解析 `(type,value)` 找 `AT_HWCAP=16`; - 只检查传入 `mmap` pointer 的前几条机器码。 四类异常分别 XOR: ```text mmap prologue suspicious b701c503 TracerPid != 0 713cb00d maps contains frid/xpos d41df12a HWCAP bad 4863a11d ``` 随后只保留: ```python fp8 = (w25 ^ (w25>>11) ^ (w25>>21)) & 0xff ``` 四个 basis: ```text mmap -> 83 Tracer-> 12 maps -> 34 HWCAP -> 2a ``` 它们在 GF(2) 下独立,因此 16 个 anomaly subset 对应 16 个不同 fp8。 payload return 值始终为: ```text 0 ``` 异常并不靠 return 报错,而是静默污染 payload32。 ## 8.1 payload32 闭式 payload 尾部 48-byte 表: ```text ebfaaf15315348c875c0807cfec9ca3e bac04d2ea70eed66036be5ee7e5e2992 53d826ceacc3e6bbeac35cbd9db9a26b ``` 输出: ```python for i in range(32): a = TAB[(5*i+11)&31] b = TAB[32+((7*i+5)&15)] c = TAB[32+((11*i+9)&15)] base = a ^ b ^ ROL8(c,(i&7)+1) \ ^ (((0x3d*i+0xa7) ^ (2*i)) & 0xff) t = fp8 * (i+1) perturb = (t ^ (t>>4)) & 0xff out[i] = base ^ perturb ``` clean `fp8=0`: ```text payload32 = 9f73be24a1dd6c96b90723bba7cdfdc9 fd521311dd1b172572014429f93dd77c ``` 而第 0 字节的 anomaly delta: ```text fp8 ^ (fp8>>4) ``` 本身就是可逆的;也就是说 payload 32 字节只是在冗余编码 1-byte anti-debug fingerprint。 ## 8.2 早期 Frida 抓到的 `0x34` 可以被精确解释 早期 Frida 输出无法通过 C7。后续知道 `maps` anomaly 的 fp8 basis 正好是: ```text 0x34 ``` 因此那次 trace 基本可以判定触发了 payload 内部的 `frid/xpos` maps 检测。 ## 8.3 C7 把 inner anti-debug 重新变成 hard gate 把全部 256 个理论 fp8 都代入 C7: ```text 有任意 seed/sp280 解的 fp8 数量 = 1 唯一 = 00 ``` 所以 payload return 虽永远为 0,但 C7 最终强制: ```text fp8 == 0 ``` Inner anti-debug 不存在另一套 hidden flag world。 --- # 9. verifier 总体 DAG clean 下可以画成: ```text .kctfguard CRC └─ context16 ├─ payload runtime key ├─ even target / local checks ├─ generated8 / w2 / G3 / C7 └─ KAT target generation odd25 || pad*7 └─ FUN_5fe8 12 rounds + XOF -> O[0:96] ├─ O[80:96] -> G1 │ └─ 派生 bridge R -> even target16 ├─ A=O[0:8] -> generated8 -> G2 ├─ B=O[8:16] -> w2/sp280 -> X -> G4 │ └──────> G3 ├─ seed+sp280+payload+ctx -> C7 └─ O/work148 -> FUN7400 KAT1/KAT2 even25 └─ reversible map -> local94 └─ 13008 -> 1325c -> 13598 -> 13878 └─ T,D,A[32],rot,width,mask,selector ├─ FUN126b8(0^128) -> compare target16(R) └─ seed fold -> seed_low -> odd verifier ``` 真正最终 hard gates: ```text sp332 : G1 sp328 : generated8 G2 sp324 : C7 sp320 : FUN7400 KAT1 sp316 : FUN7400 KAT2 G3 : sp96 == 92b28e10 G4 : 46-bit == 16eff8c39c23 G5 : low8 == d6 payload return == 0 ``` 五个 stack residual 都会 OR 进 master failure;C7 不是“完全无关的 smoke”。 --- # 10. odd 核心 `FUN_5fe8`:双 SPECK-like lane + cross coupling 输入被扩为: ```text odd25 || pad*7 ``` clean Java checksum 下: ```text pad = 0x5a ``` 按 LE64 解释为 `(A,B,C,D)`。 第 `r` 轮: ```text A1 = (ROR64(A,8) + B) ^ r B1 = ROL64(B,3) ^ A1 C1 = (ROR64(C,8) + D) ^ (r+4) D1 = ROL64(D,3) ^ C1 A' = A1 ^ D1 B' = B1 C' = C1 ^ B1 D' = D1 ``` 它本质是两条 SPECK128 round shape,再做可逆 cross-coupling。 逆轮: ```text A1 = A' ^ D' B1 = B' C1 = C' ^ B' D1 = D' B = ROR64(B1 ^ A1,3) A = ROL64(((A1 ^ r) - B),8) D = ROR64(D1 ^ C1,3) C = ROL64(((C1 ^ (r+4)) - D),8) ``` 完整 round 正逆与 XOF expansion 正逆均已做 20 万组随机双向 round-trip,0 mismatch。[ALG] ## 10.1 XOF expansion 每 32-byte block 后: ```text A = A + C B = B ^ D C = ROR64(C,47) D = ROR64(D,11) ``` 同样是双射。 ## 10.2 payload 只直接固定一个 16-byte XOF窗口 clean G1 直接固定的是: ```text O[80:96] = payload[0:16] = 9f73be24a1dd6c96b90723bba7cdfdc9 ``` 不是“三段都直接比较”。 但因为 C/D expansion 可逆,可以从 `O[80:96]` 向前逆两次: ```text O[48:64] = 5f92d06e36cbcf394fce3d18d93d6dee O[16:32] = 68379be5e79c2f49737f72eec1c8ee69 ``` 从而固定 post-12: ```text C = 0x492f9ce7e59b3768 D = 0x69eec8c1ee727f73 ``` --- # 11. odd → even bridge:看似反馈环,实际已断开 原 ARM64 直接得到: ```text R = O[60] ^ O[80] ^ ROL8(O[95],1) ^ 0x5d ``` canonical: ```text O60=d9 O80=9f O95=c9 ROL8(c9,1)=93 R=d9^9f^93^5d=88 ``` 这三个字节全部属于由 G1/XOF 逆已经固定的 C/D 窗口,所以在开始解 even 之前: ```text R = 0x88 ``` 已经是常量。 甚至可以直接从 payload 前16字节写出 R 的闭式,不需要 A/B。 所以 odd/even 表面上的双向耦合实际上可拓扑排序。 --- # 12. C7:同时检查 inner payload、seed 与 sp280 clean C7 可完全化为 8 个 byte residual: ```python r_i = ( payload[i] ^ payload[8+i] ^ payload[24+i] ^ context[5+i] ^ ((0xc3 + 41*i) & 0xff) ^ ((seed_low + 23*i) & 0xff) ^ byte(sp280, i & 3) ) ``` success 要求: ```text r_0...r_7 全部为0 ``` 扫描 256 个 seed: ```text seed 57 -> sp280 = 921bca06 seed d7 -> sp280 = 129b4a86 ``` 只有两支。[ALG] 存在严格对称: ```text seed' = seed ^ 0x80 sp280' = sp280 ^ 0x80808080 ``` 会保持所有 C7 residual 不变。 所以 C7 天生无法区分 seed bit7;后面的 generated8 / G3 / G4 才会继续破对称。 --- # 13. generated8:几千条指令,实际是 byte quasigroup + 4-cycle 目标: ```text payload[16:24] = fd521311dd1b1725 ``` 首先,入口状态严格化简为: ```text state0 = seed_low ^ context[2] ``` canonical: ```text 57 -> 24 d7 -> a4 ``` 256/256 seed exhaustive 验证。 对每轮 `i`: ```text y_i = F_i(state_i, O[i]) state_{i+1} = y_i ^ ((O[i+3] + 0x5a + i) & 0xff) ``` 关键性质:[ALG] 1. 对任意 round/state,`O[i] -> y_i` 是完整 256 元置换; 2. `y_i` 与 `O[i+3]` 独立; 3. 固定 state 与 `O[i]` 后,`O[i+3] -> next_state` 也是置换; 4. 2048 个 state-row 全属于: ```text y = ((x XOR q) + c) mod256 XOR k ``` 这种 XOR–ADD–XOR permutation 家族。 因此 target `y_i` 固定后,给定 state 可以唯一反出 `O[i]`。 进一步看状态依赖: - `state0` 已知后,偶数状态链直接唯一; - 真正需要搜索的只有奇数 `state1/3/5/7` 的 4-cycle fixed point; - 只需扫 256 个 odd-state 候选,而不是联立 8 个未知字节。 最终: ```text seed57: A bytes = ce0c6c2bc22ebede A = 0xdebe2ec22b6c0cce seedd7: A bytes = c1914230477ab658 A = 0x58b67a47304291c1 ``` 两支各唯一。 ### 13.1 generated8 不读 payload 后16字节 它只访问 `payload[0:15]`,实际索引集合为 12 个字节;完全不读取 `payload[16:31]`。 因此 payload32 的构造没有 fixed-point 自引用问题。 把 G2 扩到全部 256 个 seed-low 后,generated8 的解数分布为: ```text 0 个 A : 96 seeds 1 个 A : 90 seeds 2 个 A : 54 seeds 3 个 A : 15 seeds 5 个 A : 1 seed ``` 总计 248 个 `(seed,A)` preimage,均值接近 1;分布很接近 256 点伪随机 fixed-point map 的 Poisson(1) 形状。说明 `57/d7` 各恰好 1 个 A 并不是简单硬编码,而是该 byte-cycle 构造的自然结果。[ALG] --- # 14. sp280 / w2:真实接口比 decompile 小很多 精确依赖分析得到: ```text sp280 = W(seed8, A_low32, B64) ``` - seed high24 完全死; - A high32 完全死; - B 64 bit 全部参与。 `w2_model` 中 10 套: ```text 手写 AES MixColumns -> e9f4(InvMixColumns) ``` 全部严格恒等。 全部替换后 10 万组随机 exact 对拍 0 mismatch;同编译条件下主体从约: ```text 1600 instructions -> 891 ``` 再把 `eb6c.low8`、两段 ripple-carry 改成普通 `+` 后: ```text ~675 instructions ``` 说明函数大半代码量只是视觉噪声。 canonical d7: ```text sp280 = 0x129b4a86 ``` --- # 15. sp280 → X:64-bit 看似 hash,实际是可逆包装 G4 使用的 `X` 由 sp280 经: - xxHash-style `0x9e3779b185ebca87`; - MurmurHash3 `fmix64` 常量: ```text ff51afd7ed558ccd c4ceb9fe1a85ec53 ``` 组成。 这些奇数乘法和 xor-shift 都可逆,因此固定 `(seed,A,context,payload)` 时: ```text sp280 -> X ``` 是单射,甚至可从 X 直接反回 sp280。 canonical: ```text sp280=129b4a86 X=996bd2f8c1b7dd80 ``` 100000 组随机 full-width round-trip 零误差。[ALG] --- # 16. G4 / FUN_5998:46 条局部二次布尔式 `FUN_5998` 的后两个参数最终不影响返回,只依赖: ```text B64, X64 ``` 第 `j=0..45` 位: ```text p = j & 1 a = p ^ B[7j+3] ^ B[11j+19] ^ X[5j+1] m = 1^p ^ B[17j+5] ^ X[9j+13] q = 1^p ^ X[27j+31] ^ B[23j+29] ^ B[25j+8] Y[j] = 1 ^ a ^ (m & q) ``` 所有下标 mod64。 目标: ```text Y = 0x16eff8c39c23 ``` 每条方程只涉及 2~5 个 B 变量,并且是二次 ANF;46 条覆盖全部 64 bit,primal graph 是单一连通分量,greedy min-fill 宽度约 31。 因此最合适的方法不是把整函数 bit-blast,而是每位枚举局部真值表,生成 blocking clauses。 完整模型数: ```text seed57 : 156,466 seedd7 : 1,130,274 ``` 为了避免单 solver 累积百万 blocking clause,按少量 B bit 切成 16 个互斥 cube 后可快速并行完整计数。 局部 CNF 的核心并不需要 bit-vector solver: ```python def output_bit(j, B, X): bit = lambda v, i: (v >> (i & 63)) & 1 p = j & 1 a = p ^ bit(B,7*j+3) ^ bit(B,11*j+19) ^ bit(X,5*j+1) m = 1^p ^ bit(B,17*j+5) ^ bit(X,9*j+13) q = 1^p ^ bit(X,27*j+31) ^ bit(B,23*j+29) ^ bit(B,25*j+8) return 1 ^ a ^ (m & q) clauses = [] for j in range(46): ids = sorted({ (7*j+3)&63, (11*j+19)&63, (17*j+5)&63, (23*j+29)&63, (25*j+8)&63, }) want = (0x16eff8c39c23 >> j) & 1 for vals in itertools.product((0,1), repeat=len(ids)): B_local = sum(v << i for i,v in zip(ids,vals)) if output_bit(j,B_local,X) != want: clauses.append([-(i+1) if v else i+1 for i,v in zip(ids,vals)]) ``` 这样每个 clause 都有直接的局部语义,不会把 46 个小约束 bit-blast 成一个巨大黑盒。 --- # 17. G3:另一个独立 32-bit 唯一化 checksum G3 实际接口: ```text G3(seed8, A64, B64, sp28032) -> u32 ``` seed high24 不参与;A/B/sp280 全 bit活着。 8 个 InvMix 中: ```text 7 个 = Mix -> InvMix 恒等 1 个是真正的数据变换 ``` 5 个主体 round,每轮调用 3 次 f32c,domain tags: ```text (r, r+3, r+7) ``` `f32c` 自身也可缩成: ```text f32c(u32,u32,u32,u32,u8) -> u32 ``` 第 5 参数 low8 是真正 domain separator,高24死掉;9 套 AES 恒等和多段 carry 可以删除。 canonical target: ```text sp96 = 0x92b28e10 ``` 在 d7 的 1,130,274 个 G4 模型里: ```text G3 hit = 1 ``` 就是 canonical: ```text B = 9eb09ed6e443b907 ``` seed57 的 156,466 个 G4 模型里: ```text G3 hit = 0 ``` 更强的是,即便把 G3 里的 w2 强行固定成 C7 target,不让它读取每个 B 的真实 sp280,G3 仍只有 canonical B 命中,说明其唯一性来自直接 B-path,而不是借 C7 的力。[ALG] --- # 18. odd 最小求解链:不需要先逆所有大函数 C7 已给两支 `(seed,sp280)`,G2 给每支唯一 A,G4 给 B 候选。 对 d7 的 1,130,274 个 G4 模型,我们完整枚举了三种独立检查: ```text actual w2(B) == 129b4a86 : 1 hit G3 == 92b28e10 : 1 hit inverse FUN5 tail == 5a*7: 1 hit ``` 三者唯一点完全相同: ```text B=9eb09ed6e443b907 ``` seed57 三种 hit 都是 0。 所以 canonical odd 有至少三条彼此独立的收束路径。 这也解释了: > `FUN_7400` 两个 KAT、G5 都是真正 hard gate,但对“求出 odd 解”并不提供必需信息;便宜约束已经唯一化 A/B/C/D。它们更像冗余认证与 anti-analysis 负担。 拿到: ```text A=58b67a47304291c1 B=9eb09ed6e443b907 C=492f9ce7e59b3768 D=69eec8c1ee727f73 ``` 逆 12 轮唯一得到: ```text raw32 = 7ae31b94d256f80c41b7298e63a5df104bc8723d960fe458ad5a5a5a5a5a5a5a ``` 因此: ```text odd25 = 7ae31b94d256f80c41b7298e63a5df104bc8723d960fe458ad ``` 完整 O96: ```text c1914230477ab65807b943e4d69eb09e 68379be5e79c2f49737f72eec1c8ee69 29c9dd152f17e6a174c6310a17565ef7 5f92d06e36cbcf394fce3d18d93d6dee 885bae8465e2b5db3b080c12ce6b3319 9f73be24a1dd6c96b90723bba7cdfdc9 ``` --- # 19. `FUN_6630` 与 work148 clean work148 结构可以因式分解为: ```text work[0:64] = O[0:64] XOR periodic F16-derived block work[64:128] = O[64:80] 每字节拆成四个2-bit selector work[128:144]= O[80:96] work[144:148]= master32 ``` 四个 selector 恰好就是每个 `O[64+r]` 的四个 2-bit slice;所以 `FUN_7400` 看起来有 148-byte “key schedule”,实际大部分只是 O96 的重编码。 ## 19.1 master guard 的 decoy branch master guard 可完全化简为: ```text z = B.high32 ^ 0x4c249726 ``` 而 `0x4c249726` 恰好也是一个固定 work word `q1.high32`。 看起来更“干净”的: ```text z == 0 ``` 会强制: ```text B.high32 = 4c249726 ``` 但我们已经完整扫描其全部 `2^32` low half,两个 seed 分支都无法恢复合法 `5a*7` padding。 真正 canonical: ```text z = d29409f0 != 0 master = m0 ^ deadbeef = 863535c1 ``` 所以 `deadbeef` 不是错误码,而是 intended work key的一部分。 --- # 20. `FUN_7400`:不是 block cipher,而是故意有损的 128-bit checker 它有: - 4 个 Fisher–Yates 256-byte S-box permutation; - 4 种 row shift; - 4 个 GF(2^8) 4×4 矩阵; - bytewise `x^e`,e=`7,11,13,23`; - round key XOR; - round8 还使用标准 CRC32 nibble table。 看起来像 AES/SPN,但 4 个矩阵中: ```text M0: rank4, MDS,标准 AES MixColumns M3: rank4, MDS M1: rank3 M2: rank3 ``` 四个 circulant-style 首行分别为: ```text M0: 02 03 01 01 M1: 05 03 04 02 M2: 07 06 02 03 M3: 09 0e 05 04 ``` canonical 16 轮里,奇异矩阵出现在: ```text 1,2,4,5,7,11,13,15 ``` M1/M2 更精确地说是 **near-MDS singular**: - 所有 1×1 / 2×2 / 3×3 minors 非零; - 只有 4×4 determinant 为 0; - kernel 恰好: ```text (t,t,t,t), t ∈ GF(256) ``` canonical 16 轮中有 8 轮选到 M1/M2。 因此它不是 permutation。 ## 20.1 显式构造 `2^32` collision family round1 就选 singular matrix。 在 round1 Mix 前,对四列分别 XOR: ```text (t0,t0,t0,t0) (t1,t1,t1,t1) (t2,t2,t2,t2) (t3,t3,t3,t3) ``` 四个 `t_i` 独立,随后逆回前面的 S-box / ShiftRows / round0,即得到: ```text GF(256)^4 = 2^32 ``` 个不同初始 128-bit input,完整 16 轮输出全部相同。 例如: ```text 0123456789abcdeffedcba9876543210 886835449519fd2f799b0e871784893d 969184bed407dbb6df5131de09eb38b1 ``` 都得到: ```text 56958ed41b25a07ec6428aeea8850e9b ``` 随机 5000 个 collision-family 成员全部验证成功。[ALG] 所以两个 KAT 更像 lossy keyed verifier,不是 block-cipher KAT。 canonical 两个固定 plaintext: ```text 0123456789abcdeffedcba9876543210 a55ac33cf00f6996123456789abcdef0 ``` 目标: ```text 56958ed41b25a07ec6428aeea8850e9b cd8e054a35909d3c0b21036c41364635 ``` --- # 21. `FUN_6630` 第三层 anti-instrumentation 它再次扫描同一 5 个 API,并复用 Outer classifier 的同一语义。 对五个 iteration `j=0..4`,随机 3000 个 prologue 分类: ```text Outer vs FUN6630 suspicious predicate 3000 / 3000 完全一致 ``` 只是在 FUN6630 中再加 domain salt: ```text poison_6630(j) = poison_outer(j) ^ (j * 0x045d9f3b) ``` 然后把 marker rotate/XOR 到: - round-key 区; - S-box seed 区; - master。 线程名还检测: ```text gum-js-loop gmain gdbus frida ``` 异常时不会直接 return,而是改 work148。 单 API hook在完整 JNI上同时使: ```text G1 KAT1 KAT2 ``` 失败,而 C7/G2/G3/G5 保持不变。 对常见 5 个 API hook + 4 个 thread marker,我们按新的 G1 target反推 C/D,再用固定 canonical A/B 逆12轮,9/9 都无法得到 uniform 7-byte pad。 更强:已完整枚举: ```text <=3 个 API 同时异常 + 任意 4-bit thread parity = 8,392,589,935 个非clean环境组合(约 83.93 亿) ``` uniform-tail hit: ```text 0 ``` 4/5 个 API 同时异常的全部组合没有穷尽,因此本文不把“所有可能异常世界无解”夸成全局证明。 --- # 22. G5:不是“17轮”,而是初始化 + 16-round tail checksum 旧分析把退出条件 `17` 误叫成 17 轮。 实际: ```text counter 从1开始 重复体执行 1..16 到17退出 ``` 因此真正是: ```text 一次初始化 + 16-round tail compressor ``` 外部可变依赖进一步追踪后只有: ```text seed_low sp280 O[7:32] ``` 所谓 `tail_seed16` 不是独立输入,而是由 `FUN12650` 固定块、seed 与 sp280在前置16次 byte generator里派生。 等 G5 执行时: - G1 已固定 C/D; - G2 已固定 A; - C7/G3/G4 已固定 B、seed、sp280; 所以 G5 读到的全部变量已经唯一。 canonical: ```text G5 low8 = d6 ``` 对 320 个外部输入 bit做单 bit flip,318/320 会改变最终 low8;它扩散很广,但最后只保留 8 bit,本质是 checksum。 ## 22.1 G5 target `d6` 也被复杂常量隐藏 `FUN12650` 第 0 字节: ```text c8 ``` 加上: ```text rodata[0x140c] = 85 fold8(0x424970e0) = 9b ``` 即: ```text c8 ^ 85 ^ 9b = d6 ``` 而前面一大段“读取 FUN10598 机器码 + multiply/rotate”的结果随后被 `x ^ x` 无条件清零,是严格不可观察 chaff。 实际修改那些被读的 `FUN10598` 字节,G5/JNI完全不变;改真正保留下来的 `0x140c:85→84`,expected 立即变 `d7`,JNI失败。[DYNAMIC] --- # 23. Shared constant oracle `FUN_12650` 这个 helper 在全 SO 有 8 个 call site,是贯穿多个 gate 的固定常量骨架。 seed 由: ```text 26f3b5e9 ^ 5ec1e380 ^ (LE32(KAT1_plain) ^ LE32(KAT2_plain)) ``` 其中: ```text 67452301 ^ 3cc35aa5 = 5b8679a4 ``` 所以: ```text seed0 = 23b42fcd ``` 连续跑 4 次 Numerical Recipes LCG: ```text x = x * 0x19660d + 0x3c6ef35f ``` 得到: ```text 82a60ec8 7a496387 67ffcb3a 480f6151 ``` 即: ```text c80ea6828763497a3acbff6751610f48 ``` 在 JNI 主 verifier 的 `0xb224` 这一次调用里,第四个 word `0x480f6151` 完全没有后续业务读者;单独改它仍 `JNI=1`。但 FUN12650 在其它 call site会整16字节消费,因此只能说“该调用点 P3 是死数据”,不能删除 helper 的第4 word。 它被用于: - FUN6630 固定 F block; - T prefix `85 26 27`; - 13598 KAT常量; - 13878 config方程; - G3/G4/G5 target obfuscation; - G5 tail seed; - ec68 target体系。 ## 23.1 G3/G4 target 是 `FUN12650 ^ f020` 隐藏出来的 ```text f020(0)=101480d8 f020(1)=828affa4 f020(2)=67ffddd5 ``` 于是: ```text 82a60ec8 ^ 101480d8 = 92b28e10 # G3 target 7a496387 ^ 828affa4 = f8c39c23 # G4 low32 67ffcb3a ^ 67ffddd5 = 000016ef # G4 high14 ``` 拼起来: ```text G4 target = 16eff8c39c23 ``` `f020` 本身约 780 bytes,但可继续因式分解: ```text f020(p) ^ FUN10654(p) = 0c672ad4 if p mod3=0 aab91670 if p mod3=1 3cad0ec0 if p mod3=2 ``` 所以它主要是 target-obfuscation,不是额外独立密码 gate。 --- # 24. even25 → local94:严格可逆的字段打散器 置换: ```text P = [7,2,19,0,14,23,5,11,21,3,17,8,24,1,12,6,20,10,4,22,15,9,18,13,16] ``` 令: ```text K=STATE s0=K[7]^0xc3 ``` 第 i 步: ```text c_i = ROL8((s_i + 49*i + K[(1+7*i)&15]) & ff, i&7) L[i] = ((even[P[i]] - c_i) & ff) ^ K[(3+5*i)&15] s_{i+1} = ((L[i]+c_i)&ff) ^ ((5a+35*i)&ff) ``` 逆: ```text even[P[i]] = ((L[i]^K[(3+5*i)&15]) + c_i) & ff ``` 所以它没有增加数学难度,只是把连续的 state/config 字段打散到用户输入里。 --- # 25. even 状态机 `13008 → 1325c → 13598 → 13878` 全局: ```text 0x163a4 D / delta 0x163a8 width 0x16480 rot 0x16484 mask 0x16488 A[0..31] 0x16508 T[256] 0x16608 selector ``` 四个 helper 都不是“比较失败立刻 reject”,而是: ```text 比较失败 -> 选 fallback state -> 继续执行 ``` 这是分析 even 的核心。 --- # 26. `0x13008` 四条件: ```text C1: ((LE32(L+0)^2) & 03ffffff) == 0 C2: (L[4]&0f) == 1 C3: (LE32(L+5)^LE32(context+8)) == 5 C4: ((LE32(L+9)^LE32(context+0)) & 60ffffe0) == 40 ``` all-pass: ```text selector = 0x0001058c ``` 原代码并没有直接写“小常量2/5/0x40”,而是用一组看似代码地址的 rodata差值构造: ```text 0x10090-0x10088 = 8 >>2 = 2 (0x10410-0x10400) XOR (0x10400-0x103fc) = 0x14 >>2 = 5 (0x10594-0x1058c)<<3 = 0x40 ``` 是很典型的“伪地址常量”混淆。 canonical 代表: ```text L[0:13] = 0200000001832dbd05c70573e5 ``` --- # 27. `0x1325c`:xorshift table 与 selector shift 以: ```text u = LE32(L+9) ``` 生成 256-byte xorshift32 stream: ```python u ^= u<<13 u ^= u>>17 u ^= u<<5 stream[i] = u & 0xff ``` 然后: ```text T[(selector+i)&ff] ^= stream[i] ``` 并检查: ```text T[0:3] == 85 26 27 ``` `85 26 27` 也不是直接硬编码,而是: ```text FUN12650[0:3] = c8 0e a6 rodata = 4d 28 81 XOR = 85 26 27 ``` all-pass canonical: ```text LE32(L+9)=e57305c7 T0=85 ``` C4 + T-prefix 两组线性条件联合 rank=32,因此 canonical all-pass 中 `L[9:13]` 没有隐藏自由度。[ALG] 如果把所有 `13008/1325c` branch 都纳入,最终 retained T-table 也不是任意 256-byte 表,而只可能落在 514 张有限集合中:[ALG] ```text 1 张 identity 256 张 zero-init + unshifted xorshift 256 张 identity-init XOR xorshift 1 张 canonical shifted-xorshift ``` 除 identity 外,513 张 retained non-identity table 的前三字节都满足 `85 26 27`,`T0=85`。这把全局 even 问题从“任意表”缩成有限 table family;真正未完全穷尽的仍是每张表下 joint `(D,LCG-seed)` 64-bit ARX state。 --- # 28. `0x13598`:TEA 路标、三组 KAT 与 32-bit seed 先无条件: ```text raw_D = LE32(L+13) seed0 = LE32(L+17) ^ (T0*01010101) ``` 生成: ```text A[i+1] = A[i]*19660d + 3c6ef35f ``` 32 个 word。 再跑三组 16-round XTEA-like KAT: ```text F(x)=(((x<<4)^(x>>5))+x) mod 2^32 left += F(right) ^ (sum + key_even) right += F(left) ^ (sum + key_odd) sum += delta ``` 三组 exact known-pair target: ```text (00000001,00000002) -> (7cf2c5a2,1410ee09) (deadbeef,cafebabe) -> (c094c7f3,5c08eeea) (12345678,9abcdef0) -> (18cf95e0,8c30c302) ``` 若三组不通过: ```text D ^= 1 ``` 而不是 reject。 ## 28.1 `0x9e3779b9` 的证据强度要说准确 ELF `.data` 中 D 的初值就是: ```text 9e3779b9 ``` 并且结构明显是 TEA/XTEA。 但 `13598` 又会从 `local94[13:17]` 覆盖它,所以不能说“前序 hard gate 数学强制 delta=9e3779b9”。 正确表述: 1. 全局初值 + 标准 TEA 结构给出强 intended 候选 `9e3779b9`; 2. 固定它后扫描全部 `2^32` effective LCG seed; 3. 三组 KAT唯一得到: ```text 0x5b28455b ``` 4. 后续 `FUN126b8 + seed fold + 原JNI` 全链确认。 ## 28.2 AVX2 2^32 扫描 8 lane AVX2 完整扫描: ```text 2^32 candidates ≈ 4.53 s ``` 唯一: ```text effective LCG seed = 5b28455b ``` 因为 `T0=85`: ```text raw local seed = 5b28455b ^ 85858585 = deadc0de ``` 而 `0x135f4` 的不可达 magic instruction 恰好就是: ```text deadc0de ``` 这是非常明显的 breadcrumb。[INFER] ## 28.3 第一组 KAT和最终 early gate 都独立锁同一状态 固定 seed 扫全部 D: ```text D=9e3779b9 唯一 ``` 固定 D 扫全部 seed: ```text seed0=5b28455b 唯一 ``` 最终 `FUN126b8` 16-byte gate 对 canonical D 的 `2^32 seed` 全扫也只有同一个 seed命中。 所以三组 13598 KAT 对 intended 分支求解并非唯一信息源;最终 early gate本身也独立指向同一 64-bit state。 ## 28.4 13598 的 200ms timing 是假 anti-debug 它和 `FUN5` 用同一个 1000次短循环 + 200ms 阈值模板。 但 slow 时只是: ```text 32个LCG word正常生成并保存 再额外跑8次,不store ``` 后续无观察者。 因此: ```text FUN5 timing = 真 13598 timing = 假 ``` --- # 29. `0x13878`:rot/mask + BRK self-scan 它扫描 `FUN126b8` 前: ```text 1024 bytes / 256 dwords ``` 只判断: ```text (word >> 21) == 1697 ``` 等价于识别: ```text 0xd4200000 BRK class ``` 原始代码 count=0。 canonical raw: ```text rot = L[21]&31 = 7 mask = L[22:25] = 0x371342 ``` 八个 config equation: ```text 只看第1条 -> 4个候选 (5,dc4edd) (6,6e2777) (7,371342) (8,1b89a8) 第2条后 -> 只剩 (7,371342) 第3~8条不再缩小 ``` 所以后六条只是重复确认/代码膨胀。 `42 13 37` 本身也明显是 breadcrumb。 ## 29.1 self-scan 检的是调试痕迹,不是代码语义 把扫描区内真实执行: ```text eor w17,w9,w8 ``` 换成语义相同: ```text eor w17,w8,w9 ``` 结果: ```text BRK count=0 rot=7 JNI=1 ``` 而把一个**永远不执行**的 slot 改成 `BRK #0`: ```text slot执行次数=0 BRK count=1 raw rot 7 -> XOR1 = 6 config equation fail final rot clear to 0 early gate fail JNI=0 ``` 所以它检测的是“有没有软件断点类痕迹”,不是代码完整性。 --- # 30. `FUN126b8`:XTEA外观 + 一个故意有损的 byte layer caller固定输入: ```text p = 0^128 ``` 内部先 XOR: ```text deadbeef cafebabe 8badf00d feedface ``` 12轮两条 64-bit lane,使用同一 D 与 LCG key schedule;canonical: ```text A0 = aeffbafe A0 high nibble = a width = 0x10 | a = 26 keymask = (1<<26)-1 rot=7 ``` 这里要特别区分: ```text raw/config mask = 0x371342 # 由 L[22:25] / 13878 产生,只进 config check + seed fold keymask = (1<<26)-1 # FUN126b8 主轮真正用于截断 round key ``` 两者不是同一个东西。 每轮主要结构: ```text left += F(right) ^ (key + r*D) z = ROL32(left,rot) right += (masked-key/z 混合) ``` 前 10 轮完成后、进入第 11 轮更新前: ```text x ^= T[x & 0xff] ``` 这是唯一有意信息损失层。 canonical T 下 byte map: ```text b -> b ^ T[b] ``` 256个输入只映到: ```text 164 个输出 ``` fiber: ```text 1-to-1: 98 2-to-1: 48 3-to-1: 12 4-to-1: 4 5-to-1: 2 ``` canonical target 四个 low-byte fiber大小: ```text 2 × 2 × 2 × 1 = 8 ``` 因为其它 round 都可逆,因此完整 `FUN126b8` 对固定 state、固定 target **精确有 8 个 128-bit plaintext preimage**。 其中 canonical 是 `0^128`;另一个例子: ```text af63a5edb8d4b92f0000000000000000 ``` 也得到: ```text d6ced48b4a6f314d11f654f72956c20e ``` 但 caller 不允许改变 `p`,所以这些 collision 不产生额外 flag;它们只证明该“大密码核心”本身是 lossy verifier。 --- # 31. even early target 与 bridge `R=88` Even target先用固定材料经 `ec68` 得 base,再 `edcc(base,R,0xa1)`。 canonical: ```text GATE_BASE = e6a03b9e75425736e63343bc6cc2309f R = 88 target = d6ced48b4a6f314d11f654f72956c20e ``` `edcc` 内部: ```text s0 = p2 ^ p3 s_{i+1} = 0x3d*s_i + 0x71 + 0x13*i mod256 out[i] ^= s_i ^ (s_i>>3) ``` `0x3d` 是奇数,在 mod256 可逆;`x->x^(x>>3)` 也可逆。 因此 **任意一个 target byte 都单独一一编码 R**。 例如只用首字节: ```text d6 -> R=88 ``` 16-byte target 的信息熵只有 8 bit,但要求 even 输出同时命中 16 个冗余一致字节,作为 equality gate 仍然很强。 KAT target 的 `seed_low` 也是完全相同的冗余编码:KAT1/KAT2 任意一个 target byte都能独立反出 `d7`。 --- # 32. canonical local94 与 even25 canonical representative: ```text local94 = 02 00 00 00 01 83 2d bd 05 c7 05 73 e5 b9 79 37 9e de c0 ad de 07 42 13 37 ``` 即: ```text 0200000001832dbd05c70573e5b979379edec0adde07421337 ``` `FUN126b8`: ```text d6ced48b4a6f314d11f654f72956c20e ``` 通过 early gate。 seed fold: ```text x = selector ^ A0 ^ mask ^ (rot<<24) ^ (T0*01010101) x ^= x>>16 x *= 045d9f3b x ^= x>>15 x ^= x>>8 seed_low = (x&ff) ^ 2a ``` 得到: ```text seed_low=d7 full seed=a71cfdd7 ``` `selector=0x1058c` 本身是 rodata 硬加载的 intended 常量,并不是 seed fold 方程的数学唯一解。固定 low8=`0x8c` 后枚举另外16 bit,共有 276 个 selector 也能 fold 到 d7;`0x1058c` 只是作者选择的一个代表,同时 low8=140 又承担 T-table shift 的职责。 逆 even map: ```text even25=a77a78ffc894367d1bf5bb3faab6e2f4db0070533de8b73443 ``` --- # 33. canonical effective-state 分支精确有 16384 个 accepted inputs canonical local94 里,以下位不改变最终 effective state: ```text L[3] high6 : 64 L[4] high4 : 16 raw D : b8 / b9 两条等价路径 : 2 L[21] high3 : 8 ``` 原因: - C1 只看 L[0:4] 低26 bit; - C2 只看 L4 low nibble; - raw D=`9e3779b8` 时 KAT失败后 `D^=1`,回到 effective `9e3779b9`; - rot只取 L21 low5。 可把整族直接参数化: ```text L[0] = 02 L[1] = 00 L[2] = 00 L[3] = h6 << 2 # h6 in [0,63] L[4] = 01 | (h4 << 4) # h4 in [0,15] L[5:13] = 83 2d bd 05 c7 05 73 e5 L[13] = b8 or b9 L[14:17] = 79 37 9e L[17:21] = de c0 ad de L[21] = 07 | (h3 << 5) # h3 in [0,7] L[22:25] = 42 13 37 ``` 因此: ```text 64*16*2*8 = 16384 ``` 个 local94 全部坍缩到同一个: ```text T,D,A,rot,width,mask,selector ``` 同一个 `FUN126b8` output、同一个 seed d7。 `local94 <-> even25` 又是双射,因此正好对应 16384 个不同用户输入。[ALG] 这些输入在 even25 用户视图中被链式映射充分扩散:25个 even byte只有3个始终固定;绝大多数等价输入相对 canonical 会改变 22/25 个 even byte。 --- # 34. 一个同样大小的 decoy family:early gate 通过,但 seed=e0 存在另一组代表: ```text 0200000001832dbd057823d41ab979379edec0adde07421337 ``` 它使 C4 fail,`selector=0`。 但 xorshift table seed: ```text 0x1ad42378 ``` 恰好满足: ```text 1ad42378 = xorshift^116(e57305c7) 116 = 256 - 140 selector.low = 0x8c = 140 ``` 于是: ```text T_decoy[0:140] == T_canonical[0:140] ``` 第一处分歧恰好: ```text index 140 ``` 而 canonical `FUN126b8` round10 实际查的四个索引: ```text 33, 2e, 31, 8b ``` 最大: ```text 8b = 139 ``` 正好全部落在共同前缀里。 因此两张表虽然后半完全不同,early gate看见的4个值完全相同。 这一 decoy family 同样有: ```text 2^14 = 16384 ``` 个 raw local94,全部: ```text early target PASS ``` 但: ```text selector=0 -> seed=e0 ``` 而 clean C7 256-seed全扫描只允许 `57/d7`,因此 e0 必失败。 原 ARM64端到端: ```text early=0 seed=e0 C7=ff JNI=0 ``` 这个结构高度说明作者有意构造了一个“early gate假成功族”。[INFER] --- # 35. `FUN5` timing slow、Inner payload异常、JNI canary 都是“无解世界” 本题 anti-analysis 不是统一 `return false`,而是不断把执行切到**仍然能跑、但输入域无解**的世界。 ## 35.1 FUN5 slow 8-round 前序便宜 hard gates仍唯一钉死同一个 post-state `(A,B,C,D)`。 逆8轮得到尾7字节: ```text d6 75 9a 9c 25 a9 86 ``` 不全相等。 而 Java residual无论什么值,FUN5输入尾部只能是: ```text pad*7 ``` 所以 8-round world 对任何 padding byte都无解。[ALG] ## 35.2 Inner payload fp8 != 0 256 个理论 fp8 中,C7 只有: ```text fp8=0 ``` 存在任何 `(seed,sp280)` 解;其它255全部无解。[ALG] ## 35.3 fake JNI field canary Native故意调用: ```text GetStaticFieldID(clazz,"W","%") ``` `"%"` 不是合法 JNI field descriptor。 真实 ART:返回 null + pending exception,代码清异常后仍走 clean path。 粗糙 emulator 若错误返回非空 fieldID,会固定把 checksum mirror初态: ```text 5eed4a71 -> 8e15cd87 ``` 最终 residual: ```text 2dd064c3 ``` 四字节 XOR=`5a`,因此非零 residual规则产生: ```text pad=a5 ``` 而 canonical post-state逆12轮要求 `5a*7`,所以该 world 也无解。 假 field 实际“读到什么值”无关;canary只检测非法 descriptor 是否被错误当成有效 field。[DYNAMIC] --- # 36. challenge target 很多是反向生成的 后续深入分析可以从 intended odd/even state **不读取现成 target** 反生成题目材料。 ## 36.1 payload32 从 canonical odd: ```text FUN5 -> O96 ``` 然后: ```text payload[0:16] = O[80:96] ``` 因为 generated8只读 payload前16字节: ```text payload[16:24] = generated8(seed,ctx,payload[0:16],O[0:8]) ``` 最后由 C7 residual=0 逐字节反解: ```text payload[24:32] ``` 得到完整: ```text 9f73be24a1dd6c96b90723bba7cdfdc9 fd521311dd1b172572014429f93dd77c ``` 与真实 payload clean output 32/32 字节一致。[ALG] 因此没有 self-reference。 ## 36.2 payload 48-byte表 公式中: ```text i -> (5*i+11) mod32 ``` 是 0..31 的排列;每个 clean output byte 含一个独占 first32 table byte。 所以可以: 1. 任取后16字节 salt; 2. 按 desired payload32 唯一反解前32字节。 实际后16字节: ```text 53d826ceacc3e6bbeac35cbd9db9a26b ``` 反解出的前32字节与 payload 完全一致。 ## 36.3 even target / KAT target rodata `ec68` 对固定 seed: ```python out[i] = src[i] ^ ROL8(key[(3+7*i)&15], (seed+i)&7) ^ (seed+0x31*i) ^ FUN12650[i] ``` `(3+7i) mod16` 是排列,因此给定 desired target: - 任取 key16,src16 唯一; - 任取 src16,key16 唯一。 再接可逆 `edcc`。 我们从 canonical even/KAT output 反解出的三组 src 与 SO 中 rodata逐字节一致: ```text even src: dfc964a013144fe5e087025d5e5d4ee2 KAT1 src: fd1ca790382cd2556c97e7cf01f101d7 KAT2 src: e42aaada29f8509078ca3070f34535cf ``` 所以这些很像作者围绕 intended state 反向生成的 challenge vector,而不是独立秘密。[INFER] --- # 37. 常量“词汇表”:作者大量拼接公开可逆 primitive 题里反复出现: ```text 0x5851f42d4c957f2d / 0x14057b7ef767814f PCG LCG 0x0019660d / 0x3c6ef35f Numerical Recipes LCG 0x9e3779b9 TEA / golden ratio 0x9e3779b185ebca87 xxHash64 prime 0xff51afd7ed558ccd / 0xc4ceb9fe1a85ec53 Murmur fmix64 0x7feb352d / 0x846ca68b lowbias32 0x045d9f3b / 0x119de1f3 互为 mod2^32 乘法逆元 0x2c1b3c6d / 0x297a2d39 prospector32-style 0xedb88320 CRC32 reflected poly SHA-256 IV 多处 ``` 这说明表面上很多“自定义密码”其实是把公开的: ```text integer permutation ARX CRC GF(256) LCG ``` 小零件拼成大函数,再用假数据依赖和显式抵消遮住边界。 一个很实用的逆向原则是: > 先识别常量家族和数据依赖图,再逐语句读 decompiler,通常更快。 --- # 38. 最终交织 ```python decoded = bytearray(50) decoded[0::2] = even25 decoded[1::2] = odd25 ``` canonical: ```text even25 = a77a78ffc894367d1bf5bb3faab6e2f4db0070533de8b73443 odd25 = 7ae31b94d256f80c41b7298e63a5df104bc8723d960fe458ad ``` 得到: ```text a77a7ae3781bff94c8d2945636f87d0c1b41f5b7bb293f8eaa63b6a5e2dff410db4b00c87072533d3d96e80fb7e4345843ad ``` --- # 39. 严格验证层次 本题最终不是只靠纯 Python Exp。 ## 39.1 纯模型 验证: - Java material/context; - payload runtime key; - clean payload32; - FUN5 forward/inverse/XOF; - C7; - generated8; - w2/sp280; - X; - G3/G4; - FUN6630 work148; - FUN7400 KAT; - even local94/state helpers; - FUN126b8; - seed fold。 ## 39.2 原 ARM64 JNI clean harness 只模拟真实 Android 环境语义: ```text getauxval(AT_HWCAP).low2 = 3 dlsym("mprotect") -> working stub mmap/munmap/proc/JNI minimal semantics ``` 不替换算法中间值,不手改 PC。 canonical 自然: ```text payload key = d23a... payload32 = 9f73... local94 = canonical early gate = 0 seed = d7 final w19 = 0 JNI return = 1 ``` ## 39.3 原 APK / 真机 原题解还在 root Pixel 6 上用原 APK、无 Frida/无内存补丁验证: ```text Correct! Flag accepted. ``` --- # 40. 这题最值得记住的解题经验 1. **先校准环境,再谈算法。** 一个错误的 `getauxval=0` 足以把整题稳定送进假世界。 2. **ABI / FDE / 机器码优先于 decompiler。** Java 甚至有一处 JADX 明确结构化错。 3. **内部 self-check 失败不等于 reject。** 这题大量比较只是选择 fallback state。 4. **先找数据流分解。** 50-byte 输入很快能拆成 odd/even + 一个 1-byte bridge。 5. **能逆就不要搜。** FUN5/XOF 都是严格双射。 6. **局部二次布尔关系自己写 CNF。** G4 每条只碰 <=5 个 B bit,通用 BV solver 反而是坏表示。 7. **32-bit 高速前向函数适合 AVX2 全扫。** 13598 的 seed 搜索 2^32 只需几秒。 8. **巨大函数不代表高信息量。** FUN7400、G5、G3 中大量约束在求解上是冗余认证。 9. **识别“有损层”。** FUN7400 的 singular near-MDS、FUN126b8 的 byte map 才是真信息损失;大多数 hash-looking arithmetic 反而是 permutation。 10. **anti-debug 可以是“无解世界”,不是直接 return false。** timing、payload fingerprint、JNI canary、work148 poison 全都采用了这种思路。 11. **完整性 tripwire 与安全认证是两回事。** CRC32 guard/self-code fingerprint 都可以做碰撞;BRK scan只检测特定调试痕迹。 12. **最后验证必须回原 ARM64/原 APK。** 纯模型只负责理解和求解,最终成功只认真实 checker。 --- # 41. 本次深入复盘对原题解的关键修改清单 - `0x13f00`:`sysconf` → **`getauxval(AT_HWCAP)`**; - clean HWCAP:不是 `-1`,而是 **low2=3**; - opaque helper:从“副作用先保留”升级为**整个封闭子系统可删除**; - G1:不是直接固定三段 XOF,而是**直接固定 O[80:96],其余两段由 XOF逆派生**; - C7:最终 accumulator **确实是 hard gate**; - G5:“17-round” → **初始化 + 16-round**; - `FUN7400`:AES-like permutation → **故意有损的 lossy SPN**; - `FUN126b8`:block-cipher-like → **canonical target精确8-to-1**; - `0x13598` delta:应写成**TEA路标/强 intended 候选 + 全链验证**,不是前序 hard constraint直接强制; - `0x13cbc`:作者手写 cache sync → **更像标准 compiler/runtime clear-cache routine**; - canonical flag:**不是唯一接受字符串**,canonical effective branch已有 16384 个接受输入; - sparse-CNF:固定 C7 sp280 计算 X 时,还应显式检查每个 B 的 **actual w2(B)==C7 target**;补上后 d7 百万模型直接唯一、57为0; - generated8:从“复杂8轮逆”进一步缩成 **state0已知 + 偶链唯一 + 256-state odd 4-cycle**; - payload32 / KAT/even target:可从 intended state **反向重建**,强烈支持 target-oriented challenge construction。 --- # 42. 仍然保留的证明边界 深入研究不应该把没有证明的东西包装成结论。 目前仍没有全局证明: ```text 整个程序所有 accepted 100-hex 的总数 == 16384 ``` 已经证明的是: 1. clean odd 侧全局唯一到 canonical odd25; 2. canonical effective even state恰有 16384 个 raw local94/even25; 3. 存在同样大小的 decoy early-gate family,但 seed=e0 被 C7排除; 4. canonical T/rot branch下固定 D扫2^32、固定 seed扫2^32均唯一; 5. 目前未穷尽其它 possible T table + joint `(D,LCG seed)` 的全部64-bit组合。 同理,FUN6630 anti-hook异常世界已经完备排除到: ```text <=3 API同时异常 + 任意thread parity ``` 8,392,589,935 个非clean组合(约 83.93 亿),0 个合法 padding preimage;4/5 API同时异常的全空间没有穷尽。 这些边界会保留在 writeup 中,避免“做题答案正确”被误写成“所有状态空间都已数学穷尽”。 --- # 43. 附:canonical 关键值速查 ```text APK SHA256 21f932aa6222a37ffc4183861017794c45c3e07a5eb88a7b9050e044256e763f guard 0x13d60 / 0x60 / CRC32 0x3e0695ce Java share eab0aaacd8c35c932ba824ccc628a5c5ce95063e603d010060000000c34224ac context16 870573e5f5c63d52862dbd05ab3d9494 payload key d23adbe6ea44b7f98bdbd44c0662ab4c payload32 9f73be24a1dd6c96b90723bba7cdfdc9fd521311dd1b172572014429f93dd77c seed low d7 full seed a71cfdd7 A 58b67a47304291c1 B 9eb09ed6e443b907 C 492f9ce7e59b3768 D 69eec8c1ee727f73 sp280 129b4a86 X 996bd2f8c1b7dd80 sp96 92b28e10 G4 16eff8c39c23 R 88 local94 canonical 0200000001832dbd05c70573e5b979379edec0adde07421337 even25 canonical a77a78ffc894367d1bf5bb3faab6e2f4db0070533de8b73443 odd25 canonical 7ae31b94d256f80c41b7298e63a5df104bc8723d960fe458ad canonical accepted input a77a7ae3781bff94c8d2945636f87d0c1b41f5b7bb293f8eaa63b6a5e2dff410db4b00c87072533d3d96e80fb7e4345843ad ``` --- # 44. 附录:最终 canonical 状态纯 Python 验证器 下面这份脚本**不是完整 solver**:AVX2 2^32 seed 搜索、G4 百万模型 CNF 枚举、generated8 state-cycle 求解都已经在正文解释。它的用途是从已经恢复出的 canonical state 出发,独立验证: - odd 12-round inverse 与 `5a*7`; - X mixer; - G4 46-bit target; - 13598 三组 KAT; - even xorshift table; - `FUN126b8`; - seed fold; - `local94 -> even25`; - 最终 50-byte 交织。 ```python from __future__ import annotations import struct MASK32 = 0xFFFFFFFF MASK64 = 0xFFFFFFFFFFFFFFFF STATE = bytes.fromhex("870573e5f5c63d52862dbd05ab3d9494") TARGET16 = bytes.fromhex("d6ced48b4a6f314d11f654f72956c20e") TARGET46 = 0x16EFF8C39C23 PERM = [ 7, 2, 19, 0, 14, 23, 5, 11, 21, 3, 17, 8, 24, 1, 12, 6, 20, 10, 4, 22, 15, 9, 18, 13, 16, ] def rol(value: int, count: int, bits: int) -> int: count %= bits mask = (1 << bits) - 1 return ((value << count) | (value >> (bits - count))) & mask def ror64(value: int, count: int) -> int: return rol(value, -count, 64) def inverse_odd_round(state: tuple[int, int, int, int], index: int): ap, bp, cp, dp = state p = ap ^ dp q = bp r = cp ^ bp d = ror64(dp ^ r, 3) c = rol(((r ^ (index + 4)) - d) & MASK64, 8, 64) b = ror64(q ^ p, 3) a = rol(((p ^ index) - b) & MASK64, 8, 64) return a, b, c, d def recover_odd() -> bytes: # FUN_5998's 46 sparse Boolean equations and the seven 0x5a padding # bytes leave this unique post-12-round B word on the clean seed-d7 path. state = ( 0x58B67A47304291C1, 0x9EB09ED6E443B907, 0x492F9CE7E59B3768, 0x69EEC8C1EE727F73, ) for index in range(11, -1, -1): state = inverse_odd_round(state, index) raw = struct.pack("<QQQQ", *state) assert raw[25:] == b"\x5a" * 7 return raw[:25] def x_mixer(seed: int, a_bytes: bytes, sp280: int) -> int: context = STATE payload = bytes.fromhex("9f73be24a1dd6c96b90723bba7cdfdc9") a_low = int.from_bytes(a_bytes[:4], "little") a_high = int.from_bytes(a_bytes[4:], "little") c0 = int.from_bytes(context[0:4], "little") c1 = int.from_bytes(context[4:8], "little") p0 = int.from_bytes(payload[0:4], "little") p2 = int.from_bytes(payload[8:12], "little") value = (sp280 * 0x9E3779B185EBCA87) & MASK64 value ^= (seed * 0x0101010101010101) & MASK64 value ^= ((p0 << 9) | (c0 >> 3)) & MASK64 value ^= (c1 | (p2 << 32)) & MASK64 value ^= (a_low << 16) & MASK64 value ^= ((a_high << 1) ^ (value >> 33)) & MASK64 value = (value * 0xFF51AFD7ED558CCD) & MASK64 value ^= value >> 33 value = (value * 0xC4CEB9FE1A85EC53) & MASK64 return (value ^ (value >> 33)) & MASK64 def fun5998_short(b_value: int, x_value: int) -> int: def bit(value: int, index: int) -> int: return (value >> (index & 63)) & 1 result = 0 for j in range(46): parity = j & 1 linear = parity ^ bit(b_value, 7 * j + 3) linear ^= bit(b_value, 11 * j + 19) ^ bit(x_value, 5 * j + 1) left = 1 ^ parity ^ bit(b_value, 17 * j + 5) left ^= bit(x_value, 9 * j + 13) right = 1 ^ parity ^ bit(x_value, 27 * j + 31) right ^= bit(b_value, 23 * j + 29) ^ bit(b_value, 25 * j + 8) result |= (1 ^ linear ^ (left & right)) << j return result def xtea_kat(seed: int, delta: int, left: int, right: int): key = seed total = delta for _ in range(16): key = (key * 0x19660D + 0x3C6EF35F) & MASK32 f = ((((right << 4) & MASK32) ^ (right >> 5)) + right) & MASK32 left = (left + (f ^ ((total + key) & MASK32))) & MASK32 key = (key * 0x19660D + 0x3C6EF35F) & MASK32 f = ((((left << 4) & MASK32) ^ (left >> 5)) + left) & MASK32 right = (right + (f ^ ((total + key) & MASK32))) & MASK32 total = (total + delta) & MASK32 return left, right def build_table(local: bytes) -> bytearray: # All four 13008 conditions pass, so selector is 0x1058c and the table # starts zeroed. 1325c writes a rotated xorshift stream into it. value = int.from_bytes(local[9:13], "little") selector = 0x1058C table = bytearray(256) for index in range(256): value ^= (value << 13) & MASK32 value ^= value >> 17 value ^= (value << 5) & MASK32 value &= MASK32 table[(selector + index) & 0xFF] ^= value & 0xFF assert table[:3] == bytes.fromhex("852627") return table def gen126b8(local: bytes, table: bytearray) -> bytes: delta = int.from_bytes(local[13:17], "little") seed = int.from_bytes(local[17:21], "little") ^ (table[0] * 0x01010101) keys = [] key = seed for _ in range(32): key = (key * 0x19660D + 0x3C6EF35F) & MASK32 keys.append(key) width = 0x10 | (keys[0] >> 28) key_mask = (1 << width) - 1 def lane(left: int, right: int, parity: int): for index in range(12): if index == 10: left ^= table[left & 0xFF] right ^= table[right & 0xFF] key_word = keys[2 * index + parity] round_key = (key_word + (index + 1) * delta) & MASK32 f = ((((right << 4) & MASK32) ^ (right >> 5)) + right) & MASK32 new_left = (left + (round_key ^ f)) & MASK32 rotated = rol(new_left, 7, 32) masked_key = key_word & key_mask mix = (masked_key + (rotated >> 5)) & MASK32 mix ^= ((masked_key >> 12) + ((rotated << 4) & MASK32)) & MASK32 mix ^= round_key ^ rotated right = (right + mix) & MASK32 left = new_left return left, right words = (*lane(0xDEADBEEF, 0xCAFEBABE, 0), *lane(0x8BADF00D, 0xFEEDFACE, 1)) return b"".join(word.to_bytes(4, "little") for word in words) def invert_even(local: bytes) -> bytes: even = bytearray(25) chain = STATE[7] ^ 0xC3 for index, value in enumerate(local): q = (chain + 49 * index + STATE[(1 + 7 * index) & 15]) & 0xFF rotated = rol(q, index & 7, 8) even[PERM[index]] = ( (value ^ STATE[(3 + 5 * index) & 15]) + rotated ) & 0xFF chain = ((value + rotated) & 0xFF) ^ ((0x5A + 35 * index) & 0xFF) return bytes(even) def main() -> None: # On the intended branch, the TEA delta breadcrumb is 0x9e3779b9; # fixing it and scanning the 32-bit effective LCG seed gives 0x5b28455b. # The successful table branch has T[0]=0x85, hence the raw # local seed is 0x5b28455b ^ 0x85858585 = 0xdeadc0de. delta = 0x9E3779B9 effective_seed = 0x5B28455B kats = ( ((1, 2), (0x7CF2C5A2, 0x1410EE09)), ((0xDEADBEEF, 0xCAFEBABE), (0xC094C7F3, 0x5C08EEEA)), ((0x12345678, 0x9ABCDEF0), (0x18CF95E0, 0x8C30C302)), ) for plain, expected in kats: assert xtea_kat(effective_seed, delta, *plain) == expected local = bytes.fromhex( "0200000001832dbd05c70573e5" "b979379e" # 0x9e3779b9 "dec0adde" # 0xdeadc0de "07421337" ) table = build_table(local) generated = gen126b8(local, table) assert generated == TARGET16 # The final JNI seed fold must select the solved odd seed d7. key0 = (effective_seed * 0x19660D + 0x3C6EF35F) & MASK32 folded = 0x1058C ^ key0 ^ 0x371342 ^ (7 << 24) ^ (0x85 * 0x01010101) folded ^= folded >> 16 folded = (folded * 0x045D9F3B) & MASK32 folded ^= folded >> 15 folded ^= folded >> 8 assert ((folded & 0xFF) ^ 0x2A) == 0xD7 odd = recover_odd() x_value = x_mixer(0xD7, bytes.fromhex("c1914230477ab658"), 0x129B4A86) assert x_value == 0x996BD2F8C1B7DD80 assert fun5998_short(0x9EB09ED6E443B907, x_value) == TARGET46 even = invert_even(local) decoded = bytearray(50) decoded[0::2] = even decoded[1::2] = odd print(decoded.hex()) if __name__ == "__main__": main() ``` 运行输出应为: ```text a77a7ae3781bff94c8d2945636f87d0c1b41f5b7bb293f8eaa63b6a5e2dff410db4b00c87072533d3d96e80fb7e4345843ad ```
登录后可查看完整内容
冰与火的战歌:Windows内核攻防实战高级班!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。
收藏
・
1
点赞
・
4
打赏
分享
分享到微信
分享到QQ
分享到微博
赞赏记录
参与人
雪币
留言
时间
aut0run
你的帖子非常有用,感谢分享!
3天前
Itachi_
你的分享对大家帮助很大,非常感谢!
2026-8-28 13:33
梧桐生
感谢你的贡献,论坛因你而更加精彩!
2026-8-20 15:36
代陌
谢谢你的细致分析,受益匪浅!
2026-8-20 14:46
查看更多
赞赏
×
1 雪花
5 雪花
10 雪花
20 雪花
50 雪花
80 雪花
100 雪花
150 雪花
200 雪花
支付方式:
微信支付
赞赏留言:
快捷留言
感谢分享~
精品文章~
原创内容~
精彩转帖~
助人为乐~
感谢分享~
最新回复
(
1
)
Imxz
雪 币:
112
活跃值:
(9395)
能力值:
( LV2,RANK:10 )
在线值:
发帖
6
回帖
749
粉丝
9
关注
私信
Imxz
2
楼
tql
2026-8-20 16:33
0
游客
登录
|
注册
方可回帖
回帖
表情
雪币赚取及消费
高级回复
返回
mxym_
2
11
发帖
2
回帖
120
RANK
关注
私信
他的文章
[原创] 第十题:卯时·曦光初现 WP
1512
[原创]第九题:丑寅同墟·星海抉择 WP
63
[原创]第八题:亥子合辰·塔影迷楼 WP
44
[原创]第七题:戌时·暗能潜流
18
[原创]第六题:酉时·书院迷局 WP
1500
关于我们
联系我们
企业服务
看雪公众号
专注于PC、移动、智能设备安全研究及逆向工程的开发者社区
看原图
赞赏
×
雪币:
+
留言:
快捷留言
为你点赞!
返回
顶部