我剛為一個金融機構完成了一個移動應用的滲透測試。我寫下這些主要是為將來的手工反編譯工作做個筆記。我看了很多文章,測試了一些用于Android應用反編譯的工具,但是他們大多數是用于分析惡意軟件的。有時候我需要做滲透測試而進行反編譯和測試應用。
很多時候,是分析惡意軟件還是分析應用都無所謂,但它們是有區別的。例如,當測試一個銀行或者金融應用(跟一個團隊一起):
大家可能會問:如果是為了滲透測試,為什么不直接要應用的debug版本呢?在很多情況下可以這么做,這使得我們的工作變得很容易。而有些情況下,由于銀行和應用提供者之間的合同(或者其他法律或技術原因),他們只提供一個Play Store或者iTunes鏈接。
我不能告訴大家我測試的應用,但我可以說下所使用的加殼方法。
在手工開始工作之前,有幾個反編譯工具和網站可以在很多混淆場景提供幫助。APK Deguard是其中之一。它最大只支持16Mb的APK文件,所以如果有很多資源文件就要刪了確保不會超過限制。這個工具可以識別庫文件,所以有時候可以完美地得到重構方法和類名。不幸的是,它也有很多bug:有些變量是從類里消失的方法、有時候它生成4個字節大小的類(就是null)。
我試過幾個其他的看起不錯的工具,例如simplify(確實不錯,但我測試它時,很慢)。我還試了Dex-Oracle(沒用)。JADX也有一些簡單的編譯重命名工具,但這種情況下不夠用。
每當我發現一個工具不起作用,我通常會花一些時間看看能不能讓它工作。最后發現手工有時是最好的。
有些情況下,使用XPosed框架是很好的,我們可以記錄下任何方法,或者替換存在的方法。有一點我很不喜歡,就是每次更新模塊都需要重啟(或者軟重啟)。
還有幾個模塊,例如JustTrustMe,可以和很多應用一起使用,用來繞過SSL pinning。但它不是對所有應用起作用。例如,上次我發現對Instagram不起作用(但當然,可能有人打了補丁可以用了)。還有RootCloak,也可以在很多應用隱藏root信息,但這個模塊已經有些時間沒更新了。
難過的是我測試過的應用,這些工具都不能用,應用還是可以檢測到設備的root信息,而且也不能繞過SSL pinning。
Frida也是一個有趣的工具,很多時候有用。已經有一些基于Frida的有趣的腳本,例如:appmon。
Frida和XPosed都有一個缺點:函數內部執行跟蹤,例如我們無法在一個方法中打印一個確定的值。
這種情況很常見:檢查應用是否檢查它自己的簽名。首先,我使用一個鎖定bootloader、沒有root的真實設備(不是模擬器)。我們可以用apktool解包應用:
apktool d app.apk
cd app
apktool b
對dist/app.apk重簽名然后在設備上安裝。我遇到的情況是:應用無法運行,只顯示一個提示“App is not official”。
我們可以用:
grep -r const-string smali/
來提取所有代碼里的所有字符串。我遇到的情況是:沒能找到很多字符串。我找到的字符串,是用于加載類的。這意味著當我們重命名一個類時要小心,因為它可能作為一個字符串在某些地方被引用。
通過一些努力,我們可以調試一個小項目,但我更喜歡為兩件事做調試日志:反編譯字符串和跟蹤執行。
為了插入調試信息,我創建了一個Java文件然后轉換成smali代碼。這個方法可以打印任何Java對象。首先,在smali文件夾下增加用于調試的smali文件。
手工插入日志代碼,我們只需要:
invoke-static {v1}, LLogger;->printObject(Ljava/lang/Object;)V
用我們想要打印的寄存器替換v1。
大多數時候,反編譯函數在所有地方都有相同的參數和返回值,在這個情況下,簽名是:
.method private X(III)Ljava/lang/String;
我們可以寫一個腳本:
打印反編譯函數中的結果字符串是容易的,但有一個問題:這字符串是從哪來的(哪一行,哪個文件)?
我們可以像這樣插入更詳細的日志代碼:
const-string v1, "Line 1 file http.java"
invoke-static {v1}, LMyLogger;->logString(Ljava/lang/String;)V
但這需要有未使用的寄存器來存字符串(需要追蹤現在哪個寄存器是未使用的),或者我們可以增加本地寄存器數量然后使用最后一個寄存器(在函數已經使用了所有寄存器時不起作用)。
我用了另一個方法:我們可以用一個堆棧跟蹤(StackTrace)來跟蹤這個方法在哪被調用。要識別行號,我們只需要在smali文件中,在調用反編譯函數之前增加新的“.line”指令。為了讓編譯的類名便于記憶,在smali最前面增加“.source”。剛開始我們還不知道這個類是做什么用的,所以只需要用uuid給它一個唯一標識符。
在Java里,我們可以創建靜態初始化器(static initializer),然后當類第一次被使用時它將會被執行。我們可以在 <clinit> 開始處增加日志代碼:
class Test {
static {
System.out.println("test");
}
}
這里我用了UUID(隨機生成UUID然后將它當做字符串放在每個類里),它將幫助我處理編譯命名。
class Test {
static {
System.out.println("c5922d09-6520-4b25-a0eb-4f556594a692");
}
}
如果這個信息出現在logcat里,我們就可以知道類被調用/使用了。我可以像這樣編輯命名:
vi $(grep -r UUID smali|cut -f 1 -d ':' )
或者我們也可以設置一個文件夾,放置帶有到原始文件鏈接的UUID。
我們可以手工編寫簡單的smali代碼,但更復雜的代碼我們應該用Java來寫,然后再轉換成smali。確保它在設備上有效也是一個不錯的主意。
javac *.java
dx --dex --output=classes.dex *.class
zip Test.zip classes.dex
apktool d Test.zip
現在我們得到一個可以插入的smali(復制到smali文件夾)
這個方法也可以用來測試應用本身的部分代碼。我們可以提取smali代碼,加上main,然后運行。
adb push Test.zip /sdcard/
adb shell ANDROID_DATA=/sdcard dalvikvm -cp /sdcard/Test.zip NameOfMainClass
應用里有幾個類從字節數組中提取一個dex文件為臨時命名,然后移除該文件。這個數組時加密的,文件名時隨機的。我們想知道的第一件事是:這個文件是否重要?我們需要修復它嗎?
為了保存文件,我們可以修復反編譯字符串:如果它返回“delete”,我們就返回“canRead”。函數的簽名是兼容的,即“()Z”(一個不接受參數并且返回布爾值的函數)
事實證明替換文件(修復)有點困難。在smali代碼中看起來有點復雜,但總體來說有這些方面:
我創建一個叫“FakeOutputStream”的類,然后修改代碼讓它不是查找java.io.FileOutputStrem,而是加載FakeOutputStream。
FakeOutputStream將把源代碼寫入/sdcard/orig-x-y,x和y是偏移量和大小。相反地,它會加載/sdcard/fake-x-y的內容然后寫入到臨時文件。
注意:當我第一次運行這個應用時,它會生成/sdcard/orig-x-y,并且我可以逆向生成的DEX。我也可以修改這個dex文件并且把它當做/sdcard/fake-x-y push,然后這個文件會被加載。
所有文件內容解密后,我們就可以開始修復工作了,例如移除root檢測,包簽名檢測,調試檢測,SSL pinning檢測等等。
在主APK外動態獲取dex文件有一個優勢:我們可以輕易地通過在應用外替換dex文件來測試增加替換函數。