Android逆向分析從入門到深入
- 2019 年 10 月 7 日
- 筆記
學習逆向的初衷是想系統學習Android下的hook技術和工具, 想系統學習Android的hook技術和工具是因為Android移動性能實戰這本書. 這本書里用hook技術hook一些關鍵函數來計算關鍵函數的調用參數和調用時長, 從而確定性能問題發生的位置和原因. 但目前沒有比較系統的講解hook的書籍, 所以就系統的了解下逆向分析.
在讀了姜維的Android應用安全防護和逆向分析和豐生強的Android軟件安全與逆向分析後, 準備分享下這方面知識. 在寫文章時發現, 這兩本書缺少對最新的逆向工具和加固工具的描述. 在查閱相關文獻後補充了這一部分.
本文從五個維度來講解Android逆向, 每個維度盡量分'原理', '工具', '實例'三個方面.
- 反編譯
- 靜態分析
- 動態分析
- 重編譯
- Docker
1.反編譯
1.1 原理
說到反編譯, 先來看下正向編譯, 如上圖, 正向編譯是
java -> class -> dex -> apk
反編譯和正向編譯稍有不同, 反編譯可以分成兩類:
java <- smali <- dex <- apk
這種方法是將dex文件轉為smali, smali是Dalvik虛擬機的彙編語言, 可以用來動態調試程序.
java <- class/jar <- dex <- apk
這種方法中是將Dalvik位元組碼轉化為等價的Java位元組碼, 然後用豐富的java分析工具分析源碼.
如何反反編譯(即對抗反編譯):
- 閱讀反編譯工具源碼查找缺陷
- 壓力測試找反編譯工具bug(下載很多apk, 寫個腳本調用ApkTool反編譯這些apk, ApkTool因為某些bug無法反編譯某個apk, 這時我們就通過壓力測試找到了ApkTool的bug, 將發現的這個應用到我們的apk中, 即可保護我們的apk免受ApkTool反編譯)
如何反反反編譯呢:
- 閱讀反編譯源碼修復缺陷
1.2 工具
上圖的反編譯工具走的java <- class/jar <- dex <- apk
路線, 即先把apk里的dex找到, 然後使用Enjarify/dex2jar/classyshark/jadx反編譯得到jar包, 然後使用jd-gui/CFR/Procyon閱讀jar包里的java源碼. 這些工具各有優缺點, 我們一般選擇dex2jar+jd-gui, 相比其他工具, jd-gui雖然很久不更新了, 但是支持跳轉, 方便查看代碼. 特別說明下Bytecode-Viewer, 其是Procyon的一個前端, 同時集成了很多其他工具, 功能強大.
看下上圖, 這些工具走的是java <- class/jar <- dex <- apk
路線. 將dex文件轉化為smali彙編, 然後直接閱讀smali彙編語言, 或者smali再轉為java(這裡沒有強大的工具, 可能經常無法成功轉化).
最常用反編譯工具
從上圖可以看到有很多反編譯工具, 我們平時最常用的是dex2jar+jd-gui
和ApkTool
.
jd-gui不僅有不錯的界面, 最關鍵的是支持類之間的跳轉, 在混淆後的代碼中跳轉可以大大方便我們查看.
ApkTool隱隱有無冕之王的聲勢, 可以反編譯代碼和資源, 修改後可以重編譯成apk, 在Android Studio下使用smalidea插件還可以完成無源碼調試, 十分強大.
工具地址:
https://github.com/Storyyeller/enjarify
https://github.com/pxb1988/dex2jar
https://github.com/google/android-classyshark
https://github.com/skylot/jadx
https://github.com/java-decompiler/jd-gui
http://www.benf.org/other/cfr/
https://bitbucket.org/mstrobel/procyon/wiki/Java%20Decompiler
https://github.com/Konloch/bytecode-viewer
https://github.com/deathmarine/Luyten
https://github.com/iBotPeaches/Apktool
https://github.com/demitsuri/smali2java
1.3 實例
這裡以一個實例說明下反反編譯和反反反編譯:
使用早期ApkTool反編譯apk時,可能會遇到反編譯失敗, 出現如下問題:
Exception in thread "main" brut.androlib.AndrolibException: Multiple res specs: attr/name at 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
names`, 其github提交如下:
實例地址:
https://github.com/iBotPeaches/Apktool/commit/567907b187ad2f78b3564d0a0405e3b207832e17
2.靜態分析
2.1 原理
什麼是靜態分析?
不運行代碼,採用反編譯工具生成程序的反編譯代碼,然後閱讀反編譯代碼來掌握程序功能.
Android靜態分析步驟:
- 反編譯apk程序
- 查看Application類(在Activity啟動之前, 一般加固/授權放在這裡)
- 查看MainActivity類
- 找關鍵代碼
反靜態分析:
- 代碼混淆(ProGuard等)
- 使用NDK+STL編寫
- 手動註冊native函數()
- 默認情況, 使用javah, com.example.k12 -> java_完整包名_類名_方法名. 但可以使用函數映射表 `static JNINativeMethod methods[] = { {"dynamicGenerateKey", "(Ljava/lang/String;)Ljava/lang/String;", (void *) native_dynamic_key}}; RegisterNatives(jclass clazz, const JNINativeMethod* methods,jint nMethods)`來註冊native函數名, 提高破解難度.
- 加固(dex/so加殼,指令抽取等)
反反靜態分析:
- 定位關鍵代碼技巧
- 信息反饋法(點擊界面, 出現`註冊失敗`, 那麼檢查代碼里哪裡使用到了`註冊失敗`)
- 特徵函數法/關鍵系統調用(一般情況下, 最終都會調用到系統函數. 為了提升難度, 可以自制和系統函數功能相同的函數, 這樣難以下斷點)
- Log代碼注入法/棧跟蹤法(動靜分析結合, 在合適位置注入log, 編譯運行時可以打印當前上下文信息和堆棧信息)
- IDA分析彙編(asm->c, 雖然很多函數還沒重定位, 但是c比彙編的表達力更強, 更便於分析)
- 脫殼
- IDA脫殼(dvm:dvmDexFileOpenPartial, art:openDexFileNative, 無論如何, 最終都是要調用系統API加載dex, 在這裡加斷點, 然後dump出內存中的dex文件[現在一些加固工具都是自己寫加載dex的函數, 這樣簡單在上述方法上加斷點是無法命中的])
- Xposed/VirtualXposed
Dex文件格式
這裡不詳細介紹,
感興趣參考"https://blog.csdn.net/jiangwei0910410003/article/details/50668549"
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文件格式和尋址方式
這裡不詳細介紹,
感興趣的同學可以參考"https://blog.csdn.net/jiangwei0910410003/article/details/49336613"
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, 號稱無人一定能破.
從難度方面來說, 二代加固一般還有破解思路, 但到了四代加固這裡, 一般的逆向脫殼技術全部失效, 你面對的是如何破解這個虛擬機.
https://blog.csdn.net/jiangwei0910410003/article/details/78070610
https://www.leiphone.com/news/201712/TABfBNU8x0lZIPoT.html
https://bbs.pediy.com/thread-224921.htm
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地址:
https://github.com/oncealong/apk_dex_shell
demo分為三個項目:
- DexReinforcingTools
- 給Apk加殼的工具, 可以用java或者cpp或者任何其他語言寫成.
- MyApk
- 需要加固的Apk
- ShellingMyApk
- 脫殼Apk, 實際安裝到用戶手機上的是該Apk, 其在Application的attachBaseContext 時會解壓得到實際的apk文件, 然後運行實際的Apk.
這裡再說下, 這種二代加殼是現在最簡單的加殼方式, 也是最基本的加殼方式.
參考文檔:
https://blog.csdn.net/jiangwei0910410003/article/details/48415225
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而真機沒有這個屬性)
反反動態分析:
- 對抗反調試
- 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以前未設置此屬性,可以備份恢複數據
參考地址:
https://tech.meituan.com/android-remote-debug.html
https://ops.tips/gists/using-c-to-inspect-linux-syscalls/
https://www.nevermoe.com/?p=854
https://github.com/wpvsyou/mprop
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還要陡峭
工具地址:
https://forum.xda-developers.com/showthread.php?t=3034811
https://github.com/android-hacker/VirtualXposed
https://github.com/frida/frida
https://www.hex-rays.com/products/ida/
https://github.com/radare/radare2
https://www.megabeets.net/a-journey-into-radare-2-part-1/
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
這裡的示例不在展開, 只說明這種方法和其效果, 對其感興趣可以看下述鏈接.
參考地址:
http://www.cnblogs.com/goodhacker/p/5592313.html
https://droidyue.com/blog/2017/05/14/a-little-but-useful-debug-skill_for_android/
IDA動態調試
IDA動態調試可以獲得內存中的信息, 比如在dvmDexFileOpenPartial函數上加斷點, 然後執行IDA腳本直接把內存中的dex拷貝出來以脫殼. 詳情見Android應用安全防護和逆向分析相關章節. 這裡也不做詳細介紹,
只用下圖展示IDA的強大.
參考地址:
https://blog.csdn.net/jltxgcy/article/details/50600241
https://blog.csdn.net/qq1084283172/article/details/46872937
VirtualXposed hook java
VirtualXposed可以hook java, 相比Xposed安裝插件需要重啟手機, VirtualXposed只用重啟下Xposed程序, 如果前者重啟手機耗時1min, 後者重啟Xposed程序只用1s不到. 對於一些簡單的hook或者逆向, 或者驗證Xposed插件邏輯, 這裡強烈推薦VirtualXposed. 不過Xposed只支持hook java層, 如果需要hook native層, 可以使用下一個工具Frida.
參考地址:
https://github.com/android-hacker/VirtualXposed
https://github.com/ac-pm/Inspeckage
http://www.cnblogs.com/lkislam/p/4859959.html
Frida
Frida支持java/native層的hook. 而且Frida支持腳本, 這樣可以更方便的復現結果.
比如Frida的這個Android示例. 將下面的代碼放到一個py腳本中, 隨時運行都可以獲得結果. 不像IDA還需要恢復現場.
參考地址:
https://github.com/frida/frida/releases
https://github.com/dweinstein/awesome-Frida
https://www.anquanke.com/post/id/85758
https://www.anquanke.com/post/id/85759
https://koz.io/using-frida-on-android-without-root/
https://software.intel.com/en-us/articles/pin-a-dynamic-binary-instrumentation-tool
http://blog.mengy.org/how-valgrind-work/
http://www.ninoishere.com/frida-learn-by-example/
https://www.frida.re/docs/presentations/osdc-2015-the-engineering-behind-the-reverse-engineering.pdf
http://dogewatch.github.io/2017/05/15/Hook-Native-Function-Use-Frida/
4.重編譯
4.1 原理
反重編譯:
運行時檢查簽名(signatures比較長,hash後比較)
運行時校驗保護(校驗classes.dex的md5)
反反重編譯:
查關鍵函數, 注釋掉或nop掉
如果到這一步, 光靠本地的檢測基本無效, 可以考慮在http請求時加入對apk簽名的檢查, 如果不合法就不返回數據. 但是這樣無法阻止app被非法本地運行, 逆向者也可以通過抓包正常apk的請求來模擬正常請求. 不過這樣可以進一步提高破解門檻.
5.Docker
5.1 原理
與逆向工具高內聚,與外界系統低耦合
在Linux下, Docker性能不錯, 還可以使用VNC連接桌面.
# pull image docker pull cryptax/android-re:latest # run locally interactive docker run -it --rm -e DISPLAY=$DISPLAY -v /tmp/.X11-unix:/tmp/.X11-unix cryptax/android-re:latest /bin/bash # run through ssh or VNC docker run -d -p SSH_PORT:22 -p VNC_PORT:5900 cryptax/android-re ## sample: docker run -d --privileged -p 5900:5900 -p 5022:22 cryptax/android-re ssh -X -p SSH_PORT [email protected] ## sample: ssh -p 5022 -X [email protected] #password: rootpass vncviewer HOST::VNC_PORT ##vncviewer 127.0.0.1::5900
工具地址:
https://github.com/cryptax/androidre/