======================================================================
〇、引言
======================================================================
Linux下的遠程注入與HOOK網上已有不少文章與代碼實現,而對於Android平臺,注入有不少,但HOOK卻不多。經過了兩個多禮拜的研究,我初步實現了在擁有root權限的Android 2.3平臺上針對system_server中binder通訊的攔截,寫下來分享一下。
======================================================================
一、動態鏈接機制
======================================================================
首先回顧一下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/<pid>/maps來遍歷模塊。
======================================================================
二、Binder攔截選址
======================================================================
Binder是Andorid上的輕量級跨進程通訊機制,由用戶空間的libbinder.so和內核的binder驅動協作構成。一次完整的Binder調用的流程(拿對system_server中的Service的調用舉例)是從用戶進程到用戶進程加載的libbinder.so到ioctl到binder驅動並阻塞,Service端在等待時通過libbinder.so收到驅動傳上來的調用請求,把數據整好後通過libbinder.so再通過ioctl返回給驅動,之前用戶端阻塞的ioctl收到應答而返回,回到libbinder.so再回到用戶進程,從而完成了一次完整的調用請求。
注意,這裏用戶進程空間所加載的libbinder.so和system_server端加載的libbinder.so在邏輯上不是同一個東西。正因爲不是同一個東西,我們才能針對system_server進程中加載的libbinder.so動手,攔截其GOT表中對ioctl的調用,從而提前知道Service要返回的內容(如果想改,則需要分析Binder數據再改了)。這個ioctl就是攔截的選址所在。
======================================================================
三、具體實現
======================================================================
----------------------------------------------------------------------
3.1 實現思路
----------------------------------------------------------------------
在嘗試了各種思路並失敗了很多次後,最終確定下來攔截system_server進程中的binder通訊的思路如下:
1、以root身份運行注入程序,通過ptrace停止並附加system_server。
2、遠程注入shellcode,加載注入的共享庫並解除附加,讓其調用共享庫中的一特定函數。
3、此特定函數將庫中待接替ioctl的新函數地址以及ioctl的真實地址寫入Android的Property供外界使用。
4、注入程序通過Android的Property獲得ioctl的原始地址以及接替ioctl的新函數地址。
5、注入程序再次通過ptrace附加system_server,定位libbinder.so中名爲.got的Section,並搜索其項尋找ioctl的原始地址。
6、找到GOT表中的原始地址後將其替換爲接替ioctl的新函數地址。
7、解除附加system_server讓其重新運行,完成攔截。
其中,1和2在網上有現成的實現,是一個叫LibInject的包,其中有inject.c/h以及Android.mk,還有個大牛給出的shellcode.s。不過這段shellcode加載共享庫並調用後會立即dlclose卸載之,不符合我們常駐的需求,因此我又寫了個新共享庫讓shellcode加載的共享庫調用,多了一步。此庫最終常駐system_server的內存。
----------------------------------------------------------------------
3.2 注入共享庫中的新函數實現
----------------------------------------------------------------------
在這個常駐system_server進程內的共享庫裏,只實現了簡單幾個函數,其中do_hook函數在注入後通過外界調用,它不做具體的hook動作,僅僅只是把所需的兩個函數地址寫入Android的Property供外界使用:
// 將新舊ioctl地址寫入Andorid的Property供外界使用 int do_hook(void * param) { old_ioctl = ioctl; printf("Ioctl addr: %p. New addr %p\n", ioctl, new_ioctl); char value[PROPERTY_VALUE_MAX] = {'\0'}; snprintf(value, PROPERTY_VALUE_MAX, "%u", ioctl); property_set(PROP_OLD_IOCTL_ADDR, value); snprintf(value, PROPERTY_VALUE_MAX, "%u", new_ioctl); property_set(PROP_NEW_IOCTL_ADDR, value); return 0; } // 全局變量用以保存舊的ioctl地址,其實也可直接使用ioctl int (*old_ioctl) (int __fd, unsigned long int __request, void * arg) = 0; // 欲接替ioctl的新函數地址,其中內部調用了老的ioctl int new_ioctl (int __fd, unsigned long int __request, void * arg) { if ( __request == BINDER_WRITE_READ ) { call_count++; char value[PROPERTY_VALUE_MAX] = {'\0'}; snprintf(value, PROPERTY_VALUE_MAX, "%d", call_count); property_set(PROP_IOCTL_CALL_COUNT, value); } int res = (*old_ioctl)(__fd, __request, arg); return res; }
----------------------------------------------------------------------
3.3 注入程序的搜索機制實現
----------------------------------------------------------------------
注入程序在上述第四步之後的流程是本文的核心。程序由於涉及到elf解析,還得使用linux下的elf.h。
由於設置屬性的property_set是個異步過程,因此調用共享庫中的設置Property函數後,注入程序需要循環等待屬性被設置上,類似於:
char value[PROPERTY_VALUE_MAX] = {'\0'}; do { sleep(0); property_get(PROP_OLD_IOCTL_ADDR, value, "0"); } while ( strcmp(value, "0") == 0 ); unsigned long old_ioctl_addr = atoi(value);
void * binder_addr = get_module_base(target_pid, BINDER_LIB_PATH);
打開/system/lib/libbinder.so文件,獲取其ELF頭。
read(fd, ehdr, sizeof(Elf32_Ehdr));
unsigned long shdr_addr = ehdr->e_shoff; int shnum = ehdr->e_shnum; int shent_size = ehdr->e_shentsize; unsigned long stridx = ehdr->e_shstrndx;
// 讀取Section Header中關於字符串表的描述,得到其尺寸和位置 lseek(fd, shdr_addr + stridx * shent_size, SEEK_SET); read(fd, shdr, shent_size); // 根據尺寸分配內存 char * string_table = (char *)malloc(shdr->sh_size); lseek(fd, shdr->sh_offset, SEEK_SET); // 將字符串表內容讀入 read(fd, string_table, shdr->sh_size);
lseek(fd, shdr_addr, SEEK_SET); int i; for ( i = 0; i < shnum; i++ ) { read(fd, shdr, shent_size); if ( shdr->sh_type == SHT_PROGBITS ) { int name_idx = shdr->sh_name; if ( strcmp(&(string_table[name_idx]), ".got") == 0 ) { /* 就是 GOT 表! */ *out_addr = base_addr + shdr->sh_offset; *out_size = shdr->sh_size; return 0; } } }
然後搜索與Hook就好辦了:
for ( i = 0; i < out_size; i ++) { ptrace_readdata(target_pid, out_addr, &got_item, 4); if ( got_item == old_ioctl_addr ) { /* !!! 拿到了 ioctl 地址 !!! 改成我們的。 */ ptrace_writedata(target_pid, out_addr, &new_ioctl_addr, sizeof(new_ioctl_addr)); break; } else if ( got_item == new_ioctl_addr ) { /* 已經是我們的了,不重複Hook。 */ break; } out_addr++; }
再跑命令:
# getprop persist.sys.ioctl.callcount
getprop persist.sys.ioctl.callcount
502
隨便動動手機,數字在不斷增加中,不過有點影響性能。
======================================================================
四、補充說明
======================================================================
1、每個被加載的模塊,無論是可執行程序還是共享庫,均有自己獨立的PLT和GOT表。所以攔截這個模塊的對外調用的GOT,不影響其他模塊。
2、本文只實現了攔截模塊的調出到其他模塊的動作,其他模塊的調入沒有涉及到(可能還涉及到比較複雜的重定位操作)。
3、system_server是system用戶,不是有權限寫所有名字的Property,這裏用了persist.sys.開頭的屬性名,而persist.sys.開頭的屬性會保存至磁盤,因此性能會差點兒。
4、ioctl雖然實質聲明是個可變參數:int new_ioctl (int __fd, unsigned long int __request, /*void * arg*/ ...),這種聲明的函數要直接透明地將參數從舊函數傳遞給新函數似乎還不可行,搜了很多資料也沒找到。幸好搜了一把libbinder.so源碼,裏頭對ioctl的調用參數均是仨,乾脆就不處理變長形式了。
5、如果不以root身份運行注入程序,則ptrace附加時會失敗。
6、Andriod系統的大部分Service都運行在system_server進程中,可以攔截到。但部分自定義的用戶Service在用戶進程中,如需要攔截,則要ptrace到那個用戶進程才行,攔截方法也類似。
7、至於攔截Binder的數據分析與修改,則是下一篇文章的內容了。
======================================================================
五、參考資料
======================================================================
http://wenku.baidu.com/view/222f348f84868762caaed558.html
http://blog.csdn.net/lingfong_cool/article/details/7976112
http://blog.csdn.net/ylyuanlu/article/details/6638825
http://os.pku.edu.cn:8080/gaikuang/submission/TN05.ELF.Format.Summary.pdf
http://blog.csdn.net/fengkehuan/article/details/6223406
http://blog.csdn.net/innost/article/details/6124685
還有不少,沒法全寫下來,在此向所有無私共享自己研究成果的人致以誠摯的謝意。