frida安装

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
pip install frida
pip install frida-tools(frida-tools里有frida)
frida --version

版本对应
frida12.3.6 Android5-6 python3.7
frida12.8.0 Android7-8 python3.8
frida14+ Android9+ python3.8

安装指定版本frida(虚拟环境中可以安装最新的,不一定能装指定版本的)
先切换系统环境变量,把对应版本的python环境变量设置好,再安装指定版本的frida
再装指定版本的frida-tools(注意先后顺序,一个tools对应多个frida版本)
frida-tools版本查看
https://github.com/frida/frida/releases里面有frida的版本,每个下面有frida-tools的版本号
装好之后在python的目录下有frida相关的东西,把这些文件复制到虚拟环境中的目录下
(直接进虚拟环境的python,pip装不上包)
最后再进虚拟环境,运行pip install frida-tools
(实际上虚拟环境这个不一定得配)

frida代码提示的配置
npm i @types/frida-gum

frida-server的配置
https://github.com/frida/frida/releases里面有frida的版本,每个下面有frida-server的版本号,frida-server版本与frida版本要匹配
adb push frida-server-14.2.18-android-arm64 /data/local/tmp/
adb shell进去,然后./运行(root权限运行),cmd中运行frida-ps -U如果能查看手机上的进程,就说明没问题

静态方法和实例方法的Hook

不需要管修饰符(共有、私有、受保护的、没有修饰符的、默认修饰符的),不需要区分静态和实例方法,都是使用Java.use

以下类源码

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
33
34
35
36
37
38
39
40
package com.xxx.hook;

/* loaded from: classes.dex */
public class Money {
private static String flag;
private int amount;
private String currency;

public void setCurrency(String str) {
}

public Money(String str, int i) {
this.currency = str;
this.amount = i;
}

public int getAmount() {
return this.amount;
}

public void setAmount(int i) {
this.amount = i;
}

public static String getFlag() {
return flag;
}

public static void setFlag(String str) {
flag = str;
}

public String getCurrency() {
return this.currency;
}

public String getInfo() {
return this.currency + ": " + this.amount + ": " + flag;
}
}

首先Hook的逻辑要在perform的匿名函数内部

1
2
3
Java.perform(function (){
Java.use....
});
1
2
3
4
5
6
7
8
9
10
11
//静态方法和实例方法的hook
var money = Java.use("com.xxx.hook.Money");
money.getInfo.implementation = function () {
var result = this.getInfo();
console.log("money.getInfo result: ", result)
return result;
}
money.setFlag.implementation = function (a) {
console.log("money.setFlag param: ", a);
return this.setFlag(a);
}

至于返回值的话,目标函数的返回值是什么就返回什么,void的话可返回可不返回,一般是返回一下

函数参数和返回值的修改

实际上就是在return的时候修改值

1
2
3
4
5
6
7
8
9
10
11
12
13
//函数参数和返回值的修改
var money = Java.use("com.xxx.hook.Money");
//var string = Java.use("java.lang.String");
money.getInfo.implementation = function () {
var result = this.getInfo();
console.log("money.getInfo result: ", result)
//return string.$new("这是修改后的返回值111111");
return "这是修改后的返回值";
}
money.setFlag.implementation = function (a) {
console.log("money.setFlag param: ", a);
return this.setFlag("这是我新设置的参数");
}

构造方法的hook

先获取类,再调用类名.$init

1
2
3
4
5
6
//构造方法的hook,注意参数类型和个数
var money = Java.use("com.xxx.hook.Money");
money.$init.implementation = function (a, b) {
console.log("money.$init param: ", a, b);
return this.$init("美元", 200);
}

对象参数的修改

也就是函数的参数是一个对象,需要用到$new语法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Wallet {
private static String flag;
private int balance;
private String brand;
private InnerStructure innerStructure = new InnerStructure();
private String name;

public Wallet(String str, String str2, int i) {
this.name = str;
this.brand = str2;
this.balance = i;
}

public boolean deposit(Money money) {
if (money == null || money.getAmount() <= 0) {
return false;
}
this.balance += money.getAmount();
return true;
}
...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//对象参数的构造与修改
var wallet = Java.use("com.xxx.hook.Wallet");
var money = Java.use("com.xxx.hook.Money");
// 参数a是Money对象,有getInfo方法
wallet.deposit.implementation = function (a) {
console.log("wallet.deposit param: ", a.getInfo());
// 通过$new来传入新的对象
return this.deposit(money.$new("美元", 200));
}

var wallet = Java.use("com.xxx.hook.Wallet");
wallet.deposit.implementation = function (a) {
a.setAmount(2000);
console.log("wallet.deposit param: ", a.getInfo());
return this.deposit(a);
}

HashMap的打印

上一个里面,在打印篡改后的值还有封装好的函数getInfo(),而且类里面也没有重写toString方法(不重写toString方法的话在打印toString的时候会输出类名@xxx这种东西),那么没有这种封装函数的时候怎么办呢?

Utils.shufferMap方法如下(不是系统自带的,是自写的)

1
2
3
4
5
6
7
public static String shufferMap(HashMap<String, String> hashMap) {
StringBuilder sb = new StringBuilder();
for (String str : hashMap.keySet()) {
sb.append(hashMap.get(str));
}
return sb.toString();
}

现在要Hook这个方法,取里面的参数,把HashMap参数打印出来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//HashMap的打印
var utils = Java.use("com.xxx.hook.Utils");
var stringBuilder = Java.use("java.lang.StringBuilder");
utils.shufferMap.implementation = function (a) {
// 有时候直接console.log(a)也能打印,但是不一定
var key = a.keySet();
var it = key.iterator();
var result = stringBuilder.$new();
while(it.hasNext()){
var keystr = it.next();
var valuestr = a.get(keystr);
result.append(valuestr);
}
console.log("utils.shufferMap param: ", result.toString());
var result = this.shufferMap(a);
console.log("utils.shufferMap result: ", result);
return result;
}

从Java里扣代码进js运行的时候,有些类型js里是没有的,需要改,比如stringsetIterator这种类型在js都是var类型,js里数据类型只有var或者let,且不需要强转,要注意哪些参数是js类型,哪些是java类型

那为什么之前在hook函数的内部 return "aaaa" 能顺利返回?这里"aaaa"是js的类型,为什么直接传递给java就没事,而不需要去用stringBuilder.$new("aaaa")来转换为java类型呢?因为frida在获取参数和返回参数这里帮我们封装好了,比如前面的function(a),这里的a参数frida就会自己去识别是什么java类型,然后我们就可以直接调用它里面有的方法,总之分清楚哪些是js的类型哪些是java类型的意义就是有的方法只在js有,有的方法只在java有,具体还得测,这个是比较迷

重载方法的hook

.overload()语法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 重载方法的hook
var utils = Java.use("com.xxx.hook.Utils");
utils.getCalc.overload('int', 'int').implementation = function (a, b) {
console.log("utils.getCalc param: ", a, b);
return this.getCalc(a, b);
}
utils.getCalc.overload('int', 'int', 'int').implementation = function (a, b, c) {
console.log("utils.getCalc param: ", a, b, c);
return this.getCalc(a, b, c);
}
utils.getCalc.overload('int', 'int', 'int', 'int').implementation = function (a, b, c, d) {
console.log("utils.getCalc param: ", a, b, c, d);
return this.getCalc(a, b, c, d);
}

hook所有重载方法

方法名.overloads会返回所有重载函数,方法名.length会返回重载函数的个数,方法名.overloads[int i] 会直接访问到该函数

js里arguments会返回当前函数的参数数组(动态的,给几个参数,arguments就有多长),arguments[int i] 直接访问到对应序号的参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// hook方法的所有重载
var utils = Java.use("com.xxxx.hook.Utils");
var overloadsArr = utils.getCalc.overloads;
for (var i = 0; i < overloadsArr.length; i++) {
overloadsArr[i].implementation = function () {
showStack();
var params = "";
for (var j = 0; j < arguments.length; j++) {
params += arguments[j] + " ";
}
console.log("utils.getCalc is called! params is: ", params);
// if(arguments.length == 2){
// return this.getCalc(arguments[0], arguments[1]);
// }else if(arguments.length == 3){
// return this.getCalc(arguments[0], arguments[1], arguments[2]);
// }else if(arguments.length == 4){
// return this.getCalc(arguments[0], arguments[1], arguments[2], arguments[3]);
// }
console.log(this);
// 通过js里的apply方法避免重复调用造轮子
return this.getCalc.apply(this, arguments);
}
}

js里的this,如果this在函数体内部,谁调用了这个函数谁就是this

1
2
3
4
utils.getCalc.overload('int', 'int', 'int', 'int').implementation = function (a, b, c, d) {
console.log("utils.getCalc param: ", a, b, c, d);
return this.getCalc(a, b, c, d);
}

上面的代码里的this就是utils这个类,因为是utils类调用了getCalc方法

主动调用Java函数

区分静态方法和实例方法,静态方法直接拿到类之后调用类名.方法名,实例方法需要先实例化对象再调用

实例方法的调用又区分是否需要新建对象,还是说直接在内存中搜索对应的对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 8. 主动调用
function newcall(){
Java.perform(function (){
// a) 静态方法
var money = Java.use("com.xxx.hook.Money");
money.setFlag("test");
// b) 实例方法 创建新对象
var moneyObj = money.$new("卢布", 1000);
console.log(moneyObj.getInfo());
// c) 实例方法 获取已有对象(Java.choose),第二个参数是callback函数
Java.choose("com.xxx.hook.Money", {
// onMatch是找到一个就调用一次
onMatch: function (obj){
console.log(obj.getInfo());
},
onComplete: function (){
console.log("内存中的Money对象搜索完毕");
}
});
})
}

然后就可以在frida命令开始之后直接在终端命令行调用newcall函数

函数调用栈的打印

主动调用,打印堆栈,用的是android.util.Log.getStackTraceString方法

1
Log.getStackTraceString(new Throwable())
1
2
3
4
5
function showStack(){
Java.perform(function (){
console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()));
});
}

获取和修改类的字段

访问静态字段使用 类名.属性名.value 来访问或者修改

访问实例字段需要先创建对象,然后使用 对象名.属性名.value 来访问或者修改

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
// 10. 获取和修改类的字段 
function test(){
Java.perform(function () {
// a) 静态字段
var money = Java.use("com.xxx.hook.Money");
console.log(money.flag.value);
money.flag.value = "tt";
console.log(money.flag.value);
// b) 实例字段 创建新对象
var moneyObj = money.$new("欧元", 2000);
console.log(moneyObj.currency.value);
moneyObj.currency.value = "tt currency";
console.log(moneyObj.currency.value);
// c) 实例字段(获取已有对象)
Java.choose("com.xxx.hook.Money", {
onMatch: function (obj) {
console.log("Java.choose Money: ", obj.currency.value);
}, onComplete: function () {

}
});
// 如果字段名和方法名一样 需要加下划线前缀
Java.choose("com.xxx.hook.BankCard", {
onMatch: function (obj) {
console.log("Java.choose BankCard: ", obj._accountName.value);
}, onComplete: function () {

}
});
});
}

内部类与匿名类的hook

demo如下

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
33
34
35
36
37
38
public class Wallet {
private static String flag;
private int balance;
private String brand;
private InnerStructure innerStructure = new InnerStructure();
private String name;

public Wallet(String str, String str2, int i) {
this.name = str;
this.brand = str2;
this.balance = i;
}

public boolean deposit(Money money) {
...
}

public Money withdraw(String str, int i) {
...
}

public boolean addBankCard(BankCard bankCard) {
...
}

/* loaded from: classes.dex */
public class InnerStructure {
private ArrayList<BankCard> bankCardsList = new ArrayList<>();

public InnerStructure() {
}

public String toString() {
return this.bankCardsList.toString();
}
}
...
}

访问 Wallet类的InnerStructure内部类,需要使用$来访问

1
var Wallet$InnerStructure = Java.use("com.xxx.hook.Wallet$InnerStructure");

现在要获取InnerStructure内部类的bankCardsList,这里就应该搜索内存去寻找这个类,而不是自己去新建

匿名内部类就是在方法内部直接new对象

1
2
3
4
5
6
logOutPut(new Money("欧元", ItemTouchHelper.Callback.DEFAULT_DRAG_ANIMATION_DURATION) {
@Override
public String getInfo() {
return getCurrency() + " " + getAmount() + " 这是匿名内部类";
}
}.getInfo());

匿名内部类的访问就是$1或者$2…以此类推,序号可以看smali代码或者枚举所有已经加载的类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function test1(){
// 11. Hook内部类与匿名类
// var Wallet$InnerStructure = Java.use("com.xiaojianbang.hook.Wallet$InnerStructure");
// console.log(Wallet$InnerStructure);
Java.perform(function () {
Java.choose("com.xxx.hook.Wallet$InnerStructure", {
onMatch: function (obj) {
console.log("Java.choose Wallet$InnerStructure: ", obj.bankCardsList.value);
}, onComplete: function () {

}
});
var money$1 = Java.use("com.xxx.app.MainActivity$1");
money$1.getInfo.implementation = function () {
var result = this.getInfo();
console.log("money.getInfo result: ", result);
return result;
}
});
}

枚举所有已加载的类与枚举类的所有方法

枚举所有已加载类实际上就是执行enumerateLoadedClassesSync(),命令行里面直接执行也可以

1
2
Java.enumerateLoadedClassesSync();
console.log(Java.enumerateLoadedClassesSync().join("\n"));

枚举类的所有方法需要用到反射

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
function test2(){
Java.perform(function (){
//console.log(Java.enumerateLoadedClassesSync().join("\n"));
var wallet = Java.use("com.xxx.hook.Wallet");
var methods = wallet.class.getDeclaredMethods();
var constructors = wallet.class.getDeclaredConstructors();
var fields = wallet.class.getDeclaredFields();
var classes = wallet.class.getDeclaredClasses();

for (let i = 0; i < methods.length; i++) {
console.log(methods[i].getName());
}
console.log("============================");
for (let i = 0; i < constructors.length; i++) {
console.log(constructors[i].getName());
}
console.log("============================");
for (let i = 0; i < fields.length; i++) {
console.log(fields[i].getName());
}
console.log("============================");
for (let i = 0; i < classes.length; i++) {
console.log(classes[i].getName());
//classes[i] 这里得到的已经是类的字节码,不需要再.calss
var Wallet$InnerStructure = classes[i].getDeclaredFields();
for (let j = 0; j < Wallet$InnerStructure.length; j++) {
console.log(Wallet$InnerStructure[j].getName());
}
}

});
}

hook类的所有方法

实际上就是把上面的结合起来,先获取类,再枚举这个类下面的所有方法,再对每个方法,去Hook方法的所有重载

这里要注意的一点

1
2
3
4
5
6
7
8
9
10
var methodName = methods[i].getName();
var overloads = utils.methodName.overloads;
这样是访问不到这个方法下面的所有重载的,跟js的语法有关系,这里utils.methodName会被当初属性去处理而不是字符串
需要使用utils[methodName]去访问该方法
就和
var obj = {a:"aaaaa",b:"bbbbb"};
obj.a;//访问obj对象的a属性
var methodName = 'a';
obj.methodName;//报错
obj[methodName];//正确,访问obj对象的a属性

代码实现循环的时候要注意作用域,var定义的变量和let定义的变量的作用域不一样,有时候循环会出问题(超出methods的范围),这个具体调试的时候再去改,js语法的问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Java.perform(function () {
function hookFunc(methodName) {
console.log(methodName);
var overloadsArr = utils[methodName].overloads;
for (var j = 0; j < overloadsArr.length; j++) {
overloadsArr[j].implementation = function () {
var params = "";
for (var k = 0; k < arguments.length; k++) {
params += arguments[k] + " ";
}
console.log("utils." + methodName + " is called! params is: ", params);
return this[methodName].apply(this, arguments);
}
}
}

var utils = Java.use("com.xxx.hook.Utils");
var methods = utils.class.getDeclaredMethods();
for (var i = 0; i < methods.length; i++) {
var methodName = methods[i].getName();
hookFunc(methodName);
}
});

Java.registerClass

用来给app注入新的类

实际上这个使用有点麻烦,要求app中有一个未实现的接口,然后我们实现这个接口,去注入实现接口的类

了解即可,一般使用注入dex文件的方法来注入类

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
33
34
35
36
37
const MyWeirdTrustManager = Java.registerClass({
name: 'com.xxx.app.MyRegisterClass',
implements: [Java.use("com.xxx.app.TestRegisterClass")],
fields: {
description: 'java.lang.String',
limit: 'int',
},
methods: {
$init() {
console.log('Constructor called');
},
// 重载函数要包含在数组里
test1: [{
returnType: 'void',
argumentTypes: [],
implementation() {
console.log('test1 called');
}
}, {
returnType: 'void',
argumentTypes: ['java.lang.String', 'int'],
implementation(str, num) {
console.log('test1(str, num) called', str, num);
}
}],
test2(str, num) {
console.log('test2(str, num) called', str, num);
return null;
},
}
});
var myObj = MyWeirdTrustManager.$new();
myObj.test1();
myObj.test1("test1", 100);
myObj.test2("test2", 200);
myObj.limit.value = 10000;
console.log(myObj.limit.value);

frida注入dex文件

怎么生成一个自己的dex去注入呢?第一种方法肯定是IDEA自己写个Java代码,然后编译成.class文件(AS中也一样,可以找到编译之后生成的.class文件),然后用dx工具将.class文件编译成.dex文件

dx工具打包dex文件

AS编译,选Build->Make project然后在build目录的outputs文件夹下有apk文件,在intermediates文件夹下的javac文件夹下可以找到.class文件

dx工具的路径在 SDK\build-tools\版本号 目录里面的dx.bat

dx打包.class文件为.dex文件

1
2
3
4
dx --dex --output=patch.dex class文件所在目录,可以是打包所有class也可以指定class
dx --dex --output=patch.dex com\xxx\new\*
dx --dex --output=patch.dex com\xxx\new\Test.class
这里目录不能是绝对路径,要是相对路径,最好就是包名路径

使用baksmali与smali打包dex文件

1
2
3
4
源码 https://github.com/JesusFreke/smali
jar https://bitbucket.org/JesusFreke/smali/downloads/
反编译dex java -jar baksmali-2.5.2.jar d classes.dex
回编译ex java -jar smali-2.5.2.jar a smali

baksmali是把dex编译生smali文件,smali是把smali反编译为dex文件

把apk作为压缩包打开,拖出来里面的dex文件,运行上面的命令,生成一个out文件夹,里面有包名,包名目录下的就有.smali文件,一般是每个类对应一个.smali文件,然后筛选一下只保留我们需要的smali文件,其他的都删了

回编译的时候参数为目录名 dex java -jar smali-2.5.2.jar a out 然后会生成一个out.dex文件

apktool的使用

使用apktool将apk文件解包提取出dex文件

1
2
3
apktool d demo.apk -o app 会输出一个app目录,里面有smali文件和dex文件
apktool d demo.apk -r -s app 不解密源代码和资源文件
apktool b app -o demo2.apk 指定目录,输出为一个apk文件,但是这个apk文件中没有META-INF文件夹,也就是缺少签名

apksigner的使用

对apk进行签名,进入Android SDK/build-tools/SDK版本,输入命令

1
apksigner sign --ks myname.jks app.apk

若密钥库中有多个密钥对,则必须指定密钥别名

1
apksigner sign --ks myname.jks --ks-key-alias myname app.apk

默认同时使用v1和v2签名,若想禁用v2签名

1
2
3
apksigner sign -v2-signing-enabled false --ks myname.jks app.apk
-v2-signing-enabled 是否开启v2签名,默认开启
-v1-signing-enabled 是否开启v1签名,默认开启

密钥库生成方法,AS中 build-> generate signed bundle or apk -> 选apk next -> create new

1
2
3
4
Key store path随意,下面的password随意,这个密码是密钥库的密码
Key部分
Alias是密钥的别名,随意,password随意,这个密码是密钥的密码
Certificate里面的信息随意

完成之后点击OK

dex注入

语法

1
Java.openClassFile("/data/local/tmp/mydemo.dex").load();

然后再在Hook.js里使用dex里面的类

1
2
3
4
5
6
7
8
9
10
Java.perform(function () {
Java.openClassFile("/data/local/tmp/patch.dex").load();
var test = Java.use("com.xxx.myapplication.Test");
var utils = Java.use("com.xxx.hook.Utils");
utils.shufferMap.implementation = function (map) {
var result = test.print(map);
console.log(result);
return result;
}
});

Hook枚举类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
枚举类enum概念,枚举类是一种特殊的类,里面包含一组有限的只读对象,枚举是一组常量的集合
比如季节,一共4个,这种应该是只读

自定义枚举类
1.构造器私有化
2.定义public static fianl修饰的属性(比如spring summer autumn winter)
3.枚举对象名(常量)通常全部大写,多个单词下划线分割
4.创建对象赋值给这些属性
5.定义toString方法

enum类
1.使用关键字enum代替class
2.public static final Season SPRING = new Season("春天");可以简化成 SPRING("春天")
3.枚举对象必须放在枚举类的第一行
4.有多个枚举对象,使用逗号间隔,最后一个分号结尾
5.使用无参构造器创建枚举对象,则小括号可以忽略
6.枚举对象可以有多个属性
7.enum类重写了toString方法

1
2
3
4
5
6
7
8
9
10
package com.xxx.app;

/* loaded from: classes.dex */
public enum Season {
SPRING,
SUMMER,
AUTUMN,
WINTER
}
不写明toString方法,实际上在enum类中默认已经重写了toString方法,可以直接输出对象

枚举类一旦被加载,所有的对象都被创建,需要搜索内存去找到对象

1
2
3
4
5
6
7
8
9
10
11
12
Java.perform(function () {
Java.choose("com.xxx.app.Season", {
onMatch: function (obj) {
// 找到对象之后可以调用对象的方法
console.log(obj.ordinal());
}, onComplete: function () {

}
})
// 或者直接调用类的方法
console.log(Java.use("com.xxx.app.Season").values());
});

frida写文件

frida只提供了写文件的api,读文件的话需要Hook so文件,这个是后话

1
2
3
4
var ios = new File("/sdcard/test.txt","w");
ios.write("test new");
ios.flush();
ios.close();

正常运行会报错,没权限,即使app被赋予了存储空间的权限也写不进去,这是高版本Android的设置

SD卡分为共有存储空间与私有存储空间

1
2
/data/data/包名 app的私有目录
/sdcard/Android/data/包名 app的私有目录

可以在app的私有目录下写

获取私有目录的方法

1
2
3
4
5
6
Environment.getRootDirectory().toString();
Environment.getDataDirectory().toString();
Environment.getDownloadCacheDirectory().toString();
Environment.getExternalStorageDirectory().toString();
Environment.getExternalStorageState().toString();
getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS).toString();

以Hook getExternalFilesDir方法为例,该方法是非静态方法,需要先实例化对象,getExternalFilesDir方法来自于类android.content.ContextWrapper,所以需要实例化android.content.ContextWrapper类,而android.content.ContextWrapper的构造方法需要参数context,就需要获取context

1
2
3
4
5
6
7
8
9
10
11
Java.perform(function () {
// 获取context
var current_application = Java.use('android.app.ActivityThread').currentApplication();
var context = current_application.getApplicationContext();
var path = Java.use("android.content.ContextWrapper").$new(context).getExternalFilesDir("Download").toString()
console.log(path);
var ios = new File(path + "/xxx.txt", "w");
ios.write("xiaojianbang is very very good!!!\n");
ios.flush();
ios.close();
});

类型强转Java.cast

1
Java.cast(对象变量,目标类型)

主要用于解决向上转型的时候,比如Hashmap对象赋值给了map类这种,在调用toString方法无法得到结果(会输出 [object…]),类似的还有List类,ArrayList对象赋值给了List类也无法直接打印

需要再强行转为子类

1
2
3
4
5
6
7
8
9
10
11
var utils = Java.use("com.xxx.hook.Utils");
utils.shufferMap.implementation = function (hashmap) {
console.log("hashmap: ", hashmap);
return this.shufferMap(hashmap);
}
utils.shufferMap2.implementation = function (map) {
console.log("map: ", map);
var result = Java.cast(map, Java.use("java.util.HashMap"));
console.log("map: ", result);
return this.shufferMap2(result);
}

Java.array处理对象数组

有的情况下,想去Hook参数为数组对象的函数

1
2
3
4
5
6
7
8
public static String myPrint(String[] strArr) {
StringBuilder sb = new StringBuilder();
for (String str : strArr) {
sb.append(str);
sb.append("|");
}
return sb.toString();
}
1
2
3
4
5
var utils = Java.use("com.xxxx.hook.Utils");
// console.log(utils.myPrint(["name", "jack", "harry", "tom"]));
// 这里的Ljava.lang.String里的L表示数组,参数就得是字符串数组
var strarr = Java.array("Ljava.lang.String;", ["name", "jack", "harry", "tom"]);
console.log(utils.myPrint(strarr));

Object数组的构建及可变数组

1
2
3
4
5
6
7
8
public static String myPrint(Object... objArr) {
StringBuilder sb = new StringBuilder();
for (Object obj : objArr) {
sb.append(obj);
sb.append("|");
}
return sb.toString();
}
1
2
3
4
5
6
7
var utils = Java.use("com.xxx.hook.Utils");
var bankCard = Java.use("com.xxx.hook.BankCard");
var bankCardObj = bankCard.$new("jack", "123456789", "CBDA", 1, "15900000000");
var integer = Java.use("java.lang.Integer");
var boolean = Java.use("java.lang.Boolean");
//var objarr = Java.array("Ljava.lang.Object;", ["xxx", integer.$new(30), boolean.$new(true), bankCardObj]);
console.log(utils.myPrint(["xxx", integer.$new(30), boolean.$new(true), bankCardObj]));

这里就不能用 console.log(utils.myPrint(["xxx", 30, true, bankCardObj])); 方式,因为类型不同,必须得用Java.array处理,而且intboolean类型不能被frida自动转换为对象,只有字符串可以,所以得new出来

Arraylist的主动调用

arrayList的add方法,接受的参数也是object,和上面一样,只有字符串不需要进行转换,其他的都需要在js中进行封装为对象之后再传给add方法

1
2
3
4
5
6
7
8
9
10
11
var arrayList = Java.use("java.util.ArrayList").$new();
var integer = Java.use("java.lang.Integer");
var boolean = Java.use("java.lang.Boolean");
var bankCard = Java.use("com.xxx.hook.BankCard");
var bankCardObj = bankCard.$new("jack", "123456789", "CBDA", 1, "15900000000");
arrayList.add("xiaojianbang");
arrayList.add(integer.$new(30));
arrayList.add(boolean.$new(true));
arrayList.add(bankCardObj);
var utils = Java.use("com.xxx.hook.Utils");
console.log(utils.myPrint(arrayList));

Java.enumerateClassLoaders

Hook动态加载的dex

demo

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
33
34
public void dynamic() {
String str = getDir("shell", 0).getAbsolutePath() + File.separator + "xxx.dex";
Context applicationContext = getApplicationContext();
try {
// 打开dex文件
InputStream open = applicationContext.getAssets().open("xxx.dex");
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
byte[] bArr = new byte[1024];
while (true) {
int read = open.read(bArr);
if (read != -1) {
// 如果读取完毕
byteArrayOutputStream.write(bArr, 0, read);
} else {
// 将数据转换为byte数组
byte[] decrypt = decrypt(byteArrayOutputStream.toByteArray());
open.close();
byteArrayOutputStream.close();
FileOutputStream fileOutputStream = new FileOutputStream(new File(str));
fileOutputStream.write(decrypt);
fileOutputStream.close();
DexClassLoader dexClassLoader = new DexClassLoader(str, applicationContext.getCacheDir().getAbsolutePath(), getApplicationInfo().nativeLibraryDir, getClassLoader());
new File(str).delete();
// dexClassLoader必须加载落盘的dex文件(早期加固方法)
// 只能通过反射调用方法,因为代码里根本没这个类
Class loadClass = dexClassLoader.loadClass("com.xxx.app.Dynamic");
Toast.makeText(this, (String) loadClass.getDeclaredMethod("sayHello", new Class[0]).invoke(loadClass.newInstance(), new Object[0]), 1).show();
return;
}
}
} catch (Exception e) {
e.printStackTrace();
}
}

xxx.dex文件里面就一个sayHello方法,这里decrypt()方法未解密,只是表示一下这个逻辑,dex文件是未加密的

1
2
3
4
5
6
7
8
package com.xxx.app;

/* loaded from: assets/xxx.dex */
public class Dynamic {
public String sayHello() {
return "Dynamic is called";
}
}

此时调用Java.enumerateLoadedClassesSync() ,如果不触发事件的话,是不会加载dex文件的,也就搜索不到这个类

而且就算内存中枚举出这个类,也无法使用 Java.use("com.xxx.app.Dynamic") 来获取类,因为frida里面认为两个类是同一个类的依据不仅仅是类名,还有类的加载方式,这里的动态加载类是通过dexClassLoader来加载的,使用默认的loader无法识别,而且每次调用dexClassLoader也会被认为是不同的loader(这就导致hook代码有时候需要在触发dex加载操作的之前或者之后再跑,反正多跑几遍,但是呢主动调用没问题,只是hook在这种情况下会比较迷)

使用如下语法,遍历所有的classLoader,每个loader都加载一遍,直到成功加载

1
Java.enumerateClassLoaders();
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
Java.perform(function () {

// console.log(Java.enumerateLoadedClassesSync().join("\n"));
// var dynamic = Java.use("com.xxx.app.Dynamic");
// console.log(dynamic); 使用默认的loader报错

Java.enumerateClassLoaders({
onMatch: function (loader){
try {
// 指明特定的loader
Java.classFactory.loader = loader;
var dynamic = Java.use("com.xiaojianbang.app.Dynamic");
console.log("dynamic: ", dynamic);
//console.log(dynamic.$new().sayHello());
dynamic.sayHello.implementation = function () {
console.log("hook dynamic.sayHello is run!");
return "xiaojianbang";
}
}catch (e) {
console.log(loader);
}
}, onComplete: function () {

}
});
});

DexClassLoader的hook

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
var dexClassLoader = Java.use("dalvik.system.DexClassLoader");
dexClassLoader.loadClass.overload('java.lang.String').implementation = function (className) {
//console.log(className);
//在这里已经成功获取到类了,可以调用类的方法但是不能使用这个类直接进行hook,因为frida里hook只认Java.use获取的类
var result = this.loadClass(className);
//console.log("class: ", result);
//console.log("class.class: ", result.class);
//console.log("xxxxxxxx: ", result.getDeclaredMethods());
if("com.xxx.app.Dynamic" === className){
Java.classFactory.loader = this;
var dynamic = Java.use("com.xxx.app.Dynamic");
console.log("dynamic: ", dynamic);
// 有时候需要.class有时候不需要,实际的时候再测
//var clazz = dynamic.class;
//console.log("xxxxxxxx: ", clazz.getDeclaredMethods()[0].invoke(clazz.newInstance(), []));
//console.log(dynamic.$new().sayHello());
dynamic.sayHello.implementation = function () {
console.log("dynamic.sayHello is called");
return "test";
}
console.log(dynamic.$new().sayHello());

}
return result;
}

上面的代码在Hook的时候也会比较迷,有时候会在第一次触发的时候hook到,后面再触发就不能hook到了

让Hook只在指定函数内生效

hook某个函数或者某个类,这个类使用很频繁,如果直接hook,可能导致app崩溃,现在比如MainActivity里的generateAESKey()方法调用了new StringBuilder(),只想hook StringBuilder()的逻辑只在generateAESKey()被执行的时候生效,执行其他方法内的StringBuilder()则不生效

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
Java.perform(function () {

function showStacks() {
console.log(
Java.use("android.util.Log")
.getStackTraceString(
Java.use("java.lang.Throwable").$new()
)
);
}

var mainActivity = Java.use("com.xiaojianbang.app.MainActivity");
var stringBuilder = Java.use('java.lang.StringBuilder');
mainActivity.generateAESKey.implementation = function () {
console.log("mainActivity.generateAESKey is called!");
stringBuilder.toString.implementation = function () {
var result = this.toString();
console.log(result);
return result;
};
var result = this.generateAESKey.apply(this, arguments);
stringBuilder.toString.implementation = null; //取消hook
return result;
};


});

其他命令

命令行 %resume 可以重启app的主进程,用来hook启动过程中的函数

-p 可以指定pid进行注入,因为有时候一个app会有两个进程,这个时候指定包名注入不进去

连接多设备多端口,frida server指定监听端口和地址

1
firdaserver -l 0.0.0.0:9000

连接的时候需要指定IP和端口,这里-H参数和-U参数不能同时存在,因为-U是指定USB设备

1
frida -H <ip>:9000 -f ....

frida的python库使用

包名attach

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import frida, sys

jsCode = """
Java.perform(function(){
...
});
"""

# get_usb_device
# get_remote_device
process = frida.get_usb_device().attach('com.dodonew.online')
script = process.create_script(jsCode)
script.load()
print("开始运行")
# .read()是为了让进程不在注入完毕之后退出
sys.stdin.read()

pid attach

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# -*- coding: UTF-8 -*-
import frida, sys

jsCode = """
Java.perform(function(){
...
});
"""

# get_usb_device
# get_remote_device
process = frida.get_usb_device().attach(9999)
script = process.create_script(jsCode)
script.load()
print("开始运行")
sys.stdin.read()

spawn启动hook

类似于--no-pause

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import frida, sys

jsCode = """
Java.perform(function(){
...
});
"""

# get_usb_device
# get_remote_device
device = frida.get_usb_device()
print("device: ", device)
pid = device.spawn(["com.dodonew.online"]) # 以挂起方式创建进程
print("pid: ", pid)
process = device.attach(pid)
print("process: ", process)
script = process.create_script(jsCode)
script.load()
device.resume(pid) # 加载完脚本, 恢复进程运行
print("开始运行")
sys.stdin.read()

连接非标准端口和多个设备

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import frida, sys

jsCode = """
Java.perform(function(){
...
});
"""

# get_usb_device
# get_remote_device
process = frida.get_device_manager().add_remote_device('192.168.3.68:8888').attach('com.dodonew.online')
# process1 = frida.get_device_manager().add_remote_device('192.168.3.69:8888').attach('com.dodonew.online')
# process2 = frida.get_device_manager().add_remote_device('192.168.3.70:8888').attach('com.dodonew.online')
script = process.create_script(jsCode)
# script1 = process.create_script(jsCode)
# script2 = process.create_script(jsCode)
script.load()
# script1.load()
# script2.load()
print("开始运行")
sys.stdin.read()

send

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
33
34
35
36
37
38
39
40
41
42
43
44
import frida, sys

jsCode = """
Java.perform(function(){
var RequestUtil = Java.use('com.dodonew.online.http.RequestUtil');
RequestUtil.encodeDesMap.overload('java.lang.String', 'java.lang.String', 'java.lang.String').implementation = function(a, b, c){
console.log('data: ', a);
console.log('desKey: ', b);
console.log('desIV: ', c);
var retval = this.encodeDesMap(a, b, c);
console.log('retval: ', retval);
return retval;
}
var Utils = Java.use('com.dodonew.online.util.Utils');
Utils.md5.implementation = function(a){
console.log('MD5 string: ', a);
var retval = this.md5(a);
send(retval);
return retval;
}
});
"""


def messageFunc(message, data):
# message其实就是上面send()函数的参数
# send()只能有一个参数,起到的作用就是把数据丢给python
print(message)
if message["type"] == 'send':
print(u"[*] {0}".format(message['payload']))
else:
print(message)

# get_usb_device
# get_remote_device
process = frida.get_usb_device().attach('com.dodonew.online')
script = process.create_script(jsCode)
# 要想调用send,必须调用script.on进行注册
# script.on的第一个参数不能改,第二个参数随意
# 当send被调用时,就会触发message事件,然后调用messageFunc函数
script.on('message', messageFunc)
script.load()
print("开始运行")
sys.stdin.read()

recv

recv一般用来处理python传递给js的数据

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import frida, sys
import time

jsCode = """
Java.perform(function(){
var RequestUtil = Java.use('com.dodonew.online.http.RequestUtil');
RequestUtil.encodeDesMap.overload('java.lang.String', 'java.lang.String', 'java.lang.String').implementation = function(a, b, c){
console.log('data: ', a);
console.log('desKey: ', b);
console.log('desIV: ', c);
var retval = this.encodeDesMap(a, b, c);
console.log('retval: ', retval);
return retval;
}
var Utils = Java.use('com.dodonew.online.util.Utils');
Utils.md5.implementation = function(a){
console.log('MD5 string: ', a);
var retval = this.md5(a);
send(retval);
// recv.wait()会阻塞js进程,等待python返回的数据
recv(function(obj){
console.log(JSON.stringify(obj));
console.log("Python:", obj.data);
retval = obj.data;
}).wait();
return retval;
}
});
"""

def messageFunc(message, data):
print(message)
if message["type"] == 'send':
print(u"[*] {0}".format(message['payload']))
time.sleep(10)
# 调用script.post把数据传递给js
script.post({"data": "0e8315152843b943563031945032e957"})
else:
print(message)

# get_usb_device
# get_remote_device
process = frida.get_usb_device().attach('com.dodonew.online')
script = process.create_script(jsCode)
script.on('message', messageFunc)
script.load()
print("开始运行")
sys.stdin.read()

frida的rpc调用

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
import frida, sys

jsCode = """
Java.perform(function(){
var RequestUtil = Java.use('com.dodonew.online.http.RequestUtil');
RequestUtil.encodeDesMap.overload('java.lang.String', 'java.lang.String', 'java.lang.String').implementation = function(a, b, c){
console.log('data: ', a);
console.log('desKey: ', b);
console.log('desIV: ', c);
var retval = this.encodeDesMap(a, b, c);
console.log('retval: ', retval);
return retval;
}
var Utils = Java.use('com.dodonew.online.util.Utils');
Utils.md5.implementation = function(a){
console.log('MD5 string: ', a);
var retval = this.md5(a);
console.log('retval: ', retval);
return retval;
}
});

function test(data){
var result = "";
Java.perform(function(){
result = Java.use('com.dodonew.online.util.Utils').md5(data);
});
return result;
}

// 需要在js里先用rpc.exports定义一个类似于接口rpc.exports = {xx:xxx}
// 在python里使用script.rpc.xx()去调用对应的方法
// 类似于主动调用,只不过是在python里主动调用
rpc.exports = {
rpcfunc: test
};

"""

# get_usb_device
# get_remote_device
device = frida.get_usb_device()
print("device: ", device)
pid = device.spawn(["com.dodonew.online"]) # 以挂起方式创建进程
print("pid: ", pid)
process = device.attach(pid)
print("process: ", process)
script = process.create_script(jsCode)
script.load()
device.resume(pid) # 加载完脚本, 恢复进程运行

# 函数名使用rpcFUnc其实传到js里都给转成小写了,如果要访问大写字母的函数,要在大写字母前面加上下划线
result = script.exports.rpcFUnc('thisisafunc')
print(result)
print("开始运行")
sys.stdin.read()