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