HiHi,
被老闆出了個題目研究,就順便紀錄一下了
相關題目跟wp可以從這邊獲得
https://github.com/OWASP/owasp-mstg/tree/master/Crackmes
這邊先從 Android 開始研究,看之後時間夠不夠再看 iOS (剛剛突然發現我的 Heap 研究還擱著..)
UnCrackable App for Android Level 1
下載後打開來看:
可以看到他有 Root Detection, 按 ok 後就關閉了
我們首先要找他是哪個 function 在做這事
首先把 apk 解壓後將 class.dex
轉成 jar 檔再用 jadx
打開來看
這個流程網路上很多教學了
我們打開 MainActivity
很快就可以看到 Root Detection 的 if else
這邊有兩個方法,一個是讓 if 內的條件式變成 false, 另一個是改寫下面的
a function,讓他就算 if 是 true 也不會退出
以方便度來說當然只要改寫一個 a function 比較方便
這邊給一個還不錯的連結
https://medium.com/@buff3r/root-detection-ssl-pinning-bypass-with-frida-framework-31769d31723a
裡面有 frida hook function 的 js code
1 | Java.perform(function(){ |
我們只要改一下上面的 code 改 hook a function 就可以 bypass 了
喔對,frida 記得要在手機端執行 frida-server
這是我的code:
1 | Java.perform(function(){ |
跑起來:
可以看到我們成功改寫,讓他不會跳 Root Detection 了
再來我們要找他的通關密語
可以在 MainActivity
看到有判定的 if else
我們追回去 code 追到這:
恩….太難了 (原本打算自己構一個一樣的function解出他的明文)
這邊我想做的就是 hook 住sg.vantagepoint.uncrackable1.a.a
的 return value
因為 sg.vantagepoint.uncrackable1.a
這邊是將通關的密文與 key 設好後
拋去 sg.vantagepoint.uncrackable1.a.a
解密再跟我們的輸入做比較
所以sg.vantagepoint.uncrackable1.a.a
的 return value 就是通關密語
可以利用 overload.implementation
來達成
1 | Java.perform(function(){ |
這邊我是這樣理解的:
如果要重寫function就用 implementation
如果要保留原本的function並且額外追加code的話用overload
+implementation
並且搭配 this.call()
call自己取得結果
這邊的 [B
意思就是 byte 型別,因為要 hookpublic static byte[] a(byte[] bArr, byte[] bArr2)
的緣故
至於 [B
, 這個是 java byte array
https://reverseengineering.stackexchange.com/questions/17429/b-symbol-in-java-bytecode
因為是 byte array, 所以之後要用 String.fromCharCode
將 byte 轉回 char
結果:
一開始他並不會馬上輸出答案,我們要先輸入錯的讓他比較後才能拿到正確的答案
P.S 偷吃步解法是,我們直接把判斷密語是否正確的 function 改寫為 return true
這樣不管輸入什麼都會得到正確的結果了
1 | Java.perform(function(){ |
UnCrackable App for Android Level 2
一樣我們先跑起來看:
我們先 decompile MainActivity 看看:
可以看到我們可以用老方法 bypass Root,
再來就是找密語在哪了
我們去看判斷密語的 if else
this.m
是屬於 CodeCheck
這個型別的
我們看 CodeCheck
native
我找到的意思是在 JAVA層 include 非 JAVA 的程式碼來執行
那我們第一時間當然會想到 MainActivity
裡面的 System.loadLibrary("foo");
unzip
apk 之後可以再 lib 的資料夾發現 libfoo.so
所以意思是 libfoo
裡面有個 function 會 return 字串是否相等
我們 objdump -R
來看看
果然有用到 strncmp
接下來就是找這裡面哪個 function 有用到 strncmp
我用 r2
一開始就看到 CodeCheck
的 function, 於是馬上進去看
看到了像是一個字串的東西,一開始還沒有很肯定那就是要比較的密語
再往下看:
因為 call function 並不會打亂原本 stack 內的資訊
所以我在這邊會算他的 offset, 是不是真的拿上面的字串做 strncmp
這邊拿 objdump 來看可能比較好看一點
在 0xfbc 的位置開始算我們的 esp offset
記住每 push 一個值 esp 就會 -4
, 每 pop 一個值就會 +4
從 0xfc5 開始算:
0-4-12(連三個push)+16(0x10)-8-8(連兩個push)+16(0x10)-4 = -4
但最後的 lea eax,[esp+0x4]
會把他補回0
因此可以確定 strncmp
是以上面的明文做比較的,並且是取前23(0x17)個字串比較
這是為什麼我取 Thanks for all the fish
而不是 Thanks for all the fishD!
我們 bypass 完 root 後直接輸入試看看
喔對,在 bypass Root 我遇到這個訊息
原因是 MainActivity
有 extend c, 而 c class 裡面也有一個 a function
因為兩個 a function 裡面的 perameter 不一樣
用 overload
指定一下是哪個 a function就好了
UnCrackable App for Android Level 3
- 2022/07/17
想不到時隔多年,因為後輩的提及加上自己也想看看以解當年的遺憾,決定再來看一次 LV3,當年我連 write up 都看不懂
不知道經過工作的洗禮後會不會有長進
首先以 frida 嘗試開啟他,但失敗了(閃退),原因是它除了 anti-root 之外還有 anti-debug
將 app 正常開啟並且讓他閃退後,透過 adb logcat
可以看到以下的 error
透過 r2 快速的找到 libfoo.so
內部對應的 function 內容
首先追出到底是哪裡 call 了 goodbye
這邊我放棄,直接用 ghidra 追
大概看了一下 code,應該是用 strstr
比對 /proc/self/maps
裡面有沒有 xposed
和 frida
的字串,如果有的話就會跳出 while 迴圈觸發 goodbye
所以這邊先將 anti-debug 解決
1 | Interceptor.attach(Module.findExportByName('libc.so', 'strstr'), { |
做到後面發現直接 hook libc 好像最通用,看過另一個說法是應用程式在 load libfoo.so
之前就已經將 libc
load 進去了,所以 hook libc 的效果更好
透過上面的 js 以 frida 開啟 app 後就發現不會 crash 了,會正常的出現 Root Detection 的 alert
因為是 while 迴圈,所以如果我們在裡面 console.log
輸出內容的話會出現一堆所以後來我就沒輸出了,再來就卡在如何將 bypass root 跟 anti-debug 合在一起
無論是將 anti-debug 放在 Java.perform
內,或是將 root bypass 放在 onEnter
內都沒辦法,後來找了許久才知道要用 Java.performNow
查了一下,這邊附上官方的說明Java.perform(fn: () => void): void
1
2
3Function to run while attached to the VM.
Ensures that the current thread is attached to the VM and calls fn. (This isn’t necessary in callbacks from Java.)
Will defer calling fn if the app’s class loader is not available yet. Use Java.performNow() if access to the app’s classes is not needed.
還有找到製作團隊對於 Java.performNow()
的補充說明
https://twitter.com/oleavr/status/1324649867109670912
結合起來長這樣
1 | Interceptor.attach(Module.findExportByName('libc.so', 'strstr'), { |
其實最一開始是打算 hook showDialog
的,但發現 app 好像有針對 dex 做處理,沒辦法順利 hook,查了一下解法後,找到一個方法是可以轉嘗試 hook 上游(或是說更底層)的方法
所以這邊 hook 更底層的 System.exit
比較容易
而且上面 hook java.lang.System
的方法也適用於 LV1 和 LV2 的方法,算蠻通用的
再來是找到 secret
在一開始就不難在 MainActivity
內發現 xorkey pizzapizzapizzapizzapizz
在這邊發現 secret 判斷式
在往內追進去可以發現它是從 native binary 拉 secret 出來的
經過一個比較仔細的逆向追 code 後得知,xorkey
會藉由 libfoo.so
內的 init
function,將 key 值 strncpy
進去一個空間
然後我輸入的值會進入 libfoo.so
內的 bar
function,比對輸入的值是否正確就是在這邊實踐的
這邊應該是透過 while 迴圈將字串變成字元陣列,然後將密文與 xorkey 一個一個進行 xor 後的結果跟輸入的值進行比對 (我有稍微改一下變數名稱方便辨識)
可以看到一開始的 secret
其實是 0,那麼唯一有可能對 secret
動手腳的就是 25 行的那個 function 了 ,一起進去 function 看看
這裡面太多東西了,所以我只針對輸入進去的參數─也就是存有 secret
的記憶體位址做了那些更動就好
從 decompile 出來的 C code 實在難以看出 secret
,所以我直接看 Arm assambly code
1 | 00102ed4 00 2d c1 3d ldr q0,[x8, #0x4b0]=>DAT_001034b0 = 1Dh |
有關 param_1
的更動對應的就是上面的 asm code,以能跟 xorkey 進行 xor 解密來說的話,那 secret
也要是一個長度為 24 的字串
最後八個字元是從 14130817005A0E08
轉過來的這點應該是無庸置疑,重點是前面 16 個是什麼呢?
直接透過 ghidra 去看 DAT_001034b0
的值
ghidra 真的好方便…
1 | secret = bytes.fromhex("1d0811130f1749150d0003195a1d1315080e5a0017081314").decode("utf-8") |
經過確認後長度剛好為24
最後簡單寫一個 code 將 secret 與 xorkey 進行 xor 之後就可以得到解答了
至於說為什麼是以 big-endian 的方式去看密文跟解密呢,明明官方說預設是走 little-endian 阿!!因為我看解答的
原因可以從 while 迴圈那邊 decrypt 的順序得到,因為 while 是從 0 開始─也就是最後一個字元開始讀取並解密的,所以我們也照著一樣的順序進行解密而已