การ Export อินเทอร์เฟซโดยใช้ Frida

คุณสามารถใช้ฟังก์ชันนี้เพื่อ export เมธอดจาก Frida RPC และเรียกใช้เป็นส่วนขยายอินเทอร์เฟซของ FIRERPA ได้ คุณสามารถควบคุมแอปพลิเคชันได้อย่างเต็มที่โดยการเขียนโค้ด Hook ที่มีฟังก์ชันการทำงานเฉพาะด้วยตนเอง ฟังก์ชันนี้ต้องการให้คุณคุ้นเคยกับการเขียนสคริปต์ Frida hook หากคุณมีสคริปต์อยู่แล้ว คุณจะต้องแก้ไขสคริปต์เล็กน้อยเพื่อใช้เมธอดเฉพาะของเรา

ข้อควรสนใจ

ตั้งแต่เวอร์ชัน 9.0 เป็นต้นไป Frida 17.x ที่เราใช้ภายในจะต้องมีการแพ็คเกจ `frida-java-bridge` เข้าไปในสคริปต์ด้วยตนเอง มิฉะนั้นจะเกิดข้อผิดพลาดเกี่ยวกับ `Java not defined` การเปลี่ยนแปลงนี้เป็นการเปลี่ยนแปลงอย่างเป็นทางการจาก Frida ซึ่งตามคำแนะนำการเปลี่ยนแปลงอย่างเป็นทางการ คุณจะต้องสร้างโปรเจกต์ Node.js ใหม่และนำเข้า Java bridge สำหรับรายละเอียดเพิ่มเติม โปรดอ้างอิงที่ https://github.com/oleavr/frida-agent-example หรือใช้ `tools/frida_script_generate.py` ที่เรามีให้เพื่อทำการ encapsulate สคริปต์ JS เดิมของคุณอีกครั้ง

การเขียนสคริปต์สำหรับ Export

คุณต้องเขียนสคริปต์ในรูปแบบเฉพาะ ชื่อฟังก์ชันที่ export ต้องเป็นไปตามหลักการตั้งชื่อ โดยต้องเป็นรูปแบบ camelCase ที่ขึ้นต้นด้วยตัวพิมพ์เล็ก สำหรับชื่อที่เป็นคำจำกัดความ เช่น HTTP การตั้งชื่อเมธอดไม่ควรใช้ตัวพิมพ์ใหญ่ทั้งหมด ตัวอย่างเช่น ชื่อ sendHTTPRequest ควรเขียนเป็น sendHttpRequest ในสคริปต์ที่ export แทนที่จะใช้ HTTP เป็นตัวพิมพ์ใหญ่ทั้งหมด ด้านล่างนี้คือโครงสร้างโดยรวมที่สคริปต์ควรปฏิบัติตาม

Java.perform(function() {
const String = Java.use("java.lang.String")
rpc.exports.exampleFunc1 =  function (a, b) {
        return performRpcJVMCall(function() {
                // ทำงานบน JVM thread
                return String.$new("Execute on JVM thread:" + a + b).toString()
        })
}
rpc.exports.exampleFunc2 = function (a, b) {
        return performRpcJVMCallOnMain(function() {
                // ทำงานบน Main UI thread
                return String.$new("Execute on Main UI thread:" + a + b).toString()
        })
}
rpc.exports.exampleFunc3 = function (a, b) {
        return performRpcCall(function() {
                // ทำงานโค้ด JS ปกติ
                return a + b
        })
}})

ในสคริปต์ตัวอย่างข้างต้น คุณจะเห็นการกำหนดเมธอดสามรูปแบบ ซึ่งบล็อกโค้ดในรูปแบบ return performRpc เป็นสิ่งที่จำเป็นต้องใช้ เพื่อให้แน่ใจว่าค่าของคุณจะถูกส่งคืนอย่างถูกต้อง บล็อกโค้ดการเรียก performRpc ทั้งสามแบบนี้มีความหมายดังต่อไปนี้:

return performRpcJVMCall(function() {
        // ทำงานบน JVM thread
})

บล็อกโค้ด performRpcJVMCall หมายถึงการรันโค้ดของคุณใน JVM คุณสามารถใช้ฟังก์ชันที่เกี่ยวข้องกับ JVM ภายในบล็อกนี้ได้ เช่น Java.use หรือการดำเนินการอื่นๆ ที่เกี่ยวข้องกับเลเยอร์ Java ของแอปพลิเคชัน

return performRpcJVMCallOnMain(function() {
        // ทำงานบน UI thread
})

บล็อกโค้ด performRpcJVMCallOnMain หมายถึงการรันโค้ดของคุณใน JVM และบน UI thread หลัก สำหรับการดำเนินการที่เกี่ยวข้องกับ UI หรือเธรดหลัก คุณต้องรันโค้ดภายในบล็อกนี้จึงจะสำเร็จ ในขณะเดียวกันต้องระวังไม่ให้โค้ดของคุณบล็อกการทำงานของเธรดหลัก มิฉะนั้นอาจทำให้แอปพลิเคชันไม่ตอบสนองหรือแครชได้

return performRpcCall(function() {
        // ทำงานโค้ด JS ปกติ
})

บล็อกโค้ด performRpcCall คุณสามารถรันได้เฉพาะโค้ด JS พื้นฐานเท่านั้น และไม่สามารถใช้ตรรกะของเลเยอร์ Java ภายในบล็อกนี้ได้

ข้อควรสนใจ

คุณอาจยังคงต้องใช้ `Java.perform` เพื่อครอบบล็อกโค้ดทั้งหมดเพื่อให้แน่ใจว่าตรรกะของ Java ทำงานได้อย่างถูกต้อง

ตอนนี้คุณได้เข้าใจตรรกะพื้นฐานในการเขียนโค้ดของเราแล้ว คุณสามารถนำโค้ดของคุณมาปรับให้เข้ากับรูปแบบนี้ได้ หากคุณเพียงต้องการทดสอบ ก็สามารถคัดลอกโค้ดส่วนนี้ไปใช้เพื่อเริ่มต้นได้เลย ในส่วนต่อไปเราจะใช้โค้ด Hook นี้ในการอธิบาย

การ Inject สคริปต์สำหรับ Export

วิธีการ inject สคริปต์สำหรับ export ในส่วนนี้เหมือนกับวิธีการใช้งานในบท Persistent Frida Script ทุกประการ

app = d.application("com.android.settings")
app.attach_script(script, runtime=ScriptRuntime.RUNTIME_QJS, standup=5)

หลังจากเรียกใช้อินเทอร์เฟซสำหรับ inject แล้ว สคริปต์ของคุณควรจะถูก inject เข้าไปเรียบร้อย คุณสามารถรันคำสั่งต่อไปนี้เพื่อตรวจสอบว่าสคริปต์ทำงานปกติหรือไม่

app.is_script_alive()

การเรียกใช้เมธอดที่ Export

วิธีการเรียกใช้เมธอดที่ export ภายในสคริปต์นั้นคล้ายกับการใช้งานอินเทอร์เฟซ FIRERPA แบบปกติมาก โปรดดูโค้ดตัวอย่าง

app = d.application("com.android.settings")
app.exampleFunc1("FIRE", "RPA")

สำหรับแอปพลิเคชันโคลน (multi-instance) คุณต้องรับ instance ของแอปนั้นๆ

app = d.application("com.android.settings", user=UID)
app.exampleFunc1("FIRE", "RPA")

วิธีการเรียกใช้ข้างต้นเทียบเท่ากับวิธีด้านล่างนี้ คุณจะสังเกตเห็นว่า Func1 ในเมธอดเปลี่ยนเป็น _func1 ซึ่งสามารถใช้ได้ทั้งสองแบบ

app.example_func1("FIRE", "RPA")
>>> app.example_func1("FIRE", "RPA")
'Hello World:FIRERPA'
>>> app.example_func2("FRI", "DA")
'Hello World:FRIDA'
>>> app.example_func3("FIRE", "RPA")
'FIRERPA'

การเรียกใช้เมธอดที่ Export (ผ่าน HTTP Interface)

นอกจากการเรียกใช้ผ่าน client แล้ว เรายังรองรับการเรียกใช้ผ่าน HTTP ด้วย คุณสามารถใช้งานได้ตามรูปแบบต่อไปนี้

เคล็ดลับ

ตั้งแต่เวอร์ชัน 8.15 เป็นต้นไป เมธอดที่ export ยังรองรับการเรียกใช้ผ่านโปรโตคอล jsonrpc 2.0 (ยังไม่รองรับโหมด MultiCall)

คุณสามารถใช้ jsonrpclib เพื่อเรียกใช้โดยตรงดังตัวอย่างด้านล่าง หรือหากคุณคุ้นเคยกับโปรโตคอล JsonRPC 2.0 คุณก็สามารถเขียนโค้ดเพื่อส่งคำขอด้วยตนเองได้ ซึ่งเป็นวิธีที่เป็นมาตรฐานและมีเอกสารอ้างอิงครบถ้วนที่สุด

import jsonrpclib
server = jsonrpclib.Server('http://192.168.0.2:65000/script/com.android.settings/0')
server.example_func1("FIRE", "RPA")

แน่นอนว่าคุณยังสามารถใช้โปรโตคอลการเรียกใช้แบบเดิมดังต่อไปนี้ได้ ซึ่งค่อนข้างง่ายกว่าแต่ไม่เป็นมาตรฐานเท่า

import json
import requests
url = "http://192.168.0.2:65000/script/com.android.settings/exampleFunc1?user=0"
res = requests.post(url, data={"args": json.dumps(["FIRE", "RPA"])})
print (res.status_code, res.json()["result"])

query parameter user ในลิงก์คือ UID ของแอปพลิเคชันโคลน ค่าเริ่มต้นคือ 0 ซึ่งไม่จำเป็นต้องระบุ หากเป็นแอปพลิเคชันโคลน จะต้องระบุ UID ที่นี่

โค้ดข้างต้นเป็นการเรียกใช้อินเทอร์เฟซที่ export จากสคริปต์ผ่าน HTTP com.android.settings คือชื่อแพ็คเกจของแอปพลิเคชัน และ exampleFunc1 คือชื่อฟังก์ชันที่ export พารามิเตอร์ args ของคำขอจะต้องถูก serialize โดยใช้ json.dumps รายการพารามิเตอร์สามารถมีได้หลายตัว ขึ้นอยู่กับจำนวนพารามิเตอร์ของเมธอดของคุณ การส่งรายการว่าง [] หมายความว่าฟังก์ชันที่ export นั้นไม่มีพารามิเตอร์

หาก FIRERPA ของคุณเปิดใช้งาน certificate สำหรับอินเทอร์เฟซ คุณจะต้องเข้าถึงผ่าน https และต้องระบุรหัสผ่านของ certificate

headers = {"X-Token": "รหัสผ่าน certificate"}
res = requests.post(url, data={"args": json.dumps(["LAM", "DA"])}, headers=headers, verify=False)
print (res.status_code, res.json()["result"])

รหัสสถานะ HTTP (HTTP Status Codes)

การเรียกใช้ผ่าน HTTP interface จะมีรหัสสถานะเฉพาะ ซึ่งคุณอาจต้องใช้ในการตัดสินใจ สถานะและความหมายมีดังนี้

รหัสสถานะคำอธิบายรหัสสถานะ
200ทุกอย่างปกติ
410สคริปต์ยังไม่ได้ inject หรือยังไม่ได้ติดตั้ง
500สคริปต์หรือพารามิเตอร์ผิดปกติ
400พารามิเตอร์ไม่ถูกต้อง

การแก้ไขปัญหา

หากคุณพบปัญหาการค้างหรือ timeout เมื่อเรียกใช้อินเทอร์เฟซผ่าน API หรือ HTTP interface มีความเป็นไปได้สูงว่าแอปพลิเคชันของคุณถูกระบบบังคับให้พักการทำงาน (sleep) เนื่องจากอยู่ใน background ดังนั้นคุณอาจต้องทำให้แอปพลิเคชันทำงานอยู่เบื้องหน้า (foreground) ตลอดเวลา