-
-
[转帖]Android下通过root实现对system_server中binder的ioctl调用拦截
-
发表于: 2013-4-12 13:41 1291
-
转自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供外界使用:
[cpp] view plaincopy
// 将新旧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函数后,注入程序需要循环等待属性被设置上,类似于:
[cpp] view plaincopy
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中提供的)。
[cpp] view plaincopy
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头。
[cpp] view plaincopy
read(fd, ehdr, sizeof(Elf32_Ehdr));
获得其Section Header区表的地址、数量和大小,并获得字符串表Section的索引号:
[cpp] view plaincopy
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用:
[cpp] view plaincopy
// 读取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:
[cpp] view plaincopy
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就好办了:
[cpp] view plaincopy
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
还有不少,没法全写下来,在此向所有无私共享自己研究成果的人致以诚挚的谢意。
〇、引言
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供外界使用:
[cpp] view plaincopy
// 将新旧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函数后,注入程序需要循环等待属性被设置上,类似于:
[cpp] view plaincopy
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中提供的)。
[cpp] view plaincopy
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头。
[cpp] view plaincopy
read(fd, ehdr, sizeof(Elf32_Ehdr));
获得其Section Header区表的地址、数量和大小,并获得字符串表Section的索引号:
[cpp] view plaincopy
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用:
[cpp] view plaincopy
// 读取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:
[cpp] view plaincopy
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就好办了:
[cpp] view plaincopy
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
还有不少,没法全写下来,在此向所有无私共享自己研究成果的人致以诚挚的谢意。
赞赏
他的文章
看原图
赞赏
雪币:
留言: