Hook梦幻旅途之Frida

  • A+
所属分类:逆向工程

这是 酒仙桥六号部队 的第 75 篇文章。

全文共计8297个字,预计阅读时长25分钟。


一、基础知识

Frida是全世界最好的Hook框架。在此我们详细记录各种各样常用的代码套路,它可以帮助逆向人员对指定的进程的so模块进行分析。它主要提供了功能简单的python接口和功能丰富的js接口,使得hook函数和修改so编程化,值得一提的是接口中包含了主控端与目标进程的交互接口,由此我们可以即时获取信息并随时进行修改。使用frida可以获取进程的信息(模块列表,线程列表,库导出函数),可以拦截指定函数和调用指定函数,可以注入代码,总而言之,使用frida我们可以对进程模块进行手术刀式剖析。

1.1 Frida安装

需要安装Python Frida库以及对应手机架构的Frida server,Frida如果安装极慢或者失败,原因在于国内网络状况。

1.1.1 启动进程

启动手机Frida server进程
adb shell
sucd /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 sysimport frida# 找寻手机frida serverdevice = 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 sysimport frida# 找寻手机frida serverdevice = 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.identifierprint(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 scriptPathAttach方式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 Java

1.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 #aa = 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);


Hook梦幻旅途之Frida


Hook梦幻旅途之Frida

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: