下载安装和简要说明

下载:

1
pip install frida-tools

下载 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
let ObjectName = ClassName.$new() //实例化类,有静态修饰的不用做这一步
//new里面可加参数
//如实例化一个字符串类
const stringClass = Java.use("java.lang.String")
let res = ObejectName.MethodName( stringClass.$new('123') ) //执行方法且实例化并得到结果

//base64
const b64Class = Java.use("android.util.Base64")
console.log( b64Class.encodeToString(res,0) ) //打印base64编码res信息到调试窗
})

总结:

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,...)
{
//your aim...
}

//同种写法
ClassName["FuncName"].implementation = function(p1,p2,...)
{
//your aim...
}

也是需要和上面的一样,先构造类常量,然后实例化;

此写法是直接覆盖原函数的内容,不会执行原本函数的内容,所以要规定好调用约定,返回对应的内容;

在改写函数中使用:

1
this.FuncName(...)   #同款写法: this["FuncName"](...)

可以实现调用此类的原函数;

如果函数有重载,则写法如下:

1
2
3
4
ClassName.FuncName.overload('').implementation = function(p1,p2,...)
{
//your aim...
}

单引号里是参数的类型,如果是基本类型,则表示法为: [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
//此函数接收一个指针,一般指向hook函数
Interceptor.attach( Module.findExportByName('your_so.so','FuncName'), //根据so文件名和函数名通过Export找
//第二个参数是回调函数
{
//函数开始时
onEnter(args){
let size = args[2].add(0x4).readu32() //args[2]也就是函数的第三个参数,加4得到结构体地址第二个字段地址
//将内容当作指针读取,再读取真实的结构体数据
let data = args[2].readPointer().readByteArray(size)

console.log(data) //打印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
//因为hook java层,所以用java.use,上述目标函数在runtime里
const runtimeClass = Java.use('java.lang.Runtime')
//hook目标函数
runtimeClass.nativeLoad.overload('java.lang.String','java.lang.ClassLoader','java.lang.Class').implementation = function(p1,p2,p3)
{
//调用一次原函数,使得so文件被装载
let res = this.nativeLoad(p1,p2,p3)
//查找第一个参数里是否带目标so文件的路径字符串
if(p1.indexOf('your_so.so') != -1)
{
//hook so层实现
}
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')
//hook java层加载so的函数
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)
{
//hook so层函数
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;