利用 Frida 和 QBDI 動態(tài)分析 Android Native 的各項函數
由于可以檢索應用程序代碼的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特權運行此命令行即可完成此工作:
- setenforce 0
現在我們已經具備了基于Frida和QBDI編寫腳本的所有要求。
跟蹤本機函數
在對JNI共享庫執(zhí)行反向工程時,始終值得檢查JNI_OnLoad()。確實,此函數在庫加載后立即調用,并負責初始化。它能夠與Java端進行交互,例如設置類的屬性,調用Java函數以及通過幾個JNI函數注冊其他本機方法。攻擊者通常依靠這些屬性來隱藏一些敏感的檢查和秘密的內部機制。
接下來,讓我們假設我們要分析一個流行的Android應用程序,比如Whatsapp,其軟件包名稱為com.whatsapp,這是當前Android上最廣泛的即時消息解決方案。它嵌入了一堆共享庫,其中一個是libwhatsapp.so。不過要注意的是,該庫并不位于常規(guī)的lib /目錄中,因為在運行時存在一種解壓縮機制,該機制可將其從存檔中提取出來,然后將其加載到內存中,我們的目標是弄清楚它的初始化函數在做什么。
利用 Frida
- /** * frida -Uf com.whatsapp --no-pause -l script.js */function processJniOnLoad(libraryName) {
- const funcSym = "JNI_OnLoad";
- const funcPtr = Module.findExportByName(libraryName, funcSym);
- console.log("[+] Hooking " + funcSym + "() @ " + funcPtr + "...");
- // jint JNI_OnLoad(JavaVM *vm, void *reserved);
- var funcHook = Interceptor.attach(funcPtr, {
- onEnter: function (args) {
- const vm = args[0];
- const reserved = args[1];
- console.log("[+] " + funcSym + "(" + vm + ", " + reserved + ") called");
- },
- onLeave: function (retval) {
- console.log("[+]\t= " + retval);
- }
- });}function waitForLibLoading(libraryName) {
- var isLibLoaded = false;
- Interceptor.attach(Module.findExportByName(null, "android_dlopen_ext"), {
- onEnter: function (args) {
- var libraryPath = Memory.readCString(args[0]);
- if (libraryPath.includes(libraryName)) {
- console.log("[+] Loading library " + libraryPath + "...");
- isLibLoaded = true;
- }
- },
- onLeave: function (args) {
- if (isLibLoaded) {
- processJniOnLoad(libraryName);
- isLibLoaded = false;
- }
- }
- });}Java.perform(function() {
- const libraryName = "libwhatsapp.so";
- 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上下文。
- const qbdi = require("/path/to/frida-qbdi");qbdi.import();function qbdiExec(ctx, funcPtr, funcSym, args, postSync) {
- var vm = new QBDI(); // create a QBDI VM
- var state = vm.getGPRState();
- var stack = vm.allocateVirtualStack(state, 0x10000); // allocate a virtual stack
- state.synchronizeContext(ctx, SyncDirection.FRIDA_TO_QBDI); // set up QBDI's context
- vm.addInstrumentedModuleFromAddr(funcPtr);
- var icbk = vm.newInstCallback(function (vm, gpr, fpr, data) {
- var inst = vm.getInstAnalysis();
- console.log("0x" + inst.address.toString(16) + " " + inst.disassembly);
- return VMAction.CONTINUE;
- });
- var iid = vm.addCodeCB(InstPosition.PREINST, icbk); // register pre-instruction callback
- var vcbk = vm.newVMCallback(function (vm, evt, gpr, fpr, data) {
- const module = Process.getModuleByAddress(evt.basicBlockStart);
- const offset = ptr(evt.basicBlockStart - module.base);
- if (evt.event & VMEvent.EXEC_TRANSFER_CALL) {
- console.warn(" -> transfer call to 0x" + evt.basicBlockStart.toString(16) + " (" + module.name + "@" + offset + ")");
- }
- if (evt.event & VMEvent.EXEC_TRANSFER_RETURN) {
- console.warn(" <- transfer return from 0x" + evt.basicBlockStart.toString(16) + " (" + module.name + "@" + offset + ")");
- }
- return VMAction.CONTINUE;
- });
- var vid = vm.addVMEventCB(VMEvent.EXEC_TRANSFER_CALL, vcbk); // register transfer callback
- var vid2 = vm.addVMEventCB(VMEvent.EXEC_TRANSFER_RETURN, vcbk); // register return callback
- const javavm = ptr(args[0]);
- const reserved = ptr(args[1]);
- console.log("[+] Executing " + funcSym + "(" + javavm + ", " + reserved + ") through QBDI...");
- vm.call(funcPtr, [javavm, reserved]);
- var retVal = state.getRegister(0); // x86 so return value is stored on $eax
- console.log("[+] " + funcSym + "() returned " + retVal);
- if (postSync) {
- state.synchronizeContext(ctx, SyncDirection.QBDI_TO_FRIDA);
- }
- 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í)行所有操作。
- var vcbk = vm.newVMCallback(function (vm, evt, gpr, fpr, data) {
- const module = Process.getModuleByAddress(evt.basicBlockStart);
- const base_addr = ptr(evt.basicBlockStart - module.base); // address must be relative to the module's start
- const size = evt.basicBlockEnd - evt.basicBlockStart;
- send({"bb": 1}, getBBInfo(base_addr, size, module)); // send the newly discovered basic block to the Python side
- 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如若轉載,請注明原文地址: