1、前言linux
對於用戶在Android移動設備商保存重要的隱私文件,一般採用一些加密保存的軟件。但在手機上實現隱私空間的軟件鱗次櫛比,可是問題在於打開文件都須要使用該隱私空間,將加密文件解密到臨時文件,而後再選擇應用程序打開文件。這將致使用戶重要文件在設備上明文的存在,存在泄漏的風險。android
並且根據筆者的調研,對於360隱私空間,應用程序對臨時文件修改後不能再逆向加密回密文,致使加密操做只能一次進行。LBE隱私空間,相對較好,但其臨時文件存在生命週期過長。程序員
所以筆者經過現有知識討論一種採用hook技術實現的透明加解密方法,不須要在設備上生成臨時文件,從而保護用戶重要隱私。編程
2、技術要點數組
因爲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操做進行的時候將內存中的明文加密到本地密文存儲。
3、關鍵流程
一、閱讀Android代碼查找打開文件和關閉文件過程。這是咱們實現透明加解密的關鍵。
參考http://blog.chinaunix.net/uid-26926660-id-3326678.html的方式
能夠發現讀取文件流的函數最終經過JNI方式的read函數實現,一樣打開文件的操做最終都歸結於open函數。
而實現Java代碼的JNI支持的動態庫是nativehelper.so所以咱們須要hook的動態庫即nativehelper.so。
注:在Android早期版本即android2.三、Android4.0上open和close符號在nativehelper.so中,該文件有140k大小。而在android4.1版本以上,谷歌重寫了android原生庫的實現,nativehelper.so被拆分,筆者在4.0平臺進行的開發並無閱讀尋找4.1版本之上的。
二、進行進程注入和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則相對簡單,在調用可執行程序的主函數中實現便可:
這個文件編譯生成注入入口和符號表替換邏輯。
* 一、在該函數中加載libhook.so經過其中的do_hook函數返回原來的open和close地址以及要替換的新的open和close函數地址
* 二、而後靜態打開libnativehelper動態庫,讀取其結構遍歷節表,找到全局符號表(GOT表),該表存儲了外部依賴符號的地址
* 三、遍歷GOT表找到原先的open函數和close函數地址,分別替換爲新的open函數和新的close函數便可
三、在學習這一過程當中,須要瞭解linux的ELF格式,如下是學習ELF的筆記:參見《程序員的自我修養》若是熟悉則可跳過。
ehdr->e_shstrndx索引指向shstrtab的節,能夠用來索引節頭的字符串名稱描述。shstrtab表(Section Header String Table)保存段表中用到的字符串,最多見的就是段名、
經常使用的段名說明 .rodata1Read Only Data,這種段裏存放的是隻讀數據,好比字符串常量、全局const變量。跟」.rodata」同樣 .comment存放的是編譯器版本信息 .debug 調試信息 .dynamic動態連接信息 .hash 符號哈希表 .line 調試時的行號表 .note 額外的編譯器信息。好比程序的公司名、發佈版本號等 .strtab String Table.字符串表 .symtab Symbol Table.符號表 .shstrtabSection 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平臺上,一個模塊甲須要調用另一個模塊乙中的函數時的動態連接機制:
一、模塊甲在編譯期間,將要引用的模塊乙的名字與函數名寫入自身的符號表。
二、運行期模塊甲調用時,調用流程是從調用代碼到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/ /maps來遍歷模塊。
四、閱讀代碼中的注意事項
在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
五、須要編寫本身的open函數和close函數實現加解密操做
該過程使用Android 平臺下的openssl EVP編程,該過程的難度不大。
關鍵點一是在於使用密鑰空間構造。推薦密鑰空間使用數組。使用char*字符串即便在字符串最後存在’’也會因爲內存中的其餘內容影響密鑰初始化,出現意想不到的問題。
關鍵點二在close時,參數只有文件描述符,能夠經過下述代碼得到文件名。
關鍵點三在於使用Openssl進行對稱加解密時會填充到相應的塊大小,須要手動剝離這些填充。能夠採用國際通用填充方式構造填充,或者自主構造密文文件頭記錄填充大小。
http://en.wikipedia.org/wiki/Padding_(cryptography)
六、記得在Makefile文件中加入
LOCAL_LDLIBS+=-L$(SYSROOT)/usr/lib -llog
LOCAL_LDLIBS+=-L$(SYSROOT)/usr/lib -lcrypto
LOCAL_LDLIBS+=-L$(SYSROOT)/usr/lib -lssl
七、須要再進行密鑰管理模塊的開發,該過程再也不描述。
4、總結
該種方案可以實如今android平臺上的透明加解密。不足之處在於須要使用root權限,提早捕捉用戶程序啓動,對其進行hook。在移動設備上效率是瓶頸,並且文件不宜過大。對docxlspdfppttxt等文本、jpg等圖片支持較好,其餘格式的文件筆者沒有進行測試。