ctf-wiki Android 原生層靜態分析實戰:以「2015 海峽兩岸 · 一個APK,逆向試試吧」為例
文档网络安全教程【免费下载链接】ctf-wikiCome and join us, we need you!项目地址https://gitcode.com/gh_mirrors/ct/ctf-wiki点击查看免费下载本篇文章基於 ctf-wiki 倉庫中 so-example.md 的核心案例系統講解 Android 應用原生層.so靜態分析的方法論如何從 APK 中提取 so 文件、利用 jadx 定位 Java 層入口、借助 IDA 逆向 JNI_OnLoad 動態註冊表並結合 Java 層代碼反推 native 函數的校驗邏輯最終求解 flag。讀完本文你將掌握一套可復用的「Java 層定位 so 層逆推 腳本求解」完整分析流程並理解_init/JNI_OnLoad等 so 加載時機對逆向結果的影響。基本方法原生層靜態分析的四步流程Android 應用存在 Java 層與原生層兩類代碼原生層代碼以.soShared Object形式存在。相比 Java 層可以直接使用 jadx 反編譯閱讀原生層代碼是編譯後的機器指令需要借助 IDA 等工具進行反彙編閱讀。針對原生層程序so-example.md 給出了四步基本流程提取 so 文件從 APK 中取出lib/abi/libxxx.so一般位於lib/armeabi、lib/arm64-v8a等目錄下IDA 反編譯 so 文件將 so 拖入 IDA閱讀其彙編/反編譯代碼根據 Java 層代碼分析 so 代碼通過 jadx 反編譯出的 Java 代碼尤其是native方法聲明、System.loadLibrary調用確定 so 中需要重點分析的函數根據 so 代碼的邏輯輔助整個程序的分析將原生層的校驗邏輯與 Java 層的業務邏輯串聯形成完整的程序行為認知。之所以必須先看 Java 層是因為 so 中的 native 函數通常由 Java 層的native方法聲明並通過 JNI 調用。Java 層代碼直接揭示了「輸入是什麼、輸出是什麼、何時被調用」是原生層分析的「地圖」。為什麼要理解 so 的加載機制在深入案例之前有必要先建立一個關鍵背景so 文件被加載時除了導出符號表中的函數外還會依次執行一段初始化代碼。根據倉庫中 so.md 對 Android 動態鏈接器bionic/linker的分析加載順序為.init函數DT_INIT.init_array中的函數DT_INIT_ARRAYJNI_OnLoad函數。其中JNI_OnLoad是很多 so 採用的「動態註冊」入口native 函數並非通過「Java 方法名 下劃線」的靜態命名規則暴露而是由JNI_OnLoad內調用RegisterNatives將 Java 方法名與 C 函數指針綁定。這正是後續案例中「直接搜testFlag找不到、必須先分析JNI_OnLoad」的根本原因而.init_array的存在則解釋了案例中「seed 字符串在真正校驗前已被悄悄改動」的陷阱。理解這條加載鏈是避免靜態分析時被「偽數據」誤導的前提。案例2015 海峽兩岸「一個APK逆向試試吧」下面以 2015 海峽兩岸 CTF 的經典題目為例完整走一遍原生層靜態分析流程。該題目是一個 NDK 應用核心校驗邏輯全部位於原生層。第一步jadx 反編譯確定主活動使用 jadx 反編譯 APK 後首先閱讀AndroidManifest.xml確定應用的主活動與包名?xml version1.0 encodingutf-8? manifest xmlns:androidhttp://schemas.android.com/apk/res/android xmlns:apphttp://schemas.android.com/apk/res-auto android:versionCode1 android:versionName1.0 packagecom.example.mobicrackndk uses-sdk android:minSdkVersion8 android:targetSdkVersion17 / application android:themestyle/AppTheme android:labelstring/app_name android:icondrawable/ic_launcher android:allowBackuptrue activity android:labelstring/app_name android:namecom.example.mobicrackndk.CrackMe intent-filter action android:nameandroid.intent.action.MAIN / category android:nameandroid.intent.category.LAUNCHER / /intent-filter /activity /application /manifest不難看出程序的主活動為com.example.mobicrackndk.CrackMe包名為com.example.mobicrackndk。正如倉庫中 android_code_location.md 所述AndroidManifest.xml中的包名與主 Activity 是定位分析起點的第一手線索——沒有主 Activity 的程序往往是隱藏程序如惡意樣本需要格外警惕。第二步分析主活動鎖定 native 函數閱讀主活動CrackMe的 Java 代碼可以確定程序的基本情況利用 native 函數testFlag判斷用戶輸入的pwdEditText是否滿足要求。public native boolean testFlag(String str); static { System.loadLibrary(mobicrackNDK); } protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView((int) R.layout.activity_crack_me); this.inputButton (Button) findViewById(R.id.input_button); this.pwdEditText (EditText) findViewById(R.id.pwd); this.inputButton.setOnClickListener(new OnClickListener() { public void onClick(View v) { CrackMe.this.input CrackMe.this.pwdEditText.getText().toString(); if (CrackMe.this.input null) { return; } if (CrackMe.this.testFlag(CrackMe.this.input)) { Toast.makeText(CrackMe.this, CrackMe.this.input, 1).show(); } else { Toast.makeText(CrackMe.this, Wrong flag, 1).show(); } } }); }從代碼中可以得到三個關鍵信息靜態代碼塊通過System.loadLibrary(mobicrackNDK)加載原生庫。根據 so.md 的說明System.loadLibrary加載的是項目中libs目錄下的libmobicrackNDK.so文件即lib 名稱 .so聲明瞭public native boolean testFlag(String str)輸入為用戶輸入的字符串返回值為布爾值決定 Toast 顯示「輸入內容」還是「Wrong flag」校驗邏輯完全被隱藏在 native 層Java 層沒有任何可逆的比較邏輯因此必須轉入 so 分析。第三步分析 so 文件從 JNI_OnLoad 找到真正的函數提取libmobicrackNDK.so後拖入 IDA。按照直覺我們首先會去直接查找testFlag函數但並沒有直接找到——這正是前面提到的動態註冊場景。於是只能退而分析JNI_OnLoad函數signed int __fastcall JNI_OnLoad(JNIEnv *a1) { JNIEnv *v1; // r4 int v2; // r5 char *v3; // r7 int v4; // r1 const char *v5; // r1 int v7; // [spCh] [bp-1Ch] v1 a1; v7 0; printf(JNI_OnLoad); if ( ((*v1)-FindClass)(v1, v7, 65540) ) goto LABEL_7; v2 v7; v3 classPathName[0]; fprintf((_sF 168), RegisterNatives start for %s, classPathName[0]); v4 (*(*v2 24))(v2, v3); if ( !v4 ) { v5 Native registration unable to find class %s; LABEL_6: fprintf((_sF 168), v5, v3); LABEL_7: fputs(GetEnv failed, (_sF 168)); return -1; } if ( (*(*v2 860))(v2, v4, off_400C, 2) 0 ) { v5 RegisterNatives failed for %s; goto LABEL_6; } return 65540; }可以發現程序在JNI_OnLoad中通過RegisterNatives動態註冊了類和相應的函數表off_400C註冊數量為 2最後一個參數。仔細查看.data段的off_400C.data:0000400C off_400C DCD aTestflag ; DATA XREF: JNI_OnLoad68↑o .data:0000400C ; .text:off_1258↑o .data:0000400C ; testFlag .data:00004010 DCD aLjavaLangStrin_0 ; (Ljava/lang/String;)Z .data:00004014 DCD abcdefghijklmn1 .data:00004018 DCD aHello ; hello .data:0000401C DCD aLjavaLangStrin_1 ; ()Ljava/lang/String; .data:00004020 DCD native_hello1 .data:00004020 ; .data ends這張表是 JNI 動態註冊的標準結構——JNINativeMethod數組每條記錄包含三個字段Java 層方法名如testFlag、hello方法簽名如(Ljava/lang/String;)Z表示「接收一個 String返回 boolean」()Ljava/lang/String;表示「無參數返回 String」對應的 C 函數指針如abcdefghijklmn1、native_hello1。由此可以確認Java 層的testFlag對應的 C 函數名為abcdefghijklmn。同時也註冊了第二個函數hello對應native_hello這在 Java 層代碼中並未出現屬於隱藏的額外入口值得後續繼續挖掘。逆向提示IDA 中看到XREF交叉引用指向.data段的函數指針表時應立即聯想到 JNI 動態註冊。逐條解析JNINativeMethod結構是從「搜不到導出函數」困境中脫身的標準手段。第四步分析 abcdefghijklmn 的三重校驗定位到abcdefghijklmn函數後可以發現程序主要在三個部分對輸入v10進行判斷。判斷 1長度校驗if ( strlen(v10) 16 )說明輸入的字符串長度必須為 16。判斷 2前 8 字符與 seed 比較v3 0; do { s2[v3] v10[v3] - v3; v3; } while ( v3 ! 8 ); v2 0; v12 0; if ( !strcmp(seed[0], s2) )這段代碼對輸入的前 8 個字符做變換第v3個字符減去其下標v3即s2[i] v10[i] - i然後與全局變量seed[0]比較。注意這裡v3從 0 到 7所以前 8 個字符是「字符碼減下標」後等於seed[0]。判斷 3調用 Java 靜態方法獲取 key再比較後 8 字符v9 ((*jniEnv)-FindClass)(); if ( !v9 ) { v4 class,failed; LABEL_11: _android_log_print(4, log, v4); exit(1); } v5 ((*jniEnv)-GetStaticMethodID)(); if ( !v5 ) { v4 method,failed; goto LABEL_11; } _JNIEnv::CallStaticVoidMethod(jniEnv, v9, v5); v6 ((*v1)-GetStaticFieldID)(v1, v9, key, Ljava/lang/String;); if ( !v6 ) _android_log_print(4, log, fid,failed); ((*v1)-GetStaticObjectField)(v1, v9, v6); v7 ((*jniEnv)-GetStringUTFChars)(); while ( v3 strlen(v7) 8 ) { v13[v3 - 8] v10[v3] - v3; v3; } v14 0; v2 strcmp(v7, v13) 0;第三個判斷中so 通過 JNI 反向調用了 Java 層的靜態方法。從彙編代碼可以看出它查找並調用了calcKey類中的靜態方法.text:00001070 LDR R0, [R5] .text:00001072 LDR R2, (aCalckey - 0x1080) .text:00001074 LDR R3, (aV - 0x1084) .text:00001076 LDR R4, [R0] .text:00001078 MOVS R1, #0x1C4 .text:0000107C ADD R2, PC ; calcKey .text:0000107E LDR R4, [R4,R1] .text:00001080 ADD R3, PC ; ()V對應的 Java 層代碼類calcKey如下public static String key; public static void calcKey() { key new StringBuffer(c7^WVHZ,).reverse().toString(); } }即調用calcKey()後將字符串c7^WVHZ,反轉得到key值。而後面的循環對輸入的第 8 個字符之後的部分再次做「字符碼減下標」變換v13[v3-8] v10[v3] - v3再與key比較。此處體現了 Android 逆向的一個重要特點native 層與 Java 層可以互相調用。so 中FindClass→GetStaticMethodID→CallStaticVoidMethod→GetStaticFieldID→GetStaticObjectField→GetStringUTFChars是 JNI 反向調用 Java 的典型鏈路在分析時必須同步回到 jadx 中查找被調用的類與字段。第五步第一次求解——失敗綜合三個判斷輸入的 16 個字符滿足長度為 16前 8 位input[i] - i seed[0][i]後 8 位input[i] - i key[i-8]其中key由c7^WVHZ,反轉得到。先嘗試按直覺求解seed[0]的初始內容對應字符串QflMnfH從後面第二次求解時給出的目標串可以反推此處先以程序中的比較目標為準key為,ZHVW^7c則s QflMnfH,ZHVW^7c flag for idx,c in enumerate(s): flag chr(ord(c)idx) print flag結果如下QgnPrelO4cRackEr但輸入QgnPrelO4cRackEr之後並不對——校驗失敗。第六步再次分析——被 _init_my 修改的 seed求解失敗說明我們對程序的理解有遺漏。此時要考慮程序是不是在執行校驗之前修改了相應的字符串對seed進行交叉引用XREF發現它在_init_my中被使用size_t _init_my() { size_t i; // r7 char *v1; // r4 size_t result; // r0 for ( i 0; ; i ) { v1 seed[0]; result strlen(seed[0]); if ( i result ) break; t[i] v1[i] - 3; } seed[0] t; byte_4038 0; return result; }函數名_init_my提示這是一個自定義的初始化函數——結合前面 so.md 講解的 so 加載流程.init→.init_array→JNI_OnLoad可以推斷_init_my被註冊在.init_array中在JNI_OnLoad之前就已執行。它對seed[0]的每個字符減 3然後將seed[0]指向新的內存t。也就是說我們在 IDA 中看到的seed原始內容QflMnfH只是「初始數據」真正參與校驗的seed[0]已經在加載階段被整體替換為「每個字符減 3」之後的內容。若直接在 IDA 中閱讀.data 段就會落入陷阱——這正是「靜態分析必須結合加載時機」的典型案例。第七步修正腳本獲得正確 flag既然前 8 位比較的seed[0]實際上是原始數據每個字符減 3 的結果則求解時需要在第 8 位之前補償這 3 的偏移。修正後的腳本如下s QflMnfH,ZHVW^7c flag for idx,c in enumerate(s): tmp ord(c) if idx8: tmp-3 flag chr(tmpidx) print flag最終得到正確的 flag➜ 2015-海峽兩岸一個APK逆向試試吧 python exp.py NdkMobiL4cRackEr驗證輸入NdkMobiL4cRackEr程序 Toast 顯示該字符串即校驗通過。當然該題目也可以使用動態調試在運行時直接觀察內存中seed的真實值來驗證上述推斷。本案例的知識點總結通過該題目可以沉澱出以下可復用的靜態分析經驗環節關鍵動作可驗證依據入口定位jadx 讀AndroidManifest.xml找主 Activity、包名主活動com.example.mobicrackndk.CrackMe函數鎖定Java 層找native聲明與System.loadLibrarytestFlag、libmobicrackNDK.so動態註冊IDA 分析JNI_OnLoad與RegisterNatives解析JNINativeMethod表off_400C→testFlag↔abcdefghijklmn雙向調用so 反向調用 JavaFindClass/GetStaticMethodID/GetStaticFieldIDcalcKey()生成key reverse(c7^WVHZ,)初始化陷阱交叉引用.data數據檢查.init_array等加載期改寫_init_my對seed逐字符減 3求解驗證用 Python 復刻變換邏輯逐字符補償偏移首次結果QgnPrelO4cRackEr失敗修正後NdkMobiL4cRackEr其中「初始化陷阱」是本章節最具普適性的教訓so 中的全局數據在加載期可能被.init/.init_array/JNI_OnLoad改寫靜態分析時不能輕信 IDA 顯示的.data初始值應對關鍵字符串逐一檢查交叉引用確認是否存在運行時修改路徑。延伸閱讀與練習靜態分析的方法論Java 層與原生層的定位方式參見 overview.md 與 android_code_location.mdJava 層靜態分析完整案例參見 java-example.md涵蓋 tinyCTF、Numdroid、Sharif、0CTF 等題目Java 層 原生層混合的綜合題目參見 complex-example.mdISCC Crackone、NJCTF easycrack、強網杯 picture lockso 加載機制的底層原理System.loadLibrary→doLoad→dvmLoadNativeCode→.init/.init_array/JNI_OnLoad參見 so.md若靜態分析受阻可轉向動態調試利用 IDA android_server 對原生層程序進行 attach 調試方法參見 ida_native_debug.md 與 dynamic_debug.md。練習題建議可以自行尋找 GCTF 2017 Android1、GCTF 2017 Android2、ISG 2017 Crackme、XMAN 2017 mobile3 rev1 等題目套用上述「Java 定位 → JNI 註冊表解析 → 校驗邏輯逆推 → 初始化陷阱排查」的流程進行訓練逐步建立對 Android 原生層逆向的肌肉記憶。赞分享文档网络安全教程【免费下载链接】ctf-wikiCome and join us, we need you!项目地址https://gitcode.com/gh_mirrors/ct/ctf-wiki点击查看免费下载相关推荐CTF-Wiki Android 逆向關鍵代碼定位實戰從靜態分析到動態調試的方法體系CTF Wiki Android 逆向關鍵代碼定位實戰從靜態分析到動態調試的方法體系 本文以 CTF Wiki 倉庫中 Android 逆向基本介紹 http文档网络安全教程ctf-wiki Android 動態調試debuggable 判斷邏輯、啟用方法與調試工具鏈指南ctf wiki Android 動態調試debuggable 判斷邏輯、啟用方法與調試工具鏈指南 Android 動態調試的前提是目標應用必須處於「可調試」文档网络安全教程CTF-Wiki 虛擬化 Pwn 環境實戰QEMU 源碼下載、編譯與調試完全指南CTF Wiki 虛擬化 Pwn 環境實戰QEMU 源碼下載、編譯與調試完全指南 本篇基於 CTF Wiki 虛擬化 Pwn 板塊的 QEMU 下載與編譯 h文档网络安全教程创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考