LD_PRELOAD是Linux系統的一個環境變量,它可以影響程序的運行時的鏈接(Runtime linker),它允許你定義在程序運行前優先加載的動態鏈接庫。這個功能主要就是用來有選擇性的載入不同動態鏈接庫中的相同函數。通過這個環境變量,我們可以在主程序和其動態鏈接庫的中間加載別的動態鏈接庫,甚至覆蓋正常的函數庫。一方面,我們可以以此功能來使用自己的或是更好的函數(無需別人的源碼),而另一方面,我們也可以以向別人的程序注入程序,從而達到特定的目的。
一般情況下,其加載順序為LD_PRELOAD>LD_LIBRARY_PATH>/etc/ld.so.cache>/lib>/usr/lib。幾米夜空轉載的文章《LD_PRELOAD作用》程序調用流圖和代碼例子值得一看。
1. 簡單舉例
我們以Rafał Cieślak的一篇文章的例子為例說明它的用法,翻譯自Rafał Cieślak的博客,譯者qhwdw。
random_num.c:
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int main() {
srand(time(NULL));
int i = 10;
while(i--)
printf("%d\n", rand()%100);
return 0;
}
我不使用任何參數來編譯它,如下所示:
gcc random_num.c -o random_num
我希望它輸出的結果是明確的:從 0-99 中選擇的十個隨機數字,希望每次你運行這個程序時它的輸出都不相同。
現在,讓我們假裝真的不知道這個可執行程序的出處。甚至將它的源文件刪除,或者把它移動到別的地方 —— 我們已不再需要它了。我們將對這個程序的行為進行重大的修改,而你並不需要接觸到它的源代碼,也不需要重新編譯它。
因此,讓我們來創建另外一個簡單的 C 文件:
unrandom.c:
int rand() {
// the most random number in the universe
return 42;
}
我們將編譯它進入一個共享庫中:
gcc -shared -fPIC unrandom.c -o unrandom.so
因此,現在我們已經有了一個可以輸出一些隨機數的應用程序,和一個定制的庫,它使用一個常數值 42 實現了一個 rand() 函數。現在……就像運行 random_num 一樣,然后再觀察結果:
LD_PRELOAD=$PWD/unrandom.so ./random_num
如果你想偷懶或者不想自動親自動手(或者不知什么原因猜不出發生了什么),我來告訴你 —— 它輸出了十次常數 42。
如果先這樣執行:
export LD_PRELOAD=$PWD/unrandom.so
然后再以正常方式運行這個程序,這個結果也許會更讓你吃驚:一個未被改變過的應用程序在一個正常的運行方式中,看上去受到了我們做的一個極小的庫的影響……
當我們的程序啟動后,為程序提供所需要的函數的某些庫被加載。我們可以使用 ldd 去學習它是怎么工作的:
ldd random_num
linux-vdso.so.1 => (0x00007fff4bdfe000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f48c03ec000)
/lib64/ld-linux-x86-64.so.2 (0x00007f48c07e3000)
它列出了被程序 random_num 所需要的庫的列表。這個列表是構建進可執行程序中的,並且它是在編譯時決定的。在你的機器上的具體的輸出可能與示例有所不同,但是,一個 libc.so 肯定是有的 —— 這個文件提供了核心的 C 函數。它包含了 “真正的” rand()。
我使用下列的命令可以得到一個全部的函數列表,我們看一看 libc 提供了哪些函數:
nm -D /lib/libc.so.6
這個 nm 命令列出了在一個二進制文件中找到的符號。-D 標志告訴它去查找動態符號,因為 libc.so.6 是一個動態庫。這個輸出是很長的,但它確實在列出的很多標准函數中包括了 rand()。
現在,在我們設置了環境變量 LD_PRELOAD 后發生了什么?這個變量為一個程序強制加載一些庫。在我們的案例中,它為 random_num 加載了 unrandom.so,盡管程序本身並沒有這樣去要求它。下列的命令可以看得出來:
LD_PRELOAD=$PWD/unrandom.so ldd random_nums
linux-vdso.so.1 => (0x00007fff369dc000)
/some/path/to/unrandom.so (0x00007f262b439000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f262b044000)
/lib64/ld-linux-x86-64.so.2 (0x00007f262b63d000)
注意,它列出了我們當前的庫。實際上這就是代碼為什么得以運行的原因:random_num 調用了 rand(),但是,如果 unrandom.so 被加載,它調用的是我們所提供的實現了 rand() 的庫。
2. 打開文件顯示路徑
編寫inspect_open.c:
#define _GNU_SOURCE
#include <dlfcn.h>
#include <stdio.h>
typedef int (*orig_open_f_type) (const char *pathname, int flags);
int open(const char *pathname, int flags, ...)
{
// remember to include stdio.h!
printf("open():%s\n", pathname);
/* Some evil injected code goes here. */
orig_open_f_type orig_open;
orig_open = (orig_open_f_type) dlsym(RTLD_NEXT, "open");
return orig_open(pathname, flags);
}
FILE *fopen(const char *path, const char *mode) {
printf("fopen():%s\n", path);
FILE* (*original_fopen) (const char*, const char*);
original_fopen = dlsym(RTLD_NEXT, "fopen");
return (*original_fopen)(path, mode);
}
編譯為動態庫:
gcc -shared -fPIC inspect_open.c -o inspect_open.so -ldl
編寫讀取文件的例子test.c:
#include<stdlib.h>
#include<stdio.h>
int main()
{
FILE*fp;
fp=fopen("file01.txt","r");
if(fp==NULL)
{
printf("Can not openthe file!\n");
exit(0);
}
fclose(fp);
return 0;
}
編譯:
gcc test.c -o test
加載庫運行:
LD_PRELOAD=$PWD/inspect_open.so ./test
結果如下,成功注入了函數,打印文件路徑:
inspect_open.c中還重寫了open,不過這個實現是不完美的,使用會導致異常。有一些參考文章可以學習: