前言
在Java中有一个叫做JNI的东西,它的全称是Java Native Interface,即Java本地接口,这是Java调用Native语言的一种特性(这里说的Native语言通常指C/C++)。有了JNI,Java就可以调用由C/C++编写的代码了。
要想还原so文件中的C/C++代码,就不能用反编译工具了,要使用反汇编工具,目前比较流行的就是IDA。但需要注意,完整还原C/C++代码几乎是不可能的,工具只能帮助我们还原出大概的执行逻辑。
IDA,又叫IDA Pro,英文全称是 Interactive Disassembler Professional,即交互式反汇编器专业版。
定位 so 文件
将 apk 复制一份,改后缀为 zip,然后解压缩,在 lib -> armeabi-v7a 中寻找 libbili.so 文件:
**注意:**还有下面这种load方式
因为是在 armeabi-v7a 中寻找的 so 文件,因此需要打开 32 位的 IDA:
IDA 加载 so 文件后页面如下:
补充:
lib文件夹下可能有多个文件夹,分别是 arm64-v8a、armeabi-v7a、x86 和 x86_64,so 文件可以运行在使用对应指令架构的设备上,这些设备分别如下:
- arm64-v8a:适配第 8 代、64 位 ARM 处理器,主要是 Android 真机。
- armeabi-v7a:适配第 7 代、32 位 ARM 处理器,主要是 Android 真机。
- x86:适配 x86 架构、32 位处理器,主要是模拟器或一些平板设备。
- x86_64:适配 x86 架构、64 位处理器,主要是模拟器或一些平板设备。
可以运行如下命令来查看自己的手机用的是那种处理器:
adb shell getprop ro.product.cpu.abi
这样当 App 运行时,就会加载执行对应指令架构文件夹下的so文件。在选择反汇编的so文件时就选择对应指令架构的文件夹。
但武佩奇说选择那个指令架构的文件夹都可以,这里迷惑?
优化显示
点击进入 jni_onload 方法,然后按 F5 ,跳转到方法的具体实现中:
看起来不是很直观,右键->点击 Hide casts,隐藏转换后代码可读性会好一点:
把 JNI_OnLoad 方法的第一个参数进行转化成VM类型,可读性会更好:
在 int 上右键->Convert to struct *,选择 JavaVM:
(只有JNI_OnLoad 方法的第一个参数是JavaVM,其他方法的第一个参数都是JNIEnv对象)
补充:如果可选类型中没有VM类型怎么办?
File -> Load file -> Parse C header file,选择 jni.h 即可:
输出so文件的名称
当无法找到是那个so文件时,如海南航空,如何办?
可以使用下列脚本:
如果是静态注册:
Java.perform(function () {
var dlsymadd = Module.findExportByName("libdl.so", 'dlsym');
Interceptor.attach(dlsymadd, {
onEnter: function (args) {
this.info = args[1];
}, onLeave: function (retval) {
//那个so文件 module.name
var module = Process.findModuleByAddress(retval);
if (module == null) {
return retval;
}
// native方法
var funcName = this.info.readCString();
if (funcName.indexOf("getHNASignature") !== -1) { // 需要修改
console.log(module.name);
console.log('\t', funcName);
}
return retval;
}
})
});
// Application(identifier="com.rytong.hnair", name="海南航空", pid=14958, parameters={})
// frida -U -f com.rytong.hnair -l static_find_so.js
如果是动态注册:
var symbols = Module.enumerateSymbolsSync("libart.so");
var addrRegisterNatives = null;
for (var i = 0; i < symbols.length; i++) {
var symbol = symbols[i];
if (symbol.name.indexOf("art") >= 0 &&
symbol.name.indexOf("JNI") >= 0 &&
symbol.name.indexOf("RegisterNatives") >= 0 &&
symbol.name.indexOf("CheckJNI") < 0) {
addrRegisterNatives = symbol.address;
console.log("RegisterNatives is at ", symbol.address, symbol.name);
}
}
console.log("addrRegisterNatives=", addrRegisterNatives);
if (addrRegisterNatives != null) {
Interceptor.attach(addrRegisterNatives, {
onEnter: function (args) {
var env = args[0];
var java_class = args[1];
var class_name = Java.vm.tryGetEnv().getClassName(java_class);
var taget_class = "com.xunmeng.pinduoduo.secure.DeviceNative"; // 需要修改
if (class_name === taget_class) {
console.log("\n[RegisterNatives] method_count:", args[3]);
var methods_ptr = ptr(args[2]);
var method_count = parseInt(args[3]);
for (var i = 0; i < method_count; i++) {
// Java中的函数名
var name_ptr = Memory.readPointer(methods_ptr.add(i * Process.pointerSize * 3));
// 参数和返回值类型
var sig_ptr = Memory.readPointer(methods_ptr.add(i * Process.pointerSize * 3 + Process.pointerSize));
// C中的函数指针
var fnPtr_ptr = Memory.readPointer(methods_ptr.add(i * Process.pointerSize * 3 + Process.pointerSize * 2));
var name = Memory.readCString(name_ptr); // 读取java中函数名
var sig = Memory.readCString(sig_ptr); // 参数和返回值类型
var find_module = Process.findModuleByAddress(fnPtr_ptr); // 根据C中函数指针获取模块
var offset = ptr(fnPtr_ptr).sub(find_module.base) // fnPtr_ptr - 模块基地址
// console.log("[RegisterNatives] java_class:", class_name);
console.log("name:", name, "sig:", sig, "module_name:", find_module.name, "offset:", offset);
}
}
}
});
}
// frida -U -f com.xunmeng.pinduoduo -l dynamic_find_so.js
拓展:
IDA还可以对so文件进行动态调试分析,在崔庆才的书中有介绍,但我看完后感觉没啥用。记录一下,以后用到时翻阅。
【PS:Android 逆向进阶专栏地址:https://blog.csdn.net/dafan0/category_12682484.html】