一. Frida简介
官网https://frida.re
frida是一款常用的hook框架,支持android/ios/windows/macos等多种平台
可以实现java层和native层的函数级hook,以及native层的指令级patch等操作
一般在工作机上安装frida,编写脚本后使用命令行工具,启动frida并将脚本注入到靶机指定进程
同时需要在靶机上安装相同版本的frida-server用于监听工作机传来的操作指令
二. Frida环境配置
-
安装frida
参考下方的frida对应关系表安装指定版本的frida和frida-tools
建议使用anaconda的python虚拟环境,针对不同靶机可能需要多个版本frida
不同版本的frida对python版本要求不同且不能共存
1
2
3
4
|
pip install frida
pip install frida-tools
#可以通过frida=version的操作安装指定版本
#例如pip install frida=12.8.0
|
-
配置开发环境
下载WebStorm https://www.jetbrains.com/zh-cn/webstorm/download js IDE
下载Node.js https://nodejs.org/zh-cn/download/prebuilt-binaries js解释器
frida代码补全提示配置:
创建项目后,在根目录打开cmd ,执行以下代码后安装代码补全包自动开启
frida对应关系表
Android版本 |
frida版本 |
frida-tools版本 |
python版本 |
objection版本 |
android5-6 |
frida12.3.6 |
|
python=3.7 |
|
andorid7-8 |
frida12.8.0 |
frida-tools=5.3.0 |
python=3.8 |
objection=1.8.4 |
andorid9-10 |
frida14.2.14 |
frida-tools=9.2.2 |
python=3.8 |
objection=1.11.0 |
andorid10-12 |
frida15.2.2 |
frida-tools=11.0.0 |
python=3.9/3.10 |
|
android10-12 |
frida15+ |
|
python=3.9/3.10 |
|
android12+ |
frida16.0.19 |
|
python=3.10/3.11 |
|
frida12 最好在<=安卓8.1的版本上用,最好别在>=安卓10上用
frida14 最好在>=安卓10的版本上用,别在<=8.1的版本上用
三. Frida常用命令
frida命令参数
-U: 指定USB设备
-D: 指定id设备
-R: 远程frida-server
-H: 指定远程frida-server地址 ip:port
-f: swapn模式
-p: attach的目标pid
-n: attach的目标名称
-F: attach模式(默认)
-l: 加载脚本
-o: 输出日志到指定文件
–no-pause: spawn启动app时不挂起,直接运行
列出手机进程信息
-U: 指定USB设备
-a: 列出所有进程
-i: 显示进程详细信息
frida注入进程
附加注入进程 直接注入后可使用控制台调用frida api进行交互
1
|
frida -U <package_name>
|
附加注入进程并加载脚本
1
|
frida -U <package_name> -l <script.js>
|
swapn注入进程并加载脚本
1
|
frida -U -f <package_name> -l <script.js>
|
四. Frida-labs
用于入门熟悉frida的基本hook操作,包括java和native层hook
参考https://github.com/DERE-ad2001/Frida-labs和[原创]Frida-Hook-Java层操作大全
lab1 Overload&Implement Method
hook重载函数并实现
1
2
3
4
5
6
7
8
9
10
|
function main(){
Java.perform(function (){
var mainActivity=Java.use("com.ad2001.frida0x1.MainActivity");
mainActivity.check.overload("int","int").implementation=function (x,y){
console.log("Origin i=",x,"i2=",y)
this.check(2,8)
}
})
}
setTimeout(main,500)
|
lab2 CallStaticMethod
MainActivity的静态方法get_flag(),当参数a==4919时getflag

获取MainActivity类,主动调用get_flag()即可
1
2
3
4
5
6
7
|
function main(){
Java.perform(function (){
var mainActivity=Java.use("com.ad2001.frida0x2.MainActivity");
mainActivity.get_flag(4919)
})
}
setTimeout(main,500)
|
lab3 ModifyStaticValue
当Checker.code==512时getflag

Checker.code为静态变量

获取Checker类,通过.value直接修改静态变量code的值即可
1
2
3
4
5
6
7
8
|
function main(){
Java.perform(function (){
var Checker=Java.use("com.ad2001.frida0x3.Checker")
console.log(Checker)
Checker.code.value=512
})
}
setTimeout(main,500)
|
lab4 CallInstanceMethod
MainActivity没有getflag逻辑,在Check类中有实例方法get_flag

获取Check类并调用new方法创建实例,之后调用get_flag
1
2
3
4
5
6
7
8
9
10
11
|
function main(){
Java.perform(function (){
var Check=Java.use("com.ad2001.frida0x4.Check")
console.log(Check)
var instance=Check.$new()
console.log(instance)
var flag=instance.get_flag(1337)
console.log(flag)
})
}
setTimeout(main,500)
|
lab5 FindInstance
MainActivity的实例方法flag

在app启动时会创建MainActivity实例,此时我们如果使用Java.use获取MainActivity类并调用new创建实例,会导致错误
因为Android的UI组件依赖程序上下文运行,并且已经和MainActivity实例绑定,再次创建实例显然不行
通过Java.choose搜索实例对象,之后通过onMatch()回调进行hook逻辑,onComplete()进行结束处理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
function main(){
Java.perform(function (){
Java.choose("com.ad2001.frida0x5.MainActivity",{
onMatch: function (instance){
console.log(instance);
instance.flag(1337)
},
onComplete: function (){
console.log("unhook")
}
})
})
}
setTimeout(main,500)
|
lab6 NewInstance
MainActivity.get_flag()实例方法要求传入一个Checker实例对象
当实例对象的num1==1234且num2==4321时获取flag

Checker.num1/num2均为实例变量

搜索MainActivity实例,之后创建Checker实例修改num变量,调用MainActivity.get_flag()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
function getclass(className) {
return Java.use(className);
}
function main(){
Java.perform(function (){
Java.choose("com.ad2001.frida0x6.MainActivity",{
onMatch: function (instance){
console.log("hook success")
console.log(instance);
var Checker=getclass("com.ad2001.frida0x6.Checker").$new()
console.log(Checker)
Checker.num1.value=1234
Checker.num2.value=4321
instance.get_flag(Checker)
},
onComplete: function (){
console.log("unhook")
}
})
})
}
setTimeout(main,500)
|
lab7 HookConstructor
MainActivity创建时生成Checker实例对象,通过构造函数传递123,321,调用flag方法

Checker对象默认的构造函数如下

显然默认情况下构造函数传参不满足getflag条件
frida中<class>.$init表示类的构造函数,我们可以hookChecker的构造函数,修改传入的a,b值
1
2
3
4
5
6
7
8
9
|
function main(){
Java.perform(function (){
Java.use("com.ad2001.frida0x7.Checker").$init.implementation=function (a,b){
console.log("Origin a=",a," b=",b)
this.$init(600,600)
}
})
}
setImmediate(main)
|
注意需要以spawn模式启动(保证在MainActivity.onCreate执行前hook)
lab8 HookNaticeFunc
关键函数cmpstr为Native函数,在libfrida0x8.so中定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
function main(){
//获取libc的strcmp地址
var strcmp_addr=Module.findExportByName("libc.so","strcmp");
Interceptor.attach(strcmp_addr ,{
onEnter: function (args){
var arg0=Memory.readUtf8String(args[0]);//strcmp(str1,str2)
var flag=Memory.readUtf8String(args[1]);
//判断参数1确认为点击submit提交输入时,防止输出其他调用处字符串干扰
if(arg0.includes("hello")){
console.log("input: ",arg0,"\nflag: ",flag)
}
},
onLeave:function(retval){
}
})
}
setImmediate(main)
|
lab9 ReplaceNativeFuncRetval

hook到check_flag函数后通过retval.replace修改返回地址即可
1
2
3
4
5
6
7
8
9
10
11
12
13
|
function main(){
var check_flag_addr= Module.findExportByName("liba0x9.so","Java_com_ad2001_a0x9_MainActivity_check_1flag")
Interceptor.attach(check_flag_addr ,{
onEnter: function (){
console.log("Hook check_flag success ");
},
onLeave:function(retval){
console.log("origin retval=",retval);
retval.replace(1337)
}
})
}
setImmediate(main)
|
lab10 CallNativeFunc
MainActivity只能看到stringFromJNI,但跟进发现该函数只返回"Hello Hackers"

搜索发现libfrida0xa.so存在一个导出的get_flag函数,没有被调用

通过基址+偏移得到函数真实地址,再通过NativeFunction创建函数对象,之后直接调用即可再logcat看到flag
1
2
3
4
5
6
7
8
9
10
|
function main(){
//lib加载基址+get_flag偏移=真实地址
//get_flag的Native名称为_Z8get_flagii
var addr = Module.findBaseAddress("libfrida0xa.so").add(0x1dd60)
var get_flag_ptr = new NativePointer(addr);//Native层指针
//获取Native函数对象 参数分别为函数指针,返回值类型,参数类型列表
const get_flag = new NativeFunction(get_flag_ptr, 'void', ['int', 'int']);
get_flag(1,2);
}
setTimeout(main,500)
|
lab11 PatchNativeInstruction
调用Native函数getFlag

此处为永假跳转,patch掉if跳转逻辑即可绕过

获取到目标patch地址后,通过Memory.protect取消页面保护, 再创建对应架构的Writer,通过writer修改指令即可
方法1 直接nop b.ne
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
function main(){
var addr = Module.findBaseAddress("libfrida0xb.so").add(0x15248);//b.ne指令地址
Memory.protect(addr, 0x1000, "rwx");//修改页面保护属性为可读可写可执行
var writer = new Arm64Writer(addr); // 创建Writer对象
try {
writer.putNop()//直接nop b.ne跳转 注意arm64的nop=0xD503201F,长度为4,只需要调用一次即可
writer.flush();//刷新缓冲区,保证成功写入指令
console.log(`Nop instruction inserted at ${addr}`);
} finally {
writer.dispose();
}
}
setTimeout(main,500)
|
方法2 修改b.ne跳转地址
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
function main(){
var addr = Module.findBaseAddress("libfrida0xb.so").add(0x15248);//b.ne指令地址
Memory.protect(addr, 0x1000, "rwx");//修改页面保护属性为可读可写可执行
var writer = new Arm64Writer(addr); // 创建Writer对象
var target = Module.findBaseAddress("libfrida0xb.so").add(0x15250);//跳转的目标地址
try {
writer.putBImm(target); //B target
writer.flush();//刷新缓冲区,保证成功写入指令
console.log(`Branch instruction inserted at ${addr}`);
} finally {
writer.dispose();
}
}
setTimeout(main,500)
|
五. FridaHook进阶
Frida的数据结构
数组
frida会自动将js字符串转换为java字符串,但传递基本数据类型时,不会转换成封装类型,frida提供了api进行转换:
Java.array(“Ljava.lang.Object;”…)
1
2
3
4
5
6
7
8
9
10
11
|
function main(){
Java.perform(function (){
var Integer=Java.use("java.lang.Integer");
var Boolean=Java.use("java.lang.Boolean");
var strarr=Java.array(
"Ljava.lang.String;",
["hello",Boolean.$new(true),Integer.$new(666)]
)
})
}
setTimeout(main,500)
|
打印/枚举操作
Show StackTrace
1
2
3
4
5
6
7
8
9
10
|
function showStacks(){
console.log("====================StackTraceStart====================");
console.log(
Java.use("android.util.Log")
.getStackTraceString(
Java.use("java.lang.Throwable").$new()
)
);
console.log("=====================StackTraceEnd=====================");
}
|
枚举类的所有属性和方法
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
|
function main(){
Java.perform(function (){
var mainActivity=Java.use("com.ad2001.frida0x6.MainActivity")
//以下均为Java.class自带的方法
var methods=mainActivity.class.getDeclaredMethods(); //方法
var constructors=mainActivity.class.getDeclaredConstructors(); //构造函数
var fields=mainActivity.class.getDeclaredFields(); //字段
var classes=mainActivity.class.getDeclaredClasses(); //内部类
console.log("\n==========methods==========");
for(let i=0;i<methods.length;i++){
console.log(methods[i].getName());
}
console.log("\n==========constructors==========");
for(let i=0;i<constructors.length;i++){
console.log(constructors[i].getName());
}
console.log("\n==========fields==========");
for(let i=0;i<fields.length;i++){
console.log(fields[i].getName());
}
console.log("\n==========classes==========");
for(let i=0;i<classes.length;i++){
console.log(classes[i].getName());
}
})
}
setTimeout(main,500)
|
打印类/实例的详细信息
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
|
function printClassMethods(Class){
var methods=Class.class.getDeclaredMethods(); //方法
console.log("\n==========methods==========");
for(var i=0;i<methods.length;i++){
var methodParams=methods[i].getParameterTypes()
var methodReturnType=methods[i].getReturnType()
var methodName=methods[i].getName();
console.log(methodReturnType+" "+methodName+"("+methodParams+")");
}
}
function printClassFields(Class){
var fields=Class.class.getDeclaredFields(); //字段
console.log("\n==========fields==========");
for(var i=0;i<fields.length;i++){
var fieldName=fields[i].getName();
var fieldType=fields[i].getType();
console.log(fieldType,fieldName);
}
}
function printInstanceMethods(Instance){
printClassMethods(Instance)
}
function printInstanceFields(Instance){
var fields=Instance.class.getDeclaredFields(); //字段
console.log("\n==========fields==========");
for(var i=0;i<fields.length;i++){
var fieldName=fields[i].getName();
var fieldType=fields[i].getType();
var fieldValue=Instance[fieldName].value;
console.log(fieldType,fieldName,"=",fieldValue);
}
}
//打印实例详细信息
function printInstance(Instance){
printInstanceFields(Instance)
printInstanceMethods(Instance)
}
//打印类的详细信息
function printClass(Class){
printClassFields(Class)
printClassMethods(Class)
}
|
枚举所有已加载类
1
2
3
4
5
6
7
|
function main(){
Java.perform(function (){
var classes=Java.enumerateLoadedClassesSync()//string[]数组
console.log(classes.join("\n"))//每个元素添加换行打印
})
}
setTimeout(main,500)
|
Hook Java类/方法
hook内部类和匿名类
内部类通过$classname, 匿名类通过$classnum, classnum编号和匿名类的创建顺序有关,例如:
1
2
3
4
5
6
7
|
function main(){
Java.perform(function (){
var innerClass=Java.use("com.example.app.MainActivity$InnerClass")
var anonymousClass=Java.use("com.example.app.MainActivity$6")
})
}
setTimeout(main,500)
|
hook类的所有方法
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
26
|
function main(){
Java.perform(function (){
function hookFunc(methodName){
console.log("hook"+methodName);
let overloadsArr=mainActivity[methodName].overloads;//获取指定方法的所有重载
for(let i=0;i<overloadsArr.length;i++){//hook所有重载
console.log("Hook all overloads start")
overloadsArr[i].implementation=function(){
let params="";
for(let j=0;j<arguments.length;j++){
params+=arguments[j]+" "//存储参数
}
console.log(methodName+"is called!Params is:"+params);
return this[methodName].apply(this, params);//针对不同重载返回所有参数
}
}
}
var mainActivity=Java.use("com.ad2001.frida0x6.MainActivity")
var methods=mainActivity.class.getDeclaredMethods(); //方法
for(let i=0;i<methods.length;i++){
let methodName=methods[i].getName();
hookFunc(methodName)
}
})
}
setImmediate(main)
|
注册java类并调用
Java.registerClass
参照官方文档
使用JSON格式创建类,其中一般方法可以直接写代码,重载方法以数组形式分别编写implementation
注册成功后便可以调用,该api适合简单场景,复杂场景适合注入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
|
const MyWeirdTrustManager = Java.registerClass({
name: 'com.example.MyWeirdTrustManager',
superClass: SomeBaseClass,
implements: [X509TrustManager],
fields: {
description: 'java.lang.String',
limit: 'int',
},
methods: {
$init() {
console.log('Constructor called');
},
checkClientTrusted(chain, authType) {
console.log('checkClientTrusted');
},
checkServerTrusted: [{
returnType: 'void',
argumentTypes: ['[Ljava.security.cert.X509Certificate;', 'java.lang.String'],
implementation(chain, authType) {
console.log('checkServerTrusted A');
}
}, {
returnType: 'java.util.List',
argumentTypes: ['[Ljava.security.cert.X509Certificate;', 'java.lang.String', 'java.lang.String'],
implementation(chain, authType, host) {
console.log('checkServerTrusted B');
return null;
}
}],
getAcceptedIssuers() {
console.log('getAcceptedIssuers');
return [];
},
}
});
|
hook只在指定函数内生效
部分函数调用可能比较频繁,hook后可能导致崩溃,此时需要再制定范围内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
|
function main(){
Java.perform(function (){
var mainActivity=Java.use("com.xiaojianbang.app.MainActivity");
var stringBuilder=Java.use("java.lang.StringBuilder");
mainActivity.generateAESKey.implementation=function (){
console.log("Hook MainActivity generateAESKey Succeeded!");
//在执行generateAESKey时,对StringBuilder进行hook
stringBuilder.toString.implementation=function (){
console.log("Hook StringBuilder Succeeded!")
var result=this.toString()
console.log(result)//打印原始数据
return result;
}
var result=this.generateAESKey.apply(this,arguments);//js function.apply(object,argArray) 调用函数
//结束后取消hook 注意一定要先apply调用hook后的generateAESKey
//如果unhook stringBuilder 再执行result=this.generateAESKey.apply(this,arguments);
//会导致内部对stringBuilder的hook失效,只执行第一行log
stringBuilder.toString.implementation=null;//unhook
return result;
};
})
}
setTimeout(main,500)
|
定位接口/抽象类的实现类
实现接口/抽象类的类,执行到覆写的方法时必定不走父类
当我们希望定位某个接口/抽象类有哪些实现时,可以遍历所有已加载类
获取对应class后调用getInterfaces()/getSuperclass()获取对应的实现接口数组/父类
接口
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
|
//寻找指定app中指定接口的所有实现
function findInterfaceImplementations(packageName,interfaceName){
var classes=Java.enumerateLoadedClassesSync();//遍历所有已加载的类
//console.log("classes:",classes.join("\n"))
for(var index in classes){
var className=classes[index];//js的for-in中,index为下标而非元素
if(className.indexOf(packageName)===-1)//不是目标app的类,继续搜索
continue;
try{
var clazz=Java.use(className)//获取目标类,并获取所有实现的接口
var interfaces=clazz.class.getInterfaces();
if(interfaces.length===0)//实现的接口为0,跳过
continue;
//有实现的接口,判断是否为目标接口
for(var i=0;i<interfaces.length;i++){
//包括目标接口名,打印所有接口数组信息
//注意interfaces为Class<?>[]类型,转换为字符串判断
if(interfaces[i].toString().indexOf(interfaceName)!==-1){
console.log(className,interfaces);
}
}
}catch(e){
console.log("Didn't find class",className);
}
}
}
function main(){
Java.perform(function (){
findInterfaceImplementations("com.xiaojianbang.app","com.xiaojianbang.app.TestRegisterClass");
})
}
setTimeout(main,500)
|
抽象类
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
|
function findAbstractClassImplementations(packageName,abstractClassName){
var classes=Java.enumerateLoadedClassesSync();
for(var index in classes){
var className=classes[index];
if(className.indexOf(packageName)===-1)
continue;
try {
var superClass=Java.use(className).class.getSuperclass();
if(superClass==null)
continue;
//一个类只能继承一个父类,故抽象类无需遍历
if(superClass.toString().indexOf(abstractClassName)!==-1){
console.log(className,superClass)
}
}catch (e){
console.log("Didn't find class",className);
}
}
}
function main(){
Java.perform(function (){
findAbstractClassImplementations("com.xiaojianbang.app","com.xiaojianbang.app.TestAbstract");
})
}
setTimeout(main,500)
|
Dex操作
注入dex文件
用js写代码不方便,使用java编写类和方法后编译为dex文件并注入,方便调用
1
2
3
4
5
6
7
8
9
|
function main(){
Java.perform(function (){
Java.openFile("/data/local/tmp/patch.dex").load()//加载dex文件
var MyClass=Java.use("con.example.app.MyClass")//获取类
var instance=MyClass.$new()//创建类实例
instance.hello()//调用方法
})
}
setTimeout(main,500)
|
hook动态加载的dex
当使用Java.enumerateLoadedClassesSync() 枚举已经加载的类,但反编译找不到该类时,说明该类可能是动态加载的,此时需要通过ClassLoader获取该类
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
|
function main(){
Java.perform(function (){
Java.enumerateClassLoaders({
onMatch: function (loader){
try{
Java.classFactory.loader=loader;//替换frida默认的classloader
var dynamic=Java.use("com.xiaojianbang.app.Dynamic");
console.log("dynamic: ",dynamic);
dynamic.sayHello.implementation=function (){
console.log("Hook dynamic.sayHello Success!");
return "glass"
}
dynamic.$new().sayHello()
}
catch(e){
console.log(loader)
}
},
onComplete: function (){
//所有ClassLoader加载完毕时调用
}
})
})
}
setTimeout(main,500)
|
hook成功

Hook Java数据结构(待完善)
HashMap
HashMap, LinkedHashMap, HashSet, LinkedHashSet, java.util.concurrent.ConcurrentHashMap
hook put方法示例
1
2
3
4
5
6
7
8
9
10
11
12
13
|
function main(){
Java.perform(function (){
var hashMap=Java.use("java.util.HashMap");
hashMap.put.implementation=function (key,value){
//注意String key为java类型
if(key.equals("username")){
console.log("hashMap.put: ",key,value);
}
return this.put(key,value);
}
})
}
setTimeout(main,500)
|
ArrayList
ArrayList, Vector, LinkList的add(), addAll(), set()等方法
String
getBytes() isEmpty() 构造函数java.lang.StringFactory
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
function main(){
Java.perform(function (){
var string=Java.use("java.lang.String");
string.getBytes.overload().implementation=function (){
showStacks()
var result=this.getBytes();
var newStr=string.$new(result)
console.log("string.getBytes result:",newStr);
return result;
}
string.getBytes.overload("java.lang.String").implementation=function (str){
showStacks();
var result=this.getBytes(str);
var newStr=string.$new(result,str)
console.log("string.getBytes result:",newStr);
return result;
}
})
}
setTimeout(main,500)
|
StringFactory
String的构造函数实际上调用的是StringFactory
StringBuilder
Hook 标准加密库(待完善)
标准加密库算法
Hook Android组件(待完善)
Toast
例如输入的账号密码错误时通过toast提示即可快速定位
Native Hook
通过Interceptor.attach可以hooknative层函数,不过此时需要传递函数首地址
1
2
3
4
5
6
7
8
|
Interceptor.attach(targetAddress, {
onEnter: function (args) {
// Modify or log arguments if needed
},
onLeave: function (retval) {
// Modify or log return value if needed
}
});
|
相关API
-
Module.enumerateExports(libname)
遍历lib的导出表,获取导出函数相关信息

-
Module.enumerateImports(libname)
遍历lib的导入表,获取导入函数相关信息

-
Module.getExportByName(libname,funcname)
到指定lib查找指定函数地址,未找到返回异常
-
Module.findExportByName(libname,funcname)
到指定lib查找指定函数地址,未找到返回null
-
Module.getBaseAddress(libname)
获取lib的基址

主动调用Native函数
1
2
3
|
var native_adr = new NativePointer(<address_of_the_native_function>);
const native_function = new NativeFunction(native_adr, '<return type>', ['argument_data_type']);
native_function(<arguments>);
|
指令patch 使用Writer
https://frida.re/docs/javascript-api/#arm64writer
其他操作
frida写文件
frida只有写文件没有读文件
1
2
3
4
5
6
7
8
9
10
|
function main(){
Java.perform(function (){
var ios=new File("/sdcard/Download/helloworld.txt","w")//注意路径权限的问题
ios.write("Hello World!")
ios.flush()
ios.close()
})
}
setTimeout(main,500)
|
frida常用工具和插件的安装使用方法
Objection
由于frida更新较快,需要保证objection版本的发布时间在frida之后
最新的objection版本为1.11.0,对应的frida版本最大为14.2.14,frida-tools为9.2.2
objection注入成功后进入自带的shell页面-REPL
objection常用指令
注入目标进程(默认通过usb连接)
1
2
|
objection -g com.android.settings explore
# -g表示注入 explore表示自动搜索
|
指定ip和端口注入app
1
|
objection -N -h ip -p port -g processName explore
|
启动前执行hook命令
1
|
objection -g processName explore --startup-command "android hooking watch class <path.className>"
|
REPL常用指令
jobs
1
2
|
jobs list #查看hook了多少个类
jobs kill <jobId> # unhook
|
Java Hook
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
#列出所有已加载类
android hooking list classes
#搜索包含关键字的类
android hooking search classes <keyword>
#列举类的所有方法
android hooking list classes_methods <path.className>
# 查看当前app的所有活动/广播接收器/内容提供器/服务
android hooking list activities/receivers/providers/services
#hook类的所有方法(不包括构造方法)
android hooking watch class <path.className>
#hook类的构造方法
android hooking watch class_method <path.className.$init>
#hook指定类方法的所有重载
android hooking watch class_method <path.className.methodName>
#hook单个重载函数
android hooking watch class_method <path.className.methodName> "<argsType>"
# hook方法的所有参数,返回值,调用栈 --dump-args --dump-return --dump-backtrace
|
Native Hook
1
2
3
4
5
6
7
8
|
# 搜索堆中的实例
android heap search instances <className>
# 枚举内存中所有模块
memory list modules
# 枚举模块的所有导出函数
memory list exports <libname.so>
# 结果过多时可以导出到本地文件
memory list exports <libname.so> --json <path.fileName>
|
其他命令
1
2
3
4
5
6
|
# 尝试跳转到对应activity
android intent launch_activity
#关闭ssl校验
android sslpinning disable
#关闭root检测
android root disable
|
WallBreaker
官网https://github.com/hluwa/Wallbreaker
objection加载插件
1
|
plugin load pluginPath Wallbreaker
|
搜索类
1
|
plugin wallbreaker classsearch <pattern>
|
搜索对象
1
|
plugin wallbreaker objectsearch
|
classdump输出类结构
1
|
plugin wallbreaker classdump <classname>
|
objectdump
1
|
plugin wallbreaker objectdump <instance>
|
frida-dexdump
七. Frida持久化
-
手机上使用termux终端运行frida-server
-
frida-inject
-
frida-gadget.so
优点: 免root使用frida,frida-gadget比较稳定
缺点: 需要重打包app.可以通过魔改系统,让系统帮忙注入so免去重打包
frida-inject
参数
1
2
3
4
5
6
7
8
9
10
11
12
|
-D, --device=ID connect to device with the given ID
-f, --file=FILE spawn FILE
-p, --pid=PID attach to PID
-n, --name=NAME attach to NAME
-r, --realm=REALM attach in REALM
-s, --script=JAVASCRIPT_FILENAME
-R, --runtime=qjs|v8 Script runtime to use
-P, --parameters=PARAMETERS_JSON Parameters as JSON, same as Gadget
-e, --eternalize Eternalize script and exit
-i, --interactive Interact with script through stdin
--development Enable development mode
--version Output version information and exit
|
推送js脚本到手机端后,使用frida-inject运行脚本注入指定app
1
|
./frida-inject-16.0.19-android-arm64 -n com.example.app -s script.js -e
|
frida-gadget
https://github.com/frida/frida/releases?page=4
frida-gadget需要将so文件打包到apk,无需root权限
objection patchapk 参数介绍
1
2
3
4
5
|
-s 指定目标apk文件
-a 指定so的cpu平台
-V 指定gadget.so的版本
-D 反编译时跳过资源
-c 指定gadget的config文件路径
|
环境需求
aapt
jarsigner
apktool,adb
Android系统对应so平台
八. FridaPython
为什么需要使用frida的python库
-
js的frida操作多用于手工调试,如果需要代码自动化处理则要其他语言接入
-
frida的算法转发和frida的rpc都需要使用python
算法转发和rpc能使安卓逆向更加便捷
-
frida可以实时和python进行数据交互
-
python的各种哭让代码编写更加简单
注意: python中的js脚本无法实时更新,必须重新运行py脚本
python连接设备
attach方式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
import frida,sys
jsCode="""
......
"""
device = frida.get_usb_device() #获取usb设备
print("device: ", device)
# attach指定进程 注意进程名不一定等于包名,可使用frida-ps判断
process=device.attach("Frida 0x6")
# process=device.attach(6666)# attach指定pid进程
print("process: ",process)
script=process.create_script(jsCode)# 创建并加载js脚本
script.load()
sys.stdin.readline()# 防止命令行关闭
|
spawn方式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
import frida,sys
jsCode="""
......
"""
device = frida.get_usb_device() #获取usb设备
print("device: ", device)
pid=device.spawn("com.ad2001.frida0x6")# spawn启动
print("pid: ", pid)
process=device.attach(pid) # 启动后需要附加
print("process: ",process)
device.resume(pid) #阻塞后恢复运行
script=process.create_script(jsCode)# 创建并执行js脚本
script.load()
sys.stdin.readline()# 防止命令行关闭
|
连接非标准端口设备
手机端以非标准端口启动frida-server
-l = –listen 指定监听地址 0.0.0.0:8888表示任意ip的8888端口
1
|
./data/local/tmp/frida-server-16.0.19-android-arm64 -l 0.0.0.0:8888
|
通过device_manager添加远程设备
1
2
3
|
manager=frida.get_device_manager()
device=manager.add_remote_device("192.168.189.179:8888")
process=device.attach("Frida 0x6")
|
frida与python交互
send
js脚本中使用send发送数据
python中使用script.on(‘message’, messageFunc)注册消息处理函数
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
|
# -*- coding: UTF-8 -*-
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;
}
});
"""
# 接收send发送来的数据用于处理 注意必须为2个参数
def messageFunc(message, data):
# print(message)
# 当type为send时打印
if message["type"] == 'send':
print(u"[*] {0}".format(message['payload']))
else:
print(message)
device = frida.get_usb_device()
process = device.attach('嘟嘟牛在线')
script = process.create_script(jsCode)
script.on('message', messageFunc) #注册message事件,参数1不能改,参数2随意
script.load()
print("Run Script")
sys.stdin.read()
|
效果如下,成功接收消息并打印

recv
python中使用script.post( {name : data} )发送数据
js中使用recv(function(obj) { console.log(obj.data) } ).wait()等待接收并进行消息处理
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
|
# -*- coding: UTF-8 -*-
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(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)
# 发送数据给js 此处的script是process.create_script后的变量script
script.post({"data": "0e8315152843b943563031945032e957"})
else:
print(message)
process = frida.get_usb_device().attach('嘟嘟牛在线')
script = process.create_script(jsCode)
script.on('message', messageFunc) #注册消息事件
script.load()
print("Run Script")
sys.stdin.read()
|
效果如下

frida RPC
RPC即远程调用
js代码中使用rpc.exports={
rpcfunc : jsfunc
};
导出rpc接口
python代码中使用script.exports_sync.rpcfunc()调用
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
|
# -*- coding: UTF-8 -*-
import frida, sys,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);
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;
}
//通过rpc.exports导出接口rpcFunc
rpc.exports = {
rpcfunc: test //rpc接口和js函数映射
};
"""
# 注意js导出的rpcFunc在python中的写法为rpc_func,即大写字母前使用下划线
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) # 加载完脚本, 恢复进程运行
time.sleep(1) # 休眠1秒,防止app未加载完毕就主动调用
# python主动调用rpc函数 通过script.exports_sync.rpcfunc()
result = script.exports_sync.rpcfunc('equtype=ANDROID&loginImei=Androidnull&timeStamp=1626790668522&userPwd=a12345678&username=15968079477&key=sdlkjsdljf0j2fsjk')
print(result)
print("Run Script")
sys.stdin.read()
|
效果如下

frida 算法转发
有时我们可能想用其他语言实现rpc,但是其他语言不一定有frida的rpc库,我们可以把python作为中间层,用python开启一个服务,封装并提供接口,这样就可以让别的语言访问
首先需要两个库运行web服务
1
2
|
pip install uvicorn
pip install fastapi
|
代码如下
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
|
import requests, json
import frida
import uvicorn
import fastapi
jsCode="""
function hookTest(username, passward){
var result;
Java.perform(function(){
var time = new Date().getTime();
var signData = 'equtype=ANDROID&loginImei=Android352689082129358&timeStamp=' +
time + '&userPwd=' + passward + '&username=' + username + '&key=sdlkjsdljf0j2fsjk';
var Utils = Java.use('com.dodonew.online.util.Utils');
var sign = Utils.md5(signData).toUpperCase();
console.log('sign: ', sign);
var encryptData = '{"equtype":"ANDROID","loginImei":"Android352689082129358","sign":"'+
sign +'","timeStamp":"'+ time +'","userPwd":"' + passward + '","username":"' + username + '"}';
var RequestUtil = Java.use('com.dodonew.online.http.RequestUtil');
var Encrypt = RequestUtil.encodeDesMap(encryptData, '65102933', '32028092');
console.log('Encrypt: ', Encrypt);
result = Encrypt;
});
return result;
}
rpc.exports = {
rpcfunc: hookTest
};
"""
process = frida.get_device_manager().add_remote_device('192.168.189.179:27042').attach("嘟嘟牛在线")
script = process.create_script(jsCode)
print('[*] Running Script')
script.load()
cipherText = script.exports_sync.rpcfunc('12345678901', 'a123456789')# 测试脚本是否成功运行
print(cipherText)
# 开启web服务处理请求
app = fastapi.FastAPI()# 创建FastAPI实例
@app.get("/get")# 定义一个get请求的路由,收到get请求后执行下面的异步函数
async def getEchoApi(item_id, item_user, item_pass):
result = script.exports_sync.rpcfunc(item_user, item_pass) # 处理用户名和密码后返回结果
return {"item_id": item_id, "item_retval": result}
if __name__ == '__main__':
uvicorn.run(app, port = 8080)#使用uvicorn运行FastAPI引用,在8080端口监听等待http请求
|
效果如下
成功开启服务

发起正确的get请求即可收到返回结果

九. Frida逆向实战
过vpn检测
课件 屿秋-检测代理和root.apk
可以发现弹出通知提示网络异常以及Toast提示安全证书异常
我们可以通过hook Toast定位异常检测点
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
function showStacks(){
console.log("====================StackTraceStart====================");
console.log(
Java.use("android.util.Log")
.getStackTraceString(
Java.use("java.lang.Throwable").$new()
)
);
console.log("=====================StackTraceEnd=====================");
}
function main(){
Java.perform(function (){
var toast=Java.use("android.widget.Toast");
toast.show.implementation=function(){
showStacks();
console.log("Toast.show: ");
return this.show();
}
})
}
setTimeout(main,500)
|
Toast栈回溯信息如下

不难看出可能和以下两个类有关
1
2
|
com.jtxc.common.viewmodel.BaseViewModel.b
com.jtxc.common.viewmodel.HomeViewModel$2.accept
|
但jadx静态分析时无法发现这两个类,说明可能有加壳,查壳发现是腾讯加固
破解极简记账VIP
十. Frida API
Java Bridge
https://frida.re/docs/javascript-api/#java
-
Java.androidVersion 返回Android版本
-
Java.enumerateLoadedClasses(callbacks)
遍历已经加载的类,callbacks如下
1
2
3
4
|
callbacks{
onMatch(name,handle), //可用Java.cast()将handle转换为指定class
onComplete()
}
|
-
Java.enumerateClassLoaders(callbacks)
遍历java vm中的classloader,callback如下
1
2
3
4
|
classback{
onMatch(loader), //java.lang.ClassLoader
onComplete()
}
|
-
Java.enumerateMethods(query)
查询匹配条件的方法
用法如下
1
2
3
4
5
6
7
8
|
function main(){
Java.perform(function (){
const groups=Java.enumerateMethods("<class>!<method>/isu");
//such as query="*youtube*!on*/is"
console.log(JSON.stringify(groups,null,2));
})
}
setTimeout(main,500)
|
支持通配符*用于关键字搜索
/后跟参数
i: 大小写不敏感
s: 包括方法签名信息
u: 忽略系统类
-
Java.perform(function)
确保在主线程中执行函数
-
Java.use(className)
获取Java类
-
Java.openClassFile(filePath)
打开.dex文件,返回类有load()和getClassNames()方法
-
Java.choose(className, callbacks)
遍历实例,callbacks={onMatch(instance),onComplete()}
-
Java.cast(handle, klass)
类型转换
-
Java.array(type, elements)
创建指定类型的数组
-
Java.vm 对象
getEnv()获取JNIEnv
perform(function)执行函数
-
Java.isMainThread()
检测是否运行在主线程
Memory
https://frida.re/docs/javascript-api/#memory
-
Memory.scan(address, size, pattern, callbacks)
在指定范围内扫描指定匹配串,callbacks{onMatch(address,size),onComplete()}
-
Memory.alloc(size[, options])
申请指定大小空间,返回NativePointer
-
Memory.copy(dst, src, n)
-
Memory.protect(address, size, protection)
修改内存保护属性
NativePointer
https://frida.re/docs/javascript-api/#nativepointer
-
new NativePointer(str)
创建指针指向str描述的地址处,可用简写形式ptr(str)
-
.isNull()
判断是否为空指针
-
add(rhs),sub(rhs),and(rhs),or(rhs),xor(rhs)
对NativePointer执行对应运算并返回新的NativePointer
rhs: 可以是数字或者NativePointer
-
shr(n),shl(n)
右移/左移指定bits
-
toString([radix = 16])
转换为指定进制字符串(默认hex)
-
readPointer()/writePointer(ptr)
向该指针指向的内存区域读/写一个指针
-
readS8()
, readU8()
, readS16()
, readU16()
, readS32()
, readU32()
, readShort()
, readUShort()
, readInt()
, readUInt()
, readFloat()
, readDouble()
, readS64()
, readU64()
, readLong()
, readULong()
读取指定类型的数据
-
writeS8(value)
, writeU8(value)
, writeS16(value)
, writeU16(value)
, writeS32(value)
, writeU32(value)
, writeShort(value)
, writeUShort(value)
, writeInt(value)
, writeUInt(value)
, writeFloat(value)
, writeDouble(value)
, writeS64(value)
, writeU64(value)
, writeLong(value)
, writeULong(value)
写入指定类型数据
-
readByteArray(length)/writeByteArray(bytes)
读/写字节数组
-
readCString([size = -1])
, readUtf8String([size = -1])
, readUtf16String([length = -1])
读取字节流并转换为ASCII/UTF-8/UTF-16字符串
-
writeUtf8String(str)
, writeUtf16String(str)
写入字符串
Module
https://frida.re/docs/javascript-api/#module
-
enumerateImports(libname)
-
enumerateExports(libname)
-
enumerateSymbols(libname)
-
enumerateRanges(protection)
-
enumerateSections(libname)
-
enumerateDependencies(libname)
-
findExportByName(exportName)
-
getExportByName(exportName)
-
Module.load(path)
加载指定模块
-
Module.findBaseAddress(name)/Module.getBaseAddress(name)
未找到时,find返回null,get返回exception
-
Module.findExportByName(moduleName|null, exportName)
Module.getExportByName(moduleName|null, exportName)
返回指定模块导出函数的绝对地址
Debug Symbol
DebugSymbol.fromAddress(address)/DebugSymbol.fromName(name)
官网: DebugSymbol
功能: 查找address/name对应的调试信息并作为对象返回
该对象包含:
1
2
3
4
5
|
address //该符号对应地址 作为NativePointer
name //符号名称
moudleName //拥有该符号的模块名称
fileName //拥有该符号的文件名
lineNumber //行号
|
Common API
https://frida.re/docs/javascript-api/#java
https://frida.re/docs/javascript-api/#interceptor
https://frida.re/docs/javascript-api/#instruction
https://frida.re/docs/javascript-api/#arm64writer
https://frida.re/docs/javascript-api/#kernel
Table Of API Contents
- Runtime information
- Frida
- Script
- Process, Thread, Module and Memory
- Thread
- Process
- Module
- ModuleMap
- Memory
- MemoryAccessMonitor
- CModule
- ApiResolver
- DebugSymbol
- Kernel
- Data Types, Function and Callback
- Int64
- UInt64
- NativePointer
- ArrayBuffer
- NativeFunction
- NativeCallback
- SystemFunction
- Network
- Socket
- SocketListener
- SocketConnection
- File and Stream
- File
- IOStream
- InputStream
- OutputStream
- UnixInputStream
- UnixOutputStream
- Win32InputStream
- Win32OutputStream
- Database
- SqliteDatabase
- SqliteStatement
- Instrumentation
- Interceptor
- Stalker
- ObjC
- Java
- CPU Instruction
- Instruction
- X86Writer
- X86Relocator
- x86 enum types
- ArmWriter
- ArmRelocator
- ThumbWriter
- ThumbRelocator
- ARM enum types
- Arm64Writer
- Arm64Relocator
- AArch64 enum types
- MipsWriter
- MipsRelocator
- MIPS enum types
- Other
- Console
- Hexdump
- Shorthand
- Communication between host and injected process
- Timing events
- Garbage collection
References
Frida-Labs
零基础一站式安卓逆向2022