一:概述
在Linux開發過程中,我們會遇到這樣的情況,明明可執行程式elf與共享庫so在同一個目錄,但是進入此目錄執行./elf 卻會提示so找不到?這種情況對於從Windows平臺過渡過來的程式設計師是比較費解的。這裡其實就涉及到Linux上可執行程式搜尋需要的庫的一個範圍和順序問題。首先,是範圍問題,我們可以透過查詢ld的說明文件知曉範圍。其次,是順序問題,如果第三方程式製作了一個與系統庫同名的so庫,並把它放在了優先載入的位置,即優先於系統庫目錄的位置,定然就會發生劫持,這是一個風險點。
網路上比較流行的Linux動態庫劫持的方式是透過/etc/ld.so.preload或者環境變數LD_PRELOAD來劫持的,這種方式很容易被發現,因為它會影響所有後續啟動的程式,比如有些挖礦程式隱身的時候就使用了此技術,這種方式我們先不談,本文,我們談一談庫載入順序可能導致的風險,這個風險不容易被發現,但值得警惕。
二:so共享庫執行時載入順序驗證
1)準備
Linux共享庫的執行時載入順序為:
1:環境變數LD_LIBRARY_PATH指定的路徑
2:連線時 -rpath指定的共享庫查詢路徑
3:ldconfig 配置檔案ld.so.conf指定的路徑
4:/lib
5:/usr/lib
因為Windows搜尋dll的路徑會搜尋本地目錄和PATH路徑,我們也捎帶測試一下這兩種情況。
在作者的機器上,/lib為/usr/lib的軟連結,
所以,我們只測試前四種情況+PATH+本地
為了公平驗證所有執行時載入順序,
我們在這幾個目錄都放置好同名但print輸出不同的so庫檔案,-rpath指定好主程式的載入目錄,配置好PATH路徑,配置好ld.so.conf。執行主程式,檢視輸出結果,不斷刪除起作用的項,可逐一驗證執行時載入順序:
編寫七個檔案如下
user@kali:~/pro$ cat a1.c
#include <stdio.h>
void myprint()
{
printf("Hello a1[LD_LIBRARY_PATH]\n");
}
user@kali:~/pro$ cat a2.c
#include <stdio.h>
void myprint()
{
printf("Hello a2[rpath]\n");
}
user@kali:~/pro$ cat a3.c
#include <stdio.h>
void myprint()
{
printf("Hello a3[ld.so.conf]\n");
}
user@kali:~/pro$ cat a4.c
#include <stdio.h>
void myprint()
{
printf("Hello a4[/lib--/usr/lib]\n");
}
user@kali:~/pro$ cat a5.c
#include <stdio.h>
void myprint()
{
printf("Hello a5 PATH\n");
}
user@kali:~/pro$ cat a6.c
#include <stdio.h>
void myprint()
{
printf("Hello a6 local\n");
}
user@kali:~/pro$ cat main.c
#include <stdio.h>
extern void myprint();
int main()
{
myprint();
return 0;
}
編譯和設定,步驟如下:
// 建立資料夾
user@kali:~/pro$ mkdir dir_ld_conf dir_path dir_ld_path dir_rpath
其中:
dir_ld_conf 用來驗證ld.so.conf的作用
dir_ld_path 用來驗證LD_LIBRARY_PATH的作用
dir_path 用來驗證環境變數PATH的作用
dir_rpath 用來驗證連結時rpath的作用
// a1.c 驗證LD_LIBRARY_PATH
user@kali:~/pro$ gcc -shared -o ./dir_ld_path/liba.so a1.c
user@kali:~/pro$ export LD_LIBRARY_PATH=/home/user/pro/dir_ld_path
user@kali:~/pro$ echo $LD_LIBRARY_PATH
/home/user/pro/dir_ld_path
// a2.c 驗證rpath
user@kali:~/pro$ gcc -shared -o ./dir_rpath/liba.so a2.c
// a3.c 驗證ld.so.conf
user@kali:~/pro$ gcc -shared -o ./dir_ld_conf/liba.so a3.c
// 編輯,加入/home/user/pro/dir_ld_conf
user@kali:~/pro/dir_ld_conf$ sudo vim /etc/ld.so.conf
// 重新整理快取
user@kali:~/pro/dir_ld_conf$ sudo ldconfig
// a4.c 驗證/lib /usr/lib
user@kali:~/pro$ sudo gcc -shared -o /lib/liba.so a4.c
// a5.c 驗證PATH
user@kali:~/pro$ gcc -shared -o ./dir_path/liba.so a5.c
user@kali:~/pro$ export PATH=/home/user/pro/dir_path:$PATH
user@kali:~/pro$ echo $PATH
/home/user/pro/dir_path:/usr/local/sbin:/usr/sbin:/sbin:/usr/local/bin:/usr/bin:/bin:/usr/local/games:/usr/games
// a6.c 驗證本地
user@kali:~/pro$ gcc -shared -o liba.so a6.c
// 編譯主程式[帶rpath]
user@kali:~/pro$ gcc -o main main.c -L. -la -Wl,-rpath,dir_rpath
此時目錄結構如下:
我們看一下動態節.dynamic的內容:
可見rpath指定的路徑已經被寫入了可執行檔案中。
2)手工驗證
一切就緒,我們開始驗證:
user@kali:~/pro$ ./main
Hello a1[LD_LIBRARY_PATH]
可見 LD_LIBRARY_PATH第一個起作用
刪除dir_ld_path之後
user@kali:~/pro$ ./main
Hello a2[rpath]
可見-rpath連線選項第二個起作用
刪除dir_rpath之後
user@kali:~/pro$ ./main
Hello a3[ld.so.conf]
可見ld.so.conf配置檔案第三個起作用
刪除 dir_ld_conf之後
user@kali:~/pro$ ./main
Hello a4[/lib--/usr/lib]
可見 /lib 目錄生效,第四個起作用
刪除/lib/liba.so之後
user@kali:~/pro$ ./main
./main: error while loading shared libraries: liba.so: cannot open shared object file: No such file or directory
可見只有這四個可以起作用,我們設定的PATH中的路徑和本地路徑都沒有起作用。
如果感興趣的話,也可以用strace追蹤一下main的系統呼叫過程,也能發現呼叫過程是這樣的順序。
至此,可以得出結論:Linux的共享庫so的尋找順序為:
1:LD_LIBRARY_PATH
2:-rpath連線選項
3:ld.so.conf配置檔案
4:/lib和/usr/lib
3)原始碼驗證
既然Linux是開源系統,最權威的驗證方式,當然是原始碼了,庫的尋找順序,系統是交給動態連結器來管理的,Linux的ELF動態連結器是Glibc的一部分,作者從glibc-2.29版本中的dl-load.c檔案中找到這麼一個函式記載了執行時載入順序的尋找過程:
struct link_map *
_dl_map_object (struct link_map *loader, const char *name,
int type, int trace_mode, int mode, Lmid_t nsid)
幾個主要的程式碼片段如下:
最開始是RPATH與RUNPATH同時存在時的處理方式,RPATH是舊式的編譯器用的方式,RUNPATH是最新的編譯器支援的方式,這兩種方式可以透過在連結時指定–enable-new-dtags/–disable-new-dtags來控制。
總體的處理邏輯為:
if(物件沒有RUNPATH) {
if(物件有RPATH){
使用RPATH
} else {
遞迴查詢載入者(loader)的RPATH(或者有RUNPATH退出)
}
if(可執行程式沒有RUNPATH) {
使用可執行程式的RPATH
}
}
查詢LD_LIBRARY_PATH
查詢正被載入物件的RUNPATH
查詢ld.so.cache
查詢預設路徑
作者的編譯器是最新的,預設就會使用RUNPATH。
從原始碼也可以看出,我們之前的驗證是正確的。
使用–disable-new-dtags或舊式編譯器的人可能會發現有時候-rpath優先於LD_LIBRARY_PATH,原因就在於程式進入了RUNPATH與RPATH的處理邏輯。可以使用readelf -d 檢視動態節到底有沒有RPATH或RUNPATH來進行分析
三:執行時載入順序可能的風險分析
明白了這些執行時載入順序,最後簡單概括一下:
1:LD_LIBRARY_PATH的環境變數的影響範圍是全域性的,同LD_PRELOAD影響一樣,會有風險點,過多的使用可能會影響到其他應用程式的執行,所以多用於除錯模式。
2:-rpath連結選項是程式生成時指定的,一般程式執行前都已經生成了,所以這項暫時構成不了威脅。
3:ld.so.conf配置檔案與LD_LIBRARY_PATH一樣,都有同樣的風險
4:/lib和/usr/lib 是系統檔案,所屬許可權屬於root,因為每個so庫在各Linux系統中的位置有差異,要在這個位置預防so動態庫劫持,就需要對庫的位置進行精確定位,精準攔截。