apk-reverse dex补丁实战(下):会毁掉你构建的 dex 头部完整性字段
【免费下载链接】apk-reverseSuitable for Android APK reverse engineering analysis项目地址: https://gitcode.com/gh_mirrors/ap/apk-reverse
在 apk-reverse 这个安卓 APK 逆向分析项目里,dex 补丁有两种路线:整方法重写,和原地等长字节补丁。前一篇讲了怎么定位指令、怎么打补丁;这一篇只讲最容易翻车、也最容易被忽视的一步——重新计算 dex 头部的 checksum 与 signature 两个完整性字段。顺序写反,你的补丁文件就会在设备上"看起来能装、实际全挂"。
为什么头部完整性字段是最隐蔽的翻车点 💥
一个 dex 文件的头部里有 4 个"看不见的守卫":
| 字段 | 位置 | 算法 | 覆盖范围 |
|---|---|---|---|
checksum | 第 8~12 字节 | adler32 | 从第 12 字节到文件尾 |
signature | 第 12~32 字节 | SHA-1 | 从第 32 字节到文件尾 |
你在文件任意位置改了哪怕 1 个字节,这两个字段立刻失效。而最坑的是失效后的症状根本不是"校验失败"四个字:
- Android 日志打印
Failure to verify dex file ...: Bad checksum (computed, expected)——注意,真正的正确值会被当成 "expected" 显示,方向是反的; - 系统随后降级为"直接解释执行这个 dex",进程可能照常启动;
- 但类加载器开始随机罢工:
ClassNotFoundException: 你的 Application 类,或者某个功能页打不开、某类VerifyError。
也就是说,"我构建的 APK 坏了"这个结论,其实根因只是"头部两个字段过期了"。排查方向完全被带偏,这就是它"毁掉构建"的方式。
关键规则:signature 先算,checksum 后算 ⚠️
两个字段的覆盖范围是嵌套的——checksum的覆盖范围包含了signature所在的字节。所以重算顺序只有一种正确写法:
data[12:32] = hashlib.sha1(data[32:]).digest() # 第一步:signature data[8:12] = zlib.adler32(data[12:]).to_bytes(4, "little") # 第二步:checksum如果反过来先算checksum,此时signature字段还是旧的(或被清零),adler32 就是拿着一份错误的输入算出来的——无论你后面怎么改,这个头部永远校验不过。
更糟的是:有些 R8/优化工具产出的 dex,其signature字段本来就是全零(已知产物缺陷)。这类文件在没打过补丁前"自洽",bug 完全隐形;一旦你动了任何字节再按错误顺序重算,问题才爆出来——而且看起来像"你的补丁搞坏的"。真实案例记录见 l1-equal-length-patch-case-4.md。
项目中已经把正确顺序固化成了工具函数:
fix_dex_header()—— 按正确顺序重算两个字段:dexutil.pyverify_dex_header()—— 返回(checksum_ok, signature_ok),写完必须自检:dexutil.py
用 dex_patch_bytes.py 打补丁时,头部修复是自动的 ✅
如果你用项目的字节补丁脚本,头部完整性由工具兜底,流程是这样的:
- 先结构校验源码 dex,结构不清直接拒写;
- 按 JSON spec 定位指令、校验
expect_next(极性检查); - 等长替换指令字节;
- 调用
fix_dex_header重算头部,然后自我校验:不通过就FATAL: header does not self-verify; nothing written,拒绝写出坏文件。
工具源码:dex_patch_bytes.py
等长字符串补丁脚本 dex_strpatch.py 同理:替换字符串后先写sha1(data[32:])到 12~32,再写adler32(data[12:])到 8~12,顺序一步不乱。
想验证整套行为是否符合预期,可以看回归测试里的硬断言:补丁前后只有第 8~32 字节的头部字段和被改的 4 个指令字节变化,且输出必须通过verify_dex_header自检——见 test_dex_patch_bytes_regression.py。
打补丁前后,自己动手验证一次的三步清单 🔍
工具说"成功"不算数,自己从写出的字节重新推一遍才算数:
- 重算并比对:对输出的 dex 重新计算
sha1(data[32:])和adler32(data[12:]),两个都要和文件里存的值相等——这一步能同时证明顺序是对的; - 全文件 diff:原始文件 vs 补丁文件,差异应只出现在你声明的字节窗口 + 头部 8~32 字节;
- 重解码:把补丁后的方法完整解码一遍,断言恰好落在
insns_off + insns_size * 2结束,防止"替换指令吞掉了下一条"。
常见错误 → 症状对照表 🧯
| 你犯的错 | 设备上的表现 | 实际根因 |
|---|---|---|
| checksum 先于 signature 重算 | Bad checksum日志 +ClassNotFoundException | 头部顺序写反 |
| 只改了字节,完全没重算头部 | 启动随机崩溃、类加载失败 | 两个字段全部过期 |
在signature全零的 R8 产物上打补丁 | 补丁后才开始报错,像补丁的锅 | 源文件本来就带陈旧字段 |
| 非等长替换指令 | 方法解码错位,VerifyError | 越过了指令边界(与头部无关,但常被混淆) |
延伸阅读 📚
- 等长字节补丁完整约束与指令格式表:byte-level-patching.md
- 方法级重写 vs 字节补丁的选型决策:dex-patching.md
- 真实案例(UnCrackable-Level1 四字节补丁全链路):l1-equal-length-patch-case-4.md
- 打完补丁之后的重打包、签名与验证规则:SKILL.md
一句话总结:dex 补丁本身只有几个字节,但决定成败的是那 24 字节的头部——signature 先算、checksum 后算、写完必自检。把这三件事变成肌肉记忆,你的构建就不会再被"看似合理"的头部悄悄毁掉。
【免费下载链接】apk-reverseSuitable for Android APK reverse engineering analysis项目地址: https://gitcode.com/gh_mirrors/ap/apk-reverse
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考