Last Updated: 2023-07-13 01:56:09 Thursday
-- TOC --
动态链接的程序,在程序启动时,首先运行的是动态链接器(runtime dynamic linker),检查程序所需要的动态库文件并加载到进程的虚拟地址空间,然后才将控制权交给程序入口。
LD_PRELOAD
这个环境变量,影响的就是动态链接的顺序。被LD_PRELOAD指定的动态库会被优先加载,因此,这个机制可以被用来劫持程序。
通过这个环境变量,我们可以在主程序和其依赖的动态链接库的中间,加载别的动态链接库,或者覆盖正常的函数库接口。一方面,我们可以用此功能来动态地改变程序所使用的函数接口(无需程序源码),而另一方面,我们也可以以向程序注入恶意代码,从而达到某些那不可告人的目的。
动态加载时,Linux系统有一个细节,当出现相同符号的时候,后面出现的符号将会被忽略!因此,这个环境变量是preload!
先写一段代码:
$ cat t_strcmp.c
#include <stdio.h>
#include <string.h>
int main(void) {
char passwd[] = "abcd1234";
if (!strcmp(passwd, "1234abcd")) {
printf("Correct Password!\n");
return 0;
}
printf("Invalid Password!\n");
return 1;
}
$ gcc t_strcmp.c -o t_strcmp
$ ./t_strcmp
Invalid Password!
这段测试代码,正常情况下会提示不正确的密码。默认情况下,gcc编译采用动态链接,strcmp这个标准C库接口来自libc.so.6。
下面是hack代码:
$ cat t_hack.c
#include <stdio.h>
#include <string.h>
int strcmp(const char *s1, const char *s2) {
printf("in hack strcmp: s1=<%s>,s2=<%s>\n", s1, s2);
return 0; // always return success!!
}
$ gcc -fPIC -shared t_hack.c -o t_hack.so
$ LD_PRELOAD=./t_hack.so ./t_strcmp
in hack strcmp: s1=<abcd1234>,s2=<1234abcd>
Correct Password!
看~~!!比较密码就成功了......并且还将作比较的两个字符串都打印了出来!
还有一种劫持,只是在调用路径上插入一些代码:
$ cat t_hack2.c
#include <stdio.h>
#include <string.h>
#include <dlfcn.h>
int (*real_strcmp)(const char *s1, const char *s2);
int strcmp(const char *s1, const char *s2) {
char *err;
real_strcmp = dlsym(RTLD_NEXT, "strcmp");
if ((err=dlerror()) != NULL) {
printf("dlsym strcmp: %s\n", err);
return 1;
}
printf("in hack strcmp: s1=<%s>,s2=<%s>\n", s1, s2);
return real_strcmp(s1, s2);
}
编译:
$ man dlsym
$ gcc -fPIC -shared t_hack2.c -o t_hack2.so -ldl -D_GNU_SOURCE
$ ldd t_hack2.so
linux-vdso.so.1 (0x00007fffa43a3000)
libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f44ae004000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f44ade12000)
/lib64/ld-linux-x86-64.so.2 (0x00007f44ae024000)
-ldl
不能放在前面,否则不会链接这个libdl.so.2
库。嗨.....
RTLD_NEXT
,在当前搜索顺序中找,在当前的目标之后,找符号下一次出现的地方。 这就允许向在另一个共享目标中的函数提供一层封装。这样一来,在一个预先加载的共享目标中定义的函数中,就可以找到并调用在另一个共享目标中的真函数(其实就是一种劫持)或者有多层的预加载的时候的下一层!
这段劫持代码,目的就是将代码中调用strcmp的参数打印出来,然后还是调用真正的strcmp接口。运行效果:
$ LD_PRELOAD=./t_hack2.so ./t_strcmp
in hack strcmp: s1=<abcd1234>,s2=<1234abcd>
Invalid Password!
程序还是正常执行,但是不知不觉就把密码暴露出来了....
$ cat preload.c
#include <dlfcn.h>
#include <unistd.h>
#include <sys/types.h>
uid_t geteuid( void ) { return 0; }
uid_t getuid( void ) { return 0; }
uid_t getgid( void ) { return 0; }
重载几个系统调用接口,可以让你感觉自己好像是root:
$ gcc -fPIC -shared preload.c -o preload.so
$ whoami
xinlin
$ LD_PRELOAD=./preload.so whoami
root
据说曾经的那个著名的攻击是这样的:
$ telnet
telnet> env def LD_PRELOAD /home/hchen/test/preload.so
telnet> open localhost
#
当然,这个安全BUG早已被Fix了(虽然,通过id或是whoami或是/bin/sh让你觉得你像是root,但其实你并没有root的权限),当今的Unix系统中不会出现这个的问题。但这并不代表,我们自己写的程序,或是第三方的程序能够避免这个问题,尤其是那些以Root方式运行的第三方程序。
看到两个避免LD_PRELOAD带来隐患的思路:
这些机制本身并不恶,就看怎么用。
看到一个小项目,通过LD_PRELOAD机制,重载了socket的bind和connect函数,以非侵入式的方式(不修改源代码),给socket设置上了SO_REUSEADDR或SO_REUSEPORT参数:https://github.com/yongboy/bindp
本文链接:https://cs.pynote.net/se/202203301/
-- EOF --
-- MORE --