一、基础知识
Frida是全世界最好的Hook框架。在此我们详细记录各种各样常用的代码套路,它可以帮助逆向人员对指定的进程的so模块进行分析。它主要提供了功能简单的python接口和功能丰富的js接口,使得hook函数和修改so编程化,值得一提的是接口中包含了主控端与目标进程的交互接口,由此我们可以即时获取信息并随时进行修改。使用frida可以获取进程的信息(模块列表,线程列表,库导出函数),可以拦截指定函数和调用指定函数,可以注入代码,总而言之,使用frida我们可以对进程模块进行手术刀式剖析。 1.1 Frida安装需要安装Python Frida库以及对应手机架构的Frida server,Frida如果安装极慢或者失败,原因在于国内网络状况。 1.1.1 启动进程启动手机Frida server进程
adb shell
su
cd /data/local/tmp
chmod 777 frida-server
./frida-server
PS:/data/local/tmp是一个放置frida server的常见位置。
1.1.2 混合运行Frida以Python+Javascript混合脚本方式运行Frida(两种模式)。 // 以附加模式启动(Attach)
// 要求待测试App正在运行
run.py文件
// 导入frida库,sys系统库用于让脚本持续运行
import sys
import frida
# 找寻手机frida server
device = frida.get_usb_device()
# 选择应用进程(一般为包名)
appPackageName =""
# 附加
session = device.attach(appPackageName)
# 加载脚本,填入脚本路径
with open("script.js", encoding="utf-8")as f:
script = session.create_script(f.read())
script.load()
sys.stdin.read() //也可以不依赖sys库,使用time.sleep(10000000);
script.js文件
setImmediate(function() {
//prevent timeout
console.log(" Starting script");
Java.perform(function() {
// 具体逻辑
})
})
####################################################################################
// 启动新的进程(Spawn)
// 不要求待测试App正在运行,Frida会启动一个新的App进程并挂起
// 优点:因为是Frida启动的进程,在启动的同时注入frida代码,所以Hook的时机很早。
// 适用于在进程启动前的一些hook,如hook RegisterNative、较早进行的加解密等,注入完成后调用resume恢复进程。
// 缺点:会Hook到从App启动→想要分析的界面和逻辑的内容,干扰项多,且容易卡死。
run.py文件
import sys
import frida
# 找寻手机frida server
device = frida.get_usb_device()
# 选择应用进程(一般为包名)
appPackageName =""
# 启动新进程
pid = device.spawn([appPackageName])
device.resume(pid)
session = device.attach(pid)
# 加载脚本,填入脚本路径
with open("script.js", encoding="utf-8")as f:
script = session.create_script(f.read())
script.load()
sys.stdin.read()//也可以不依赖sys库,使用time.sleep(10000000);
script.js文件
setImmediate(function() {
//prevent timeout
console.log(" Starting script");
Java.perform(function() {
// 具体逻辑
})
})
PS:脚本的第一步总是通过get_usb_device用于寻找USB连接的手机设备,这是因为Frida是一个跨平台的Hook框架,它也可以Hook Windows、mac等PC设备,命令行输入frida-ls-devices可以展示当前环境所有可以插桩的设备,输入frida-ps展示当前PC所有进程(一个进程往往意味着一个应用),frida-ps -U即意味着展示usb所连接设备的进程信息。你可以通过Python+Js混合脚本的方式操作Frida,但其体验远没有命令行运行Frida Js脚本丝滑。 1.1.3 获取前端进程获取最前端Activity所在的进程,进程名。 // 可以省去填写包名的困扰
device = frida.get_usb_device()
front_app = device.get_frontmost_application()
print(front_app)
front_app_name = front_app.identifier
print(front_app_name)
输出1:Application(identifier="com.xxxx.xxx", name="xxxx", pid=xxxx)
输出2: com.xxxx.xxxx
1.1.4 命令行调用命令行方式使用: Spawn方式
frida -U --no-pause -f packageName -l scriptPath
Attach方式
frida -U --no-pause packageName -l scriptPath
输出内容太多时,可以将输出导出至文件
frida -U --no-pause -f packageName -l scriptPath -o savePath
可以自行查看所有的可选参数。
Hook梦幻旅途之Frida
通过CLI 进行hook有诸多优势,列举两个: 1) 当脚本出错时,会提供很好的错误提示;
Hook梦幻旅途之Frida
2)Frida进程注入后和原JS脚本保持同步,只需要修改原脚本并保存,进程就会自动使用修改后的脚本,这会让出错→修复,调试→修改调试目标 的过程更迅捷。 1.2 Frida In Java1.Frida hook 无重载Java方法; 2.Frida hook 有重载Java方法; 3.Frida hook Java方法的所有重载。 1.2.1 Hook导入导出表函数地址对So的Hook第一步就是找到对应的指针(内存地址),Frida提供了各式各样的API帮助我们完成这一工作。 获得一个存在于导出表的函数的地址: // 方法一
var so_name = "";
var function_name = "";
var this_addr = Module.findExportByName(so_name, function_name);
// 方法二
var so_name = "";
var function_name = "";
var this_addr = Module.getExportByName(so_name, function_name);
// 区别在于当找不到该函数时findExportByName返回null,而getExportByName抛出异常。
// 方法三
var so_name = "";
var function_name = "";
var this_addr = "";
var i = undefined;
var exports = Module.enumerateExportsSync(so_name);
for(i=0; i<exports.length; i++){
if(exports[i].name == function_name){
var this_addr = exports[i].address;
break;
}
}
1.2.2 枚举进程模块/导出函数枚举某个进程的所有模块/某个模块的所有导出函数。 Frida与IDA交互: 1.内存地址和IDA地址相互转换; function memAddress(memBase, idaBase, idaAddr) {
var offset = ptr(idaAddr).sub(idaBase);
var result = ptr(memBase).add(offset);
return result;
}
function idaAddress(memBase, idaBase, memAddr) {
var offset = ptr(memAddr).sub(memBase);
var result = ptr(idaBase).add(offset);
return result;
}
二、Hook JNI函数
JNI很多概念十分模糊,我们做如下定义,后续的阐述都依照此定义。 ·native:特指Java语言中的方法修饰符native。 ·Native方法:特指Java层中声明的、用native修饰的方法。 ·JNI实现方法:特指Native方法对应的JNI层的实现方法。 ·JNI函数:特指JNIEnv提供的函数。 ·Native函数:泛指C/C++层的本地库/自写函数等。 2.1 JNI编程模型如果对JNI以及NDK开发了解较少,务必阅读如下资料。(我不要你觉得,听我 的,下面都是精挑细选的。) ·《深入理解Android 卷1》——第二章:深入理解JNI 作者邓凡平 ·《Android的设计与实现 卷1》——第二章:框架基础JNI 作者杨云君 除此之外,你可能还会想了解一些其他的知识,我们回顾一下JNI编程模型。 步骤1:Java层声明Native方法。 步骤2:JNI层实现Java层声明的Native方法,在JNI层可以调用底层库/回调Java方法。这部分将被编译为动态库(SO文件)供系统加载。 步骤3:加载JNI层代码编译后生成的SO文件。 这其中有一个额外的关键点,SO文件的架构。 C/C++等Native语言直接运行在操作系统上,由CPU执行代码,所以编译后的文件既和操作系统有关,也和CPU相关。So是C/C++代码在Linux系统中编译后的文件,Window系统中为dll格式文件。 Android手机的CPU型号千千万,但CPU架构主要有七种,Mips,Mips64位,x86,x86_64,armeabi,armv7-a,armv8,编译时我们需要生成这七种架构的so文件以适配各种各样的手机。 2.2 armv7a架构成因在反编译过程中,我们需要选择某种CPU架构的so文件,得到特定架构的汇编代码。一般情况下我们选择armv7a架构,这涉及到一系列连环的原因。 2.2.1 通用情况七种架构可以简单分为Mips,X86,ARM三家,前两者的在Android处理器市场占比极小。Arm架构几乎成为了Android处理器的行业标准,IOS和Android都采用ARM架构处理器。 2.2.2 Apk臃肿考虑Apk的包体积对下载转化率、分发费直接挂钩,所以Apk一旦度过初创时期,就要考虑Apk的包体积优化,而So文件往往占据1/3-1/2的包体积,不提供市场占有率极小的Mips以及X86系列的So,可以瞬间解决Apk臃肿。 2.2.3 形势考虑形势比人强,ARM如日中天,无奈之下Mips和X86都设计了用于转换ARM汇编的中间层,即使Apk只提供了ARM的So库文件,这两种CPU架构的手机也可以以较慢速度运行APK。 2.2.4 ARM兼容性ARM有armeabi,armv7a,armv8a这三个系列,系列之间是不断发展和完善的升级关系。目前主流手机的CPU都是armv8a,即64位的ARM设备,而armeabi甚至只用在Android 4.0以下的手机,但好在Arm是向下兼容的,如果Apk不需要用到一些高性能的东西,完全可以只提供armeabi的So,这样几乎可以支持所有架构的手机。 2.3 Hook JNI函数通过上述的学习我们了解到,JNIEnv提供给了我们两百多个函数,帮助我们将Java中的对象和数据转换成C/C++的类型,帮助我们调用Java函数、帮助我们将C中生成的结果转换回Java中的对象和数据并返回,因此,如果能Hook JNI函数,会对我们逆向与分析So产生帮助。 使用Frida Hook Native函数十分简单,只需要我们提供地址即可。
Hook梦幻旅途之Frida
Frida提供了一种非常方便优雅的方式获得JNIEnv的地址,需要注意的是必须在Java.perform中调用。 var jnienv_addr = 0x0;
Java.perform(function(){
jnienv_addr = Java.vm.getEnv().handle.readPointer();
});
console.log("JNIEnv base adress get by Java.vm.getEnv().handle.readPointer():" + jnienv_addr);
JNIEnv指针指向JNINativeInterface这个数组,里面包含两百多个指针,即各种各样的JNI函数。 我们可以查看一下Jni.h头文件
Hook梦幻旅途之Frida
假设JNIEnv地址为0x1000,一个指针长4,那么reversed0地址即为0x1000,reversed1为0x1004,之后我们读取这个指针,就可以得到JNI函数的地址,从而实现Hook。 在我们上述的JNINativeInterface数组中,它排在第七个,那么偏移就是4*(7-1)=24。 function hook_native_findclass() {
var jnienv_addr = Java.vm.getEnv().handle.readPointer();
var FindClassPtr = Memory.readPointer(jnienv_addr.add(24));
// 注意,Frida提供了add(+),sub(-)等函数供我们做加减乘除,你也可以通过add(0x12)这种形式加一个十六进制数。
console.log("FindClassPtr addr: " + FindClassPtr);
Interceptor.attach(FindClassPtr, {
onEnter: function (args) {
...
}
});
}
接下来我们以IDA为例,加深理解。在我们使用IDA逆向和分析SO时,如果单纯导入SO,会有大量“无法识别”的函数。
Hook梦幻旅途之Frida
所以惯例上,我们会导入Jni.h头文件,再设置方法的第一个参数为JNIEnv类型,这样IDA就能顺利将形如*(a1+xxx)这种指针识别为JNI函数 ,但可能很多人没有想过为什么这样可以成功。
Hook梦幻旅途之Frida
事实上,导入Jni.h头文件是为了引入JNINativeInterface与JNIInvokeInterface结构体信息,而转换参数一为JNIEnv类型,就是在提醒IDA,将*(env+704)映射成对应的JNIEnv函数。 而我们现在所做的是一种相反的操作,已知各个JNI函数的名字和他们在数组中的位置,希望得到其地址。 不知道大家是否发现,由于JNI实现方法的第一个参数总是JNIEnv,所以我们也可以通过Hook一个JNI实现方法作为跳板,从而获得JNIEnv的地址。 function hook_jni(){
var so_name = ""; // 请选择目标Apk SO
var function_name = ""; //请选择目标SO中一个JNI实现方法
var open_addr = Module.findExportByName(so_name, function_name);
Interceptor.attach(open_addr, {
onEnter: function (args) {
var jnienv_addr = 0x0;
console.log("get by args[0].readPointer():" + args[0].readPointer());
Java.perform(function () {
jnienv_addr = Java.vm.getEnv().handle.readPointer();
});
console.log("get by Java.vm.getEnv().handle.readPointer():" + jnienv_addr);
},
onLeave: function (retval) {
}
});
}
hook_jni();
结果完全正确,但这种方法流程明显更加复杂,不够优雅,不建议使用。
Hook梦幻旅途之Frida
好了,我们回归到主线上来,上面我们Hook了FindClass这个函数,想一下我们Hook一个JNI函数需要做的工作,一是找到这个函数对应的偏移,二是在onEnter和onLeave中编写具体的逻辑,因为每个JNI函数的参数和返回值都不一样。 有没有办法简化这两个步骤呢?比如只需要输入JNI函数名,而不需要手动计算偏移?这个好办,我们看一下代码。 var jni_struct_array = [
"reserved0",
"reserved1",
"reserved2",
"reserved3",
"GetVersion",
"DefineClass",
"FindClass",
*******此处省略两百多个JNI函数**********
"FromReflectedMethod",
"FromReflectedField",
"ExceptionCheck",
"NewDirectByteBuffer",
"GetDirectBufferAddress",
"GetDirectBufferCapacity",
"GetObjectRefType",
]
function getJNIFunctionAdress(jnienv_addr,func_name){
var offset = jni_struct_array.indexOf(func_name) * 4;
return Memory.readPointer(jnienv_addr.add(offset))
}
代码很简单,将JNI函数罗列在数组中,通过Js中indexOf这个数组处理函数得到目标数组的索引,乘4就是偏移了,除此之外,你可以选择乘Process.pointerSize,这是Frida提供给我们的Api,返回当前平台指针所占用的内存大小,这样做可以增加脚本的移植性(其实没啥区别)。 我们进一步希望,能不能不用在onEnter和onLeave中编写具体的逻辑,反正JNI函数的参数和返回值类型都在Jni.h中定义好了,也不会有什么更多的变化了。 需要注意的是,它在理论上实现了Hook 所有JNI函数,并提供了人性化的筛选等功能,但在我的测试机上并没有很顺利或者正确的打印出全部JNI调用,更多精彩需要读者自己去挖掘喽。 三、Hook动态注册函数
在第二部分我们将尝试Hook JNIEnv提供的RegisterNatives函数,在上面我们已经讲过JNI函数的Hook,为什么要花同样的篇幅去讲解呢?当然是因为这个函数比较常用,而且可以给分析带来很大帮助。 3.1 反编译so文件在逆向时,静态注册的函数只需要找到对应的So,函数导出表中搜索即可定位。而动态注册的函数会复杂一些,下面列一下流程。 1.在导出函数中搜索JNI_OnLoad,点击进入。
Hook梦幻旅途之Frida
2.Tab或者f5键反汇编arm指令。
Hook梦幻旅途之Frida
Hook梦幻旅途之Frida
3.之前我们已经知道,凡是*(指针变量+xxx)这种形式都是在使用JNI函数,所以导入Jni.h头文件,在a1,v5,v2等变量上右键如图。
Hook梦幻旅途之Frida
Hook梦幻旅途之Frida
Hook梦幻旅途之Frida
这个时候JNI函数都正确展示出来,如果大家反编译的是自己的Apk,对照着看源码和反汇编代码,仍然会感觉“不太舒服”,我们还有一些额外的工作可以做。 4.IDA由于不确定参数的数目,常常会不显示函数的参数,用如下的方式强制展示参数(findclass显然不可能无参)。
Hook梦幻旅途之Frida
在几个jni函数上都试一下,结果如下,需要注意的是,自己写的App可能不会有这些问题。
Hook梦幻旅途之Frida
5.接下来我们隐藏掉类型转换,这样代码会更加可读。
Hook梦幻旅途之Frida
反编译的工作顺利完成了,接下来找动态注册的函数。 3.2 寻找关键函数看一下RegisterNatives这个函数的原型。 jint RegisterNatives(JNIEnv *env,jclass clazz, const JNINativeMethod *methods, jnint nMethods);
第一个参数是JNIEnv指针,所有的JNI函数第一个参数都是它。 第二个参数jclasss是类对象,通过 JNI FindClass函数得来。 第三个参数是一个数组,数组中包含了若干个结构体,每个结构体存储了Java Native方法到JNI实现方法的映射关系。 第四个参数代表了数组中结构体的数量,或者可以说此次动态注册了多少个native方法。 我们仔细品一下这个结构体,内容为Java层方法名+签名+JNI层对应的函数指针,Java层方法名并不携带包的路径,包的信息由第二个参数,也就是jclass类对象提供。签名的写法和Smali语法类似,想必大家不陌生。JNI层对应的函数指针也似乎没啥问题。 接下来我们阅读一下截图中的RegisterNatives函数,v3即类对象,“com/m4399/……”即Java native函数所声明的类,第四个参数为16,即off_20044这个数组中有十六个结构体,或者说十六组java native函数与jni实现函数的映射。 我想你应该不会对off_20044这个命名感到恐慌,这是IDA生成的假名字,详细内容见下表。off_20044即代表了这是一个数据,位于20044这个偏移位置,我们双击进去试试。
Hook梦幻旅途之Frida
data:00020044证实了我们的想法,可以发现,IDA反汇编的效果还不错,我们从上往下划分,每三行代表一个完整的映射。只要两个地方让人不太舒服。 1.第一个结构体为什么占那么多行? 这是因为作为内容的起始部分,IDA会在右方用注释的方式展示它的交叉引用状况,交叉引用占用了正常的两行,JNI_Onload+46 以及.textL0ff_14C10这两个位置引用了这份数据,正是交叉引用的注释导致第一个结构体,或者说第一行下面平白空了两行。我们可以在off_20044上按快捷键x查看其交叉引用,验证我们的观点。
Hook梦幻旅途之Frida
2.我们之前说过,每个结构体里三块内容,Java层方法名+签名+JNI层对应的函数指针,而IDA结果正确吗?aGetmd5并不像方法名,aLjavaLangStrin_0也不像正确的签名,第三个sub_xxx,根据我们上表,它代表了一个函数的起点,这倒是和“JNI层对应的函数指针”不谋而合。可是方法名和签名是怎么回事? 这是因为IDA给方法名以及签名二次取了名字。 #原代码
a = 3
#IDA反编译后
a1 = 3 #a
a = a1
IDA用注释的形式给出了真正的值,因此我们可以直接看右边注释,这结果明显就正确了,除此之外,IDA在命名时会参考原值,因此才会有aLjavaLangStrin_0这种似是而非的名字。 3.3 应用的场景至此,我们已经搞懂了动态注册,也称函数注册的定位,那么为什么还需要用Hook registernative函数呢?直接用IDA查看一下不就得了? 有多方面的考虑,考虑一下这两个情景 ·找不到某个Native声明的Java函数是哪个SO加载来的。 ·IDA反编译时遇到了防护,JNI_Onload无法顺利反编译(常见)。 这个时候Hook动态注册函数就能一把尖刀,直刺So中函数所在的位置。为了理解上更通顺,我们不考虑一步到位,而是一步步去优化Hook代码,希望对大家有所帮助。 var RevealNativeMethods = function() {
// 为了可移植性,选择使用Frida 提供的Process.pointerSize来计算指针所占用内存,也可以直接var pSize = 4
var pSize = Process.pointerSize;
// 获取当前线程的JNIEnv
var env = Java.vm.getEnv();
// 我们所需要Hook的函数是在JNIEnv指针数组的第215位,因为我们这里只是Hook单个函数,所以没有引入包含全体JNI函数的数组
var RegisterNatives = 215;
// 将通过位置计算函数地址这一步骤封装为函数
function getNativeAddress(idx) {
var nativrAddress = env.handle.readPointer().add(idx * pSize).readPointer();
console.log("nativrAddress:"+nativrAddress);
return nativrAddress;
}
// 开始Hook
Interceptor.attach(getNativeAddress(RegisterNatives), {
onEnter: function(args) {
console.log("Already enter getNativeAddress Function!");
// 遍历数组中每一个结构体,需要注意的是,参数4即代表了结构体数量,我们这里使用了它
for (var i = 0, nMethods = parseInt(args[3]); i < nMethods; i++) {
var methodsPtr = ptr(args[2]);
var structSize = pSize * 3;
var methodName = methodsPtr.add(i * structSize).readPointer();
var signature = methodsPtr.add(i * structSize + pSize).readPointer();
var fnPtr = methodsPtr.add(i * structSize + (pSize * 2)).readPointer();
/*
typedef struct {
const char* name;
const char* signature;
void* fnPtr;
} JNINativeMethod;
*/
var ret = {
// methodName与signature都是字符串,readCString和readUtf8String是Frida提供的两个字符串解析函数,
// 前者会先尝试用utf8的方式,不行再打印unicode编码,因此相比readUtf8String是更保险和优雅的选择
methodName:methodName.readCString(),
signature:signature.readCString(),
address:fnPtr,
};
// 使用JSON.stringfy()打印内容通常是好的选择
console.log(JSON.stringify(ret))
}
}
});
};
Java.perform(RevealNativeMethods);
由于registerNatives发生的时机往往很早,建议采用Spawn方式注入,否则可能毫无收获。
Hook梦幻旅途之Frida
3.3.1 代码优化似乎很不错的样子,但是自己看一下内容,却不大如人意。 Hook输出了Java方法名,但我们之前说过,Java层方法名并不携带包的路径,包的信息由第二个参数,所以方法名提供不了什么信息,第二个信息是参数签名,和我们预期一致,第三个信息是函数地址,有一个很大的问题,输出的地址是内存中的真正地址,而我们分析SO时需要用到IDA,IDA 加载模块的时候,会以基址 0 加载分析 so 模块,但是 SO运行在 Android 上的时候,每次的加载地址不是固定的,有没有办法解决这个问题呢? 办法是很多的,我们查看Frida官方文档可以发现,Frida提供了两个根据地址得到所在SO文件等信息的函数。 我们对照一下结果,修改代码输出如下: var ret = {
// methodName与signature都是字符串,readCString和readUtf8String是Frida提供的两个字符串解析函数,
// 前者会先尝试用utf8的方式,不行再打印unicode编码,因此相比readUtf8String是更保险和优雅的选择
// 只需要新增如下两行代码
module1: DebugSymbol.fromAddress(fnPtr),
module2: Process.findModuleByAddress(fnPtr),
methodName:methodName.readCString(),
signature:signature.readCString(),
address:fnPtr,
};
查看任意一条输出结果,此Native方法名为tokenDecrypt
{"module1":{"address":"0x8a339267","name":"0x17267","moduleName":"libm4399.so","fileName":"","lineNumber":0},
"module2":{"name":"libm4399.so","base":"0x8a322000","size":135168,"path":"/data/app/com.m4399.gamecenter-1/lib/arm/libm4399.so"},
"methodName":"tokenDecrypt",
"signature":"(Ljava/lang/String;)Ljava/lang/String;",
"address":"0x8a339267"}
可以发现,两个API侧重点不同,地址为0x8a339267,函数1返回自身地址,符号名(0x17267),所属SO名,具体文件名和行数(这两个字段似乎无效),符号名name可能有些不理解,我们待会儿再讲。函数2返回所属SO,base字段,即为基址,表示此SO在内存中起始的位置,size字段代表了SO的大小,path即为SO在手机中的真实路径。
Hook梦幻旅途之Frida
图中可以看出,如果想得到IDA中的虚拟地址,两个函数都可以做到。使用函数一的name字段,或者address减去函数二提供给我们的So基址。我们先通过IDA来验证tokenDecrypt这个函数结果是否准确。0x17266+1即0x17267,name字段被验证。0x8a339267-0x8a322000=0x17267,两种方法都OK。
Hook梦幻旅途之Frida
通过Frida提供的Api,我们得到了地址对应的SO文件以及它在IDA中的位置,这真是可喜的事儿。除此之外,我们补充另外一种方式来定义地址,即修改IDA中SO的基址。
Hook梦幻旅途之Frida
Hook梦幻旅途之Frida
效果如下:
Hook梦幻旅途之Frida
在我们这个场景下,这样处理并不方便, 但在IDA动态调试时,通过Rebease 基址,让其与运行时 so 的基址相同,可以极大的方便静态分析。 需要注意的是,我们使用此Hook脚本时,目的不是印证IDA中反编译的地址和Frida hook得到的地址是否相同,而是为了定位。IDA中使用快捷键G可以迅速进行地址跳转。 接下来我们需要进一步优化脚本,参数2是jclass对象,可以让我们获得这个方法所在类的信息,它是JNI方法Findclass的结果,因此我们要Hook 这个JNI方法。Findclass的结果需要和对应的RegisterNative函数匹配,这涉及到JNIEnv线程的问题,我们使用集合的方式处理。来看一下完整的代码吧。 var RevealNativeMethods = function() {
// 为了移植性,选择使用Frida API来计算指针所占用内存,也可以直接var pSize = 4
var pSize = Process.pointerSize;
// 获取当前线程的JNIEnv
var env = Java.vm.getEnv();
// 我们所需要Hook的函数是在JNIEnv指针数组的第6和第215位
var RegisterNatives = 215;
var FindClassIndex = 6;
// 将通过位置计算函数地址这一步骤封装为函数
function getNativeAddress(idx) {
var nativrAddress = env.handle.readPointer().add(idx * pSize).readPointer();
return nativrAddress;
}
// 初始化集合,用于处理两个JNI函数之间的同步关系
var jclassAddress2NameMap = {};
// Hook 两个JNI函数
Interceptor.attach(getNativeAddress(FindClassIndex), {
onEnter: function (args) {
// 设置一个集合,不同的JNIEnv线程对应不同的class
jclassAddress2NameMap[args[0]] = args[1].readCString();
}
});
Interceptor.attach(getNativeAddress(RegisterNatives), {
onEnter: function(args) {
console.log("Already enter getNativeAddress Function!");
// 遍历数组中每一个结构体,需要注意的是,参数4即代表了结构体数量,我们这里使用了它
for (var i = 0, nMethods = parseInt(args[3]); i < nMethods; i++) {
var methodsPtr = ptr(args[2]);
var structSize = pSize * 3;
var methodName = methodsPtr.add(i * structSize).readPointer();
var signature = methodsPtr.add(i * structSize + pSize).readPointer();
var fnPtr = methodsPtr.add(i * structSize + (pSize * 2)).readPointer();
/*
typedef struct {
const char* name;
const char* signature;
void* fnPtr;
} JNINativeMethod;
*/
var ret = {
// methodName与signature都是字符串,readCString和readUtf8String是Frida提供的两个字符串解析函数,
// 前者会先尝试用utf8的方式,不行再打印unicode编码,因此相比readUtf8String是更保险和优雅的选择
moduleName: DebugSymbol.fromAddress(fnPtr)["moduleName"],
jClass:jclassAddress2NameMap[args[0]],
methodName:methodName.readCString(),
signature:signature.readCString(),
address:fnPtr,
IdaAddress: DebugSymbol.fromAddress(fnPtr)["name"],
};
// 使用JSON.stringfy()打印内容通常是好的选择
console.log(JSON.stringify(ret))
}
}
});
};
Java.perform(RevealNativeMethods);
|