自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

利用 Frida 和 QBDI 動態(tài)分析 Android Native 的各項函數

開發(fā)
這篇文章將介紹如何通過充分利用Frida和QBDI來更好地理解Android Native 的各項函數。

[[343697]]

由于可以檢索應用程序代碼的Java表示形式,因此通常認為Android應用程序的逆向工程比較容易。攻擊者就是通過了解這些代碼版本,收集應用程序信息,來發(fā)現漏洞的。如今,大多數Android應用程序編輯器已經意識到了這一點,并盡力使反向工程不再那么容易。由于Java本地接口(Java Native Interface,簡稱JNI),攻擊者通常依靠集成混淆策略或將敏感函數從Java / Kotlin端轉移到本機代碼。但是,當他們決定同時使用兩者時(即,混淆的本機代碼),逆向工程過程變得更加復雜。結果,靜態(tài)查看本機庫的反匯編結果非常繁瑣且耗時。幸運的是,運行時檢查仍然是可能的,并且提供了一種便捷的方法來有效地掌握應用程序的內部機制,甚至避免混淆。JNI(Java Native Interface) Java本地接口,又叫Java原生接口。它允許Java調用C/C++的代碼,同時也允許在C/C++中調用Java的代碼??梢园袹NI理解為一個橋梁,連接Java和底層。其實根據字面意思,JNI就是一個介于Java層和Native層的接口,而Native層就是C/C++層面。

由于針對常規(guī)調試器的保護在流行的應用程序中非常普遍,因此使用動態(tài)二進制工具(DBI)框架(例如Frida)仍然是進行全面檢查的理想選擇。從技術上講,在其他強大函數中,Frida允許用戶在本機函數的開頭和結尾插入自己的代碼,或替換整個實現。但是,Frida在某些時候缺乏粒度,特別是在以指令規(guī)模檢查執(zhí)行情況時。在這種情況下,Quarkslab開發(fā)的DBI框架QBDI可以幫助Frida在調用給定的本機函數時確定已執(zhí)行了代碼的哪些部分。

首先,我們必須正確設置測試環(huán)境。我們假設設備已經植根并且Frida服務器已經在運行并且可以使用。除了Frida,我們還需要安裝QBDI。我們可以從源代碼編譯它或下載Android的發(fā)行版,使用說明可以直接從官方頁面檢索到。解壓縮后,我們必須將共享庫libQBDI.so推送到設備上的/ data / local / tmp中。除此之外,我們還可以注意到在frida-qbdi.js中定義的QBDI綁定,該文件負責提供QBDI函數的接口。換句話說,它充當QBDI和Frida之間的橋梁。

請注意,必須先關閉SELinux,否則由于某些限制規(guī)則,Frida無法將QBDI共享庫加載到內存中。這將會顯示一條明確的錯誤消息,告訴用戶權限被拒絕。在大多數情況下,僅使用root特權運行此命令行即可完成此工作: 

  1. setenforce 0 

現在我們已經具備了基于Frida和QBDI編寫腳本的所有要求。

跟蹤本機函數

在對JNI共享庫執(zhí)行反向工程時,始終值得檢查JNI_OnLoad()。確實,此函數在庫加載后立即調用,并負責初始化。它能夠與Java端進行交互,例如設置類的屬性,調用Java函數以及通過幾個JNI函數注冊其他本機方法。攻擊者通常依靠這些屬性來隱藏一些敏感的檢查和秘密的內部機制。

接下來,讓我們假設我們要分析一個流行的Android應用程序,比如Whatsapp,其軟件包名稱為com.whatsapp,這是當前Android上最廣泛的即時消息解決方案。它嵌入了一堆共享庫,其中一個是libwhatsapp.so。不過要注意的是,該庫并不位于常規(guī)的lib /目錄中,因為在運行時存在一種解壓縮機制,該機制可將其從存檔中提取出來,然后將其加載到內存中,我們的目標是弄清楚它的初始化函數在做什么。

利用 Frida

  1. /** * frida -Uf com.whatsapp --no-pause -l script.js */function processJniOnLoad(libraryName) { 
  2.     const funcSym = "JNI_OnLoad"
  3.     const funcPtr = Module.findExportByName(libraryName, funcSym); 
  4.  
  5.     console.log("[+] Hooking " + funcSym + "() @ " + funcPtr + "..."); 
  6.     // jint JNI_OnLoad(JavaVM *vm, void *reserved); 
  7.     var funcHook = Interceptor.attach(funcPtr, { 
  8.         onEnter: function (args) { 
  9.             const vm = args[0]; 
  10.             const reserved = args[1]; 
  11.             console.log("[+] " + funcSym + "(" + vm + ", " + reserved + ") called"); 
  12.         }, 
  13.         onLeave: function (retval) { 
  14.             console.log("[+]\t= " + retval); 
  15.         } 
  16.     });}function waitForLibLoading(libraryName) { 
  17.     var isLibLoaded = false
  18.  
  19.     Interceptor.attach(Module.findExportByName(null"android_dlopen_ext"), { 
  20.         onEnter: function (args) { 
  21.             var libraryPath = Memory.readCString(args[0]); 
  22.             if (libraryPath.includes(libraryName)) { 
  23.                 console.log("[+] Loading library " + libraryPath + "..."); 
  24.                 isLibLoaded = true
  25.             } 
  26.         }, 
  27.         onLeave: function (args) { 
  28.             if (isLibLoaded) { 
  29.                 processJniOnLoad(libraryName); 
  30.                 isLibLoaded = false
  31.             } 
  32.         } 
  33.     });}Java.perform(function() { 
  34.     const libraryName = "libwhatsapp.so"
  35.     waitForLibLoading(libraryName);}); 

首先,借助Frida提供的便捷API,我們可以輕松地掛接我們要研究的函數。但是,由于Android應用程序中嵌入的庫是通過System.loadLibrary()動態(tài)加載的,該函數在后臺調用了本機android_dlopen_ext(),因此我們需要等待將目標庫放入進程的內存中。使用此腳本,我們可以只訪問函數的輸入(參數)和輸出(返回值),也就是說,我們位于函數層。這是非常有限的,僅憑這一點基本上還不足以準確掌握內部的情況。因此,在這種精確的情況下,我們希望在較低級別上徹底檢查該函數。

利用 Frida 和 QBDI

QBDI提供的導入函數可以幫助我們克服以上的問題,實際上,該DBI框架允許用戶通過跟蹤執(zhí)行的指令來執(zhí)行細粒度的分析。這對我們非常有用,因為我們可以深入了解我們的目標函數。

這樣做的想法是,不是讓JNI_OnLoad()在常規(guī)啟動期間運行,而是在基本塊/指令范圍內通過有條件的上下文來執(zhí)行它,以便確切地知道已執(zhí)行了什么。由于我們可以將這兩個DBI框架結合在一起,因此可以在我們之前編寫的Frida腳本的基礎上集成這一全新的部分。

但是,我們使用的Interceptor.attach()函數只允許我們定義onEnter和onLeave回調。它意味著真正的函數總是被執(zhí)行,而不管你的條目回調應該做什么。因此,初始化函數將執(zhí)行兩次:首先通過QBDI執(zhí)行,然后正常執(zhí)行。這是有問題的,因為根據情況不同,可能會出現一些意外的運行時錯誤,因為這個函數只需要調用一次。

幸運的是,我們可以利用Frida的攔截器模塊帶來的另一個函數,該函數包括替換本機函數的實現。這樣做,我們能夠設置QBDI上下文,執(zhí)行檢測的函數并像往常一樣無縫地將返回值轉發(fā)給調用方,以防止應用程序崩潰,該技術旨在使過程足夠穩(wěn)定以恢復正常執(zhí)行。

然而,我們仍然面臨一個問題,初始函數已被我們自己的新實現完全覆蓋。換句話說,該函數的代碼不是原始代碼,而是由Frida早些時候進行檢測的。因此,在我們的代碼中,我們必須在使用QBDI執(zhí)行該函數之前恢復到真正的版本。

修改腳本后,processJniOnLoad()函數如下所示:

初始化

現在讓我們編寫負責在QBDI上下文中執(zhí)行該函數的函數,首先,我們需要初始化一個VM,實例化它的相關狀態(tài)(通用寄存器),并分配一個偽堆棧,該堆棧將在函數執(zhí)行期間使用。然后,我們必須將QBDI的上下文與當前上下文進行同步,也就是說,將實際CPU寄存器的值放入將要使用的QBDI?,F在我們可以決定要檢測代碼的哪些部分。我們可以顯式定義一個任意地址范圍,也可以要求DBI檢測函數地址所在模塊的整個地址空間。為方便起見,在本示例中將使用后者。

回調函數設置

我們必須指定所需的回調函數的種類,接下來,我們要跟蹤已執(zhí)行的每條指令,因此我要放置一條預指令代碼回調,這意味著將在位于目標模塊中的每個已執(zhí)行指令之前調用我的函數。

此外,我們還可以添加幾個事件回調函數,以便在執(zhí)行轉移到QBDI未檢測到的部分代碼中或從中返回時通知該事件。當代碼與其他模塊(例如系統(tǒng)庫)(libc.so,libart.so,libbinder.so等)進行交互時,此函數非常有用。請注意,根據您要監(jiān)視的內容,其他幾種回調類型可能會很有幫助。

函數調用

現在我們準備通過QBDI調用目標函數,當然,我們需要傳遞預期的參數,在我們的例子中是一個指向JavaVM對象的指針和一個空指針。然后,我們可以根據使用的調用約定在特定的QBDI寄存器或虛擬堆棧上檢索返回值。這個值必須從我們之前編寫的本機替換函數中被轉發(fā)和返回。否則,應用程序很可能會因為對JNI版本的檢查不滿意而停止運行,JNI_OnLoad()應該返回JNI版本。

我們可以選擇使用QBDI的CPU恢復真正的CPU上下文。

  1. const qbdi = require("/path/to/frida-qbdi");qbdi.import();function qbdiExec(ctx, funcPtr, funcSym, args, postSync) { 
  2.     var vm = new QBDI(); // create a QBDI VM 
  3.     var state = vm.getGPRState(); 
  4.     var stack = vm.allocateVirtualStack(state, 0x10000); // allocate a virtual stack 
  5.     state.synchronizeContext(ctx, SyncDirection.FRIDA_TO_QBDI); // set up QBDI's context 
  6.  
  7.     vm.addInstrumentedModuleFromAddr(funcPtr); 
  8.  
  9.     var icbk = vm.newInstCallback(function (vm, gpr, fpr, data) { 
  10.         var inst = vm.getInstAnalysis(); 
  11.         console.log("0x" + inst.address.toString(16) + " " + inst.disassembly); 
  12.         return VMAction.CONTINUE
  13.     }); 
  14.     var iid = vm.addCodeCB(InstPosition.PREINST, icbk); // register pre-instruction callback 
  15.  
  16.     var vcbk = vm.newVMCallback(function (vm, evt, gpr, fpr, data) { 
  17.         const module = Process.getModuleByAddress(evt.basicBlockStart); 
  18.         const offset = ptr(evt.basicBlockStart - module.base); 
  19.         if (evt.event & VMEvent.EXEC_TRANSFER_CALL) { 
  20.             console.warn(" -> transfer call to 0x" + evt.basicBlockStart.toString(16) + " (" + module.name + "@" + offset + ")"); 
  21.         } 
  22.         if (evt.event & VMEvent.EXEC_TRANSFER_RETURN) { 
  23.             console.warn(" <- transfer return from 0x" + evt.basicBlockStart.toString(16) + " (" + module.name + "@" + offset + ")"); 
  24.         } 
  25.         return VMAction.CONTINUE
  26.     }); 
  27.     var vid = vm.addVMEventCB(VMEvent.EXEC_TRANSFER_CALL, vcbk); // register transfer callback 
  28.     var vid2 = vm.addVMEventCB(VMEvent.EXEC_TRANSFER_RETURN, vcbk); // register return callback 
  29.  
  30.     const javavm = ptr(args[0]); 
  31.     const reserved = ptr(args[1]); 
  32.  
  33.     console.log("[+] Executing " + funcSym + "(" + javavm + ", " + reserved + ") through QBDI..."); 
  34.     vm.call(funcPtr, [javavm, reserved]); 
  35.     var retVal = state.getRegister(0); // x86 so return value is stored on $eax 
  36.     console.log("[+] " + funcSym + "() returned " + retVal); 
  37.     if (postSync) { 
  38.         state.synchronizeContext(ctx, SyncDirection.QBDI_TO_FRIDA); 
  39.     } 
  40.     return retVal;} 

最終,此腳本必須使用frida-compile進行編譯,以便正確包含包含QBDI綁定的frida-qbdi.js。官方文檔頁對編譯過程進行了詳細說明。

生成一個覆蓋文件

具有包含已執(zhí)行的所有指令的跟蹤是很有必要的,但對于反向工程來說并不方便。事實上,我們不能一眼就分辨出整個執(zhí)行過程中的路徑。為了正確地呈現捕獲的軌跡,在反匯編器中集成可能是一個好主意。這樣,人們就可以準確地看到全部的路徑。然而,大多數反匯編器本身并沒有提供這樣的選項。對我們來說幸運的是,各種插件都提供了這樣的選項。在本例中,我們使用Lighthouse和Dragondance分別用于IDA Pro和Ghidra。這些插件可以通過導入drcov格式的代碼覆蓋文件來輕松配置,DynamioRIO使用這種格式存儲關于代碼覆蓋率的信息。

drcov格式非常簡單:除了標頭字段外,還必須指定描述進程的內存布局的模塊表,為每個模塊分配一個惟一的ID。此后,就有了所謂的基本塊表。該表包含執(zhí)行期間已命中的每個基本塊,一個基本塊由三個屬性定義:它的開始(相對)地址,它的大小和它所屬模塊的ID。

由于我們能夠在每個基本塊的開頭放置一個回調,因此我們可以確定這些值,從而生成我們自己的文件?,F在,我們需要檢索基地址和所有已執(zhí)行的基本塊的大小,而不是按指令規(guī)模工作。實際上,我們必須定義一個類型為BASIC_BLOCK_NEW 的QBDI事件回調函數,該函數負責收集此類信息。每當QBDI將要執(zhí)行一個新的基本程序塊時,我們的函數都會被調用,到目前為止尚不知道。在本示例中,我們不僅要打印有關此基本塊的一些有趣的值,還要創(chuàng)建一個代碼覆蓋率文件,以后可以在反匯編器中將其導入。但是,在Frida腳本的上下文中,我們無法操作文件。結果,我們必須停止使用frida命令行實用程序,并直接依賴于Frida提供的消息傳遞系統(tǒng)從底層Python腳本運行我們的JS腳本。這樣做使我們能夠在JS和Python端之間進行通信,然后對所需的文件系統(tǒng)執(zhí)行所有操作。

  1. var vcbk = vm.newVMCallback(function (vm, evt, gpr, fpr, data) { 
  2.     const module = Process.getModuleByAddress(evt.basicBlockStart); 
  3.     const base_addr = ptr(evt.basicBlockStart - module.base); // address must be relative to the module's start 
  4.     const size = evt.basicBlockEnd - evt.basicBlockStart; 
  5.     send({"bb": 1}, getBBInfo(base_addr, size, module)); // send the newly discovered basic block to the Python side 
  6.     return VMAction.CONTINUE;});var vid = vm.addVMEventCB(VMEvent.BASIC_BLOCK_NEW, vcbk); 

請注意,getBBInfo()函數僅在發(fā)送消息之前先序列化有關基本塊的信息。顯然,Python端必須處理此類消息,將與執(zhí)行相關的內容保留在內存中,并最終以上述正確的格式相應地生成代碼覆蓋文件。如果一切順利,由于其相應的代碼覆蓋插件,可以將輸出文件加載到IDA Pro或Ghidra中。所有已執(zhí)行的基本塊都將突出顯示,現在我們可以更清楚地遵循執(zhí)行流程,而只關注代碼的相關部分。

總結

Java/Kotlin逆向工程的易用性使得Android應用程序開發(fā)人員可以使用C/ c++來實現某些漏洞層面的操作。因此,本文所講的方法就是要讓逆向工程師逆向的過程變得很困難。因此,將QBDI與Frida一起使用是一個非常好的選擇,尤其是在研究那些本機函數時。這種組合確實提供了一種方法,可以弄清一個函數在不同層次上的作用,即函數、基本塊和指令規(guī)模。此外,還可以利用QBDI的執(zhí)行傳輸事件來解析對系統(tǒng)庫的外部調用,或者跟蹤內存訪問,然后了解執(zhí)行的總體情況。為了有效地協(xié)助反向工程師,可以將收集的信息明智地集成到一些現有的面向反向工程的工具中,以完善其靜態(tài)分析。除了生成執(zhí)行流程的直觀表示之外,從運行時獲取此類反饋對于其他與安全相關的目的(如模糊測試)也很有價值。還值得注意的是,如果函數很重要,Frida和QBDI都可以提供C / C ++ API。

本文翻譯自:https://blog.quarkslab.com/why-are-frida-and-qbdi-a-great-blend-on-android.html如若轉載,請注明原文地址:

 

責任編輯:姜華 來源: 嘶吼網
相關推薦

2021-01-16 16:07:51

RustAndroid Nat內存

2017-05-11 21:30:01

Android動態(tài)代理ServiceHook

2009-11-16 16:43:24

PHP數組刪除

2020-09-21 09:58:01

Frida

2010-03-04 09:51:07

Linux動態(tài)庫

2020-10-28 14:58:21

漏洞uTorrent協(xié)議層

2009-11-16 10:16:24

PHP文件上傳

2023-10-30 11:45:44

FridaC++函數

2023-10-24 07:22:22

Nginx運維管理

2010-07-14 14:31:27

POP3和IMAP4

2022-01-19 08:00:00

靜態(tài)代碼動態(tài)代碼開發(fā)

2011-08-23 14:46:59

云計算

2020-09-22 10:05:14

AsyncRAT

2016-11-23 16:48:20

react-nativandroidjavascript

2009-12-18 10:39:50

路由器關鍵技術

2021-03-08 00:11:02

Spring注解開發(fā)

2023-02-13 14:01:32

2009-07-06 17:47:44

2009-06-29 12:30:08

2021-03-18 22:06:01

數據分析編程語言大數據
點贊
收藏

51CTO技術棧公眾號