下载安装和简要说明
下载:
下载 github 上的 frida server,类似ida的server;
Releases · frida/frida (github.com)
需要下载的名称为:frida-server-16.0.10-android-arm64.xz;(模拟器下载x64版本)
之后熟悉adb的使用:(需要root手机,模拟器也行但比较麻烦)
adb下载安装及使用_Dongs丶的博客-CSDN博客_adb下载
需要用到的指令:
1 2 3 4 5 6 7 8
| adb devices //查看是否连接到手机设备 adb shell //进入手机shell adb push A/path B/path //将电脑上A路径的文件传到手机B路径上 一般B是 /sdcard --- 进入shell后操作: su //sudo给权限 mv A/path B/path //转移(剪切)文件位置A到B,一般放到 /data/local chmod 777 file //赋全权限
|
使用命令以激活server:
1 2 3 4
| ./frida_server64 --- 电脑本机操作: adb forward tcp:27042 tcp:27042 //转发手机端口到电脑
|
查看APP包名:
1
| adb shell pm list packages -3
|
MuMu模拟器特殊说法:
1
| adb connect 127.0.0.1:7555 //以usb连接到模拟器
|
指定设备转发端口:
1
| adb -s ID forward tcp:xxx tcp:xxx
|
frida常用参数
执行命令: frida-ps, 展示进程应用;
1 2 3
| -f //启动app -R //remote -l //load JS 脚本
|
如加载一个app并装载脚本:
1
| frida -R -f app包名 -l 脚本.js
|
Frida实现调用函数
用Jeb找到包名,类名,以及函数名及其调用约定;
编写JS执行:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| Java.perform(function() { const ClassName = Java.use("PackageName.ClassName") let ObjectName = ClassName.$new() const stringClass = Java.use("java.lang.String") let res = ObejectName.MethodName( stringClass.$new('123') ) const b64Class = Java.use("android.util.Base64") console.log( b64Class.encodeToString(res,0) ) })
|
总结:
1 2 3 4 5 6 7 8 9 10 11
| Java.perform(function(){}) ---
Java.use("PakageName.ClassName") ---
ClassName.$new() ---
console.log()
|
Frida简单实现hook-java层
不管是否静态,可以如下书写:
1 2 3 4 5 6 7 8 9 10
| ClassName.FuncName.implementation = function(p1,p2,...) { }
ClassName["FuncName"].implementation = function(p1,p2,...) { }
|
也是需要和上面的一样,先构造类常量,然后实例化;
此写法是直接覆盖原函数的内容,不会执行原本函数的内容,所以要规定好调用约定,返回对应的内容;
在改写函数中使用:
1
| this.FuncName(...) #同款写法: this["FuncName"](...)
|
可以实现调用此类的原函数;
如果函数有重载,则写法如下:
1 2 3 4
| ClassName.FuncName.overload('').implementation = function(p1,p2,...) { }
|
单引号里是参数的类型,如果是基本类型,则表示法为: [B
表示为byte;
如果是类类型,则直接输入其对应包的对应类名就行,如String类型: java.lang.String
;
一般而言,对于java常用类的函数hook,要有过滤,即对应地方调用的参数特点做出if判断并更改代码逻辑;
因为很多地方也会调用到相同的函数,此时就直接返回 this.FuncName() ,就不会使得程序崩溃;
举例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| Java.perform(function() { function hookMainA() { let MainActivity = Java.use("com.example.challenge.MainActivity"); MainActivity["a"].implementation = function (bArr) { console.log(`MainActivity.a is called: bArr=${bArr}`); let result = this["a"](bArr); console.log(`MainActivity.a result=${result}`); return result; }; } hookMainA(); })
|
重载举例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| Java.perform(function() { function hookMainA() { let MainActivity = Java.use("com.example.challenge.MainActivity"); MainActivity["b"].overload('java.lang.String','[B').implementation = function (bArr,byte) { console.log(`MainActivity.a is called: bArr=${bArr}`); let result = this["a"](bArr); console.log(`MainActivity.a result=${result}`); return result; }; } hookMainA(); })
|
Hook so层
hook so层要注意一点,要当so文件动态链接到应用后,才能调用其函数,不然会空指针报错,这和pwn的ret2libc一个道理;
hook so一般有两种方法,这里先介绍第一种,导出表 Export,找到导出的地址;
操作so层时不用 java.use ,用拦截器: Interceptor;
这里用获取一个函数传参结构体的打印代码解释:
假设原代码:
1
| a = AimFunc(a1,a2,&v16,...);
|
此时第三个参数便是此时需要获取的结构体地址;
在32位程序中,指针占4字节,而结构体的地址过去第一个字段是真实的结构体数据的指针,隔了4个字节之后的,是这个结构体的大小;
那么对应hook代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| Interceptor.attach( Module.findExportByName('your_so.so','FuncName'), { onEnter(args){ let size = args[2].add(0x4).readu32() let data = args[2].readPointer().readByteArray(size) console.log(data) }, onLeave(retval){ } } )
|
此时直接跑是会报错的,因为正如前面所述,在程序开始运行的时候就直接去寻找模块了,但是因为so还没有动态地链接,所以找不到,会得到一个null指针;
此时的解决思路是:找到加载此so文件的函数,先去hook掉java层的 LoadLibrary,再对比此时传入 LoadLibrary 参数是否是目标so文件,如果是,则再实现上面的代码,如果不是,则实现原函数代码内容;
通过java层的源码分析可以知道:LoadLibrary 的实现是 调用了一个 LoadLibrary0(a1,a2,a3)去实现主要代码的,而主要代码的逻辑是:如果传入的字符串是存在的so文件,那么就会直接在其中调用 nativeLoad 函数;
而 nativeload 函数也会层层调用,最后会找到一个三参的 nativeload,原型如下:
1
| private static native String nativeLoad(String filename, ClassLoader loader, Class<?> caller);
|
此时需要hook的函数,也就是这上面这个了;
主要代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| const runtimeClass = Java.use('java.lang.Runtime')
runtimeClass.nativeLoad.overload('java.lang.String','java.lang.ClassLoader','java.lang.Class').implementation = function(p1,p2,p3) { let res = this.nativeLoad(p1,p2,p3) if(p1.indexOf('your_so.so') != -1) { } return res }
|
如果想要将想要的data数据dump出,可以用以下代码(frida提供):
1 2 3 4 5
| let file = new File('/sdcard/' + size + '.bin', 'wb') file.write(data) file.flush() file.close()
|
完整代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| Java.perform(function(){ const runtimeClass = Java.use('java.lang.Runtime')
runtimeClass.nativeLoad.overload('java.lang.String','java.lang.ClassLoader','java.lang.Class').implementation = function(p1,p2,p3) { let res = this.nativeLoad(p1,p2,p3) if(p1.indexOf('your_so.so') != -1) { Interceptor.attach( Module.findExportByName('your_so.so','FuncName'), { onEnter(args){ let size = args[2].add(0x4).readu32() let data = args[2].readPointer().readByteArray(size) let file = new File('/sdcard/' + size + '.bin', 'wb') file.write(data) file.flush() file.close() }, onLeave(retval){ } } ) } return res } })
|
第二种找到so内目标函数的方法是,没有export的情况,需要用偏移量来进行寻址,在attach函数调用的那一步,改写为:
1 2
| const so = Process.findModuleByName('your_so.so') Interceptor.attach( so.add(offset) {})
|
在IDA逆so的时候,设置segement为0,可以定位函数的地址直接为偏移量;
arm汇编中,函数传参使用r0~r4;