如何從命令行調(diào)用Android JNI函數(shù)并傳遞Java對(duì)象參數(shù)
一、前言
當(dāng)我們對(duì)某個(gè)使用原生庫(kù)(native library)的惡意軟件或者應(yīng)用進(jìn)行分析或滲透測(cè)試時(shí),如果能夠?qū)?kù)函數(shù)進(jìn)行隔離和執(zhí)行是再好不過(guò)的事情,這樣做我們就可以使用其自身的代碼來(lái)調(diào)試對(duì)抗惡意軟件。舉個(gè)例子,如果惡意軟件包含加密字符串,并使用原生函數(shù)完成解密過(guò)程,你可以選擇花大量時(shí)間逆向分析算法來(lái)編寫(xiě)自己的解密函數(shù),也可以選擇直接利用這個(gè)函數(shù)來(lái)處理任意輸入數(shù)據(jù)。如果使用后一種方法,即使惡意軟件作者完全改變了軟件的加密算法,你也可能不需要做任何修改即可完成任務(wù)。在這篇文章中,我將向讀者介紹如何利用并執(zhí)行原生庫(kù)函數(shù),即使調(diào)用這些函數(shù)時(shí)需要傳入JVM實(shí)例作為參數(shù)也沒(méi)問(wèn)題。
在之前的一篇文章中,我介紹了如何從Android原生代碼中創(chuàng)建一個(gè)Java虛擬機(jī),但我沒(méi)有給出一個(gè)具體的例子。因此,我會(huì)在本文中給出一個(gè)具體的例子來(lái)說(shuō)明這一點(diǎn)。
我們至少可以使用兩種方法來(lái)調(diào)用原生函數(shù)。第一種方法是對(duì)應(yīng)用進(jìn)行修改,使應(yīng)用接受你的輸入數(shù)據(jù)并傳遞給原生函數(shù)。例如,你可以寫(xiě)一個(gè)intent filter,將其轉(zhuǎn)化為Smali語(yǔ)言,將代碼添加到目標(biāo)應(yīng)用中,修改manifest文件,運(yùn)行應(yīng)用,使用adb命令將帶有參數(shù)的intent發(fā)送給目標(biāo)應(yīng)用即可。另一種方法更好,你可以添加一個(gè)小型socket或web服務(wù)器,使用curl向其發(fā)送請(qǐng)求,這種方法不需要修改manifest文件。
第二種方法的目標(biāo)是創(chuàng)建一個(gè)通過(guò)命令行運(yùn)行的小型原生可執(zhí)行工具,用來(lái)加載庫(kù)文件、調(diào)用目標(biāo)函數(shù)、傳遞我們輸入的任意參數(shù)。這樣我們就可以單獨(dú)運(yùn)行一個(gè)可執(zhí)行文件,而不需要運(yùn)行整個(gè)應(yīng)用程序,因此調(diào)試起來(lái)也就更為方便。
二、目標(biāo)應(yīng)用
我創(chuàng)建了一個(gè)示例應(yīng)用,方便讀者按照教程學(xué)習(xí),應(yīng)用名為“native-harness-target”。你可以使用以下命令將工程文件復(fù)制到本地并完成編譯(記得修改其中的“$ANDROID_*”變量)。
- git clone https://github.com/CalebFenton/native-harness-target.git
- cd native-harness-target
- echo 'ndk.dir=$ANDROID_NDK' > local.properties
- echo 'sdk.dir=$ANDROID_SDK' >> local.properties
- ./gradlew build
APK文件最終生成在“app/build/outputs/apk/”目錄。這篇文章中,我使用的是一個(gè)x86模擬器鏡像以及一個(gè)名為“app-universal-debug.apk”的應(yīng)用。
該應(yīng)用程序包含一個(gè)加密字符串,并會(huì)在運(yùn)行時(shí)使用原生庫(kù)對(duì)字符串進(jìn)行解密。以下是在Smail中字符串的解密過(guò)程:
- const/16 v3, 0x57
- new-array v1, v3, [B
- fill-array-data v1, :array_2a
- .local v1, "encryptedStringBytes":[B
- invoke-static {}, Lorg/cf/nativeharness/Cryptor;->getInstance()Lorg/cf/nativeharness/Cryptor;
- move-result-object v0
- .line 21
- .local v0, "c":Lorg/cf/nativeharness/Cryptor;
- # v3 contains a String made from encrypted bytes
- new-instance v3, Ljava/lang/String;
- invoke-direct {v3, v1}, Ljava/lang/String;-><init>([B)V
- # Call the decryption method, move result back to v3
- invoke-virtual {v0, v3}, Lorg/cf/nativeharness/Cryptor;->decryptString(Ljava/lang/String;)Ljava/lang/String;
- move-result-object v3
三、構(gòu)建Harness工具
我使用的是Tim 'diff' Strazzere開(kāi)發(fā)的一款名為“native-shim”的工具(Tim是RedNaga的一名成員)作為整套利用工具的基礎(chǔ),我將這個(gè)工具命名為“Harness”。在Android中,shim就像一個(gè)中間墊片,作用是加載一個(gè)庫(kù),并調(diào)用其“JNI_OnLoad”方法。它可以使調(diào)試工作更加簡(jiǎn)單,我們只需要使用調(diào)試器啟動(dòng)shim,并將具體路徑以參數(shù)形式傳遞給目標(biāo)庫(kù)即可。我們可以設(shè)置調(diào)試器的斷點(diǎn),在庫(kù)加載時(shí)觸發(fā)斷點(diǎn),這樣就能進(jìn)入“JNI_OnLoad”函數(shù)的處理流程。此外,native-shim還可以加載庫(kù)文件(.so文件)、獲取函數(shù)的引用并調(diào)用函數(shù),這一切對(duì)我們來(lái)說(shuō)都非常實(shí)用。
首先,我添加了部分代碼以初始化一個(gè)Java虛擬機(jī)實(shí)例,并將該實(shí)例傳遞給JNI_OnLoad函數(shù),這樣處理可以使JNI的初始化過(guò)程更為準(zhǔn)確。如果沒(méi)有真實(shí)的虛擬機(jī)實(shí)例,JNI庫(kù)的內(nèi)部狀態(tài)看起來(lái)可能會(huì)有些奇怪。不同庫(kù)文件的JNI_OnLoad的實(shí)現(xiàn)可能不盡相同,但這并不重要,重要的是這些實(shí)現(xiàn)都會(huì)檢查JNI版本,如這段代碼所示。因此我們需要?jiǎng)?chuàng)建一個(gè)虛擬機(jī)實(shí)例。
- printf(" [+] Initializing JavaVM Instance\n");
- JavaVM *vm = NULL;
- JNIEnv *env = NULL;
- int status = init_jvm(&vm, &env);
- if (status == 0) {
- printf(" [+] Initialization success (vm=%p, env=%p)\n", vm, env);
- } else {
- printf(" [!] Initialization failure (%i)\n", status);
- return -1;
- }
- printf(" [+] Calling JNI_OnLoad\n");
- onLoadFunc(vm, NULL);
我們的最終目標(biāo)是通過(guò)harness工具,開(kāi)啟一個(gè)socket服務(wù)器,讀取socket上傳輸?shù)膮?shù),使用這些參數(shù)來(lái)調(diào)用函數(shù)。這樣一來(lái),解密函數(shù)就會(huì)變成一個(gè)服務(wù),我們可以簡(jiǎn)單使用一個(gè)Python腳本與其通信。
四、理解目標(biāo)函數(shù)
在調(diào)用函數(shù)前,我們需要了解函數(shù)的簽名(即參數(shù)個(gè)數(shù)和參數(shù)類型)及函數(shù)的返回類型。我們可以先看一下org.cf.nativeharness.Cryptor類的反編譯代碼,類中包含decryptString原生方法的聲明,如下所示:
- public class Cryptor {
- private static Cryptor instance = null;
- public static Cryptor getInstance() {
- if (instance == null) {
- instance = new Cryptor();
- }
- return instance;
- }
- public native String decryptString(String encryptedString);
- }
從這段代碼中,我們可知該方法使用了一個(gè)String對(duì)象作為參數(shù),返回了一個(gè)String對(duì)象,看上去比較簡(jiǎn)單。現(xiàn)在我們將其轉(zhuǎn)化為原生函數(shù)形式,如下所示:
- Java_org_cf_nativeharness_Cryptor_decryptString(JNIEnv *env, jstring encryptedString)
每個(gè)JNI原生方法都需要將JNIEnv對(duì)象作為第一個(gè)參數(shù)。這意味著定義我們函數(shù)的typedef語(yǔ)句應(yīng)該如下所示:
- typedef jstring(*decryptString_t)(JNIEnv *, jstring);
不幸的是,如果你試圖使用上述typedef語(yǔ)句執(zhí)行這個(gè)函數(shù),你會(huì)得到一個(gè)錯(cuò)誤信息,如下所示:
- E/dalvikvm: JNI ERROR (app bug): attempt to use stale local reference 0x1
- E/dalvikvm: VM aborting
- A/libc: Fatal signal 6 (SIGABRT) at 0x00000a9a (code=-6), thread 2714 (harness)
這讓我困惑了好一陣子。我原先以為我可能在某個(gè)地方使用了空指針引用,因此我花了很多功夫,添加了許多printf語(yǔ)句,將內(nèi)存中所有相關(guān)指針的位置全部打印出來(lái)。這個(gè)錯(cuò)誤信息貌似在提示我某個(gè)參數(shù)出現(xiàn)了問(wèn)題,但我排查后發(fā)現(xiàn)所有指針都是正常的,沒(méi)有空引用情況。
我敢肯定我傳遞的參數(shù)沒(méi)有問(wèn)題,問(wèn)題可能出在JNI上。為了證實(shí)這一點(diǎn),我使用了javah命令,它可以生成實(shí)現(xiàn)原生方法所需要的C語(yǔ)言頭文件以及源代碼文件。
為了完成這個(gè)工作,你需要安裝dex2jar,找到正確的類路徑,將“platforms/android-19”改為你已經(jīng)安裝的具體平臺(tái),如下所示:
- $ d2j-dex2jar.sh app-universal-debug.apk
- dex2jar app-universal-debug.apk -> ./app-universal-debug-dex2jar.jar
- $ javah -cp app-universal-debug-dex2jar.jar:$ANDROID_SDK/platforms/android-19/android.jar org.cf.nativeharness.Cryptor
上述命令可以生成“_org_cf_nativeharness_Cryptor.h_”文件,其中包含如下信息:
- JNIEXPORT jstring JNICALL Java_org_cf_nativeharness_Cryptor_decryptString(JNIEnv *, jobject, jstring);
我們可以看到多了一個(gè)jobject作為第二個(gè)參數(shù),這究竟是為什么?如果你已經(jīng)知道了這個(gè)問(wèn)題的答案,我敢打賭你已經(jīng)花了很多時(shí)間深入學(xué)習(xí)了Smali,特別是其中的invoke-virtual方法。無(wú)論你在何時(shí)調(diào)用虛擬方法(通常都是些非靜態(tài)方法),第一個(gè)參數(shù)總是某個(gè)對(duì)象的實(shí)例,這個(gè)實(shí)例負(fù)責(zé)方法的具體實(shí)現(xiàn)。對(duì)于這個(gè)例子來(lái)說(shuō),此時(shí)第一個(gè)參數(shù)應(yīng)該是“org.cf.nativeharness.Cryptor”類的一個(gè)實(shí)例。
當(dāng)然,你可以投機(jī)取巧,比如可以查看str-crypt.c代碼,找到函數(shù)的具體調(diào)用形式。但你要知道你是個(gè)逆向分析師(或滲透測(cè)試員),你不可能拿到源代碼。
因此正確的typedef語(yǔ)句中應(yīng)該包含Cryptor實(shí)例的一個(gè)jobject對(duì)象,如下所示:
- typedef jstring(*decryptString_t)(JNIEnv *, jobject, jstring);
你可能會(huì)感到好奇,為什么我們不以靜態(tài)方法開(kāi)始介紹?沒(méi)有特別的理由,主要是因?yàn)槲以趯?xiě)這篇博客時(shí),所分析的原始應(yīng)用中目標(biāo)方法不是靜態(tài)方法,僅此而已。
這一部分內(nèi)容最大的收獲就是,如果你不確定函數(shù)的具體調(diào)用形式,你可以試一下javah命令,時(shí)刻牢記虛擬方法與Java中的Method#invoke()類似,使用某個(gè)實(shí)例對(duì)象作為第一個(gè)參數(shù)。
五、構(gòu)建Socket服務(wù)器
這是整個(gè)工作中最無(wú)趣的一個(gè)環(huán)節(jié),如果你不介意的話,我會(huì)跳過(guò)這一部分。你可以自行查看具體的實(shí)現(xiàn)源碼,如果愿意的話也可以提出修改意見(jiàn)。
六、Harness工具的使用方法
你可以通過(guò)如下幾個(gè)步驟來(lái)使用Harness工具。
1、啟動(dòng)模擬器
2、將harness push到設(shè)備中
3、將目標(biāo)原生庫(kù)及其他依賴庫(kù)push到設(shè)備中(本文示例中不涉及到依賴庫(kù))
4、將目標(biāo)應(yīng)用push到設(shè)備中
5、運(yùn)行harness工具
6、將模擬器的端口轉(zhuǎn)發(fā)到宿主機(jī)上
7、運(yùn)行“decrypt_string.py”,祈禱一切順利
你可以使用以下命令將應(yīng)用及原生庫(kù)push到設(shè)備中。
- $ adb push app/build/output/apk/app-universal-debug.apk /data/local/tmp/target-app.apk
- $ unzip app/build/outputs/apk/app-universal-debug.apk lib/x86/libstr-crypt.so
- Archive: app/build/outputs/apk/app-universal-debug.apk
- inflating: lib/x86/libstr-crypt.so
- $ adb push lib/x86/libstr-crypt.so /data/local/tmp
- lib/x86/libstr-crypt.so: 1 file pushed. 1.5 MB/s (5476 bytes in 0.004s)
使用如下命令將harness工具push到設(shè)備中。
- cd harness
- make && make install
注意:以上命令會(huì)將x86庫(kù)push到設(shè)備中,如果你確實(shí)想要使用其他的模擬器鏡像,你可以使用“adb push libs/
現(xiàn)在,你可以運(yùn)行harness,將目標(biāo)庫(kù)路徑作為第一個(gè)參數(shù)傳入,如下所示:
- $ adb shell /data/local/tmp/harness /data/local/tmp/libstr-crypt.so
- [*] Native Harness
- [+] Loading target: [ /data/local/tmp/libstr-crypt.so ]
- [+] Library Loaded!
- [+] Found JNI_OnLoad, good
- [+] Initializing JavaVM Instance
- WARNING: linker: libdvm.so has text relocations. This is wasting memory and is a security risk. Please fix.
- [+] Initialization success (vm=0xb8e420a0, env=0xb8e420e0)
- [+] Calling JNI_OnLoad
- [+] Found decryptString function, good (0xb761f4f0)
- [+] Finding Cryptor class
- [+] Found Cryptor class: 0x1d2001d9
- [+] Found Cryptor.getInstance(): 0xb27bc270
- [+] Instantiated Cryptor class: 0x1d2001dd
- [+] Starting socket server on port 5001
為了測(cè)試工具是否正常工作,你可以在另一個(gè)終端上運(yùn)行如下命令:
- $ ./decrypt_string.py
- Sending encrypted string
- Decrypted string: "Seek freedom and become captive of your desires. Seek discipline and find your liberty."
如果你在輸出結(jié)果中看到解密后的字符串,表明一切順利,非常完美。
七、總結(jié)
你可以根據(jù)實(shí)際情況,修改harness工具源碼中的目標(biāo)函數(shù)。另外,實(shí)際場(chǎng)景中,目標(biāo)程序錯(cuò)綜復(fù)雜,我并不能保證這種方法100%有效。