现在的位置: 首页 > 综合 > 正文

Android下通过root实现对system_server中binder的ioctl调用拦截

2013年02月04日 ⁄ 综合 ⁄ 共 5254字 ⁄ 字号 评论关闭

转自http://blog.csdn.net/passion/article/details/8085197

〇、引言


    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供外界使用:


  1. // 将新旧ioctl地址写入Andorid的Property供外界使用  
  2. int do_hook(void * param)  
  3. {  
  4.     old_ioctl = ioctl;  
  5.     printf("Ioctl addr: %p. New addr %p\n", ioctl, new_ioctl);  
  6.   
  7.     char value[PROPERTY_VALUE_MAX] = {'\0'};  
  8.     snprintf(value, PROPERTY_VALUE_MAX, "%u", ioctl);  
  9.     property_set(PROP_OLD_IOCTL_ADDR, value);  
  10.   
  11.     snprintf(value, PROPERTY_VALUE_MAX, "%u", new_ioctl);  
  12.     property_set(PROP_NEW_IOCTL_ADDR, value);  
  13.   
  14.     return 0;  
  15. }  
  16.   
  17. // 全局变量用以保存旧的ioctl地址,其实也可直接使用ioctl  
  18. int (*old_ioctl) (int __fd, unsigned long int __request, void * arg) = 0;  
  19.   
  20. // 欲接替ioctl的新函数地址,其中内部调用了老的ioctl  
  21. int new_ioctl (int __fd, unsigned long int __request, void * arg)  
  22. {  
  23.     if ( __request == BINDER_WRITE_READ )  
  24.     {  
  25.         call_count++;  
  26.   
  27.         char value[PROPERTY_VALUE_MAX] = {'\0'};  
  28.         snprintf(value, PROPERTY_VALUE_MAX, "%d", call_count);  
  29.         property_set(PROP_IOCTL_CALL_COUNT, value);  
  30.     }  
  31.   
  32.     int res = (*old_ioctl)(__fd, __request, arg);  
  33.     return res;  
  34. }  


    new_ioctl函数中,判断调用参数是否是BINDER_WRITE_READ通讯命令,是的话增加计数,并将计数写入Property,这样外界就能看见调用计数,才知道拦截成功了。


3.3 注入程序的搜索机制实现

    注入程序在上述第四步之后的流程是本文的核心。程序由于涉及到elf解析,还得使用linux下的elf.h。


    由于设置属性的property_set是个异步过程,因此调用共享库中的设置Property函数后,注入程序需要循环等待属性被设置上,类似于:

  1. char value[PROPERTY_VALUE_MAX] = {'\0'};  
  2. do {  
  3.     sleep(0);  
  4.     property_get(PROP_OLD_IOCTL_ADDR, value, "0");  
  5. while ( strcmp(value, "0") == 0 );  
  6. unsigned long old_ioctl_addr = atoi(value);  


    然后通过调用get_module_base,分析/proc/<system_server的pid>/maps文件,得到libbinder.so的加载基址(get_module_base也是LibInject中提供的)。

  1. void * binder_addr = get_module_base(target_pid, BINDER_LIB_PATH);  


    拿到新旧ioctl地址和libbinder.so基址后,就要搜索libbinder.so的GOT表,找匹配项。搜索libbinder.so既可以搜/system/lib/libbinder.so文件的内容,也可以通过ptrace的PEEKTEXT来搜system_server中被加载的libbinder.so在内存中的映像,我选择了前者,因为后者在实现时似乎有点问题,读到的Section内容不太对头,不确定是我程序问题还是被linker给改了。
    打开/system/lib/libbinder.so文件,获取其ELF头。

  1. read(fd, ehdr, sizeof(Elf32_Ehdr));  


    获得其Section Header区表的地址、数量和大小,并获得字符串表Section的索引号:

  1. unsigned long shdr_addr = ehdr->e_shoff;  
  2. int shnum = ehdr->e_shnum;  
  3. int shent_size = ehdr->e_shentsize;  
  4. unsigned long stridx = ehdr->e_shstrndx;  


    先提前把字符串表的内容读出来供遍历Section时比对其name用:

  1. // 读取Section Header中关于字符串表的描述,得到其尺寸和位置  
  2. lseek(fd, shdr_addr + stridx * shent_size, SEEK_SET);  
  3. read(fd, shdr, shent_size);  
  4.   
  5. // 根据尺寸分配内存  
  6. char * string_table = (char *)malloc(shdr->sh_size);  
  7. lseek(fd, shdr->sh_offset, SEEK_SET);  
  8.   
  9. // 将字符串表内容读入  
  10. read(fd, string_table, shdr->sh_size);  


    再重新遍历Section Header,找名为.got表的Section:

  1. lseek(fd, shdr_addr, SEEK_SET);  
  2. int i;  
  3. for ( i = 0; i < shnum; i++ )  
  4. {  
  5.     read(fd, shdr, shent_size);  
  6.   
  7.     if ( shdr->sh_type == SHT_PROGBITS )  
  8.     {  
  9.         int name_idx = shdr->sh_name;  
  10.         if ( strcmp(&(string_table[name_idx]), ".got") == 0 )  
  11.         {  
  12.             

抱歉!评论已关闭.