1. 为什么需要把 Vdex 转换回 Dex
做安卓应用分析和系统调试的朋友,应该都有过这种经历:从设备或者系统镜像里捞出一个.vdex后缀的文件,打开看一眼全是二进制乱码,用file命令一看,显示的是Android dex file或者干脆是未知格式。这时候如果想把里面的代码逻辑拿出来用jadx或者jeb看看,直接喂给工具,大概率会报错或者提示文件格式不支持。
其实.vdex就是 ART 虚拟机在安装应用时生成的一种打包文件,里面装着优化过的 dex 字节码和一堆验证数据。从 Android 7.0(Nougat)开始,系统把 dex、验证结果、快速编译产生的辅助信息统一塞进了 vdex 文件里,用来加快应用启动速度、减少运行时开销。但这个格式对分析工具很不友好——jadx、jeb、baksmali这些反编译工具认的是标准 dex 文件,碰到 vdex 基本不认。
vdexExtractor 就是干这个的:输入一个 vdex 文件,经过解析、校验、抽取,输出标准的 dex 文件。拿到 dex 之后,后面怎么反编译、怎么分析,就完全回到你熟悉的流程上了。
这篇文章我会从 vdex 的结构原理讲起,覆盖 vdexExtractor 的编译部署、完整命令行操作、常见报错排查方法,最后再分享几个实战中才用得上的细节。内容面向的是有安卓开发基础、想深入做应用逆向分析或系统研究的同学,纯小白的建议先补一下 dex 和 ART 虚拟机的基础知识再来读。
2. Vdex 文件结构拆解:搞懂格式才能玩得转
2.1 一个 Vdex 文件里到底装了什么
要理解 vdexExtractor 为什么能还原 dex,得先搞清楚 vdex 的文件布局。一个标准的 vdex 文件由三个部分组成,依次排列:
- vdex 文件头(Header):记录了魔数、版本号、dex 文件数量、dex 文件在 vdex 中的偏移量和大小,以及验证数据、快速编译数据的偏移和大小。
- dex 文件区(Dex Section):包含一个或多个 dex 文件。这里可能是完整未压缩的 dex,也可能是经过压缩存储的 dex 数据。
- 辅助数据区(Auxiliary Data):存放 dex 文件对应的校验注解(Verifier Dependencies)和快速编译数据(Quickening Data),这部分是 ART 为了运行时优化额外生成的,分析代码逻辑时通常用不上。
用十六进制编辑器打开 vdex 文件,头部最开始四个字节如果是76 64 65 78,也就是 ASCII 码的vdex,那基本可以确认这是一个 vdex 文件。再往后读取 12 字节处的版本号,常见的版本有019、021、027、028、029等,分别对应不同的 Android 版本和编译器实现。
vdexExtractor 能做的就是解析文件头,拿到 dex 文件的偏移和大小,然后根据这些元数据把 dex 区域的数据完整抠出来,同时绕过或修正校验机制,最终输出标准的.dex文件。
2.2 为什么不能直接把 dex 部分复制出来
这就得提到 vdex 文件里的 dex 数据状态了。Android 编译器在生成 vdex 时,对 dex 文件区做了特殊处理:如果 dex 文件的校验和(checksum)与文件头记录的 checksum 不一致,ART 会触发重新验证机制把 dex 重新解析一遍;而在部分 Android 版本上,vdex 里的 dex 是以"已进行快速编译"的形态存在的,里面的某些指令可能已经被替换或者打上了补丁。
还有一个典型坑:在 Android 8.0 到 9.0 时期,speed-profile编译模式下的 vdex,其内嵌 dex 的 checksum 是被人为修改过的。直接用dd之类的命令把 dex 区块扣出来,得到的 dex 文件往往打不开,jadx会报Dex checksum mismatch之类的错误。所以真正靠谱的方式是走 vdexExtractor 这类工具,它会自动处理 checksum 修复和 dex 重建工作。
vdexExtractor 在抽取 dex 文件后会自动重算 dex 头的 checksum,把文件修正为标准合法的 dex 格式。这也是我强烈建议用它而不是自己写脚本去抠数据的原因——省事,而且处理边界场景的能力比野路子脚本强得多。
3. vdexExtractor 的编译部署:从源码到可执行文件
3.1 环境准备与依赖关系
vdexExtractor 是 C 语言项目,依赖系统工具链编译,没有特别复杂的外部库依赖。官方推荐在 Linux 或 macOS 环境下编译,Windows 环境下需要用 WSL 或者 Cygwin,但我实测下来还是 WSL 最省心。
编译环境需要以下基础工具:
git:拉取源码make:执行编译脚本gcc或clang:C 编译器zlib开发库:用于解压部分压缩存储的 dex 数据
以 Ubuntu 22.04 为例,安装依赖就三条命令:
sudo apt update sudo apt install git make gcc zlib1g-devmacOS 需要确认安装了 Xcode Command Line Tools,xcode-select --install跑一下即可。zlib 在 macOS 上是自带的,不需要额外装。
这里有个细节值得注意:vdexExtractor 支持交叉编译,可以编译出在 Windows 上运行的.exe版本,但需要先装好 mingw-w64 工具链。比如用以下命令在 Linux 上交叉编译 Windows 版本:
make mingw如果只是日常分析用,我更推荐直接在有 vdex 文件的平台上原生编译,少一层交叉编译的兼容性折腾。
3.2 完整编译流程与验证
源码仓库在 GitHub 上,搜索 vdexExtractor 就能找到。克隆到本地后,直接make编译:
git clone https://github.com/anestisb/vdexExtractor.git cd vdexExtractor make编译完成后,在bin目录下会生成vdexExtractor可执行文件。先验证一下版本信息和帮助文档是否正常:
./bin/vdexExtractor -h如果能看到类似vdexExtractor 0.5.3的版本输出和参数说明,就说明编译成功了。建议顺手把二进制文件复制到一个单独的目录里,配合后续分析工作统一管理。
编译过程中如果遇到make: command not found,那说明系统没装构建工具,Ubuntu 上执行sudo apt install build-essential即可。遇到zlib.h: No such file or directory说明 zlib 开发包没装,重新安装zlib1g-dev然后重新编译。
整个编译过程通常在一两分钟内完成,没有任何魔法,跑完就能拿到可执行文件。这一款工具这么轻量,也是我优先推荐它的原因之一——对比那些一堆依赖的 Python 脚本和需要配置环境的 IDA 插件,它真的是一键搞定。
4. 实操全流程:一行命令完成 Vdex 转 Dex
4.1 基本转换命令与参数说明
拿到编译好的 vdexExtractor,使用场景非常直接。基础命令长这样:
./bin/vdexExtractor -i input.vdex -o output_dir参数含义如下:
-i:指定输入的 vdex 文件路径-o:指定输出目录,转换得到的 dex 文件会放在这个目录下
跑完之后去 output_dir 里看一眼,里面应该会出现classes.dex、classes2.dex之类的文件,名称取决于原始 vdex 里封装了几个 dex。如果是多 dex 应用,vdex 里会包含多个 dex 文件,工具会依次导出并自动命名。
实际分析时我习惯加一个-f参数:
./bin/vdexExtractor -i input.vdex -o output_dir -f-f表示输出 dex 文件时用 unzip 格式重新压缩,这样输出的文件体积更小,后续传给别人分析或者归档保存都比较方便。不加-f时输出的是不压缩的 dex,虽然文件更大,但个别反编译工具对未压缩 dex 的兼容性反而更好,这取决于你后续工具链的偏好。
还有一个值得关注的参数是--ignore-crc-error:
./bin/vdexExtractor -i input.vdex -o output_dir --ignore-crc-error这个参数的意思是忽略 dex 文件的 CRC 校验错误。前面提过,部分 vdex 里的 dex checksum 是被人为改过的,正常情况下 vdexExtractor 会尝试自动修复,但如果遇到修复失败或者特殊变体,加这个参数可以直接跳过校验强行抽取。优先级排序是:先不加参数跑一遍,报 CRC 错误了再加--ignore-crc-error重试,不要一上来就跳过校验,否则抽出来的 dex 可能有隐患。
4.2 验证转换结果是否可用
转换完成后不要急着关终端,先验证一下输出文件是不是合法 dex。
第一步用file命令检查格式:
file output_dir/classes.dex正常输出应该类似:
classes.dex: Dalvik dex file version 035看到Dalvik dex file字样就基本没问题了。如果输出是data或者Java archive (JAR),说明格式不对,需要返回上一步检查参数。
第二步用dexdump或者jadx验证可分析性。以jadx为例:
jadx output_dir/classes.dex -d output_src如果能够成功输出反编译后的 Java 代码,那这个 dex 就完全可用了。这一步是最终验收标准——你自己写的脚本说格式合法不算,反编译工具认了才算真能用。
实战中我还习惯用baksmali做二次验证,把 dex 转成 smali 指令看看整体结构是否完整。如果 smali 解析顺利,顺手还能对比一下 vdex 原始文件里的快速编译指令是不是被正常还原成了标准指令。这一层验证对后面代码审计或者去做深度分析的可靠性很重要。
4.3 批量转换多个 Vdex 文件
实际项目里经常碰到一个目录下有几十个 vdex 文件(比如系统镜像里/system/framework目录下成堆的中间框架包),一个一个跑命令太原始了。我一般用for循环批量处理:
mkdir -p dex_output for vdex in $(find . -name "*.vdex"); do name=$(basename "$vdex" .vdex) mkdir -p "dex_output/$name" ./bin/vdexExtractor -i "$vdex" -o "dex_output/$name" -f done这样每个 vdex 都会有一个独立输出目录,不会被混到一起。批量跑的时候要注意输出信息里是否有大量[ERROR]行,如果是特定文件反复报错,单独拎出来手动调试,别让报错在脚本里被淹没了。
我用这个方式处理过一整个系统镜像的 framework 目录,大概 40 多个 vdex 文件,全程没用超过两分钟——这也是 vdexExtractor 相比其他要跑虚拟机或 Python 模拟器的方案最大的优势,纯 C 编译出的原生二进制效率确实没得说。
5. 分版本适配:Android 8.0 到 12.0 的 Vdex 差异与处理
5.1 不同版本的文件头与编译器差异
vdexExtractor 之所以要不断更新版本,根本原因是 ART 编译器在 Android 各版本间变动太频繁,vdex 的文件头格式、辅助数据结构、dex 存储形态都在跟着变。拿版本号来说,早期 Android 8.0 的 vdex 版本是006,Android 8.1 是010,Android 9.0 有012和017,Android 10 用019,Android 11 升级到021,Android 12 则是027和028,不同版本之间 dex 区的 metadata 布局和校验逻辑都有差异。
如果你的 vdexExtractor 版本比较旧,碰到新版 vdex 文件时会报Header check failed或者Unsupported vdex version之类的错误。破解办法有两个:一是升级 vdexExtractor 到最新版本,这是首选;二是老项目里如果必须用旧工具,仔细阅读报错信息里提示的版本范围,看是否有兼容模式可以强行解析。
这里就得提醒一句:平时注意留意 vdexExtractor 仓库的更新动态,新手机发布或者 Android 大版本更新之后,最好立刻拉一下源码重新编译,因为大概率那个版本的新 vdex 变体会被社区在几周内修复并合入主分支。
5.2 特殊变体处理:压缩 dex 与快速编译指令
Android 9 及之后的版本中,vdex 文件里的 dex 出于节省空间考虑,可能会以storage mode为kDexStorageModeJar的方式存储,也就是把 dex 文件压缩进 jar 容器里。vdexExtractor 对这类变体是支持的,会在解析时自动解压,前提是你的编译环境里有 zlib 支持。这也就是为什么 3.1 里特意强调要装zlib1g-dev。
还有一种变体是 dex 里包含快速编译指令。ART 的 quickening 过程会把部分 dex 操作码替换为优化过的快速指令,这些指令对标准 dex 格式来说是不合法的。vdexExtractor 在抽取时会把受影响的操作码还原回标准 dex 指令,保证输出文件能正常通过 dex 文件格式校验。如果抽出来的 dex 在 jadx 里打开后大量方法体是空的或者CodeItem解析异常,十有八九就是快速编译指令还原不彻底。这时候建议用最新版工具重新抽取,并配合baksmali --use-locals之类的去混淆参数辅助分析。
6. 典型报错与排查记录:照着速查表解决问题
6.1 Header check failed 和其他解析类错误
我整理了一张速查表,覆盖 vdexExtractor 最常见的几类报错。
| 报错信息 | 触发场景 | 解决办法 |
|---|---|---|
Header check failed | 文件头魔数或版本号不匹配 | 确认文件是否为有效 vdex,升级工具到最新版 |
Unsupported vdex version | vdex 版本超出工具支持范围 | 更新源码重新编译 |
CRC check failed | dex 校验和与文件头不一致 | 加--ignore-crc-error参数尝试强行抽取 |
Failed to open file | 输入路径不对或权限不足 | 检查文件路径和设备权限,用 chmod 或 sudo |
zlib decompression failed | 压缩 dex 解压失败 | 确认 zlib 开发库安装正确,文件可能损坏 |
No dex files found in input | 输入文件不是标准 vdex 结构 | 用十六进制编辑器检查文件头魔数 |
遇到Header check failed时,我优先怀疑文件版本过新,先看看文件头的版本号,再到工具源码的vdex_versions.h里比对一下支持列表。如果确实不在支持范围内,就直接升级工具,不用浪费时间在旧版本上变魔法。
6.2 输出 dex 不可用时的排查方向
有一种情况是命令执行没有任何报错,输出的 dex 也能用file识别,但jadx反编译时大量类缺失、方法体为空白。这种"假成功"的现场,我的排查顺序是:
- 第一,确认 vdexExtractor 输出的 dex 文件大小是否正常。如果 dex 文件只有几 KB,而原始 vdex 有几十 MB,那肯定是抽取环节出了问题。
- 第二,试试不加
-f参数重新输出未压缩 dex,个别反编译工具对压缩过的 dex 解析支持不佳。 - 第三,如果还是不行,用
dexdump直接 dump dex 的 method 数量和一些关键偏移量,和原始 apk 的 dex 做交叉比对,看数据是否完整。
大部分"假成功"本质还是工具版本和文件变体不匹配造成的,升级工具版本能解决绝大多数问题。
7. 实战中的几个细节:来自一线折腾的经验
7.1 优先从系统目录批量收集 Vdex 文件
分析一台设备上的应用时,vdex 文件不只局限在/data/app目录,/system/framework和/system/app下也会有大量系统应用和框架包的 vdex。尤其在分析系统级问题、搞框架层调试或者对比系统内置组件行为时,系统目录里的 vdex 价值反而更大——系统应用的 dex 是系统镜像的一部分,很多厂商改过的 API 实现就藏在这些包里。
我自己比较常用的收集方式是 adb pull 整个相关目录:
adb pull /system/framework framework_vdex adb pull /data/app app_vdex/data/app目录需要 root 权限才能读取,这一点要先确认。如果你做的是非 root 设备上的应用分析,vdex 文件基本拿不到,那 vdexExtractor 就不是对口工具了,建议换思路从 apk 本身的 dex 入手。
7.2 和 jadx、frida 配合的完整分析工作流
vdexExtractor 通常不是终点,做完转换之后还得跟后续分析工具串起来才完整。我个人比较顺手的流程是这样:
第一步,用 vdexExtractor 把 vdex 转成 dex,得到干净的 dex 文件集。
第二步,把 dex 重新打包进一个只有 dex 的 apk 或者直接喂给 jadx:
jadx -d output_src output_dir/classes.dex第三步,如果反编译出来的代码太乱,配合baksmali转成 smali,然后用 smali 级别动态调试:
baksmali d output_dir/classes.dex -o smali_out第四步,需要动态验证的时候,结合frida,通过 hook 关键方法观察运行时行为和参数变化,形成静态分析和动态分析的闭环。
这套流程我用在很多次应用行为分析、崩溃问题排查和兼容性调查中,基本覆盖了日常遇到的绝大多数需求场景。值得一提的是,vdex 里既有 dex 也有 ART 的验证信息,某些场景下这些验证信息能间接反映应用在设备上的真实运行方式,做 frida hook 时对照着 vdex 里的版本信息选择匹配的 hook 点,成功率会明显提升。
7.3 输出文件的管理与归档建议
处理大量 vdex 文件时,输出结果的命名和归档混乱是一个容易被忽略但很费时间的问题。vdexExtractor 对输出文件会自动按原始名字命名,但多个 apk 产生的 dex 可能都是classes.dex,放在同一个目录下直接互相覆盖。
我习惯按包名或 apk 名建立两级目录,结构类似这样:
analysis_output/ ├── com.example.app1/ │ ├── classes.dex │ └── classes2.dex ├── com.example.app2/ │ └── classes.dex └── framework_jar/ ├── classes.dex └── classes2.dex配合批量转换脚本里的mkdir -p就能实现这个结构,成本很低。等到要做横向对比或者回去翻旧项目的时候,你就会发现这个分类方式能帮你省出大量时间。
还有一个小建议:转换成功后顺手生成一个 MD5 校验文件,把原始 vdex 和输出 dex 的哈希值都记下来。设备上拿到的 vdex 可能随时会被系统重新编译替换,留一份哈希记录,后续做版本对比或者判断系统是不是重新优化过,会非常有用。命令行一行就搞定:
md5sum input.vdex output_dir/classes.dex > checksums.txt7.4 版本兼容性的坑要提前踩
最后重点说一个我自己被坑过很多次的问题:系统镜像里的 vdex 和当前设备系统版本并不总是一一对应的。厂商 OTA 升级后,/system/framework下的 vdex 可能还是旧系统版本编译出来的,而/data/app下的 vdex 已经被新系统重新编译过了。这时候同一台设备上可能存在两个不同版本的 vdex 文件,用固定版本的 vdexExtractor 必然有一半会解析失败。
我的处理方法是写一个 wrapper 脚本,先自动识别 vdex 头部版本号,再根据版本选择已编译好的对应版本工具:
version=$(xxd -p -l 4 -s 12 input.vdex | xxd -r -p) echo "vdex version: $version"版本确认后手动指定工具路径去处理单个文件。虽然不是完全自动化,但能明显降低排查成本。如果你工作环境里需要频繁处理不同 Android 版本的 vdex,建议在本地一次性编译好几个主流版本的 vdexExtractor 备用。
另外提一句,vdexExtractor 项目本身维护频率不算高,隔一段时间就会有新版本或者新分支出现,遇到新版 vdex 解析不了就去看一下仓库的 recent commits,一般能找到答案。这工具整体已经比较成熟,核心功能这么多年没有大变化,掌握本文的内容之后,应付日常分析已经足够了。
我在实际项目里用它处理过的 vdex 文件数量至少上千了,从 Android 8 到 Android 14 的设备都有覆盖,可以说每一轮都是先转 dex、再丢 jadx、配合 frida 动态验证,这套组合拳几乎没让我失望过。希望这篇文章的细节能帮你把 vdexExtractor 这条工具体系完整跑通,少踩几个我当年踩过的坑。