最近在学习frida的使用,写个文档记录下…
frida常用方法总结目录
1 常用命令
1.1 启动app
1.1.1 setImmediate
立即执行
setImmediate(function () {
console.log("[*] Starting script");
Java.perform(function () {
hook()
})
})
1.1.2 setTimeout
延迟执行,因为有一些dex文件还来不及load进来,需要过一两秒,比如这段代码延迟2秒执行
setImmediate(function () {
console.log("[*] Starting script");
Java.perform(function () {
hook()
})
}, 2000)
1.1.3 frida命令启动app
当然要进入adb shell把frida server启动了先,这里不写了
立即启动
frida -U -f cn.demo.login -l .\hookMutilpleFunction.js --no-pause
-f 表示需要启动的app包名
-l 表示frida脚本
有时候某些dex文件还没被加载,需要延迟启动,可以把--no-pause去掉
frida -U -f cn.demo.login -l .\hookMutilpleFunction.js
然后用%resume启动app
1.1.4 python脚本启动
方法一:直接读取js文件
import time
import frida
apk_name = 'cn.demo.login'
device = frida.get_usb_device()
pid = device.spawn([apk_name])
device.resume(pid)
time.sleep(1)
session = device.attach(pid)
with open("frida.js",'r',encoding='UTF-8') as f:
script = session.create_script(f.read())
script.load()
input()
方法二:js以文本形式
import time
import frida
apk_name = 'cn.demo.login'
jscode = """
Java.perform(function(){
console.log('do something')
});
"""
device = frida.get_usb_device()
pid = device.spawn([apk_name])
device.resume(pid)
time.sleep(1)
session = device.attach(pid)
script = session.create_script(jscode)
script.load()
input()
1.2 查看模拟器所有进程
注意:这个是基于window平台的,linux平台把findstr改成grep即可
1.2.1 查看当前进程
frida-ps -Ua
1.2.2 搜索指定进程
frida-ps -Ua | findstr login
1.3 查看当前的top activitivy
通常用于查看当前正在运行的app
adb shell dumpsys activity top | findstr ACTIVITY
2 frida基础
2.1 frida的main函数
Java层的Hook都是从Java.perform开始的,相当于main()方法,下面的js语法都是需要写在这个里面的。
所有关于frida的操作必须在这个函数里面,否则会报错
Java.perform(function(){
//在这个编写frida逻辑
})
2.2 查找指定类的实例
用于查找运行堆中指定类的实例,获得实例后可以调用实例的函数。声明为:Java.choose(className,callbacks)
Java.choose("cn.demo.login.ui.login.LoginActivity",{
onMatch:function(){
//onMatch回调会在找到类的实例后调用,也就是说内存中有多少实例,就会调用多少次
},
onComplete:function(){
//onComplete回调会在所有onMatch完成后调用
}
})
2.3 查看虚拟机是否启动
一般用于检查当前进程的java虚拟机是否启动,防止报错
if (!Java.available) {
console.log('java虚拟机未加载!')
return
}
2.4 注册一个自定义的类
用于注册一个自定义的类到内存里
var obj = Java.registerClass({
name:"top.zyzling.User", //类的全限定名.必传
superClass:'xxx', //父类的全限定类名,可选
implements:"xxx", //该类实现的接口全限定名,可选
fields:{属性名:"类型"}, //该类的属性集,可选
methods:{} //该类的方法集,可选
})
官方的例子
//获取目标进程的SomeBaseClass类
var SomeBaseClass = Java.use('com.example.SomeBaseClass');
//获取目标进程的X509TrustManager类
var X509TrustManager = Java.use('javax.net.ssl.X509TrustManager');
var MyWeirdTrustManager = Java.registerClass({
//注册一个类是进程中的MyWeirdTrustManager类
name: 'com.example.MyWeirdTrustManager',
//父类是SomeBaseClass类
superClass: SomeBaseClass,
//实现了MyWeirdTrustManager接口类
implements: [X509TrustManager],
//类中的属性
fields: {
description: 'java.lang.String',
limit: 'int',
},
//定义的方法
methods: {
//类的构造函数
$init: function () {
console.log('Constructor called');
},
//X509TrustManager接口中方法之一,该方法作用是检查客户端的证书
checkClientTrusted: function (chain, authType) {
console.log('checkClientTrusted');
},
//该方法检查服务器的证书,不信任时。在这里通过自己实现该方法,可以使之信任我们指定的任何证书。在实现该方法时,也可以简单的不做任何处理,即一个空的函数体,由于不会抛出异常,它就会信任任何证书。
checkServerTrusted: [{
//返回值类型
returnType: 'void',
//参数列表
argumentTypes: ['[Ljava.security.cert.X509Certificate;', 'java.lang.String'],
//实现方法
implementation: function (chain, authType) {
//输出
console.log('checkServerTrusted A');
}
}, {
returnType: 'java.util.List',
argumentTypes: ['[Ljava.security.cert.X509Certificate;', 'java.lang.String', 'java.lang.String'],
implementation: function (chain, authType, host) {
console.log('checkServerTrusted B');
//返回null会信任所有证书
return null;
}
}],
// 返回受信任的X509证书数组。
getAcceptedIssuers: function () {
console.log('getAcceptedIssuers');
return [];
},
}
});
2.5 类操作
比如业务里有这一段java类
package cn.demo.login.data.model;
public class Student {
private String name;
private int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
public String Say(){
return "My name is" + name + ", age " + Integer.toString(age);
}
// 静态方法
public static String Info(){
return "This is a student class!!!";
}
}
2.5.1 获取类
注意:是获取类,相当于class.forName()不是获取类实例!
比如,获取类
Java.perform(function () {
// 获取类
var stu_cls = Java.use('cn.demo.login.data.model.Student');
})
2.5.2 初始化类
Java.perform(function () {
// 获取类
var stu_cls = Java.use('cn.demo.login.data.model.Student');
// 初始化类
var stu = stu_cls.$new("世界第一打怪物", 22);
console.log(stu)
})
2.5.3 调用类的方法
2.5.3.1 静态方法
非静态方法直接use,然后调用就可以了
Java.perform(function () {
// 获取类
var stu_cls = Java.use('cn.demo.login.data.model.Student');
// 调用类的方法
var str = stu_cls .Info()
console.log(str)
})
2.5.3.2 非静态方法
非静态的方法调用需要先初始化类实例
Java.perform(function () {
// 获取类
var stu_cls = Java.use('cn.demo.login.data.model.Student');
// 初始化类
var stu = stu_cls.$new("世界第一打怪物", 22);
// 调用类的方法
var str = stu.Say()
console.log(str)
})
2.5.4 获取类变量的值
静态变量和非静态变量类似,这里只展示非静态变量的获取
不管变量范围是啥,public or private, 都可以获取
Java.perform(function () {
// 获取类
var stu_cls = Java.use('cn.demo.login.data.model.Student');
// 初始化类
var stu = stu_cls.$new("世界第一打怪物", 22);
// 获取变量的值
var name = stu.name.value
console.log(name)
})
2.5.5 更改变量的值
Java.perform(function () {
// 获取类
var stu_cls = Java.use('cn.demo.login.data.model.Student');
// 初始化类
var stu = stu_cls.$new("世界第一打怪物", 22);
// 调用类的方法
console.log('原值:', stu.name.value)
stu.name.value = '你爸爸'
console.log('现值:', stu.name.value)
})
3 hook
下面还是以Student类为例
3.1 hook静态/非静态方法
3.1.1 hook没重载
Java.perform(function () {
console.log('ok! hook好了')
Java.use('cn.demo.login.data.model.Student').Say.implementation = function(){
var new_str = '我有一只小毛驴,我从来也不骑'
// 调用原方法的结果
var origina_str = this.Say()
console.log(origina_str)
// 更改原有的逻辑
return new_str
}
})
3.1.2 hook重载
我们改造下Student类,给Say方法增加个重载,如下
package cn.demo.login.data.model;
public class Student {
private String name;
private int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
public String Say(){
return "My name is" + name + ", age " + Integer.toString(age);
}
public String Say(String str){
return str + ", My name is" + name + ", age " + Integer.toString(age);
}
public static String Info(){
return "This is a student class!!!";
}
}
这时hook就要小心了,要加上overload(参数类型)
Java.perform(function () {
console.log('ok! hook好了')
Java.use('cn.demo.login.data.model.Student').Say.overload('java.lang.String').implementation = function(str){
var new_str = '这是新的字符串'
// 调用原方法的结果
var origina_str = this.Say(str)
console.log(origina_str)
// 更改原有的逻辑
return new_str
}
})
常用java参数类型如下,注意对比java和js类型的不同
类型全名 | 类型缩写 |
---|---|
boolean | Z |
byte | B |
char | C |
double | D |
float | F |
int | I |
long | J |
short | S |
String | java.lang.String |
数组表示一般是“[类型缩写”,比如“[]int”, 缩写为“[I” |
3.1.3 通用hook方法
有时候想通过更改配置文件的方式来hook,而不是改代码,可以使用如下通用的hook代码,通过指定类名,方法名以及参数即可(为什么不需要指定返回值,因为方法的重载是不需要靠他的!)
// 根据overload获取参数字符串
function getTargetArgumentsByOverload(overload) {
var curArguments = "(";
overload.argumentTypes.forEach(function (type) {
curArguments += type.className + ",";
});
if (curArguments.length > 1) {
curArguments = curArguments.substr(0, curArguments.length - 1);
}
curArguments += ")";
return curArguments
}
// hook函数
function hook(targetClass, targetMethod, targetArguments) {
// 替换掉所有的空格, 防止写配置的时候不小心
targetClass = targetClass.replace(/\s+/g, "")
targetMethod = targetMethod.replace(/\s+/g, "")
targetArguments = targetArguments.replace(/\s+/g, "")
// 获取参数数组
var targetArgumentsArr = targetArguments.substr(1, targetArguments.length - 2).split(',')
// targetClassMethod
var targetClassMethod = targetClass + ' ' + targetMethod + targetArguments
//目标类
var hook = Java.use(targetClass);
//获取所有重载
var overloads = hook[targetMethod].overloads
//重载次数
var overloadCount = overloads.length;
//打印日志:追踪的方法有多少个重载
console.log("Tracing " + targetClass + '.' + targetMethod + " [" + overloadCount + " overload(s)]");
//每个重载都进入一次
for (var i = 0; i < overloadCount; i++) {
//获取参数类型字符串
var overload = overloads[i]
var curArguments = getTargetArgumentsByOverload(overload)
//参数不符合预期, continue!
if (curArguments != targetArguments) continue
hook[targetMethod].overloads[i].implementation = function () {
console.warn("\n*** entered " + targetClassMethod);
//打印调用栈,帮助调试,当然可能会信息量很大,不需要是注释掉它
// Java.perform(function () {
// var bt = Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Exception").$new());
// console.log("\nBacktrace:\n" + bt);
// });
// 特定参数处理
for (var j = 0; j < arguments.length; j++) {
// 处理StringString
if (targetArgumentsArr[j] == 'java.lang.String') {
// handle Stirng
}
else if(targetArgumentsArr[j] == '[B'){
// handler []byte
}
// other....
}
// 获取并打印原来函数的返回值
var retval = this[targetMethod].apply(this, arguments);
console.log(retval)
return retval;
}
}
}
然后想hook什么方法,只需要在main函数里指定hook函数的三个参数即可!
Java.perform(function () {
console.log('go hooking!')
hook('cn.demo.login.data.model.Student', 'Say', '(java.lang.String)')
})
3.1.4 模糊hook方法
模糊hook方法,根据方法名hook掉所有重载
// 模糊hook方法,根据方法名hook掉所有重载
function blurry_hook(targetClassMethod) {
//替换掉所有的空格, 防止写配置的时候不小心
targetClassMethod = targetClassMethod.replace(/\s+/g, "");
var r_point = targetClassMethod.length
for (; r_point >= 0 && targetClassMethod[r_point] != '.'; r_point--) { }
var targetClass = targetClassMethod.substring(0, r_point)
var targetMethod = targetClassMethod.substring(r_point + 1)
console.log(targetClass, targetMethod)
// 静态构造函数在frida层hook不到,暂时不处理
if (targetMethod.indexOf("<clinit>") != -1) {
// "静态构造函数在frida层hook不到,暂时不处理";
return
}
// targetClassMethod
var found = false
var targetClassMethod = targetClass + " " + targetMethod;
try {
//目标类
var hook = Java.use(targetClass);
//获取所有重载
var overloads = hook[targetMethod].overloads;
//重载次数
var overloadCount = overloads.length;
// console.log('overloadCount: ', overloadCount)
//每个重载都进入一次
for (var i = 0; i < overloadCount; i++) {
//获取参数类型字符串
var overload = overloads[i]
overload.implementation = function () {
var output = ''
try {
output += "\n\n*** entered " + targetClassMethod + '\n';
var stack_info = printStack(false)
output += stack_info
output += '=============================Val=======================\n'
for (var j = 0; j < overload.argumentTypes.length; j++) {
//处理参数
// 包括了分享内容 || 判断hashcode是否在set里面
var argumentsType = overload.argumentTypes[j].className
var argumentVal = transform(argumentsType, arguments[j])
argumentVal = tuomin(argumentsType, argumentVal)
output += 'argumentsType: ' + argumentsType + ' argumentVal: ' + argumentVal + '\n'
// argumentVal = tuomin(argumentsType, argumentVal)
arguments[j] = argumentVal
}
output += 'targetReturn: ' + overload.returnType.className + ' '
var returnValStr = transform(overload.returnType.className, returnVal)
output += 'returnVal: ' + returnValStr + '\n'
output += '===========================================================\n'
console.log(output)
found = true
// 处理好参数后,在传给方法,并返回返回值
var returnVal = this[targetMethod].apply(this, arguments)
return returnVal
} catch (err) {
console.log(targetMethod, ' -> err: ', err, ' output: ', output)
}
}
}
} catch (err) {
console.log(targetMethod, ' -> err: ', err)
}
}
3.2 hook构造方法
hook构造方法其实就是调用$init(注意构造参数),hook重载的构造方法跟hook普通重载方法一样,这里只展示hook普通构造方法。还是那个Student类!
Java.perform(function () {
console.log('go hooking!')
Java.use('cn.demo.login.data.model.Student').$init.overload('java.lang.String', 'int').implementation = function(name, age){
console.log('整你!')
var new_name = '世界超级打怪物'
var new_age = 100
this.$init(new_name, new_age)
}
})
3.3 hook内部类
下面以hook内部类的方法为例,hook其他东西类似
3.3.1 hook有名内部类的方法
给Student类增加一个常规内部类Pencil
package cn.demo.login.data.model;
public class Student {
private String name;
private int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
public String Say(){
return "My name is " + name + ", age " + Integer.toString(age);
}
public String Say(String str){
return str + ", My name is" + name + ", age " + Integer.toString(age);
}
public static String Info(){
return "This is a student class!!!";
}
// 常规内部
public class Pencil{
public String GetPencil(){
return "I have a red pencil";
}
}
}
那么怎么hook它呢?
Java.perform(function () {
console.log('go hooking!')
Java.use('cn.demo.login.data.model.Student$Pencil').GetPencil.implementation = function(){
var new_str = "I don't have pencil!"
return new_str
}
})
3.3.2 hook匿名内部类的方法
匿名内部类一般都是“类名$数字”的形式,数字一般按照在代码中定义的顺序依次递增,比如我在代码不同位置定位三个Skill匿名内部类
3.3.2.1 通过定义的顺序
package cn.demo.login.data.model;
abstract class Skill {
public abstract String Sing();
}
public class Student {
private String name;
private int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
public String Say(){
//匿名内部类 1
Skill skill = new Skill() {
@Override
public String Sing() {
return "I can fly";
}
};
return "My name is " + name + ", age " + Integer.toString(age) + "";
}
public String Say(String str){
//匿名内部类 2
Skill skill = new Skill() {
@Override
public String Sing() {
return "I can swim";
}
};
return str + ", My name is" + name + ", age " + Integer.toString(age);
}
public String Sing(){
//匿名内部类 3
Skill skill = new Skill() {
@Override
public String Sing() {
return "I can sing";
}
};
return skill.Sing();
}
}
这也是hook的时候要往下数,比如我要hook第三个,那我就类名$3,具体代码如下
Java.use('cn.demo.login.data.model.Student$3').Sing.implementation = function(){
var new_str = "I can't sing!!!"
return new_str
}
3.3.2.2 通过smali代码
可以如果代码比较多,很难数下去,可以尝试从smali代码中找到答案
- 1 AS点击File->Setting->Plugins, 然后搜索下载java2smali插件
· 2 然后选择Build->Compile to Smali,就可以得到smali代码
· 然后找到找hook的匿名函数所在的方法,比如我要找sing方法里面的类
很明显,这个匿名的skill类叫做Student$3,然后hook它就好了。
4 其他常用方法
4.1 打印调用栈
打印调用栈其实就是故意制造一个Exception,然后打印Exception的调用栈
// 打印调用栈
function printStack() {
Java.perform(function () {
var Exception = Java.use("java.lang.Exception");
var ins = Exception.$new("Exception");
var straces = ins.getStackTrace();
if (straces != undefined && straces != null) {
var strace = straces.toString();
var replaceStr = strace.replace(/,/g, "\r\n");
console.log("=============================Stack strat=======================");
console.log(replaceStr);
console.log("=============================Stack end=======================\r\n");
Exception.$dispose();
}
});
}
4.2 python与js通信
有时候一些操作在js下比较难完成(比如加密,甚至需要机器学习的操作),那我们可以发送到电脑的python脚本中处理,具体看代码注释。
python启动脚本,功能包括启动app和处理脚本
import frida
import base64
# app包名
apk_name = 'cn.demo.login'
# 消息处理函数
def message_handler(message, payload):
# 输出参数
print('message: ', message)
print('payload: ', payload)
print('加密前的data:', message['payload']['data'])
# 加密处理
encode_str = str(base64.b64encode(message['payload']['data'].encode()))
script.post({"data": encode_str})
# 获取启动app的必要信息
device = frida.get_usb_device()
pid = device.spawn([apk_name])
device.resume(pid)
session = device.attach(pid)
# 读取js文件
with open("rpc.js",'r',encoding='UTF-8') as f:
script = session.create_script(f.read())
# 注册消息处理函数
script.on("message", message_handler)
# 启动app
script.load()
#####################
input()
js的frida逻辑
function fun() {
// 发送加密前的数据
send({'data': '我有一只小毛驴,我从来也不骑' }); // 发送加密前的数据
// 接受数据
// 这里wait()就是阻塞,等待python返回结果
// 如果不加wait(),就相当于异步处理
recv(function (received_data) {
console.log('加密后的数据: ', received_data['data'])
}).wait();
//收到数据之后的操作
}
// setTimeout setImmediate
setTimeout(function () { //prevent timeout
console.log("[*] Starting script");
Java.perform(function () {
fun()
})
}, 2000)
4.3 RPC通信
如果想写个工具,能随时随地地方便的调用js脚本,可以考虑使用js的RPC通信,使用很简单,js写好函数然后RPC暴露出来即可!
js
// 加
function add(a, b){
return a+b
}
// 减
function sub(a, b){
return a-b
}
// 乘
function multi(a, b){
return a*b
}
// 除
function divide(a, b){
return a/b
}
// 定义RPC
// 格式: 外部接口名:内部函数名
// 注意: 导出名不可以有大写字母或者下划线
rpc.exports = {
rpcadd: add,
rpcsub: sub,
rpcmulti: multi,
rpcdivide: divide
};
python
import frida
import base64
# app包名
apk_name = 'cn.demo.login'
# 获取启动app的必要信息
device = frida.get_usb_device()
pid = device.spawn([apk_name])
device.resume(pid)
session = device.attach(pid)
# 读取js文件
with open("rpc.js", 'r', encoding='UTF-8') as f:
script = session.create_script(f.read())
# 启动app
script.load()
# 调用API
while True:
choice = input('输入你的选择,1:加法, 2:减法,3:乘法,4:除法,5:退出,然后输入两个数组,最后输出结果')
def input_num():
a = int(input('a='))
b = int(input('b='))
return a, b
if choice == '1':
a, b = input_num()
print(script.exports.rpcadd(a, b))
elif choice == '2':
a, b = input_num()
print(script.exports.rpcsub(a, b))
elif choice == '3':
a, b = input_num()
print(script.exports.rpcmulti(a, b))
elif choice == '4':
a, b = input_num()
print(script.exports.rpcdivide(a, b))
elif choice == '5':
break
print('-----------------------------------------------------')
#####################
input()