以前我不会 APK 脱壳
直到 AI 帮我导出 42 个运行时 DEX
同一份 APK,静态分析只能看到 4 个壳类;运行后识别出 49 个候选内存区域,其中 42 个 DEX 真实落盘。这不是 AI 猜出来的结果,而是一条可以对样本、日志和哈希逐项复算的技术链。
先看结果:我真正跨过的不是一条命令
以前遇到加固 APK,我会把文件拖进 JADX。看到的只有 StubApp、几个 native 接口和一堆不明所以的壳文件。接下来该看哪里、为什么工具报错、dump 出来的 DEX 为什么仍打不开,我没有一条完整路径。
这次我把问题交给 AI,但没有让它直接给出“脱壳成功”的答案。我让它把任务拆成能够逐项验收的问题:先确认是不是壳,再找到壳的加载入口;让 App 自己把代码还原到内存;保存原始转储;修复 DEX 头;最后用解析器、完整哈希和真实方法体证明恢复结果。
class_def
内存 DEX
可被 Androguard 解析
生成的 Java 文件
本文内容仅供学习参考,所述技术仅限用于合法授权的安全研究、教学演示及防御机制开发。请勿用于未经授权的访问、测试、数据获取或其他违法活动。读者应自行确保使用行为符合适用的法律法规,并承担因不当使用本内容产生的相应责任。
authorized_sample.apk、YOUR_AUTHORIZED_PACKAGE、YOUR_DEVICE_SERIAL 与方括号字段是公开占位符,不是证据值;内部复核材料保留真实完整值。本文选取一份 Android 应用样本,仅对 APK 加固、运行时 DEX 转储和源码恢复过程进行技术实验;所有运行均在隔离测试设备上完成,不涉及对应用业务性质、运营主体或相关行为的判断。本次完成的是“从加固 APK 到可分析源码集合”;没有生成可重新安装的脱壳 APK,部分方法仍然是 native,也没有被写成已经恢复。
第一次接触 APK 加固:从下一节开始,先理解 APK、DEX、StubApp 和壳。 想直接看实验:从“判断有没有壳”开始,按判壳、转储、修复、源码验证阅读。 要逐条复跑:正文读因果,文末直接阅读内嵌命令、日志、逐文件清单和完整脚本。
0. 从零开始:APK、加固和脱壳到底是什么?
先暂时忘掉 Frida、JADX 和命令行。Android App 在开发者电脑上原本是一批 Java/Kotlin 源代码、图片、界面资源和配置;经过编译、打包与签名后,才变成手机能安装的 .apk 文件。加固不是 Android App 必须经历的步骤,而是开发完成后的额外保护处理。
先认识 APK 里的九个常用名词
com.stub.StubApp。 | ||
libjiagu*.so | ||
ZIP 只是外层容器。Android 还规定了 Manifest、DEX、资源表、签名块和 native 库的结构与语义;随意改一个字节会破坏签名,随意压回 ZIP 也不一定能安装。
JADX 是把 DEX 指令重建成便于人读的近似 Java。原注释、部分变量名、Kotlin 语义和构建信息可能已经丢失;混淆后,类名还可能只剩 a、b、c。
下面只演示“编译+混淆+反编译”可能造成的信息损失,不来自本次 APK,也不表示某个工具一定逐字输出成这样。
| |
LoginRequest、phoneNumber、buildLoginUrl 变成了 a。JADX 可以重建控制结构,却无法凭空知道开发者原来取了什么名字、写了什么注释。一个没有加固的 App 是怎样生成的?
开发者编写 Java/Kotlin 与 C/C++ 代码,并准备布局、图片、字符串等资源。 构建工具编译代码:Java/Kotlin 代码最终转换为一个或多个 classes*.dex,C/C++ 代码编译成各 ABI 的.so。打包工具把 DEX、Manifest、资源、assets 与 SO 装入 APK。 发布者用私钥给 APK 签名。签名把该版本内容与签名密钥绑定,并让系统检查安装包是否在签名后被修改。 手机安装 APK;启动时系统依据 Manifest 创建进程、初始化组件并装载代码。
classes.dex 中,JADX 或 Androguard 读取 DEX 的类表、方法表和指令,就能建立业务代码地图。加固要阻止的,正是这种“拿到文件就能直接读代码”的路径。站在开发者角度,“进行一次加固”是什么流程?
从操作界面看并不神秘:准备普通发布 APK → 交给加固工具 → 选择保护项 → 取得加固产物 → 按发布策略签名 → 在真实设备回归测试。界面按钮可能只有几步,复杂工作发生在工具对 APK 的内部改造中。
按钮只是把一组编译、变换、注入、重打包和签名操作封装起来。使用者不需要理解壳的每个指令,但分析者必须从变化后的字节、启动入口和运行行为反推出这些步骤。
加固工具在 APK 内部究竟做了什么?
开发者先生成普通 APK,再把它交给本地加固程序或加固服务。不同厂商、不同版本的实现不完全相同,但传统 DEX 壳通常会完成下面几类变换:
- 取出或改造真实 DEX:
将业务 DEX 压缩、加密、分段、改头或转成壳自定义的 payload,使通用反编译器不能直接解析。 - 放回隐藏 payload:
保护后的数据可能放入 assets、某个 DEX 的特殊区域、native 库或自定义文件中。并非所有壳都使用同一路径。 - 注入壳程序:
加入少量可直接启动的 StubApp 和 native 壳库。它们不是主要业务,而是负责校验、解密、映射和接管类加载。 - 改启动清单:
传统壳常把 Manifest 中的 Application 改为壳 Application,让壳比业务页面更早运行;也存在使用其他初始化点的实现。 - 重新打包与签名:
APK 字节已经变化,原签名不再有效,所以要重新对齐并签名。最终由谁持有签名密钥,取决于实际加固和发布流程。

加固工具不会把外层随意变成陌生格式;最终产物仍要满足 APK 结构、Manifest、签名和 Android 安装校验。被隐藏的主要是业务代码的可读形态,外层仍是 Android 能接受的安装包。
SO 是特定 CPU 的机器码。开发者不知道用户手机一定是 ARM64、32 位 ARM 还是模拟器常见的 x86,因此壳可准备多个 ABI 版本,启动时选择与设备匹配的一份。本次 APK 中四个文件的存在是实测事实,但不能反推四种设备都实际运行过。
签名校验覆盖 APK 内容。Manifest、DEX 或资源改变后,旧签名对应的摘要不再匹配;不重新签名,系统会拒绝把它当作完整、有效的安装包。
加固后的 App 启动时怎样“自己解开自己”?
Android 读取 Manifest,看到的不是原 Application,而是 StubApp。 StubApp 判断设备 ABI,释放或定位匹配的 native 壳库,并通过 System.load或System.loadLibrary加载。壳库定位受保护 payload,完成环境校验、解密、解压或结构修复。 壳把还原的 DEX 写到私有目录或直接映射到内存,并调整 ClassLoader、运行时数据结构或装载链。 壳创建原业务 Application,随后真实 Activity、网络层和业务逻辑继续运行。某些壳还会持续执行完整性校验、反调试或方法级保护。
密钥可以由壳内常量、签名、设备信息或多段数据组合派生,也可以由 native 代码实现自定义变换。分析师不应看见一串字符就宣布它是密钥,必须以代码调用链和解密结果核验。
因为 App 必须在用户手机上完成真实业务。无论密钥怎样保存,运行时最终都要得到足够的信息,让 ART 能使用真实类和方法。动态脱壳通常不先破解算法,而是在壳已经完成还原后,从内存复制结果。
混淆、加固、签名、反调试不是一回事
native。 | ||
金融、游戏、企业服务和其他类型的 App 都可能为了防盗版、防篡改或提高逆向成本而加固。加固只描述代码保护方式;App 的具体业务性质必须结合功能、通信和实际运行结果另行分析。
通常得到一个或多个运行时 DEX、类定义或方法代码,便于静态分析。它不自动恢复开发者工程、注释、原变量名,也不保证能重新打成可安装 APK。本次实验的完成标准是“包含样本功能代码的 DEX 可解析且真实方法体可复核”,不是“制作破解版”。
.jgapp、libjiagu、com.stub.StubApp、文件大小、哈希和 DEX 数量时,才是对本次真实 APK 与隔离设备运行结果的确定性观察。先把脱壳想成打开保险箱
APK 加固像把真实程序锁进保险箱。JADX 只能看到锁和开锁程序,也就是 StubApp。App 想正常运行,壳最终必须把真实代码解密并交给 Android。我们等待它自己开锁,再从内存复制已经解密的 DEX。

因为导出的文件可能校验错误,还可能混入系统 DEX。必须修复、解析、筛选并找到真实业务方法。
判断有没有壳,以及是什么壳
先固定样本身份
PROJECT=.
APK=$(find "$PROJECT/samples" -type f -name 'authorized_sample.apk' -print -quit)
test -n "$APK"
stat -c '%s bytes' "$APK"
md5sum "$APK"
sha256sum "$APK"
unzip -tq "$APK"原始 APK 文件大小:内部复核值不公开
原始 APK MD5:内部复核值不公开
原始 APK SHA256:内部复核值不公开
ZIP 完整性:通过后面会出现设备端 base.apk、原始 dump、修复 DEX 和 JADX 源码。没有起始哈希,就无法证明这些结果来自哪一份样本。
寻找 360 特征
unzip -l "$APK" | grep -E 'AndroidManifest.xml|assets/\.jgapp|assets/libjiagu|classes.dex'
python -m apkfx.cli subpoena "$APK"subpoena 是本地项目核验器,用于补充统一基线;公开读者没有该工具时,仍可使用下面的 ZIP、Manifest、StubApp 代码与运行时结果完成四层复核,它不是品牌判断的唯一依据。
单个文件名可以伪造或残留。本例同时核对 ZIP 特征、StubApp 实际加载代码和 subpoena 核验器,三路一致才判断为 360 加固保。
不是背文件名:用四层依据闭环判断
逐条复核命令
# 1. 文件层:查壳资源
unzip -l "$APK" | grep -E 'assets/(\.jgapp|libjiagu.*\.so)'
# 2. 查看 .jgapp 原始内容,不猜它的含义
unzip -p "$APK" assets/.jgapp | xxd -g 1
# 3. Manifest 层:看真正启动的 Application
aapt dump xmltree "$APK" AndroidManifest.xml | grep -A12 -E '^ E: application'
# 4. 代码层:检查壳是否真的加载这些组件
grep -nE 'entryRunApplication|libjiagu|/\.jiagu|DtcLoader|interface5|loadClass' pre/jadx_out/sources/com/stub/StubApp.java.jgapp 的 16 字节内容:
32 65 33 35 64 65 31 30 35 33 64 37 38 35 35 66
ASCII:2e35de1053d7855f
Manifest:
android:name="com.stub.StubApp"
StubApp:
strEntryApplication = "entryRunApplication"
c = "libjiagu"
absolutePath + "/.jiagu"
System.load(...)
DtcLoader.init()
interface5(a)
classLoader.loadClass(strEntryApplication)本报告只观察到它是 ASCII 字符串
2e35de1053d7855f,没有厂商格式文档证明各字段含义。因此只把“文件存在、路径固定、与加载链共同出现”作为特征,不臆测它是版本号、密钥或客户 ID。StubApp 是通用名称,其他壳或仿制实现也能使用。只有它同时加载 libjiagu、释放到 .jiagu、调用 native 接口并切换真实 Application,品牌和功能判断才有闭环。
实际工作中的判定口径

数原包中真正定义了多少类
原包 classes.dex 大小:14309416 字节
classes.dex SHA256:
fdf3d386edc8ee9e7bf38d439d66df27d6ef976e9059164583187f92d98f9525
Androguard class_def:4
Lcom/stub/StubApp;
Lcom/tianyu/util/Configuration;
Lcom/tianyu/util/DtcLoader;
Lcom/tianyu/util/a;体积不能代表业务代码量。加固可以填充数据或把 payload 放进 DEX 的非普通类区域。class_def 才是当前 DEX 明确暴露的类定义。
理解 StubApp:壳怎样把真代码装进内存?
Application 为什么是壳入口?
// StubApp.java:33-40
public final class StubApp extends Application {
public static String strEntryApplication = "entryRunApplication";
private static String c = "libjiagu";
private static boolean loadFromLib = false;
}Application 比界面创建得早。壳必须在任何业务 Activity 加载前准备真 DEX 和 ClassLoader,所以利用 attachBaseContext 和 onCreate。
壳释放并加载 native 库
// StubApp.java:573-637
String str = absolutePath + "/.jiagu";
a.a(context, c + "_a64.so", str, i);
System.load(str + "/" + i);
DtcLoader.init();
interface5(a);Java DEX 容易反编译,native ELF 的控制流、符号和反调试成本更高。解密、内存映射和类加载修复因此落在 libjiagu 中。
assets 中的文件不是普通动态库路径。壳根据 CPU 架构复制对应 so 到应用私有目录,再用 System.load 加载绝对路径。
壳接回真实 Application
Class<?> real = context.getClassLoader().loadClass(strEntryApplication);
b = (Application) real.newInstance();
Method attach = Application.class.getDeclaredMethod("attach", Context.class);
attach.setAccessible(true);
attach.invoke(b, context);
interface8(b, context);ClassLoader 只有在 ART 已经认识该类定义时才能加载它。壳准备加载 entryRunApplication,意味着 native 阶段已经映射或注册了承载该类的 DEX。
搭建环境:为什么版本问题比命令问题更常见?
本次只使用 0 号隔离机:Redmi K40、Android 13、arm64、Magisk root。1 号机不参与;设备不放个人 SIM、账号、照片或支付数据。
python3 --version
apktool --version
jadx --version
androguard version
./.venv-frida16/bin/frida --version
./.venv-frida16/bin/frida-dexdump --version
adb -s YOUR_DEVICE_SERIAL shell 'getprop ro.product.cpu.abilist; su -c id'Python 3.13.12
Apktool 2.11.1
JADX 1.5.2
Androguard 4.1.4
Frida Python 16.7.19
frida-dexdump 2.0.1
设备:arm64-v8a / uid=0(root)准备主机和设备端
python3 -m venv .venv-frida16
./.venv-frida16/bin/pip install "frida==16.7.19" "frida-tools==13.7.0" "frida-dexdump==2.0.1"
adb -s YOUR_DEVICE_SERIAL push frida-server-16.6.6-android-arm64 /data/local/tmp/frida-server16
adb -s YOUR_DEVICE_SERIAL shell su -c 'chmod 755 /data/local/tmp/frida-server16'
adb -s YOUR_DEVICE_SERIAL shell su -c '/data/local/tmp/frida-server16 >/data/local/tmp/frida16.log 2>&1 &'
adb -s YOUR_DEVICE_SERIAL forward tcp:27042 tcp:27042
./.venv-frida16/bin/frida-ps -D YOUR_DEVICE_SERIAL从零安装服务端时,可检索 Frida 官方文档中的「Android」章节;本次实验实际使用的主机端、设备端命令和探活方法已完整写在下方内嵌附录中。
主机端是控制器;设备端 frida-server 在 Android 上完成进程注入和内存读取。两端缺一不可。
进程存在不代表协议兼容、端口可达或目标可注入。必须用相同虚拟环境的 frida-ps 成功枚举设备进程。
真实失败:Frida 17 链路
frida-dexdump -D YOUR_DEVICE_SERIAL -f YOUR_AUTHORIZED_PACKAGE -o runtime/dexdump_attempt_1 --sleep 8Spawning 'YOUR_AUTHORIZED_PACKAGE'...
Failed to spawn: unable to find process with name 'system_server'错误发生在 Frida 建立 spawn 链路时,没有观察到样本检测代码的直接依据。切换 Frida 16 后成功,证明这一轮首先是环境故障。
让壳自己解密,再从内存抓 DEX
安装并核对设备端 APK
adb -s YOUR_DEVICE_SERIAL push "$APK" /data/local/tmp/authorized_sample.apk
adb -s YOUR_DEVICE_SERIAL shell su -c 'pm install -r -g /data/local/tmp/authorized_sample.apk'
BASE=$(adb -s YOUR_DEVICE_SERIAL shell su -c 'pm path YOUR_AUTHORIZED_PACKAGE' | sed -n 's/^package://p' | tr -d '\r')
adb -s YOUR_DEVICE_SERIAL shell su -c "sha256sum '$BASE'"安装:Success
设备端 base.apk SHA256:与主机端原始样本的完整 SHA256 一致
具体值:公开版不展示安装时可能选错同包名版本。设备端 base.apk 与原始样本一致,才能确认随后抓到的内存来自同一份 APK。
执行运行时转储
./.venv-frida16/bin/frida-dexdump -D YOUR_DEVICE_SERIAL -f YOUR_AUTHORIZED_PACKAGE -o runtime/dexdump_attempt_2 --sleep 8 2>&1 | tee runtime/dexdump_attempt_2/console.logSpawning 'YOUR_AUTHORIZED_PACKAGE'...
Attach. PID=23347
Waiting 8s...
Searching...
Successful found 49 dex, used 3 time.
Starting dump...
DexMd5=9cf6c991b4ff82e5206875a4780437f2
SavePath=runtime/dexdump_attempt_2/classes.dex
DexSize=0x526784
...
Error: access violation accessing 0x762dc5e000
...
All done...spawn 从进程创建阶段接管,时序更可控。手动 attach 可能错过壳初始化,也可能每次落在不同页面状态。
立即扫描可能只有壳 DEX。等待给 StubApp、libjiagu 和 ClassLoader 时间完成真代码映射。第二轮等待 15 秒,用来检验时序差异。
发现是候选内存范围;保存要求整个范围可读并成功落盘。部分范围跨越不可读页,另有 MD5 去重,因此两个数字不能混写。

内存映射会随类加载、系统框架和运行阶段变化。两轮有 37 个相同 SHA256,主体稳定,但候选区域数量不是固定常量。

为什么 dump 出来还不能直接放进 JADX?
for f in runtime/dexdump_attempt_2/*.dex; do file -b "$f"; done | sort | uniq -c15 Dalvik dex file version 035
27 Dalvik dex file version 039file 主要检查魔数和版本;严格解析器还会验证文件长度、SHA-1 signature、Adler32 checksum 和 map 表。像一本书封面正确,不代表目录和页码没有损坏。
修复前:
Androguard 可解析 5/42
失败 37/42
其中 14 个:Wrong Adler32 checksum for DEX file
其中 23 个:HiddenApiClassDataItem 解析异常查看 frida-dexdump 的修复边界
frida-dexdump 2.0.1 的 fix_header() 只修:
magic / file_size / header_size / endian_tag
没有重算:
SHA-1 signature / Adler32 checksumsignature 覆盖偏移 0x20 到文件末尾;checksum 覆盖 0x0c 到文件末尾,包含 signature。顺序颠倒会让刚算好的 checksum 失效。
保留原件,只修复副本
python tools/repair_dex_headers.py runtime/dexdump_attempt_2 runtime/repaired_attempt_2 | tee runtime/repaired_attempt_2_manifest.tsvrepaired[0x0C:0x20] = SHA1(repaired[0x20:])
repaired[0x08:0x0C] = Adler32(repaired[0x0C:])修复会改变字节和哈希。覆盖原件就无法复核工具做了什么。正确做法是保存 source SHA256、repaired SHA256 和一一映射。

从 42 个 DEX 中建立可分析的源码集合
Android 进程还会映射 boot classpath、系统框架和 MIUI 组件。内存扫描只认识 DEX,不认识归属;全部计入会夸大业务代码量。
按解析能力和命名空间筛选
修复后选入分析集合的 7 个 DEX 均能被 Androguard 解析,合计 27,210 个 class_def。它们出现 com/tiocloud、com/watayouxiang,也包含 Google、华为、腾讯、AndroidX、极光、支付宝和 WebRTC 等第三方依赖;选择它们是为了恢复样本功能代码及调用关系,不表示集合内全部代码属于样本自研。其他可解析文件继续按逐 DEX 命名空间表排查。
这些包可能来自公开框架、商业组件或二次开发。它们能说明业务技术栈相关,但不能单凭包名推断具体开发团队。
恢复源码
jadx -q -d runtime/jadx_repaired_core runtime/repaired_attempt_2/classes.dex runtime/repaired_attempt_2/classes02.dex runtime/repaired_attempt_2/classes03.dex runtime/repaired_attempt_2/classes04.dex runtime/repaired_attempt_2/classes05.dex runtime/repaired_attempt_2/classes06.dex runtime/repaired_attempt_2/classes07.dexJava 文件总数:14,275
com/tiocloud:564
com/watayouxiang:1,080用真实方法体证明恢复成功
// QueryConfReq.java:18-32
public TioMap<String, String> k() {
return TioMap.getParamMap().append("id", this.id);
}
public String l() {
return "/api1/conf/confAppController/queryConf";
}// PwdRegisterReq.java:18-37
this.mobile = str;
this.pwd = df2.a(str2);
this.areacode = str3;
...
return "/api1/user/userAppController/pwdRegister";
原包只有壳类,修复后出现注册参数和业务 API path,证明真业务 DEX 已恢复。但代码存在只是静态能力;本次未登录,也没有动态观察这些请求被调用。
脱壳前后到底有什么区别?
重打包还涉及真实 Application、资源引用、native 桥、签名和完整性检测。用于静态分析时,恢复业务 DEX 已达到本次研究目的;可运行 APK 必须作为独立实验验证。
AI 没有替我脱壳,它把“不会”拆成了下一步
这次最有价值的变化,不是 AI 记住了多少命令,而是每次卡住时,它都能把一个模糊问题改写成下一项可以测量的问题。答案仍然可能错,所以每一步都要回到真实文件和真实输出。
frida-ps 探活并成功转储。 | ||
AI 生成的数字不能代替实测结果。哈希、DEX 数量、checksum、class_def 和源码命中必须由确定性程序计算。AI 可以组织步骤、解释异常、生成脚本草稿;样本身份、代码归属、结论边界仍需逐项核对。
初学者最容易卡住的六个为什么
为什么照抄命令仍然失败?
先检查设备序列号、ABI、root、客户端/服务端版本和 frida-ps。命令格式通常不是首要问题。
为什么只得到 StubApp?
可能仍在静态分析原包,也可能扫描太早。确认壳已运行,重置应用状态后调整 sleep 复测,并分别保存每轮日志。
为什么 DEX 有魔数却打不开?
检查 file_size、SHA-1、Adler32 和 map 表,不要只依赖 file 命令。
为什么 JADX 输出很多系统类?
内存扫描会抓到整个进程映射的 DEX。按包名、Manifest、调用关系和第三方组件清单排噪。
为什么恢复后还有 native 方法?
DEX 整体隐藏与方法 native 化是两层保护。抓到 DEX 只解决第一层。
为什么要跑两次?
内存映射受启动时序影响。重复实验能区分稳定主体与偶然映射,本次两轮共有 37 个完全相同 SHA256。
完整复现:让每个数字都能重新算出来
到这里,脱壳的完整路径已经讲完。但如果只留下“转储出 42 个 DEX”和“恢复出 14,275 个 Java 文件”,这仍然只是一组结论,还不是一条可以复核的技术链。
下面给出本次实验的执行顺序、公开脱敏后的 Frida 失败与成功日志、42 个 DEX 的修复映射与逐文件解析清单,以及两份完整脚本。它们分别回答四个问题:命令怎样复跑、失败怎样判断、文件怎样一一对应、数字怎样由程序计算。
附录 A:从样本基线到源码恢复的最短执行顺序
# 0. 在项目目录中使用相对路径,并在 Bash 中按顺序执行
set -eu
RUN=./work/reproduction
APK=./samples/authorized_sample.apk
SERIAL=YOUR_DEVICE_SERIAL
PKG=YOUR_AUTHORIZED_PACKAGE
FRIDA_SERVER=./tools/frida-server-16.6.6-android-arm64
FRIDA_VENV=./.venv-frida16
TOOLS=./tools
mkdir -p "$RUN"/{pre,runtime,environment}
# 在切换工作目录前固定变量;正文不暴露主机真实目录
RUN=$(realpath "$RUN")
APK=$(realpath "$APK")
FRIDA_SERVER=$(realpath "$FRIDA_SERVER")
FRIDA_VENV=$(realpath "$FRIDA_VENV")
TOOLS=$(realpath "$TOOLS")
cd "$RUN"
# 先将本文附录 E、F 的代码分别保存为以下两个文件
test -f "$TOOLS/repair_dex_headers.py"
test -f "$TOOLS/verify_dex_set.py"
test -f "$APK"
test -f "$FRIDA_SERVER"
test -x "$FRIDA_VENV/bin/frida-ps"
# 1. 固定样本和环境
stat -c '%n\t%s bytes' "$APK"
md5sum "$APK"
sha256sum "$APK"
unzip -tq "$APK"
adb version
"$FRIDA_VENV/bin/pip" freeze | sort
adb -s "$SERIAL" shell 'getprop ro.product.cpu.abilist; getenforce; su -c id'
# 2. 静态判壳
unzip -l "$APK" | grep -E 'AndroidManifest.xml|assets/\.jgapp|assets/libjiagu|classes.dex'
aapt dump xmltree "$APK" AndroidManifest.xml | grep -A12 -E '^ E: application'
apktool d --no-res --force -o pre/apktool_out "$APK"
jadx -q -d pre/jadx_out "$APK"
grep -nE 'entryRunApplication|libjiagu|/\.jiagu|DtcLoader|interface5|loadClass' \
pre/jadx_out/sources/com/stub/StubApp.java
# 3. 安装前确认专用设备不存在同包名应用
adb -s "$SERIAL" shell pm path "$PKG"
# 若返回 package: 路径,先停止;封存原状态后再人工处理,不直接覆盖。
# 4. 启动已核对版本和完整 SHA256 的 frida-server
sha256sum "$FRIDA_SERVER" | tee environment/frida-server-host-sha256.txt
adb -s "$SERIAL" shell su -c \
'for n in frida-server frida-server16 frida-server17; do p=$(pidof $n 2>/dev/null); [ -z "$p" ] || kill $p; done'
adb -s "$SERIAL" forward --remove tcp:27042 2>/dev/null || true
adb -s "$SERIAL" push "$FRIDA_SERVER" /data/local/tmp/frida-server16
adb -s "$SERIAL" shell su -c 'chmod 755 /data/local/tmp/frida-server16'
adb -s "$SERIAL" shell su -c 'sha256sum /data/local/tmp/frida-server16' \
| tee environment/frida-server-device-sha256.txt
adb -s "$SERIAL" shell su -c \
'/data/local/tmp/frida-server16 >/data/local/tmp/frida16.log 2>&1 &'
adb -s "$SERIAL" forward tcp:27042 tcp:27042
"$FRIDA_VENV/bin/frida-ps" -D "$SERIAL" | tee runtime/frida16-ps.log
# 5. 安装后复核设备端 base.apk
adb -s "$SERIAL" push "$APK" /data/local/tmp/authorized_sample.apk
adb -s "$SERIAL" shell su -c 'pm install -g /data/local/tmp/authorized_sample.apk'
BASE=$(adb -s "$SERIAL" shell su -c "pm path $PKG" \
| sed -n 's/^package://p' | tr -d '\r')
adb -s "$SERIAL" shell su -c "sha256sum '$BASE'"
# 6. 运行时转储
mkdir -p runtime/dexdump_attempt_2
"$FRIDA_VENV/bin/frida-dexdump" -D "$SERIAL" -f "$PKG" \
-o runtime/dexdump_attempt_2 --sleep 8 \
2>&1 | tee runtime/dexdump_attempt_2/console.log
(cd runtime/dexdump_attempt_2 && sha256sum classes*.dex) \
| tee runtime/dexdump_attempt_2/sha256.txt
grep -c '^ERROR:frida-dexdump:\[-\] Error: access violation' \
runtime/dexdump_attempt_2/console.log \
| tee runtime/dexdump_attempt_2/top-level-access-violation-count.txt
# 7. 卸载重装后进行第二轮转储,避免沿用应用私有数据
adb -s "$SERIAL" shell su -c "am force-stop $PKG; pm uninstall $PKG" \
| tee runtime/reset-before-attempt3.txt
adb -s "$SERIAL" shell su -c \
'pm install -g /data/local/tmp/authorized_sample.apk' \
| tee runtime/install-attempt3.txt
BASE=$(adb -s "$SERIAL" shell su -c "pm path $PKG" \
| sed -n 's/^package://p' | tr -d '\r')
adb -s "$SERIAL" shell su -c "sha256sum '$BASE'" \
| tee runtime/device-base-attempt3-sha256.txt
mkdir -p runtime/dexdump_attempt_3
"$FRIDA_VENV/bin/frida-dexdump" -D "$SERIAL" -f "$PKG" \
-o runtime/dexdump_attempt_3 --sleep 15 \
2>&1 | tee runtime/dexdump_attempt_3/console.log
(cd runtime/dexdump_attempt_3 && sha256sum classes*.dex) \
| tee runtime/dexdump_attempt_3/sha256.txt
awk '{print $1}' runtime/dexdump_attempt_2/sha256.txt | sort -u \
> runtime/attempt2-sha256-only.txt
awk '{print $1}' runtime/dexdump_attempt_3/sha256.txt | sort -u \
> runtime/attempt3-sha256-only.txt
comm -12 runtime/attempt2-sha256-only.txt runtime/attempt3-sha256-only.txt \
| tee runtime/attempt2-attempt3-sha256-intersection.txt \
| wc -l | tee runtime/attempt2-attempt3-intersection-count.txt
# 8. 原件不覆盖,修复到新目录
python "$TOOLS/repair_dex_headers.py" \
runtime/dexdump_attempt_2 runtime/repaired_attempt_2 \
| tee runtime/repaired_attempt_2_manifest.tsv
# 9. 修复前后使用同一解析器逐文件核验
python "$TOOLS/verify_dex_set.py" runtime/dexdump_attempt_2 \
--tsv runtime/dexdump_attempt_2_verify.tsv \
--summary runtime/dexdump_attempt_2_verify.json
python "$TOOLS/verify_dex_set.py" runtime/repaired_attempt_2 \
--tsv runtime/repaired_attempt_2_verify.tsv \
--summary runtime/repaired_attempt_2_verify.json
# 10. 只把已经解释纳入理由的 DEX 交给 JADX
jadx -q -d runtime/jadx_repaired_analysis_set \
runtime/repaired_attempt_2/classes.dex \
runtime/repaired_attempt_2/classes02.dex \
runtime/repaired_attempt_2/classes03.dex \
runtime/repaired_attempt_2/classes04.dex \
runtime/repaired_attempt_2/classes05.dex \
runtime/repaired_attempt_2/classes06.dex \
runtime/repaired_attempt_2/classes07.dex
# 11. 结束后清理设备;主机产物保留
adb -s "$SERIAL" shell su -c "am force-stop $PKG" 2>/dev/null || true
adb -s "$SERIAL" shell su -c "pm uninstall $PKG" | tee runtime/uninstall.txt
adb -s "$SERIAL" shell su -c \
'for n in frida-server frida-server16 frida-server17; do p=$(pidof $n 2>/dev/null); [ -z "$p" ] || kill $p; done'
adb -s "$SERIAL" shell su -c \
'rm -f /data/local/tmp/authorized_sample.apk /data/local/tmp/frida-server16 /data/local/tmp/frida-server17'
adb -s "$SERIAL" forward --remove tcp:27042 2>/dev/null || trueRUN、APK、SERIAL、FRIDA_SERVER、FRIDA_VENV 和 TOOLS 必须换成本机真实值,并先把附录 E、F 保存到 TOOLS 指向的目录。起点是已经 root 的隔离测试设备;如果存在同包名旧应用或旧 Frida 服务,应先核对并处理状态,不带着未知状态继续执行。附录 B:失败与成功日志的复核口径
B.1 Frida 17 失败日志公开脱敏版(原始 13 行)
-----------------------------------------------------------------------------
__ _ _ _ _
/ _|_ __(_) __| | __ _ __| | _____ ____| |_ _ _ __ ___ _ __
| |_| '__| |/ _` |/ _` |_____ / _` |/ _ \ \/ / _` | | | | '_ ` _ \| '_ \
| _| | | | (_| | (_| |_____| (_| | __/> < (_| | |_| | | | | | | |_) |
|_| |_| |_|\__,_|\__,_| \__,_|\___/_/\_\__,_|\__,_|_| |_| |_| .__/
|_|
https://github.com/hluwa/frida-dexdump
-----------------------------------------------------------------------------
Spawning `YOUR_AUTHORIZED_PACKAGE`...
Failed to spawn: unable to find process with name 'system_server'
B.2 第一次成功转储日志公开脱敏版(原始 160 行)
公开版把真实包名替换为 YOUR_AUTHORIZED_PACKAGE,并把虚拟环境前缀和输出目录改写为 ./.venv-frida16/ 与 ./runtime/;PID、异常地址、DEX 大小、MD5、错误内容、计数和行序均保留。该段不是未经修改的原始日志。
-----------------------------------------------------------------------------
__ _ _ _ _
/ _|_ __(_) __| | __ _ __| | _____ ____| |_ _ _ __ ___ _ __
| |_| '__| |/ _` |/ _` |_____ / _` |/ _ \ \/ / _` | | | | '_ ` _ \| '_ \
| _| | | | (_| | (_| |_____| (_| | __/> < (_| | |_| | | | | | | |_) |
|_| |_| |_|\__,_|\__,_| \__,_|\___/_/\_\__,_|\__,_|_| |_| |_| .__/
|_|
https://github.com/hluwa/frida-dexdump
-----------------------------------------------------------------------------
Spawning `YOUR_AUTHORIZED_PACKAGE`...
INFO:Agent:DexDumpAgent<Connection(pid=Session(pid=23347), connected:True), attached=True>: Attach.
INFO:frida-dexdump:Waiting 8s...
INFO:frida-dexdump:[+] Searching...
INFO:frida-dexdump:[*] Successful found 49 dex, used 3 time.
INFO:frida-dexdump:[+] Starting dump to './runtime/dexdump_attempt_2'...
INFO:frida-dexdump:[+] DexMd5=9cf6c991b4ff82e5206875a4780437f2, SavePath=./runtime/dexdump_attempt_2/classes.dex, DexSize=0x526784
INFO:frida-dexdump:[+] DexMd5=63ec8a22f4cb5794d2b0441552bc3d65, SavePath=./runtime/dexdump_attempt_2/classes02.dex, DexSize=0x2befc4
INFO:frida-dexdump:[+] DexMd5=8df4ba9a0ecf9791e253eba7e83aabe1, SavePath=./runtime/dexdump_attempt_2/classes03.dex, DexSize=0x1b2294
INFO:frida-dexdump:[+] DexMd5=19d4ea49140d1b9e42f41532cd6ef256, SavePath=./runtime/dexdump_attempt_2/classes04.dex, DexSize=0x653f50
INFO:frida-dexdump:[+] DexMd5=59468cbe96edf8765d953b2be683dc0a, SavePath=./runtime/dexdump_attempt_2/classes05.dex, DexSize=0x69410c
INFO:frida-dexdump:[+] DexMd5=f12e61dcd3c6e72758dc4ce1900fd7c1, SavePath=./runtime/dexdump_attempt_2/classes06.dex, DexSize=0x5de378
INFO:frida-dexdump:[+] DexMd5=20ec37aa89701ee1e1bd5c55622abc32, SavePath=./runtime/dexdump_attempt_2/classes07.dex, DexSize=0x25d180
INFO:frida-dexdump:[+] DexMd5=3f5efe431556eba45b0470db6894ce9e, SavePath=./runtime/dexdump_attempt_2/classes08.dex, DexSize=0x5f50
INFO:frida-dexdump:[+] DexMd5=f1771b68f5f9b168b79ff59ae2daabe4, SavePath=./runtime/dexdump_attempt_2/classes09.dex, DexSize=0x11c
INFO:frida-dexdump:[+] DexMd5=2132326755e690cd4729e89d3ecc5555, SavePath=./runtime/dexdump_attempt_2/classes10.dex, DexSize=0x8d034
ERROR:frida-dexdump:[-] Error: access violation accessing 0x762dc5e000
at <anonymous> (frida/runtime/core.js:145)
at memorydump (src/search.ts:41)
at call (native)
at <anonymous> (frida/runtime/message-dispatcher.js:11)
at o (frida/runtime/message-dispatcher.js:23): {'addr': '0x762d7db620', 'size': 6923808}
Traceback (most recent call last):
File "./.venv-frida16/lib/python3.13/site-packages/frida_dexdump/__main__.py", line 81, in dump
bs = self.agent.memory_dump(dex['addr'], dex['size'])
File "./.venv-frida16/lib/python3.13/site-packages/frida_dexdump/agent/__init__.py", line 24, in memory_dump
return self._rpc.memorydump(base, size)
~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^
File "./.venv-frida16/lib/python3.13/site-packages/frida/core.py", line 180, in method
return script._rpc_request(request, data, **kwargs)
~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^
File "./.venv-frida16/lib/python3.13/site-packages/frida/core.py", line 86, in wrapper
return f(*args, **kwargs)
File "./.venv-frida16/lib/python3.13/site-packages/frida/core.py", line 497, in _rpc_request
raise result.error
frida.core.RPCException: Error: access violation accessing 0x762dc5e000
at <anonymous> (frida/runtime/core.js:145)
at memorydump (src/search.ts:41)
at call (native)
at <anonymous> (frida/runtime/message-dispatcher.js:11)
at o (frida/runtime/message-dispatcher.js:23)
ERROR:frida-dexdump:[-] Error: access violation accessing 0x762dc5e000
at <anonymous> (frida/runtime/core.js:145)
at memorydump (src/search.ts:41)
at call (native)
at <anonymous> (frida/runtime/message-dispatcher.js:11)
at o (frida/runtime/message-dispatcher.js:23): {'addr': '0x762d7db700', 'size': 6660980}
Traceback (most recent call last):
File "./.venv-frida16/lib/python3.13/site-packages/frida_dexdump/__main__.py", line 81, in dump
bs = self.agent.memory_dump(dex['addr'], dex['size'])
File "./.venv-frida16/lib/python3.13/site-packages/frida_dexdump/agent/__init__.py", line 24, in memory_dump
return self._rpc.memorydump(base, size)
~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^
File "./.venv-frida16/lib/python3.13/site-packages/frida/core.py", line 180, in method
return script._rpc_request(request, data, **kwargs)
~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^
File "./.venv-frida16/lib/python3.13/site-packages/frida/core.py", line 86, in wrapper
return f(*args, **kwargs)
File "./.venv-frida16/lib/python3.13/site-packages/frida/core.py", line 497, in _rpc_request
raise result.error
frida.core.RPCException: Error: access violation accessing 0x762dc5e000
at <anonymous> (frida/runtime/core.js:145)
at memorydump (src/search.ts:41)
at call (native)
at <anonymous> (frida/runtime/message-dispatcher.js:11)
at o (frida/runtime/message-dispatcher.js:23)
ERROR:frida-dexdump:[-] Error: access violation accessing 0x762dc5e000
at <anonymous> (frida/runtime/core.js:145)
at memorydump (src/search.ts:41)
at call (native)
at <anonymous> (frida/runtime/message-dispatcher.js:11)
at o (frida/runtime/message-dispatcher.js:23): {'addr': '0x762d7dbfc0', 'size': 5426448}
Traceback (most recent call last):
File "./.venv-frida16/lib/python3.13/site-packages/frida_dexdump/__main__.py", line 81, in dump
bs = self.agent.memory_dump(dex['addr'], dex['size'])
File "./.venv-frida16/lib/python3.13/site-packages/frida_dexdump/agent/__init__.py", line 24, in memory_dump
return self._rpc.memorydump(base, size)
~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^
File "./.venv-frida16/lib/python3.13/site-packages/frida/core.py", line 180, in method
return script._rpc_request(request, data, **kwargs)
~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^
File "./.venv-frida16/lib/python3.13/site-packages/frida/core.py", line 86, in wrapper
return f(*args, **kwargs)
File "./.venv-frida16/lib/python3.13/site-packages/frida/core.py", line 497, in _rpc_request
raise result.error
frida.core.RPCException: Error: access violation accessing 0x762dc5e000
at <anonymous> (frida/runtime/core.js:145)
at memorydump (src/search.ts:41)
at call (native)
at <anonymous> (frida/runtime/message-dispatcher.js:11)
at o (frida/runtime/message-dispatcher.js:23)
INFO:frida-dexdump:[+] DexMd5=77dc553d1e0111391054c418ffa7a101, SavePath=./runtime/dexdump_attempt_2/classes11.dex, DexSize=0x2c6108
INFO:frida-dexdump:[+] DexMd5=df0f44584b671b49d45069b471457c88, SavePath=./runtime/dexdump_attempt_2/classes12.dex, DexSize=0x26361c
INFO:frida-dexdump:[+] DexMd5=35186e725af35abf9475c15fb7cae9cc, SavePath=./runtime/dexdump_attempt_2/classes13.dex, DexSize=0x1b86f4
ERROR:frida-dexdump:[-] Error: access violation accessing 0x762dc5e000
at <anonymous> (frida/runtime/core.js:145)
at memorydump (src/search.ts:41)
at call (native)
at <anonymous> (frida/runtime/message-dispatcher.js:11)
at o (frida/runtime/message-dispatcher.js:23): {'addr': '0x762d7e17b0', 'size': 6179036}
Traceback (most recent call last):
File "./.venv-frida16/lib/python3.13/site-packages/frida_dexdump/__main__.py", line 81, in dump
bs = self.agent.memory_dump(dex['addr'], dex['size'])
File "./.venv-frida16/lib/python3.13/site-packages/frida_dexdump/agent/__init__.py", line 24, in memory_dump
return self._rpc.memorydump(base, size)
~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^
File "./.venv-frida16/lib/python3.13/site-packages/frida/core.py", line 180, in method
return script._rpc_request(request, data, **kwargs)
~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^
File "./.venv-frida16/lib/python3.13/site-packages/frida/core.py", line 86, in wrapper
return f(*args, **kwargs)
File "./.venv-frida16/lib/python3.13/site-packages/frida/core.py", line 497, in _rpc_request
raise result.error
frida.core.RPCException: Error: access violation accessing 0x762dc5e000
at <anonymous> (frida/runtime/core.js:145)
at memorydump (src/search.ts:41)
at call (native)
at <anonymous> (frida/runtime/message-dispatcher.js:11)
at o (frida/runtime/message-dispatcher.js:23)
INFO:frida-dexdump:[+] DexMd5=c7f2571de2257af0a1336d04336781d0, SavePath=./runtime/dexdump_attempt_2/classes14.dex, DexSize=0x16d40
INFO:frida-dexdump:[+] DexMd5=d9d7cc39a091c08cc354f0c92177e919, SavePath=./runtime/dexdump_attempt_2/classes15.dex, DexSize=0x3e618
INFO:frida-dexdump:[+] DexMd5=44084e6878cbb31f2ca70c0bd8ddd26b, SavePath=./runtime/dexdump_attempt_2/classes16.dex, DexSize=0x11c
INFO:frida-dexdump:[+] DexMd5=9340a52ffa4ec1fc93ee99b14ca429e6, SavePath=./runtime/dexdump_attempt_2/classes17.dex, DexSize=0xf765c
INFO:frida-dexdump:[+] DexMd5=5b7ff8a6157b776643b396df4734570f, SavePath=./runtime/dexdump_attempt_2/classes18.dex, DexSize=0x79edc
INFO:frida-dexdump:[+] DexMd5=d0385abed2fd7f2c1bd8c2dcdf9f5966, SavePath=./runtime/dexdump_attempt_2/classes19.dex, DexSize=0xa61ac
INFO:frida-dexdump:[+] DexMd5=000e3b8b1bcb10f3c85f982fbbf018ea, SavePath=./runtime/dexdump_attempt_2/classes20.dex, DexSize=0x2b3930
INFO:frida-dexdump:[+] DexMd5=685328b2411b5406a94df6cb1116b963, SavePath=./runtime/dexdump_attempt_2/classes21.dex, DexSize=0x96190
INFO:frida-dexdump:[+] DexMd5=5db6db3feb9f37ec3acf82c63918c3a8, SavePath=./runtime/dexdump_attempt_2/classes22.dex, DexSize=0x4f1304
INFO:frida-dexdump:[+] DexMd5=9c579e4ebd6b165c9b330eb5c1132a53, SavePath=./runtime/dexdump_attempt_2/classes23.dex, DexSize=0x127f3c
INFO:frida-dexdump:[+] DexMd5=16322f1041c3ce8b9266f53efd5298a9, SavePath=./runtime/dexdump_attempt_2/classes24.dex, DexSize=0x50685c
INFO:frida-dexdump:[+] DexMd5=8e9228435eb9bd695e87af29735241f5, SavePath=./runtime/dexdump_attempt_2/classes25.dex, DexSize=0x78bf0
INFO:frida-dexdump:[+] DexMd5=4a582f161d121ec02bdc55a0af6b36d7, SavePath=./runtime/dexdump_attempt_2/classes26.dex, DexSize=0x168bb8
INFO:frida-dexdump:[+] DexMd5=267ebbddfcc88da5a55e524c07562ed4, SavePath=./runtime/dexdump_attempt_2/classes27.dex, DexSize=0x899cc
INFO:frida-dexdump:[+] DexMd5=828ac28b03147eda52e265863705e4a6, SavePath=./runtime/dexdump_attempt_2/classes28.dex, DexSize=0x71508
INFO:frida-dexdump:[+] DexMd5=20cba490ad8f1d764c2d087459e77a42, SavePath=./runtime/dexdump_attempt_2/classes29.dex, DexSize=0x10930
INFO:frida-dexdump:[+] DexMd5=2313656b960affe9cdb07d062ee2cd21, SavePath=./runtime/dexdump_attempt_2/classes30.dex, DexSize=0x5d634
INFO:frida-dexdump:[+] DexMd5=9c519a9ca23b8c843959c0b761ff3dff, SavePath=./runtime/dexdump_attempt_2/classes31.dex, DexSize=0x2b524
INFO:frida-dexdump:[+] DexMd5=cb40c7107cc3bca30ff03e236ecea9cf, SavePath=./runtime/dexdump_attempt_2/classes32.dex, DexSize=0xfeac
INFO:frida-dexdump:[+] DexMd5=198255ad5c1b84959d85114d5ca879ad, SavePath=./runtime/dexdump_attempt_2/classes33.dex, DexSize=0x124cc
INFO:frida-dexdump:[+] DexMd5=ae88554cf19ecaea2079e4880375d6ca, SavePath=./runtime/dexdump_attempt_2/classes34.dex, DexSize=0x22964
INFO:frida-dexdump:[+] DexMd5=4d8db8d5b46c13abf0b5b2971fe44de2, SavePath=./runtime/dexdump_attempt_2/classes35.dex, DexSize=0x12638
INFO:frida-dexdump:[+] DexMd5=3b87770b069461c1cd3c2ffa3b93db99, SavePath=./runtime/dexdump_attempt_2/classes36.dex, DexSize=0x2ffc
INFO:frida-dexdump:[+] DexMd5=4d33839be9bafcb9f0856641d20de47f, SavePath=./runtime/dexdump_attempt_2/classes37.dex, DexSize=0x655c
INFO:frida-dexdump:[+] DexMd5=37307f9265761a236d7444d6405bcf71, SavePath=./runtime/dexdump_attempt_2/classes38.dex, DexSize=0x264
INFO:frida-dexdump:[+] DexMd5=e785555870cc3d0301cee34741bc7ee9, SavePath=./runtime/dexdump_attempt_2/classes39.dex, DexSize=0x23c8
INFO:frida-dexdump:[+] DexMd5=1e29d1936a49ccf21a1c6975c33777b8, SavePath=./runtime/dexdump_attempt_2/classes40.dex, DexSize=0x2ec8
INFO:frida-dexdump:[+] DexMd5=fa7e49bcd3b28de8440f9c7e80ce0e80, SavePath=./runtime/dexdump_attempt_2/classes41.dex, DexSize=0x63bc4
INFO:frida-dexdump:[+] DexMd5=32122b7e7c550aac7ba2d98913c2bff1, SavePath=./runtime/dexdump_attempt_2/classes42.dex, DexSize=0x17b0
INFO:frida-dexdump:[*] All done...
DexMd5 不是落盘文件 MD5。frida-dexdump 2.0.1 的执行顺序是先对内存读取结果计算 MD5,再调用 fix_header() 修改魔数、文件大小、头大小或端序字段,随后写入文件;日志最后打印的仍是修改前 MD5。逐行复算后,33/42 与落盘文件相同,9/42 不同。因此本文不把该字段作为落盘 DEX 的证据指纹,文件身份以附录 C 的完整 SHA256 为准。文件 日志 DexMd5(修头前) 落盘文件 MD5(修头后)
classes.dex 9cf6c991b4ff82e5206875a4780437f2 ea59ca13add07a0d34ecf9a3dc98b148
classes02.dex 63ec8a22f4cb5794d2b0441552bc3d65 41f127905e8800525c854578f17afccb
classes03.dex 8df4ba9a0ecf9791e253eba7e83aabe1 edf4830ececc6b257357aa2eb8d9cfac
classes04.dex 19d4ea49140d1b9e42f41532cd6ef256 62597492b92a5674c251eed86d128727
classes05.dex 59468cbe96edf8765d953b2be683dc0a 1748fe5e6bd911ea5f491829172c4632
classes06.dex f12e61dcd3c6e72758dc4ce1900fd7c1 b2c44b1d63ff8145402f9133b5a586da
classes07.dex 20ec37aa89701ee1e1bd5c55622abc32 602b06b5259a0e1b032fb9376b803374
classes08.dex 3f5efe431556eba45b0470db6894ce9e 4b218375fd1e994995e126b739bccf77
classes29.dex 20cba490ad8f1d764c2d087459e77a42 5eb4a6d181924d368d40d7b71d57f2f8# frida-dexdump 2.0.1 的关键执行顺序
bs = self.agent.memory_dump(dex['addr'], dex['size'])
md = md5(bs)
bs = fix_header(bs)
out.write(bs)
logger.info("DexMd5={}".format(md))B.3 从原始日志形成的计数结论
Frida 17 失败:
Spawning `YOUR_AUTHORIZED_PACKAGE`...
Failed to spawn: unable to find process with name 'system_server'
Frida 16 成功轮:
Attach. PID=23347
Waiting 8s...
Successful found 49 dex, used 3 time.
Starting dump...
All done...
第一次成功轮:
发现内存区域:49
成功保存 DEX:42
落盘总字节:62,155,380
顶层 access violation:4
第二次成功轮:
发现内存区域:47
成功保存 DEX:41
落盘总字节:62,155,096
顶层 access violation:4
两轮 SHA256 交集:37# 只计顶层异常,避免把同一异常的堆栈重复算入
grep -c '^ERROR:frida-dexdump:\[-\] Error: access violation' \
runtime/dexdump_attempt_2/console.logFrida 17 的错误发生在 spawn 链路,没有样本检测代码的直接依据,因此只能写“该环境链路失败”。切换到大版本对齐的 Frida 16 链路并通过
frida-ps 探活后成功,不能把第一次失败包装成样本反调试。附录 C:42 个 DEX 修复前后完整 SHA256 映射
每一行对应一个原始转储及其修复副本。哈希全部完整展示;源文件与修复文件一一对应,原始转储没有被覆盖。
file size source_sha256 repaired_sha256
classes.dex 5400452 55d0b11c5f5ea08522442c063e962339d4e90deb9320595824107442f66bd10d 097b85f40cd3c09b534eeaae2fc3c462c388d073147d15f2399c007d518127be
classes02.dex 2879428 87f008a16dd7a5ef99034de2d474127d2a84f798e494a0d65256bf586eb5d299 e20287ef6b7f0f7110d4b008fae58d2a92f1a02f7a9afed1400cb7e2102484c7
classes03.dex 1778324 31ed4f5a9d7ff9e481cda3956506f50922d6bb147479309dd109ea5f833fb4ef b79b9afce5de1fcc8d063248813e00864ed20c231d79b57be6fd9c5dcc759d5e
classes04.dex 6635344 a968e5686acd48d10c56cd44d357a96fbcd7f8d94373748d6fdbed5150d2a95b 31b8ad6b191c14a34d3ed520568548c758a931a3366220d1ed3952b9ea8a84fa
classes05.dex 6897932 2bf4d11ed2df017fde48d2a252537cf724e7315e0d8266e6d2d30c96bb712f57 0837a02db066727390da25b25087aaf40f20d23718096372e853c69686509e21
classes06.dex 6153080 909ff6406409a738c526e1584f4dd5629ea053f197761afa4ec4e504b37f0e58 1cf99ceee5ecc0c100cfced27486850f4d6910a517f6bf807445990b4f2d73f0
classes07.dex 2478464 491ec99521122a449c24a50b7a67eeb9690e904893040c5ff813753725811f29 75ceacfdeedf4f59a61c5b0cc31b6b441bdb1f30bcb1e974727aa060ae7bf5cd
classes08.dex 24400 960dca48086129f20ea2ba3dd52e5993679f5656f98c209b7799ff5d5aad6790 510c0b2af2defe660fc22b98d224d867dbf7d1e1f895ef08fb9570676e259d24
classes09.dex 284 9f8898ce35a47aeafced99ea0d17c33e73037bb2307c7688e50819966f4ae939 9f8898ce35a47aeafced99ea0d17c33e73037bb2307c7688e50819966f4ae939
classes10.dex 577588 af3d0526fa5c97530deb63b5d2a20e2005dcb3b264211c17ca1ec7ab0a2ab169 af3d0526fa5c97530deb63b5d2a20e2005dcb3b264211c17ca1ec7ab0a2ab169
classes11.dex 2908424 8aba8f65aa004764427e749e68deec2126794b77cfa87fddfb128087c0ce6f15 6bf87caeebb4928eda4822292eabafad3aa55b15e4b90bb2ce89cdadbe2c030e
classes12.dex 2504220 6f95033c783a5d89b93500f8b68d40ce7582500655e046c8e1563259c10c21c1 ea7c7dfd8fafef6d9b448944f5a36a8433c49668ac57b1ceeb1178d25ea8104e
classes13.dex 1804020 322f98136572342334e6facfa6716a5b8e158cc6bf913d7ccabbfa4f879137ad 0629fa4a4819a8ed2a8c329aaca386fd2b03ea4d600bc703b98a4431cf8c2922
classes14.dex 93504 34b666fa173067ba144c64c965fddc355881546e0c680775964294efc9d37e61 60fc0aab932cb91ffa94f3cca37c5a1132b864f1f5191225d10b959fb0f1b4e9
classes15.dex 255512 1131295c7abc870772432c49d6e0bb52fac5160cba4d5742eb1d81518b950db7 1131295c7abc870772432c49d6e0bb52fac5160cba4d5742eb1d81518b950db7
classes16.dex 284 43c449a26afe0f11d132192d8a89104f4048915560bca69117b79aaa057bf4ab f0cad9de0ed83090755ae3515c5d80bdd2de2c27646db339931c519d6394db90
classes17.dex 1013340 1f6a823a1669a4cb51b09a3deca50eb014f20493c3b8d15bab1ebbd6f88eee13 da1f939ffdec1b7d5d9da1b7fe3079e3efad19dc38011b937c03964e7a503c45
classes18.dex 499420 0e4c10c378a34985e04a15e5da1c32923a67b35b150a77b48465091b6a23cf60 a61aa408567a245318e3cae2dd3a9aa0deecf98211d284650291043896331e45
classes19.dex 680364 824460c298785a08db194c829848f4f9fc888fd5595d18f9830c44365d734427 11b2a800299c53eab0954b228241e824a49182c3103a520f091860c4ef61f5ee
classes20.dex 2832688 fca125bc2ab5b2ae0033e6e5fe78a2ef037c2888ba87fef81e90c1c84148e74f e964c9b07b77ca97b33de14c27acea5ab76e132e69cce685726f53795d820f8d
classes21.dex 614800 3945bab322e38cf48efcc05afe85c54b9cc70111db48116f6242d0274c0a3214 7dc497285ccd61b96afc7336bdf9038cbd8f15cb3bd6ee05bab903277b60e892
classes22.dex 5182212 e4049ec13d2c4d1e24c427fd0fdbfa4decfc1ef4fd52a72130d23f5e338843de 0dedac4e81f088ab42247bc06e26e3453a2e5cd248896df7f45a697b725706bd
classes23.dex 1212220 a71d29054c5684147c7a20bd54d6401dcb38400d7038aafcc6815b1421eaf262 c379a0b066e919f3199be92b998e1afa3d6b96b14e9620298af27369c7e75ea8
classes24.dex 5269596 07d6659f6af1d8ea4eae517eb78bd410dd64d01d8d7af63ea8fac1a0d5b0cd19 b74c357754e22825f7f45454511c8fb2f1f3e2b98ce55eb9674bacff649db615
classes25.dex 494576 21eecc327d31326c9dee3be96774fd30b5eb8b3a64f874649b6c5282fa32116d e43a29ff6a3924e8298650fe72bf9e8b4126cd69799759049e7b32bc01a6a945
classes26.dex 1477560 8d9df0ece076bc1d78dc9b4593b54d6ffdb99da0a24f6fb45444b16c5de7414c 190831053a69ee9222d9770a4f80b47fb459f529cc49f9c0601bd36768121507
classes27.dex 563660 9133534698f3e259fc793c9f32f8b841432773e7c92bd33efa3eda743909d0af 7ea255a01ddd2753dfa2f195dbde869c9289d41243a65c64dbdab4cfd6fb4823
classes28.dex 464136 5102f935d7846288a5be0955bc7db8d19267d793a6cd87e736999b6facb6712e bec5e57a68567db001837d0fa43aa1ae3bd94f1a89b5203522eb7638225e2036
classes29.dex 67888 f50faa6e925772e44cbf7db08af5d9b3f5a3c1595268bfad59c51495c9a7c1b0 4c0fd46d15ea7f30b764de44e5eec1afb6f9b1e226c7b44a0a7944b9db4f4c90
classes30.dex 382516 627cd94768a6ea67890519345757c1590fb3cee8f7c25e7ae722c5c4cf603719 9d75ea3508a094310caa9f4592b2f79afd637df8691f81f489cbf0c33e36e371
classes31.dex 177444 b75232e68dd13c4dfc54405a30e0a21518a0ccee689be1e59f34843099928810 8fadb485fb67e5795b9bca655db186edb42784893aa51e83ee16cd0d7b0af55d
classes32.dex 65196 d15113dcdee8d56728405f8b28f87d1aeddce7c276769894f5859c67d60d0250 054b13875de8e068ddcdf418692a7ab962315a0facdd17c230641b99fc076d0f
classes33.dex 74956 7629568ea42ceb6b1971c414d3b67ae7f17e0e073960c0235de323310c99a7af 2b0629d59dc51afecd79ee10552d8d92a3c574c83625014a019e7a5cba63c402
classes34.dex 141668 935cc98e5b5af5c6d2ebf89ee874a33fb966bfcb9f341dde449a5aee0409582d ee6a418fabd77868e6e692837b533c7d083db0329a06273c2ac8bbed42c97bda
classes35.dex 75320 e0d40be4e0e209669ff10886077c80ace1cc17152721a10be8eee85307d201a7 3b54393cd9a96c9c001af589e494478ea6ca58d1de27ba1d77f3fdd9edb6e330
classes36.dex 12284 b73507572610552bbcf41ac135f652c8359ee0622675e07e692347e93640e977 4703ca8ba530166547dc9bb07afec7930a4e311d43c74da148d34e5a6b7c3352
classes37.dex 25948 167b1ac6e3c9a2bbbcd97722a8db81fb7e48d2aa843eb9689e508c5269f1626f f5d0fd877459a3ba6496f06f58dcf2493a32658b1e3c907c68ba06b4abefd4a8
classes38.dex 612 b2d310dc1ab2794bff723bc9d2e79cbb928318295cc03cd7d93491aeadb7ebad 8c5217e754c4133b427b87a534934f45da1f3f6201a720e604d531cb5126a119
classes39.dex 9160 2cd61316d09bb5bd7201bf50fad84d3f663575fb5ce5ce359f741872ae0f00ac a073700d4f74f63f0b719304b1fba729da440ca7e48a02aaf18bb55c8c5b03f9
classes40.dex 11976 524d516a12ee7ba854c1920dc3f51165c2e1ff7c070b18d11a8e440f5b1351f2 026af4e41d5845bc4f474609facb4b382b6c7535d3e0a0cff86fb7967ef03286
classes41.dex 408516 dece29d831d4ba640f158f130a4ed0339e1c505e781e5053f9fd324b5e7c3a88 4ca121c27082a6f45665a7b9901f04b844f78b7e71331a11b06534a7b69f5158
classes42.dex 6064 1e2db216cc49ec42da32f8ff3d31eb0f19a7732c5cf0612b885c0d56e401825f 1e2cb8aebd677153b15239bf19bb282ba42211333fdc03326dfc63377164c017
附录 D:修复后逐 DEX 解析与命名空间清单
字段依次记录文件名、大小、完整 SHA256、DEX 版本、头部文件大小、头部类数、文件长度/SHA-1/Adler32 校验、解析结果、已解析类数、主要命名空间和解析异常。该清单直接支持 17/42、28,103 以及分析集合选择,不凭印象筛文件。
file size sha256 dex_version header_file_size header_class_defs file_size_ok signature_ok checksum_ok parse_ok parsed_class_defs top_namespaces parser_error
classes.dex 5400452 097b85f40cd3c09b534eeaae2fc3c462c388d073147d15f2399c007d518127be 035 5400452 4416 True True True True 4416 com/google=1716;com/huawei=871;com/tencent=549;com/qmuiteam=234;com/sina=166;com/huantansheng=132;com/sensetime=123;com/shuyu=116
classes02.dex 2879428 e20287ef6b7f0f7110d4b008fae58d2a92f1a02f7a9afed1400cb7e2102484c7 035 2879428 2371 True True True True 2371 com/tiocloud=1813;p/a=558
classes03.dex 1778324 b79b9afce5de1fcc8d063248813e00864ed20c231d79b57be6fd9c5dcc759d5e 035 1778324 1776 True True True True 1776 com/watayouxiang=1392;p/a=384
classes04.dex 6635344 31b8ad6b191c14a34d3ed520568548c758a931a3366220d1ed3952b9ea8a84fa 035 6635344 6324 True True True True 6324 com/google=4498;p/a=1738;com/github=88
classes05.dex 6897932 0837a02db066727390da25b25087aaf40f20d23718096372e853c69686509e21 035 6897932 5134 True True True True 5134 androidx=2894;com/ehking=458;cn/jiguang=358;cn/jpush=291;com/alibaba=234;android=172;com/facebook=163;com/alipay=161
classes06.dex 6153080 1cf99ceee5ecc0c100cfced27486850f4d6910a517f6bf807445990b4f2d73f0 035 6153080 4602 True True True True 4602 p/a=1057;kotlin=880;im/zego=553;com/xiaomi=477;org/webrtc=397;okhttp3/internal=214;com/watayouxiang=164;com/zego=141
classes07.dex 2478464 75ceacfdeedf4f59a61c5b0cc31b6b441bdb1f30bcb1e974727aa060ae7bf5cd 035 2478464 2587 True True True True 2587 p/a=2153;com/watayouxiang=168;com/vivo=160;com/tiocloud=76;com/transitionseverywhere=17;com/trello=13
classes08.dex 24400 510c0b2af2defe660fc22b98d224d867dbf7d1e1f895ef08fb9570676e259d24 035 24400 4 True True True True 4 com/tianyu=3;com/stub=1
classes09.dex 284 9f8898ce35a47aeafced99ea0d17c33e73037bb2307c7688e50819966f4ae939 035 284 1 True True True True 1 Empty=1
classes10.dex 577588 af3d0526fa5c97530deb63b5d2a20e2005dcb3b264211c17ca1ec7ab0a2ab169 039 577588 502 True True True True 502 miui/provider=119;miui/yellowpage=87;com/milink=68;miui/bluetooth=51;miui/util=36;miui/notification=22;com/miui=21;miui/payment=21
classes11.dex 2908424 6bf87caeebb4928eda4822292eabafad3aa55b15e4b90bb2ce89cdadbe2c030e 035 2908424 2371 True True True True 0
classes12.dex 2504220 ea7c7dfd8fafef6d9b448944f5a36a8433c49668ac57b1ceeb1178d25ea8104e 035 2504220 2587 True True True True 0
classes13.dex 1804020 0629fa4a4819a8ed2a8c329aaca386fd2b03ea4d600bc703b98a4431cf8c2922 035 1804020 1776 True True True True 0
classes14.dex 93504 60fc0aab932cb91ffa94f3cca37c5a1132b864f1f5191225d10b959fb0f1b4e9 035 93504 79 True True True False 0 ValueError: 31992 is not a valid TypeMapItem
classes15.dex 255512 1131295c7abc870772432c49d6e0bb52fac5160cba4d5742eb1d81518b950db7 039 255512 302 True True True True 302 miui/cloud=206;com/xiaomi=96
classes16.dex 284 f0cad9de0ed83090755ae3515c5d80bdd2de2c27646db339931c519d6394db90 035 284 4278190080 True True True False 0 ValueError: 65535 is not a valid TypeMapItem
classes17.dex 1013340 da1f939ffdec1b7d5d9da1b7fe3079e3efad19dc38011b937c03964e7a503c45 039 1013340 785 True True True False 0 ValueError: 4 is not a valid HiddenApiClassDataItem.DomapiApiFlag
classes18.dex 499420 a61aa408567a245318e3cae2dd3a9aa0deecf98211d284650291043896331e45 039 499420 406 True True True False 0 ValueError: 4 is not a valid HiddenApiClassDataItem.DomapiApiFlag
classes19.dex 680364 11b2a800299c53eab0954b228241e824a49182c3103a520f091860c4ef61f5ee 039 680364 645 True True True False 0 ValueError: 4 is not a valid HiddenApiClassDataItem.DomapiApiFlag
classes20.dex 2832688 e964c9b07b77ca97b33de14c27acea5ab76e132e69cce685726f53795d820f8d 039 2832688 1747 True True True False 0 ValueError: 6 is not a valid HiddenApiClassDataItem.DomapiApiFlag
classes21.dex 614800 7dc497285ccd61b96afc7336bdf9038cbd8f15cb3bd6ee05bab903277b60e892 039 614800 213 True True True False 0 ValueError: 4 is not a valid HiddenApiClassDataItem.DomapiApiFlag
classes22.dex 5182212 0dedac4e81f088ab42247bc06e26e3453a2e5cd248896df7f45a697b725706bd 039 5182212 3334 True True True False 0 ValueError: 4 is not a valid HiddenApiClassDataItem.DomapiApiFlag
classes23.dex 1212220 c379a0b066e919f3199be92b998e1afa3d6b96b14e9620298af27369c7e75ea8 039 1212220 679 True True True False 0 ValueError: 6 is not a valid HiddenApiClassDataItem.DomapiApiFlag
classes24.dex 5269596 b74c357754e22825f7f45454511c8fb2f1f3e2b98ce55eb9674bacff649db615 039 5269596 3843 True True True False 0 ValueError: 6 is not a valid HiddenApiClassDataItem.DomapiApiFlag
classes25.dex 494576 e43a29ff6a3924e8298650fe72bf9e8b4126cd69799759049e7b32bc01a6a945 039 494576 446 True True True False 0 ValueError: 4 is not a valid HiddenApiClassDataItem.DomapiApiFlag
classes26.dex 1477560 190831053a69ee9222d9770a4f80b47fb459f529cc49f9c0601bd36768121507 039 1477560 1186 True True True False 0 ValueError: 6 is not a valid HiddenApiClassDataItem.DomapiApiFlag
classes27.dex 563660 7ea255a01ddd2753dfa2f195dbde869c9289d41243a65c64dbdab4cfd6fb4823 039 563660 449 True True True False 0 ValueError: 6 is not a valid HiddenApiClassDataItem.DomapiApiFlag
classes28.dex 464136 bec5e57a68567db001837d0fa43aa1ae3bd94f1a89b5203522eb7638225e2036 039 464136 346 True True True False 0 ValueError: 4 is not a valid HiddenApiClassDataItem.DomapiApiFlag
classes29.dex 67888 4c0fd46d15ea7f30b764de44e5eec1afb6f9b1e226c7b44a0a7944b9db4f4c90 035 67888 79 True True True True 79 com/jg=64;com/qihoo=12;com/stub=3
classes30.dex 382516 9d75ea3508a094310caa9f4592b2f79afd637df8691f81f489cbf0c33e36e371 039 382516 393 True True True False 0 ValueError: 4 is not a valid HiddenApiClassDataItem.DomapiApiFlag
classes31.dex 177444 8fadb485fb67e5795b9bca655db186edb42784893aa51e83ee16cd0d7b0af55d 039 177444 173 True True True False 0 ValueError: 4 is not a valid HiddenApiClassDataItem.DomapiApiFlag
classes32.dex 65196 054b13875de8e068ddcdf418692a7ab962315a0facdd17c230641b99fc076d0f 039 65196 81 True True True False 0 ValueError: 4 is not a valid HiddenApiClassDataItem.DomapiApiFlag
classes33.dex 74956 2b0629d59dc51afecd79ee10552d8d92a3c574c83625014a019e7a5cba63c402 039 74956 49 True True True False 0 ValueError: 6 is not a valid HiddenApiClassDataItem.DomapiApiFlag
classes34.dex 141668 ee6a418fabd77868e6e692837b533c7d083db0329a06273c2ac8bbed42c97bda 039 141668 139 True True True False 0 ValueError: 4 is not a valid HiddenApiClassDataItem.DomapiApiFlag
classes35.dex 75320 3b54393cd9a96c9c001af589e494478ea6ca58d1de27ba1d77f3fdd9edb6e330 039 75320 61 True True True False 0 ValueError: 4 is not a valid HiddenApiClassDataItem.DomapiApiFlag
classes36.dex 12284 4703ca8ba530166547dc9bb07afec7930a4e311d43c74da148d34e5a6b7c3352 039 12284 15 True True True False 0 ValueError: 6 is not a valid HiddenApiClassDataItem.DomapiApiFlag
classes37.dex 25948 f5d0fd877459a3ba6496f06f58dcf2493a32658b1e3c907c68ba06b4abefd4a8 039 25948 40 True True True False 0 ValueError: 4 is not a valid HiddenApiClassDataItem.DomapiApiFlag
classes38.dex 612 8c5217e754c4133b427b87a534934f45da1f3f6201a720e604d531cb5126a119 039 612 1 True True True True 1 android=1
classes39.dex 9160 a073700d4f74f63f0b719304b1fba729da440ca7e48a02aaf18bb55c8c5b03f9 039 9160 12 True True True False 0 ValueError: 4 is not a valid HiddenApiClassDataItem.DomapiApiFlag
classes40.dex 11976 026af4e41d5845bc4f474609facb4b382b6c7535d3e0a0cff86fb7967ef03286 039 11976 15 True True True False 0 ValueError: 4 is not a valid HiddenApiClassDataItem.DomapiApiFlag
classes41.dex 408516 4ca121c27082a6f45665a7b9901f04b844f78b7e71331a11b06534a7b69f5158 039 408516 242 True True True False 0 ValueError: 6 is not a valid HiddenApiClassDataItem.DomapiApiFlag
classes42.dex 6064 1e2cb8aebd677153b15239bf19bb282ba42211333fdc03326dfc63377164c017 039 6064 4 True True True True 4 android=4
附录 E:DEX 头修复脚本
脚本只在输出目录生成副本,顺序为文件大小 → header size → SHA-1 → Adler32,并输出源/修复完整 SHA256。
#!/usr/bin/env python3
"""Repair DEX header signature/checksum on copies of memory-dumped DEX files."""
from __future__ import annotations
import argparse
import hashlib
import struct
import zlib
from pathlib import Path
def repair_dex(data: bytes) -> bytes:
if len(data) < 0x70:
raise ValueError(f"DEX shorter than header: {len(data)} bytes")
if data[:4] != b"dex\n":
raise ValueError(f"unexpected magic: {data[:8]!r}")
repaired = bytearray(data)
repaired[0x20:0x24] = struct.pack("<I", len(repaired))
repaired[0x24:0x28] = struct.pack("<I", 0x70)
repaired[0x0C:0x20] = hashlib.sha1(repaired[0x20:]).digest()
repaired[0x08:0x0C] = struct.pack("<I", zlib.adler32(repaired[0x0C:]) & 0xFFFFFFFF)
return bytes(repaired)
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("source", type=Path)
parser.add_argument("output", type=Path)
args = parser.parse_args()
args.output.mkdir(parents=True, exist_ok=True)
dex_files = sorted(args.source.glob("*.dex"))
if not dex_files:
raise SystemExit("no .dex files found")
print("file\tsize\tsource_sha256\trepaired_sha256")
for source in dex_files:
raw = source.read_bytes()
repaired = repair_dex(raw)
target = args.output / source.name
target.write_bytes(repaired)
print(
f"{source.name}\t{len(raw)}\t{hashlib.sha256(raw).hexdigest()}\t"
f"{hashlib.sha256(repaired).hexdigest()}"
)
return 0
if __name__ == "__main__":
raise SystemExit(main())
附录 F:DEX 逐文件核验脚本
下面是本文实际使用的核验脚本全文。它输出逐文件 TSV 和汇总 JSON;所有数量均由文件实测,不由 AI 填写。
#!/usr/bin/env python3
"""Deterministically inventory and parse every DEX in a directory.
The TSV is the per-file evidence record. The JSON is the machine-readable
summary used to reproduce parser success/failure and class_def totals.
"""
from __future__ import annotations
import argparse
import hashlib
import json
import struct
import sys
import zlib
from collections import Counter
from pathlib import Path
from androguard.core.dex import DEX
try:
from loguru import logger
logger.remove()
except ImportError:
pass
TSV_FIELDS = (
"file",
"size",
"sha256",
"dex_version",
"header_file_size",
"header_class_defs",
"file_size_ok",
"signature_ok",
"checksum_ok",
"parse_ok",
"parsed_class_defs",
"top_namespaces",
"parser_error",
)
def dex_sort_key(path: Path) -> tuple[int, str]:
stem = path.stem
if stem == "classes":
return (1, path.name)
suffix = stem.removeprefix("classes")
return (int(suffix), path.name) if suffix.isdigit() else (10**9, path.name)
def sanitize(value: object) -> str:
return str(value).replace("\t", " ").replace("\r", " ").replace("\n", " ")
def class_namespace(name: str) -> str:
clean = name.removeprefix("L").removesuffix(";")
parts = [part for part in clean.split("/") if part]
if not parts:
return "(empty)"
if len(parts) == 1:
return parts[0]
if parts[0] in {"android", "androidx", "dalvik", "java", "javax", "kotlin"}:
return parts[0]
return "/".join(parts[:2])
def inspect(path: Path) -> dict[str, object]:
raw = path.read_bytes()
sha256 = hashlib.sha256(raw).hexdigest()
row: dict[str, object] = {
"file": path.name,
"size": len(raw),
"sha256": sha256,
"dex_version": "",
"header_file_size": "",
"header_class_defs": "",
"file_size_ok": False,
"signature_ok": False,
"checksum_ok": False,
"parse_ok": False,
"parsed_class_defs": 0,
"top_namespaces": "",
"parser_error": "",
}
if len(raw) >= 0x70:
row["dex_version"] = raw[4:7].decode("ascii", errors="replace")
row["header_file_size"] = struct.unpack_from("<I", raw, 0x20)[0]
row["header_class_defs"] = struct.unpack_from("<I", raw, 0x60)[0]
row["file_size_ok"] = row["header_file_size"] == len(raw)
row["signature_ok"] = raw[0x0C:0x20] == hashlib.sha1(raw[0x20:]).digest()
expected_checksum = zlib.adler32(raw[0x0C:]) & 0xFFFFFFFF
row["checksum_ok"] = struct.unpack_from("<I", raw, 0x08)[0] == expected_checksum
try:
dex = DEX(raw)
classes = dex.get_classes()
namespaces = Counter(class_namespace(item.get_name()) for item in classes)
row["parse_ok"] = True
row["parsed_class_defs"] = len(classes)
row["top_namespaces"] = ";".join(
f"{name}={count}" for name, count in namespaces.most_common(8)
)
except Exception as exc: # The exact parser exception is evidence.
row["parser_error"] = f"{type(exc).__name__}: {sanitize(exc)}"
return row
def write_tsv(path: Path | None, rows: list[dict[str, object]]) -> None:
lines = ["\t".join(TSV_FIELDS)]
lines.extend("\t".join(sanitize(row[field]) for field in TSV_FIELDS) for row in rows)
payload = "\n".join(lines) + "\n"
if path is None:
sys.stdout.write(payload)
return
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(payload, encoding="utf-8")
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("directory", type=Path)
parser.add_argument("--tsv", type=Path, help="write the per-file TSV here")
parser.add_argument("--summary", type=Path, help="write the summary JSON here")
args = parser.parse_args()
dex_files = sorted(args.directory.glob("*.dex"), key=dex_sort_key)
if not dex_files:
raise SystemExit(f"no .dex files found in {args.directory}")
rows = [inspect(path) for path in dex_files]
summary = {
"directory": str(args.directory.resolve()),
"files": len(rows),
"unique_sha256": len({row["sha256"] for row in rows}),
"parse_ok": sum(bool(row["parse_ok"]) for row in rows),
"parse_fail": sum(not bool(row["parse_ok"]) for row in rows),
"parsed_class_defs": sum(int(row["parsed_class_defs"]) for row in rows),
"file_size_ok": sum(bool(row["file_size_ok"]) for row in rows),
"signature_ok": sum(bool(row["signature_ok"]) for row in rows),
"checksum_ok": sum(bool(row["checksum_ok"]) for row in rows),
}
write_tsv(args.tsv, rows)
summary_text = json.dumps(summary, ensure_ascii=False, indent=2) + "\n"
if args.summary:
args.summary.parent.mkdir(parents=True, exist_ok=True)
args.summary.write_text(summary_text, encoding="utf-8")
else:
sys.stderr.write(summary_text)
return 0
if __name__ == "__main__":
raise SystemExit(main())
第一篇之后,我还想把失败继续拆开
这篇先交代完整路径,后面的文章不再重复整条流程,而是把真正容易翻车的环节逐个做深:DEX 为什么看起来正常却无法解析、Frida 为什么进程存在仍不能通信、抓到 DEX 后为什么还有 native 方法。
从 DEX Header、SHA-1、Adler32 到 5/42 → 17/42 的逐文件修复实验。
Android 加固分析 · 0 号隔离设备实测 · 原始 APK 未移动