Android逆向分析從入門到深入

  • 2019 年 10 月 7 日
  • 筆記

學習逆向的初衷是想系統學習Android下的hook技術和工具, 想系統學習Android的hook技術和工具是因為Android移動性能實戰這本書. 這本書里用hook技術hook一些關鍵函數來計算關鍵函數的調用參數和調用時長, 從而確定性能問題發生的位置和原因. 但目前沒有比較系統的講解hook的書籍, 所以就系統的了解下逆向分析.

在讀了姜維的Android應用安全防護和逆向分析和豐生強的Android軟件安全與逆向分析後, 準備分享下這方面知識. 在寫文章時發現, 這兩本書缺少對最新的逆向工具和加固工具的描述. 在查閱相關文獻後補充了這一部分.

本文從五個維度來講解Android逆向, 每個維度盡量分'原理', '工具', '實例'三個方面.

  • 反編譯
  • 靜態分析
  • 動態分析
  • 重編譯
  • Docker

1.反編譯

1.1 原理

Android App正向編譯

說到反編譯, 先來看下正向編譯, 如上圖, 正向編譯是

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 工具

反編譯工具-dex-class

上圖的反編譯工具走的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的一個前端, 同時集成了很多其他工具, 功能強大.

反編譯工具-dex-smali

看下上圖, 這些工具走的是java <- class/jar <- dex <- apk路線. 將dex文件轉化為smali彙編, 然後直接閱讀smali彙編語言, 或者smali再轉為java(這裡沒有強大的工具, 可能經常無法成功轉化).

最常用反編譯工具

從上圖可以看到有很多反編譯工具, 我們平時最常用的是dex2jar+jd-guiApkTool.

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

http://www.secureteam.net/d4j

https://github.com/iBotPeaches/Apktool

https://github.com/demitsuri/smali2java

https://www.pnfsoftware.com/

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提交如下:

create fake names to prevent abuse from duplicate key names

實例地址:

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的方法是個空方法.

指令抽取-抽取完成
指令抽取-hook-findClass

該方法的流程如下:

指令抽取流程

四代加固VMP技術:

基於三代加固技術,把原本可執行文件中的機器指令代碼轉換成了它自己虛擬機的指令,而且還插入了大量的垃圾代碼。

這種方法將核心代碼轉化為虛擬機自己的指令, 破解apk的難度和破解虛擬機指令的難度一致. PC上存在類似的VMProtect, 號稱無人一定能破.

VMP加固原理

從難度方面來說, 二代加固一般還有破解思路, 但到了四代加固這裡, 一般的逆向脫殼技術全部失效, 你面對的是如何破解這個虛擬機.


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加殼示例

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原理圖

JPDA分為三層, 分別是JVMTI,JDWP,JDI.

JVMTI(Java Virtual Machine Tool Interface)是一套由虛擬機直接提供的 native接口,通過這些接口,開發人員不僅調試在該虛擬機上運行的 Java程序,還能查看它們運行的狀態,設置回調函數,控制某些環境變量,從而優化程序性能。

JDWP(Java Debug Wire Protocol)是一個為 Java調試而設計的一個通訊交互協議,它定義了調試器和被調試程序之間傳遞的信息的格式。

JDI(Java Debug Interface)提供 Java API 來遠程控制被調試虛擬機

JPDA-JVM

Android調試模型是一種JPDA框架的具體實現

有兩點主要區別:

  • JVM TI適配了Android設備特有的Dalvik虛擬機/ART虛擬機
  • JDWP的實現支持ADB和Socket兩種通信方式
JPDA-Android

ptrace(process trace)

ptrace原理

ptrace()

提供了跟蹤和調試的功能。它允許一個進程(跟蹤進程tracer)去控制另外一個進程(被跟蹤進程tracee)。

tracer可以觀察和控制tracee的運行,可以查看和改變tracee的內存和寄存器。它主要用來實現斷點調試和系統調用跟蹤。

tracer流程一般如下:

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調試也是通過它實現的.

gdb流程圖

檢測ptrace

當我們使用ptrace方式跟蹤一個進程時,目標進程會記錄自己被誰跟蹤,可以查看/proc/pid/status來確認. 所以apk里為了防止被逆向, 一般都會新開一個線程, 對status做檢測, 如果TracerPid不為0, 立刻退出apk.

/proc/pid/status

正常情況

被ptrace時的status狀態

被ptrace時

反動態分析:

  • 檢查是否有調試
-  Debug.isDebuggerConnected();
-  針對ptrace, 檢查TracerPid是否為0
  • 檢測是否在模擬器
-   getprop不同(虛擬機和真機的環境變量不同,      比如虛擬機的ro.kernel.qemu=1而真機沒有這個屬性)

反反動態分析:

  • 對抗反調試
-   java層:smali代碼注釋掉
-   native層 (nop掉so文件或內存中指令, 斷點fopen/fget並修改內存)

Android程序是否可調試:

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

http://burningcodes.net/%E7%90%86%E8%A7%A3ptrace%E8%B0%83%E8%AF%95%E5%8F%8A%E5%8F%8D%E8%B0%83%E8%AF%95/

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下非越獄的逆向.

Frida原理

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

http://rada.re/r/cmp.html

https://www.megabeets.net/a-journey-into-radare-2-part-1/

3.3 實例

無源碼動態調試smali代碼

可以將apk用ApkTool反編譯後, 使用AndroidStudio+smalidea插件來調試apk.

這裡來張圖感受下無源碼調試的強大.

AndroidStudio+smalidea無源碼調試

分享一個小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的強大.

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.

VirtualXposed hook 構造函數
VirtualXposed hook 方法

參考地址:

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還需要恢復現場.

Frida-hook

參考地址:

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/