Android 脱壳实战:Frida Hook dlopen + AI 辅助,在内存中 dump 出被加固的 DEX
静态分析(Static Analysis)面对加固 APP 是无力的,因为 DEX 是加密存储的。但动态运行(Dynamic Runtime)不会撒谎。加载壳的 SO:通过加载壳的 Native 代码。解密 DEX:在 Native 层解密出原始 DEX 数据。加载 DEX:调用系统函数(如OpenCommonOpenMemory)将 DEX 放入内存,准备执行。守株待兔。我们不需要知道它怎么解密
标签: #AndroidReverse #Frida #脱壳 #MobileSecurity #Hook #AI辅助
⚠️ 免责声明:本文仅供技术研究与安全防御教学使用。请勿将相关技术用于非法破解、制作外挂或破坏商业软件,否则后果自负。
📉 前言:脱壳的核心原理——“落地为安”
静态分析(Static Analysis)面对加固 APP 是无力的,因为 DEX 是加密存储的。但动态运行(Dynamic Runtime)不会撒谎。
当 APP 启动时,加固壳的逻辑通常如下:
- 加载壳的 SO:通过
System.loadLibrary加载壳的 Native 代码。 - 解密 DEX:在 Native 层解密出原始 DEX 数据。
- 加载 DEX:调用系统函数(如
OpenCommon,OpenMemory,DefineClass)将 DEX 放入内存,准备执行。
我们的战术很简单:守株待兔。
我们不需要知道它怎么解密的,我们只需要 Hook 住 加载 DEX 的那个系统函数,把它的参数(DEX 内存地址)拿出来,写入文件。
脱壳攻击路径 (Mermaid):
🛠️ 一、 寻找切入点:为什么是 dlopen?
在 Android 中,加载动态链接库(.so)的底层核心函数是 dlopen (或 android_dlopen_ext)。加固壳通常会在 JNI_OnLoad 或 .init_array 中尽早执行解密逻辑。
如果我们直接 Hook libart.so 里的 OpenMemory,可能会因为 libart.so 还没加载或者是壳还没跑起来而失败。
Hook dlopen 的目的是为了寻找“时机”:
- 确保
libart.so已经加载,所有的 ART 运行时函数地址都能找到了。 - 或者监听壳自己的 SO (
libjiagu.so,libbangcle.so) 何时加载,以此作为开始 Dump 的信号。
💻 二、 实战脚本:Frida 核心代码
我们将编写一个 TypeScript/JavaScript 脚本。为了适配不同 Android 版本(C++ 符号名不同),我们可以引入 AI 来辅助生成模糊匹配逻辑。
1. 监听 SO 加载 (The Trigger)
首先,拦截 android_dlopen_ext 来感知库的加载。
// hook_dlopen.js
function hook_dlopen() {
// 适配 Android 7.0+ 的 dlopen
const dlopen = Module.findExportByName(null, "android_dlopen_ext");
if (dlopen) {
Interceptor.attach(dlopen, {
onEnter: function(args) {
// args[0] 是 so 文件的路径
this.path = args[0].readCString();
},
onLeave: function(retval) {
if (this.path && this.path.indexOf("libart.so") >= 0) {
console.log("[+] libart.so loaded! Ready to hook ART functions.");
hook_art(); // libart 加载了,开始 Hook 核心函数
}
}
});
} else {
console.log("[-] android_dlopen_ext not found, trying dlopen...");
// 尝试旧版 dlopen...
}
}
2. 核心 Hook:OpenMemory (The Dump)
在 libart.so 中,加载 DEX 的关键函数通常是 OpenMemory。
AI 辅助点:由于 C++ Name Mangling(符号修饰),函数名会变得很长且乱(例如 _ZN3art7DexFile10OpenMemory...)。我们可以让 AI 帮我们写一个模糊匹配的逻辑。
Prompt 给 AI: “Frida script to enumerate symbols in libart.so and find functions containing ‘OpenMemory’ and ‘DexFile’.”
生成的代码逻辑如下:
function hook_art() {
const libart = Process.findModuleByName("libart.so");
if (!libart) return;
// 遍历符号,寻找包含 OpenMemory 的函数
const symbols = libart.enumerateSymbols();
let openMemoryAddr = null;
for (let i = 0; i < symbols.length; i++) {
const name = symbols[i].name;
// 模糊匹配 OpenMemory,这是加载内存 DEX 的常用函数
if (name.indexOf("OpenMemory") >= 0 && name.indexOf("DexFile") >= 0) {
openMemoryAddr = symbols[i].address;
console.log("[*] Found OpenMemory: " + name);
break;
}
}
if (openMemoryAddr) {
Interceptor.attach(openMemoryAddr, {
onEnter: function(args) {
// OpenMemory 参数通常是 (base, size, location, check_checksum, ...)
// ⚠️ args[0] 通常是 DEX 在内存中的起始地址 (const uint8_t* base)
// ⚠️ args[1] 或者是 DEX 的大小
// 这里假设 args[0] 是 base 地址 (不同版本需微调)
const dexBase = args[0];
console.log("[*] OpenMemory called. Base: " + dexBase);
// 策略:读取 Header 中的 filesize 字段
// DEX Header + 32 字节处是 file_size (4 bytes, Little Endian)
// 偏移 32 = 0x20
const fileSize = dexBase.add(0x20).readU32();
console.log("[*] Dex Size from Header: " + fileSize);
// 简单的 Magic Header 检查 ('dex\n035')
// 0x64 0x65 0x78 0x0A
const magic = dexBase.readU32();
if (magic == 0x0A786564) { // Little Endian "dex\n"
dump_dex(dexBase, fileSize);
}
}
});
}
}
3. 写入文件 (The Output)
将内存数据保存到 APP 的私有目录下(避免权限问题)。
function dump_dex(base, size) {
// 构造文件名:/data/data/包名/files/dump_size.dex
const filename = "/data/data/com.example.targetapp/files/" + size + ".dex";
const file = new File(filename, "wb");
if (file) {
// 从内存读取字节流
const buffer = base.readByteArray(size);
file.write(buffer);
file.flush();
file.close();
console.log("[+] DEX Dumped successfully: " + filename);
}
}
// 启动脚本
setImmediate(hook_dlopen);
🔎 三、 运行与验证
- 启动 Frida Server: 在手机端运行
frida-server。 - 执行攻击:
# -U: USB设备, -f: 强制启动 APP (让 dlopen 尽早触发)
frida -U -f com.example.targetapp -l hook_dlopen.js --no-pause
- 观察日志:
[+] libart.so loaded![*] Found OpenMemory...[+] DEX Dumped successfully...
- 提取文件:
adb pull /data/data/com.example.targetapp/files/ .
⚠️ 四、 避坑指南:壳的对抗手段
现在的壳也没那么傻,它们有反制措施:
- DEX 头部抹除:
壳在加载完 DEX 后,会故意把内存中 DEX 文件的 Header(魔数dex.035)抹成 00,防止你通过搜索 Header 特征来 Dump。
- 对策:Dump 出来后,用 010 Editor 手动修复头部,把
64 65 78 0A填回去。
- 函数抽取 (Code Item Extraction):
你 Dump 出来的 DEX,里面的 Method 指令全是空的(nop),或者是一个无效的跳转。真正的指令在执行时才通过OnMethodEnter动态恢复。
- 对策:这是高阶对抗。需要使用 Frida-DexDump (基于内存搜索所有
dex特征) 或者定制化的 ART 虚拟机(如 FART)来进行“主动调用”,强迫壳把指令还原回内存。
🎯 总结
通过 Hook dlopen 和 OpenMemory,我们绕过了复杂的解密算法,直接在终点站截获了 DEX。这就是**“降维打击”**。
不管加密算法多牛,数据终究是要给 CPU 跑的。只要它敢在内存里露头,Frida 就能把它揪出来。
Next Step:
你 Dump 出来的 DEX 很可能是“函数抽取”后的残缺版。建议下一步学习如何使用 FART (Fast Android Runtime) 原理,或者搜索 GitHub 上的 Frida-DexDump 工具,对比一下手动 Hook 和自动化扫描工具的效果差异。
更多推荐

所有评论(0)