android dex2oat 编译dex文件分析

一、现象

复制代码
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数据,重新启动,在类的加载流程上去脱壳

相关推荐
Digitally6 分钟前
6 种简单方法:在 Mac 电脑与安卓手机之间传输文件
android
鹏程十八少8 分钟前
3. 2026金三银四 Android 背完这 23 道题,Android 线程面试横着走
android·面试·前端框架
冬奇Lab11 小时前
Android 开发要变天了:Google 专为 Agent 重建工具链,Token 减少 70%、速度提升 3 倍
android·人工智能·ai编程
imuliuliang13 小时前
存储过程(SQL)
android·数据库·sql
AgCl2315 小时前
MYSQL-6-函数与约束-3/17
android·数据库·mysql
zzb158016 小时前
Fragment 生命周期深度图解:从 onAttach 到 onDetach 完整流程(面试必备)
android·java·面试·安卓
众少成多积小致巨16 小时前
Android 源码查看笔记
android·源码
angerdream16 小时前
Android手把手编写儿童手机远程监控App之前台服务
android
敲代码的瓦龙18 小时前
Android?Activity!!!
android
重生之我在安卓搞音频19 小时前
二、Android 音频框架
android·音视频