探索黑客技术攻防,实战研究与安全创新

导航菜单

AndroidAPIHook之LD_PRELOAD

说到APIHook,Windows平台的朋友肯定再熟悉不过了。在Windows平台下,很多软件都用到了Hook技术,其中最常见到的就属安全软件了,为了优先获得系统的控制权,杀软们三三两两的都来Hook,从最初纯洁的R3下消息钩子到后来的R0下的NativeAPIHook,Windows平台下的Hook正泛滥着。在Linux平台下,Hook技术由于系统架构的不同导致实现起来比较困难,但Linux也有自己的特点,部分开发人员经过研究,同样挖掘出了一些可行的Hook方案。

现在Android来了,它带来了一种全新的软件开发模式,如何在Android平台下使用Hook技术也是很多开发者十分关注的话题,尽管Android与众不同,但它身上依然流淌着Linux的血液,因此也继承了Linux的部分特性,如Hook技术。围绕这个话题,本文将与大家探讨LInux平台下Hook技术之一LD_PRELOAD在Android中是如何上实现的。

LD_PRELOAD原理

LD_PRELOAD并不是什么深不可测的东西,它只是Linux系统中的一个环境变量。理解LD_PRELOAD的作用还得从程序的链接说起。和Windows平台一样,Linux平台的程序也分成静态链接与动态链接两种:静态链接把所有引用到的函数全部地编译进可执行文件中,程序在运行时可以直接调用;动态链接则没有把函数编译到可执行文件中,而是在程序运行时动态地载入函数库。在动态链接时,会需要查找动态链接库所在的位置,以便从动态链接库中查找函数的地址供程序使用。对于动态链接的程序来说,这个动态链接库具体在哪里?函数地址如何计算程序自己都不用关心,这个工作交给程序的加载器来完成。

系统提供动态链接使多个程序可以共用相同的代码,节省了程序占用的磁盘空间,同时,在不重新编译程序的情况下,升级动态库中的函数就可以升级整个程序,大大降低了开发人员的维护成本。然而事情总是有好有坏,如果动态链接时动态加载的函数不是开发人员所编写的,而是别有用心的人所写的代码,那程序的流程或返回值可能就会被恶意的修改,也就是说,你的程序被Hook了。

LD_PRELOAD就是这样存在的,它可以影响程序的运行时的动态链接。只需通过它预先指定想要加载的动态链接库文件,程序在运行时,系统就会优先加载这个动态链接库,从而覆盖掉正常的动态链接库,一个巧妙“偷梁换柱”手法。说到这里,很多朋友可能会想到,可以在LD_PRELOAD变量中指定Hook的动态链接库,在动态链接库中提供想Hook的函数,在程序被执行时,这个Hook函数就会替代原函数被执行!在Linux平台下确实如此,然而在Android平台下也会是这样吗?

Android中的LD_PRELOAD

Android的动态链接加载器是/system/bin/linker。后者主要负责初始化Android原生程序运行环境,包括加载程序所引用的动态链接库,修正函数调用地址,执行程序入口函数等。为了搞清楚LD_PRELOAD变量在Android中是否有效,去Android源码中看看。

linker的源码位于bionic\linker目录下,主程序文件为linker.c。打开该文件查找LD_PRELOAD,可以发现如下代码:

android远程监控技术


/*Sanitizeenvironmentifwe'reloadingasetuidprogram*/
if(program_is_setuid)
linker_env_secure();
debugger_init();
/*Getafewenvironmentvariables*/
{
constchar*env;
env=linker_env_get(DEBUG);/*XXX:TODO:ChangetoLD_DEBUG*/
if(env)
debug_verbosity=atoi(env);
/*Normally,thesearecleanedbylinker_env_secure,butthetest
*againstprogram_is_setuiddoesn'tcostusanything*/
if(!program_is_setuid){
ldpath_env=linker_env_get(LD_LIBRARY_PATH);
ldpreload_env=linker_env_get(LD_PRELOAD);
}
}


linker_env_secure()函数的代码位于linker_environ.c文件中,主要功能是清除掉系统认为危险的环境变量,这里包括LD_PRELOAD,因此,前三行代码意味着如果执行的是setuid()函数,则LD_PRELOAD环境变量是无效的!相反,如果program_is_setuid为假时,LD_LIBRARY_PATH与LD_PRELOAD环境变量就生效了,这时LD_PRELOAD环境变量的值赋给了

ldpreload_env。往后继续看,可以发现如下代码:


if(ldpath_env)
parse_library_path(ldpath_env,:);
if(ldpreload_env){
parse_preloads(ldpreload_env,:);
}
if(link_image(si,0)){
charerrmsg[]=CANNOTLINKEXECUTABLE\n;
write(2,__linker_dl_err_buf,strlen(__linker_dl_err_buf));
write(2,errmsg,sizeof(errmsg));
exit(-1);
}


parse_library_path()与parse_preloads()分别解析库加载路径与LD_PRELOAD库路径,LD_PRELOAD中所有的库文件路径是以冒号“:”分隔的,在parse_preloads()函数中它们被分隔并保存到了一个全局的缓冲区ldpreloads_buf中,这个缓冲区被设定为512个字节,LD_PRELOAD环境变量中指定的库的个数最多为8个,parse_preloads()代码如下:


staticvoidparse_preloads(constchar*path,char*delim)
{
size_tlen;
char*ldpreloads_bufp=ldpreloads_buf;
inti=0;
len=strlcpy(ldpreloads_buf,path,sizeof(ldpreloads_buf));
while(iLDPRELOAD_MAX&&(ldpreload_names[i]=strsep(&ldpreloads_bufp,


android远程监控技术


delim))){
if(*ldpreload_names[i]!='\0'){
++i;
}
}
if(i0&&len=sizeof(ldpreloads_buf)&&
ldpreloads_buf[sizeof(ldpreloads_buf)-2]!='\0'){
ldpreload_names[i-1]=NULL;
}else{
ldpreload_names[i]=NULL;
}
}


在库文件搜索并设置完成后,调用link_image()完成真正的链接工作。link_image()函数的代码很肥硕,主要的工作是加载动态链接库,修正函数地址等,详细的不跟了。到这里也明白了,LD_PRELOAD环境变量在Android中真的有!

编写测试代码

编写代码部分相对比较简单了,与编写Linux的c程序一样,我们先写一个正常的程序,代码如下:


/*app.c*/
#includestdio.h
#includestring.h
#defineboolint
boolcheckSN(constchar*sn){
charrealcode[]=ilikeandroid;
return!(strcmp(realcode,sn));
}
voidmain(intargc,char**argv){
if(argc2){
printf(pleaseinputyoursn);
return;
}
if(!checkSN(argv[1])){
printf(thesnwasinvalid!\n);
return;
}
printf(thanksforregistion!\n);
}


这段代码简单的模拟了注册码验证。我们现在要Hook这个验证函数,让程序输入任意字符串都提示注册成功!编写hook.c代码如下:


/*hook.c*/
#includestdio.h
#includedlfcn.h
void*getrealaddr(constchar*s1,constchar*s2){
void*lib=dlopen(libc.so,RTLD_NOW|RTLD_GLOBAL);
void*symbol;
if(lib==NULL){
fprintf(stderr,
Could
not
open
self-executable
with
dlopen(NULL)!!:%s\n,dlerror());
return-1;
}
symbol=dlsym(lib,strcmp);
fprintf(stderr,therealstrcmpaddress:0x%8x\n,symbol);
dlclose(lib);
returnsymbol;
}
intstrcmp(constchar*s1,constchar*s2){
fprintf(stderr,thefirstargis:%s,thesecondargis:%s\n,s1,s2);
getrealaddr(s1,s2);
return0;
}


hook.c中提供了strcmp()函数,它首先打印了输入参数,然后获取真正的strcmp()函数的地址并输出,函数最后返回0表示字符串s1与s2相同。Hook代码有一点需要注意,要想Hook的函数不能在其它文件中被定义,否则在编译时会报“error:conflictingtypesforXXX”的错误。

正常程序与Hook程序都使用AndroidNDK来编译,将AndroidNDKSample目录下的Hello-jni目录复制一份,并重命名为hook,修改jni目录下的脚本文件Android.mk内容如下:


LOCAL_PATH:=$(callmy-dir)
#modulehook
include$(CLEAR_VARS)
LOCAL_MODULE
:=hook
LOCAL_SRC_FILES:=hook.c
include$(BUILD_SHARED_LIBRARY)
#moduleapp
include$(CLEAR_VARS)
LOCAL_MODULE
:=app
LOCAL_SRC_FILES:=app.c
include$(BUILD_EXECUTABLE)


脚本共生成两个模块,分别是app可执行程序与libhook.so动态链接库。打开CMD窗口,进入hook目录,输入ndk-build编译工程,代码无误会输出如图1所示,生成的可执行程序位于libs/armeabi/目录下。

001.png

图1使用AndroidNDK编译程序

编译成功后开启模拟器或连接真机来测试效果。为了少打几行字,编写自动部署批处理install.bat内容如下:


@echooff
adbpushlibs\armeabi\app/data/local/tmp/
adbpushlibs\armeabi\libhook.so/data/local/tmp/
adbshellchmod777/data/local/tmp/app
adbshellchmod777/data/local/tmp/libhook.so
adbshell


运行install.bat,依次输入以下几行命令查看执行结果:


cd/data/local/tmp
./app12345
exportLD_PRELOAD=/data/local/tmp/libhook.so
./app12345
./app54321


在输入export这行代码前,程序的输出如下:


#./app12345
./app12345
thesnwasinvalid!


在输入export这行代码后,程序的输出如下:


#./app12345
./app12345
thefirstargis:ilikeandroid,thesecondargis:12345
therealstrcmpaddress:0xafd37a41
thanksforregistion!
#./app54321
./app54321
thefirstargis:ilikeandroid,thesecondargis:54321
therealstrcmpaddress:0xafd37a41
thanksforregistion!


可见Hook代码成功起作用了!

最后有几点需要注意:

1.在linker中,LD_PRELOAD的环境变量保存在一个全局的缓冲区中的,Hook的函数在整个程序的执行会话中都是有效的,但不影响其它的会话。

2.Hook的函数可以是系统API,也可以是其它动态链接库的函数。

3.Hook系统API时,在Hook的代码中不能调用这个被Hook的函数,否则程序会异常退出。

4.想要使Hook对整个系统会话生效,可以在Android系统启动配置文件中init.rc中声明LD_PRELOAD环境变量,目前发现有些软件是这么做的。

总结

本文主要分析并实现了Android平台下使用LD_PRELOAD来完成HookAPI的方法。对于Android平台的Hook技术来说,这是最简单的,其它的方法我会在以后陆续与大家分享。另外,本人水平有限,如果文中有任何表述不当或错误的地方,还望斧正。