從原始碼分析:Linux共享庫安全風險剖析 之 執行時載入順序風險

安芯網盾發表於2020-04-15

一:概述

       在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的軟連結,

從原始碼分析:Linux共享庫安全風險剖析 之 執行時載入順序風險

所以,我們只測試前四種情況+PATH+本地

從原始碼分析:Linux共享庫安全風險剖析 之 執行時載入順序風險

為了公平驗證所有執行時載入順序,

我們在這幾個目錄都放置好同名但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 

從原始碼分析:Linux共享庫安全風險剖析 之 執行時載入順序風險

// 重新整理快取

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

從原始碼分析:Linux共享庫安全風險剖析 之 執行時載入順序風險

// 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

此時目錄結構如下:

從原始碼分析:Linux共享庫安全風險剖析 之 執行時載入順序風險

我們看一下動態節.dynamic的內容:

從原始碼分析:Linux共享庫安全風險剖析 之 執行時載入順序風險

可見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)

幾個主要的程式碼片段如下:

從原始碼分析:Linux共享庫安全風險剖析 之 執行時載入順序風險

從原始碼分析:Linux共享庫安全風險剖析 之 執行時載入順序風險

從原始碼分析:Linux共享庫安全風險剖析 之 執行時載入順序風險

從原始碼分析:Linux共享庫安全風險剖析 之 執行時載入順序風險

最開始是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動態庫劫持,就需要對庫的位置進行精確定位,精準攔截。

相關文章