Frida
- Frida是一个动态代码执行工具包,通过Frida开源把一段Javascript代码注入到一个进程中去,或者把一个动态库加载到另一个进程中去。
为什么使用 Python API,但使用 JavaScript 调试逻辑?
- Frida核心引擎是用C写的,并且集成了Google的Javascript引擎V8,把包含V8的代码注入到目标进程之后,通过一个专门的信息通道,目标进程就可以和这个注入进行的Javascript引擎进行通信了,这样通过Javascript代码就可以在目标进程中做很多事情,例如:内存访问、函数Hook、甚至调用目标进程中的原生函数都可以。
Installation
使用Pip进行安装
pip3 install frida-tools
查看下载的frida-tools的版本,下载对应版本的frida-server
alice@mac ~ % pip3 list
Package Version
------------------ --------
...
frida 15.1.1
frida-tools 10.2.2
...
- 将frida-server推入设备(设备需要Root)并启动
alice@mac Downloads % adb root
restarting adbd as root
alice@mac Downloads % adb push frida-server /data/local/tmp/
frida-server: 1 file pushed. 29.0 MB/s (46654900 bytes in 1.534s)
alice@mac Downloads % adb shell "chmod 755 /data/local/tmp/frida-server"
alice@mac Downloads % adb shell "/data/local/tmp/frida-server &"
- 使用frida-ps -U命令连接上去,就可以看到正在运行的进程。
alice@mac ~ % frida-ps -U
PID Name
----- ---------------------------------------------------
6830 Google
2008 My Application
23351 TiltMazes
15329 YouTube
3578 adbd
1850 android.hardware.audio@2.0-service
1917 android.hardware.biometrics.fingerprint@2.1-service
1851 android.hardware.broadcastradio@1.1-service
1852 android.hardware.camera.provider@2.4-service
1853 android.hardware.cas@1.1-service
1854 android.hardware.configstore@1.1-service
1855 android.hardware.drm@1.0-service
1856 android.hardware.drm@1.2-service.clearkey
....
使用Frida来Hook参数、修改结果
- 编写一个简单的app,功能是想日志输出两个数相加的结果。
package com.example.demo01;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
while (true){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
fun(50,30);
}
}
void fun(int x , int y ){
Log.d("Sum" , String.valueOf(x+y));
}
}
- 写一段js代码,用frida-server将这段代码还在到com.example.demo01中去,执行其中的hook函数。
Java.perform(function x() {
console.log("Inside java perform function");
//定位类
var my_class = Java.use("com.example.demo01.MainActivity");
console.log("Java.Use.Successfully!");//定位类成功!
//在这里更改类的方法的实现(implementation)
my_class.fun.implementation = function(x,y){
//打印替换前的参数
console.log( "original call: fun("+ x + ", " + y + ")");
//把参数替换成2和5,依旧调用原函数
var ret_value = this.fun(2, 5);
return ret_value;
}
});
- 写一个python脚本,将这段js代码传递给安卓系统里正在运行的frida-server
import sys
import frida
device = frida.get_usb_device()
pid = device.spawn(["com.example.demo01"])
device.resume(pid)
session = device.attach(pid)
with open('./s1.js') as f:
script = session.create_script(f.read())
script.load()
#等待用户输入
sys.stdin.read()
- 关闭机器的selinux
alice@mac ~ % adb shell
alice@mac ~ % su
alice@mac ~ % setenforce 0
- 在主机上运行python脚本,可以看到com.example.demo01这个app重启了,然后adb logcat|grep Sum的内容也变了。
08-09 13:19:32.255 10280 10280 D Sum : 80
08-09 13:19:33.256 10280 10280 D Sum : 80
08-09 13:19:34.257 10280 10280 D Sum : 80
08-09 13:19:35.261 10280 10280 D Sum : 80
08-09 13:19:36.262 10280 10280 D Sum : 80
08-09 13:19:37.263 10280 10280 D Sum : 80
08-09 13:19:40.118 10478 10478 D Sum : 7
08-09 13:19:48.136 10478 10478 D Sum : 7
08-09 13:19:49.139 10478 10478 D Sum : 7
08-09 13:19:53.155 10478 10478 D Sum : 7
08-09 13:19:54.158 10478 10478 D Sum : 7
- 在主机上可以观察到
Inside java perform function
Java.Use.Successfully!
original call: fun(50, 30)
original call: fun(50, 30)
original call: fun(50, 30)
original call: fun(50, 30)
original call: fun(50, 30)
方法重载、隐藏函数的处理
- 在app中添加如下代码:
package com.example.demo01;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
public class MainActivity extends AppCompatActivity {
private String total = "@@@###@@@";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
while (true){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
fun(50,30);
Log.d("Demo.String", fun("LoWeRcAsE Me!!!!!!!!!"));
}
}
void fun(int x , int y ){
Log.d("Demo.Sum" , String.valueOf(x+y));
}
String fun(String x){
total += x;
return x.toLowerCase();
}
String secret(){
return total;
}
}
- app运行起来后,使用logcat打印日志:
08-18 20:06:21.529 24734 24734 D Demo.String: lowercase me!!!!!!!!!
08-18 20:06:22.530 24734 24734 D Demo.Sum: 80
08-18 20:06:22.532 24734 24734 D Demo.String: lowercase me!!!!!!!!!
08-18 20:06:23.533 24734 24734 D Demo.Sum: 80
08-18 20:06:23.534 24734 24734 D Demo.String: lowercase me!!!!!!!!!
08-18 20:06:24.549 24734 24734 D Demo.Sum: 80
08-18 20:06:24.549 24734 24734 D Demo.String: lowercase me!!!!!!!!!
08-18 20:06:25.552 24734 24734 D Demo.Sum: 80
08-18 20:06:25.552 24734 24734 D Demo.String: lowercase me!!!!!!!!!
08-18 20:06:26.553 24734 24734 D Demo.Sum: 80
- 上述func()函数有了重载之后,在参数是两个int的情况下,返回两个int只和,在参数为String类型之下,则返回字符串的小写形式。
- secret()方法为隐藏方法,在app里面没有直接调用。
- 直接使用上一节里面的js脚本和loader.js来加载的话,肯定会崩溃,为了看到崩溃信息,我们对loader.py做一些处理。
#定义错误处理
def my_message_handler(message , payload):
print message
print payload
#调用错误处理
script.on("message" , my_message_handler)
- 在运行loader.py的话,会看到如下错误信息返回:
$ python3 loader.py
Script loaded successfully
Inside java perform function
Java.Use.Successfully!
{u'columnNumber': 1, u'description': u"Error: fun(): has more than one overload, use .overload(<signature>) to choose from:\n\t.overload('java.lang.String')\n\t.overload('int', 'int')", u'fileName': u'frida/node_modules/frida-java/lib/class-factory.js', u'lineNumber': 2233, u'type': u'error', u'stack': u"Error: fun(): has more than one overload, use .overload(<signature>) to choose from:\n\t.overload('java.lang.String')\n\t.overload('int', 'int')\n at throwOverloadError (frida/node_modules/frida-java/lib/class-factory.js:2233)\n at frida/node_modules/frida-java/lib/class-factory.js:1468\n at x (/script1.js:14)\n at frida/node_modules/frida-java/lib/vm.js:43\n at M (frida/node_modules/frida-java/index.js:347)\n at frida/node_modules/frida-java/index.js:299\n at frida/node_modules/frida-java/lib/vm.js:43\n at frida/node_modules/frida-java/index.js:279\n at /script1.js:15"}
None
- 可以看出是一个throwOverloadError,这就是因为我们没有对重载函数进行处理,我们对js代码修改如下:
console.log("Script loaded successfully ");
Java.perform(function x() {
console.log("Inside java perform function");
var my_class = Java.use("com.example.demo01.MainActivity");
my_class.fun.overload("int", "int").implementation = function (x, y) { //hooking the old function
console.log("original call: fun(" + x + ", " + y + ")");
var ret_value = this.fun(2, 5);
return ret_value;
};
var string_class = Java.use("java.lang.String");
my_class.fun.overload("java.lang.String").implementation = function (x) { //hooking the new function
console.log("*************************************")
var my_string = string_class.$new("My TeSt String#####");
console.log("Original arg: " + x);
var ret = this.fun(my_string);
console.log("Return value: " + ret);
console.log("*************************************")
return ret;
};
});
- 再次运行loader.py脚本:
Script loaded successfully
Inside java perform function
original call: fun(50, 30)
*************************************
Original arg: LoWeRcAsE Me!!!!!!!!!
Return value: my test string#####
*************************************
- 观看打印的logcat
08-18 20:16:29.165 25089 25089 D Demo.String: my test string#####
08-18 20:16:30.166 25089 25089 D Demo.Sum: 7
08-18 20:16:30.167 25089 25089 D Demo.String: my test string#####
08-18 20:16:31.169 25089 25089 D Demo.Sum: 7
08-18 20:16:31.170 25089 25089 D Demo.String: my test string#####
08-18 20:16:32.171 25089 25089 D Demo.Sum: 7
08-18 20:16:32.173 25089 25089 D Demo.String: my test string#####
08-18 20:16:33.174 25089 25089 D Demo.Sum: 7
08-18 20:16:33.175 25089 25089 D Demo.String: my test string#####
08-18 20:16:34.176 25089 25089 D Demo.Sum: 7
08-18 20:16:34.177 25089 25089 D Demo.String: my test string#####
08-18 20:16:35.179 25089 25089 D Demo.Sum: 7
- 对于隐藏方法的调用,可以直接到内存里去寻找方法,也就是Java.choose(className, callbacks)函数,通过类名触发回调函数:(note:Java.choose在API 29运行会导致frida.TransportError: the connection is closed)
Java.perform(function x() {
...
Java.choose("com.example.demo01.MainActivity", {
onMatch: function (instance) {
console.log("Found instance: " + instance);
console.log("Result of secret func: " + instance.secret());
},
onComplete: function () { }
});
});
- 再次运行loader.py脚本
Script loaded successfully
Inside java perform function
Found instance: com.example.demo01.MainActivity@570c3bc
Result of secret func: @@@###@@@
original call: fun(50, 30)
*************************************
远程调用
- 上一小节,我们在安卓机器上使用js脚本调用了隐藏函数secret(),它在app内虽然没有被任何地方调用,但是仍然被我们的脚本找到并调用起来。
- 这一节我们要实现的是,不仅在泡在安卓机上的js脚本调用这个函数, 还要在主机上的py脚本里,直接调用这个函数。
- 创建s3.js,内容如下:
console.log("Script loaded successfully ");
function callSecretFun() {
Java.perform(function () {
Java.choose("com.example.demo01.MainActivity",{
onMatch:function (instance) {
console.log("found instance: "+instance);
console.log("Result of secret func: "+ instance.secret());
},
onComplete:function () {
}
});
});
}
rpc.exports = {
callsecretfunction: callSecretFun
};
- 创建loader3.py,内容如下:
import sys
import time
import frida
def my_message_handler(message, payload):
print(message)
print(payload)
device = frida.get_usb_device()
pid = device.spawn(["com.example.demo01"])
device.resume(pid)
session = device.attach(pid)
with open('s3.js') as f:
script = session.create_script(f.read())
script.on("message", my_message_handler)
script.load()
command = ""
while 1 == 1:
command = input("enter input:")
if command == "1":
break
elif command == "2":
script.exports.callsecretfunction()
- 运行loader3.py脚本,输出如下:
Script loaded successfully
enter input:2
found instance: com.example.demo01.MainActivity@c0ecf45
Result of secret func: @@@###@@@LoWeRcAsE Me!!!!!!!!!LoWeRcAsE Me!!!!!!!!!
enter input:2
found instance: com.example.demo01.MainActivity@c0ecf45
Result of secret func: @@@###@@@LoWeRcAsE Me!!!!!!!!!LoWeRcAsE Me!!!!!!!!!LoWeRcAsE
互通互联、动态修改
-Goal: 不仅仅可以在主机上调用安卓app里的函数,还可以把数据从安卓app里传递到主机上,在主机上进行修改,再传递回安卓app里面去。
- 编写这样一个app,其中最核心的地方在于判断用户是否为admin,如果是,则直接返回错误,禁止登陆。如果不是,则把用户和密码上传到服务器上进行验证.
package com.example.demo01;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Base64;
import android.util.Log;
import android.view.View;
import android.widget.TextView;
public class MainActivity extends AppCompatActivity {
private TextView mMessage_tv;
private TextView mSubmit;
private TextView mPassword;
private TextView mUsername;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mUsername = this.findViewById(R.id.username);
mPassword = this.findViewById(R.id.password);
mMessage_tv = this.findViewById(R.id.message_tv);
mSubmit = this.findViewById(R.id.submit);
mSubmit.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (mUsername.getText().toString().compareTo("admin") == 0) {
mMessage_tv.setText("you cannot login as admin");
return;
}
mMessage_tv.setText("Sending to the server :" + Base64.encodeToString((mUsername.getText().toString() + ":" + mPassword.getText().toString()).getBytes(), Base64.DEFAULT));
}
});
}
}
- 我们的目标就是自爱主机上得到输入框输入的内容,并且修改其输入的内容,并且传输给安卓机器,使其通过验证,也就是说,哪怕输入admin的账户和密码,也可以绕过本地校验,进行登陆操作。
- 安卓端的js代码的逻辑就是,截取输入,传输给主机,暂停执行,得到主机传回的数据之后,继续执行,代码如下:
Java.perform(function () {
var tv_class = Java.use("android.widget.TextView");
tv_class.setText.overload("java.lang.CharSequence").implementation = function (x) {
var string_to_send = x.toString();
var string_to_recv;
send(string_to_send); // 将数据发送给kali主机的python代码
recv(function (received_json_object) {
string_to_recv = received_json_object.my_data;
console.log("string_to_recv: " + string_to_recv);
}).wait(); //收到数据之后,再执行下去
return this.setText.overload("java.lang.CharSequence").call(this,"send to server:"+string_to_recv);
};
});
- 主机端的流程就是,将接受到的JSON数据解析,提取出密码部分,然后将用户名修改为admin,这样就实现了将admin和pw发送给服务器的效果。
import base64
import codecs
import frida
import time
import base64
def my_message_handler(message, payload):
print(message)
print(payload)
if message['type'] == 'send':
print(message['payload'])
data = message['payload'].split(":")[1].strip()
print('data -> ',data)
data = base64.b64decode(data.encode('utf-8')).decode('utf-8')
print('data ->', data)
user, pw = data.split(":")
data = base64.b64encode(("admin" + ":" + pw).encode('utf-8')).decode('utf-8')
print("encoded data:",data)
script.post({"my_data":data})
print("Modified data sent")
device = frida.get_usb_device()
pid = device.spawn(["com.example.demo01"])
device.resume(pid)
time.sleep(1)
session = device.attach(pid)
with open("s4.js") as f:
script = session.create_script(f.read())
script.on("message", my_message_handler) # 注册消息处理函数
script.load()
input()
- 我们只要输入(非admin)+密码,非admin用户名可以绕过compareTo校验,然后frida会帮助我们将用户名改成admin,最终就是admin:pw的组合发送到服务器
{'type': 'send', 'payload': 'Sending to the server :MTIxOjEyMQ==\n'}
None
Sending to the server :MTIxOjEyMQ==
data -> MTIxOjEyMQ==
data -> 121:121
encoded data: YWRtaW46MTIx
Modified data sent
string_to_recv: YWRtaW46MTIx
- 到这里,动态修改输入就完成了。