环境与系统

Windows

python,目前没有研究过哪个版本安装这个比较好,但是笔者通过测试的版本是 python3.7.9 。

adb,需要有 adb 的支持。

一个已经 root 或者具有 root 权限的 Android 手机。



安装 frida

pip 安装 frida 库

这里笔者按照教程安装的是 12.8.0 版本。

1
2
pip install frida==12.8.0
pip install frida-tools==5.3.0

安装 frida server

下载 frida server

进入 Frida Release 页面。选择和前面安装 frida 一致的版本的 server(笔者这里是12.8.0 版本的)。同时注意自己手机的架构,这里选择的是 arm64。笔者下载的版本链接: frida-server-12.8.0-android-arm64.xz

安装&运行 frida server

  1. push 文件到手机

    • 将上面下载的 xz 文件进行解压,可以得到一个可执行文件 frida-server-12.8.0-android-arm64

    • 打开 Windows 终端,运行下面命令(注意修改路径),将文件发放到手机中。

      1
      adb push [PATH OF THIS FILE]/frida-server-12.8.0-android-arm64 /data/local/tmp
  2. 进入 Android Linux 控制台,,添加可执行权限。

    • 首先确保手机正确连接到电脑,并且开启了 USB 调试 模式。(可以在电脑终端通过 adb devices 查看自己的设备是否正确连接。

    • 运行命令 adb shell ,即可进入 手机的 Linux 命令行中。

    • 进入目录给目标添加权限。(确保自己的 root 权限)

      1
      2
      cd /data/local/tmp
      chmod 777 frida-server-12.8.0-android-arm64
  3. 在手机中运行服务端

    • 运行下面的命令,可以在手机中开启服务端服务,等待来自电脑客户端的控制连接。(& 可以让该任务运行在后台进行

      1
      ./frida-server-12.8.0-android-arm64 &
    • 开启端口转发,新开一个终端(不要关闭上一个后台运行服务的终端)(任意开一个就可以了,端口随意取的,不冲突就行)

      1
      2
      adb forward tcp:27042 tcp:27042
      adb forward tcp:27043 tcp:27043

      frida server 运行测试

在开启端口转发的终端中运行下面的命令查看 Android 中正在运行的进程列表

1
frida-ps -R

如果有下面的这种效果的输出,那就没啥问题了

1
2
3
4
5
6
7
8
9
10
11
12
  PID  Name
----- ----------------------------------------------------------
9113 .dataservices
1160 ATFWD-daemon
18557 adbd
1118 adsprpcd
8401 android.hardware.audio@2.0-service
693 android.hardware.bluetooth@1.0-service-qti
694 android.hardware.camera.provider@2.4-service
695 android.hardware.cas@1.0-service
696 android.hardware.configstore@1.0-service
。。。。。。。。。



frida 调试测试

在安装好上述的环境配置等之后,可以编写脚本进行测试。 读者可以参考下方是实例 GitHub demo,但是笔者更推荐手动编写。

GitHub demo

GitHub 上有 demo 可以安装教程进行简单的使用测试。自然此玩意要求有 node.js 的环境。

大致 demo 用法如下:

1
2
3
4
$ git clone git://github.com/oleavr/frida-agent-example.git
$ cd frida-agent-example/
$ npm install
$ frida -U -f com.example.android --no-pause -l _agent.js


手动编写 demo

手动编写 demo 笔者认为理论上这样就可以不用安装多余的 node.js 。只是安装了再配上一个好的编辑器他会有自动补全等功能。

下面先编写测试脚本:

si.js

1
2
3
4
5
6
function main() {
Java.perform(function x() {
console.log("hello world")
})
}
setImmediate(main)

loader.py

1
2
3
4
5
6
7
8
9
10
11
12
import time
import frida

device8 = frida.get_device_manager().add_remote_device("127.0.0.1:27042") # 找到 device 这里我通过和主机进行 usb 连接后进行端口转发
pid = device8.spawn("com.android.settings") # 再通过 spawn 找到设备中运行的 settings 的 pis
device8.resume(pid) # 然后通过 pid 启动(准确说是恢复)它
time.sleep(1) # 然后 睡一秒
session = device8.attach(pid) # 然后给这个进程拿到一个 session
with open("si.js", "r", encoding="utf-8") as f:
script = session.create_script(f.read()) # 让这个 session 执行脚本
script.load() # 执行脚本
input() # 等待输入

解释一下,这个脚本就是先通过frida.get_device_manager().add_remote_device来找到device,然后spawn方式启动settings,然后attach到上面,并执行frida脚本。

注意,这里第4行的端口一定要和前面运行时进行端口转发的端口一致才能进行转发。

然后将上面的两个文件放在同一个文件夹下面,并在此文件夹中启动一个终端,运行 python 脚本

1
2
3
PS D:\Users\Desktop\frida> python .\loader.py
hello world
This_Is_What_I_Input_To_Continue

在一切都顺利的情况下,连接的手机设备会自动弹出设置页面,然后运行 python 的终端就会打印一个 “hello world” 。


frida 打印和修改参数

下面将进行模拟对目标程序进行 js 注入,然后打印和修改其返回值。

frida_demo.apk

首先编写一个 demo 的简单的 apk,用于进行测试。

MainActivity.java

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
package com.example.frida_demo;

import androidx.appcompat.app.AppCompatActivity;
import android.util.Log;
import android.os.Bundle;

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("Ron_Log_TAG_string" , fun("LoWeRcAsE ThIs!!!!!!!!!"));
}
}
void fun(int x , int y ){
Log.d("Ron_Log_TAG_sum" , String.valueOf(x+y));
}
String fun(String x){
total +=x;
return x.toLowerCase();
}
String secret(){
return total;
}
}

然后 Android Studio 会自动生成 xml 等文件,保持其不变,然后编译并安装此 apk 到调试机上,点击其中的 logcat 查看系统的日志,可以看到如下所示的日志记录:

2021-03-22 14:12:44.365 2570-2570/com.example.frida_demo D/Ron_Log_TAG_sum: 80
2021-03-22 14:12:44.366 2570-2570/com.example.frida_demo D/Ron_Log_TAG_string: lowercase this!!!!!!!!!
2021-03-22 14:12:45.366 2570-2570/com.example.frida_demo D/Ron_Log_TAG_sum: 80
2021-03-22 14:12:45.367 2570-2570/com.example.frida_demo D/Ron_Log_TAG_string: lowercase this!!!!!!!!!
2021-03-22 14:12:46.373 2570-2570/com.example.frida_demo D/Ron_Log_TAG_sum: 80
2021-03-22 14:12:46.375 2570-2570/com.example.frida_demo D/Ron_Log_TAG_string: lowercase this!!!!!!!!!
2021-03-22 14:12:47.377 2570-2570/com.example.frida_demo D/Ron_Log_TAG_sum: 80
2021-03-22 14:12:47.379 2570-2570/com.example.frida_demo D/Ron_Log_TAG_string: lowercase this!!!!!!!!!
2021-03-22 14:12:48.380 2570-2570/com.example.frida_demo D/Ron_Log_TAG_sum: 80
2021-03-22 14:12:48.382 2570-2570/com.example.frida_demo D/Ron_Log_TAG_string: lowercase this!!!!!!!!!
2021-03-22 14:12:49.383 2570-2570/com.example.frida_demo D/Ron_Log_TAG_sum: 80

由于写了一个死循环,因此此日志会应该程序一直运行一直打印日志。

frida_demo.js

下面和前面手写的 demo 一样,编写一个 js 用于注入进行执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function main() {
console.log("Enter the Script!");
Java.perform(function x() {
console.log("Inside Java perform by frida_demo.js");
var MainActivity = Java.use("com.example.frida_demo.MainActivity");
// 重载找到指定的函数
MainActivity.fun.overload('java.lang.String').implementation = function (str) {
//打印参数
console.log("original call : str:" + str);
//修改结果
var ret_value = "Ron_js";
return ret_value;
};
})
}
setImmediate(main);

运行测试

首先检测机器中是否在运行测试的 frida_demo.apk 。 通过 findstr 实现结果的过滤。

1
2
3
PS D:\Users\Desktop\frida\frida_demo> frida-ps -U | findstr "frida"
3071 com.example.frida_demo
21252 frida-helper-32

然后通过 -f 即通过 spawn,重启 apk 注入 js 代码。

利用 Android Studio 启动应用,查看 logcat 依旧可以看到如前面的日志一样的 Ron_Log_TAG_string: lowercase this!!!!!!!!!

然后在保存 js 文件的文件夹中打开终端, 输入下面的命令后,测试机上的应用会被重启,可以看到 Android Studio 中的日志也在刷新重启。然后,按照终端中的提示,下面第 14 行 [OPPO PAFM00::com.example.frida_demo]-> 后面输入 %resume 以恢复应用,不然应用会出现 Process terminated 而无法看到结果。

然后可以看到终端中不断打印出应用中函数 fun(string) 的输入变量的值。同时查看 Android Studio 中的 logcat 可以看到日志已经被修改了 /Ron_Log_TAG_string: Ron_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
PS D:\Users\D1esktop\frida\frida_demo> frida -U -f com.example.frida_demo -l frida_demo.js
____
/ _ | Frida 12.8.0 - A world-class dynamic instrumentation toolkit
| (_| |
> _ | Commands:
/_/ |_| help -> Displays the help system
. . . . object? -> Display information about 'object'
. . . . exit/quit -> Exit
. . . .
. . . . More info at https://www.frida.re/docs/home/
Spawning `com.example.frida_demo`...
Enter the Script!
Spawned `com.example.frida_demo`. Use %resume to let the main thread start executing!
[OPPO PAFM00::com.example.frida_demo]-> %resume
[OPPO PAFM00::com.example.frida_demo]-> Inside Java perform by frida_demo.js
original call : str:LoWeRcAsE ThIs!!!!!!!!!
original call : str:LoWeRcAsE ThIs!!!!!!!!!
original call : str:LoWeRcAsE ThIs!!!!!!!!!
......(省略一万行)
original call : str:LoWeRcAsE ThIs!!!!!!!!!
Process terminated
[OPPO PAFM00::com.example.frida_demo]->

Thank you for using Frida!

2021-03-22 14:42:24.575 5732-5732/com.example.frida_demo D/Ron_Log_TAG_sum: 80
2021-03-22 14:42:24.580 5732-5732/com.example.frida_demo D/Ron_Log_TAG_string: Ron_js
2021-03-22 14:42:25.582 5732-5732/com.example.frida_demo D/Ron_Log_TAG_sum: 80
2021-03-22 14:42:25.590 5732-5732/com.example.frida_demo D/Ron_Log_TAG_string: Ron_js
2021-03-22 14:42:26.591 5732-5732/com.example.frida_demo D/Ron_Log_TAG_sum: 80
2021-03-22 14:42:26.597 5732-5732/com.example.frida_demo D/Ron_Log_TAG_string: Ron_js
2021-03-22 14:42:27.599 5732-5732/com.example.frida_demo D/Ron_Log_TAG_sum: 80
2021-03-22 14:42:27.606 5732-5732/com.example.frida_demo D/Ron_Log_TAG_string: Ron_js
2021-03-22 14:42:28.607 5732-5732/com.example.frida_demo D/Ron_Log_TAG_sum: 80
2021-03-22 14:42:28.614 5732-5732/com.example.frida_demo D/Ron_Log_TAG_string: Ron_js


frida 选择 instance,并进行修改

继续利用上面的 frida_demo.apk 进行测试 。

frida_demo_rpc.js

新建一个 js 用于查找目标程序的 instance 当找到的时候,这里调用了其中的 fun(String) 函数,传入参数为 “Ron_rpc_js”, 并获取其返回值进行打印。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function callFun() {
Java.perform(function fn() {
console.log("begin");
Java.choose("com.example.frida_demo.MainActivity", {
onMatch: function (x) {
console.log("find instance by frida_demo_rpc.js :" + x);
// 调用 x 中的 function fun(String) 并传入参数为 "Ron_rpc_js" 然后获取其执行后的返回值
console.log("result of fun(string):" + x.fun(Java.use("java.lang.String").$new("Ron_rpc_js")));
},
onComplete: function () {
console.log("end");
}
})
})
}
rpc.exports = {
callfun: callFun
};

loader.py

和最前面进行打开菜单的测试一样,这里通过在手机中开启 frida 的服务端并开启端口转发,然后 python 连接设备调用前面的 frida_demo_rpc.js 进行执行。

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

device = frida.get_device_manager().add_remote_device("127.0.0.1:27042")
pid = device.spawn(["com.example.frida_demo"])
device.resume(pid)
time.sleep(1)
session = device.attach(pid)
with open("frida_demo_rpc.js", "r", encoding="utf-8") as f:
script = session.create_script(f.read())

def my_message_handler(message, payload):
print(message)
print(payload)

script.on("message", my_message_handler)
script.load()

script.exports.callfun()

运行测试

新建一个终端,然后运行上面的 loader.py ,如果一切正常的话,可以看到手机中的应用被启动,然后终端打印调用其中函数后输出的结果。

1
2
3
4
5
PS D:\Users\Desktop\frida\frida_demo> python .\loader.py
begin
find instance by frida_demo_rpc.js :com.example.frida_demo.MainActivity@9f74e23
result of fun(string):ron_rpc_js
end

如果运行报错,尝试运行命令安装包后再进行测试

npm install rpc


frida 动态修改

动态修改,可以实现将手机上 app 中的内容发送到电脑端,通过 python 将数据处理之后,再转发给 app 进行后续的处理。这里的关键,参考的 blog 将是 sendrecv 函数。

如下我们将构建一个测试用的 app,它会发送一个 base64 编码后的用户名和觅马,而我们的目标是:

让 message_tv.setText 可以 ”发送” username 为 admin 的 base64 字符串。

因此,我们需要 hook 的函数是 TextView.setText

frida_demo.apk

首先还是构建测试 apk 进行测试。

MainActivity.java

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
package com.example.frida_demo;

import androidx.appcompat.app.AppCompatActivity;

import android.util.Base64;
import android.util.Log;
import android.os.Bundle;
import android.view.View;
import android.widget.EditText;
import android.widget.TextView;

public class MainActivity extends AppCompatActivity {

EditText username_et;
EditText password_et;
TextView message_tv;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

password_et = (EditText) this.findViewById(R.id.editText2);
username_et = (EditText) this.findViewById(R.id.editText);
message_tv = ((TextView) findViewById(R.id.textView));

this.findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {

if (username_et.getText().toString().compareTo("admin") == 0) {
message_tv.setText("You cannot login as admin");
return;
}
//hook target
message_tv.setText("Sending to the server :" + Base64.encodeToString((username_et.getText().toString() + ":" + password_et.getText().toString()).getBytes(), Base64.DEFAULT));

}
});

}

}

activity_main.xml

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
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">

<TextView
android:id="@+id/textView"
android:layout_width="239dp"
android:layout_height="82dp"
android:text="please input username and password"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />

<EditText
android:id="@+id/editText"
android:layout_width="fill_parent"
android:layout_height="40dp"
android:hint="username"
android:maxLength="20"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.095" />

<EditText
android:id="@+id/editText2"
android:layout_width="fill_parent"
android:layout_height="40dp"
android:hint="password"
android:maxLength="20"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.239"
tools:ignore="MissingConstraints" />

<Button
android:id="@+id/button"
android:layout_width="170dp"
android:layout_height="59dp"
android:layout_gravity="right|center_horizontal"
android:text="提交"
android:visibility="visible"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.745" />

</androidx.constraintlayout.widget.ConstraintLayout>

app 编写完毕后,记得编译并在测试机中进行安装。

此时在手机端输入用户名和觅马后点击提交,app 中会打印 base 之后的结果,这里输入

username:Ron

password:12345678

提交就打印的结果为:

b’Um9uOjEyMzQ1Njc4’

frida_demo3.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
console.log("Script loaded successfully ");
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); // 将数据发送到 python 中
recv(function (received_json_object) {
string_to_recv = received_json_object.my_data
console.log("string_to_recv: " + string_to_recv);
}).wait(); // 阻塞 app 执行,直到接收到修改后的 message
var my_string = Java.use("java.lang.String").$new(string_to_recv);
this.setText(my_string);
}
});

loader.py

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
import time
import frida
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( 'message:', message)
#data = data.decode("base64")
#data = data
data = str(base64.b64decode(data))
print( 'data:',data)
user, pw = data.split(":")
print( 'pw:',pw)
#data = ("admin" + ":" + pw).encode("base64")
data = str(base64.b64encode(("admin" + ":" + pw).encode()))
print( "encoded data:", data)
script.post({"my_data": data}) # send JSON object
print( "Modified data sent")

device = frida.get_device_manager().add_remote_device("127.0.0.1:27042")
pid = device.spawn(["com.example.frida_demo"])
device.resume(pid)
time.sleep(1)
session = device.attach(pid)
with open("frida_demo3.js", "r", encoding="utf-8") as f:
script = session.create_script(f.read())
script.on("message", my_message_handler)
script.load()
input()

运行测试

上述完毕之后,运行 loader.py 可以看到手机中的测试 demo 被启动起来,同时 python 的终端打印 Script loaded successfully 并开始等待。

此时在手机端输入和之前同样的用户名和觅马后点击提交,可以看到 python 终端已经拦截到了数据,并进行了修改和替换,然后手机端显示的结果也是 python 替换之后的。

1
2
3
4
5
6
7
8
9
10
11
12
13
PS D:\Users\Desktop\frida\frida_demo2> python .\loader.py
Script loaded successfully
{'type': 'send', 'payload': 'Sending to the server :Um9uOjEyMzQ1Njc4\n'}
None
Sending to the server :Um9uOjEyMzQ1Njc4

message: {'type': 'send', 'payload': 'Sending to the server :Um9uOjEyMzQ1Njc4\n'}
data: b'Ron:12345678'
pw: 12345678'
encoded data: b'YWRtaW46MTIzNDU2Nzgn'
Modified data sent
string_to_recv: b'YWRtaW46MTIzNDU2Nzgn'
This_Is_What_I_Input_To_Continue

打开 python 可以轻易进行验证。

1
2
3
4
5
6
7
In [1]: from base64 import b64decode, b64encode

In [2]: b64decode("YWRtaW46MTIzNDU2Nzgn")
Out[2]: b"admin:12345678'"

In [3]: b64encode(b"Ron:12345678")
Out[3]: b'Um9uOjEyMzQ1Njc4'


API List

本部分简要列举了一些常用的(js 的) API 。(其中有些前面已经用到了)

  • Java.choose(className: string, callbacks: Java.ChooseCallbacks): void
    通过扫描 Java VM 的堆来枚举 className 类的 live instance。
  • Java.use(className: string): Java.Wrapper<{}>
    动态为 className 生成 JavaScript Wrapper ,可以通过调用 $new() 来调用构造函数来实例化对象。
    在实例上调用 $dispose() 以对其进行显式清理,或者等待 JavaScript 对象被 gc。
  • Java.perform(fn: () => void): void
    Function to run while attached to the VM.
    Ensures that the current thread is attached to the VM and calls fn. (This isn’t necessary in callbacks from Java.)
    Will defer calling fn if the app’s class loader is not available yet. Use Java.performNow() if access to the app’s classes is not needed.
  • send(message: any, data?: ArrayBuffer | number[]): void
    任何 JSON 可序列化的值。
    将 JSON 序列化后的 message 发送到您的基于 Frida 的应用程序,并包含(可选)一些原始二进制数据。
    The latter is useful if you e.g. dumped some memory using NativePointer#readByteArray().
  • recv(callback: MessageCallback): MessageRecvOperation
    Requests callback to be called on the next message received from your Frida-based application.
    This will only give you one message, so you need to call recv() again to receive the next one.
  • wait(): void
    堵塞,直到 message 已经 receive 并且 callback 已经执行完毕并返回



frida 动静结合分析

frida

查询顶部 activity

有时候很不方便看到目标 app 的包名,比如其加了壳的时候,使用 frida 可以查看当前状态下顶部的 activity 是谁

1
2
3
4
5
# windows
adb shell dumpsys activity top | findstr ACTIVITY

# linux
adb shell dumpsys activity top | grep ACTIVITY

objection

虽然 frida 提供了各种且丰富的 API 供调用,但是一些具体功能的实现需要手动编写并利用各种 API 来组合。这样显然比较难受,于是有大佬将各种常见的常用的功能整合进了一个工具,以供在命令行中快捷使用。这个工具就是 objection

objection功能强大,命令众多,而且不用写一行代码,便可实现诸如内存搜索、类和模块搜索、方法 hook 打印参数返回值调用栈等常用功能,是一个非常方便的,逆向必备、内存漫游神器。

pip 安装 objection

这里同样不是安装的最新版,而是安装的 1.8.4 版本。运行完毕后,就可以在命令行中直接执行 objection 命令,键入 --help 参数可以查看帮助信息。

1
pip install objection==1.8.4


内存漫游

获取基本信息
基本操作方法
  • 键入命令回车执行
  • help 命令:在任意命令前键入 help 命令,执行会打印对当前命令的解释信息。如 help env
  • 空格:显示提示信息,上下键移动选择,再空格确认选择。
  • jobs:作业系统,可以同时运行多项 hook 作业。
实际演示

在手机上运行 frida-server (与前面一致),并在手机上打开设置,然后通过终端查看 ”设置“ 应用的包名。这里是 com.android.settings

1
2
3
4
5
PS D:\Users\Desktop\frida> frida-ps -U | findstr -i setting
19321 com.android.settings
19363 com.android.settings:index
15869 com.coloros.simsettings
19408 com.coloros.wirelesssettings

再使用 objection 注入 ”设置“ 应用。键入命令 objection -g com.android.settings explore ,如下可以看到注入成功,终端在等待命令执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
PS D:\Users\Desktop\frida\frida_demo2> objection -g com.android.settings explore
Using USB device `OPPO PAFM00`
Agent injected and responds ok!

_ _ _ _
___| |_|_|___ ___| |_|_|___ ___
| . | . | | -_| _| _| | . | |
|___|___| |___|___|_| |_|___|_|_|
|___|(object)inject(ion) v1.8.4

Runtime Mobile Exploration
by: @leonjza from @sensepost

[tab] for command suggestions
com.android.settings on (OPPO: 8.1.0) [usb] #

启动 objection 之后,会出现提示它的 logo ,这时候不知道输入啥命令的话,可以按下空格,有提示的命令及其功能出来;再按空格选中,又会有新的提示命令出来,这时候按回车就可以执行该命令,如下图执行应用环境信息命令 envfrida-server 版本信息命令。

image-20210322164326827

提取内存信息
查看内存中加载的库

通过 object 还可以查看内存中加载的库。 运行命令 memory list modules 可以查看到内存中加载的库。

1
2
3
4
5
6
7
8
9
com.android.settings on (OPPO: 8.1.0) [usb] # memory list modules
Save the output by adding `--json modules.json` to this command
Name Base Size Path
----------------------- ------------ -------------------- -----------------------------------
app_process64 0x64c770a000 32768 (32.0 KiB) /system/bin/app_process64
libandroid_runtime.so 0x7ae7b57000 2154496 (2.1 MiB) /system/lib64/libandroid_runtime.so
libbase.so 0x7ae7641000 77824 (76.0 KiB) /system/lib64/libbase.so
libbinder.so 0x7ae79e5000 589824 (576.0 KiB) /system/lib64/libbinder.so
......
查看库的导出函数

运行命令 memory list exports libssl.so ,可以查看 libssl.so 库的导出函数。

1
2
3
4
5
6
7
8
9
10
com.android.settings on (OPPO: 8.1.0) [usb] # memory list exports libssl.so
Save the output by adding `--json exports.json` to this command
Type Name Address
-------- ----------------------------------------------------- ------------
function SSL_use_certificate_ASN1 0x7a662ab200
function SSL_CTX_set_dos_protection_cb 0x7a662b3fdc
function SSL_SESSION_set_ex_data 0x7a662b628c
function SSL_CTX_set_session_psk_dhe_timeout 0x7a662b7144
function SSL_CTX_sess_accept 0x7a662b2394
......
将结果保存到 json

如果信息太多可能导致终端无法显示,可以将结果导出至文件中,再利用其他软件进行查看。

运行命令 memory list exports libart.so --json ./libart.jsonlibssl.so 库的导出函数保存到当前文件夹下的 libart.json

1
2
3
com.android.settings on (OPPO: 8.1.0) [usb] # memory list exports libart.so --json ./libart.json
Writing exports as json to ./libart.json...
Wrote exports to: ./libart.json

然后就可以在保存位置查看保存到文件的数据了。

提取整个(或部分)内存

通过命令 memory dump all from_base 可以实现。直接 dump 全部有 1.7 G,会炸。在后文脱壳中再用。

1
2
3
4
com.android.settings on (OPPO: 8.1.0) [usb] # memory dump all from_base
Will dump 886 rw- images, totalling 1.7 GiB

(frida:11832): GLib-GIO-WARNING **: 16:54:46.966: _g_dbus_worker_do_read_cb: error determining bytes needed: Blob indicates that message exceeds maximum message length (128MiB)
搜索整个内存

通过命令 memory search --string --offsets-only 可以实现,在后文脱壳中再用。

内存堆搜索与执行
在堆上搜索实例

这里还是通过运行 ”设置“ 来进行测试,为了进行演示测试,需要去官方(AOSP源码设置)查找一些 ”设置“ 中存在的类,发现存在 DisplaySettings 类。然后可以在堆上搜索是否存在着该类的实例。

依旧同样在手机中打开 ”设置“,然后在终端运行命令 android heap search instances com.android.settings.DisplaySettings ,查看相应的实例地址。按照参考博客,理论上应该出现下面的结果。

1
2
3
4
5
com.android.settings on (OPPO: 8.1.0) [usb] # android heap search instances com.android.settings.DisplaySettings
Using exsiting matches for com.android.settings.DisplaySettings. Use --fresh flag for new instances.
Handle Class toString()
-------- ------------------------------------ -----------------------------------------
0x252a com.android.settings.DisplaySettings DisplaySettings{69d91ee #0 id=0x7f0a0231}

但是笔者实际上的结果是这样,至于发生了什么,我暂且梦在蒲里。

1
2
com.android.settings on (OPPO: 8.1.0) [usb] # android heap search instances com.android.settings.DisplaySettings
Class instance enumeration complete for com.android.settings.DisplaySettings

于是,笔者故技重施,反手一个 exit 退出了当前会话,然后手机打开刚才的 frida_demo 的 app ,然后终端去 hook 这个应用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
PS D:\Users\Desktop\frida> objection -g com.example.frida_demo explore
Using USB device `OPPO PAFM00`
Agent injected and responds ok!

_ _ _ _
___| |_|_|___ ___| |_|_|___ ___
| . | . | | -_| _| _| | . | |
|___|___| |___|___|_| |_|___|_|_|
|___|(object)inject(ion) v1.8.4

Runtime Mobile Exploration
by: @leonjza from @sensepost

[tab] for command suggestions
com.example.frida_demo on (OPPO: 8.1.0) [usb] # android heap search instances com.example.frida_demo.MainActivity --fresh flag
Class instance enumeration complete for com.example.frida_demo.MainActivity
Handle Class toString()
-------- ----------------------------------- -------------------------------------------
0x203a com.example.frida_demo.MainActivity com.example.frida_demo.MainActivity@9f74e23

出现了想要的结果,查到了目标中存在的实例。至于前面设置为啥没成功,也许是不是原生安卓的原因?我暂且蒙在蒲里。

调用实例的方法

既然上面的查询设置中的实例都失败了,再调用其中的方法显然是不现实的,于是干脆就用 frida_demo 的 app 进行接下来的测试了。

为了方便起见,我们在之前的 frida_demo.java (第二个版本)中添加一个方法,放置在 onCreate() 方法之后。该方法会将文本框中的内容进行覆盖,并返回自己的函数名字。修改完毕记得进行重新编译。

1
2
3
4
protected String JustPrintTest(){
message_tv.setText("I'm func JustPrintTest()");
return "come from JustPrintTest()";
}

然后在终端中通过命令 objection -g com.example.frida_demo explore 连接到这个 session。然后通过命令 android heap execute 0x203a JustPrintTest 执行其中新添加的方法。此处注意 0X203a 地址是上一条命令返回的 com.example.frida_demo.MainActivity 的地址。

运行之后,终端显示执行成功,我们可以在测试手机中查看,文本框中的内容的确被覆盖了。

1
2
3
4
5
6
7
8
9
com.example.frida_demo on (OPPO: 8.1.0) [usb] # android heap search instances com.example.frida_demo.MainActivity --fresh flag
Class instance enumeration complete for com.example.frida_demo.MainActivity
Handle Class toString()
-------- ----------------------------------- -------------------------------------------
0x203a com.example.frida_demo.MainActivity com.example.frida_demo.MainActivity@9f74e23
com.example.frida_demo on (OPPO: 8.1.0) [usb] # android heap execute 0x203a JustPrintTest
Handle 0x203a is to class com.example.frida_demo.MainActivity
Executing method: JustPrintTest()
come from JustPrintTest()
在实例上执行 js 代码

也可以在找到的实例上直接执行 js 脚本。 输入 android heap evaluate [address] 后会进入一个迷你编辑器,输入 console.log("evaluate result:"+clazz.JustPrintTest()) 这串脚本,然后按 ESC 退出编辑器,然后回车就会执行这个脚本。

迷你编辑器展示如下

1
2
3
4
com.example.frida_demo on (OPPO: 8.1.0) [usb] # android heap evaluate 0x203a
(The handle at `0x203a` will be available as the `clazz` variable.)

JavaScript edit mode. [ESC] and then [ENTER] to accept. [CTRL] + C to cancel.

输入脚本

1
console.log("evaluate result:"+clazz.JustPrintTest());

键入 ESC 然后回车,可以看到下面的结果,打印了 evaluate result:come from JustPrintTest()

1
2
3
4
5
6
com.example.frida_demo on (OPPO: 8.1.0) [usb] # android heap evaluate 0x203a
(The handle at `0x203a` will be available as the `clazz` variable.)
console.log("evaluate result:"+clazz.JustPrintTest());
JavaScript capture complete. Evaluating...
Handle 0x203a is to class com.example.frida_demo.MainActivity
evaluate result:come from JustPrintTest()

这个功能其实非常实用,可以即时编写、出结果、即时调试自己的代码,不用再编写→注入→操作→看结果→再调整,而是直接出结果。

启动 activity 或者 service
直接启动 activity

直接上代码,想要进入显示设置,可以在 任意界面 直接运行以下代码进入显示设置:

理论上会显示下面的界面,但是笔者又失败了。

1
2
3
# android intent launch_activity com.android.settings.DisplaySettings                      
(agent) Starting activity com.android.settings.DisplaySettings...
(agent) Activity successfully asked to start

根据下面的一个子目录的提示,我觉得参考博客讲错了,也就是上面使用了删除线的 ”任意界面“ ,或者说这样说有歧义。正确的说法,应该是在当前的 app 的范围内,可以启动而不会报错,而启动别的就会报错。

果然,重新新建一个在设置中的监听之后,再执行显示,就可以正确跳转到显示设置页面中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
PS D:\Users\S9039124> objection -g com.android.settings explore
Using USB device `OPPO PAFM00`
Agent injected and responds ok!

_ _ _ _
___| |_|_|___ ___| |_|_|___ ___
| . | . | | -_| _| _| | . | |
|___|___| |___|___|_| |_|___|_|_|
|___|(object)inject(ion) v1.8.4

Runtime Mobile Exploration
by: @leonjza from @sensepost

[tab] for command suggestions
com.android.settings on (OPPO: 8.1.0) [usb] # android intent launch_activity com.android.settings.DisplaySettings
(agent) Starting activity com.android.settings.DisplaySettings...
(agent) Activity successfully asked to start.
查看当前可用的 activity

可以使用 android hooking list activities 来查看当前可用的 activities 。然后再使用上面的方法进行启动。

1
2
3
4
com.example.frida_demo on (OPPO: 8.1.0) [usb] # android hooking list activities
com.example.frida_demo.MainActivity

Found 1 classes
直接启动 service

也可以先使用 android hooking list services 查看可供开启的服务。

然后使用 android intent launch_service com.android.settings.bluetooth.BluetoothPairingService 命令来开启服务。


frida hook anywhere

我们新手在学习 Frida 的时候,遇到的第一个问题就是,无法找到正确的类及子类,无法定位到实现功能的准确的方法,无法正确的构造参数、继而进入正确的重载,这时候可以使用 Frida 进行动态调试,来确定以上具体的名称和写法,最后写出正确的 hook 代码。

objection(内存漫游)
列出内存中所有的类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
com.example.frida_demo on (OPPO: 8.1.0) [usb] # android hooking list classes
[B
[C
[D
[F
[I
[J
[Landroid.animation.Animator;
[Landroid.animation.Keyframe$FloatKeyframe;
......
sun.util.logging.PlatformLogger
sun.util.logging.PlatformLogger$1
sun.util.logging.PlatformLogger$Level
void

Found 5711 classes
内存中搜索所有的类

很显然上面的方法搜索的结果太多,我们需要过滤一下最自己有用的部分。

可以使用命令 android hooking search classes [keyword] 来进行筛选。如下,可以看到已经少了很多。

1
2
3
4
5
6
7
8
9
com.example.frida_demo on (OPPO: 8.1.0) [usb] # android hooking search classes display
[Landroid.icu.text.DisplayContext$Type;
[Landroid.icu.text.DisplayContext;
[Landroid.view.Display$Mode;
......
javax.microedition.khronos.egl.EGLDisplay
oppo.util.OppoDisplayUtils

Found 52 classes
内存中搜索关键的方法

在内存中搜索所有的方法也是同理。但是前面内存中已加载的类就已经高达 5711 个了,那么他们的方法一定是类的个数的数倍,一一列举的话,整个过程会相当庞大和耗时,因此没有必要再一一列举,这里就直接筛选服务关键字的命令。 android hooking search methods display

可以看到,即使我们进行了筛选,终端还是进行了警告。这里我们选择 y 。整个过程明显变慢了很多,因为要一一去遍历所有的方法进行对比。

1
2
3
4
5
6
7
8
9
10
11
com.example.frida_demo on (OPPO: 8.1.0) [usb] # android hooking search methods display
Warning, searching all classes may take some time and in some cases, crash the target application.
Continue? [y/N]: y
Found 5711 classes, searching methods (this may take some time)...
android.app.ActionBar.getDisplayOptions
android.app.ActionBar.setDefaultDisplayHomeAsUpEnabled
......
androidx.constraintlayout.solver.widgets.analyzer.DependencyGraph.generateDisplayNode
An unexpected internal exception has occurred. If this looks like a code related error, please file a bug report!(session detach message) process-terminated

script is destroyed

果然还是崩溃了。

列举出某个类的所有方法

当搜索到了比较关心的类之后,就可以直接查看它有哪些方法,比如我们想要查看 com.android.settings.DisplaySettings 类有哪些方法。如下所示。

1
2
3
4
5
6
7
8
9
10
11
com.android.settings on (OPPO: 8.1.0) [usb] # android hooking list class_methods com.android.settings.DisplaySettings
private static java.util.List<com.android.settings.core.PreferenceController> com.android.settings.DisplaySettings.buildPreferenceControllers(android.content.Context,com.android.settings.core.lifecycle.Lifecycle)
protected int com.android.settings.DisplaySettings.getHelpResource()
protected int com.android.settings.DisplaySettings.getPreferenceScreenResId()
protected java.lang.String com.android.settings.DisplaySettings.getLogTag()
protected java.util.List<com.android.settings.core.PreferenceController> com.android.settings.DisplaySettings.getPreferenceControllers(android.content.Context)
public int com.android.settings.DisplaySettings.getMetricsCategory()
public void com.android.settings.DisplaySettings.onAttach(android.content.Context)
static java.util.List com.android.settings.DisplaySettings.access$000(android.content.Context,com.android.settings.core.lifecycle.Lifecycle)

Found 8 method(s)

参考博客还与源码进行了比对,发现是一模一样的。有兴趣的读者也可以自己比对一下。

直接生成 hook 代码

上文中在列出类的方法时,还直接把参数也提供了,也就是说我们可以直接动手写 hook 了,既然上述写 hook 的要素已经全部都有了,objection 这个“自动化”工具,当然可以直接生成代码。使用命令 android hooking generate simple com.android.settings.DisplaySettings 可以完成。

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
57
58
com.android.settings on (OPPO: 8.1.0) [usb] # android hooking generate  simple  com.android.settings.DisplaySettings

Java.perform(function() {
var clazz = Java.use('com.android.settings.DisplaySettings');
clazz.getMetricsCategory.implementation = function() {
//
return clazz.getMetricsCategory.apply(this, arguments);
}
});
Java.perform(function() {
var clazz = Java.use('com.android.settings.DisplaySettings');
clazz.onAttach.implementation = function() {
//
return clazz.onAttach.apply(this, arguments);
}
});
Java.perform(function() {
var clazz = Java.use('com.android.settings.DisplaySettings');
clazz.getPreferenceControllers.implementation = function() {
//
return clazz.getPreferenceControllers.apply(this, arguments);
}
});
Java.perform(function() {
var clazz = Java.use('com.android.settings.DisplaySettings');
clazz.getHelpResource.implementation = function() {
//
return clazz.getHelpResource.apply(this, arguments);
}
});
Java.perform(function() {
var clazz = Java.use('com.android.settings.DisplaySettings');
clazz.buildPreferenceControllers.implementation = function() {
//
return clazz.buildPreferenceControllers.apply(this, arguments);
}
});
Java.perform(function() {
var clazz = Java.use('com.android.settings.DisplaySettings');
clazz.getPreferenceScreenResId.implementation = function() {
//
return clazz.getPreferenceScreenResId.apply(this, arguments);
}
});
Java.perform(function() {
var clazz = Java.use('com.android.settings.DisplaySettings');
clazz.getLogTag.implementation = function() {
//
return clazz.getLogTag.apply(this, arguments);
}
});
Java.perform(function() {
var clazz = Java.use('com.android.settings.DisplaySettings');
clazz.access$000.implementation = function() {
//
return clazz.access$000.apply(this, arguments);
}
});

生成的代码大部分要素都有了,只是参数貌似没有填上,还是需要我们后续补充一些,看来还是无法做到完美。如何使用后面讲到。

objection (hook)

上述操作均是基于在内存中直接枚举搜索,已经可以获取到大量有用的静态信息,我们再来介绍几个方法,可以获取到执行时动态的信息,当然、同样地,不用写一行代码。

hook 某个类的所有方法

我们以手机连接蓝牙耳机播放音乐为例为例,看看手机蓝牙接口的动态信息。首先我们将手机连接上我的蓝牙耳机 —— AirPods 2,并可以正常播放音乐;然后我们按照上文的方法,搜索一下与蓝牙相关的类,搜到一个高度可疑的类:android.bluetooth.BluetoothDevice

1
2
3
4
5
6
7
8
9
10
com.android.settings on (OPPO: 8.1.0) [usb] # android hooking search classes bluetooth
android.bluetooth.BluetoothA2dp
android.bluetooth.BluetoothA2dp$1
android.bluetooth.BluetoothA2dp$2
android.bluetooth.BluetoothAdapter
......
com.oppo.settings.widget.preference.OppoBluetoothEntryPreference
com.oppo.settings.widget.preference.OppoBluetoothEntryPreference$1

Found 36 classes

运行以下命令,hook 这个类:

1
2
3
4
5
6
7
8
com.android.settings on (OPPO: 8.1.0) [usb] # android hooking watch class android.bluetooth.BluetoothDevice
(agent) Hooking android.bluetooth.BluetoothDevice.-get0()
(agent) Hooking android.bluetooth.BluetoothDevice.-set0(android.bluetooth.IBluetooth)
......
(agent) Hooking android.bluetooth.BluetoothDevice.setSimAccessPermission(int)
(agent) Hooking android.bluetooth.BluetoothDevice.toString()
(agent) Hooking android.bluetooth.BluetoothDevice.writeToParcel(android.os.Parcel, int)
(agent) Registering job yi6z8sxiz2. Type: watch-class for: android.bluetooth.BluetoothDevice

使用 jobs list 命令可以看到 objection 为我们创建的 Hooks 数为 55 ,也就是将 android.bluetooth.BluetoothDevice 类下的所有方法都 hook 了。

1
2
3
4
com.android.settings on (OPPO: 8.1.0) [usb] # jobs list
Job ID Hooks Type
---------- ------- --------------------------------------------------
yi6z8sxiz2 55 watch-class for: android.bluetooth.BluetoothDevice

这时候我们在 设置→声音→媒体播放到 上进行操作,在蓝牙耳机与“此设备”之间切换时,会命中这些 hook 之后,此时 objection 就会将方法打印出来,会将类似这样的信息“吐”出来:

1
2
3
4
5
6
com.android.settings on (google: 9) [usb] # (agent) [h0u5g7uclo] Called android.bluetooth.BluetoothDevice.getService()
(agent) [h0u5g7uclo] Called android.bluetooth.BluetoothDevice.isConnected()
(agent) [h0u5g7uclo] Called android.bluetooth.BluetoothDevice.getService()
......
(agent) [h0u5g7uclo] Called android.bluetooth.BluetoothDevice.getName()
(agent) [h0u5g7uclo] Called android.bluetooth.BluetoothDevice.getService()

理论上应该吐出来吧?但是笔者的手机没有这个选项,无法切换输出源。

hook 方法的参数、返回值和调用栈

在这些方法中,我们对哪些方法感兴趣,就可以查看哪些个方法的参数、返回值和调用栈,比如想看getName()方法,则运行以下命令: android hooking watch class_method android.bluetooth.BluetoothDevice.getName --dump-args --dump-return --dump-backtrace

1
2
3
4
com.android.settings on (OPPO: 8.1.0) [usb] # android hooking watch class_method android.bluetooth.BluetoothDevice.getName --dump-args --dump-return --dump-backtrace
(agent) Attempting to watch class android.bluetooth.BluetoothDevice and method getName.
(agent) Hooking android.bluetooth.BluetoothDevice.getName()
(agent) Registering job cwojtdpd3w. Type: watch-method for: android.bluetooth.BluetoothDevice.getName

但是还是很遗憾,并没有打印出,我想看到的信息,理论上应该是有很多 hook 之后的输出的。

注意最后加上的三个选项 --dump-args --dump-return --dump-backtrace,为我们成功打印出来了我们想要看的信息,其实返回值 Return Value 就是getName()方法的返回值,我的蓝牙耳机的型号名字 OnePlus Bullets Wireless 2;从调用栈可以反查如何一步一步调用到 getName() 这个方法的;虽然这个方法没有参数,大家可以再找个有参数的试一下。

hook 方法的所有重载

objectionhelp 中指出,在 hook 给出的单个方法的时候,会 hook 它的所有重载。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
com.android.settings on (OPPO: 8.1.0) [usb] # help android hooking watch class_method
Command: android hooking watch class_method

Usage: android hooking watch class_method <fully qualified class method> <optional overload>
(optional: --dump-args) (optional: --dump-backtrace)
(optional: --dump-return)

Hooks a specified class method and reports on invocations, together with
the number of arguments that method was called with. This command will
also hook all of the methods available overloads unless a specific
overload is specified.

If the --include-backtrace flag is provided, a full stack trace that
lead to the methods invocation will also be dumped. This would aid in
discovering who called the original method.

Examples:
android hooking watch class_method com.example.test.login
android hooking watch class_method com.example.test.helper.executeQuery
android hooking watch class_method com.example.test.helper.executeQuery "java.lang.String,java.lang.String"
android hooking watch class_method com.example.test.helper.executeQuery --dump-backtrace
android hooking watch class_method com.example.test.login --dump-args --dump-return

那我们可以用 File 类的构造器来试一下效果。运行命令 android hooking watch class_method java.io.File.$init --dump-args

1
2
3
4
5
6
7
8
9
com.android.settings on (OPPO: 8.1.0) [usb] # android hooking watch class_method java.io.File.$init --dump-args
(agent) Attempting to watch class java.io.File and method $init.
(agent) Hooking java.io.File.$init(java.io.File, java.lang.String)
(agent) Hooking java.io.File.$init(java.lang.String)
(agent) Hooking java.io.File.$init(java.lang.String, int)
(agent) Hooking java.io.File.$init(java.lang.String, java.io.File)
(agent) Hooking java.io.File.$init(java.lang.String, java.lang.String)
(agent) Hooking java.io.File.$init(java.net.URI)
(agent) Registering job lxv3z0u0ep8. Type: watch-method for: java.io.File.$init

可以看到 objection 为我们 hookFile 构造器的所有重载,一共是 6 个。在设置界面随意进出几个子设置界面,可以看到命中很多次该方法的不同重载,每次参数的值也都不同。

1
2
3
4
5
6
7
8
com.android.settings on (OPPO: 8.1.0) [usb] # (agent) [lxv3z0u0ep8] Called java.io.File.File(java.lang.String)
(agent) [lxv3z0u0ep8] Arguments java.io.File.File(/system/media/theme/default/com.oppo.launcher)
(agent) [lxv3z0u0ep8] Called java.io.File.File(java.io.File, java.lang.String)
(agent) [lxv3z0u0ep8] Arguments java.io.File.File(/data/user_de/0/com.android.settings, shared_prefs)
(agent) [lxv3z0u0ep8] Called java.io.File.File(java.io.File, java.lang.String)
(agent) [lxv3z0u0ep8] Arguments java.io.File.File(/data/user_de/0/com.android.settings/shared_prefs, development.xml)
(agent) [lxv3z0u0ep8] Called java.io.File.File(java.io.File, java.lang.String)
......
ZenTracer(hook)

更大范围的 hook 工具 ZenTracer 请查看参考博客。地址

frida 参数打印与构造

首先解释一下下,这个是啥意思。

gson

Google 的库用于辅助构造后续的部分类型。(如 char[])。

配置步骤

  1. 下载 gson 压缩包,地址
  2. 将文件 利用 adb push 到手机中,解压到与 frida-server 放在一起。为了避免与引用了此包的 app 混淆,建议重命名一下。

使用方法

在注入的 js 代码中直接引用,如下所示:

1
2
3
4
Java.openClassFile("/data/local/tmp/r0gson.dex").load();
const gson = Java.use('com.r0ysue.gson.Gson');

console.log("charArray Object Object:",gson.$new().toJson(charArray));

这里看不懂没关系,下文中会在 char[] , byte[] 等的构造中,广泛用到。

参数打印

bytes2hex

传入的参数为 byte[],

example:[12, 0, 156, -127]

return: [0c, 00, 9c, fe]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function bytes2hex(arrBytes){
var str = "";
for (var i = 0; i < arrBytes.length; i++) {
var tmp;
var num = arrBytes[i];
if (num < 0) {
//此处填坑,当byte因为符号位导致数值为负时候,需要对数据进行处理
tmp = (255 + num + 1).toString(16);
} else {
tmp = num.toString(16);
}
if (tmp.length == 1) {
tmp = "0" + tmp;
}
if(i>0){
str += " "+tmp;
}else{
str += tmp;
}
}
return str;
}

string2bytes

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 string2bytes(str) {
var bytes = new Array();
var len, c;
len = str.length;
for(var i = 0; i < len; i++) {
c = str.charCodeAt(i);
if(c >= 0x010000 && c <= 0x10FFFF) {
bytes.push(((c >> 18) & 0x07) | 0xF0);
bytes.push(((c >> 12) & 0x3F) | 0x80);
bytes.push(((c >> 6) & 0x3F) | 0x80);
bytes.push((c & 0x3F) | 0x80);
} else if(c >= 0x000800 && c <= 0x00FFFF) {
bytes.push(((c >> 12) & 0x0F) | 0xE0);
bytes.push(((c >> 6) & 0x3F) | 0x80);
bytes.push((c & 0x3F) | 0x80);
} else if(c >= 0x000080 && c <= 0x0007FF) {
bytes.push(((c >> 6) & 0x1F) | 0xC0);
bytes.push((c & 0x3F) | 0x80);
} else { // 属于 扩展 ascii。
bytes.push(c & 0xFF);
}
}
return bytes;
}

bytes2string

据说这个转化在 Chrome 的 console 里面是没问题的,但是放入 frida 的脚本中,如果遇到非 unicode 解码范围的值就会报错。

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
 function bytes2string(arr) {
if(typeof arr === 'string') {
return arr;
}
var str = '',
_arr = arr;
for(var i = 0; i < _arr.length; i++) {
var one = _arr[i].toString(2),
v = one.match(/^1+?(?=0)/);
if(v && one.length == 8) {
var bytesLength = v[0].length;
var store = _arr[i].toString(2).slice(7 - bytesLength);
for(var st = 1; st < bytesLength; st++) {
store += _arr[st + i].toString(2).slice(2);
}
try {
str += String.fromCharCode(parseInt(store, 2));
} catch (error) {
str += parseInt(store, 2).toString();
console.log(error);
}
i += bytesLength - 1;
} else {
try {
str += String.fromCharCode(_arr[i]);
} catch (error) {
str += parseInt(store, 2).toString();
console.log(error);
}
}
}
return str;
}

也可以通过使用 Java 的函数来实现。

1
2
3
4
5
function bytesToString(value) {
var buffer = Java.array('byte', value);
var StringClass = Java.use('java.lang.String');
return StringClass.$new(buffer);
}

下面的就需要使用 Google 的 gson 库来进行辅助构造。同时还需要参阅 JNI 签名。

char[]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Java.openClassFile("/data/local/tmp/r0gson.dex").load();
const gson = Java.use('com.r0ysue.gson.Gson');

// 这里重载只是为了打印看一下 toString 前后的情况
Java.use("java.lang.Character").toString.overload('char').implementation = function(char){
var result = this.toString(char);
console.log("char,result",char,result);
return result;
}
//这里也是加了打印看一下情况
Java.use("java.util.Arrays").toString.overload('[C').implementation = function(charArray){
var result = this.toString(charArray);
console.log("charArray,result:",charArray,result)
console.log("charArray Object Object:",gson.$new().toJson(charArray));
return result;
}

byte[]

1
2
3
4
5
6
7
8
9
Java.openClassFile("/data/local/tmp/r0gson.dex").load();
const gson = Java.use('com.r0ysue.gson.Gson');

Java.use("java.util.Arrays").toString.overload('[B').implementation = function(byteArray){
var result = this.toString(byteArray);
console.log("byteArray,result):",byteArray,result)
console.log("byteArray Object Object:",gson.$new().toJson(byteArray));
return result;
}

byteBuffer

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
Java.openClassFile("/data/local/tmp/r0gson.dex").load();
const gson = Java.use('com.r0ysue.gson.Gson');

my_class.b.overload('java.nio.ByteBuffer', 'java.nio.ByteBuffer').implementation = function(x0,x1) {

var result = this.b(x0,x1);
//Suppose the byte[] in the Bytebuffer has a key value of 'hb'.
var bytesArray = bytesBuffer2bytesArray(x0, 'hb');
var tmp = gson.$new().toJson(x0);
tmp = JSON.parse(tmp);
console.log("tmp:"+typeof(tmp));
console.log("decrypt --> message: ",tmp["backingArray"]);
// console.log("decrypt --> message*hexdump: ",hexdump(gson.$new().toJson(x0)["backingArray"]));
// console.log("decrypt --> decryptTo: ",gson.$new().toJson(x1));
// console.log("decrypt --> decryptTo*hexdump: ",hexdump(gson.$new().toJson(x1)["backingArray"]));
return result;
}


function bytesBuffer2bytesArray(bytesBuffer, key) {
Java.openClassFile("/data/local/tmp/r0gson.dex").load();
const gson = Java.use('com.r0ysue.gson.Gson');
var tmp = gson.$new().toJson(bytesBuffer);
var dataKey = key
tmp = JSON.parse(tmp);
tmp = new Array(tmp[dataKey]);
tmp = tmp.toString();
var tmp_array = tmp.split(",");
var tmp_int_array=[];
tmp_int_array=tmp_array.map(function(data){
return +data;
});
return tmp_int_array
}

打印memorybuffer

1
2
3
4
// 打印内存
var view = new DataView(this.context.r0.readByteArray(12));
var value = '0x' + view.getUint8(11).toString(16) + view.getUint8(10).toString(16) + view.getUint8(9).toString(16) + view.getUint8(8).toString(16);

打印non-ascii

https://api-caller.com/2019/03/30/frida-note/#non-ascii
类名非 ASCII 字符串时,先编码打印出来, 再用编码后的字符串去 hook.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//场景hook cls.forName寻找目标类的classloader。
cls.forName.overload('java.lang.String', 'boolean', 'java.lang.ClassLoader').implementation = function (arg1, arg2, arg3) {
var clsName = cls.forName(arg1, arg2, arg3);
console.log('oriClassName:' + arg1)
var base64Name = encodeURIComponent(arg1)
console.log('encodeName:' + base64Name);
//通过日志确认base64后的非ascii字符串,下面对比并打印classloader
//clsName为特殊字符o.ÎÉ«
if ('o.%CE%99%C9%AB' == base64Name) {
//打印classloader
console.log(arg3);
}
return clsName;
}

hook enum

Java 代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
enum Signal {
GREEN, YELLOW, RED
}
public class TrafficLight {
public static Signal color = Signal.RED;
public static void main() {
Log.d("4enum", "enum "+ color.getClass().getName().toString());
switch (color) {
case RED:
color = Signal.GREEN;
break;
case YELLOW:
color = Signal.RED;
break;
case GREEN:
color = Signal.YELLOW;
break;
}
}
}

JavaScript 代码如下

1
2
3
4
5
6
7
8
9
10
Java.perform(function(){
Java.choose("com.r0ysue.a0526printout.Signal",{ // Signal 类的路径
onMatch:function(instance){
console.log("instance.name:",instance.name());
console.log("instance.getDeclaringClass:",instance.getDeclaringClass());
},onComplete:function(){
console.log("search completed!")
}
})
})

打印 hash map

1
2
3
4
5
6
7
8
9
10
11
Java.perform(function(){
Java.choose("java.util.HashMap",{
onMatch:function(instance){
if(instance.toString().indexOf("ISBN")!= -1){
console.log("instance.toString:",instance.toString());
}
},onComplete:function(){
console.log("search complete!")
}
})
})

参数构造

如果不仅仅是想打印参数,同时还想替换掉原来的参数,则需要自行先构造参数。

Java array 构造

使用 Java.array API 构造 charArray 。

Java.array 的解释文档。

1
2
3
4
5
6
7
8
9
10
11
/**
* Creates a Java array with elements of the specified `type`, from a
* JavaScript array `elements`. The resulting Java array behaves like
* a JS array, but can be passed by reference to Java APIs in order to
* allow them to modify its contents.
*
* @param type Type name of elements.
* @param elements Array of JavaScript values to use for constructing the
* Java array.
*/
function array(type: string, elements: any[]): any[];

构建代码如下

1
2
3
4
5
6
7
8
Java.use("java.util.Arrays").toString.overload('[C').implementation = function(charArray){
var newCharArray = Java.array('char', [ '一','去','二','三','里' ]);
var result = this.toString(newCharArray);
console.log("newCharArray,result:",newCharArray,result)
console.log("newCharArray Object Object:",gson.$new().toJson(newCharArray));
var newResult = Java.use('java.lang.String').$new(Java.array('char', [ '烟','村','四','五','家']))
return newResult;
}

类的多态与转型(Java.cast)

可以通过 getClass().getName().toString() 来查看当前实例的类型。

找到一个 instance ,通过 Java.cast 来强制转换对象的类型。

Java.cast 的解释文档

1
2
3
4
5
6
7
8
/**
* Creates a JavaScript wrapper given the existing instance at `handle` of
* given class `klass` as returned from `Java.use()`.
*
* @param handle An existing wrapper or a JNI handle.
* @param klass Class wrapper for type to cast to.
*/
function cast(handle: Wrapper | NativePointerValue, klass: Wrapper): Wrapper;

Java 代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Water { // 水 类
public static String flow(Water W) { // 水 的方法
// SomeSentence
Log.d("2Object", "water flow: I`m flowing");
return "water flow: I`m flowing";
}

public String still(Water W) { // 水 的方法
// SomeSentence
Log.d("2Object", "water still: still water runs deep!");
return "water still: still water runs deep!";
}
}
...
public class Juice extends Water { // 果汁 类 继承了水类

public String fillEnergy(){
Log.d("2Object", "Juice: i`m fillingEnergy!");
return "Juice: i`m fillingEnergy!";
}

查询处理代码

1
2
3
4
5
6
7
8
9
10
11
12
13
var JuiceHandle = null ;
Java.choose("com.r0ysue.a0526printout.Juice",{
onMatch:function(instance){
console.log("found juice instance",instance);
console.log("juice instance call fill",instance.fillEnergy());
JuiceHandle = instance;
},onComplete:function(){
console.log("juice handle search completed!")
}
})
console.log("Saved juice handle :",JuiceHandle);
var WaterHandle = Java.cast(JuiceHandle,Java.use("com.r0ysue.a0526printout.Water"))
console.log("call Waterhandle still method:",WaterHandle.still(WaterHandle));

Interface/Java.registerClass

1
2
3
public interface liquid {
public String flow();
}

frida 可以构建一个新的 class。 registerClass 的说明。

1
2
3
4
5
6
/**
* Creates a new Java class.
*
* @param spec Object describing the class to be created.
*/
function registerClass(spec: ClassSpec): Wrapper;

首先获取要实现的 interface ,然后调用 registerClass 来实现 interface。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Java.perform(function(){
var liquid = Java.use("com.r0ysue.a0526printout.liquid");
var beer = Java.registerClass({
name: 'com.r0ysue.a0526printout.beer',
implements: [liquid],
methods: {
flow: function () {
console.log("look, beer is flowing!")
return "look, beer is flowing!";
}
}
});
console.log("beer.bubble:",beer.$new().flow())
})

参考链接

参考连接1:安装搭建 frida https://cloud.tencent.com/developer/article/1610276

参考连接2:安装和用法 https://eternalsakura13.com/2020/07/04/frida/

参考连接3:objection https://www.anquanke.com/post/id/197657

参考链接4:frida 打印与参数构造 https://www.wangt.cc/2020/11/frida%E6%89%93%E5%8D%B0%E4%B8%8E%E5%8F%82%E6%95%B0%E6%9E%84%E9%80%A0/

参考链接5: frida hook 常用函数分享 https://www.52pojie.cn/thread-1196917-1-1.html