说到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/目录下。
图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技术来说,这是最简单的,其它的方法我会在以后陆续与大家分享。另外,本人水平有限,如果文中有任何表述不当或错误的地方,还望斧正。