Android逆向新手答疑解惑篇——JNI與動態註冊

葫蘆娃發表於2018-02-15

Android逆向新手答疑解惑篇——JNI與動態註冊

何為JNI

JNI全稱為Java Native Interface,是使Java方法與C\C++函式互通的一座橋樑。通俗的講,它的作用就是使Java可以呼叫C\C++寫的函式、使C\C++可以呼叫Java寫的方法。

JNI的情景應用

效能

眾所周知,Android開發一般採用Java語言,雖Google推出了Kotlin語言的開發方案,但其實Kotlin的本質亦是基於Java虛擬機器,那麼在Android上系統,亦是基於Dalvik虛擬機器的,所以效能上,與跟採用Java開發是沒有任何區別的。由於Java是虛擬機器語言(指需要被編譯成虛擬機器程式碼,由虛擬機器執行的語言),所以無論是JVM(Java虛擬機器)還是Dalvik(Android定製版JVM),其程式效能在效能需求較高的情況下,就顯得有些不足了。
那麼這個時候就需要編譯型語言出馬了,編譯型語言將原始碼編譯為機器碼直接由CPU執行程式碼,使效能大幅提升。

程式碼安全性

Java程式碼的安全性很弱! 如果你沒有逆向Java或者Android程式的經驗,那麼可以請你寫一個簡單的Java程式或者Android程式,然後在Github或者其他地方下載一個jadx,開啟jadx-gui或者使用命令列,反編譯你編譯出來的程式,你可能會發現這是一個新世界,噢天哪,程式碼邏輯清晰可見,簡直就跟在看原始碼一樣!當然,這些只是反編譯器生成的虛擬碼,但也足以驚人。
這個時候,你就可以開始考慮將關鍵程式碼放到C\C++裡面寫了,因為其編譯之後就只有機器碼,機器碼可以反編譯成彙編,但彙編比高階語言更加的晦澀難懂,沒有一定技術功底的人無法直觀的理解彙編程式碼。雖可透過一些神器(如:IDA F5)來獲取偽碼,但這些偽碼相比Java的偽碼,簡直不堪入目。
所以編寫原生程式碼,不但可以擁有更高的效能,還可獲得一定的程式碼安全性保障。

JNI的使用

Google為Android的原生開發提供了開發者工具NDK(Native Development Kit),用來編譯C/C++專案。起初的時候構建一個NDK專案還需一番配置,現在隨著Android Studio的不斷更新,已經可以在Android Studio的專案中直接編寫、編譯了。

配置Android Studio & SDK

需要先對Android Studio進行一番配置。首先開啟Android Studio的設定頁面,File-Settings,搜尋Android SDK,勾選上CMake(編譯C\C++原始碼的程式)、LLDB(偵錯程式)、NDK,然後點選Apply進行更新。
配置Android SDK
此處我沒有勾選NDK是因為我使用自行下載的NDK版本,每個專案自行選擇NDK路徑。

新建專案

開啟Android Studio新建一個Project,並第一步勾選Include C++ support:

其餘選項可按需改動。新建完成後,就是一個完整的JNI的Hello World了。

專案分析

在左側的Andorid檢視中,可以看到比正常的專案多了一個cpp目錄,這就是我們存放C\C++原始碼的地方了:
Android檢視
生成的這個函式宣告看起來有點反人類,其實他是這樣子的

JNIEXPORT jstring JNICALL Java_cn_hluwa_demo01_MainActivity_stringFromJNI(JNIEnv *env,jobject /* this */)

Ctrl單擊JNIEXPORT可以看到其宏定義,是一個defalut屬性,而JNICALL則是個空定義,所以其實這兩個是可以忽略的。
重點關注的是返回型別jstring函式名Java_cn_hluwa_demo01_MainActivity_stringFromJNI引數列表JNIEnv和jobject

JNI中資料型別

大傢伙知道,Java中的基本資料型別是int、long、short、float、double、char、byte、boolean這些,為了避免與C語言的基本資料型別衝突,在JNI中,將JAVA的基本資料型別重定義成了:jint、jlong、jshort、jfloat、jdouble、jchar、jbyte、jboolean。那jstring又是怎麼回事呢?雖然String不是Java基本資料型別,但它實在是太常用了,所以便有了jstring;對於陣列,則是再後面再加個Array,如:jintArray、jbyteArray,但是沒有jstringArray,欸,那如何表示呢?還有其他的非基本型別呢? 除了上述以及jclass、jthrowable、jarray這些有專用重定義之外其他型別均使用jobject表示,所以String陣列就是jobjectArray啦。Ctrl+單擊jstring就可跳到jni.h標頭檔案檢視各個定義了。

JNI函式命名規則

可以看到這個函式名非常的長,這是因為JNI函式的繫結需要依賴於一個函式命名規則,讓Java層一下子就可以找到對應的原生函式。可以先看到java層的程式碼:

package cn.hluwa.demo01;
...
public class MainActivity extends AppCompatActivity {
    static {
        System.loadLibrary("native-lib");
    }
...
    public native String stringFromJNI();
}

stringFromJNI 加了一個native描述符,表示是一個原生函式,MainActivity是類名,cn.hluwa.demo01是包名,Java_cn_hluwa_demo01_MainActivity_stringFromJNI是對應的C函式名,那麼這個規則就很顯而易見了,將包名的.替換成_(因為.不能用於函式命名),然後Java_PackName_CLassName_MethodName。執行時,JNI就會依賴此規則來對函式進行繫結。

 

至於Native層呼叫Java層呢,JNI提供裡一系列函式,比如:

    jclass      (*FindClass)(JNIEnv*, const char*);
    jclass      (*GetObjectClass)(JNIEnv*, jobject);
    jboolean    (*IsInstanceOf)(JNIEnv*, jobject, jclass);
    jmethodID   (*GetMethodID)(JNIEnv*, jclass, const char*, const char*);
    jobject     (*CallObjectMethod)(JNIEnv*, jobject, jmethodID, ...);

同樣在jni.h中可以看到,或可自行查閱文件。

JNI的逆向

JNI的載入流程

在上述的Java程式碼中,可以看到static程式碼塊中多了一個System.loadLibrary("native-lib");,在Android開發中,原生程式碼一般使用C\C++編寫,然後編譯為一個動態連結庫,即檔案字尾為".so"的ELF檔案loadLibrary的作用就是載入這個動態連結庫,這樣後面的程式碼呼叫才能成功的找到對應的原生函式。而靜態程式碼塊的執行時機非常早,比什麼建構函式、onCreate都要早,在類載入的時候就被呼叫庫載入並非一定要在當前類、static塊中!。載入庫還有其他方法,比如使用System.load(String)方法,其傳入連結庫的具體路徑;甚至有的是在Native層中使用dlopen、mmap等方式來進行載入,就相當於自己實現了一個loadLibrary,但是最終的目的都是一樣的:將程式碼載入入記憶體中
Android編譯後的Apk其實只是個zip壓縮包,開啟後在其lib目錄中可以看到那些被loadLibrary載入的庫(lib中可能有多個資料夾,對應多種CPU架構)。

初始化函式

  1. 在Android系統中,對連結庫進行載入的程式叫做linker,檔案路徑為/system/bin/linker。linker載入so的時候會依次呼叫其init_array中的函式來執行開發者的初始化程式碼,可在IDA中按shift+f7開啟Segmentation檢視,若有.init_array項,那麼其中的函式就會被依次執行,這些函式都沒有引數。
    注:更多精彩可看linker的原始碼。:)
    init_array內容
  2. linker中載入so的函式叫做dlopen,而loadLibrary跟load其實也是基於dlopen,但其新增了一個回撥就是JNI_OnLoad,只要在程式碼中定義一個名為JNI_OnLoad的函式,dlopen完成之後就會將其呼叫。JNI_OnLoad的定義如下:
    jint JNI_OnLoad(JavaVM* vm, void* reserved)
    
    vm引數一般只是用來獲取env,以便呼叫一系列JNI函式。在IDA中,如果使用F5看到的是一個沒有引數或者引數型別不對的JNI_OnLoad,比如這樣:
    JNI_OnLoad
    這是因為IDA不能準確的識別函式宣告或變數型別,請點選函式名或者相應的變數名,然後按下y鍵,修改成正確的宣告\型別即可。

    JNI函式的引數

    根據stringFromJNI的例子可知,Native層多了兩個接收引數JNIEnv*和jobject,然後後面才是java層傳遞過來的引數。IDA經常不能正確識別引數列表,所以手動y的時候一定要正確的修改,就像這樣:
    stringFromJNI

動態註冊

如今許多開發者都出於安全性考慮或其他需求,不願使用函式名規則繫結,而是自己動態註冊來繫結native函式。方法也很簡單,只需呼叫RegisterNatives函式即可。其申明如下:

jint RegisterNatives(jclass clazz, const JNINativeMethod* methods,jint nMethods)

clazz 就是native函式所在的類,可透過FindClass獲取(將.換成/);methods是一個陣列,其中包含註冊資訊,nMethods是數量。例項程式碼如下:

JNIEXPORT jstring JNICALL stringFromJNI(JNIEnv *env,jobject /* this */) {
    std::string hello = "Hello from C++";
    return env->NewStringUTF(hello.c_str());
}


jint JNI_OnLoad(JavaVM* vm, void* reserved)
{
    JNIEnv * env;
    vm->GetEnv((void**)&env,JNI_VERSION_1_6);
    JNINativeMethod methods[] = {
            {"stringFromJNI","()Ljava/lang/String;",(void*)stringFromJNI},
    };
    env->RegisterNatives(env->FindClass("cn/hluwa/demo01/MainActivity"),methods,1);
    return JNI_VERSION_1_6;
}

JNINativeMethod結構體有三個成員,第一個是java層的方法名,第二個是方法簽名(括號內是引數型別括號後是返回型別,具體可搜尋JNINativeMethod signature這裡暫不多講),第三個是C函式指標。這樣三個引數就便成了一組註冊資訊。

 

反編譯的時候可能會是這樣子的(C++編譯。C編譯出來函式名只有RegisterNatives):

哇塞為什麼有四個引數?不要慌..第一個其實就是JNIEnv,第二個是class,第三個是methods。
所以如果在逆向過程中看到這個函式的呼叫,那麼直接檢視第三個引數即可得到具體的註冊資訊。

最後

祝大家2018新年快樂,萬事如意。
前面涉及的一些理論知識,廢話稍多..見諒
若還有何新手常見的問題可留言提出,然後順便點一下關注謝謝:)

相關文章