安卓 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)
device = frida.get_usb_device()
pid = device.spawn(["com.example.app"]) device = resume(pid) time.sleep(1) session = device.attach(pid)
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
| 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 }
|
1 2 3 4 5 6 7 8 9 10 11
| 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; } 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
|
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); 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
|
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", "")
raw = base64.b64decode(b64_str) decoded = raw.decode("utf-8", errors="replace") print("[*] decoded:", decoded)
user, pw = decoded.split(":", 1)
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()
|