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

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


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

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

5)在代碼中有關于dynsym符號讀取順序的錯誤。但是不影響使用。使用androidSDK下的工具readelf

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

關鍵點三在于使用Openssl進行對稱加解密時會填充到相應的塊大小,需要手動剝離這些填充。可以采用國際通用填充方式構造填充,或者自主構造密文文件頭記錄填充大小。
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、需要再進行密鑰管理模塊的開發,該過程不再描述。
四、總結
該種方案能夠實現在android平臺上的透明加解密。不足之處在于需要使用root權限,提前捕捉用戶程序啟動,對其進行hook。在移動設備上效率是瓶頸,而且文件不宜過大。對docxlspdfppttxt等文本、jpg等圖片支持較好,其他格式的文件筆者沒有進行測試。