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

看雪專欄:zhuanlan.kanxue.com/use

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

何為JNI

JNI全稱為Java Native Interface,是使Java方法與CC++函數互通的一座橋樑。通俗的講,它的作用就是使Java可以調用CC++寫的函數、使CC++可以調用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或者使用命令行,反編譯你編譯出來的程序,你可能會發現這是一個新世界,噢天哪,代碼邏輯清晰可見,簡直就跟在看源碼一樣!當然,這些只是反編譯器生成的偽代碼,但也足以驚人。

這個時候,你就可以開始考慮將關鍵代碼放到CC++裡面寫了,因為其編譯之後就只有機器碼,機器碼可以反編譯成彙編,但彙編比高級語言更加的晦澀難懂,沒有一定技術功底的人無法直觀的理解彙編代碼。雖可通過一些神器(如: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(編譯CC++源碼的程序)、LLDB(調試器)、NDK,然後點擊Apply進行更新。

此處我沒有勾選NDK是因為我使用自行下載的NDK版本,每個項目自行選擇NDK路徑。

新建項目

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

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

項目分析

在左側的Andorid視圖中,可以看到比正常的項目多了一個cpp目錄,這就是我們存放CC++源碼的地方了:

生成的這個函數聲明看起來有點反人類,其實他是這樣子的

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

Ctrl單擊JNIEXPORT可以看到其宏定義,是一個defalut屬性,而JNICALL則是個空定義,所以其實這兩個是可以忽略的。

重點關注的是返回類型jstring函數名Javacnhluwademo01MainActivity_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開發中,原生代碼一般使用CC++編寫,然後編譯為一個動態鏈接庫,即文件後綴為".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的時候會依次調用其initarray中的函數來執行開發者的初始化代碼,可在IDA中按shift+f7打開Segmentation視圖,若有.init_array項,那麼其中的函數就會被依次執行,這些函數都沒有參數。

    注:更多精彩可看linker的源代碼。:)

  1. linker中載入so的函數叫做dlopen,而loadLibrary跟load其實也是基於dlopen,但其添加了一個回調就是JNIOnLoad,只要在代碼中定義一個名為JNIOnLoad的函數,dlopen完成之後就會將其調用。JNI_OnLoad的定義如下:

jint JNI_OnLoad(JavaVM* vm, void* reserved)

vm參數一般只是用來獲取env,以便調用一系列JNI函數。在IDA中,如果使用F5看到的是一個沒有參數或者參數類型不對的JNIOnLoad,比如這樣:

這是因為IDA不能準確的識別函數聲明或變數類型,請點擊函數名或者相應的變數名,然後按下y鍵,修改成正確的聲明類型即可。

JNI函數的參數

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

動態註冊

如今許多開發者都出於安全性考慮或其他需求,不願使用函數名規則綁定,而是自己動態註冊來綁定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新年快樂,萬事如意。 前面涉及的一些理論知識,廢話稍多..見諒 若還有何新手常見的問題可留言提出

推薦閱讀:

超棒黑客必備清單
Golang二進位文件混淆保護
青蛙旅行 — Unity3d類安卓遊戲逆向分析初探
那些年病毒用過的損招——反調試技術
GCHQ 對卡巴斯基實驗室的商業軟體進行逆向工程,卡巴斯基實驗室是否可以提起法律訴訟?

TAG:逆向工程 | 移動安全 | Android開發 |