使用鉤子技術(shù)改進(jìn)Android程序安全性(上篇)
譯文一、 簡介
在Android開發(fā)世界中,開發(fā)人員通常利用第三方庫(例如游戲引擎,數(shù)據(jù)庫引擎或移動支付引擎)來開發(fā)他們的應(yīng)用程序。通常情況下,這些第三方庫是閉源代碼庫,因此開發(fā)商不能更改它們。有時(shí),第三方庫會給應(yīng)用程序帶來一定的安全問題。例如,用于調(diào)試目的內(nèi)部日志打印可能會在用戶登錄和付款時(shí)泄漏憑據(jù)信息,或者是游戲引擎中一些存儲在本地的明文形式的資源和腳本有可能輕易為攻擊者所獲得。
在本文中,我想和諸位分享一些近階段的研究成果;具體地說是,使用鉤子技術(shù)來提供一種簡單有效的保護(hù)方案以應(yīng)對某些針對Android應(yīng)用的離線攻擊。
二、 Android應(yīng)用中普遍存在的安全問題
(一) Android應(yīng)用打包概述
Android應(yīng)用程序通常是用Java編程語言編寫的。當(dāng)開發(fā)人員有高性能需求或低級API訪問時(shí),他們可以使用C/C++代碼并編譯為本機(jī)庫,然后通過Java本機(jī)接口(JNI)調(diào)用它們。之后,Android SDK工具就會把所有已編譯的代碼、數(shù)據(jù)和資源文件打包到Android包(APK)中。
Android應(yīng)用程序是以APK格式打包和發(fā)行的,其實(shí)這是一個(gè)標(biāo)準(zhǔn)的ZIP文件格式,可以使用任何ZIP工具解壓縮。一旦解壓縮,APK文件可能包含以下文件夾和文件(參考圖1)︰
1.META-INF目錄
- MANNIFEST.MF:清單文件
- CERT.RSA:應(yīng)用程序證書
- CERT.SF:相應(yīng)于MANNIFEST.MF文件的資源和SHA-1 Digest清單
2.classes.dex:編譯成DEX文件格式的Java類,為Dalvik虛擬機(jī)所理解和執(zhí)行
3.lib:該目錄包含特定于處理器的軟件層的已編譯代碼,其下一般還包括如下子目錄:
- armeabi:包含所有基于ARM*處理器的編譯代碼
- armeabi-v7a:包含所有基于ARMv7及以上版本處理器的編譯代碼
- x86:包含基于Intel® x86處理器的編譯代碼
- mips:包含基于MIPS處理器的編譯代碼
4.assets:該目錄下包含應(yīng)用程序資源,可以通過AssetManager來檢索這個(gè)目錄
5.AndroidManifest.xml: Android配置文件,描述了程序的名稱、版本、訪問權(quán)限、應(yīng)用程序引用的庫文件等
6.res:所有應(yīng)用程序資源都放置在此目錄下
7.resources.arsc:該文件中包含預(yù)編譯資源
圖1:一個(gè)典型的Android APK包中的內(nèi)容
一旦程序包被安裝在用戶設(shè)備上,它的文件將被提取并放置在以下目錄中:
1.整個(gè)應(yīng)用程序的包文件復(fù)制到路徑/data/app
2.Classes.dex文件被提取和優(yōu)化,并將優(yōu)化后的文件復(fù)制到路徑/data/dalvik-cache
3.本機(jī)庫被提取并復(fù)制到路徑/data/app-lib/<package-name>
4. 創(chuàng)建一個(gè)名為 /data/data/<package-name>的文件夾并分配給應(yīng)用程序用以存儲其私有數(shù)據(jù)。
(二) Android開發(fā)中的風(fēng)險(xiǎn)意識
通過在上一節(jié)中分析的文件夾和文件結(jié)構(gòu),作為開發(fā)人員必須應(yīng)該知道應(yīng)用程序中存在的幾個(gè)弱點(diǎn)。攻擊者可以利用這些弱點(diǎn)獲得大量的有價(jià)值的信息。
例如,第一個(gè)脆弱點(diǎn)是,應(yīng)用程序往往都把游戲引擎所使用的原始數(shù)據(jù)資源存儲在assets文件夾中。這包括音頻和視頻材料、游戲邏輯腳本文件以及精靈和場景的紋理資源。因?yàn)锳ndroid應(yīng)用程序的包并不加密,所以攻擊者可以從應(yīng)用程序商店或另一個(gè)Android設(shè)備中通過獲得對應(yīng)的包以后進(jìn)而很容易地得到這些資源。
另一個(gè)易受攻擊點(diǎn)是,針對根設(shè)備和外部存儲的脆弱的文件訪問控制。攻擊者可以通過受害者設(shè)備的根特權(quán)來獲取應(yīng)用程序的私有數(shù)據(jù)文件,或者把應(yīng)用程序數(shù)據(jù)寫入例如SD卡這類外部存儲上。如果不很好保護(hù)私有數(shù)據(jù),攻擊者可以從該文件中獲得如用戶帳戶和密碼等信息。
最后,調(diào)試信息可能是可見的。如果開發(fā)人員忘記在發(fā)布應(yīng)用程序之前注釋掉有關(guān)調(diào)試代碼,攻擊者可以通過使用Logcat工具來檢索調(diào)試輸出信息。
三、 鉤子技術(shù)概述
(一) 何謂鉤子
鉤子是一系列用于更改代碼技術(shù)的術(shù)語,用于修改原始代碼運(yùn)行序列的行為,其方式是通過在運(yùn)行時(shí)代碼段中插入一定的指令來實(shí)現(xiàn)。圖2展示了鉤子技術(shù)的基本實(shí)現(xiàn)流程。
圖2:鉤子可以更改程序的運(yùn)行順序
在這篇文章中,將研究兩種類型的鉤子技術(shù):
1.符號表重定向
通過分析動態(tài)鏈接庫的符號表,我們可以找到所有的外部調(diào)用函數(shù)Func1() 的重定位地址。然后,我們把每個(gè)重定位地址修改到掛鉤函數(shù)Hook_Func1()的起始地址(請參見圖3)。
圖3:符號表重定向流程示意圖
2.內(nèi)聯(lián)重定向
與符號表重定向必須修改每一個(gè)重定向地址不同的是,內(nèi)聯(lián)鉤子只覆蓋我們想要鉤住的目標(biāo)函數(shù)的起始字節(jié)(見圖4)。內(nèi)聯(lián)重定向比符號表重定向更健壯,因?yàn)樗谌魏螘r(shí)候只修改一次。缺點(diǎn)是,如果在應(yīng)用程序中的任何地方調(diào)用原始函數(shù),那么它還會執(zhí)行被鉤住的函數(shù)中的代碼。所以,我們在重定向函數(shù)中必須仔細(xì)地標(biāo)識調(diào)用者。
圖4︰內(nèi)聯(lián)重定向的流程示意圖
四、 實(shí)現(xiàn)鉤子
因?yàn)锳ndroid操作系統(tǒng)基于Linux*內(nèi)核,因此許多Linux的研究技術(shù)都適用于安卓系統(tǒng)。本文中給出的詳細(xì)示例就是基于Ubuntu * 12.04.5 LTS。
(一) 內(nèi)聯(lián)式重定向
創(chuàng)建內(nèi)聯(lián)重定向的最簡單方法是在函數(shù)的起始地址處插入JMP指令。當(dāng)代碼調(diào)用目標(biāo)函數(shù)時(shí),它將立即跳至重定向函數(shù)中。請參閱圖5中所給的示例。
在主進(jìn)程中,代碼運(yùn)行func1()來處理一些數(shù)據(jù),然后返回到主進(jìn)程。這里,func1()的起始地址是0xf7e6c7e0。
圖5:內(nèi)聯(lián)掛鉤中使用前五個(gè)字節(jié)的函數(shù)來插入JMP指令
內(nèi)聯(lián)鉤子注入過程會將地址中的前五個(gè)字節(jié)的數(shù)據(jù)替換成0xE9 E0 D7 E6 F7 。這個(gè)過程將創(chuàng)建一個(gè)跳轉(zhuǎn)指令,此指令會跳轉(zhuǎn)到地址0xF7E6D7E0,而這個(gè)地址正好是函數(shù)my_func1()的入口。于是,所有對 func1()的代碼調(diào)用都將被重定向到my_func1()。輸入到my_func(1)的數(shù)據(jù)經(jīng)過一個(gè)預(yù)處理階段,然后將處理過的數(shù)據(jù)傳遞給func1()來完成原始過程。圖6展示了鉤住func1()后的代碼運(yùn)行序列,而接下來的圖7展示了建立鉤子后func1()的偽C代碼。
圖6:使用鉤子:在func1()中插入my_func1()
使用此方法,原始代碼不會意識到數(shù)據(jù)處理流程的變化。但是,更多的處理代碼被追加到原始函數(shù)func1()中。開發(fā)人員可以使用這種技術(shù)在運(yùn)行時(shí)添加程序補(bǔ)丁。
圖7:使用鉤子——圖6的偽C代碼
(二) 符號表重定向
相對于內(nèi)聯(lián)重定向,符號表重定向更復(fù)雜。有關(guān)鉤子代碼必須解析整個(gè)符號表,處理所有可能的情況,一個(gè)接一個(gè)地搜索和替換重定位函數(shù)的地址。DLL(動態(tài)鏈接庫)中的符號表將非常不同,這取決于使用了什么樣的編譯器參數(shù)以及開發(fā)人員調(diào)用外部函數(shù)的方式。
為了研究有關(guān)符號表的所有情況,需要創(chuàng)建包含兩個(gè)使用不同編譯器參數(shù)的動態(tài)庫的測試工程,它們分別是:
1. 位置獨(dú)立代碼(PIC)對象:libtest_PIC.so
2. 非PIC對象:libtest_nonPIC.so
圖8給出了測試程序的代碼執(zhí)行流程,以及l(fā)ibtest1()/libtest2()的源代碼(注意:它們幾乎具有完全相同的功能,除了使用不同的編譯器參數(shù)編譯外),還有程序的輸出。
圖8︰測試項(xiàng)目的軟件工作流程
函數(shù)printf()用于實(shí)現(xiàn)鉤子,它是打印信息到控制臺的最常用的函數(shù)。它定義在文件stdio.h中,而函數(shù)代碼位于庫文件glibc.so中。
在libtest_PIC和libtest_nonPIC庫中,使用了三個(gè)外部函數(shù)調(diào)用約定:
1.直接函數(shù)調(diào)用
2.間接函數(shù)調(diào)用
- 本地函數(shù)指針
- 全局函數(shù)指針
圖9:libtest1()的代碼
圖10:libtest2()的代碼,與libtest1()相同
圖11:測試程序的輸出結(jié)果
五、 libtest_nonPIC.so庫中的非PIC代碼研究
一個(gè)標(biāo)準(zhǔn)的DLL對象文件是由多個(gè)節(jié)(section)組成。每一節(jié)都有它自己的作用和定義。例如,Rel.dyn節(jié)中就包含了動態(tài)重定位表信息。文件的節(jié)信息可以通過命令 objdump -D libtest_nonPIC.so反編譯得到。
在庫文件libtest_nonPIC.so的重定位節(jié)rel.dyn中(見圖12),共有四個(gè)地方包含了函數(shù)printf()的重定位信息。動態(tài)重定位節(jié)中的每個(gè)條目包括以下類型:
1.偏移量Offset中的值標(biāo)識要調(diào)整的對象的位置。
2.類型字段Type標(biāo)識重定位類型。R_386_32對應(yīng)于把符號的32位絕對地址置于指定內(nèi)存位置的重定位數(shù)據(jù),而R_386_PC32則對應(yīng)于把符號的32位PC相對地址置于指定內(nèi)存位置的重定位數(shù)據(jù)。
3.Sym部分指向被引用的符號的索引。
圖13展示了函數(shù)libtest1()的生成的匯編代碼。有紅色標(biāo)記的printf()的入口地址在圖12中重定位節(jié)rel.dyn中被標(biāo)記出來。
圖12:庫文件libtest_nonPIC.so的重定位節(jié)信息
圖13︰libtest1()的反匯編代碼以非PIC格式編譯
為了把函數(shù)printf()重定向到另一個(gè)稱為hooked_printf()的函數(shù),掛鉤函數(shù)把hooked_printf()的地址寫入這四個(gè)偏移地址。
圖14:語句printf("libtest1: 1st call to the original printf()\n");的工作流程
圖15:語句global_printf1("libtest1: global_printf1()\n");的工作流程
圖16:語句local_printf("libtest1: local_printf()\n");的工作流程
如圖14-16所示的,當(dāng)鏈接器把動態(tài)庫加載到內(nèi)存時(shí),它首先找到重新定位的符號printf的名稱,然后將printf的真實(shí)地址寫入相應(yīng)的地址(偏移量0x4b5、 0x4c2、0x4cf和0x200c)。這些相應(yīng)的地址在重定位節(jié)rel.dyn中定義。之后,libtest1()中的代碼便可以正確地跳轉(zhuǎn)到printf()函數(shù)處。
【51CTO譯稿,合作站點(diǎn)轉(zhuǎn)載請注明原文譯者和出處為51CTO.com】