利用FRIDA攻擊Android應用程序(一)
前言
直到去年參加RadareCon大會時,我才開始接觸動態代碼插樁框架Frida。最初,我感覺這玩意還有點意思,後來發現這種感覺是不對的:應該是非常有意思。您還記得遊戲中的上帝模式嗎?麵對本地應用程序的時候,一旦擁有了Frida,也就擁有了這種感覺。在這篇文章中,我們重點介紹Frida在Android應用方麵的應用。在本文的第二篇中,我們將會介紹如何利用Frida來應付Android環境下的crackme問題。
什麼是動態代碼插樁?
動態二進製插樁(DBI)意味著將外部代碼注入到現有的(正在運行的)二進製文件中,從而讓它們執行以前沒有做過的事情。注意,這並非漏洞利用,因為代碼的注入無需借助於漏洞。同時,它不是調試,因為你不必將二進製代碼附加到一個調試器上麵,當然,如果你非要這麼做的好也未嚐不可。那麼DBI可以用來做什麼呢?實際上,它可以用來做許多很酷的事情:
訪問進程內存
在應用程序運行時覆蓋函數
從導入的類調用函數
在堆上查找對象實例並使用它們
Hook、跟蹤和攔截函數等。
當然,調試器也能完成所有這些事情,但是會比較麻煩。例如。在Android平台中,應用程序必須先進行反匯編和重新編譯處理,才能進行調試。一些應用程序會嚐試檢測並阻止調試器,這時你必須先克服這一點,才能進行調試。然而,這一切做起來都會非常麻煩。在DBI與Frida的幫助下,這些事情都不是我們要關心的,所以調試會變得更加便捷。
FRIDA入門
Frida“允許您在Windows、macOS、Linux、iOS、Android和QNX的本機應用程序中注入JavaScript或自己的庫代碼。”最開始的時候,它是基於穀歌的V8 Javascript運行時的,但是從版本9開始,Frida已經開始使用其內部的Duktape運行時了。不過,如果你需要V8的話,仍然可以切換回去。Frida可以通過多種操作模式與二進製程序進行交互(包括在非root的設備上給應用程序“插樁”),但是這裏我們隻介紹最簡單的情形,同時也不關心其內部運行原理。
為了完成我們的實驗,你需要
Frida
您可以從這裏下載frida服務器的二進製代碼(截止寫作本文為止,最新版本為frida-server-9.1.16-android-arm.xz)
Android模擬器或已經獲得root權限的設備。雖然Frida是在Android 4.4 ARM上麵開發的,不過應該同樣適用於更高的版本。就本文來說,使用Android 7.1 ARM完全沒有一點問題。對於第二部分的crackme來說,則需要使用比Android 4.4更高的版本。
這裏假設以linux係統作為主機操作係統,所以如果你使用Windows或Mac的話,有些命令可能需要進行相應的調整。
Frida的啟動方式花樣繁多,包括各種API和方法。您可以使用命令行界麵或類似frida-trace的工具來跟蹤底層函數(例如libc.so中的“open”函數),以便快速運行。同時,你還可以使用C、NodeJS或Python綁定完成更複雜的任務。但是在其內部,Frida使用Javascript的時候較多,換句話說,你可以通過這種語言完成大部分的插樁工作。所以,如果你像我一樣不太喜歡Javascript的話(除了XSS功能),Frida倒是一個讓你進一步了解它的理由。
首先,請安裝Frida,具體如下所示(此外,您還可以通過查看README了解其他安裝方式):
pip install frida
npm install frida
啟動模擬器或連接設備,確保adb正在運行並列出您的設備:
michael@sixtyseven:~$ adb devices
List of devices attached
emulator-5556device
然後,開始安裝frida-server。先進行解壓,並將二進製文件放入設備中:
adb push /home/michael/Downloads/frida-server-9.1.16-android-arm /data/local/tmp/frida-server
在設備上打開一個shell,切換到root用戶,並啟動frida:
adb shell
su
cd /data/local/tmp
chmod 755 frida-server
./frida-server
(注意事項1:如果frida-server沒有啟動,請檢查當前是否為root用戶,以及文件是否在傳輸過程中發生損壞。當文件傳輸而導致文件損壞的時候,經常會出現一些讓人奇怪的錯誤提示。注意事項2:如果你想以後台進程的方式啟動frida-server的話,則需要使用./frida-server&)
您可以另一個終端的常規操作係統shell中檢查Frida是否正在運行,並列出Android上的進程:
frida-ps -U
-U代表USB,允許Frida檢查USB設備,同時還可用於仿真器。這時,您將看到一個如下所示進程列表:
michael@sixtyseven:~$ frida-ps -U
PID Name
---- --------------------------------------------------
696 adbd
5828 android.ext.services
6188 android.process.acore
5210 audioserver
5211 cameraserver
8334 com.android.calendar
6685 com.android.chrome
6245 com.android.deskclock
5528 com.android.inputmethod.latin
6120 com.android.phone
6485 com.android.printspooler
8355 com.android.providers.calendar
5844 com.android.systemui
7944 com.google.android.apps.nexuslauncher
6416 com.google.android.gms
[...]
您將看到進程標識(PID)和正在運行的進程(名稱)。現在,您可以通過Frida掛鉤到任何一個進程並對其進行“篡改”了。
例如,您可以跟蹤由Chrome使用的特定調用(如果還沒有運行該瀏覽器的話,請首先在模擬器中啟動它):
frida-trace -i "open" -U com.android.chrome
輸出結果如下所示:
michael@sixtyseven:~$ frida-trace -i open -U -f com.android.chrome
Instrumenting functions...
open: Loaded handler at "/home/michael/__handlers__/libc.so/open.js"
Started tracing 1 function. Press Ctrl+C to stop.
/* TID 0x2740 */
282 ms open(pathname=0xa843ffc9, flags=0x80002)
/* TID 0x2755 */
299 ms open(pathname=0xa80d0c44, flags=0x2)
/* TID 0x2756 */
309 ms open(pathname=0xa80d0c44, flags=0x2)
/* TID 0x2740 */
341 ms open(pathname=0xa80d06f7, flags=0x2)
592 ms open(pathname=0xa77dd3bc, flags=0x0)
596 ms open(pathname=0xa80d06f7, flags=0x2)
699 ms open(pathname=0xa80d105e, flags=0x80000)
717 ms open(pathname=0x9aff0d70, flags=0x42)
742 ms open(pathname=0x9ceffda0, flags=0x0)
758 ms open(pathname=0xa63b04c0, flags=0x0)
frida-trace命令會生成一個小巧的javascript文件,然後Frida會將其注入到進程中,並跟蹤特定的調用。您可以觀察一下在__handlers __ / libc.so/open.js路徑下麵生成的open.js腳本。它將鉤住libc.so中的open函數並輸出參數。使用Frida的情況下,這非常簡單:
[...]
onEnter: function (log, args, state) {
log("open(" + "pathname=" + args[0] + ", flags=" + args[1] + ")");
},
[...]
請注意Frida是如何訪問Chrome內部調用的open函數的調用參數(args [0],args [1]等)的。現在,讓我們對這個腳本稍做修改。如果我們輸出以純文本形式打開的文件的路徑,而不是存儲這些路徑的內存地址,那不是更好嗎? 幸運的是,我們可以直接訪問內存。為此,您可以參考Frida API和Memory對象。我們可以修改腳本,讓它將內存地址中的內容作為UTF8字符串輸出,這樣結果會更加一目了然。現在修改腳本,具體為:
onEnter: function (log, args, state) {
log("open(" + "pathname=" + Memory.readUtf8String(args[0])+ ", flags=" + args[1] + ")");
},
(我們隻是添加了Memory.readUtf8String函數)我們會得到如下所示輸出:
michael@sixtyseven:~$ frida-trace -i open -U -f com.android.chrome
Instrumenting functions...
open: Loaded handler at "/home/michael/__handlers__/libc.so/open.js"
Started tracing 1 function. Press Ctrl+C to stop.
/* TID 0x29bf */
240 ms open(pathname=/dev/binder, flags=0x80002)
/* TID 0x29d3 */
259 ms open(pathname=/dev/ashmem, flags=0x2)
/* TID 0x29d4 */
269 ms open(pathname=/dev/ashmem, flags=0x2)
/* TID 0x29bf */
291 ms open(pathname=/sys/qemu_trace/process_name, flags=0x2)
453 ms open(pathname=/dev/alarm, flags=0x0)
456 ms open(pathname=/sys/qemu_trace/process_name, flags=0x2)
562 ms open(pathname=/proc/self/cmdline, flags=0x80000)
576 ms open(pathname=/data/dalvik-cache/arm/system@app@Chrome@Chrome.apk@classes.dex.flock, flags=0x42)
Frida打印出了路徑名。這很容易,對吧?
另一個要注意的是,你可以先啟動一個應用程序,然後讓Frida注入它的magic,或者傳遞-f選項給Frida,讓它創建進程。
現在,我們來考察Fridas的命令行接口frida-cli:
frida -U -f com.android.chrome
這將啟動Frida和Chrome應用。但是,仍啟動Chrome的主進程。這是為了讓您可以在應用程序啟動主進程之前注入Frida代碼。不幸的是,在我實驗時,它總是導致應用程序2秒後自動終止。這不是我們想要的結果。您可以利用這2秒鍾時間輸入%resume,並讓應用程序啟動其主進程;或者,直接使用--no-pause選項啟動Frida,這樣就不會中斷應用程序了,並將生成的進程的任務留給Frida。
無論使用哪種方法,你都會得到一個shell(不會被殺死),這樣就可以使用它的Javascript API向Frida寫命令了。通過TAB可以查看可用的命令。此外,這個shell還支持命令自動完成功能。
它提供了非常詳盡的文檔說明。對於Android,請檢查JavaScript-API的Java部分(這裏將討論一個“Java API”,雖然從技術上說應該是一個訪問Java對象的Javascript包裝器)。在下麵,我們將重點介紹這個Java API,因為在跟Android應用程序打交道的時候,這是一種更加方便的方法。不同於掛鉤libc函數,實際上我們可以直接使用Java函數和對象。
作為使用Java API的第一步,不妨從顯示Frida的命令行界麵運行的Android的版本開始:
[USB::Android Emulator 5556::['com.android.chrome']]-> Java.androidVersion
"7.1.1"
或者列出加載的類(警告:這會輸出大量內容,下麵我會對代碼進行相應的解釋):
[USB::Android Emulator 5556::['com.android.chrome']]-> Java.perform(function(){Java.enumerateLoadedClasses({"onMatch":function(className){ console.log(className) },"onComplete":function(){}})})
org.apache.http.HttpEntityEnclosingRequest
org.apache.http.ProtocolVersion
org.apache.http.HttpResponse
org.apache.http.impl.cookie.DateParseException
org.apache.http.HeaderIterator
我們在這裏輸入了一個比較長的命令,確切地說是一些嵌套的函數代碼。首先,請注意,我們輸入的代碼必須包裝在Java.perform(function(){...})中,這是Fridas的Java API的硬性要求。
下麵是我們在Java.perform包裝器中插入的函數體:
Java.enumerateLoadedClasses(
{
"onMatch": function(className){
console.log(className)
},
"onComplete":function(){}
}
)
上麵的代碼非常簡單:我們使用Fridas API的Java.enumerateLoadedClasses枚舉所有加載的類,並使用console.log將匹配的類輸出到控製台。這種回調對象在Frida中是一種非常常見的模式。你可以提供一個回調對象,形式如下所示
{
"onMatch":function(arg1, ...){ ... },
"onComplete":function(){ ... },
}
當Frida找到符合要求的匹配項時,就會使用一個或多個參數來調用onMatch;當Frida完成匹配工作時,就會調用onComplete。
現在,讓我們進一步學習Frida的magic,並通過Frida覆蓋一個函數。此外,我們還將介紹如何從外部腳本加載代碼,而不是將代碼鍵入cli,因為這種方式更方便。首先,將下麵的代碼保存到一個腳本文件中,例如chrome.js:
Java.perform(function () {
var Activity = Java.use("android.app.Activity");
Activity.onResume.implementation = function () {
console.log("[*] onResume() got called!");
this.onResume();
};
});
上麵的代碼將會覆蓋android.app.Activity類的onResume函數。它會調用Java.use來接收這個類的包裝對象,並訪問其onResume函數的implementation屬性,以提供一個新的實現。在新的函數體中,它將通過this.onResume()調用原始的onResume實現,所以應用程序依然可以繼續正常運行。
打開您的模擬器和Chrome,然後通過-l選項來注入這個腳本:
frida -U -l chrome.js com.android.chrome
一旦觸發了onResume——例如切換到另一個應用程序並返回到模擬器中的Chrome——您將收到下列輸出:
[*] onResume() got called!
很好,不是嗎?我們實際上覆蓋了應用程序中的一個函數。這就給控製目標應用程序的行為提供了可能性。但是,實際上我們可以繼續發揮:還能夠利用Javaschoose查找堆中已經實例化的對象。
需要注意的是,當你的模擬速度較慢的時候,Frida經常會超時。為了防止這種情況,請將腳本封裝到函數setImmediate中,或將它們導出為rpc。RPC在Frida默認情況下不超時(感謝@oleavr給予的提示)。在修改腳本文件後,setImmediate將自動重新運行你的腳本,所以這是相當方便的。同時,它還在後台運行您的腳本。這意味著你會立刻得到一個cli,即使Frida仍然在忙著處理你的腳本。請繼續等待,不要離開cli,直到Frida顯示腳本的輸出為止。然後,再次修改chrome.js:
setImmediate(function() {
console.log("[*] Starting script");
Java.perform(function () {
Java.choose("android.view.View", {
"onMatch":function(instance){
console.log("[*] Instance found");
},
"onComplete":function() {
console.log("[*] Finished heap search")
}
});
});
});
運行frida -U -l chrome.js com.android.chrome,這時應該會產生以下輸出:
[*] Starting script
[*] Instance found
[*] Instance found
[*] Instance found
[*] Instance found
[*] Finished heap search
我們在堆上找到了4個android.view.View對象的實例。讓我們看看能用這些搞點什麼事情。首先,我們可以調用這些實例的對象方法。這裏,我們隻是為console.log輸出添加instance.toString()。由於我們使用了setImmediate,所以現在隻需修改我們的腳本,然後Frida會自動重新加載它:
setImmediate(function() {
console.log("[*] Starting script");
Java.perform(function () {
Java.choose("android.view.View", {
"onMatch":function(instance){
console.log("[*] Instance found: " + instance.toString());
},
"onComplete":function() {
console.log("[*] Finished heap search")
}
});
});
});
返回的結果為:
[*] Starting script
[*] Instance found: android.view.View{7ccea78 G.ED..... ......ID 0,0-0,0 #7f0c01fc app:id/action_bar_black_background}
[*] Instance found: android.view.View{2809551 V.ED..... ........ 0,1731-0,1731 #7f0c01ff app:id/menu_anchor_stub}
[*] Instance found: android.view.View{be471b6 G.ED..... ......I. 0,0-0,0 #7f0c01f5 app:id/location_bar_verbose_status_separator}
[*] Instance found: android.view.View{3ae0eb7 V.ED..... ........ 0,0-1080,63 #102002f android:id/statusBarBackground}
[*] Finished heap search
Frida實際上為我們調用了android.view.View對象實例的toString方法。酷斃了!所以,在Frida的幫助下,我們可以讀取進程內存、修改函數、查找實際的對象實例,並且所有這些隻需寥寥幾行代碼就可以搞定。
現在,我們已經對Frida有了一個基本的了解,如果想要進一步深入了解它的話,可以自學其文檔和API。為了使得這篇文章更加全麵,本文還將介紹兩個主題,即Frida的綁定和r2frida。但是在此之前,需要首先指出一些注意事項。
注意事項
當使用Frida時,經常會出現一些不穩定的情形。首先,將外部代碼注入另一個進程容易導致崩潰,畢竟應用程序是以其非預期的方式被觸發,來執行某些額外的功能的。第二,Frida本身貌似仍然處於實驗階段。它的確非常有用,但是許多時候我們必須嚐試各種方式才能獲得所需的結果。例如,當我嚐試從命令行加載腳本然後生成一個命令的進程時,Frida總是崩潰。所以,我不得不先生成進程,然後讓Frida注入腳本。這就是為什麼我展示Frida的使用和防止超時的各種方法的原因。當然,許多時候您要根據自己的具體情況來找出最有效的方法。
Python綁定
若想利用Frida進一步提升自己工作的自動化程度的話,你應該學習應用性更高的Python、C或NodeJS綁定,當然,前提是你已經熟悉了Frida的工作原理。例如,要從Python注入chrome.js腳本的話,可以使用Frida的Python綁定。首先,創建一個chrome.py腳本:
script.load()
更多的例子,請參考Frida的文檔。
Frida和Radare2:r2frida
如果我們還可以使用類似Radare2之類的反匯編框架來檢查應用程序的內存的話,那不是更好嗎?別急,我們有r2frida。您可以使用r2frida將Radare2連接到Frida,然後對進程的內存進行靜態分析和反匯編處理。不過,我們這裏不會對r2frida進行詳細的介紹,因為我們假設您已經了解了Radare2的相關知識(如果您對它還比較陌生的話,建議您抽時間學習一下,我認為這是非常值得的)。無論如何,您都沒有必要過於擔心,因為這個軟件的用法非常容易上手,看看下麵的例子您就知道此言不虛。
您可以使用Radare2的數據包管理程序來安裝r2frida(假設您已經安裝了Radare2):
r2pm install r2frida
回到我們的frida-trace示例,刪除或重命名我們修改的腳本,讓frida-trace再次生成默認的腳本,並重新查看日誌:
使用r2frida的話,您可以輕鬆地檢查所顯示的內存地址的內容並讀取路徑名(在本例中為/ dev / binder):
root@sixtyseven:~# r2 frida://emulator-5556/com.android.chrome
-- Enhance your graphs by increasing the size of the block and graph.depth eval variable.
[0x00000000]> s 0xa843ffc9
[0xa843ffc9]> px
- offset - 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
0xa843ffc9 2f64 6576 2f62 696e 6465 7200 4269 6e64 /dev/binder.Bind
0xa843ffd9 6572 2069 6f63 746c 2074 6f20 6f62 7461 er ioctl to obta
0xa843ffe9 696e 2076 6572 7369 6f6e 2066 6169 6c65 in version faile
0xa843fff9 643a 2025 7300 4269 6e64 6572 2064 7269 d: %s.Binder dri
[...]
訪問進程以及讓r2frida執行注入操作的語法如下所示:
r2 frida://DEVICE-ID/PROCESS
下麵展示以=!為前綴的情況下,有哪些可用的r2frida命令,其中,您可以快速搜索內存區域中特定的內容或對任意內存地址執行寫入操作:
[0x00000000]> =!?
r2frida commands available via =!
? Show this help
?V Show target Frida version
/[x][j] <string|hexpairs> Search hex/string pattern in memory ranges (see search.in=?)
/w[j] string Search wide string
[...]
小結
在這篇文章中,我們重點介紹Frida在Android應用方麵的應用。在本教程的第二篇中,我們將介紹如何通過Frida輕鬆搞定crackme。
最後更新:2017-04-07 21:25:10