利用 Frida 和 QBDI 動(dòng)態(tài)分析 Android Native 的各項(xiàng)函數(shù)
由于可以檢索應(yīng)用程序代碼的Java表示形式,因此通常認(rèn)為Android應(yīng)用程序的逆向工程比較容易。攻擊者就是通過(guò)了解這些代碼版本,收集應(yīng)用程序信息,來(lái)發(fā)現(xiàn)漏洞的。如今,大多數(shù)Android應(yīng)用程序編輯器已經(jīng)意識(shí)到了這一點(diǎn),并盡力使反向工程不再那么容易。由于Java本地接口(Java Native Interface,簡(jiǎn)稱JNI),攻擊者通常依靠集成混淆策略或?qū)⒚舾泻瘮?shù)從Java / Kotlin端轉(zhuǎn)移到本機(jī)代碼。但是,當(dāng)他們決定同時(shí)使用兩者時(shí)(即,混淆的本機(jī)代碼),逆向工程過(guò)程變得更加復(fù)雜。結(jié)果,靜態(tài)查看本機(jī)庫(kù)的反匯編結(jié)果非常繁瑣且耗時(shí)。幸運(yùn)的是,運(yùn)行時(shí)檢查仍然是可能的,并且提供了一種便捷的方法來(lái)有效地掌握應(yīng)用程序的內(nèi)部機(jī)制,甚至避免混淆。JNI(Java Native Interface) Java本地接口,又叫Java原生接口。它允許Java調(diào)用C/C++的代碼,同時(shí)也允許在C/C++中調(diào)用Java的代碼。可以把JNI理解為一個(gè)橋梁,連接Java和底層。其實(shí)根據(jù)字面意思,JNI就是一個(gè)介于Java層和Native層的接口,而Native層就是C/C++層面。
由于針對(duì)常規(guī)調(diào)試器的保護(hù)在流行的應(yīng)用程序中非常普遍,因此使用動(dòng)態(tài)二進(jìn)制工具(DBI)框架(例如Frida)仍然是進(jìn)行全面檢查的理想選擇。從技術(shù)上講,在其他強(qiáng)大函數(shù)中,F(xiàn)rida允許用戶在本機(jī)函數(shù)的開(kāi)頭和結(jié)尾插入自己的代碼,或替換整個(gè)實(shí)現(xiàn)。但是,F(xiàn)rida在某些時(shí)候缺乏粒度,特別是在以指令規(guī)模檢查執(zhí)行情況時(shí)。在這種情況下,Quarkslab開(kāi)發(fā)的DBI框架QBDI可以幫助Frida在調(diào)用給定的本機(jī)函數(shù)時(shí)確定已執(zhí)行了代碼的哪些部分。
首先,我們必須正確設(shè)置測(cè)試環(huán)境。我們假設(shè)設(shè)備已經(jīng)植根并且Frida服務(wù)器已經(jīng)在運(yùn)行并且可以使用。除了Frida,我們還需要安裝QBDI。我們可以從源代碼編譯它或下載Android的發(fā)行版,使用說(shuō)明可以直接從官方頁(yè)面檢索到。解壓縮后,我們必須將共享庫(kù)libQBDI.so推送到設(shè)備上的/ data / local / tmp中。除此之外,我們還可以注意到在frida-qbdi.js中定義的QBDI綁定,該文件負(fù)責(zé)提供QBDI函數(shù)的接口。換句話說(shuō),它充當(dāng)QBDI和Frida之間的橋梁。
請(qǐng)注意,必須先關(guān)閉SELinux,否則由于某些限制規(guī)則,F(xiàn)rida無(wú)法將QBDI共享庫(kù)加載到內(nèi)存中。這將會(huì)顯示一條明確的錯(cuò)誤消息,告訴用戶權(quán)限被拒絕。在大多數(shù)情況下,僅使用root特權(quán)運(yùn)行此命令行即可完成此工作:
- setenforce 0
現(xiàn)在我們已經(jīng)具備了基于Frida和QBDI編寫(xiě)腳本的所有要求。
跟蹤本機(jī)函數(shù)
在對(duì)JNI共享庫(kù)執(zhí)行反向工程時(shí),始終值得檢查JNI_OnLoad()。確實(shí),此函數(shù)在庫(kù)加載后立即調(diào)用,并負(fù)責(zé)初始化。它能夠與Java端進(jìn)行交互,例如設(shè)置類的屬性,調(diào)用Java函數(shù)以及通過(guò)幾個(gè)JNI函數(shù)注冊(cè)其他本機(jī)方法。攻擊者通常依靠這些屬性來(lái)隱藏一些敏感的檢查和秘密的內(nèi)部機(jī)制。
接下來(lái),讓我們假設(shè)我們要分析一個(gè)流行的Android應(yīng)用程序,比如Whatsapp,其軟件包名稱為com.whatsapp,這是當(dāng)前Android上最廣泛的即時(shí)消息解決方案。它嵌入了一堆共享庫(kù),其中一個(gè)是libwhatsapp.so。不過(guò)要注意的是,該庫(kù)并不位于常規(guī)的lib /目錄中,因?yàn)樵谶\(yùn)行時(shí)存在一種解壓縮機(jī)制,該機(jī)制可將其從存檔中提取出來(lái),然后將其加載到內(nèi)存中,我們的目標(biāo)是弄清楚它的初始化函數(shù)在做什么。
利用 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,我們可以輕松地掛接我們要研究的函數(shù)。但是,由于Android應(yīng)用程序中嵌入的庫(kù)是通過(guò)System.loadLibrary()動(dòng)態(tài)加載的,該函數(shù)在后臺(tái)調(diào)用了本機(jī)android_dlopen_ext(),因此我們需要等待將目標(biāo)庫(kù)放入進(jìn)程的內(nèi)存中。使用此腳本,我們可以只訪問(wèn)函數(shù)的輸入(參數(shù))和輸出(返回值),也就是說(shuō),我們位于函數(shù)層。這是非常有限的,僅憑這一點(diǎn)基本上還不足以準(zhǔn)確掌握內(nèi)部的情況。因此,在這種精確的情況下,我們希望在較低級(jí)別上徹底檢查該函數(shù)。
利用 Frida 和 QBDI
QBDI提供的導(dǎo)入函數(shù)可以幫助我們克服以上的問(wèn)題,實(shí)際上,該DBI框架允許用戶通過(guò)跟蹤執(zhí)行的指令來(lái)執(zhí)行細(xì)粒度的分析。這對(duì)我們非常有用,因?yàn)槲覀兛梢陨钊肓私馕覀兊哪繕?biāo)函數(shù)。
這樣做的想法是,不是讓JNI_OnLoad()在常規(guī)啟動(dòng)期間運(yùn)行,而是在基本塊/指令范圍內(nèi)通過(guò)有條件的上下文來(lái)執(zhí)行它,以便確切地知道已執(zhí)行了什么。由于我們可以將這兩個(gè)DBI框架結(jié)合在一起,因此可以在我們之前編寫(xiě)的Frida腳本的基礎(chǔ)上集成這一全新的部分。
但是,我們使用的Interceptor.attach()函數(shù)只允許我們定義onEnter和onLeave回調(diào)。它意味著真正的函數(shù)總是被執(zhí)行,而不管你的條目回調(diào)應(yīng)該做什么。因此,初始化函數(shù)將執(zhí)行兩次:首先通過(guò)QBDI執(zhí)行,然后正常執(zhí)行。這是有問(wèn)題的,因?yàn)楦鶕?jù)情況不同,可能會(huì)出現(xiàn)一些意外的運(yùn)行時(shí)錯(cuò)誤,因?yàn)檫@個(gè)函數(shù)只需要調(diào)用一次。
幸運(yùn)的是,我們可以利用Frida的攔截器模塊帶來(lái)的另一個(gè)函數(shù),該函數(shù)包括替換本機(jī)函數(shù)的實(shí)現(xiàn)。這樣做,我們能夠設(shè)置QBDI上下文,執(zhí)行檢測(cè)的函數(shù)并像往常一樣無(wú)縫地將返回值轉(zhuǎn)發(fā)給調(diào)用方,以防止應(yīng)用程序崩潰,該技術(shù)旨在使過(guò)程足夠穩(wěn)定以恢復(fù)正常執(zhí)行。
然而,我們?nèi)匀幻媾R一個(gè)問(wèn)題,初始函數(shù)已被我們自己的新實(shí)現(xiàn)完全覆蓋。換句話說(shuō),該函數(shù)的代碼不是原始代碼,而是由Frida早些時(shí)候進(jìn)行檢測(cè)的。因此,在我們的代碼中,我們必須在使用QBDI執(zhí)行該函數(shù)之前恢復(fù)到真正的版本。
修改腳本后,processJniOnLoad()函數(shù)如下所示:
初始化
現(xiàn)在讓我們編寫(xiě)負(fù)責(zé)在QBDI上下文中執(zhí)行該函數(shù)的函數(shù),首先,我們需要初始化一個(gè)VM,實(shí)例化它的相關(guān)狀態(tài)(通用寄存器),并分配一個(gè)偽堆棧,該堆棧將在函數(shù)執(zhí)行期間使用。然后,我們必須將QBDI的上下文與當(dāng)前上下文進(jìn)行同步,也就是說(shuō),將實(shí)際CPU寄存器的值放入將要使用的QBDI。現(xiàn)在我們可以決定要檢測(cè)代碼的哪些部分。我們可以顯式定義一個(gè)任意地址范圍,也可以要求DBI檢測(cè)函數(shù)地址所在模塊的整個(gè)地址空間。為方便起見(jiàn),在本示例中將使用后者。
回調(diào)函數(shù)設(shè)置
我們必須指定所需的回調(diào)函數(shù)的種類,接下來(lái),我們要跟蹤已執(zhí)行的每條指令,因此我要放置一條預(yù)指令代碼回調(diào),這意味著將在位于目標(biāo)模塊中的每個(gè)已執(zhí)行指令之前調(diào)用我的函數(shù)。
此外,我們還可以添加幾個(gè)事件回調(diào)函數(shù),以便在執(zhí)行轉(zhuǎn)移到QBDI未檢測(cè)到的部分代碼中或從中返回時(shí)通知該事件。當(dāng)代碼與其他模塊(例如系統(tǒng)庫(kù))(libc.so,libart.so,libbinder.so等)進(jìn)行交互時(shí),此函數(shù)非常有用。請(qǐng)注意,根據(jù)您要監(jiān)視的內(nèi)容,其他幾種回調(diào)類型可能會(huì)很有幫助。
函數(shù)調(diào)用
現(xiàn)在我們準(zhǔn)備通過(guò)QBDI調(diào)用目標(biāo)函數(shù),當(dāng)然,我們需要傳遞預(yù)期的參數(shù),在我們的例子中是一個(gè)指向JavaVM對(duì)象的指針和一個(gè)空指針。然后,我們可以根據(jù)使用的調(diào)用約定在特定的QBDI寄存器或虛擬堆棧上檢索返回值。這個(gè)值必須從我們之前編寫(xiě)的本機(jī)替換函數(shù)中被轉(zhuǎn)發(fā)和返回。否則,應(yīng)用程序很可能會(huì)因?yàn)閷?duì)JNI版本的檢查不滿意而停止運(yùn)行,JNI_OnLoad()應(yīng)該返回JNI版本。
我們可以選擇使用QBDI的CPU恢復(fù)真正的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進(jìn)行編譯,以便正確包含包含QBDI綁定的frida-qbdi.js。官方文檔頁(yè)對(duì)編譯過(guò)程進(jìn)行了詳細(xì)說(shuō)明。
生成一個(gè)覆蓋文件
具有包含已執(zhí)行的所有指令的跟蹤是很有必要的,但對(duì)于反向工程來(lái)說(shuō)并不方便。事實(shí)上,我們不能一眼就分辨出整個(gè)執(zhí)行過(guò)程中的路徑。為了正確地呈現(xiàn)捕獲的軌跡,在反匯編器中集成可能是一個(gè)好主意。這樣,人們就可以準(zhǔn)確地看到全部的路徑。然而,大多數(shù)反匯編器本身并沒(méi)有提供這樣的選項(xiàng)。對(duì)我們來(lái)說(shuō)幸運(yùn)的是,各種插件都提供了這樣的選項(xiàng)。在本例中,我們使用Lighthouse和Dragondance分別用于IDA Pro和Ghidra。這些插件可以通過(guò)導(dǎo)入drcov格式的代碼覆蓋文件來(lái)輕松配置,DynamioRIO使用這種格式存儲(chǔ)關(guān)于代碼覆蓋率的信息。
drcov格式非常簡(jiǎn)單:除了標(biāo)頭字段外,還必須指定描述進(jìn)程的內(nèi)存布局的模塊表,為每個(gè)模塊分配一個(gè)惟一的ID。此后,就有了所謂的基本塊表。該表包含執(zhí)行期間已命中的每個(gè)基本塊,一個(gè)基本塊由三個(gè)屬性定義:它的開(kāi)始(相對(duì))地址,它的大小和它所屬模塊的ID。
由于我們能夠在每個(gè)基本塊的開(kāi)頭放置一個(gè)回調(diào),因此我們可以確定這些值,從而生成我們自己的文件。現(xiàn)在,我們需要檢索基地址和所有已執(zhí)行的基本塊的大小,而不是按指令規(guī)模工作。實(shí)際上,我們必須定義一個(gè)類型為BASIC_BLOCK_NEW 的QBDI事件回調(diào)函數(shù),該函數(shù)負(fù)責(zé)收集此類信息。每當(dāng)QBDI將要執(zhí)行一個(gè)新的基本程序塊時(shí),我們的函數(shù)都會(huì)被調(diào)用,到目前為止尚不知道。在本示例中,我們不僅要打印有關(guān)此基本塊的一些有趣的值,還要?jiǎng)?chuàng)建一個(gè)代碼覆蓋率文件,以后可以在反匯編器中將其導(dǎo)入。但是,在Frida腳本的上下文中,我們無(wú)法操作文件。結(jié)果,我們必須停止使用frida命令行實(shí)用程序,并直接依賴于Frida提供的消息傳遞系統(tǒng)從底層Python腳本運(yùn)行我們的JS腳本。這樣做使我們能夠在JS和Python端之間進(jìn)行通信,然后對(duì)所需的文件系統(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);
請(qǐng)注意,getBBInfo()函數(shù)僅在發(fā)送消息之前先序列化有關(guān)基本塊的信息。顯然,Python端必須處理此類消息,將與執(zhí)行相關(guān)的內(nèi)容保留在內(nèi)存中,并最終以上述正確的格式相應(yīng)地生成代碼覆蓋文件。如果一切順利,由于其相應(yīng)的代碼覆蓋插件,可以將輸出文件加載到IDA Pro或Ghidra中。所有已執(zhí)行的基本塊都將突出顯示,現(xiàn)在我們可以更清楚地遵循執(zhí)行流程,而只關(guān)注代碼的相關(guān)部分。
總結(jié)
Java/Kotlin逆向工程的易用性使得Android應(yīng)用程序開(kāi)發(fā)人員可以使用C/ c++來(lái)實(shí)現(xiàn)某些漏洞層面的操作。因此,本文所講的方法就是要讓逆向工程師逆向的過(guò)程變得很困難。因此,將QBDI與Frida一起使用是一個(gè)非常好的選擇,尤其是在研究那些本機(jī)函數(shù)時(shí)。這種組合確實(shí)提供了一種方法,可以弄清一個(gè)函數(shù)在不同層次上的作用,即函數(shù)、基本塊和指令規(guī)模。此外,還可以利用QBDI的執(zhí)行傳輸事件來(lái)解析對(duì)系統(tǒng)庫(kù)的外部調(diào)用,或者跟蹤內(nèi)存訪問(wèn),然后了解執(zhí)行的總體情況。為了有效地協(xié)助反向工程師,可以將收集的信息明智地集成到一些現(xiàn)有的面向反向工程的工具中,以完善其靜態(tài)分析。除了生成執(zhí)行流程的直觀表示之外,從運(yùn)行時(shí)獲取此類反饋對(duì)于其他與安全相關(guān)的目的(如模糊測(cè)試)也很有價(jià)值。還值得注意的是,如果函數(shù)很重要,F(xiàn)rida和QBDI都可以提供C / C ++ API。
本文翻譯自:https://blog.quarkslab.com/why-are-frida-and-qbdi-a-great-blend-on-android.html如若轉(zhuǎn)載,請(qǐng)注明原文地址: