你提到的是在Android 8以下系统中使用Frida配合fdex2进行应用脱壳的工具组合。让我详细介绍一下:
- 动态插桩工具,通过JavaScript代码注入到目标进程
- 可Hook Java方法和Native函数
- 支持Android 4.4-11+(不同版本兼容性不同)
- 专门用于Android应用的DEX文件脱壳工具
- 基于Xposed框架或Frida实现
- 主要针对市面上常见的加固方案(如梆梆、360、腾讯等)
# 安装Frida
pip install frida-tools
# 下载对应架构的frida-server
# 查看设备架构:adb shell getprop ro.product.cpu.abi方法一:直接使用fdex2的Frida脚本
// fdex2_frida.js 核心代码示例
Java.perform(function() {
var classLoader = Java.use("java.lang.ClassLoader");
classLoader.loadClass.overload('java.lang.String').implementation = function(name) {
var result = this.loadClass(name);
var DexFile = Java.use("dalvik.system.DexFile");
// 获取DEX文件数据并导出
// ...
return result;
};
});方法二:改进版的多DEX支持
Java.perform(function() {
var DexFile = Java.use("dalvik.system.DexFile");
var mCookie = DexFile.mCookie.value;
// 遍历所有已加载的DEX
var dexFiles = DexFile.getDexFileList(mCookie);
for (var i = 0; i < dexFiles.length; i++) {
var dexPath = dexFiles[i];
dumpDexFile(dexPath);
}
});# 推送frida-server到设备
adb push frida-server-android /data/local/tmp/
adb shell chmod 755 /data/local/tmp/frida-server-android
adb shell /data/local/tmp/frida-server-android &# 查找目标进程
frida-ps -U
# 附加到目标应用
frida -U -f com.target.app -l fdex2.js --no-pause// auto_dump.js
setImmediate(function() {
Java.perform(function() {
var Application = Java.use("android.app.Application");
Application.attach.overload('android.content.Context').implementation = function(context) {
var result = this.attach(context);
// 在应用启动时执行脱壳
dumpAllDexFiles();
return result;
};
});
});// Hook DexClassLoader
var DexClassLoader = Java.use("dalvik.system.DexClassLoader");
DexClassLoader.loadClass.overload('java.lang.String').implementation = function(name) {
console.log("[*] Loading class: " + name);
dumpCurrentDex();
return this.loadClass(name);
};
// Hook BaseDexClassLoader
var BaseDexClassLoader = Java.use("dalvik.system.BaseDexClassLoader");
BaseDexClassLoader.findClass.overload('java.lang.String').implementation = function(name) {
var result = this.findClass(name);
var pathList = this.pathList.value;
// 通过pathList获取DEX文件
return result;
};-
Android版本兼容性
- Android 5.0-7.1 支持较好
- 部分Android 4.4设备可能需要调整
-
加固对抗
- 某些加固会检测Frida
- 可能需要反反调试技巧
-
权限要求
adb root # 需要root权限 adb remount -
多DEX处理
- 现代应用可能有多个DEX文件
- 需要遍历所有已加载的DEX
// 隐藏Frida特征
Java.perform(function() {
var System = Java.use("java.lang.System");
System.getProperty.overload('java.lang.String').implementation = function(key) {
if (key.contains("frida")) return null;
return this.getProperty(key);
};
});// 搜索内存中的DEX文件头(magic: dex\n035\0)
Memory.scan(ptr(0), Process.enumerateRanges('rw-')[0].size,
"6465780a30333500", {
onMatch: function(address, size) {
console.log("[+] Found DEX at: " + address);
dumpMemoryToFile(address, size);
}
});- DumpDex - 更新更活跃的脱壳工具
- Youpk - 针对抽取壳的脱壳工具
- FDex2(Xposed版) - 更稳定的Xposed实现
对于Android 8以下系统,Frida + fdex2组合是比较成熟的脱壳方案,但需要注意:
- 确保设备已root
- 选择合适版本的frida-server
- 根据目标应用的加固类型调整Hook点
- 及时保存脱出的DEX文件
需要针对特定应用分析时,可以先尝试通用脚本,再根据日志调整具体的Hook策略。
使用mCookie进行DEX脱壳是一种经典的基于Frida的动态脱壳方法。让我详细解释其原理和实现方式:
在Android的Dalvik虚拟机中:
- mCookie 是DexFile对象的内部标识符,指向已加载的DEX文件内存数据
- 每个DEX文件加载后都有一个对应的cookie值
- 通过cookie可以获取DEX文件的原始字节数据
Java.perform(function() {
// 方法1:Hook PathClassLoader
var PathClassLoader = Java.use("dalvik.system.PathClassLoader");
PathClassLoader.loadClass.overload('java.lang.String').implementation = function(name) {
console.log("[*] Loading class: " + name);
// 获取父类的pathList
var pathList = this.pathList.value;
if (pathList) {
// 获取dexElements数组
var dexElements = pathList.dexElements.value;
for (var i = 0; i < dexElements.length; i++) {
var dexFile = dexElements[i].dexFile.value;
var cookie = dexFile.mCookie.value;
dumpDexByCookie(cookie, "dex_" + i + ".dex");
}
}
return this.loadClass(name);
};
});function dumpAllDexByCookie() {
Java.perform(function() {
// 获取当前应用的ClassLoader
var currentApplication = Java.use("android.app.ActivityThread").currentApplication();
var classLoader = currentApplication.getClassLoader();
// 获取BaseDexClassLoader的pathList
var BaseDexClassLoader = Java.use("dalvik.system.BaseDexClassLoader");
var pathList = classLoader.pathList.value;
if (pathList) {
console.log("[*] Found pathList");
var dexElements = pathList.dexElements.value;
console.log("[*] Number of dexElements: " + dexElements.length);
for (var i = 0; i < dexElements.length; i++) {
try {
var element = dexElements[i];
if (element) {
var dexFile = element.dexFile.value;
if (dexFile) {
var cookie = dexFile.mCookie.value;
console.log("[*] DEX " + i + " cookie: " + cookie);
if (cookie) {
// Android 5.0+ mCookie是long类型,需要处理
dumpDexFromCookie(cookie, "classes" + (i > 0 ? i : "") + ".dex");
}
}
}
} catch (e) {
console.log("[!] Error processing element " + i + ": " + e);
}
}
}
});
}function dumpDexFromCookie(cookie, filename) {
// cookie在不同Android版本中的处理
var cookiePtr = null;
if (typeof cookie === 'number' || typeof cookie === 'long') {
// Android 5.0+:mCookie是指向指针数组的long值
var cookieArray = ptr(cookie);
var index = 0;
while (true) {
var dexPtr = cookieArray.add(index * Process.pointerSize).readPointer();
if (dexPtr.isNull()) {
break;
}
// 检查DEX魔数
var magic = dexPtr.readCString(8);
if (magic && magic.includes("dex")) {
console.log("[+] Found DEX at: " + dexPtr + ", magic: " + magic);
dumpMemoryToFile(dexPtr, getDexSize(dexPtr), filename + "_part" + index + ".dex");
}
index++;
}
} else if (cookie instanceof NativePointer) {
// 直接是指针的情况
dumpSingleDex(cookie, filename);
}
}
function dumpSingleDex(dexPtr, filename) {
// 读取DEX文件头
var dexSize = getDexSize(dexPtr);
if (dexSize > 0) {
console.log("[+] Dumping DEX: " + filename + ", size: " + dexSize + " bytes");
// 读取内存数据
var dexData = dexPtr.readByteArray(dexSize);
// 写入文件
var file = new File("/sdcard/" + filename, "wb");
file.write(dexData);
file.close();
console.log("[√] Dumped to /sdcard/" + filename);
}
}
function getDexSize(dexPtr) {
try {
// 从DEX文件头获取文件大小(偏移0x20)
var size = dexPtr.add(0x20).readU32();
// 验证是否为合理的DEX大小(1KB - 100MB)
if (size > 1024 && size < 100 * 1024 * 1024) {
return size;
}
} catch (e) {
console.log("[!] Error reading DEX size: " + e);
}
// 备用方法:扫描查找结束标志
return findDexEnd(dexPtr);
}
function findDexEnd(startPtr) {
var current = startPtr;
var maxSize = 50 * 1024 * 1024; // 最多50MB
// 尝试查找DEX结束特征或下一个DEX的开始
for (var offset = 0x70; offset < maxSize; offset += 4) {
var checkPtr = startPtr.add(offset);
try {
var magic = checkPtr.readCString(8);
if (magic && (magic.includes("dex") || magic.includes("odex"))) {
// 找到下一个DEX文件头,当前位置就是当前DEX的结束
return offset;
}
} catch (e) {
break;
}
}
return maxSize; // 返回最大尝试大小
}// mCookie_dumper.js
var dumpedFiles = new Set();
function dumpViaCookie() {
Java.perform(function() {
// Hook关键点:当DEX文件被打开时
var DexFile = Java.use("dalvik.system.DexFile");
// 方法1:Hook loadDex
DexFile.loadDex.overload('java.lang.String', 'java.lang.String', 'int').implementation = function(sourcePath, outputPath, flags) {
console.log("[*] loadDex called: " + sourcePath);
var result = this.loadDex(sourcePath, outputPath, flags);
dumpDexFromObject(result);
return result;
};
// 方法2:Hook DexFile构造函数
DexFile.$init.overload('java.lang.String', 'java.lang.String', 'int').implementation = function(sourceName, outputName, flags) {
console.log("[*] DexFile init: " + sourceName);
var result = this.$init(sourceName, outputName, flags);
dumpDexFromObject(this);
return result;
};
// 方法3:通过ClassLoader枚举(更彻底)
setTimeout(function() {
enumerateAllClassLoaders();
}, 3000);
});
}
function dumpDexFromObject(dexFileObj) {
try {
var cookie = dexFileObj.mCookie.value;
if (cookie) {
var timestamp = new Date().getTime();
var filename = "dex_cookie_" + timestamp + "_" + dumpedFiles.size + ".dex";
if (!dumpedFiles.has(cookie.toString())) {
dumpedFiles.add(cookie.toString());
dumpDexFromCookie(cookie, filename);
}
}
} catch (e) {
console.log("[!] Error dumping from object: " + e);
}
}
function enumerateAllClassLoaders() {
Java.enumerateClassLoaders({
onMatch: function(loader) {
console.log("[*] Enumerating class loader: " + loader);
try {
if (loader && loader.pathList) {
var dexElements = loader.pathList.dexElements.value;
if (dexElements) {
for (var i = 0; i < dexElements.length; i++) {
var dexFile = dexElements[i].dexFile.value;
if (dexFile) {
dumpDexFromObject(dexFile);
}
}
}
}
} catch (e) {
console.log("[!] Error enumerating: " + e);
}
},
onComplete: function() {
console.log("[√] Class loader enumeration complete");
}
});
}
// 启动脱壳
setImmediate(dumpViaCookie);# 1. 启动frida-server
adb shell /data/local/tmp/frida-server &
# 2. 附加到目标进程
frida -U -f com.target.app -l mCookie_dumper.js --no-pause
# 或附加到已运行的进程
frida -U com.target.app -l mCookie_dumper.jsfunction dumpDalvikCookie(cookie) {
// Dalvik中cookie直接是DEX的内存地址
var dexPtr = ptr(cookie);
dumpSingleDex(dexPtr, "dalvik.dex");
}function dumpARTCookie(cookie) {
// ART中cookie是指向DexFile指针数组的指针
var cookieArray = ptr(cookie);
var index = 0;
while (true) {
var dexFilePtr = cookieArray.add(index * Process.pointerSize).readPointer();
if (dexFilePtr.isNull()) break;
// ART DexFile结构更复杂,需要读取特定偏移
var beginPtr = dexFilePtr.add(Process.pointerSize * 2).readPointer();
var size = dexFilePtr.add(Process.pointerSize * 3).readU32();
if (size > 0) {
dumpMemoryToFile(beginPtr, size, "art_" + index + ".dex");
}
index++;
}
}- 去重机制
var dumpedCookies = new Map();
function shouldDump(cookie) {
var key = cookie.toString();
if (!dumpedCookies.has(key)) {
dumpedCookies.set(key, true);
return true;
}
return false;
}- 批量导出
function batchDump(dexFiles) {
dexFiles.forEach(function(dexFile, index) {
setTimeout(function() {
dumpDexFromObject(dexFile);
}, index * 100); // 避免同时大量IO操作
});
}- 实时监控
// 监控新DEX加载
Java.scheduleOnMainThread(function() {
setInterval(function() {
checkNewDexFiles();
}, 5000);
});- 权限问题:确保有sd卡写入权限
- 内存访问:访问无效指针可能导致崩溃
- 加固对抗:某些加固会清空mCookie或伪装
- 性能影响:大量DEX文件时注意性能
这种方法的优势是直接访问虚拟机内部数据结构,可以脱出内存中的原始DEX,包括动态加载的DEX文件。