Android下通過hook技術(shù)實(shí)現(xiàn)透明加解密保障數(shù)據(jù)安全
一、前言
對(duì)于用戶在Android移動(dòng)設(shè)備商保存重要的隱私文件,通常采用一些加密保存的軟件。但在手機(jī)上實(shí)現(xiàn)隱私空間的軟件鱗次櫛比,但是問題在于打開文件都需要使用該隱私空間,將加密文件解密到臨時(shí)文件,然后再選擇應(yīng)用程序打開文件。這將導(dǎo)致用戶重要文件在設(shè)備上明文的存在,存在泄漏的風(fēng)險(xiǎn)。
而且根據(jù)筆者的調(diào)研,對(duì)于360隱私空間,應(yīng)用程序?qū)εR時(shí)文件修改后不能再逆向加密回密文,導(dǎo)致加密操作只能一次進(jìn)行。LBE隱私空間,相對(duì)較好,但其臨時(shí)文件存在生命周期過長(zhǎng)。
因此筆者通過現(xiàn)有知識(shí)討論一種采用hook技術(shù)實(shí)現(xiàn)的透明加解密方法,不需要在設(shè)備上生成臨時(shí)文件,從而保護(hù)用戶重要隱私。
二、技術(shù)要點(diǎn)
由于Android是基于linux內(nèi)核的開源系統(tǒng),根據(jù)語(yǔ)言環(huán)境不同可以分為Java層、Native C層、Linux Kernel層。Java層的安全是使用Java語(yǔ)言開發(fā),基于SDK,能實(shí)現(xiàn)的功能相對(duì)簡(jiǎn)單。Linux Kernel層安全,需要從源碼做起,編譯自己的系統(tǒng),通用性不強(qiáng)。因此在Native C層,通過JNI開發(fā),可以使用linux提供的函數(shù)實(shí)現(xiàn)更多的功能。
在hook API方面與linux的hook類似使用ptrace 函數(shù)與plt表實(shí)現(xiàn),還可以采用Inline hook的方式實(shí)現(xiàn),但是不是很穩(wěn)定,操作難度大。其本質(zhì)都是劫持函數(shù)調(diào)用。
但是由于處于Linux用戶態(tài),每個(gè)進(jìn)程都有自己獨(dú)立的進(jìn)程空間,所以必須先注入到所要hook的進(jìn)程空間,修改其內(nèi)存中的進(jìn)程代碼,替換其中過程表的符號(hào)地址,因此其生存空間是所注入的進(jìn)程,只能對(duì)某一進(jìn)程進(jìn)行HOOK。
Ptrace函數(shù)是調(diào)試程序所用,功能強(qiáng)大,不僅可以附加某一進(jìn)程(PTRACE_ATTACH),而且可以任意修改目標(biāo)進(jìn)程的內(nèi)存空間(PTRACE_PEEKDATA,讀內(nèi)存。PTRACE_POKEDATA,寫內(nèi)存),甚至是寄存器(PTRACE_SETREGS,PTRACE_GETREGS)
基本流程是利用寄存器指令中斷:
① PTRACE_ATTACH,綁定目標(biāo)進(jìn)程。
② PTRACE_GETREGS,獲取目標(biāo)進(jìn)程寄存器狀態(tài),并保存。
③ PTRACE_PEEKDATA與PTRACE_POKEDATA配合,保存原代碼,寫入要注入的代碼到當(dāng)前運(yùn)行位置。
④ PTRACE_SETREGS,恢復(fù)寄存器狀態(tài),并繼續(xù)執(zhí)行,這是注入的代碼開始在目標(biāo)進(jìn)程內(nèi)執(zhí)行,注入代碼完成HOOK,過程與Windows下相似。
⑤ 在HOOK完成后,注入的代碼執(zhí)行int3被ptrace捕獲,目標(biāo)進(jìn)程再次暫停執(zhí)行。
⑥ PTRACE_GETREGS,再次保存寄存器。
⑦ PTRACE_PEEKDATA與PTRACE_POKEDATA配合還原代碼。
⑧ PTRACE_SETREGS,恢復(fù)寄存器,目標(biāo)進(jìn)程繼續(xù)執(zhí)行。
⑨ PTRACE_DETACH,撤銷綁定目標(biāo)進(jìn)程。
參考LBE實(shí)現(xiàn)原理和看雪上關(guān)于Hook Ioctl的文章都基本上按照這種流程實(shí)現(xiàn)HOOK。
在明白hook工作機(jī)制后,對(duì)于實(shí)現(xiàn)Android上的透明加解密需要找到open和close函數(shù)的符號(hào)存在哪個(gè)動(dòng)態(tài)鏈接庫(kù)中,hook該應(yīng)用程序的這個(gè)動(dòng)態(tài)鏈接庫(kù),在open操作進(jìn)行的時(shí)候,將密文文件分塊解密到內(nèi)存中,并將該內(nèi)存中的文件標(biāo)識(shí)符返回。在close操作進(jìn)行的時(shí)候?qū)?nèi)存中的明文加密到本地密文存儲(chǔ)。
三、關(guān)鍵流程
1、閱讀Android代碼查找打開文件和關(guān)閉文件過程。這是我們實(shí)現(xiàn)透明加解密的關(guān)鍵。
參考http://blog.chinaunix.net/uid-26926660-id-3326678.html的方式
可以發(fā)現(xiàn)讀取文件流的函數(shù)最終通過JNI方式的read函數(shù)實(shí)現(xiàn),同樣打開文件的操作最終都?xì)w結(jié)于open函數(shù)。
而實(shí)現(xiàn)Java代碼的JNI支持的動(dòng)態(tài)庫(kù)是nativehelper.so因此我們需要hook的動(dòng)態(tài)庫(kù)即nativehelper.so。
注:在Android早期版本即android2.3、Android4.0上open和close符號(hào)在nativehelper.so中,該文件有140k大小。而在android4.1版本以上,谷歌重寫了android原生庫(kù)的實(shí)現(xiàn),nativehelper.so被拆分,筆者在4.0平臺(tái)進(jìn)行的開發(fā)并沒有閱讀尋找4.1版本之上的。
2、進(jìn)行進(jìn)程注入和ELF節(jié)替換
進(jìn)程注入就是將一段代碼拷貝到目標(biāo)進(jìn)程,然后讓目標(biāo)進(jìn)程執(zhí)行這段代碼的技術(shù)。由于這樣的代碼構(gòu)造起來比較復(fù)雜,所以實(shí)際情況下,只將很少的代碼注入到目標(biāo)進(jìn)程,而將真正做事的代碼放到一個(gè)共享庫(kù)中,即.so文件。被注入的那段代碼只負(fù)責(zé)加載這個(gè).so,并執(zhí)行里面的函數(shù)。由于.so中的函數(shù)是在目標(biāo)進(jìn)程中執(zhí)行的,所以在.so中的函數(shù)可以修改目標(biāo)進(jìn)程空間的任何內(nèi)存,當(dāng)然也可以加鉤子,從而達(dá)到改變目標(biāo)進(jìn)程工作機(jī)制的目的。
當(dāng)然不是任何進(jìn)程都有權(quán)限執(zhí)行注入操作的。Android平臺(tái)上的進(jìn)程注入是基于ptrace()的,要調(diào)用ptrace()需要有root權(quán)限。目前市面上的主流安全軟件也都是基于進(jìn)程注入來管理和控制其他應(yīng)用進(jìn)程的。這也就是為什么這些安全軟件需要獲得root權(quán)限的原因。
關(guān)于如何.so注入的實(shí)現(xiàn),可以參考看雪論壇的上的一個(gè)注入庫(kù)LibInject 和洗大師的一個(gè)開源項(xiàng)目Android Injector Library。
筆者對(duì)這種兩種方式都有實(shí)驗(yàn),對(duì)于Libinject,就是向目標(biāo)進(jìn)程中注入libhook.so,首先調(diào)用ptrace()函數(shù),掛起該進(jìn)程。然后遍歷進(jìn)程加載的libc.so,通過dlopen和dlsym函數(shù)修改arm寄存器的值,然后壓入?yún)?shù),so路徑,并將之前找到的dlopen地址壓入寄存器,直接操作blx,就可以讓目標(biāo)進(jìn)程調(diào)用dlopen加載我們的so,同理dlsym調(diào)用我們的so里的函數(shù)。
注入完成后,會(huì)調(diào)用libhook.so庫(kù)中的hook_entry()函數(shù),該函數(shù)實(shí)現(xiàn)加載hook函數(shù)實(shí)現(xiàn)的動(dòng)態(tài)庫(kù),并對(duì)libnativehelper.so的got表和plt表的遍歷和修改。修改為自己編寫的實(shí)現(xiàn)open函數(shù)和close函數(shù)的動(dòng)態(tài)庫(kù)中的符號(hào)地址。因此需要注入兩個(gè)庫(kù),因?yàn)閘ibhook.so在執(zhí)行完后需要detach目標(biāo)進(jìn)程,從而釋放,而具體操作的動(dòng)態(tài)庫(kù)需要常駐內(nèi)存。實(shí)現(xiàn)常駐內(nèi)存需要在hook_entry()函數(shù)中顯式加載動(dòng)態(tài)庫(kù)。
以上注入以后的過程都由自己編程實(shí)現(xiàn),能夠加深對(duì)ELF格式理解。
而采用Android Injector Library則相對(duì)簡(jiǎn)單,在調(diào)用可執(zhí)行程序的主函數(shù)中實(shí)現(xiàn)即可:
這個(gè)文件編譯生成注入入口和符號(hào)表替換邏輯。
* 1、在該函數(shù)中加載libhook.so通過其中的do_hook函數(shù)返回原來的open和close地址以及要替換的新的open和close函數(shù)地址
* 2、然后靜態(tài)打開libnativehelper動(dòng)態(tài)庫(kù),讀取其結(jié)構(gòu)遍歷節(jié)表,找到全局符號(hào)表(GOT表),該表存儲(chǔ)了外部依賴符號(hào)的地址
* 3、遍歷GOT表找到原先的open函數(shù)和close函數(shù)地址,分別替換為新的open函數(shù)和新的close函數(shù)即可

3、在學(xué)習(xí)這一過程中,需要了解linux的ELF格式,以下是學(xué)習(xí)ELF的筆記:參見《程序員的自我修養(yǎng)》如果熟悉則可跳過。


ehdr->e_shstrndx索引指向shstrtab的節(jié),可以用來索引節(jié)頭的字符串名稱描述。shstrtab表(Section Header String Table)保存段表中用到的字符串,最常見的就是段名、
常用的段名 說明 .rodata1 Read Only Data,這種段里存放的是只讀數(shù)據(jù),比如字符串常量、全局const變量。跟”.rodata”一樣 .comment 存放的是編譯器版本信息 .debug 調(diào)試信息 .dynamic 動(dòng)態(tài)鏈接信息 .hash 符號(hào)哈希表 .line 調(diào)試時(shí)的行號(hào)表 .note 額外的編譯器信息。比如程序的公司名、發(fā)布版本號(hào)等 .strtab String Table.字符串表 .symtab Symbol Table.符號(hào)表 .shstrtab Section String Table.段名表 .plt .got 動(dòng)態(tài)鏈接的跳轉(zhuǎn)表和全局入口表 .init .fini 程序初始化與終結(jié)代碼段
符號(hào)節(jié),遍歷節(jié)頭時(shí)候。判斷每一個(gè)節(jié)的類型是不是SHT_SYMTAB或SHT_DYNSYM,那么對(duì)應(yīng)的節(jié)就是符號(hào)節(jié)。符號(hào)節(jié)存放的是一張符號(hào)表,符號(hào)表也是一個(gè)連續(xù)存儲(chǔ)的結(jié)構(gòu)數(shù)組.
編程過程中用到的變量和函數(shù)都可以稱之為符號(hào),一個(gè)ELF文件中并不只有一個(gè)符號(hào)節(jié),通常是兩個(gè),一個(gè)為”.dynsym”的動(dòng)態(tài)節(jié)類型為SHT_DYNSYM,所有引入的外部符號(hào)在這里有所體現(xiàn),另一個(gè)為SHT_SYMTAB,名字為“.symtab”保存了所有有用符號(hào)信息。
Symbol Table 符號(hào)表保存了一個(gè)程序在定位和重定位時(shí)需要的定義和引用的信息。一個(gè)符號(hào)表索引是相應(yīng)的下標(biāo)。符號(hào)表的存在意義是體現(xiàn)在多個(gè)目標(biāo)文件進(jìn)行鏈接的時(shí)候,在鏈接中,目標(biāo)文件之間相互拼合實(shí)際上是目標(biāo)文件之間對(duì)地址的引用,即對(duì)函數(shù)和變量的地址的引用,而函數(shù)和變量可以統(tǒng)稱為符號(hào)(Symbol),函數(shù)名或變量名就是符號(hào)名(Symbol Name)。我們可以將符號(hào)看作是是鏈接中的粘合劑,整個(gè)鏈接過程就是基于符號(hào)才能夠正確完成。在符號(hào)表”.symtab“中,其也是像段表的結(jié)構(gòu)一樣,是一個(gè)數(shù)組,每個(gè)數(shù)組元素是一個(gè)固定的結(jié)構(gòu)來保存符號(hào)的相關(guān)信息,比如符號(hào)名(不是字符串,而是該符號(hào)名在字符串表的下標(biāo))、符號(hào)對(duì)應(yīng)的值(可能是段中的偏移,也可能是符號(hào)的虛擬地址)、符號(hào)大小(數(shù)據(jù)類型的大小)等等。符號(hào)表中記錄的一般是全局符號(hào),比如全局變量、全局函數(shù)等等。
目標(biāo)文件的符號(hào)表包含定位或重定位程序符號(hào)定義和引用時(shí)所需要的信息。符號(hào)表入口結(jié)構(gòu)定義如下:
typedef struct{ Elf32_Word st_name; Elf32_Addr st_value; Elf32_Word st_size; Unsigned char st_info; Unsigned char st_other; Elf32_Half st_shndx; }Elf32_Sym;
其中st_name包含指向符號(hào)表字符串表(strtab)中的索引,從而可以獲得符號(hào)名。St_value指出符號(hào)的值,可能是一個(gè)絕對(duì)值、地址等。St_size指出符號(hào)相關(guān)的內(nèi)存大小,比如一個(gè)數(shù)據(jù)結(jié)構(gòu)包含的字節(jié)數(shù)等。St_info規(guī)定了符號(hào)的類型和綁定屬性,指出這個(gè)符號(hào)是一個(gè)數(shù)據(jù)名、函數(shù)名、section名還是源文件名;并且指出該符號(hào)的綁定屬性是local、global還是weak。
GOT表和PLT表
GOT(Global Offset Table)表中每一項(xiàng)都是本運(yùn)行模塊要引用的一個(gè)全局變量或函數(shù)的地址。可以用GOT表來間接引用全局變量、函數(shù),也可以把GOT表的首地址作為一個(gè)基 準(zhǔn),用相對(duì)于該基準(zhǔn)的偏移量來引用靜態(tài)變量、靜態(tài)函數(shù)。由于加載器不會(huì)把運(yùn)行模塊加載到固定地址,在不同進(jìn)程的地址空間中,各運(yùn)行模塊的絕對(duì)地址、相對(duì)位 置都不同。這種不同反映到GOT表上,就是每個(gè)進(jìn)程的每個(gè)運(yùn)行模塊都有獨(dú)立的GOT表,所以進(jìn)程間不能共享GOT表。

動(dòng)態(tài)鏈接機(jī)制
首先回顧一下Linux平臺(tái)上,一個(gè)模塊甲需要調(diào)用另外一個(gè)模塊乙中的函數(shù)時(shí)的動(dòng)態(tài)鏈接機(jī)制:
1、模塊甲在編譯期間,將要引用的模塊乙的名字與函數(shù)名寫入自身的符號(hào)表。
2、運(yùn)行期模塊甲調(diào)用時(shí),調(diào)用流程是從調(diào)用代碼到PLT表到GOT表再跳入模塊乙。
而如何保證模塊甲的代碼能從其PLT/GOT跳到正確的模塊乙入口,這就是鏈接器做的事情。
標(biāo)準(zhǔn)Linux鏈接器是ld.so,支持懶綁定,也就是說,模塊甲在編譯期間生成的調(diào)用模塊乙的原始代碼,流程是從調(diào)用代碼到PLT表到鏈接器。運(yùn)行期第一次調(diào)模塊乙時(shí),首先進(jìn)入鏈接器,鏈接器根據(jù)調(diào)用信息加載模塊乙搜尋其符號(hào)并將找到的函數(shù)地址填入GOT表,之后的后續(xù)調(diào)用流程就直接走PLT/GOT表了。這種機(jī)制能減少加載時(shí)的開銷,為L(zhǎng)inux發(fā)行版等采用。
Android雖然內(nèi)核基于Linux,但其動(dòng)態(tài)鏈接機(jī)制卻不是ld.so而是自帶的linker,不支持懶綁定。也就是說,上述模塊甲乙如果在Android平臺(tái)上,則是模塊甲加載時(shí),linker就會(huì)根據(jù)模塊甲中的.rel.plt表和字符串表中的內(nèi)容加載模塊乙并搜索其所需函數(shù)地址并預(yù)先填入GOT表。之后調(diào)用流程每次都直接走PLT/GOT表,不再進(jìn)linker,PLT表中也省去了跳至linker的代碼,這種流程和“勤勞”綁定類似,倒是為攔截提供了一點(diǎn)方便。如果攔截懶綁定的入口時(shí)模塊乙還沒加載地址也沒找到,攔截就沒法進(jìn)行了。
要攔截模塊甲對(duì)乙的調(diào)用,一般思路是通過ptrace遠(yuǎn)程注入并加載一新攔截模塊至模塊甲,并搜索模塊甲的GOT表,找到對(duì)模塊乙的調(diào)用地址,改成新模塊內(nèi)的某函數(shù)地址,然后新模塊內(nèi)的這個(gè)函數(shù)在進(jìn)行了自己的處理后,再跳到模塊乙中。
Android和Linux的鏈接器不同導(dǎo)致了內(nèi)存布局的差異,也導(dǎo)致了網(wǎng)上流行的Linux注入與HOOK的方法行不通。網(wǎng)上的方法是通過ptrace注入后,搜索dynamic的section中的PLTGOT區(qū),去里頭取link_map以遍歷此進(jìn)程所加載的模塊來搜索需要hook的函數(shù)地址。但Android上,dynamic的section的PLTGOT區(qū)前幾項(xiàng)都是空的,沒有l(wèi)ink_map這個(gè)數(shù)據(jù)結(jié)構(gòu),只能通過分析/proc/
4、閱讀代碼中的注意事項(xiàng)
在Android Injector Library閱讀過程中有幾個(gè)需要注意的地方。
1)利用捕捉SIGSEGV的無(wú)效內(nèi)存引用或者段錯(cuò)誤的異常信號(hào)來執(zhí)行ptrace。
2)ptrace(PTRACE_PEEKTEXT, pid, addr, data)
描述:從內(nèi)存地址中讀取一個(gè)字節(jié),pid表示被跟蹤的子進(jìn)程,內(nèi)存地址由addr給出,data為用戶變量地址用于返回讀到的數(shù)據(jù)。
在Linux(i386)中用戶代碼段與用戶數(shù)據(jù)段重合所以讀取代碼段和數(shù)據(jù)段數(shù)據(jù)處理是一樣的。
3)linker 主要用于實(shí)現(xiàn)共享庫(kù)的加載與鏈接。它支持應(yīng)用程序?qū)?kù)函數(shù)的隱式和顯式調(diào)用。查找/system/bin/linker中加載的libdl.so,加載位置固定,定義了dlopen,dlcose,dlsym,dlerror。
4)有下列代碼理解,即dynsym和symtab的關(guān)系

5)在代碼中有關(guān)于dynsym符號(hào)讀取順序的錯(cuò)誤。但是不影響使用。使用androidSDK下的工具readelf

5、需要編寫自己的open函數(shù)和close函數(shù)實(shí)現(xiàn)加解密操作
該過程使用Android 平臺(tái)下的openssl EVP編程,該過程的難度不大。
關(guān)鍵點(diǎn)一是在于使用密鑰空間構(gòu)造。推薦密鑰空間使用數(shù)組。使用char*字符串即使在字符串最后存在’’也會(huì)由于內(nèi)存中的其他內(nèi)容影響密鑰初始化,出現(xiàn)意想不到的問題。
關(guān)鍵點(diǎn)二在close時(shí),參數(shù)只有文件描述符,可以通過下述代碼獲得文件名。

關(guān)鍵點(diǎn)三在于使用Openssl進(jìn)行對(duì)稱加解密時(shí)會(huì)填充到相應(yīng)的塊大小,需要手動(dòng)剝離這些填充??梢圆捎脟?guó)際通用填充方式構(gòu)造填充,或者自主構(gòu)造密文文件頭記錄填充大小。
http://en.wikipedia.org/wiki/Padding_(cryptography)
6、記得在Makefile文件中加入
LOCAL_LDLIBS+=-L$(SYSROOT)/usr/lib -llog
LOCAL_LDLIBS+=-L$(SYSROOT)/usr/lib -lcrypto
LOCAL_LDLIBS+=-L$(SYSROOT)/usr/lib -lssl
7、需要再進(jìn)行密鑰管理模塊的開發(fā),該過程不再描述。
四、總結(jié)
該種方案能夠?qū)崿F(xiàn)在android平臺(tái)上的透明加解密。不足之處在于需要使用root權(quán)限,提前捕捉用戶程序啟動(dòng),對(duì)其進(jìn)行hook。在移動(dòng)設(shè)備上效率是瓶頸,而且文件不宜過大。對(duì)docxlspdfppttxt等文本、jpg等圖片支持較好,其他格式的文件筆者沒有進(jìn)行測(cè)試。