Android逆向新手答疑解惑篇——JNI與動態註冊
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進行更新。
此處我沒有勾選NDK是因為我使用自行下載的NDK版本,每個專案自行選擇NDK路徑。
新建專案
開啟Android Studio新建一個Project,並第一步勾選Include C++ support:
其餘選項可按需改動。新建完成後,就是一個完整的JNI的Hello World了。
專案分析
在左側的Andorid檢視中,可以看到比正常的專案多了一個cpp目錄,這就是我們存放C\C++原始碼的地方了:
生成的這個函式宣告看起來有點反人類,其實他是這樣子的
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架構)。
初始化函式
- 在Android系統中,對連結庫進行載入的程式叫做linker,檔案路徑為
/system/bin/linker
。linker載入so的時候會依次呼叫其init_array中的函式來執行開發者的初始化程式碼,可在IDA中按shift+f7
開啟Segmentation檢視,若有.init_array
項,那麼其中的函式就會被依次執行,這些函式都沒有引數。
注:更多精彩可看linker的原始碼。:) - 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,比如這樣:
這是因為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新年快樂,萬事如意。
前面涉及的一些理論知識,廢話稍多..見諒
若還有何新手常見的問題可留言提出,然後順便點一下關注謝謝:)
相關文章
- Android 深入理解 JNI(一)JNI 原理與靜態、動態註冊2017-06-21Android
- JNI原始碼分析(並實現JNI動態註冊)2017-09-26原始碼
- Oracle Listener 動態註冊 與 靜態註冊2013-12-02Oracle
- Oracle Listener 動態註冊與靜態註冊2011-09-21Oracle
- oracle監聽動態註冊與靜態註冊2013-04-15Oracle
- Android中動態註冊2020-03-10Android
- listener的靜態註冊與動態註冊詳述2009-03-30
- Oracle監聽的動態註冊與靜態註冊2011-09-01Oracle
- oracle監聽動態註冊與靜態註冊[轉帖]2011-01-05Oracle
- 動態註冊和靜態註冊2018-05-21
- 靜態註冊和動態註冊2013-11-27
- Oracle listener靜態註冊和動態註冊2010-04-12Oracle
- 【監聽】動態註冊和靜態註冊2015-11-30
- oracle的靜態註冊和動態註冊2024-11-11Oracle
- oracle監聽靜態註冊和動態註冊2010-10-20Oracle
- 靜態註冊和動態註冊總結(zt)2008-03-13
- listener靜態註冊和動態註冊總結2009-05-11
- Windows 98 答疑解惑(轉)2007-08-12Windows
- Android:JNI與NDK(二)交叉編譯與動態庫,靜態庫2019-07-31Android編譯
- Oracle監聽的靜態註冊和動態註冊2017-08-30Oracle
- Oracle listener靜態註冊和動態註冊總結2008-07-17Oracle
- SpringBoot基礎篇Bean之動態註冊2018-10-21Spring BootBean
- vue動態註冊元件2019-05-29Vue元件
- 監聽動態註冊2016-08-04
- 動態註冊監聽2015-06-08
- 動態監聽註冊2009-08-28
- listener的動態註冊2009-12-18
- Oracle監聽器的靜態註冊與動態註冊,以及DB_DOMAIN問題2009-07-08OracleAI
- 為Linux初學者答疑解惑2022-08-25Linux
- 關於Listener動態註冊2013-07-25
- rac監聽動態註冊2010-12-03
- 動態註冊監聽Listener2008-04-14
- Oracle 動態監聽註冊2009-12-18Oracle
- oracle listener 靜態和動態註冊2012-12-12Oracle
- oracle 監聽器動態與靜態註冊服務_listener2009-09-04Oracle
- oracle監聽器動態註冊於靜態註冊的區別2016-11-12Oracle
- Android廣播之靜態註冊2020-03-10Android
- Android逆向之旅---動態方式破解apk前奏篇(Eclipse動態除錯smail原始碼)2016-12-02AndroidAPKEclipse除錯AI原始碼