安卓 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#

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) 拿到对象后调用。

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 端调用

// 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 符号,导出名不能有大写字母和下划线
}
# 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()#

recv('head', function(message) {
    console.log(message.payload);
    console.log(message.nonmirror);
    message.nonmirror = "no";
    send(message)
});
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 字段

动态修改绕过判断的思路#

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 完成。

// 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));
		}
});
# 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()