Android逆向分析概述
學習逆向的初衷是想系統學習Android下的hook技術和工具, 想系統學習Android的hook技術和工具是因為Android移動效能實戰這本書. 這本書裡用hook技術hook一些關鍵函式來計算關鍵函式的呼叫引數和呼叫時長, 從而確定性能問題發生的位置和原因. 但目前沒有比較系統的講解hook的書籍, 所以就係統的瞭解下逆向分析.
在讀了姜維的Android應用安全防護和逆向分析和豐生強的Android軟體安全與逆向分析後, 準備分享下這方面知識. 在寫文章時發現, 這兩本書缺少對最新的逆向工具和加固工具的描述. 在查閱相關文獻後補充了這一部分.
本文從五個維度來講解Android逆向, 每個維度儘量分’原理’, ‘工具’, ‘例項’三個方面.
- 反編譯
- 靜態分析
- 動態分析
- 重編譯
- Docker
1.反編譯
1.1 原理
說到反編譯, 先來看下正向編譯, 如上圖, 正向編譯是
java -> class -> dex -> apk
反編譯和正向編譯稍有不同, 反編譯可以分成兩類:
java
這種方法是將dex檔案轉為smali, smali是Dalvik虛擬機器的組合語言, 可以用來動態除錯程式.
java
這種方法中是將Dalvik位元組碼轉化為等價的Java位元組碼, 然後用豐富的java分析工具分析原始碼.
如何反反編譯(即對抗反編譯):
- 閱讀反編譯工具原始碼查詢缺陷
- 壓力測試找反編譯工具bug(下載很多apk, 寫個指令碼呼叫ApkTool反編譯這些apk, ApkTool因為某些bug無法反編譯某個apk, 這時我們就通過壓力測試找到了ApkTool的bug, 將發現的這個應用到我們的apk中, 即可保護我們的apk免受ApkTool反編譯)
如何反反反編譯呢:
- 閱讀反編譯原始碼修復缺陷
1.2 工具
上圖的反編譯工具走的java 路線, 即先把apk裡的dex找到, 然後使用Enjarify/dex2jar/classyshark/jadx反編譯得到jar包, 然後使用jd-gui/CFR/Procyon閱讀jar包裡的java原始碼. 這些工具各有優缺點, 我們一般選擇dex2jar+jd-gui, 相比其他工具, jd-gui雖然很久不更新了, 但是支援跳轉, 方便檢視程式碼. 特別說明下Bytecode-Viewer, 其是Procyon的一個前端, 同時集成了很多其他工具, 功能強大.
看下上圖, 這些工具走的是java 路線. 將dex檔案轉化為smali彙編, 然後直接閱讀smali組合語言, 或者smali再轉為java(這裡沒有強大的工具, 可能經常無法成功轉化).
最常用反編譯工具
從上圖可以看到有很多反編譯工具, 我們平時最常用的是dex2jar+jd-gui
和ApkTool
.
jd-gui不僅有不錯的介面, 最關鍵的是支援類之間的跳轉, 在混淆後的程式碼中跳轉可以大大方便我們檢視.
ApkTool隱隱有無冕之王的聲勢, 可以反編譯程式碼和資源, 修改後可以重編譯成apk, 在Android Studio下使用smalidea外掛還可以完成無原始碼除錯, 十分強大.
工具地址:
1.3 例項
這裡以一個例項說明下反反編譯和反反反編譯:
使用早期ApkTool反編譯apk時,可能會遇到反編譯失敗, 出現如下問題:
12345678910111213141516 | Exception inthread"main"brut.androlib.AndrolibException:Multiple res specs:attr/nameat brut.androlib.res.data.ResTypeSpec.addResSpec(ResTypeSpec.java:78)at brut.androlib.res.decoder.ARSCDecoder.readEntry(ARSCDecoder.java:248)at brut.androlib.res.decoder.ARSCDecoder.readTableType(ARSCDecoder.java:212)at brut.androlib.res.decoder.ARSCDecoder.readTableTypeSpec(ARSCDecoder.java:154)at brut.androlib.res.decoder.ARSCDecoder.readTablePackage(ARSCDecoder.java:116)at brut.androlib.res.decoder.ARSCDecoder.readTableHeader(ARSCDecoder.java:78)at brut.androlib.res.decoder.ARSCDecoder.decode(ARSCDecoder.java:47)at brut.androlib.res.AndrolibResources.getResPackagesFromApk(AndrolibResources.java:544)at brut.androlib.res.AndrolibResources.loadMainPkg(AndrolibResources.java:63)at brut.androlib.res.AndrolibResources.getResTable(AndrolibResources.java:55)at brut.androlib.Androlib.getResTable(Androlib.java:66)at brut.androlib.ApkDecoder.setTargetSdkVersion(ApkDecoder.java:198)at brut.androlib.ApkDecoder.decode(ApkDecoder.java:96)at brut.apktool.Main.cmdDecode(Main.java:165)at brut.apktool.Main.main(Main.java:81) |
檢視ApkTool程式碼發現, 是Apk利用了ApkTool的一個bug, Apk做了混淆,在編譯時存入了重複id值,導致ApkTool crash.
針對這個問題, 解決辦法是create fake names to prevent abuse from duplicate key
, 其github提交如下:
names
例項地址:
2.靜態分析
2.1 原理
什麼是靜態分析?
不執行程式碼,採用反編譯工具生成程式的反編譯程式碼,然後閱讀反編譯程式碼來掌握程式功能.
Android靜態分析步驟:
- 反編譯apk程式
- 檢視Application類(在Activity啟動之前, 一般加固/授權放在這裡)
- 檢視MainActivity類
- 找關鍵程式碼
反靜態分析:
- 程式碼混淆(ProGuard等)
- 使用NDK+STL編寫
- 手動註冊native函式()
- 預設情況, 使用javah, com.example.k12 -> java_完整包名類名方法名.
但可以使用函式對映表static JNINativeMethod methods[] = {
來註冊native函式名,
{"dynamicGenerateKey", "(Ljava/lang/String;)Ljava/lang/String;", (void
*) native_dynamic_key}}; RegisterNatives(jclass clazz, const
JNINativeMethod* methods,jint nMethods)
提高破解難度.
- 預設情況, 使用javah, com.example.k12 -> java_完整包名類名方法名.
- 加固(dex/so加殼,指令抽取等)
反反靜態分析:
- 定位關鍵程式碼技巧
- 資訊反饋法(點選介面, 出現
註冊失敗
, 那麼檢查程式碼裡哪裡使用到了註冊失敗
) - 特徵函式法/關鍵系統呼叫(一般情況下, 最終都會呼叫到系統函式. 為了提升難度, 可以自制和系統函式功能相同的函式, 這樣難以下斷點)
- Log程式碼注入法/棧跟蹤法(動靜分析結合, 在合適位置注入log, 編譯執行時可以列印當前上下文資訊和堆疊資訊)
- 資訊反饋法(點選介面, 出現
- IDA分析彙編(asm->c, 雖然很多函式還沒重定位, 但是c比彙編的表達力更強, 更便於分析)
- 脫殼
- IDA脫殼(dvm:dvmDexFileOpenPartial, art:openDexFileNative, 無論如何, 最終都是要呼叫系統API載入dex, 在這裡加斷點, 然後dump出記憶體中的dex檔案[現在一些加固工具都是自己寫載入dex的函式, 這樣簡單在上述方法上加斷點是無法命中的])
- Xposed/VirtualXposed
Dex檔案格式
Dalvik指令集
空指令 暫存器資料操作指令 返回指令 資料定義指令 鎖指令 例項操作指令
陣列/欄位操作指令 異常指令 跳轉指令 比較指令 方法呼叫指令 資料轉換指令
資料運算指令
.field private isFlag:z 定義變數
.method 方法
.parameter 方法引數
.prologue 方法開始
.line 12 此方法位於第12行
return-void 函式返回void
.end method 函式結束
new-instance 建立例項
iput-object 物件賦值
iget-object 呼叫物件
invoke-static 呼叫靜態函式條件跳轉分支:
invoke-super 呼叫父函式
invoke-direct 呼叫函式
“if-eq vA, vB, :cond_**” 如果vA等於vB則跳轉到:cond_**
“if-ne vA, vB, :cond_**” 如果vA不等於vB則跳轉到:cond_**
“if-lt vA, vB, :cond_**” 如果vA小於vB則跳轉到:cond_**
“if-ge vA, vB, :cond_**” 如果vA大於等於vB則跳轉到:cond_**
“if-gt vA, vB, :cond_**” 如果vA大於vB則跳轉到:cond_**
“if-le vA, vB, :cond_**” 如果vA小於等於vB則跳轉到:cond_**
“if-eqz vA, :cond_**” 如果vA等於0則跳轉到:cond_**
“if-nez vA, :cond_**” 如果vA不等於0則跳轉到:cond_**
“if-ltz vA, :cond_**” 如果vA小於0則跳轉到:cond_**
“if-gez vA, :cond_**” 如果vA大於等於0則跳轉到:cond_**
“if-gtz vA, :cond_**” 如果vA大於0則跳轉到:cond_**
“if-lez vA, :cond_**” 如果vA小於等於0則跳轉到:cond_**
這裡主要關注跳轉指令, 因為我們逆向Apk時, 一般只關注特殊的幾點邏輯,
注意跳轉語句跳轉到了哪些特殊函式.
ELF檔案格式和定址方式
Arm彙編語法
跳轉指令 儲存器訪問指令 資料處理指令(加減乘除)
空操作 軟中斷
arm彙編裡我們主要關注如下函式呼叫語句:
BL 執行函式呼叫
BLX執行函式呼叫, 可以在ARM和Thumb指令集間切換
這裡解釋下ARM和Thumb指令集的區別:
Thumb是ARM體系結構中一種指令集。
Thumb指令只有16bit,可以減小程式碼量。
Thumb指令功能並不完整,必要時仍需要使用ARM指令集。
擴充套件下NEON/VFP知識點:
VFP是一種浮點硬體加速器。
NEON是一個SIMD(單指令多資料)協處理器。
以加法指令為例,單指令單資料(SISD)的CPU對加法指令譯碼後,執行部件先訪問記憶體,取得第一個運算元;之後再一次訪問記憶體,取得第二個運算元;隨後才能進行求和運算。而在SIMD型的CPU中,指令譯碼後幾個執行部件同時訪問記憶體,一次性獲得所有運算元進行運算。這個特點使SIMD特別適合於多媒體應用等資料密集型運算。
加固技術:
第一代加固技術——混淆技術;
第二代加固技術——加殼技術(落地與不落地脫殼);
第三代加固技術——指令抽離;
第四代加固技術——指令轉換,即VMP(虛擬軟體保護)加固技術。
二代加固:
加殼是指給可執行檔案加個外衣, 這個外衣就是殼程式. 殼程式先取得程式的控制權, 之後把加密的可執行程式在記憶體中解開為真正的程式並執行.
三代加固:
抽取dex檔案中DexCode的部分結構,即虛擬機器操作碼。在虛擬機器載入到此類的時候對DexCode結構進行還原。
比如此圖中的getPwd方法很重要,需要抽取. 那麼生成Dex檔案後, 找到Dex檔案中的getPwd的方法體, 將對應的方法體抽取出來放到so檔案或者特定位置. 然後Hook住系統的FindClass方法, 當系統查詢CoreUtils類時, 找到getPwd在記憶體中的位置, 然後將抽取出來的方法重新寫入. 這樣即使被破解拿到Dex, 這個Dex也是殘缺的, 沒有關鍵的函式.這時候如果我們檢視Dex, 會發現getPwd的方法是個空方法.
該方法的流程如下:
四代加固VMP技術:
基於三代加固技術,把原本可執行檔案中的機器指令程式碼轉換成了它自己虛擬機器的指令,而且還插入了大量的垃圾程式碼。
這種方法將核心程式碼轉化為虛擬機器自己的指令, 破解apk的難度和破解虛擬機器指令的難度一致. PC上存在類似的VMProtect, 號稱無人一定能破.
從難度方面來說, 二代加固一般還有破解思路, 但到了四代加固這裡, 一般的逆向脫殼技術全部失效, 你面對的是如何破解這個虛擬機器.
2.2 例項
apk加殼例項:
apk加殼例項可以用上圖來說明, 我們把要加固的myapk.apk放到一個dex尾部. 這個dex有脫殼邏輯, 程式執行時, 首先執行這個脫殼dex, 脫殼dex從dex尾部獲取到要加密的apk的大小, 然後從自己的dex中拷貝出這個myapk.apk, 最後呼叫Android系統API執行myapk.apk. 這樣就算用ApkTool等逆向工具, 也無法直接獲得我們加固的myapk.apk. 為了增大逆向難度, 我們可以把脫殼邏輯用c實現放到so檔案中, 同時把加密的myapk.apk分段放到so檔案中. 為了防止特徵破解, 我們可以改寫apk魔數. 這樣下來, 一個簡單的加固工具就完成了.
這裡提供一個demo, 只有最簡單的把myapk.apk放到脫殼dex尾部的功能, git地址:
demo分為三個專案:
- DexReinforcingTools
- 給Apk加殼的工具, 可以用java或者cpp或者任何其他語言寫成.
- MyApk
- 需要加固的Apk
- ShellingMyApk
- 脫殼Apk, 實際安裝到使用者手機上的是該Apk, 其在Application的attachBaseContext 時會解壓得到實際的apk檔案, 然後執行實際的Apk.
這裡再說下, 這種二代加殼是現在最簡單的加殼方式, 也是最基本的加殼方式.
參考文件:
3.動態分析
3.1 原理
動態分析主要基於下面兩個工具:
JPDA(Java Platform Debugger Architecture)
JPDA分為三層, 分別是JVMTI,JDWP,JDI.
JVMTI(Java Virtual Machine Tool Interface)是一套由虛擬機器直接提供的 native介面,通過這些介面,開發人員不僅除錯在該虛擬機器上執行的 Java程式,還能檢視它們執行的狀態,設定回撥函式,控制某些環境變數,從而優化程式效能。
JDWP(Java Debug Wire Protocol)是一個為 Java除錯而設計的一個通訊互動協議,它定義了偵錯程式和被除錯程式之間傳遞的資訊的格式。
JDI(Java Debug Interface)提供 Java API 來遠端控制被除錯虛擬機器
Android除錯模型是一種JPDA框架的具體實現
有兩點主要區別:
- JVM TI適配了Android裝置特有的Dalvik虛擬機器/ART虛擬機器
- JDWP的實現支援ADB和Socket兩種通訊方式
ptrace(process trace)
ptrace()
提供了跟蹤和除錯的功能。它允許一個程序(跟蹤程序tracer)去控制另外一個程序(被跟蹤程序tracee)。
tracer可以觀察和控制tracee的執行,可以檢視和改變tracee的記憶體和暫存器。它主要用來實現斷點除錯和系統呼叫跟蹤。
tracer流程一般如下:
其中PTRACE_ATTACH/PTRACE_GETREGS/PTRACE_POKETEXT/PTRACE_SETREGS/PTRACE_DETACH定義如下:
PTRACE_ATTACH,表示附加到指定遠端程序;
PTRACE_DETACH,表示從指定遠端程序分離
PTRACE_GETREGS,表示讀取遠端程序當前暫存器環境
PTRACE_SETREGS,表示設定遠端程序的暫存器環境
PTRACE_CONT,表示使遠端程序繼續執行
PTRACE_PEEKTEXT,從遠端程序指定記憶體地址讀取一個word大小的資料
PTRACE_POKETEXT,往遠端程序指定記憶體地址寫入一個word大小的資料
ptrace是*nix系統上最常用的系統呼叫之一, 常見的gdb除錯也是通過它實現的.
檢測ptrace
當我們使用ptrace方式跟蹤一個程序時,目標程序會記錄自己被誰跟蹤,可以檢視/proc/pid/status來確認. 所以apk裡為了防止被逆向, 一般都會新開一個執行緒, 對status做檢測, 如果TracerPid不為0, 立刻退出apk.
正常情況
被ptrace時
反動態分析:
- 檢查是否有除錯
- Debug.isDebuggerConnected();
- 針對ptrace, 檢查TracerPid是否為0
- 檢測是否在模擬器
- getprop不同(虛擬機器和真機的環境變數不同,
比如虛擬機器的ro.kernel.qemu=1而真機沒有這個屬性)
- getprop不同(虛擬機器和真機的環境變數不同,
反反動態分析:
- 對抗反除錯
- java層:smali程式碼註釋掉
- native層 (nop掉so檔案或記憶體中指令, 斷點fopen/fget並修改記憶體)
Android程式是否可除錯:
開啟除錯:
1.下載mprop
, 注入init程序, 修改記憶體中屬性值
./mprop ro.debuggable 1
2.重啟adbd
stop;start
tip:
說到android:debuggable這個屬性, 想到另一個屬性android:allowBackup.
android:allowBackup預設為true, 一定要顯式設定android:allowBackup=false.
否則adb backup/adb restore備份恢復資料
微信6.0以前未設定此屬性,可以備份恢復資料
參考地址:
3.2 工具
這裡特別推薦下VirtualXposed, 其基於VirtualApp和epic, 將Xposed安裝到VirtualApp中, 可以不用root許可權就使用Xposed, 而且安裝外掛後重啟極快.
Frida是一個DBI工具, 使用其進行動態分析時, 被分析程序的TracerPid仍為0. 下圖是Frida原理, 其最初建立連線時通過ptrace向相關程序注入程式碼, 其後使用其特有的通道來通訊, 如下圖. Frida-Gadget支援Android下非root和iOS下非越獄的逆向.
IDA家喻戶曉, 其支援dex和so的動態分析, 尤其是asm->c的轉化, 可以大大方便分析.
radare是一個比IDA還要強大的工具, 其起源是調查取證, 不過目前支援數不勝數的功能. 但是其學習曲線比Vim還要陡峭
工具地址:
3.3 例項
無原始碼動態除錯smali程式碼
可以將apk用ApkTool反編譯後, 使用AndroidStudio+smalidea外掛來除錯apk.
這裡來張圖感受下無原始碼除錯的強大.
分享一個小tip, 如何讓程式暫停在啟動介面.
因為反逆向程式碼一般在Application的onCreate或更早就執行, 如果等到程式執行到MainActivity再attach程序, 時機就太晚了.
可以用如下命令讓app停在等待debug介面:
等待debug一次: adb shell am set-debug-app -w com.oncealong.sample
一次debug不一定能解決問題,多次除錯則在所難免,如果每次除錯都執行上述語句, 稍顯囉嗦, 那麼此時可以執行下述語句:
一直等待debug: adb shell am set-debug-app -w --persistent com.oncealong.sample
待debug完畢, 使用下述語句取消開啟app時的等待.
取消等待debug: adb shell am clear-debug-app
這裡的示例不在展開, 只說明這種方法和其效果, 對其感興趣可以看下述連結.
參考地址:
IDA動態除錯
IDA動態除錯可以獲得記憶體中的資訊, 比如在dvmDexFileOpenPartial函式上加斷點, 然後執行IDA指令碼直接把記憶體中的dex拷貝出來以脫殼. 詳情見Android應用安全防護和逆向分析相關章節. 這裡也不做詳細介紹,
只用下圖展示IDA的強大.
參考地址:
VirtualXposed hook java
VirtualXposed可以hook java, 相比Xposed安裝外掛需要重啟手機, VirtualXposed只用重啟下Xposed程式, 如果前者重啟手機耗時1min, 後者重啟Xposed程式只用1s不到. 對於一些簡單的hook或者逆向, 或者驗證Xposed外掛邏輯, 這裡強烈推薦VirtualXposed. 不過Xposed只支援hook java層, 如果需要hook native層, 可以使用下一個工具Frida.
參考地址:
Frida
Frida支援java/native層的hook. 而且Frida支援指令碼, 這樣可以更方便的復現結果.
比如Frida的這個Android示例. 將下面的程式碼放到一個py指令碼中, 隨時執行都可以獲得結果. 不像IDA還需要恢復現場.
參考地址:
4.重編譯
4.1 原理
反重編譯:
執行時檢查簽名(signatures比較長,hash後比較)
執行時校驗保護(校驗classes.dex的md5)
反反重編譯:
查關鍵函式, 註釋掉或nop掉
如果到這一步, 光靠本地的檢測基本無效, 可以考慮在http請求時加入對apk簽名的檢查, 如果不合法就不返回資料. 但是這樣無法阻止app被非法本地執行, 逆向者也可以通過抓包正常apk的請求來模擬正常請求. 不過這樣可以進一步提高破解門檻.
5.Docker
5.1 原理
與逆向工具高內聚,與外界系統低耦合
在Linux下, Docker效能不錯, 還可以使用VNC連線桌面.
1234567891011121314151617181920212223 | # pull imagedocker pull cryptax/android-re:latest# run locally interactivedocker run-it--rm-eDISPLAY=$DISPLAY-v/tmp/.X11-unix:/tmp/.X11-unixcryptax/android-re:latest/bin/bash# run through ssh or VNCdocker run-d-pSSH_PORT:22-pVNC_PORT:5900cryptax/android-re## sample: docker run -d --privileged -p 5900:5900 -p 5022:22cryptax/android-ressh-X-pSSH_PORT root@127.0.0.1## sample: ssh -p 5022 -X [email protected] #password: rootpassvncviewer HOST::VNC_PORT##vncviewer 127.0.0.1::5900 |
工具地址: