一、现象
2026-03-27 22:07:26.994 2716-2766/? W/dex2oat: Compilation of java.util.Map c.d.c.b.h.a(java.lang.reflect.Type) took 177.351ms
dex2oat: /system/bin/dex2oat --dex-file=/data/data/com.jiaoyinbrother.monkeyking/.jiagu/classes.dex --dex-file=/data/data/com.jiaoyinbrother.mon
在日常逆向工程中,发现logcat日志中有这两条日志, 通过在线搜索源码定位其关键部位是:

http://xrefandroid.com/android-8.1.0_r81/xref/art/compiler/driver/compiler_driver.cc#613
也就是所谓的: static void CompileMethod 函数,本人是在aosp8.1中分析的,此为日志1
日志2:oat_file_assistant.cc - OpenGrok cross reference for /art/runtime/oat_file_assistant.cc

和上述日志非常相似, 然后往下跟看到了Dex2Oat 这个函数,发现最后是执行了:

执行了Exec return Exec(argv, error_msg); // fork + execv → 启动新进程 ,它只是一个wrapper,等价于在 shell 中执行/system/bin/dex2oat --dex-file=xxx --oat-file=xxx,上述的CompileMethod函数说明了dex2Oat在编译的过程中是逐个函数编译的,并且我发现都是系统重启的时候,就会有上述两条log日志,所以,为了便于后续分析,我们要禁用dex2Oat, Dex2Oat函数返回的是bool, 那么我们就直接在Exec前面加上False就行了,然后刷机。
二、android 应用主动调用dex2oat
某空租车app:由壳代码自行调用 /system/bin/dex2oat (通过 Runtime.exec() 或 ProcessBuilder 直接执行 dex2oat 二进制)
cpp
2026-03-29 15:46:33.238 2678-2678/? I/dex2oat: /system/bin/dex2oat --dex-file=/data/data/com.jiaoyinbrother.monkeyking/.jiagu/classes.dex --dex-file=/data/data/com.jiaoyinbrother.monkeyking/.jiagu/classes.dex!classes2.dex --dex-file=/data/data/com.jiaoyinbrother.monkeyking/.jiagu/classes.dex!classes3.dex --oat-file=/data/data/com.jiaoyinbrother.monkeyking/.jiagu/oat/arm64/classes.odex --inline-max-code-units=0 --compiler-filter=speed

这个过程完全绕过了 OatFileAssistant::Dex2Oat() ,而是直接执行 /system/bin/dex2oat 这个 独立的可执行文件,为了验证,我们可以在dex2oat.cc main方法中加入一个log日志:
cpp
int main(int argc, char** argv) {
LOG(INFO) << "dex2oat invoked by pid=" << getpid()
<< " ppid=" << getppid()
<< " uid=" << getuid();
int result = static_cast<int>(art::Dex2oat(argc, argv));
// Everything was done, do an explicit exit here to avoid running Runtime destructors that take
// time (bug 10645725) unless we're a debug build or running on valgrind. Note: The Dex2Oat class
// should not destruct the runtime in this case.
if (!art::kIsDebugBuild && (RUNNING_ON_MEMORY_TOOL == 0)) {
_exit(result);
}
return result;
}
需要:#include <android-base/logging.h> // 系统LOG #include <unistd.h> // getpid(), getppid() #include <sys/types.h> // getuid()
编译刷机,刷入system.img, log日志就可以看到:2026-03-29 15:46:33.226 2678-2678/? I//system/bin/dex2oat: dex2oat invoked by pid=2678 ppid=2246 uid=10066
使用命令:ps -A | grep 2246 或者 cat /proc/2246/cmdline 就可以知道哪个进程在调用了。
加固壳(.jiagu)的典型行为: 应用进程 (com.jiaoyinbrother.monkeyking, PID 4112) ├── 解密释放 dex 到 .jiagu/ 目录 ├── fork + exec /system/bin/dex2oat ← 直接执行二进制 │ 参数: --dex-file=.jiagu/classes.dex --oat-file=.jiagu/oat/arm64/classes.odex └── 加载编译后的 odex 执行;正常应用的 dex 在安装时由 installd / PackageManagerService 触发 dex2oat 编译。但加固壳的真实 dex 是运行时才解密释放 的,系统完全不知道 .jiagu/classes.dex 的存在,不会帮你编译。如果壳不调用 dex2oat,就只能走解释执行或 JIT,首次启动会非常卡顿。调用 dex2oat 做 AOT 编译后,后续启动直接加载 odex,体验接近正常应用。第一次启动:解密 dex → dex2oat 编译 → 生成 odex → 加载执行(慢) 第二次启动:解密 dex → 发现 odex 已存在 → 直接加载(快)
三、dex2oat 中的脱壳时机
**dex2oat 的执行反而给了脱壳者更多机会,但也带来一些复杂性。**
1dex2oat 过程中的脱壳机会
1. dex 文件落地磁盘
解密前:APK 中的 dex 是加密的,无法直接获取
解密后:壳必须将明文 dex 写入磁盘,供 dex2oat 读取
/data/data/包名/.jiagu/classes.dex ← 明文 dex!
/data/data/包名/.jiagu/classes.dex!classes2.dex
/data/data/包名/.jiagu/classes.dex!classes3.dex
**脱壳方法:** 在 dex 写入磁盘后、dex2oat 执行期间,直接拷贝这些文件:
```bash
简单粗暴
cp /data/data/包名/.jiagu/classes.dex /sdcard/dumped.dex
```
2dex2oat 进程能看到完整 dex
dex2oat 作为独立进程,必须**完整读取 dex 文件**才能编译:
```
应用进程 (加密/混淆保护)
│
│ exec
▼
dex2oat 进程 (独立进程,保护较少)
│
├── 打开 classes.dex → mmap 到内存
├── 解析所有 class_def、method、code_item
├── 编译每个方法为机器码
└── 输出 odex
```
**脱壳方法:** hook 或修改 dex2oat,在它读取 dex 后 dump。具体的脱壳点:dex2oat.cc - OpenGrok cross reference for /art/dex2oat/dex2oat.cc main方法,然后调用了:
bool OatFileAssistant::Dex2Oat
static dex2oat::ReturnCode CompileApp(Dex2Oat& dex2oat)
最后编译app的dex文件,肯定是
static void CompileMethod
他会一个一个方法去编译,这地放就是脱壳点。compiler_driver.cc - OpenGrok cross reference for /art/compiler/driver/compiler_driver.cc
```cpp
// 修改 art/dex2oat/dex2oat.cc
// 在 dex 文件被打开后
static int dex2oat(int argc, char** argv) {
// ... 正常逻辑 ...
// dex 文件已加载到内存,此时 dump
for (const auto& dex_file : dex_files) {
// dex_file->Begin() 就是完整的明文 dex
SaveToFile(dex_file->Begin(), dex_file->Size(), "/sdcard/dump.dex");
}
}
```
3生成的 odex 包含完整信息
odex/vdex 反向获取 dex
当然这种方法不常用,借助于frida可以很轻松的脱壳
4各种脱壳时机对比
```
时间线:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
壳解密 dex dex 写入磁盘 dex2oat 执行 odex 生成 加载执行
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
│ │ │ │
│ ▼ ▼ ▼
│ ┌─────────┐ ┌──────────┐ ┌───────────┐
│ │ 拷贝 dex │ │hook dex2oat│ │提取vdex/odex│
│ │ 文件 │ │ dump 内存 │ │ 反编译 │
│ └─────────┘ └──────────┘ └───────────┘
│
│ 脱壳点1 脱壳点2 脱壳点3 脱壳点4
```
5壳的对抗手段
壳如何防止通过 dex2oat 脱壳
```
对抗手段 效果
─────────────────────────────────────────────────
不调用 dex2oat 只走解释执行,不落地完整 dex
(牺牲性能)
或者系统禁用dex0at,对于二代壳(抽取壳便是如此),对于这种的,开源工具fart可以用,还有其他的。
6实际脱壳策略
在 ART 运行时 hook `ClassLinker::DefineClass` 或 `DexFile::OpenMemory`:
```cpp
// frida 脚本示例
Interceptor.attach(Module.findExportByName("libart.so", "_ZN3art11ClassLinker11DefineClassEPNS_6ThreadEPKcmNS_6HandleINS_6mirror11ClassLoaderEEERKNS_7DexFileERKNS9_8ClassDefE"), {
onEnter: function(args) {
// args[2] = descriptor (类名)
// args[7] = DexFile*
// 从 DexFile 中 dump 完整内容
}
});
```
为了防止dexoat,我们可以修改系统源码禁用掉,或者清空app数据,重新启动,在类的加载流程上去脱壳