如何從命令行調用Android JNI函數并傳遞Java對象參數
一、前言
當我們對某個使用原生庫(native library)的惡意軟件或者應用進行分析或滲透測試時,如果能夠對庫函數進行隔離和執行是再好不過的事情,這樣做我們就可以使用其自身的代碼來調試對抗惡意軟件。舉個例子,如果惡意軟件包含加密字符串,并使用原生函數完成解密過程,你可以選擇花大量時間逆向分析算法來編寫自己的解密函數,也可以選擇直接利用這個函數來處理任意輸入數據。如果使用后一種方法,即使惡意軟件作者完全改變了軟件的加密算法,你也可能不需要做任何修改即可完成任務。在這篇文章中,我將向讀者介紹如何利用并執行原生庫函數,即使調用這些函數時需要傳入JVM實例作為參數也沒問題。
在之前的一篇文章中,我介紹了如何從Android原生代碼中創建一個Java虛擬機,但我沒有給出一個具體的例子。因此,我會在本文中給出一個具體的例子來說明這一點。
我們至少可以使用兩種方法來調用原生函數。第一種方法是對應用進行修改,使應用接受你的輸入數據并傳遞給原生函數。例如,你可以寫一個intent filter,將其轉化為Smali語言,將代碼添加到目標應用中,修改manifest文件,運行應用,使用adb命令將帶有參數的intent發送給目標應用即可。另一種方法更好,你可以添加一個小型socket或web服務器,使用curl向其發送請求,這種方法不需要修改manifest文件。
第二種方法的目標是創建一個通過命令行運行的小型原生可執行工具,用來加載庫文件、調用目標函數、傳遞我們輸入的任意參數。這樣我們就可以單獨運行一個可執行文件,而不需要運行整個應用程序,因此調試起來也就更為方便。
二、目標應用
我創建了一個示例應用,方便讀者按照教程學習,應用名為“native-harness-target”。你可以使用以下命令將工程文件復制到本地并完成編譯(記得修改其中的“$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/”目錄。這篇文章中,我使用的是一個x86模擬器鏡像以及一個名為“app-universal-debug.apk”的應用。
該應用程序包含一個加密字符串,并會在運行時使用原生庫對字符串進行解密。以下是在Smail中字符串的解密過程:
- 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
三、構建Harness工具
我使用的是Tim 'diff' Strazzere開發的一款名為“native-shim”的工具(Tim是RedNaga的一名成員)作為整套利用工具的基礎,我將這個工具命名為“Harness”。在Android中,shim就像一個中間墊片,作用是加載一個庫,并調用其“JNI_OnLoad”方法。它可以使調試工作更加簡單,我們只需要使用調試器啟動shim,并將具體路徑以參數形式傳遞給目標庫即可。我們可以設置調試器的斷點,在庫加載時觸發斷點,這樣就能進入“JNI_OnLoad”函數的處理流程。此外,native-shim還可以加載庫文件(.so文件)、獲取函數的引用并調用函數,這一切對我們來說都非常實用。
首先,我添加了部分代碼以初始化一個Java虛擬機實例,并將該實例傳遞給JNI_OnLoad函數,這樣處理可以使JNI的初始化過程更為準確。如果沒有真實的虛擬機實例,JNI庫的內部狀態看起來可能會有些奇怪。不同庫文件的JNI_OnLoad的實現可能不盡相同,但這并不重要,重要的是這些實現都會檢查JNI版本,如這段代碼所示。因此我們需要創建一個虛擬機實例。
- 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);
我們的最終目標是通過harness工具,開啟一個socket服務器,讀取socket上傳輸的參數,使用這些參數來調用函數。這樣一來,解密函數就會變成一個服務,我們可以簡單使用一個Python腳本與其通信。
四、理解目標函數
在調用函數前,我們需要了解函數的簽名(即參數個數和參數類型)及函數的返回類型。我們可以先看一下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);
- }
從這段代碼中,我們可知該方法使用了一個String對象作為參數,返回了一個String對象,看上去比較簡單。現在我們將其轉化為原生函數形式,如下所示:
- Java_org_cf_nativeharness_Cryptor_decryptString(JNIEnv *env, jstring encryptedString)
每個JNI原生方法都需要將JNIEnv對象作為第一個參數。這意味著定義我們函數的typedef語句應該如下所示:
- typedef jstring(*decryptString_t)(JNIEnv *, jstring);
不幸的是,如果你試圖使用上述typedef語句執行這個函數,你會得到一個錯誤信息,如下所示:
- 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)
這讓我困惑了好一陣子。我原先以為我可能在某個地方使用了空指針引用,因此我花了很多功夫,添加了許多printf語句,將內存中所有相關指針的位置全部打印出來。這個錯誤信息貌似在提示我某個參數出現了問題,但我排查后發現所有指針都是正常的,沒有空引用情況。
我敢肯定我傳遞的參數沒有問題,問題可能出在JNI上。為了證實這一點,我使用了javah命令,它可以生成實現原生方法所需要的C語言頭文件以及源代碼文件。
為了完成這個工作,你需要安裝dex2jar,找到正確的類路徑,將“platforms/android-19”改為你已經安裝的具體平臺,如下所示:
- $ 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);
我們可以看到多了一個jobject作為第二個參數,這究竟是為什么?如果你已經知道了這個問題的答案,我敢打賭你已經花了很多時間深入學習了Smali,特別是其中的invoke-virtual方法。無論你在何時調用虛擬方法(通常都是些非靜態方法),第一個參數總是某個對象的實例,這個實例負責方法的具體實現。對于這個例子來說,此時第一個參數應該是“org.cf.nativeharness.Cryptor”類的一個實例。
當然,你可以投機取巧,比如可以查看str-crypt.c代碼,找到函數的具體調用形式。但你要知道你是個逆向分析師(或滲透測試員),你不可能拿到源代碼。
因此正確的typedef語句中應該包含Cryptor實例的一個jobject對象,如下所示:
- typedef jstring(*decryptString_t)(JNIEnv *, jobject, jstring);
你可能會感到好奇,為什么我們不以靜態方法開始介紹?沒有特別的理由,主要是因為我在寫這篇博客時,所分析的原始應用中目標方法不是靜態方法,僅此而已。
這一部分內容最大的收獲就是,如果你不確定函數的具體調用形式,你可以試一下javah命令,時刻牢記虛擬方法與Java中的Method#invoke()類似,使用某個實例對象作為第一個參數。
五、構建Socket服務器
這是整個工作中最無趣的一個環節,如果你不介意的話,我會跳過這一部分。你可以自行查看具體的實現源碼,如果愿意的話也可以提出修改意見。
六、Harness工具的使用方法
你可以通過如下幾個步驟來使用Harness工具。
1、啟動模擬器
2、將harness push到設備中
3、將目標原生庫及其他依賴庫push到設備中(本文示例中不涉及到依賴庫)
4、將目標應用push到設備中
5、運行harness工具
6、將模擬器的端口轉發到宿主機上
7、運行“decrypt_string.py”,祈禱一切順利
你可以使用以下命令將應用及原生庫push到設備中。
- $ 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到設備中。
- cd harness
- make && make install
注意:以上命令會將x86庫push到設備中,如果你確實想要使用其他的模擬器鏡像,你可以使用“adb push libs/
現在,你可以運行harness,將目標庫路徑作為第一個參數傳入,如下所示:
- $ 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
為了測試工具是否正常工作,你可以在另一個終端上運行如下命令:
- $ ./decrypt_string.py
- Sending encrypted string
- Decrypted string: "Seek freedom and become captive of your desires. Seek discipline and find your liberty."
如果你在輸出結果中看到解密后的字符串,表明一切順利,非常完美。
七、總結
你可以根據實際情況,修改harness工具源碼中的目標函數。另外,實際場景中,目標程序錯綜復雜,我并不能保證這種方法100%有效。