首页
社区
课程
招聘
[原创]Android下通过root实现对system_server中binder的ioctl调用拦截
发表于: 2012-10-18 13:53 223544

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

2012-10-18 13:53
223544

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;
    }
    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);
   read(fd, ehdr, sizeof(Elf32_Ehdr));

[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

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

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

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

然后链接时链接入liblog。

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

您提到 “写了一个新共享库让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进程内存的?是否有简单的通用办法?谢谢
2013-3-5 10:41
0
雪    币: 216
活跃值: (53)
能力值: ( LV2,RANK:140 )
在线值:
发帖
回帖
粉丝
14
楼上,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了导致被系统重启?
2013-3-5 20:14
0
雪    币: 442
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
15
谢谢您, 我试了一下,您说的是对的, 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的理解还不是很透彻,不知道这个理解是否正确?
2013-3-5 23:31
0
雪    币: 200
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
eoc
16
几大主流Android安软早就已经放弃从C层做binder hook了,Java层的hook可以拿到更多的信息,也更安全,建议考虑一下。
2013-3-9 15:07
0
雪    币: 53
活跃值: (280)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
17
这些service binder调用格式是固定的,从Parcel数据中可以取出service discribetor,可以知道是哪个service,进而通过transaction的code可以得知是该service的哪个函数
去看看android_util_binder.cpp transact函数吧
2013-3-10 11:47
0
雪    币: 13
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
18
楼主第一段代码里的 BINDER_WRITE_READ 怎么定义的(在自己的代码中)?
2013-3-13 11:17
0
雪    币: 442
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
19
谢谢您的回复。
你说的是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对应的函数的参数。

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