From:https://www.52pojie.cn/forum.php?mod=viewthread&tid=931872
Frida 文档:https://frida.re/docs/home/
Frida 从入门到入门 --- 安卓逆向菜鸟的 frida 使用说明:https://bbs.pediy.com/thread-226846.htm
frida入门总结:https://www.52pojie.cn/thread-1128884-1-1.html
*****************************************************************************************
https://bbs.pediy.com/user-811277.htm
初识Frida--Android逆向之Java层hook (一)
初识Frida--Android逆向之Java层hook (二)
进阶Frida--Android逆向之动态加载dex Hook(三)(上篇)
进阶Frida--Android逆向之动态加载dex Hook(三)(下篇)
*****************************************************************************************
frida分客户端环境和服务端环境。在客户端我们可以编写Python代码,用于连接远程设备,提交要注入的代码到远程,接受服务端的发来的消息等。在服务端,我们需要用Javascript代码注入到目标进程,操作内存数据,给客户端发送消息等操作。我们也可以把客户端理解成控制端,服务端理解成被控端。
假如我们要用PC来对Android设备上的某个进程进行操作,那么PC就是客户端,而Android设备就是服务端。
本文,服务端在Android平台测试。服务端环境准备步骤如下:
adb push frida-server /data/local/tmp/frida-server
adb shell chmod 777 /data/local/tmp/frida-server
注:Windows系统执行命令可以在CMD中进行;Linux和MacOS执行命令可以在终端中进行。adb是Android一个调试工具,具体安装方法不是本文的重点。
在PC上安装Python的运行环境,安装完成后执行下面的命令安装 frida:pip install frida
下面是 frida 客户端命令行的参数解释,看一下就好
如果将一个脚本注入到 Android 目标进程:frida -U -l myhook.js com.xxx.xxxx
参数解释:
frida-ps -U
命令查看frida 运行过程中,执行%resume
重新注入,执行%reload
来重新加载脚本;执行exit
结束脚本注入
Java.use方法用于声明一个Java类,在用一个Java类之前首先得声明。比如声明一个String类,要指定完整的类名:
var StringClass=Java.use("java.lang.String");
修改一个函数的实现是逆向调试中相当有用的。修改一个函数的实现后,如果这个函数被调用,我们的Javascript代码里的函数实现也会被调用。
不同的参数类型都有自己的表示方法
1. 对于基本类型,直接用它在Java中的表示方法就可以了,不用改变,例如:
2. 基本类型数组,用左中括号接上基本类型的缩写
基本类型缩写表示表:
例如:int[]
类型,在重载时要写成 [I
任意类,直接写完整类名即可。例如:java.lang.String
对象数组,用左中括号接上完整类名再接上分号。例如:[java.lang.String;
修改参数为 byte[] 类型的构造函数的实现
ClassName.$init.overload('[B').implementation=function(param){
//do something
}
注:ClassName是使用Java.use定义的类; param是可以在函数体中访问的参数
改多参数的构造函数的实现
ClassName.$init.overload('[B','int','int').implementation=function(param1,param2,param3){
//do something
}
ClassName.$init.overload().implementation=function(){
//do something
}
调用原构造函数
ClassName.$init.overload().implementation=function(){
//do something
this.$init();
//do something
}
注意:当构造函数(函数)有多种重载形式,比如一个类中有两个形式的func:
void func()
和void func(int)
,要加上overload来对函数进行重载,否则可以省略overload
修改函数名为func,参数为byte[]类型的函数的实现
ClassName.func.overload('[B').implementation=function(param){
//do something
//return ...
}
ClassName.func.overload().implementation=function(){
//do something
}
注: 在修改函数实现时,如果原函数有返回值,那么我们在实现时也要返回合适的值
ClassName.func.overload().implementation=function(){
//do something
return this.func();
}
和 Java 一样,创建类实例就是调用构造函数,而在这里用$new
表示一个构造函数。
var ClassName=Java.use("com.luoye.test.ClassName");
var instance = ClassName.$new();
实例化以后调用其他函数
var ClassName=Java.use("com.luoye.test.ClassName");
var instance = ClassName.$new();
instance.func();
用Java.cast
方法来对一个对象进行类型转换,如将variable
转换成java.lang.String
:
var StringClass=Java.use("java.lang.String");
var NewTypeClass=Java.cast(variable,StringClass);
这个字段标记Java虚拟机(例如: Dalvik 或者 ART)是否已加载, 操作Java任何东西的之前,要确认这个值是否为true
Java.perform(fn)在Javascript代码成功被附加到目标进程时调用,我们核心的代码要在里面写。格式:
Java.perform(function(){
//do something...
});
有了以上的基础知识,我们就可以进行编写代码了
假设有以下的程序,给isExcellent方法传入两个值,通过计算,返回一个布尔值,表示是否优秀。默认情况下,它是只会显示是否优秀:false
的,因为我们默认传入的数很小:
Java 代码:
public class MainActivity extends AppCompatActivity {
private String TAG="Crackme";
private TextView textView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
textView =findViewById(R.id.tv);
textView.setText("是否优秀:"+isExcellent(46,54));
}
private boolean isExcellent(int chinese, int math){
if( chinese + math >=180){
return true;
}
else{
return false;
}
}
}
我们编写一个脚本来Hook isExcellent函数,使它返回true,显示为是否优秀:true
对于这种简单的场景,直接修改返回值就可以了,因为只有结果是重要的。
想直接返回结果很简单,直接在匿名方法里return即可。
if(Java.available){
Java.perform(function(){
var MainActivity = Java.use("com.luoyesiqiu.crackme.MainActivity");
MainActivity.isExcellent.implementation=function(){
return true;
}
});
}
将上面的代码保存为:exp1.js
执行adb shell 'su -c /data/local/tmp/frida-server'
启动服务端
运行目标App
执行frida -U -l exp1.js com.luoyesiqiu.crackme
注入代码
按返回键返回桌面,再重新打开App,发现达到预期
在命令行输入exit
,回车,停止注入代码
注:这里为什么要打开两次App?第一打开是为了让frida能够找到进程,第二次打开是为了验证结果,即使Hook成功了,界面是有缓存的,并不能实时显示 Hook 结果,所以需要重新打开 App
假设有以下场景,isExcellent除了返回是否优秀以外,方法的内部还把分数打印出来。
Java 代码:
public class MainActivity extends AppCompatActivity {
private String TAG="Crackme";
private TextView textView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
textView =findViewById(R.id.tv);
textView.append("是否优秀:"+isExcellent(46,54)+"\n");
}
private boolean isExcellent(int chinese, int math){
textView.append("语文+数学总分:"+(chinese+math)+"\n");
if( chinese + math >=180){
return true;
}
else{
return false;
}
}
}
这种情况下我们不可能只返回是否优秀吧,显示的总分很低,但是却返回优秀,是很尴尬的...所以我们要修改isExcellent方法的参数,使其通过计算打印和返回合理的值。
if(Java.available){
Java.perform(function(){
var MainActivity = Java.use("com.luoyesiqiu.crackme.MainActivity");
MainActivity.isExcellent.overload("int","int").implementation=function(chinese,math){
return this.isExcellent(95,96);
}
});
}
上面的代码,通过overload方法重载参数,修改isExcellent方法实现,并在实现函数里调用原来的方法,得到新的返回值
将上面的代码保存为:exp2.js
执行adb shell 'su -c /data/local/tmp/frida-server'
启动服务端(如果上面启动的服务端还开着可省略这一步)
运行目标App
执行frida -U -l exp2.js com.luoyesiqiu.crackme
注入代码
按返回键,再重新打开App,发现达到预期
在命令行输入exit
,回车,停止注入代码
在本文刚开始的时候说到,我们可以编写 Python 代码来配合 Javascript 代码注入。
下面我们来看看,怎么使用,先看一段代码:
# -*- coding: UTF-8 -*-
import frida, sys
jscode = """
if(Java.available){
Java.perform(function(){
var MainActivity = Java.use("com.luoyesiqiu.crackme.MainActivity");
MainActivity.isExcellent.overload("int","int").implementation=function(chinese,math){
console.log("[javascript] isExcellent be called.");
send("isExcellent be called.");
return this.isExcellent(95,96);
}
});
}
"""
def on_message(message, data):
if message['type'] == 'send':
print(" {0}".format(message['payload']))
else:
print(message)
pass
# 查找USB设备并附加到目标进程
session = frida.get_usb_device().attach('com.luoyesiqiu.crackme')
# 在目标进程里创建脚本
script = session.create_script(jscode)
# 注册消息回调
script.on('message', on_message)
print(' Start attach')
# 加载创建好的javascript脚本
script.load()
# 读取系统输入
sys.stdin.read()
将上面的代码,保存为exp3.py
执行adb shell 'su -c /data/local/tmp/frida-server'
启动服务端(如果上面启动的服务端还开着可省略这一步)
运行目标App
执行python exp3.py
注入代码
按返回键,再重新打开App,发现达到预期
按Ctrl+C
停止脚本和停止注入代码
上面是一段 Python 代码,我们来分析它的步骤:
frida.get_usb_device()
方法来得到一个连接中的USB设备(Device类)实例attach()
方法来附加到目标进程并得到一个会话(Session类)实例,该方法有一个参数,就是需要注入的进程名create_script()
方法创建一个脚本,传入需要注入的javascript代码并得到Script类实例on()
方法添加一个消息回调,第一个参数是信号名,乖乖传入message
就行,第二个是回调函数load()
方法来加载刚才创建的脚本。注:如果想在javascript输出日志,可以调用
console.log()
方法。如果想给客户端发送消息,可以在 javascript 代码里调用send()
方法,并在客户端 Python 代码里注册一个消息回调来接收服务端发来的消息。
可以看到,结合python代码,使注入更加的灵活了。如果想看Python端frida模块的代码,可以访问:https://github.com/frida/frida-python/blob/master/frida/core.py
示例代码:
# -*- coding: UTF-8 -*-
import frida, sys
jscode = """
if(Java.available){
Java.perform(function(){
var MainActivity = Java.use("com.lanshifu.demo_module.ui.activity.DemoMainActivity");
MainActivity.testCrash.overload("int").implementation=function(chinese,math){
console.log("[javascript] testCrash method be called.");
send("hook testCrash method success>>>");
return this.testCrash(12345678);
}
//声明一个Java类
var MyClass = Java.use("com.lanshifu.demo_module.ui.activity.DemoHookTestActivity$MyClass");
//hook 无参构造
MyClass.$init.overload().implementation=function(){
//调用原来构造
this.$init();
send("hook 无参构造 ");
}
//hook 有参构造,入参是 int[]
MyClass.$init.overload('[I').implementation=function(param){
send("hook 有参构造 init(int[] i) method ");
}
//hook 多个参数构造,入参是 int,int
MyClass.$init.overload("int","int").implementation=function(param1,param2){
send("hook success ");
send(param1);
send(param2);
//修改返回值
return 100;
}
//void函数
MyClass.method1.overload().implementation=function(){
send("hook method1 ");
this.method1();
}
//有入参的函数
MyClass.method2.overload("int","int").implementation=function(param1,param2){
send("hook method2 ");
//实例化一个类并调用它的method3方法
var ClassName=Java.use("com.lanshifu.demo_module.ui.activity.DemoHookTestActivity$MyClass");
var instance = ClassName.$new();
instance.method3(1,2,3);
this.method2(100,100)
}
//
MyClass.method3.overload("int","int","int").implementation=function(param1,param2,param3){
send("hook method3 ");
this.method3(100,100,100)
}
});
}
"""
def on_message(message, data):
if message['type'] == 'send':
print("[*] {0}".format(message['payload']))
else:
print(message)
pass
# 查找USB设备并附加到目标进程
session = frida.get_usb_device().attach('com.lanshifu.demo_module')
# 在目标进程里创建脚本
script = session.create_script(jscode)
# 注册消息回调
script.on('message', on_message)
print('[*] Start attach')
# 加载创建好的javascript脚本
script.load()
# 读取系统输入
sys.stdin.read()