安卓 day3
怎么隔了这么多天?

@source: https://github.com/r0ysue/AndroidSecurityStudy/tree/master/FRIDA/A02#%E4%B8%AD%E7%BA%A7%E8%83%BD%E5%8A%9B%E8%BF%9C%E7%A8%8B%E8%B0%83%E7%94%A8

python loader

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import frida, time

def on_message(message, data):
if message['type'] == 'error':
print("[!] " + message['stack'])
elif message['type'] == 'send':
print("[*] " + message['payload'])
else:
print(message)

# 连接设备上的 frida-server
device = frida.get_usb_device()
# 启动目标 app
pid = device.spawn(["com.example.app"])
device = resume(pid) # spawn 会把进程挂起,resume 运行
time.sleep(1) # 等待 app 启动
session = device.attach(pid)
# 加载 js 脚本
with open(s1.js) as f:
script = session.create_script(f.read())
script.on('message', on_message)
script.load()

input()

frida script

Java.perform(function x) 与安卓交互

var target_class = Java.use("com.example.app.MainActivity") 定位到 Java 类

定位到的类可以直接用 target_class.fun.implementation = function() {} 获取(修改)参数、返回值(var ret_value = this.fun(...) 调用原函数并获取返回值)。如果 fun 有重载,用 target_class.fun.overload("int", "int").implementation... 指定

需要用到 java.lang 中的类时用 Java.use("java.lang.String").$new("My test String") 实例化

Java.choose

类中未调用的方法没办法直接 hook,用 Java.choose(className, callBacks) 拿到对象后调用。

1
2
3
4
5
6
7
8
9
10
11
Java.perform(function() {
Java.choose("com.example.app.MainActivity", {
onMatch: function(instance) {
console.log("Found instance:", instance);
console.log("secret() =>", instance.secret());
},
onComplete: function() {
console.log("Done.");
}
});
});

回调语义

  • onMatch(instance) 每找到一个存活实例就回调一次。onMatch 里面可以调用方法、读写字段(instance.someFiled.value = 123;)、保存引用以供后续使用(saved = instance;)、控制扫描(return “stop”; // 找到一个实例就停止扫描)
  • onComplete() 扫描结束后回调一次

原理

  • 在内存里枚举对象引用(heap enumeration / instance enumeration)
  • 判断对象是否为目标类,命中则传给 onMatch

RPC (Remote Procedure Call)

可以把安卓的函数导出为 python 符号,在 python 端调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// s3.js
console.log("Script loaded successfully");

function callScretFun() { // 导出函数
Java.perform(function() {
Java.choose("com.example.app.MainActivity", {
onMatchL function(instance) {
console.log("Found instance: " + instance);
console.log("secret() => ") + instance.secret());
},
onComplete: function() {}
});
});
}

rpc.expoets = {
callsecretfunction: callSecretFun // 把 callSecretFun 导出为 callsecretfunction 符号,导出名不能有大写字母和下划线
}
1
2
3
4
5
6
7
8
9
10
11
# loader3.py
import frida, time

...
command = ""
while True:
command = input("Enter command:\n1: Exit\n2: Call secret function\nchoice:")
if command == "1":
break
elif command == "2": # 调用点
script.exports.callsecretfunction()

send() recv()

1
2
3
4
5
6
recv('head', function(message) {
console.log(message.payload);
console.log(message.nonmirror);
message.nonmirror = "no";
send(message)
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import frida

def on_message(message, data):
print(message)

session = frida.attach("hello")
with open("send.js") as f:
script = session.create_script(f.read())

script.on('message', on_message)
script.load()
script.post({'type': 'head', 'payload': 123, 'nonmirror': 'yes'})
input()

"""
123
yes
{'type': 'send', 'payload': {'type': 'head', 'payload': 123, 'nonmirror': 'no'}}
"""

message 格式为 json,字段用 . 读写。send 的数据放在 message 的 payload 字段

动态修改绕过判断的思路

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
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;
}
// setText 模拟向服务器发送请求,即 hook 目标
message_tv.setText("Sending to the server: " + Base64.encodeToString((username_et.getText().toString() + ":" + password_et.getText().toString()).getBytes(), Base64.DEFAULT));
}
});
}
}

username_et, password_et 为用户输入,message_tv 为回显字符。为绕过 admin 的判断,只需要 hook setText() 获取输入的用户名密码,再把用户名改成 admin 即可。中间有一些编码解码的过程,借助 loader 完成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// s4.js

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); // 将数据("Sending to the server: " + Base64.encodeToString...)发给 python
recv(function (received_json_object) {
string_to_recv = received_json_object.modified_data
console.log("string_to_recv: " + string_to_recv);
}).wait(); // 收到数据之后再执行下去

return this.setText(Java.use("java.lang.String").$new(string_to_recv));
}
});

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
# loader4.py

import time
import base64
import frida


def on_message(message, payload):
print('message:', message)
if message["type"] == "error":
print("[!] " + message.get("stack", str(message)))
return

if message["type"] != "send":
print(message)
return

data = message["payload"]
print("[*] payload:", data)

b64_str = data.rsplit(":", 1)[1].strip()
b64_str = b64_str.replace("\n", "").replace("\r", "")

# 解码:str -> bytes -> str
raw = base64.b64decode(b64_str)
decoded = raw.decode("utf-8", errors="replace")
print("[*] decoded:", decoded)

user, pw = decoded.split(":", 1)

# 改成 admin:pw 再 Base64 编码回去
new_plain = f"admin:{pw}".encode("utf-8")
new_b64 = base64.b64encode(new_plain).decode("utf-8")

print("[*] encoded data:", new_b64)
script.post({"modified_data": new_b64})
print("[*] Modified data sent")


device = frida.get_usb_device()
pid = device.spawn(["com.example.myapplication2"])
device.resume(pid)
time.sleep(1)

session = device.attach(pid)
with open("s4.js", "r", encoding="utf-8") as f:
script = session.create_script(f.read())

script.on("message", on_message)
script.load()
input()