FRIDA - API使用篇:rpc、Process、Module、Memory 使用方法及示例
官方 API (JavaScript API):https://frida.re/docs/javascript-api/
From:?(?FRIDA-API使用篇 ):https://www.anquanke.com/post/id/195215
前言
在這篇文章中來對其官方的一些非常常用的 API 進行學習。所謂工欲善其事,必先利其器。想要好好學習 FRIDA 我們就必須對 FRIDA API 深入的學習以對其有更深的了解和使用,通常大部分核心原理也在官方 API 中寫著,我們學會來使用一些案例來結合 API 的使用。
注意:運行以下任何代碼時都需要提前啟動手機中的 frida-server 文件。
系列文章目錄搬新“家”了,地址:https://github.com/r0ysue/AndroidSecurityStudy?,接下來窩會努力寫更多喔 ~
rpc、Process、Module、Memory使用方法及示例
1.1 FRIDA 輸出打印
1.1.1 console 輸出
不論是什么語言都好,第一個要學習總是如何 輸出和打印,那我們就來學習在 FRIDA 打印值。
在官方 API 有兩種打印的方式,分別是:console、send,
我們先來學習非常的簡單的 console,這里我創建一個 js 文件,代碼示例如下。
function hello_printf() {Java.perform(function () {console.log("");console.log("hello-log");console.warn("hello-warn");console.error("hello-error");}); } setImmediate(hello_printf,0);當文件創建好之后,我們需要運行在手機中安裝的 frida-server 文件,在上一章我們學過了如何安裝在 android 手機安裝 frida-server,現在來使用它,我們在 ubuntu 中開啟一個終端,運行以下代碼,啟動我們安裝好的 frida-server 文件。
roysue@ubuntu:~$ adb shell sailfish:/ $ su sailfish:/ $ ./data/local/tmp/frida-server然后執行以下代碼,對目標應用 app 的進程 com.roysue.roysueapplication 使用 -l 命令注入 Chap03.js 中的代碼。1-1以及執行腳本之后的效果圖1-1!
frida -U com.roysue.roysueapplication -l Chap03.js圖1-1 終端執行
可以到終點已經成功注入了腳本并且打印了 hello,但是顏色不同,這是 log 的級別的原因,在 FRIDA 的 console 中有三個級別分別是 log、warn、error。
| log | 正常 |
| warn | 警告 |
| error | 錯誤 |
1.1.2 console 之 hexdump
console 的三個級別:
- error 級別最為嚴重,
- 其次 warn,
- 但是一般在使用中我們只會使用 log 來輸出想看的值;
然后我們繼續學習 console 的好兄弟,hexdump,其含義:打印內存中的地址,target 參數可以是 ArrayBuffer 或者 NativePointer,而 options 參數則是自定義輸出格式,可以填這幾個參數 offset、lengt、header、ansi。
hexdump 代碼示例以及執行效果如下。
var libc = Module.findBaseAddress('libc.so'); console.log(hexdump(libc, {offset: 0,length: 64,header: true,ansi: true }));0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF 00000000 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 .ELF............ 00000010 03 00 28 00 01 00 00 00 00 00 00 00 34 00 00 00 ..(.........4... 00000020 34 a8 04 00 00 00 00 05 34 00 20 00 08 00 28 00 4.......4. ...(. 00000030 1e 00 1d 00 06 00 00 00 34 00 00 00 34 00 00 00 ........4...4...1.1.3 send
send 是在 python 層定義的 on_message 回調函數,jscode 內所有的信息都被監控 script.on('message',?on_message),當輸出信息的時候 on_message 函數會拿到其數據再通過 format 轉換, 其最重要的功能也是最核心的是能夠直接將數據以 json 格式輸出,當然數據是二進制的時候也依然是可以使用 send,十分方便,我們來看代碼1-2示例以及執行效果。
# -*- coding: utf-8 -*- import frida import sysdef on_message(message, data):if message['type'] == 'send':print("[*] {0}".format(message['payload']))else:print(message)jscode = """Java.perform(function () {var jni_env = Java.vm.getEnv();console.log(jni_env);send(jni_env);});"""process = frida.get_usb_device().attach('com.roysue.roysueapplication') script = process.create_script(jscode) script.on('message', on_message) script.load() sys.stdin.read()運行腳本效果如下:roysue@ubuntu:~/Desktop/Chap09$ python Chap03.py [object Object] [*] {'handle': '0xdf4f8000', 'vm': {}}可以看出這里兩種方式輸出的不同的效果,
- console 直接輸出了[object Object],無法輸出其正常的內容,因為 jni_env實際上是一個對象,
- 但是使用 send 的時候會自動將對象轉 json 格式輸出。通過對比,我們就知道 send 的好處啦~
1.2 FRIDA 變量類型
學完輸出之后,我們來學習如何聲明變量類型。
| 1 | new Int64(v) | 定義一個有符號Int64類型的變量值為v,參數v可以是字符串或者以0x開頭的的十六進制值 |
| 2 | new UInt64(v) | 定義一個無符號Int64類型的變量值為v,參數v可以是字符串或者以0x開頭的的十六進制值 |
| 3 | new NativePointer(s) | 定義一個指針,指針地址為s |
| 4 | ptr(“0”) | 同上 |
代碼示例以及效果
Java.perform(function () {console.log("");console.log("new Int64(1):"+new Int64(1));console.log("new UInt64(1):"+new UInt64(1));console.log("new NativePointer(0xEC644071):"+new NativePointer(0xEC644071));console.log("new ptr('0xEC644071'):"+new ptr(0xEC644071)); });輸出效果如下:new Int64(1):1new UInt64(1):1new NativePointer(0xEC644071):0xec644071new ptr('0xEC644071'):0xec644071frida 也為 Int64(v) 提供了一些相關的 API:
| 1 | add(rhs)、sub(rhs)、and(rhs)、or(rhs)、xor(rhs) | 加、減、邏輯運算 |
| 2 | shr(N)、shl(n) | 向右/向左移位n位生成新的Int64 |
| 3 | Compare(Rhs) | 返回整數比較結果 |
| 4 | toNumber() | 轉換為數字 |
| 5 | toString([radix=10]) | 轉換為可選基數的字符串(默認為10) |
我也寫了一些使用案例,代碼如下。
function hello_type() {Java.perform(function () {console.log("");//8888 + 1 = 8889console.log("8888 + 1:"+new Int64("8888").add(1));//8888 - 1 = 8887console.log("8888 - 1:"+new Int64("8888").sub(1));//8888 << 1 = 4444console.log("8888 << 1:"+new Int64("8888").shr(1));//8888 == 22 = 1 1是falseconsole.log("8888 == 22:"+new Int64("8888").compare(22));//轉stringconsole.log("8888 toString:"+new Int64("8888").toString());}); }代碼執行效果如圖1-2。
圖1-2 Int64 API
1.3 RPC 遠程調用
可以替換或插入的空對象,以向應用程序公開RPC樣式的API。該鍵指定方法名稱,該值是導出的函數。此函數可以返回一個純值以立即返回給調用方,或者承諾異步返回。也就是說可以通過rpc的導出的功能使用在python層,使python層與js交互,官方示例代碼有Node.js版本與python版本,我們在這里使用python版本,代碼如下。
1.3.1 遠程調用代碼示例
import fridadef on_message(message, data):if message['type'] == 'send':print(message['payload'])elif message['type'] == 'error':print(message['stack'])session = frida.get_usb_device().attach('com.roysue.roysueapplication')source = """rpc.exports = {add: function (a, b) {return a + b;},sub: function (a, b) {return new Promise(function (resolve) {setTimeout(function () {resolve(a - b);}, 100);});}}; """script = session.create_script(source) script.on('message', on_message) script.load() print(script.exports.add(2, 3)) print(script.exports.sub(5, 3)) session.detach()1.3.2 遠程調用代碼示例詳解
官方源碼示例是附加在目標進程為 iTunes,再通過將 rpc 的 ./agent.js 文件讀取到 source,進行使用。我這里修改了附加的目標的進程以及直接將 rpc 的代碼定義在 source 中。我們來看看這段是咋運行的,仍然先對目標進程附加,然后在寫 js 中代碼,也是 source 變量,通過 rpc.exports 關鍵字定義需要導出的兩個函數,上面定義了 add函數 和 sub函數,兩個的函數寫作方式不一樣,大家以后寫按照 add 方法寫就好了,sub 稍微有點復雜。聲明完函數之后創建了一個腳本并且注入進程,加載了腳本之后可以到 print(script.exports.add(2, 3)) 以及 print(script.exports.sub(5, 3)),在 python 層直接調用。add 的返回的結果為5,sub則是2,下見下圖1-3。
圖1-3 執行python腳本
1.4 Process 對象
我們現在來介紹以及使用一些Process對象中比較常用的api~
1.4.1 Process.id
Process.id:返回附加目標進程的PID
1.4.2 Process.isDebuggerAttached()
Process.isDebuggerAttached():檢測當前是否對目標程序已經附加
1.4.3 Process.enumerateModules()
枚舉當前加載的模塊,返回模塊對象的數組。
Process.enumerateModules() 會?枚舉當前所有已加載的 so 模塊,并且返回了 Module對象 的 數組,
Module對象下一節我們來詳細說,在這里我們暫時只使用 Module對象的name屬性。
function frida_Process() {Java.perform(function () {var process_Obj_Module_Arr = Process.enumerateModules();for(var i = 0; i < process_Obj_Module_Arr.length; i++) {console.log("",process_Obj_Module_Arr[i].name);}}); } setImmediate(frida_Process,0);我來們開看看這段 js 代碼寫了啥:在 js 中能夠直接使用 Process 對象的所有 api,調用了 Process.enumerateModules() 方法之后會返回一個數組,數組中存儲 N 個叫 Module 的 對象,既然已經知道返回了的是一個數組,很簡單我們就來 for 循環它便是,這里我使用下標的方式調用了 Module對象的 name 屬性,name 是 so 模塊的名稱。見下圖1-4。
圖1-4 終端輸出了所有已加載的so
1.4.4 Process.enumerateThreads()
Process.enumerateThreads():枚舉當前所有的線程,返回包含以下屬性的對象數組:
| 1 | id | 線程id |
| 2 | state | 當前運行狀態有running, stopped, waiting, uninterruptible or halted |
| 3 | context | 帶有鍵pc和sp的對象,它們是分別為ia32/x64/arm指定EIP/RIP/PC和ESP/RSP/SP的NativePointer對象。也可以使用其他處理器特定的密鑰,例如eax、rax、r0、x0等。 |
使用代碼示例如下:
function frida_Process() {Java.perform(function () {var enumerateThreads = Process.enumerateThreads();for(var i = 0; i < enumerateThreads.length; i++) {console.log("");console.log("id:",enumerateThreads[i].id);console.log("state:",enumerateThreads[i].state);console.log("context:",JSON.stringify(enumerateThreads[i].context));}}); } setImmediate(frida_Process,0);獲取當前所有線程,之后返回了一個數組,然后循環輸出它的值,如下圖1-5。
圖1-4 終端執行
1.4.5 Process.getCurrentThreadId()
Process.getCurrentThreadId():獲取此線程的操作系統特定 ID 作為數字
1.5 Module 對象
3.4 章節中 Process.EnumererateModules() 方法返回了就是一個 Module對象,咱們這里來詳細說說 Module對象,先來瞧瞧它都有哪些屬性。
1.5.1 Module對象 的 屬性
| 1 | name | 模塊名稱 |
| 2 | base | 模塊地址,其變量類型為NativePointer |
| 3 | size | 大小 |
| 4 | path | 完整文件系統路徑 |
除了屬性我們再來看看它有什么方法。
1.5.2 Module對象 的 API
| 1 | Module.load() | 加載指定so文件,返回一個Module對象 |
| 2 | enumerateImports() | 枚舉所有Import庫函數,返回Module數組對象 |
| 3 | enumerateExports() | 枚舉所有Export庫函數,返回Module數組對象 |
| 4 | enumerateSymbols() | 枚舉所有Symbol庫函數,返回Module數組對象 |
| 5 | Module.findExportByName(exportName)、Module.getExportByName(exportName) | 尋找指定so中export庫中的函數地址 |
| 6 | Module.findBaseAddress(name)、Module.getBaseAddress(name) | 返回so的基地址 |
findExportByName(exportName) 和?getExportByName(exportName) 區別:
findExportByName 參數就是 lib 中 函數的名字 ( 推薦用這個?),例如:
var string_with_jni_addr = Module.findExportByName("libnative-lib.so","Java_myapplication_example_com_ndk_1demo_MainActivity_stringWithJNI" )getExportByName 參數是 需要在 ida 中查找的,例如:
?
1.5.3 Module.load()
在 frida-12-5 版本中更新了該 API,主要用于加載指定 so 文件,返回一個 Module對象。
使用代碼示例如下:
function frida_Module() {Java.perform(function () {//參數為so的名稱 返回一個Module對象const hooks = Module.load('libhello.so');//輸出console.log("模塊名稱:",hooks.name);console.log("模塊地址:",hooks.base);console.log("大小:",hooks.size);console.log("文件系統路徑",hooks.path);}); } setImmediate(frida_Module,0);輸出如下: 模塊名稱: libhello.so 模塊地址: 0xdf2d3000 大小: 24576 文件系統路徑 /data/app/com.roysue.roysueapplication-7adQZoYIyp5t3G5Ef5wevQ==/lib/arm/libhello.so這邊如果去除判斷的話會打印所有加載的so的信息,這里我們就知道了哪些方法返回了Module對象了,然后我們再繼續深入學習Module對象自帶的API。
1.5.5 enumerateImports()
該 API 會枚舉模塊中所有的?Import 函數,示例代碼如下。
function frida_Module() {Java.perform(function () {const hooks = Module.load('libhello.so');var Imports = hooks.enumerateImports();for(var i = 0; i < Imports.length; i++) {//函數類型console.log("type:",Imports[i].type);//函數名稱console.log("name:",Imports[i].name);//屬于的模塊console.log("module:",Imports[i].module);//函數地址console.log("address:",Imports[i].address);}}); } setImmediate(frida_Module,0);輸出如下: [Google Pixel::com.roysue.roysueapplication]-> type: function name: __cxa_atexit module: /system/lib/libc.so address: 0xf58f4521 type: function name: __cxa_finalize module: /system/lib/libc.so address: 0xf58f462d type: function name: __stack_chk_fail module: /system/lib/libc.so address: 0xf58e2681 ...1.5.6 enumerateExports()
該 API 會枚舉模塊中所有的 Export 函數,示例代碼如下。
function frida_Module() {Java.perform(function () {const hooks = Module.load('libhello.so');var Exports = hooks.enumerateExports();for(var i = 0; i < Exports.length; i++) {//函數類型console.log("type:",Exports[i].type);//函數名稱console.log("name:",Exports[i].name);//函數地址console.log("address:",Exports[i].address);}}); } setImmediate(frida_Module,0);輸出如下: [Google Pixel::com.roysue.roysueapplication]-> type: function name: Java_com_roysue_roysueapplication_hellojni_getSum address: 0xdf2d411b type: function name: unw_save_vfp_as_X address: 0xdf2d4c43 type: function address: 0xdf2d4209 type: function ...1.5.7 enumerateSymbols()
代碼示例如下。
function frida_Module() {Java.perform(function () {const hooks = Module.load('libc.so');var Symbol = hooks.enumerateSymbols();for(var i = 0; i < Symbol.length; i++) {console.log("isGlobal:",Symbol[i].isGlobal);console.log("type:",Symbol[i].type);console.log("section:",JSON.stringify(Symbol[i].section));console.log("name:",Symbol[i].name);console.log("address:",Symbol[i].address);}}); } setImmediate(frida_Module,0);輸出如下: isGlobal: true type: function section: {"id":"13.text","protection":"r-x"} name: _Unwind_GetRegionStart address: 0xf591c798 isGlobal: true type: function section: {"id":"13.text","protection":"r-x"} name: _Unwind_GetTextRelBase address: 0xf591c7cc ...1.5.8 Module.findExportByName(exportName), Module.getExportByName(exportName)
返回 so文件 中 Export函數庫 中 函數名稱為exportName函數 的 絕對地址。
代碼示例如下。
function frida_Module() {Java.perform(function () {Module.getExportByName('libhello.so', 'c_getStr')console.log("Java_com_roysue_roysueapplication_hellojni_getStr address:",Module.findExportByName('libhello.so', 'Java_com_roysue_roysueapplication_hellojni_getStr'));console.log("Java_com_roysue_roysueapplication_hellojni_getStr address:",Module.getExportByName('libhello.so', 'Java_com_roysue_roysueapplication_hellojni_getStr'));}); } setImmediate(frida_Module,0);輸出如下: Java_com_roysue_roysueapplication_hellojni_getStr address: 0xdf2d413d Java_com_roysue_roysueapplication_hellojni_getStr address: 0xdf2d413d1.5.9 Module.findBaseAddress(name)、Module.getBaseAddress(name)
返回 name 模塊的基地址。
代碼示例如下。
function frida_Module() {Java.perform(function () {var name = "libhello.so";console.log("so address:",Module.findBaseAddress(name));console.log("so address:",Module.getBaseAddress(name));}); } setImmediate(frida_Module,0);輸出如下: so address: 0xdf2d3000 so address: 0xdf2d30001.6 Memory 對象
Memory 的一些 API 通常是對 內存處理,譬如 Memory.copy() 復制內存,又如 writeByteArray 寫入字節到指定內存中,那我們這章中就是學習使用 Memory API 向內存中寫入數據、讀取數據。
1.6.1 Memory.scan 搜索內存數據
其主要功能是搜索內存中以 address 地址開始,搜索長度為 size,需要搜是條件是 pattern,callbacks搜索之后的回調函數;此函數相當于搜索內存的功能。
我們來直接看例子,然后結合例子講解,如下圖1-5。
圖1-5 IDA中 so 文件某處數據
如果我想搜索在內存中?112A地址 的起始數據要怎么做,代碼示例如下。
function frida_Memory() {Java.perform(function () {//先獲取so的module對象var module = Process.findModuleByName("libhello.so"); //??是通配符var pattern = "03 49 ?? 50 20 44";//基址console.log("base:"+module.base)//從so的基址開始搜索,搜索大小為so文件的大小,搜指定條件03 49 ?? 50 20 44的數據var res = Memory.scan(module.base, module.size, pattern, {onMatch: function(address, size){//搜索成功console.log('搜索到 ' +pattern +" 地址是:"+ address.toString()); }, onError: function(reason){//搜索失敗console.log('搜索失敗');},onComplete: function(){//搜索完畢console.log("搜索完畢")}});}); } setImmediate(frida_Memory,0);先來看看回調函數的含義:
- onMatch:function(address,size):使用包含作為NativePointer的實例地址的address和指定大小為數字的size調用,此函數可能會返回字符串STOP以提前取消內存掃描。
- onError:Function(Reason):當掃描時出現內存訪問錯誤時使用原因調用。
- onComplete:function():當內存范圍已完全掃描時調用。
我們來來說上面這段代碼做了什么事情:搜索libhello.so文件在內存中的數據,搜索以pattern條件的在內存中能匹配的數據。搜索到之后根據回調函數返回數據。
我們來看看執行之后的效果圖1-6。
圖1-6 終端執行
我們要如何驗證搜索到底是不是圖1-5中112A地址,其實很簡單。so的基址是0xdf2d3000,而搜到的地址是0xdf2d412a,我們只要df2d412a-df2d3000=112A。就是說我們已經搜索到了!
1.6.2 搜索內存數據 Memory.scanSync
功能與Memory.scan一樣,只不過它是返回多個匹配到條件的數據。
代碼示例如下。
1.6.3 內存分配 Memory.alloc
在目標進程中的堆上申請size大小的內存,并且會按照Process.pageSize對齊,返回一個NativePointer,并且申請的內存如果在JavaScript里面沒有對這個內存的使用的時候會自動釋放的。也就是說,如果你不想要這個內存被釋放,你需要自己保存一份對這個內存塊的引用。
使用案例如下
function frida_Memory() {Java.perform(function () {const r = Memory.alloc(10);console.log(hexdump(r, {offset: 0,length: 10,header: true,ansi: false}));}); } setImmediate(frida_Memory,0);以上代碼在目標進程中申請了10字節的空間~我們來看執行腳本的效果圖1-7。
圖1-7 內存分配
可以看到在0xdfe4cd40處申請了10個字節內存空間~
也可以使用:
- Memory.allocUtf8String(str)?分配?utf字符串
- Memory.allocUtf16String?分配?utf16字符串
- Memory.allocAnsiString?分配?ansi字符串
1.6.4 內存復制 Memory.copy
如同c api memcp一樣調用,使用案例如下。
function frida_Memory() {Java.perform(function () {//獲取so模塊的Module對象var module = Process.findModuleByName("libhello.so"); //條件var pattern = "03 49 ?? 50 20 44";//搜字符串 只是為了將so的內存數據復制出來 方便演示~var scanSync = Memory.scanSync(module.base, module.size, pattern);//申請一個內存空間大小為10個字節const r = Memory.alloc(10);//復制以module.base地址開始的10個字節 那肯定會是7F 45 4C 46...因為一個ELF文件的Magic屬性如此。Memory.copy(r,module.base,10);console.log(hexdump(r, {offset: 0,length: 10,header: true,ansi: false}));}); } setImmediate(frida_Memory,0);輸出如下。0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF e8142070 7f 45 4c 46 01 01 01 00 00 00 .ELF......從?module.base?中復制10個字節的內存到新申請的?r?內。
1.6.5 寫入內存 Memory.writeByteArray
將字節數組寫入一個指定內存,代碼示例如下:
function frida_Memory() { Java.perform(function () {//定義需要寫入的字節數組 這個字節數組是字符串"roysue"的十六進制var arr = [ 0x72, 0x6F, 0x79, 0x73, 0x75, 0x65];//申請一個新的內存空間 返回指針 大小是arr.lengthconst r = Memory.alloc(arr.length);//將arr數組寫入R地址中Memory.writeByteArray(r,arr);//輸出console.log(hexdump(r, {offset: 0,length: arr.length,header: true,ansi: false})); }); } setImmediate(frida_Memory,0);輸出如下。0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF 00000000 72 6f 79 73 75 65 roysue1.6.6 讀取內存 Memory.readByteArray
將一個指定地址的數據,代碼示例如下:
function frida_Memory() { Java.perform(function () {//定義需要寫入的字節數組 這個字節數組是字符串"roysue"的十六進制var arr = [ 0x72, 0x6F, 0x79, 0x73, 0x75, 0x65];//申請一個新的內存空間 返回指針 大小是arr.lengthconst r = Memory.alloc(arr.length);//將arr數組寫入R地址中Memory.writeByteArray(r,arr);//讀取r指針,長度是arr.length 也就是會打印上面一樣的值var buffer = Memory.readByteArray(r, arr.length);//輸出console.log("Memory.readByteArray:");console.log(hexdump(buffer, {offset: 0,length: arr.length,header: true,ansi: false}));}); }); } setImmediate(frida_Memory,0);輸出如下。 [Google Pixel::com.roysue.roysueapplication]-> Memory.readByteArray:0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF 00000000 72 6f 79 73 75 65 roysue結語
在這篇中我們學會了在 FRIDACLI 中如何輸出想要輸出格式,也學會如何聲明變量,一步步的學習。在逐步的學習的過程,總是會遇到不同的問題。歌曲<奇跡再現>我相信你一定聽過吧~,新的風暴已經出現,怎么能夠停止不前…遇到問題不要怕,總會解決的。
咱們會在下一篇中來學會如何使用 FRIDA 中的 Java對象、Interceptor對象、NativePointer對象、NativeFunction對象、NativeCallback對象,咱們拭目以待吧~
Java、Interceptor、NativePointer ( Function / Callback ) 使用方法及示例
1.1 Java對象
Java 是極其重要的 API,無論是想對 so層,亦或 java層 進行攔截,都必須編寫 Java.perform,在使用上面這些 API 時,應該都已經發現了吧。
這章我們就來詳細看看 Java對象 都有哪些?API
1.1.1 Java.available
該函數一般用來判斷當前進程是否加載了JavaVM,Dalvik或`ART虛擬機,咱們來看代碼示例!
import sys import fridajs_code = """ function inject_func() {var pid = Process.id;console.log(pid); }function frida_Java() {Java.perform(function () {//作為判斷用if (Java.available) {//注入的邏輯代碼console.log("", Java.androidVersion);inject_func();} else {//未能正常加載JAVA VMconsole.log("error");}}); }setImmediate(frida_Java, 0); """def js_callback_func(msg, data):# js中執行send函數后要回調的函數if 'send' == msg['type']:print(f'[*] {msg["payload"]}')else:print(msg)devices = frida.get_remote_device() pid = devices.spawn(['com.android.settings']) process = devices.attach(pid) script = process.create_script(js_code) script.on('message', js_callback_func) script.load() devices.resume(pid) sys.stdin.read()核心注入的邏輯代碼寫在 <注入的邏輯代碼> 內會非常的安全萬無一失~
1.1.2 Java.androidVersion
顯示 android 系統版本號
function frida_Java() {Java.perform(function () {//作為判斷用if(Java.available){//注入的邏輯代碼console.log("",Java.androidVersion);}else{//未能正常加載JAVA VMconsole.log("error");}}); } setImmediate(frida_Java,0);輸出如下。 9 因為我的系統版本是9版本~1.1.3 枚舉類 Java.enumerateLoadedClasses
該 API 枚舉當前加載的所有類信息,它有一個回調函數分別是?onMatch、onComplete?函數,我們來看看代碼示例以及效果!
function frida_Java() {Java.perform(function () {if(Java.available){//console.log("",Java.androidVersion);//枚舉當前加載的所有類Java.enumerateLoadedClasses({//每一次回調此函數時其參數className就是類的信息onMatch: function (className){//輸出類字符串console.log("",className);},//枚舉完畢所有類之后的回調函數onComplete: function (){//輸出類字符串console.log("輸出完畢");}});}else{console.log("error");}}); } setImmediate(frida_Java,0);咱們來看執行的效果圖1-7。
圖1-7 終端執行
它還有一個好兄弟?Java.enumerateLoadedClassesSync(),它返回的是一個數組。
1.1.4 枚舉類加載器 Java.enumerateLoadeders
該 api 枚舉 Java VM 中存在的 類加載器,其有一個回調函數,分別是 onMatch: function (loader) 與 onComplete: function ()
接著我們來看代碼示例。
function frida_Java() {Java.perform(function () {if(Java.available){//枚舉當前加載的Java VM類加載器Java.enumerateClassLoaders({//回調函數,參數loader是類加載的信息onMatch: function (loader){console.log("",loader);},//枚舉完畢所有類加載器之后的回調函數onComplete: function (){console.log("end");}});}else{console.log("error");}}); } setImmediate(frida_Java,0);執行的效果圖1-8。
圖1-8 終端執行
它也有一個好兄弟叫 `Java.enumerateClassLoadersSync()` 也是返回的數組。
1.1.5 附加調用 Java.perform
該 API 極其重要,Java.perform(fn)主要用于當前線程附加到 Java VM,并且調用 fn 方法。
我們來看看示例代碼及其含義。
function frida_Java() {//運行當前js腳本時會對當前線程附加到Java VM虛擬機,并且執行function方法Java.perform(function () {//判斷是否Java VM正常運行if(Java.available){//如不意外會直接輸出 helloconsole.log("hello");}else{console.log("error");}}); } setImmediate(frida_Java,0);輸出如下。 [Google Pixel::com.roysue.roysueapplication]-> hello沒錯你猜對了,它也有一個好兄弟。Java.performNow(fn)~
1.1.6 獲取類 Java.use
Java.use(className),動態獲取 className 的類定義,通過對其調用 $new() 來調用構造函數,可以從中實例化對象。
當想要回收類時可以調用 $Dispose()方法顯式釋放,當然也可以等待 JavaScript 的垃圾回收機制,
當實例化一個對象之后,可以通過其實例對象調用類中的靜態或非靜態的方法,
官方代碼示例定義如下。
Java.perform(function () {//獲取android.app.Activity類var Activity = Java.use('android.app.Activity');//獲取java.lang.Exception類var Exception = Java.use('java.lang.Exception');//攔截Activity類的onResume方法Activity.onResume.implementation = function () {//調用onResume方法的時候,會在此處被攔截并且調用以下代碼拋出異常!throw Exception.$new('Oh noes!');}; });1.1.7 掃描實例類 Java.choose
在堆上查找 實例化 的 對象,
示例代碼如下!
Java.perform(function () {//查找android.view.View類在堆上的實例化對象Java.choose("android.view.View", {//枚舉時調用onMatch:function(instance){//打印實例console.log(instance);},//枚舉完成后調用onComplete:function() {console.log("end")}}); });輸出如下: android.view.View{2292774 V.ED..... ......ID 0,1794-1080,1920 #1020030 android:id/navigationBarBackground} android.view.View{d43549d V.ED..... ......ID 0,0-1080,63 #102002f android:id/statusBarBackground} end1.1.8 類型轉換器 Java.cast
Java.cast(handle, klass),就是將指定 變量 或者 數據 強制轉換成你所有需要的類型;創建一個 JavaScript 包裝器,給定從 Java.use() 返回的給定類klas的句柄的現有實例。此類包裝器還具有用于獲取其類的包裝器的類屬性,以及用于獲取其類名的字符串表示的$className屬性,通常在攔截so層時會使用此函數將 jstring、jarray等等轉換之后查看其值。
1.1.9 定義任意數組類型 Java.array
frida 提供了在 js 代碼中定義 java 數組 的 api,該數組可以用于傳遞給 java API。
我們來看看如何定義,代碼示例如下。
Java.perform(function () {//定義一個int數組、值是1003, 1005, 1007var intarr = Java.array('int', [ 1003, 1005, 1007 ]);//定義一個byte數組、值是0x48, 0x65, 0x69var bytearr = Java.array('byte', [ 0x48, 0x65, 0x69 ]);for(var i=0;i<bytearr.length;i++){//輸出每個byte元素console.log(bytearr[i])} });我們通過上面定義 int數組 和 byte數組 的例子,可以知道其定義格式為:Java.array('type', [value1,value2,....]);
那它都支持 type 呢?我們來看看~
| 1 | Z | boolean |
| 2 | B | byte |
| 3 | C | char |
| 4 | S | short |
| 5 | I | int |
| 6 | J | long |
| 7 | F | float |
| 8 | D | double |
| 9 | V | void |
1.1.10 注冊類 Java.registerClass(spec)
Java.registerClass:創建一個新的 Java類,并返回一個包裝器,其中規范是一個包含:
- name:指定類名稱的字符串。
- superClass:(可選)父類。要從?java.lang.Object?繼承的省略。
- implements:(可選)由此類實現的接口數組。
- fields:(可選)對象,指定要公開的每個字段的名稱和類型。
- methods:(可選)對象,指定要實現的方法。
注冊一個類,返回類的實例,下面我貼一個基本的用法~實例化目標類對象并且調用類中的方法
Java.perform(function () {//注冊一個目標進程中的類,返回的是一個類對象var hellojni = Java.registerClass({name: 'com.roysue.roysueapplication.hellojni'});console.log(hellojni.addInt(1,2)); });我們再深入看看官方怎么來玩的:
//獲取目標進程的SomeBaseClass類 var SomeBaseClass = Java.use('com.example.SomeBaseClass'); //獲取目標進程的X509TrustManager類 var X509TrustManager = Java.use('javax.net.ssl.X509TrustManager');var MyWeirdTrustManager = Java.registerClass({//注冊一個類是進程中的MyWeirdTrustManager類name: 'com.example.MyWeirdTrustManager',//父類是SomeBaseClass類superClass: SomeBaseClass,//實現了MyWeirdTrustManager接口類implements: [X509TrustManager],//類中的屬性fields: {description: 'java.lang.String',limit: 'int',},//定義的方法methods: {//類的構造函數$init: function () {console.log('Constructor called');},//X509TrustManager接口中方法之一,該方法作用是檢查客戶端的證書checkClientTrusted: function (chain, authType) {console.log('checkClientTrusted');},//該方法檢查服務器的證書,不信任時。在這里通過自己實現該方法,可以使之信任我們指定的任何證書。在實現該方法時,也可以簡單的不做任何處理,即一個空的函數體,由于不會拋出異常,它就會信任任何證書。checkServerTrusted: [{//返回值類型returnType: 'void',//參數列表argumentTypes: ['[Ljava.security.cert.X509Certificate;', 'java.lang.String'],//實現方法implementation: function (chain, authType) {//輸出console.log('checkServerTrusted A');}}, {returnType: 'java.util.List',argumentTypes: ['[Ljava.security.cert.X509Certificate;', 'java.lang.String', 'java.lang.String'],implementation: function (chain, authType, host) {console.log('checkServerTrusted B');//返回null會信任所有證書return null;}}],// 返回受信任的X509證書數組。getAcceptedIssuers: function () {console.log('getAcceptedIssuers');return [];},} });我們來看看上面的示例都做了啥? 實現了證書類的javax.net.ssl.X509TrustManager類,,這里就是相當于自己在目標進程中重新創建了一個類,實現了自己想要實現的類構造,重構造了其中的三個接口函數、從而繞過證書校驗。
1.1.11 Java.vm 對象
Java.vm 對象十分常用,比如想要拿到 JNI層 的 JNIEnv對象,可以使用 getEnv();
我們來看看具體的使用和基本小實例。~
function frida_Java() { Java.perform(function () {//攔截getStr函數Interceptor.attach(Module.findExportByName("libhello.so" , "Java_com_roysue_roysueapplication_hellojni_getStr"), {onEnter: function(args) {console.log("getStr");},onLeave:function(retval){//它的返回值的是retval 在jni層getStr的返回值的jstring //我們在這里做的事情就是替換掉結果//先獲取一個Env對象var env = Java.vm.getEnv();//通過newStringUtf方法構建一個jstirng字符串var jstring = env.newStringUtf('roysue');//replace替換掉結果retval.replace(jstring);console.log("getSum方法返回值為:roysue")}}); } setImmediate(frida_Java,0);1.2 Interceptor 對象
該對象功能十分強大,函數原型是?Interceptor.attach(target, callbacks):
- 參數 target 是需要攔截的位置的函數地址,也就是填某個 so層 函數的地址即可對其攔截,target 是一個 NativePointer參數,用來指定你想要攔截的函數的地址,NativePointer 是一個指針。需要注意的是對于 Thumb 函數需要對函數地址+1,
- 參數 callbacks 是回調函數,分別是以下兩個回調函數:
1.2.1 Interceptor.attach
- onEnter:函數(args):回調函數,給定一個參數 args,可用于讀取或寫入參數作為?NativePointer對象的數組。
- onLeave:函數(retval):回調函數給定一個參數?retval,該參數是包含原始返回值的?NativePointer?派生對象。可以調用?retval.replace(1337) 以整數 1337 替換返回值,或者調用 retval.replace(ptr("0x1234"))以替換為指針。請注意,此對象在OnLeave?調用中回收,因此不要將其存儲在回調之外并使用它。如果需要存儲包含的值,請制作深副本,例如:ptr(retval.toString())。
我們來看看示例代碼~
//使用Module對象getExportByNameAPI直接獲取libc.so中的導出函數read的地址,對read函數進行附加攔截 Interceptor.attach(Module.getExportByName('libc.so', 'read'), {//每次read函數調用的時候會執行onEnter回調函數onEnter: function (args) {this.fileDescriptor = args[0].toInt32();},//read函數執行完成之后會執行onLeave回調函數onLeave: function (retval) {if (retval.toInt32() > 0) {/* do something with this.fileDescriptor */}} });通過我們對?Interceptor.attach?函數有一些基本了解了~它還包含一些屬性。
| 1 | returnAddress | 返回地址,類型是NativePointer |
| 2 | context | 上下文:具有鍵pc和sp的對象,它們是分別為ia32/x64/arm指定EIP/RIP/PC和ESP/RSP/SP的NativePointer對象。其他處理器特定的鍵也可用,例如eax、rax、r0、x0等。也可以通過分配給這些鍵來更新寄存器值。 |
| 3 | errno | 當前errno值 |
| 4 | lastError | 當前操作系統錯誤值 |
| 5 | threadId | 操作系統線程ID |
| 6 | depth | 相對于其他調用的調用深度 |
我們來看看示例代碼。
function frida_Interceptor() {Java.perform(function () {//對So層的導出函數getSum進行攔截Interceptor.attach(Module.findExportByName("libhello.so" , "Java_com_roysue_roysueapplication_hellojni_getSum"), {onEnter: function(args) {//輸出console.log('Context information:');//輸出上下文因其是一個Objection對象,需要它進行接送、轉換才能正常看到值console.log('Context : ' + JSON.stringify(this.context));//輸出返回地址console.log('Return : ' + this.returnAddress);//輸出線程idconsole.log('ThreadId : ' + this.threadId);console.log('Depth : ' + this.depth);console.log('Errornr : ' + this.err);},onLeave:function(retval){}});}); } setImmediate(frida_Interceptor,0);我們注入腳本之后來看看執行之后的效果以及輸出的這些都是啥,執行的效果圖1-9。
圖1-9 終端執行
1.2.2 Interceptor.detachAll
簡單來說這個的函數的作用就是讓之前所有的Interceptor.attach附加攔截的回調函數失效。
1.2.3 Interceptor.replace
相當于替換掉原本的函數,用替換時的實現替換目標處的函數。如果想要完全或部分替換現有函數的實現,則通常使用此函數。,我們也看例子,例子是最直觀的!代碼如下。
function frida_Interceptor() {Java.perform(function () {//這個c_getSum方法有兩個int參數、返回結果為兩個參數相加//這里用NativeFunction函數自己定義了一個c_getSum函數var add_method = new NativeFunction(Module.findExportByName('libhello.so', 'c_getSum'), 'int',['int','int']);//輸出結果 那結果肯定就是 3console.log("result:",add_method(1,2));//這里對原函數的功能進行替換實現Interceptor.replace(add_method, new NativeCallback(function (a, b) {//h不論是什么參數都返回123return 123;}, 'int', ['int', 'int']));//再次調用 則返回123console.log("result:",add_method(1,2));}); }我來看注入腳本之后的終端是是不是顯示了3和123見下圖1-10。
圖1-10 終端執行
1.3 NativePointer 對象
同等與C語言中的指針
1.3.1 new NativePointer(s)
聲明定義NativePointer類型
function frida_NativePointer() {Java.perform(function () {//第一種字符串定義方式 十進制的100 輸出為十六進制0x64const ptr1 = new NativePointer("100");console.log("ptr1:",ptr1);//第二種字符串定義方式 直接定義0x64 同等與定義十六進制的64const ptr2 = new NativePointer("0x64");console.log("ptr2:",ptr2); //第三種定數值義方式 定義數字int類型 十進制的100 是0x64const ptr3 = new NativePointer(100);console.log("ptr3:",ptr3);}); } setImmediate(frida_NativePointer,0);輸出如下,都會自動轉為十六進制的0x64 ptr1: 0x64 ptr2: 0x64 ptr3: 0x641.3.2 運算符以及指針讀寫 API
它也能調用以下運算符
看完API含義之后,我們來使用他們,下面該腳本是readByteArray()示例~
function frida_NativePointer() {Java.perform(function () {console.log("");//拿到libc.so在內存中的地址var pointer = Process.findModuleByName("libc.so").base;//讀取從pointer地址開始的16個字節console.log(pointer.readByteArray(0x10));}); } setImmediate(frida_NativePointer,0); 輸出如下:0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF 00000000 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 .ELF............首先我先來用readByteArray函數來讀取libc.so文件在內存中的數據,這樣我們方便測試,我們從libc文件讀取0x10個字節的長度,肯定會是7F 45 4C 46...因為ELF文件頭部信息中的Magic屬性。
1.3.3 readPointer()
咱們直接從API索引11開始玩readPointer(),定義是從此內存位置讀取NativePointer,示例代碼如下。省略function以及Java.perform~
var pointer = Process.findModuleByName("libc.so").base;console.log(pointer.readByteArray(0x10));console.log("readPointer():"+pointer.readPointer());輸出如下。readPointer():0x464c457f也就是將readPointer的前四個字節的內容轉成地址產生一個新的NativePointer。
1.3.4 writePointer(ptr)
讀取ptr指針地址到當前指針
//先打印pointer指針地址console.log("pointer :"+pointer);//分配四個字節的空間地址const r = Memory.alloc(4);//將pointer指針寫入剛剛申請的r內r.writePointer(pointer);//讀取r指針的數據var buffer = Memory.readByteArray(r, 4);//r指針內放的pointer指針地址console.log(buffer);輸出如下。 //console.log("pointer :"+pointer); 這句打印的地址 也就是libc的地址 pointer :0xf588f000 //console.log(buffer); 輸出buffer 0xf588f000在內存數據會以00 f0 88 f5方式顯示0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF 00000000 00 f0 88 f5 ....1.3.5 readS32()、readU32()
從該內存位置讀取有符號或無符號8/16/32/etc或浮點數/雙精度值,并將其作為數字返回。這里拿readS32()、readU32()作為演示.
//從pointer地址讀4個字節 有符號console.log(pointer.readS32());//從pointer地址讀4個字節 無符號console.log(pointer.readU32());輸出如下。0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF 00000000 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 .ELF............ 1179403647 == 0x464c457f 1179403647 == 0x464c457f1.3.6 writeS32()、writeU32()
將有符號或無符號8/16/32/等或浮點數/雙精度值寫入此內存位置。
//申請四個字節的內存空間const r = Memory.alloc(4);//將0x12345678寫入r地址中r.writeS32(0x12345678);//輸出console.log(r.readByteArray(0x10)); // writeS32()、writeU32()輸出的也是一樣的,只是區別是有符號和無符號 輸出如下。0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF 00000000 78 56 34 12 00 00 00 00 00 00 00 00 00 00 00 00 xV4.............1.3.7 readByteArray(length))、writeByteArray(bytes)
readByteArray(length))連續讀取內存length個字節,writeByteArray連續寫入內存bytes。
//先定義一個需要寫入的字節數組var arr = [ 0x72, 0x6F, 0x79, 0x73, 0x75, 0x65];//這里申請以arr大小的內存空間const r = Memory.alloc(arr.length);//將arr數組字節寫入rMemory.writeByteArray(r,arr);//讀取arr.length大小的數組var buffer = Memory.readByteArray(r, arr.length);console.log("Memory.readByteArray:");console.log(hexdump(buffer, {offset: 0,length: arr.length,header: true,ansi: false}));輸出如下。 Memory.readByteArray:0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF 00000000 72 6f 79 73 75 65 roysue1.3.8 readCString([size = -1])、writeUtf8String(str)
readCString功能是讀取指針地址位置的字節字符串,對應的writeUtf8String是寫入指針地址位置的字符串處。(這里的r是接著上面的代碼的變量)。
//在這里直接使用readCString讀取會把上面的'roysue'字符串讀取出來console.log("readCString():"+r.readCString());//這里是寫入字符串 也就是 roysue起始位置開始被替換為hahaconst newPtrstr = r.writeUtf8String("haha");//替換完了之后再繼續輸出 必然是hahaconsole.log("readCString():"+newPtrstr.readCString());咱們來看看執行的效果~~見下圖1-11。
圖1-11 終端執行
1.4 NativeFunction對象
創建新的NativeFunction以調用address處的函數(用NativePointer指定),其中rereturn Type指定返回類型,argTypes數組指定參數類型。如果不是系統默認值,還可以選擇指定ABI。對于可變函數,添加一個‘.’固定參數和可變參數之間的argTypes條目,我們來看看官方的例子。
// LargeObject HandyClass::friendlyFunctionName(); //創建friendlyFunctionPtr地址的函數 var friendlyFunctionName = new NativeFunction(friendlyFunctionPtr,'void', ['pointer', 'pointer']); //申請內存空間 var returnValue = Memory.alloc(sizeOfLargeObject); //調用friendlyFunctionName函數 friendlyFunctionName(returnValue, thisPtr);我來看看它的格式,函數定義格式為new NativeFunction(address, returnType, argTypes[, options]),參照這個格式能夠創建函數并且調用!returnType和argTypes[,]分別可以填void、pointer、int、uint、long、ulong、char、uchar、float、double、int8、uint8、int16、uint16、int32、uint32、int64、uint64這些類型,根據函數的所需要的type來定義即可。
在定義的時候必須要將參數類型個數和參數類型以及返回值完全匹配,假設有三個參數都是int,則new NativeFunction(address, returnType, ['int', 'int', 'int']),而返回值是int則new NativeFunction(address, 'int', argTypes[, options]),必須要全部匹配,并且第一個參數一定要是函數地址指針。
1.5 NativeCallback對象
new NativeCallback(func,rereturn Type,argTypes[,ABI]):創建一個由JavaScript函數func實現的新NativeCallback,其中rereturn Type指定返回類型,argTypes數組指定參數類型。您還可以指定ABI(如果不是系統默認值)。有關支持的類型和Abis的詳細信息,請參見NativeFunction。注意,返回的對象也是一個NativePointer,因此可以傳遞給Interceptor#replace。當將產生的回調與Interceptor.replace()一起使用時,將調用func,并將其綁定到具有一些有用屬性的對象,就像Interceptor.Attach()中的那樣。我們來看一個例子。如下,利用NativeCallback做一個函數替換。
Java.perform(function () {var add_method = new NativeFunction(Module.findExportByName('libhello.so', 'c_getSum'), 'int',['int','int']);console.log("result:",add_method(1,2));//在這里new一個新的函數,但是參數的個數和返回值必須對應Interceptor.replace(add_method, new NativeCallback(function (a, b) {return 123;}, 'int', ['int', 'int']));console.log("result:",add_method(1,2));});結語
本篇咱們學習了非常實用的 API,如:
- Interceptor對象 對 so層 導出庫函數攔截、
- NativePointer對象的指針操作、
- NativeFunction對象的實例化
- so函數的使用 等
這些都是當前灰常好用的函數建議多多嘗試
總結
以上是生活随笔為你收集整理的FRIDA - API使用篇:rpc、Process、Module、Memory 使用方法及示例的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 王爽 汇编语言第三版 第8章( 寻址方式
- 下一篇: 小甲鱼 OllyDbg 教程系列 (三)