首页
论坛
课程
招聘
[原创]Android下通过root实现对system_server中binder的ioctl调用拦截
2012-10-18 13:53 219267

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

2012-10-18 13:53
219267
Android下通过root实现对system_server中binder的ioctl调用拦截
作者:passion
2012-10-18
关键字:Android, Hook, API, Binder, 注入, 拦截
(转载请注明出处)

======================================================================
〇、引言
======================================================================

    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;
    }


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

----------------------------------------------------------------------
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);

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

    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头。

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


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

    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时比对其name用:

    // 读取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);

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

    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;
            }
        }
    }


    这样,out_addr和out_size,就是system_server中libbinder.so的GOT表所在的位置和长度。
    然后搜索与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++;
    }


    写Android.mk将其在 Android 2.3 源码树下编译后,adb push上去(共有注入程序、shellcode使用的共享库、我们的共享库三个文件),运行注入程序,提示注入成功。如果再次运行,则提示已经注入过了。
    再跑命令:

    # 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

    还有不少,没法全写下来,在此向所有无私共享自己研究成果的人致以诚挚的谢意。

安卓应用层抓包通杀脚本发布!《高研班》2021年3月班开始招生!

收藏
点赞0
打赏
分享
最新回复 (48)
雪    币: 2985
活跃值: 活跃值 (238)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
b23526 活跃值 2012-10-18 14:49
2
0
好文,标记收藏
雪    币: 755
活跃值: 活跃值 (66)
能力值: ( LV12,RANK:310 )
在线值:
发帖
回帖
粉丝
古河 活跃值 6 2012-10-18 15:14
3
0
顶一下,看来在下的LibInject还能派上点用场
雪    币: 2299
活跃值: 活跃值 (281)
能力值: (RANK:350 )
在线值:
发帖
回帖
粉丝
非虫 活跃值 7 2012-10-18 17:52
4
0
好文,不过要恢复的话,只需要一句“su killall system_server”就让它歇菜了
雪    币: 193
活跃值: 活跃值 (17)
能力值: ( LV2,RANK:140 )
在线值:
发帖
回帖
粉丝
Passion 活跃值 3 2012-10-18 22:53
5
0
啊,你是LibInject的原作者?握手握手。好厉害的功力。
雪    币: 193
活跃值: 活跃值 (17)
能力值: ( LV2,RANK:140 )
在线值:
发帖
回帖
粉丝
Passion 活跃值 3 2012-10-18 22:55
6
0
是的。system_server被干掉或crash后会被init自动重启,原先的hook就没了,需要再找个时机得到system_server的启动通知再来hook它一下才行。我暂时没想到好的思路,不知有无建议?
雪    币: 14
活跃值: 活跃值 (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
Charlesccz 活跃值 2012-10-19 10:24
7
0
mark。有用标记一下
雪    币: 225
活跃值: 活跃值 (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
polelf 活跃值 2012-10-19 11:06
8
0
mark
雪    币: 199
活跃值: 活跃值 (25)
能力值: ( LV10,RANK:160 )
在线值:
发帖
回帖
粉丝
hellok 活跃值 3 2012-10-21 16:33
9
0
围观+1.好文感谢分享
雪    币: 3
活跃值: 活跃值 (11)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
踏歌长行 活跃值 2012-10-22 16:49
10
0
我勒个去啊,跪读了
雪    币: 4
活跃值: 活跃值 (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
loste 活跃值 2012-10-23 00:20
11
0
我想问下楼主怎么调试这段程序的,我还不知道怎么在android上方便的调试类似的程序
雪    币: 193
活跃值: 活跃值 (17)
能力值: ( LV2,RANK:140 )
在线值:
发帖
回帖
粉丝
Passion 活跃值 3 2012-10-23 14:14
12
0
我是通过打log来调试的。

#include <cutils/log.h>
#define LOG_TAG "inject"

代码中用LOGD("xxxxx %d", 0);这样输出。

然后链接时链接入liblog。

跑命令logcat -s inject 就能看见输出的xxxxx 0了。
雪    币: 442
活跃值: 活跃值 (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
shanguren 活跃值 2013-3-5 10:41
13
0
楼主您好,首先感谢您的分享。我按照您的方法实验了一下,有个问题想请教:

您提到 “写了一个新共享库让shellcode加载的共享库调用,以实现新共享库常驻system_server内存”,我想问一下这里具体是怎么实现常驻内存的?

为了描述方便,假设shellcode通过dlopen加载的共享库是libpayload.so,您提到的新共享库(也就是do_hook函数所在的共享库)的名称是libhook.so。  

我实验时是这样的: 生成libpayload.so时链接了libhook.so,然后libpayload.so中的hook_entry调用do_hook函数。
也就是:
shellcode        libpayload.so         libhook.so
dlopen 
                ---》  hook_entry
                                           ---》 do_hook
                                          《---
               《---
dlclose

但是我觉得这样也实现不了libhook.so常驻内存,因为dlclose卸载libpayload.so时候,应该会递归卸载掉libhook.so吧。
我按照这种方式实际实验的结果也是无法实现常驻,结果是用new_ioctl地址替换ioctl地址之后, system_server进程被重启。

注入程序查找和替换ioctl地址的实现部分是没有问题的,因为我尝试了如果shellcode中不调用dlclose卸载libpayload.so,则new_ioctl能够被调用。

所以想请问大家是如何实现libhook常驻system_server进程内存的?是否有简单的通用办法?谢谢
雪    币: 193
活跃值: 活跃值 (17)
能力值: ( LV2,RANK:140 )
在线值:
发帖
回帖
粉丝
Passion 活跃值 3 2013-3-5 20:14
14
0
楼上,dlclose不会递归卸载目标中显式加载的so,也就是说,A如果dlopen了B,B代码里dlopen了C,那么A来dlclose B的时候,应该不会影响到C的,除非B显式dlclose了C。

你说的递归卸载,是隐含链接的形式,如果A在编译期就指定了运行时要被动态链接B(不是显式dlopen),B要链C,那么A起来时会递归加载B,B起来时也会递归加载C,A退出时会递归卸载B再递归卸载C。

你碰到的system_server被重启的情况,可能是其他原因crash了导致被系统重启?
雪    币: 442
活跃值: 活跃值 (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
shanguren 活跃值 2013-3-5 23:31
15
0
谢谢您, 我试了一下,您说的是对的, libpayload 显式链接 libhook ,并且不主动dlclose卸载libhook,libhook就保留在内存了。
我之前是隐式链接的,libpayload被卸载的时候导致libhook也被卸载,system_server执行new_ioctl的时候,这个函数不在内存了,导致system_server崩溃。

另外不知道您后来对Binder通信有没有进一步研究, 我觉得在system_server进程中没法通过解析Binder通信数据了解客户端应用在请求哪个服务的什么功能。因为system_server进程中有多个Service线程,例如ActivityManager,PowerManagerService等等,通过拦截ioctl得到binder_transaction_data后,目前根据其中的Binder对象指针似乎不能找到其对应的Service,也就不能解析出通信数据中命令码(binder_transaction_data中的code成员)是对应哪个Service的哪个函数。
我对Binder的理解还不是很透彻,不知道这个理解是否正确?
雪    币: 200
活跃值: 活跃值 (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
eoc 活跃值 2013-3-9 15:07
16
0
几大主流Android安软早就已经放弃从C层做binder hook了,Java层的hook可以拿到更多的信息,也更安全,建议考虑一下。
雪    币: 111
活跃值: 活跃值 (72)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
netsniffer 活跃值 2013-3-10 11:47
17
0
这些service binder调用格式是固定的,从Parcel数据中可以取出service discribetor,可以知道是哪个service,进而通过transaction的code可以得知是该service的哪个函数
去看看android_util_binder.cpp transact函数吧
雪    币: 13
活跃值: 活跃值 (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
小昆仑 活跃值 2013-3-13 11:17
18
0
楼主第一段代码里的 BINDER_WRITE_READ 怎么定义的(在自己的代码中)?
雪    币: 442
活跃值: 活跃值 (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
shanguren 活跃值 2013-3-13 15:46
19
0
谢谢您的回复。
你说的是framework/base/core/jni/android_util_Binder.cpp 中的 android_os_BinderProxy_transact函数么?
我看了这个函数但是还是看不出请求一个Service时候的具体格式,比如第一个字段是什么,第二个字段是什么。

后来看到这个文件中android_os_Parcel_writeInterfaceToken函数的一句注释
“ In the current implementation, the token is just the serialized interface name that the caller expects to be invoking” 提示了一些信息。

然后结合service_manager.c中的svcmgr_handler函数和拦截的消息,目前猜测是:
4字节 strict_policy_mask(作用未知),然后是4字节interface_name的长度,后面跟着interface_name字符串。在后面应该是code对应的函数的参数。

不过不知道哪里有文档定义了这些格式。
雪    币: 442
活跃值: 活跃值 (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
shanguren 活跃值 2013-3-13 17:00
20
0
在binder驱动头文件里binder.h
雪    币: 40
活跃值: 活跃值 (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
ysguet 活跃值 2013-4-10 14:35
21
0
楼主您好,我是一个菜鸟,想问下文中提到的:打开/system/lib/libbinder.so文件,获取其ELF头。
read(fd, ehdr, sizeof(Elf32_Ehdr));
请问下:是通过什么函数打开/system/lib/libbinder.so文件的呢?怎样获得了fd。
雪    币: 3
活跃值: 活跃值 (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
RunkSun 活跃值 2013-4-10 15:44
22
0
好文章,顶一下
雪    币: 40
活跃值: 活跃值 (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
ysguet 活跃值 2013-4-10 21:09
23
0
楼主您好,我是一个菜鸟,想问下文中提到的:打开/system/lib/libbinder.so文件,获取其ELF头。
read(fd, ehdr, sizeof(Elf32_Ehdr));
请问下:是通过什么函数打开/system/lib/libbinder.so文件的呢?怎样获得了fd。
雪    币: 0
活跃值: 活跃值 (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
hellouv 活跃值 2013-4-15 21:46
24
0
楼主你好,我是人大的大三学生,我正在自己学习截获ioctl命令的相关知识,看到你的文章,觉得对我的帮助非常大,很想认真的学习一下,所以想请问你能不能传给我你的源代码?文章上的大都是片段,理解起来有困难,我的邮箱是zhoumeizi311@126.com,如果可以的话,我将十分感谢。ps。仅用于自己学习。
雪    币: 30
活跃值: 活跃值 (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
NISL 活跃值 2013-5-29 10:27
25
0
好文,顶你没有理由!!!
游客
登录 | 注册 方可回帖
返回