Android應用的加固和對抗不斷升級,單純的靜態加固效果已無法滿足需求,所以出現了隱藏方法加固,運行時動態恢復和反調試等方法來對抗,本文通過實例來分析有哪些對抗和反調試手段。
首先使用apktool進行反編譯,發現該應用使用的加固方式會讓apktool卡死,通過調試apktool源碼(如何調試apktool可參見前文《Android應用資源文件格式解析與保護對抗研究》),發現解析時拋出異常,如下圖:
根據異常信息可知是readSmallUint出錯,調用者是getDebugInfo,查看源碼如下:
可見其在計算該偏移處的uleb值時得到的結果小于0,從而拋出異常。 在前文《Android程序的反編譯對抗研究》中介紹了DEX的文件格式,其中提到與DebugInfo相關的字段為DexCode結構的debugInfoOff字段。猜測應該是在此處做了手腳,在010editor中打開dex文件,運行模板DEXTemplate.bt,找到debugInfoOff字段。果然,該值被設置為了0xFEEEEEEE。
接下來修復就比較簡單了,由于debugInfoOff一般情況下是無關緊要的字段,所以只要關閉異常就行了。
為了保險起見,在readSmallUint方法后面添加一個新方法readSmallUint_DebugInfo,復制readSmallUint的代碼,if語句內result賦值為0并注釋掉拋異常代碼。
然后在getDebugInfo中調用readSmallUint_DebugInfo即可。
重新編譯apktool,對apk進行反編譯,一切正常。
然而以上只是開胃菜,雖然apktool可以正常反編譯了,但查看反編譯后的smali代碼,發現所有的虛方法都是native方法,而且類的初始化方法
其基本原理是在dex文件中隱藏虛方法,運行后在第一次加載類時通過在
ProxyApplication類只有2個方法,clinit和init,clinit主要是判斷系統版本和架構,加載指定版本的so保護模塊(X86或ARM);而init方法也是native方法,調用時直接進入了so模塊。
那么它是如何恢復被隱藏的方法的呢?這就要深入SO模塊內部一探究竟了。
如何使用IDA調試android的SO模塊,網上有很多教程,這里簡單說明一下。
adb push d:\IDA\dbgsrv\android_server /data/data/sv
adb shell chmod 755 /data/data/sv
adb shell /data/data/sv
adb forward tcp:23946 tcp:23946
adb shell am start -D -n hw.helloworld/hw.helloworld.MainActivity
這時,正常狀態下會斷下來:
然后設置在模塊加載時中斷:
點擊OK,按F9運行。
然后打開DDMS并執行以下命令,模擬器就會自動斷下來:
jdb -connect com.sun.jdi.SocketAttach:hostname=127.0.0.1,port=8700
(如果出現如下無法附加到目標VM的錯誤,可嘗試端口8600)
此時,可在IDA中正常下斷點調試,這里我們斷JNI_OnLoad和init函數。
由于IDA調試器還不夠完善,單步調試的時候經常報錯,最好先做一個內存快照,然后分析關鍵點的函數調用,在關鍵點下斷而不是單步調試。
一般反調試在JNI_OnLoad中執行,也有的是在INIT_ARRAY段和INIT段中早于JNI_OnLoad執行。可通過readelf工具查看INIT_ARRAY段和INIT段的信息,定位到對應代碼進行分析。
INIT_ARRAY如下:
其中函數sub_80407A88的代碼如下,通過檢測時間差來檢測是否中間有被單步調試執行:
sub_8040903C函數里就是脫殼了,首先讀取/proc/self/maps找到自身模塊基址,然后解析ELF文件格式,從程序頭部表中找到類型為PT_LOAD,p_offset!=0的程序頭部表項,并從該程序段末尾讀取自定義的數組,該數組保存了被加密的代碼的偏移和大小,然后逐項解密。
函數check_com_android_reverse里檢測是否加載了com.android.reverse,檢測到則直接退出。
JNI_OnLoad函數中有幾個關鍵的函數調用:
call_system_property_get檢測手機上的一些硬件信息,判斷是否在調試器中。
checkProcStatus函數檢測進程的狀態,打開/proc/$PID/status,讀取第6行得到TracerPid,發現被跟蹤調試則直接退出。
通過命令行查詢進程信息,一共有3個同名進程,創建順序為33->415->430->431。其中415和431處于調試狀態:
進程415被進程405(即IDA的android_server)調試:
進程431被其父進程430調試:
要過這種反調試可在調用點直接修改跳轉指令,讓代碼在檢測到被調試后繼續正常的執行路徑,或者干脆nop掉整個函數即可。 檢測調試之后,就是調用ptrace附加自身,防止其他進程再一次附加,起到反調試作用。
修改跳轉指令BNE(0xD1)為B(0xE0),直接返回即可。
當然,更加徹底的方法是修改android源碼中bionic中的libc中的ptrace系統調用。檢測到一個進程試圖附加自身時直接返回0即可。
上面幾處反調試點在檢測到調試器后都直接調用exit()退出進程了,所以直接nop掉后按F9執行。然后就斷在了init函數入口,順利過掉反調試:
init函數在每個類加載的時候被調用,用于恢復當前類的被隱藏方法.首次調用時解密dex文件末尾的附加數據,得到事先保存的所有類的方法屬性,然后根據傳入的類名查找該類的被隱藏方法,并恢復對應屬性字段。 執行完init函數,當前類的方法已經恢復了。然后轉到dex文件的內存地址
dump出dex文件,保存為dump.dex。
對比一下原始dex文件,發現dex文件末尾的附加數據被解密出來了:
仔細分析一下附加數據的數據結構可以發現,它是一個數組,保存了所有類的所有方法的method_idx、access_flags、code_off、debug_info_off屬性,解密后的這些屬性都是uint類型的,如下圖:
其中黃色框里的就是MainActivity的各方法的屬性,知道這些就可以修復dex文件,恢復出被隱藏的方法了。下圖就是恢復后的MainActivity類:
以上就是通過實例分析展示出來的對抗和反調試手段。so模塊中的反調試手段比較初級,可以非常簡單的手工patch內存指令過掉,而隱藏方法的這種手段對art模式不兼容,不推薦使用這種方法加固應用。總的來說還是過于簡單。預計未來通過虛擬機來加固應用將是一大發展方向。