Android APP破解利器Frida之反調試對抗
在我發表了關於Frida的第二個博文後不久,@muellerberndt決定發布另外一個新的OWASP Android 破解APP,我很想知道我是否可以再次使用Frida解決這個CrackMe。如果你想跟隨我一起操作,你需要做以下準備:
· OWASP Level2的CrackMe APK
· Android SDK和模擬器(我使用的是Android 7.1 x64映像)
· Frida安裝(以及frida-server二進位文件)
· ByteCode viewer
· radare2(或你喜歡使用的其他一些反彙編程序)
· apktool
如果你需要知道如何安裝Frida,請查看Frida的相關 文檔。對於Frida的使用,你還可以查看本教程系列的第I部分。我想你在繼續本文的操作之前已經擁有所有需要的東西,並且基本上熟悉了Frida的使用。另外,確保Frida可以連接到你的設備或模擬器(例如使用frida -ps -U 命令)。
警告:本教程不僅僅是一個快速破解Crackme的練習。相反,我將向你展示各種方法來克服所遇到的具體問題。如果你只是尋找一個快速的破解方案,請在本教程末尾查看相關的Frida腳本。
注意:如果你遇到了下面這個錯誤:
Error: access violation accessing 0xebad8082
或者使用Frida時會出現了類似的錯誤,再模擬器中擦除用戶數據,重新啟動並重新安裝該apk可能有助於解決此類錯誤問題。
做好多次嘗試的準備。該應用程序可能會崩潰,模擬器也可能會重新啟動,一切也可能會搞砸,但是,它依舊可以正常工作。
先把APP跑起來
在開始時,我們做的事情和之前破解UnCrackable1是一樣的,先運行應用程序:接下來,當你在模擬器中運行它時,它會檢測到它是在有root許可權的設備上運行的。
我們可能會嘗試像之前破解UnCrackable 1一樣Hook OnClickListener。但首先我們來看看我們是否可以連接到Frida來進行篡改:
michael@sixtyseven:~/Development$ frida -U sg.vantagepoint.uncrackable2 ____ / _ | Frida 9.1.27 - A world-class dynamic instrumentation framework | (_| | > _ | Commands: /_/ |_| help -> Displays the help system . . . . object? -> Display information about object . . . . exit/quit -> Exit . . . . . . . . More info at http://www.frida.re/docs/home/Failed to attach: ambiguous name; it matches: sg.vantagepoint.uncrackable2 (pid: 5184), sg.vantagepoint.uncrackable2 (pid: 5201)
這是什麼東西?有兩個名稱相同的進程?我們可以使用frida -ps -U命令來驗證一下:
奇怪。我們試著將Frida注入到父進程:
michael@sixtyseven:~/Development$ frida -U 5184 ____ / _ | Frida 9.1.27 - A world-class dynamic instrumentation framework | (_| | > _ | Commands: /_/ |_| help -> Displays the help system . . . . object? -> Display information about object . . . . exit/quit -> Exit . . . . . . . . More info at http://www.frida.re/docs/home/Failed to attach: unable to access process with pid 5184 due to system restrictions; try `sudo sysctl kernel.yama.ptrace_scope=0`, or run Frida as root
看起來它並不起作用,因為當我們使用root許可權運行Frida時,我們得到了相同的結果,所以提出的解決方案並沒有什麼幫助。這裡到底發生了什麼?我們來看看應用程序吧。解壓縮apk並使用ByteCodeViewer 查看classes.dex並進行反編譯(例如CFR-Decompiler):
我們注意到一個調用static的靜態調用System.load載入了foo庫(參見[1])。該應用程序還在其onCreate方法的第一行調用了this.init()(參見[3]),該方法被聲明為一種native方法(參見[2]),因此它可能是foo的一部分。
現在讓我們來看看foo庫。在radare2中,打開這個庫文件(你可以在lib文件夾中找到各種架構的多個庫文件,我在這裡使用的是lib/x86_64),分析並列出其導出函數:
我們注意到,該庫出口2個有趣的導出函數:
Java_sg_vantagepoint_uncrackable2_MainActivity_init和Java_sg_vantagepoint_uncrackable2_CodeCheck_bar
(對這些方法的具體命名可以查看Java介面NATIV JNI)。
我們來看看Java_sg_vantagepoint_uncrackable2_MainActivity_init:
這是一個相當短的函數:
它調用了另一個函數sub.fork_820,在這個函數中還有更多的函數調用:
我們看到有調用fork,pthread_create,getppid,ptrace和waitpid。不需要花費太多的時間來進行反彙編,我們就可以猜測出,主進程使用ptrace作為調試器來fork了一個附加到它的子進程。這是一個基本的常見的反調試技術,你可以在這裡閱讀到更多信息。
由於Frida 使用 ptrace用於初始的注入操作,所以這就解釋了為什麼我們沒有成果連接到父進程:附加調試進程被阻止,因為已經有其他的進程作為調試器進行了連接。
繞過反調試解決方案1:Frida
Frida來解決問題的目的不是將Frida注入正在運行的進程,而是我們可以讓它為我們生成進程。使用-f選項,我們告訴Frida注入Zygote並開始啟動應用程序。關閉設備上的應用程序,看看當我們啟動Frida時會發生什麼:
我們得到如下輸出:
michael@sixtyseven:~/Development/UnCrackable2/lib/x86_64$ frida -U -f sg.vantagepoint.uncrackable2 --no-pause ____ / _ | Frida 9.1.27 - A world-class dynamic instrumentation framework | (_| | > _ | Commands: /_/ |_| help -> Displays the help system . . . . object? -> Display information about object . . . . exit/quit -> Exit . . . . . . . . More info at http://www.frida.re/docs/home/Spawned `sg.vantagepoint.uncrackable2`. Resuming main thread! [USB::Android Emulator 5554::[ sg.vantagepoint.uncrackable2 ]]->
好吧!Frida注入到了Zygote,產生了我們的進程並等待輸入。(我承認,告訴你在啟動Frida時添加-f選項是一個很長的介紹,不過你已被警告過...)
我們現在準備繼續破解, 但是在我們繼續之前,我們將要檢查另一個解決方案來克服這個破解的調試保護。
繞過反調試解決方案2:補丁
除了讓Frida生成進程之外,我們還可以通過對應用打補丁來解決我們遇到的問題。這意味著我們需要反彙編應用程序,對修改的apk進行重建和簽名。但是,在這種情況下,這將會給我們麻煩帶來麻煩。不過我還是會告訴你如何解決這個麻煩,我們稍後會注意到這個問題。
我們可以通過apktool來實現補丁:
michael@sixtyseven:~/Disassembly$ /opt/apktool/apktool.sh -r d UnCrackable-Level2.apkI: Using Apktool 2.2.0 on UnCrackable-Level2.apkI: Copying raw resources...I: Baksmaling classes.dex...I: Copying assets and libs...I: Copying unknown files...I: Copying original files...
(我跳過了資源的提取, 因為-r參數在重新編譯apk時引起了新的問題,我們在這裡不需要APP的資源。)
看一下smali/sg/vantagepoint/uncrackable2/MainActivity.smali 文件里的smali代碼。你可以在82行處找到調用init的代碼,並且可以將其注釋掉:
重新編譯apk(忽略致命錯誤...):
michael@sixtyseven:~/Disassembly/UnCrackable-Level2$ /opt/apktool/apktool.sh bI: Using Apktool 2.2.0I: Checking whether sources has changed...I: Smaling smali folder into classes.dex...[Fatal Error] AndroidManifest.xml:1:1: Content ist nicht zul?ssig in Prolog.I: Checking whether resources has changed...I: Copying raw resources...I: Copying libs... (/lib)I: Building apk file...I: Copying unknown files/dir...
對齊:
michael@sixtyseven:~/Disassembly/UnCrackable-Level2$ zipalign -v 4 dist/UnCrackable-Level2.apk UnCrackable2.recompiled.aligned.apkVerifying alignment of UnCrackable2.recompiled.aligned.apk (4)... 49 AndroidManifest.xml (OK - compressed) 914 classes.dex (OK - compressed) 269899 lib/arm64-v8a/libfoo.so (OK - compressed) 273297 lib/armeabi-v7a/libfoo.so (OK - compressed) 279346 lib/armeabi/libfoo.so (OK - compressed)
簽名(注意:你需要有一個密鑰和密鑰庫):
michael@sixtyseven:~/Disassembly/UnCrackable-Level2$ jarsigner -verbose -keystore ~/.android/debug.keystore UnCrackable2.recompiled.aligned.apk signkeyEnter Passphrase for keystore: adding: META-INF/MANIFEST.MF adding: META-INF/SIGNKEY.SF adding: META-INF/SIGNKEY.RSA signing: AndroidManifest.xml signing: classes.dex signing: lib/arm64-v8a/libfoo.so signing: lib/armeabi-v7a/libfoo.so signing: lib/armeabi/libfoo.so signing: lib/mips/libfoo.so[...]
你可以在「 OWASP移動安全測試指南」中找到更廣泛的描述。卸載掉原始的apk並安裝打過補丁的apk:
再次啟動該應用程序。運行frida-ps後我們發現現在只有一個進程:
Frida連接也沒有問題:
michael@sixtyseven:~/Disassembly/UnCrackable-Level2$ frida -U sg.vantagepoint.uncrackable2 ____ / _ | Frida 9.1.27 - A world-class dynamic instrumentation framework | (_| | > _ | Commands: /_/ |_| help -> Displays the help system . . . . object? -> Display information about object . . . . exit/quit -> Exit . . . . . . . . More info at http://www.frida.re/docs/home/[USB::Android Emulator 5554::sg.vantagepoint.uncrackable2]->
比起給Frida增加 -r選項更為繁瑣一點,但也更普遍的適用。
如前所述,當我們使用補丁版本後(我會告訴你如何克服這個,所以不要把它丟棄),我們就不能輕易地提取這個秘密字元串了。但是現在我們繼續使用原來的apk。確保你按照正確的方式安裝了原始的apk。
繼續解惑
在我們找到了一些擺脫反調試的可能性之後,我們來看看我們接下來該如何進行。一旦我們按下OK按鈕,應用程序就會在模擬器的運行時中進行root許可權的檢測並退出。我們已經知道UnCrackable1的一些行為。另外,我們可以對這種檢測行為打補丁,刪除System.exit的調用代碼,但是我們試圖用Frida來解決這個問題。再看看反編譯的代碼,我們看到沒有OnClickListener類,只是一個匿名的內部類。由於onClickListener的實現中調用了System.exit所以我們簡單地Hook住該函數並無卵用。
這是Frida腳本:
再次關閉任何正在運行的UnCrackable2實例,並再次在Frida的幫助下啟動它:
等到App啟動後,Frida 在控制台中顯示了Hooking calls...的消息。然後按「確定」。你應該會得到這樣的輸出信息:
michael@sixtyseven:~/Development/frida$ frida -U -f sg.vantagepoint.uncrackable2 --no-pause -l uncrackable2.js ____ / _ | Frida 9.1.27 - A world-class dynamic instrumentation framework | (_| | > _ | Commands: /_/ |_| help -> Displays the help system . . . . object? -> Display information about object . . . . exit/quit -> Exit . . . . . . . . More info at http://www.frida.re/docs/home/Spawned `sg.vantagepoint.uncrackable2`. Resuming main thread! [USB::Android Emulator 5554::[ sg.vantagepoint.uncrackable2 ]]-> [*] Hooking calls to System.exit[*] System.exit called
該應用程序不再退出。我們可以輸入一個秘密字元串試試:
但是我們應該在這裡輸入什麼呢?看看MainActivity的Android代碼,檢查正確的輸入邏輯如下:
this.m = new CodeCheck();[...]//in method: public void verifyif (this.m.a(string)) { alertDialog.setTitle((CharSequence)"Success!"); alertDialog.setMessage((CharSequence)"This is the correct secret.");}
這是CodeCheck類代碼:
package sg.vantagepoint.uncrackable2;public class CodeCheck { private native boolean bar(byte[] var1); public boolean a(String string) { return this.bar(string.getBytes()); //Call to a native function }}
我們注意到,我們的文本欄位的輸入也就是我們的「秘密字元串」 被傳遞給一個native函數bar。我們可以在libfoo.so庫中找到這個函數。搜索函數地址(如我們之前使用的init函數),並用radare2進行反彙編:
看看反彙編代碼,我們注意到有一些字元串比較的邏輯,另外我們還注意到一個有趣的明文字元串Thanks for all t.測試這個字元串作為我們的crackme的解決方案後,我們發現它並不起作用。我們必須繼續向前。
看看0x000010d8這個地址處的反彙編代碼:
0x000010d8 83f817 cmp eax, 0x17 0x000010db 7519 jne 0x10f6 ;[1]
這裡有一個eax寄存器與0x17的比較,其十進位格式為23。如果這個比較不成功,strncmp在這裡就不會調用。我們也注意到了0x17已作為參數傳遞給了在
記住,根據linux 64位的調用約定,函數參數在寄存器中傳遞至少1到6個參數。尤其是前3個參數要按照這個順序在RDI,RSI和RDX中傳遞(參見[PDF],第20頁)。Strncmp函數聲明如下:
int strncmp ( const char * str1, const char * str2, size_t num );
所以我們的strncmp函數將比較0x17 = 23個字元。我們可以推斷,我們的秘密字元串的長度應該是23個字元。
讓我們最後嘗試Hook住strncmp函數,並簡單地列印出它的參數。我們可以期望這裡給我們直接輸出解密的輸入字元串。我們必須
1. 查找strncmp函數在libfoo.so中的內存地址
2. 使用 Interceptor.attach Hook libfoo.so的strncmp函數和轉儲參數
如果你這樣做了,你會發現有很多地方調用了strncmp,我們將進一步限制輸出。這是Frida代碼片段:
var strncmp = undefined;imports = Module.enumerateImportsSync("libfoo.so");for(i = 0; i < imports.length; i++) }Interceptor.attach(strncmp, { onEnter: function (args) { if(args[2].toInt32() == 23 && Memory.readUtf8String(args[0],23) == "01234567890123456789012") { console.log("[*] Secret string at " + args[1] + ": " + Memory.readUtf8String(args[1],23)); } }});
對這個腳本的一些說明:
1. 該腳本調用Module.enumerateImportsSync從libfoo.so(查閱文檔)中獲取有關導入信息的對象數組。我們遍歷這個數組,直到找到strncmp並檢索其地址。然後我們將攔截器附加到它上面。
2. Java中的字元串不會已空字元終止。當我們使用Frida的Memory.readUtf8String方法訪問strncmp字元串指針的內存位置,並且不提供長度時,Frida會期望一個終止符,否則會輸出一些內存垃圾。因為它不知道字元串在哪裡結束。如果我們指定要讀取的字元數量作為第二個參數,我們就可以解決掉這個警告。
3. 如果我們沒有限制我們轉儲strncmp參數的條件,我們會得到很多輸出。所以我們只輸出strncmp中當第三個參數的參數size_t為23且第一參數的字元串指針指向我們在輸入框輸入的01234567890123456789012這個字元串作為過濾條件
我是如何知道args[0]也就是我們的輸入和args[1]也就是秘密字元串的?我其實並不知道,我只是在測試它,並將大量的輸出轉儲到屏幕上,以找到我的輸入。如果你不想跳過,你可以在上述腳本中移除if語句,並使用Frida的hexdump輸出:
buf = Memory.readByteArray(args[0],32);console.log(hexdump(buf, { offset: 0, length: 32, header: true, ansi: true}));buf = Memory.readByteArray(args[1],32);console.log(hexdump(buf, { offset: 0, length: 32, header: true, ansi: true}));
每次調用strncmp時都會輸出很多hexdumps,所以要提醒你一下。
以下是完整版本的腳本,可以更好地輸出參數:
setImmediate(function() { Java.perform(function() { console.log("[*] Hooking calls to System.exit"); exitClass = Java.use("java.lang.System"); exitClass.exit.implementation = function() { console.log("[*] System.exit called"); } var strncmp = undefined; imports = Module.enumerateImportsSync("libfoo.so"); for(i = 0; i < imports.length; i++) { if(imports[i].name == "strncmp") { strncmp = imports[i].address; break; } } Interceptor.attach(strncmp, { onEnter: function (args) { if(args[2].toInt32() == 23 && Memory.readUtf8String(args[0],23) == "01234567890123456789012") { console.log("[*] Secret string at " + args[1] + ": " + Memory.readUtf8String(args[1],23)); } }, }); console.log("[*] Intercepting strncmp"); });});
現在,啟動Frida並載入腳本:
輸入字元串並按驗證:
在控制台,你會得到:
michael@sixtyseven:~/Development/frida$ frida -U -f sg.vantagepoint.uncrackable2 --no-pause -l uncrackable2.js ____ / _ | Frida 9.1.27 - A world-class dynamic instrumentation framework | (_| | > _ | Commands: /_/ |_| help -> Displays the help system . . . . object? -> Display information about object . . . . exit/quit -> Exit . . . . . . . . More info at http://www.frida.re/docs/home/Spawned `sg.vantagepoint.uncrackable2`. Resuming main thread! [USB::Android Emulator 5554::[ sg.vantagepoint.uncrackable2 ]]-> [*] Hooking calls to System.exit[*] Intercepting strncmp[*] System.exit called[*] Secret string at 0x7fffa628f010: Thanks for all the fish
沒錯,輸出內容很好很簡單:完整的秘密字元串。將其輸入到輸入框中並享受成功吧
修復補丁的解決方案
最後,是有一些關於補丁修復的說明,以及為什麼我們在使用打過補丁的apk時不會得到秘密字元串。Libfoo中的init函數包含一些初始化邏輯,阻止應用程序根據我們的輸入檢查或解碼秘密字元串。
如果我們再次看看init函數的反彙編,我們會看到有趣的一行代碼:
在bar函數的後面會檢查相同的變數libfoo,如果未設置,則代碼會跳過strncmp:
所以在後面它可能是一個布爾變數,如果init函數運行,它將被設置。如果我們想要在我們的apk的修補版本中調用strncmp那麼我們必須設置此變數,或者至少阻止它跳過實際的strncmp調用。
我們現在可以再次進行修補,反編譯apk,覆蓋jmp指令,並重新編譯一次。的確有些餓笨重。 由於這是Frida教程,我們將使用Frida來動態更改內存。
因此,我們需要:
1. 獲取載入foo庫的基址
2. 找到相對於庫基地址的變數(通過反彙編中,我們知道從基地址的偏移量是0x400C位元組)
3. 將變數設置為1
所以,在Frida中的相關代碼如下:
//Get base address of libraryvar libfoo = Module.findBaseAddress("libfoo.so");//Calculate address of variablevar initialized = libfoo.add(ptr("0x400C"));//Write 1 to the variableMemory.writeInt(initialized,1);
以下是該修補程序版本的完整腳本:
setImmediate(function() { Java.perform(function() { console.log("[*] Hooking calls to System.exit"); exitClass = Java.use("java.lang.System"); exitClass.exit.implementation = function() { console.log("[*] System.exit called"); } var strncmp = undefined; imports = Module.enumerateImportsSync("libfoo.so"); for(i = 0; i < imports.length; i++) { if(imports[i].name == "strncmp") { strncmp = imports[i].address; break; } } //Get base address of library var libfoo = Module.findBaseAddress("libfoo.so"); //Calculate address of variable var initialized = libfoo.add(ptr("0x400C")); //Write 1 to the variable Memory.writeInt(initialized,1); Interceptor.attach(strncmp, { onEnter: function (args) { if(args[2].toInt32() == 23 && Memory.readUtf8String(args[0],23) == "01234567890123456789012") { console.log("[*] Secret string at " + args[1] + ": " + Memory.readUtf8String(args[1],23)); } }, }); console.log("[*] Intercepting strncmp"); });});
現在運行應用程序,通過frida載入腳本,再次輸入01234567890123456789012。按驗證按鈕。應用程序會調用strncmp並且會記錄秘密字元串:
root@sixtyseven:/home/michael/Development/frida# frida -U sg.vantagepoint.uncrackable2 -l uncrackable2-final.js ____ / _ | Frida 9.1.27 - A world-class dynamic instrumentation framework | (_| | > _ | Commands: /_/ |_| help -> Displays the help system . . . . object? -> Display information about object . . . . exit/quit -> Exit . . . . . . . . More info at http://www.frida.re/docs/home/[USB::Android Emulator 5554::sg.vantagepoint.uncrackable2]-> [*] Hooking calls to System.exit[*] Intercepting strncmp[*] System.exit called[*] Secret string at 0x7fffd52c6570: Thanks for all the fish
希望你在使用Frida後能體驗到更多的樂趣。


※從404到默認頁面,通過.cshtml拿到webshel??l
※http81殭屍網路預警:專門攻擊攝像頭,國內5萬台設備已淪陷
※DNSPod客服審核不嚴,導致國內某技術論壇域名被劫持一小時
TAG:嘶吼RoarTalk |
※Facebook 開源其調試平台 Sonar,支持 Android 與 iOS
※使用Visual Studio Code編譯、調試Apollo項目
※如何使用Ghostscript調試PostScript
※A 站受黑客攻擊,近千萬條用戶數據外泄;Facebook 開源其調試平台 Sonar,支持 Android與iOS
※使用 Visual Studio Code 搭建 C/C+開發和調試環境
※Servlet 調試
※Android遠程調試Web頁面
※Undo 發布用於 Linux 調試的 Live Recorder 5.0
※如何使用curl調試openstack的api
※如何在 Linux 或者 UNIX 下調試 Bash Shell 腳本
※Eclipse Debug 調試
※PyTorch代碼調試利器:自動print每行代碼的Tensor信息
※linux性能調試之iostat
※喲,寫Bug呢?Facebook發布AI代碼調試工具SapFix
※Chrome 調試技巧
※挖掘IntelliJ IDEA的調試功能
※使用pdb進行Python調試
※strace幫助你調試PHP代碼
※教你使用Vue.js的DevTools來調試你的vue項目
※使用systemtap調試工具分析MySQL的性能