AI自动化脱壳①:以前我不会 APK 脱壳,直到 AI 帮我导出 42 个运行时 DEX

作者:0xSec笔记本 发布:2026-08-24 08:08 收录:2026-09-03 09:16 2 次阅读 约 14606 字
摘要:0xSEC 笔记本 AI FORENSICS / 001 AI × ANDROID · 自动化脱壳 01 以前我不会 APK 脱壳 直到 AI 帮我导出 42 个运行时 DEX 同一份 APK,静态分析只能看到 4 个壳类;运行后识别出 49 个候选内存区域,其中 42 个 DEX 真实落盘。…
推荐理由:本文涵盖「Androguard」、「Frida-dexdump」、「AI自动化」等多个主题,重点关注 Androguard。
0xSEC 笔记本
AI FORENSICS / 001
AI × ANDROID · 自动化脱壳 01

以前我不会 APK 脱壳
直到 AI 帮我导出 42 个运行时 DEX

同一份 APK,静态分析只能看到 4 个壳类;运行后识别出 49 个候选内存区域,其中 42 个 DEX 真实落盘。这不是 AI 猜出来的结果,而是一条可以对样本、日志和哈希逐项复算的技术链。

样本证据可复算 · 原始转储不覆盖 · 未声称完成重打包
INDEX / 目录先看结果零基础:什么是加固脱壳全图判断 360理解壳搭环境抓内存修 DEX看源码AI 做了什么常见问题完整复现下一篇
0x00

先看结果:我真正跨过的不是一条命令

以前遇到加固 APK,我会把文件拖进 JADX。看到的只有 StubApp、几个 native 接口和一堆不明所以的壳文件。接下来该看哪里、为什么工具报错、dump 出来的 DEX 为什么仍打不开,我没有一条完整路径。

这次我把问题交给 AI,但没有让它直接给出“脱壳成功”的答案。我让它把任务拆成能够逐项验收的问题:先确认是不是壳,再找到壳的加载入口;让 App 自己把代码还原到内存;保存原始转储;修复 DEX 头;最后用解析器、完整哈希和真实方法体证明恢复结果。

4原包明确暴露的
class_def
42第一轮成功保存的
内存 DEX
17/42头部修复后
可被 Androguard 解析
14,2757 DEX 分析集合
生成的 Java 文件
参与者
这次实际承担的工作
AI
拆解步骤、解释失败、建议下一项可验证检查、协助生成修复与核验脚本。
确定性工具
计算 MD5/SHA256、检查 ZIP、转储内存、修复 DEX、统计类数并定位真实代码。
限定样本和设备、执行实验、核对每个数字、区分样本代码与第三方依赖,并保留失败和完成边界。
📢 免责声明
本文内容仅供学习参考,所述技术仅限用于合法授权的安全研究、教学演示及防御机制开发。请勿用于未经授权的访问、测试、数据获取或其他违法活动。读者应自行确保使用行为符合适用的法律法规,并承担因不当使用本内容产生的相应责任。
公开版脱敏说明:应用显示名、真实包名、文件名、版本、文件大小、原始 APK 的 MD5/SHA256、测试设备序列号及可关联业务主机均不公开。文中的 authorized_sample.apkYOUR_AUTHORIZED_PACKAGEYOUR_DEVICE_SERIAL 与方括号字段是公开占位符,不是证据值;内部复核材料保留真实完整值。
本次实验的完成边界:
本文选取一份 Android 应用样本,仅对 APK 加固、运行时 DEX 转储和源码恢复过程进行技术实验;所有运行均在隔离测试设备上完成,不涉及对应用业务性质、运营主体或相关行为的判断。本次完成的是“从加固 APK 到可分析源码集合”;没有生成可重新安装的脱壳 APK,部分方法仍然是 native,也没有被写成已经恢复。
这篇文章有三种读法:
  • 第一次接触 APK 加固:从下一节开始,先理解 APK、DEX、StubApp 和壳。
  • 想直接看实验:从“判断有没有壳”开始,按判壳、转储、修复、源码验证阅读。
  • 要逐条复跑:正文读因果,文末直接阅读内嵌命令、日志、逐文件清单和完整脚本。
0x01

0. 从零开始:APK、加固和脱壳到底是什么?

先暂时忘掉 Frida、JADX 和命令行。Android App 在开发者电脑上原本是一批 Java/Kotlin 源代码、图片、界面资源和配置;经过编译、打包与签名后,才变成手机能安装的 .apk 文件。加固不是 Android App 必须经历的步骤,而是开发完成后的额外保护处理。

先认识 APK 里的九个常用名词

名词
零基础解释
本次实验为什么会用到
APK
Android 安装包,是一种结构固定、可用 ZIP 工具查看的归档文件;它不只是“一个程序文件”,里面还装着代码、资源、清单、证书和库。
先核验整包哈希,再检查包内壳文件。
AndroidManifest.xml
应用清单,告诉系统包名、组件、权限和进程初始化时使用的 Application 等信息。APK 内通常是编译后的二进制 XML。
本次样本的清单把 Application 指向 com.stub.StubApp
DEX
Dalvik Executable。Java/Kotlin 通常先编译成 JVM 字节码,再转换为 Android Runtime 能装载的 DEX 字节码。
脱壳的核心目标是恢复可解析的真实业务 DEX。
Application
应用进程初始化时的关键全局对象之一。传统壳常把这里替换成自己的入口,以便业务界面出现前先完成解密和装载。
本次样本被替换成 StubApp。
Activity
通常对应一个可交互页面,例如登录页、聊天页。它依赖所属类已被运行时找到并装载。
真实 Activity 在壳完成装载后才能正常创建。
SO
Android 的 native 动态库,通常是 ELF 格式的 ARM 或 x86 机器码,由 C/C++ 等编译而来。
libjiagu*.so
 承担壳的 native 处理。
ABI
CPU 架构与调用约定,例如 arm64-v8a、armeabi-v7a、x86_64;不同 ABI 的机器码不能混用。
本包准备了 ARM、ARM64、x86 和 x86_64 壳库。
ClassLoader
类加载器,负责告诉运行时去哪里找某个类的定义。
壳要让 ClassLoader 能找到刚还原的业务类。
ART
Android Runtime,负责装载、验证和执行 DEX 代码,也会把热点代码编译为机器码。
真代码要运行,就必须以 ART 可使用的形式出现。
为什么 APK 能用 unzip 查看,却不能把它简单理解成 ZIP?
ZIP 只是外层容器。Android 还规定了 Manifest、DEX、资源表、签名块和 native 库的结构与语义;随意改一个字节会破坏签名,随意压回 ZIP 也不一定能安装。
为什么反编译得到的 Java 不是开发者原始源码?
JADX 是把 DEX 指令重建成便于人读的近似 Java。原注释、部分变量名、Kotlin 语义和构建信息可能已经丢失;混淆后,类名还可能只剩 a、b、c。
教学示例:语义还在,但“人写代码时的信息”已经丢了。
下面只演示“编译+混淆+反编译”可能造成的信息损失,不来自本次 APK,也不表示某个工具一定逐字输出成这样。
开发者手写源码
混淆后由 JADX 重建的近似 Java
/** 拼接登录地址 */
public final class LoginRequest {
    private String phoneNumber;

    public String buildLoginUrl() {
        return "/api/login?phone="
            + phoneNumber;
    }
}
public final class a {
    private String a;

    public String a() {
        return "/api/login?phone="
            + this.a;
    }
}
怎样看这个例子:两段代码表达的可观察逻辑接近,但注释消失,LoginRequestphoneNumberbuildLoginUrl 变成了 a。JADX 可以重建控制结构,却无法凭空知道开发者原来取了什么名字、写了什么注释。

一个没有加固的 App 是怎样生成的?

  1. 开发者编写 Java/Kotlin 与 C/C++ 代码,并准备布局、图片、字符串等资源。
  2. 构建工具编译代码:Java/Kotlin 代码最终转换为一个或多个 classes*.dex,C/C++ 代码编译成各 ABI 的 .so
  3. 打包工具把 DEX、Manifest、资源、assets 与 SO 装入 APK。
  4. 发布者用私钥给 APK 签名。签名把该版本内容与签名密钥绑定,并让系统检查安装包是否在签名后被修改。
  5. 手机安装 APK;启动时系统依据 Manifest 创建进程、初始化组件并装载代码。
普通静态分析为什么通常能工作:如果真实业务 DEX 直接放在 classes.dex 中,JADX 或 Androguard 读取 DEX 的类表、方法表和指令,就能建立业务代码地图。加固要阻止的,正是这种“拿到文件就能直接读代码”的路径。

站在开发者角度,“进行一次加固”是什么流程?

从操作界面看并不神秘:准备普通发布 APK → 交给加固工具 → 选择保护项 → 取得加固产物 → 按发布策略签名 → 在真实设备回归测试。界面按钮可能只有几步,复杂工作发生在工具对 APK 的内部改造中。

操作者看到的步骤
应检查什么
背后发生什么
1. 构建普通 release APK
包名、版本、功能、原始 MD5/SHA256
此时业务 DEX 通常仍能被静态工具读取。
2. 把 APK 输入本地工具或授权服务
服务来源、保密边界、是否上传源 APK
工具解析 ZIP、Manifest、DEX、资源、SO 与现有签名。
3. 选择 DEX、资源、SO、反调试等保护项
保护范围与兼容性
不同选项会决定隐藏哪些代码、注入哪些壳组件和运行时检查。
4. 导出加固产物
输出是否未签名、证书是否符合发布策略
文件内容已改变,输出 APK 的 MD5/SHA256 必然不同于输入 APK。
5. 对齐、签名、验证并安装测试
签名证书、安装、启动、升级、各 ABI 和核心功能
系统验签后安装;运行时先执行壳,再接回业务。
为什么加固界面看起来简单,逆向却很复杂?
按钮只是把一组编译、变换、注入、重打包和签名操作封装起来。使用者不需要理解壳的每个指令,但分析者必须从变化后的字节、启动入口和运行行为反推出这些步骤。

加固工具在 APK 内部究竟做了什么?

开发者先生成普通 APK,再把它交给本地加固程序或加固服务。不同厂商、不同版本的实现不完全相同,但传统 DEX 壳通常会完成下面几类变换:

  1. 取出或改造真实 DEX:
    将业务 DEX 压缩、加密、分段、改头或转成壳自定义的 payload,使通用反编译器不能直接解析。
  2. 放回隐藏 payload:
    保护后的数据可能放入 assets、某个 DEX 的特殊区域、native 库或自定义文件中。并非所有壳都使用同一路径。
  3. 注入壳程序:
    加入少量可直接启动的 StubApp 和 native 壳库。它们不是主要业务,而是负责校验、解密、映射和接管类加载。
  4. 改启动清单:
    传统壳常把 Manifest 中的 Application 改为壳 Application,让壳比业务页面更早运行;也存在使用其他初始化点的实现。
  5. 重新打包与签名:
    APK 字节已经变化,原签名不再有效,所以要重新对齐并签名。最终由谁持有签名密钥,取决于实际加固和发布流程。
● ● ●0xSEC_CAPTURE / FIG.01
普通 APK 到加固 APK,再到运行时还原的流程
图 1 通用原理图:正常构建、加固改造、运行时还原。它用于解释机制,不宣称是 360 未公开的内部文件格式。
为什么加固后仍然能正常安装?
加固工具不会把外层随意变成陌生格式;最终产物仍要满足 APK 结构、Manifest、签名和 Android 安装校验。被隐藏的主要是业务代码的可读形态,外层仍是 Android 能接受的安装包。
为什么壳常同时提供四种 libjiagu?
SO 是特定 CPU 的机器码。开发者不知道用户手机一定是 ARM64、32 位 ARM 还是模拟器常见的 x86,因此壳可准备多个 ABI 版本,启动时选择与设备匹配的一份。本次 APK 中四个文件的存在是实测事实,但不能反推四种设备都实际运行过。
为什么修改 Manifest 后必须重新签名?
签名校验覆盖 APK 内容。Manifest、DEX 或资源改变后,旧签名对应的摘要不再匹配;不重新签名,系统会拒绝把它当作完整、有效的安装包。

加固后的 App 启动时怎样“自己解开自己”?

  1. Android 读取 Manifest,看到的不是原 Application,而是 StubApp。
  2. StubApp 判断设备 ABI,释放或定位匹配的 native 壳库,并通过 System.load 或 System.loadLibrary 加载。
  3. 壳库定位受保护 payload,完成环境校验、解密、解压或结构修复。
  4. 壳把还原的 DEX 写到私有目录或直接映射到内存,并调整 ClassLoader、运行时数据结构或装载链。
  5. 壳创建原业务 Application,随后真实 Activity、网络层和业务逻辑继续运行。某些壳还会持续执行完整性校验、反调试或方法级保护。
为什么壳不一定在文件里保存一个明文“解密密钥”?
密钥可以由壳内常量、签名、设备信息或多段数据组合派生,也可以由 native 代码实现自定义变换。分析师不应看见一串字符就宣布它是密钥,必须以代码调用链和解密结果核验。
既然有加密,为什么仍然可能脱壳?
因为 App 必须在用户手机上完成真实业务。无论密钥怎样保存,运行时最终都要得到足够的信息,让 ART 能使用真实类和方法。动态脱壳通常不先破解算法,而是在壳已经完成还原后,从内存复制结果。

混淆、加固、签名、反调试不是一回事

技术
它改变什么
分析时的典型现象
代码混淆
重命名类/方法/字段,删除无用信息,也可能改造控制流。
DEX 仍能打开,但名称像 a、b、c,逻辑更难读。
APK 加固/加壳
隐藏或变换业务 DEX,并增加运行时还原和装载层。
静态只见 StubApp、壳接口或少量类,运行后内存出现更多 DEX。
签名
用密码学方式绑定 APK 内容与签名密钥,支持完整性和升级身份校验。
不是隐藏代码;有签名的 APK 仍可被反编译。
反调试/反注入
检测调试器、root、Frida、模拟器或异常运行环境。
App 闪退、卡死、返回假数据或拒绝继续。
方法 native 化
把特定 Java 方法的实现移到 native 层或运行时恢复。
抓到主体 DEX 后,个别方法仍显示 native
脱壳
在授权分析中恢复壳运行时已经还原的代码表示。
得到可校验的 DEX 或类/方法数据,供后续反编译和技术研究。
为什么“加了壳”不能直接说明 App 的业务性质?
金融、游戏、企业服务和其他类型的 App 都可能为了防盗版、防篡改或提高逆向成本而加固。加固只描述代码保护方式;App 的具体业务性质必须结合功能、通信和实际运行结果另行分析。
脱壳成功到底得到什么?
通常得到一个或多个运行时 DEX、类定义或方法代码,便于静态分析。它不自动恢复开发者工程、注释、原变量名,也不保证能重新打成可安装 APK。本次实验的完成标准是“包含样本功能代码的 DEX 可解析且真实方法体可复核”,不是“制作破解版”。
通用原理与本次实验的边界:上面的流程是传统 Android 加固的通用教学模型;下面涉及 .jgapplibjiagucom.stub.StubApp、文件大小、哈希和 DEX 数量时,才是对本次真实 APK 与隔离设备运行结果的确定性观察。
0x02

先把脱壳想成打开保险箱

APK 加固像把真实程序锁进保险箱。JADX 只能看到锁和开锁程序,也就是 StubApp。App 想正常运行,壳最终必须把真实代码解密并交给 Android。我们等待它自己开锁,再从内存复制已经解密的 DEX。

● ● ●0xSEC_CAPTURE / FIG.02
完整脱壳复核流程
图 2 判壳、等待解密、内存提取、DEX 修复、源码验证
为什么一条 dump 命令不算完整脱壳?
因为导出的文件可能校验错误,还可能混入系统 DEX。必须修复、解析、筛选并找到真实业务方法。
0x03

判断有没有壳,以及是什么壳

先固定样本身份

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 代码与运行时结果完成四层复核,它不是品牌判断的唯一依据。

真实条目
字节
意义
assets/.jgapp
16
360 加固标记
assets/libjiagu.so
860820
ARM 壳库
assets/libjiagu_a64.so
1160496
ARM64 壳库
assets/libjiagu_x64.so
856504
x86_64 壳库
assets/libjiagu_x86.so
762268
x86 壳库
为什么看到 libjiagu.so 还不够?
单个文件名可以伪造或残留。本例同时核对 ZIP 特征、StubApp 实际加载代码和 subpoena 核验器,三路一致才判断为 360 加固保。

不是背文件名:用四层依据闭环判断

层级
本样本实测依据
证明力
为什么
文件层
.jgapp、libjiagu.so、libjiagu_a64.so、libjiagu_x64.so、libjiagu_x86.so
线索
名称和多 ABI 组合与 360 加固常见布局一致,但文件可以被复制或残留
Manifest 层
application android:name = com.stub.StubApp
强依据
Android 首先创建壳 Application,而不是原业务 Application
代码层
StubApp 明确引用 libjiagu、.jiagu、entryRunApplication、DtcLoader、interface5
强依据
这些信号形成释放壳库、native 初始化、加载真实 Application 的连续调用链
运行时层
原包 4 个 class_def;壳运行后抓到包含样本功能代码的 DEX,7 个 DEX 的分析集合可恢复源码
行为确认
证明 StubApp 和 libjiagu 确实承担隐藏并运行时装载代码的壳功能;集合内第三方依赖不归为样本自研

逐条复核命令

# 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)
为什么 .jgapp 的 16 字节内容不能单独解释成“360 编号”?
本报告只观察到它是 ASCII 字符串 2e35de1053d7855f,没有厂商格式文档证明各字段含义。因此只把“文件存在、路径固定、与加载链共同出现”作为特征,不臆测它是版本号、密钥或客户 ID。
为什么 com.stub.StubApp 单独也不够?
StubApp 是通用名称,其他壳或仿制实现也能使用。只有它同时加载 libjiagu、释放到 .jiagu、调用 native 接口并切换真实 Application,品牌和功能判断才有闭环。

实际工作中的判定口径

观察结果
建议表述
只发现一个 libjiagu 文件名
“发现 360 加固命名线索,尚未确认;需核对 Manifest 与调用链”
.jgapp + 多 ABI libjiagu + Manifest 指向 StubApp
“与 360 加固典型静态结构一致,置信度高”
再确认 StubApp 的 .jiagu/System.load/interface5/真实 Application 加载链
“静态代码闭环支持 360 加固壳判定”
运行后原来不可见的业务 DEX 出现在内存并可恢复源码
“动态结果确认该组件实际承担运行时解密/装载功能”
本样本最终技术口径:ZIP 布局、Manifest 壳入口、StubApp 加载链、subpoena 核验和运行时 DEX 恢复相互印证,判定为 360 加固保。技术报告应同时列出这些观察事实,不只写“工具识别为 360”。
● ● ●0xSEC_CAPTURE / FIG.03
360 加固四层判断依据
图 3 重点图:ZIP、Manifest、StubApp 加载链与运行时结果共同支持 360 加固判断

数原包中真正定义了多少类

原包 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;
为什么 DEX 有 14 MB,却只有 4 个类?
体积不能代表业务代码量。加固可以填充数据或把 payload 放进 DEX 的非普通类区域。class_def 才是当前 DEX 明确暴露的类定义。
判壳结论:原包只暴露 4 个壳类,并存在 .jgapp、libjiagu 多架构库及 StubApp 加载链,确定为 360 加固样本。
0x04

理解 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;
}
为什么不把解密放在某个 Activity?
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);
为什么核心逻辑放在 native so?
Java DEX 容易反编译,native ELF 的控制流、符号和反调试成本更高。解密、内存映射和类加载修复因此落在 libjiagu 中。
为什么先释放到 .jiagu 目录?
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);
为什么 loadClass 能成功就说明真 DEX 已经准备好?
ClassLoader 只有在 ART 已经认识该类定义时才能加载它。壳准备加载 entryRunApplication,意味着 native 阶段已经映射或注册了承载该类的 DEX。
真正的抓取窗口:等 interface5 完成、真实类可加载后扫描内存。太早只能得到 StubApp;太晚可能受进程状态和内存权限变化影响。
0x05

搭建环境:为什么版本问题比命令问题更常见?

本次只使用 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?
主机端是控制器;设备端 frida-server 在 Android 上完成进程注入和内存读取。两端缺一不可。
为什么不能只看 frida-server 进程存在?
进程存在不代表协议兼容、端口可达或目标可注入。必须用相同虚拟环境的 frida-ps 成功枚举设备进程。

真实失败:Frida 17 链路

frida-dexdump -D YOUR_DEVICE_SERIAL -f YOUR_AUTHORIZED_PACKAGE -o runtime/dexdump_attempt_1 --sleep 8
Spawning 'YOUR_AUTHORIZED_PACKAGE'...
Failed to spawn: unable to find process with name 'system_server'
为什么不能说这是样本反调试?
错误发生在 Frida 建立 spawn 链路时,没有观察到样本检测代码的直接依据。切换 Frida 16 后成功,证明这一轮首先是环境故障。
0x06

让壳自己解密,再从内存抓 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 一致
具体值:公开版不展示
为什么设备端还要再算 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.log
Spawning '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...
为什么用 -f spawn,而不是手动打开后 attach?
spawn 从进程创建阶段接管,时序更可控。手动 attach 可能错过壳初始化,也可能每次落在不同页面状态。
为什么等待 8 秒?
立即扫描可能只有壳 DEX。等待给 StubApp、libjiagu 和 ClassLoader 时间完成真代码映射。第二轮等待 15 秒,用来检验时序差异。
为什么发现 49 个,只保存 42 个?
发现是候选内存范围;保存要求整个范围可读并成功落盘。部分范围跨越不可读页,另有 MD5 去重,因此两个数字不能混写。
轮次
等待
发现
保存
总字节
访问异常
第一次成功
8 秒
49
42
62,155,380
4
第二次成功
15 秒
47
41
62,155,096
4
● ● ●0xSEC_CAPTURE / FIG.04
运行时 DEX 转储日志与复算结果
图 4 重点图:第一次成功转储的真实日志、候选区域、落盘数、总字节和访问异常
为什么第二次反而少一个?
内存映射会随类加载、系统框架和运行阶段变化。两轮有 37 个相同 SHA256,主体稳定,但候选区域数量不是固定常量。
● ● ●0xSEC_CAPTURE / FIG.05
隔离测试设备协议页(应用标识已局部打码)
图 5 真实运行画面:停留在协议页,未登录、未输入个人数据;应用名和状态栏应用图标已局部打码。
0x07

为什么 dump 出来还不能直接放进 JADX?

for f in runtime/dexdump_attempt_2/*.dex; do file -b "$f"; done | sort | uniq -c
15 Dalvik dex file version 035
27 Dalvik dex file version 039
为什么 file 说是 DEX,Androguard 仍报错?
file 主要检查魔数和版本;严格解析器还会验证文件长度、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 checksum
为什么先算 SHA-1,再算 Adler32?
signature 覆盖偏移 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.tsv
repaired[0x0C:0x20] = SHA1(repaired[0x20:])
repaired[0x08:0x0C] = Adler32(repaired[0x0C:])
为什么不能覆盖原始 dump?
修复会改变字节和哈希。覆盖原件就无法复核工具做了什么。正确做法是保存 source SHA256、repaired SHA256 和一一映射。
修复效果:Androguard 可解析数从 5/42 提升到 17/42,累计解析 28,103 个 class_def;修复副本的 SHA-1 与 Adler32 均为 42/42 正确。剩余 25 个解析失败项中,23 个为 HiddenApiClassDataItem 异常,2 个为 TypeMapItem 异常,没有被算成成功。
● ● ●0xSEC_CAPTURE / FIG.06
DEX 修复前后解析结果
图 6 重点图:DEX 头修复顺序、解析成功数变化和完整 SHA256 映射
0x08

从 42 个 DEX 中建立可分析的源码集合

为什么 42 个 DEX 不能全部算 App 代码?
Android 进程还会映射 boot classpath、系统框架和 MIUI 组件。内存扫描只认识 DEX,不认识归属;全部计入会夸大业务代码量。

按解析能力和命名空间筛选

修复后选入分析集合的 7 个 DEX 均能被 Androguard 解析,合计 27,210 个 class_def。它们出现 com/tiocloudcom/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.dex
Java 文件总数: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";
● ● ●0xSEC_CAPTURE / FIG.07
恢复出的业务源码和统计结果
图 7 重点图:真实业务方法体、分析集合 DEX 数量和 JADX 恢复结果
为什么这能证明业务源码恢复,却不能证明相关功能已经执行?
原包只有壳类,修复后出现注册参数和业务 API path,证明真业务 DEX 已恢复。但代码存在只是静态能力;本次未登录,也没有动态观察这些请求被调用。
仍有方法级保护:PwdLoginActivity.onCreate() 仍显示 native,并调用 StubApp.interface11。抓到 DEX 解决了整体隐藏,但未自动解除全部方法 native 化;后续需要 ART method dump 或 native 逆向。
0x09

脱壳前后到底有什么区别?

问题
脱壳前
内存转储后
修复并反编译后
能看到什么
StubApp、libjiagu、native 接口
42 个内存 DEX
登录、注册、配置等源码及其依赖
代码数量
4 class_def
不能把文件数当类数
7 DEX 分析集合:27,210 class_def;14,275 Java
能否分析业务
不能
只能做初步字符串检查
可以定位类、参数和方法体,并按命名空间排除第三方依赖
是否可安装
原包可以
不可以
本次未重打包验证
为什么源码恢复不等于可运行脱壳 APK?
重打包还涉及真实 Application、资源引用、native 桥、签名和完整性检测。用于静态分析时,恢复业务 DEX 已达到本次研究目的;可运行 APK 必须作为独立实验验证。
0x0A

AI 没有替我脱壳,它把“不会”拆成了下一步

这次最有价值的变化,不是 AI 记住了多少命令,而是每次卡住时,它都能把一个模糊问题改写成下一项可以测量的问题。答案仍然可能错,所以每一步都要回到真实文件和真实输出。

我卡住的地方
AI 帮我推进什么
最后怎样验收
JADX 只看到 StubApp
把“是不是壳”拆成 ZIP、Manifest、加载代码和运行时四层检查。
文件条目、Application、StubApp 调用链和运行时 DEX 相互印证。
Frida 17 无法 spawn
先区分环境故障、版本不匹配和样本反调试,不急着下结论。
保存失败日志;切换大版本对齐链路后用 frida-ps 探活并成功转储。
42 个 DEX 仍有 37 个打不开
检查 frida-dexdump 的修复边界,定位没有重算 SHA-1 和 Adler32。
解析率从 5/42 提升至 17/42;修复后 SHA-1、Adler32 均为 42/42 正确。
不知道哪些 DEX 值得进 JADX
生成逐文件解析与命名空间清单,避免只按文件编号猜归属。
7 个 DEX 合计 27,210 个 class_def,并明确其中同时存在第三方依赖。
为什么不能让 AI 直接宣布“脱壳成功”?
AI 生成的数字不能代替实测结果。哈希、DEX 数量、checksum、class_def 和源码命中必须由确定性程序计算。AI 可以组织步骤、解释异常、生成脚本草稿;样本身份、代码归属、结论边界仍需逐项核对。
我现在对“AI 自动化”的理解:不是把专业判断藏进一个按钮,而是把原本只有程序员熟悉的操作拆成普通学习者也能执行、看到输出、发现错误并继续追问的验证链。
0x0B

初学者最容易卡住的六个为什么

为什么照抄命令仍然失败?

先检查设备序列号、ABI、root、客户端/服务端版本和 frida-ps。命令格式通常不是首要问题。

为什么只得到 StubApp?

可能仍在静态分析原包,也可能扫描太早。确认壳已运行,重置应用状态后调整 sleep 复测,并分别保存每轮日志。

为什么 DEX 有魔数却打不开?

检查 file_size、SHA-1、Adler32 和 map 表,不要只依赖 file 命令。

为什么 JADX 输出很多系统类?

内存扫描会抓到整个进程映射的 DEX。按包名、Manifest、调用关系和第三方组件清单排噪。

为什么恢复后还有 native 方法?

DEX 整体隐藏与方法 native 化是两层保护。抓到 DEX 只解决第一层。

为什么要跑两次?

内存映射受启动时序影响。重复实验能区分稳定主体与偶然映射,本次两轮共有 37 个完全相同 SHA256。

0x0C

完整复现:让每个数字都能重新算出来

到这里,脱壳的完整路径已经讲完。但如果只留下“转储出 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 || true
复制前必须修改什么?
RUNAPKSERIALFRIDA_SERVERFRIDA_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.log
怎样解释失败?
Frida 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())
复核材料的边界:这些命令、日志、映射和脚本用于复算本文样本证据。换用另一份 APK 时,应生成属于新样本的哈希、数量和解析结果,不得照抄本文数字。
0x0D

第一篇之后,我还想把失败继续拆开

这篇先交代完整路径,后面的文章不再重复整条流程,而是把真正容易翻车的环节逐个做深:DEX 为什么看起来正常却无法解析、Frida 为什么进程存在仍不能通信、抓到 DEX 后为什么还有 native 方法。

AI 自动化脱壳 / 002 · 下一篇42 个 DEX 已经 Dump 出来,为什么只有 5 个能打开?

从 DEX Header、SHA-1、Adler32 到 5/42 → 17/42 的逐文件修复实验。

0xSec 笔记本AI 辅助电子取证实验 · 第 1 篇
Android 加固分析 · 0 号隔离设备实测 · 原始 APK 未移动

转载声明:本文转载自原发布平台 (作者:0xSec笔记本), 原文标题《AI自动化脱壳①:以前我不会 APK 脱壳,直到 AI 帮我导出 42 个运行时 DEX》, 查看原文。 版权归原作者及原发布平台所有,本站仅作收录与展示,未对正文内容作实质性修改; 若涉及侵权请联系本站处理。