目前通过frida脚本对android进行脱壳获取dex文件是目前的主流手段。本次对frida_dump脱壳脚本原理分析是为了加深脱壳脚本如何工作的。知己知彼,百战不殆!
脚本分析
整体分析
frida_dump一共有5个函数,自上至下分别是
1. get_self_process_name
2. mkdir
3. chmod
4. dump_dex
5. hook_dlopen
其中关于脱壳的核心函数是dump_dex。
核心函数分析
function dump_dex() {
var libart = Process.findModuleByName("libart.so");
var addr_DefineClass = null;
var symbols = libart.enumerateSymbols();
for (var index = 0; index = 0 &&
symbol_name.indexOf("DefineClass") >= 0 &&
symbol_name.indexOf("Thread") >= 0 &&
symbol_name.indexOf("DexFile") >= 0) {
console.log(symbol_name, symbol.address);
addr_DefineClass = symbol.address;
}
}
var dex_maps = {};
var dex_count = 1;
console.log("[DefineClass:]", addr_DefineClass);
if (addr_DefineClass) {
Interceptor.attach(addr_DefineClass, {
onEnter: function(args) {
var dex_file = args[5];
//ptr(dex_file).add(Process.pointerSize) is "const uint8_t* const begin_;"
//ptr(dex_file).add(Process.pointerSize + Process.pointerSize) is "const size_t size_;"
var base = ptr(dex_file).add(Process.pointerSize).readPointer();
var size = ptr(dex_file).add(Process.pointerSize + Process.pointerSize).readUInt();
if (dex_maps[base] == undefined) {
dex_maps[base] = size;
var magic = ptr(base).readCString();
if (magic.indexOf("dex") == 0) {
var process_name = get_self_process_name();
if (process_name != "-1") {
var dex_dir_path = "/data/data/" + process_name + "/files/dump_dex_" + process_name;
mkdir(dex_dir_path);
var dex_path = dex_dir_path + "/class" + (dex_count == 1 ? "" : dex_count) + ".dex";
console.log("[find dex]:", dex_path);
var fd = new File(dex_path, "wb");
if (fd && fd != null) {
dex_count++;
var dex_buffer = ptr(base).readByteArray(size);
fd.write(dex_buffer);
fd.flush();
fd.close();
console.log("[dump dex]:", dex_path);
}
}
}
}
},
onLeave: function(retval) {}
});
}
}
从函数第一行开始分析可知,首先获取libart.so的模块基址,然后通过frida提供的api(enumerateSymbols)来遍历模块的导出函数。
从里面得到DefineClass的函数地址,从导出名称看出libart.so在编译导出函数时没有按照C风格也是extern “C”对函数名称进行修饰。
导致导出函数名称不规则,通过对DefineClass函数名称中的特征字符来来过滤DefineClass的函数地址。
获取到DefineClass函数地址后,用frida提供的Interceptor来附加在函数地址上,(通过frida提供的注释看,Interceptor附加采用的是inline hook的方式)
为什么要hook DefineClass呢?因为DefinClass的第6个参数(第一个参数是this指针)是dexFile对象的引用。
//DefineClass函数声明,是一个native函数
mirror::Class* ClassLinker::DefineClass(Thread* self,
const char* descriptor,
size_t hash,
Handle class_loader,
const DexFile& dex_file,
const DexFile::ClassDef& dex_class_def);
DexFile::DexFile(const uint8_t* base, //dex文件基址
size_t size, // dex文件长度
const uint8_t* data_begin,
size_t data_size,
const std::string& location,
uint32_t location_checksum,
const OatDexFile* oat_dex_file,
std::unique_ptr container,
bool is_compact_dex)
dexFile是dex文件加载到内存后的对象。该对象是包含了dex在内存中的基址和长度。有了这些信息就可以把dex文件dump出来了
脚本会将DefineClass加载的每一个dex文件dump到当前包名的files目录下。并按照顺序对dex文件命名"class1 class2"。
myfridadump.js
myfridadump.js是我对照dump_dex.js自己实现的dump dex的脚本,其核心一样是在DefineClass hook拿到dexFile后dump dex文件到手机上。
区别是删除了原脚本的无用函数hook_dlopen,增加了更多的注释,帮助理解,本来想通过chrome的devtools对frida的js脚本进行单步调试,
这是会对脚本执行过程的理解更清晰,无奈折腾半天附加不上js脚本后放弃,采用日志输出的方式来看执行流程。
【myfridadump.js github地址】
总结
通过这次对frida_dump的脚本进行分析,对脱壳脚本执行更加清晰,增加了对frida脚本编写的理解