Linux下的Rootkit駐留技術(shù)分析
前言
Linux作為服務(wù)器和IoT設(shè)備使用的主要操作系統(tǒng),針對它的惡意軟件也層出不窮。針對Linux設(shè)備的惡意軟件(以下稱為rootkit)通常需要長期駐留于目標操作系統(tǒng)以達到獲利目的,所以如何實現(xiàn)駐留也是Linux rootkit作者的重點考慮內(nèi)容之一,對此,實驗室進行了可能的思路探索和分析。
在接下來的說明中,我們統(tǒng)一使用一個名為evil的靜態(tài)鏈接ELF文件作為我們要實現(xiàn)駐留的rootkit,所有的駐留嘗試均圍繞這個程序展開。
技術(shù)匯總
1. 用戶態(tài)下的可利用點
1.1 各種init的利用
Linux init
在systemd成為主流之前,sysvinit是大多數(shù)發(fā)行版的選擇,即使是Ubuntu之前使用的upstart,和sysvinit也是完全兼容的,直到今天,Debian系發(fā)行版仍保留sysvinit的兼容性。作為Linux的init程序,也就是PID 1,負責(zé)啟動之后的所有進程,所有的服務(wù)都是由它管理,因此它是實現(xiàn)rootkit駐留的最常見手段。
對于傳統(tǒng)的sysvinit,常見的駐留點都需要以root身份寫入:
- /etc/init.d/etc/rc[runlevel].d/etc/rc.local
其實sysv的服務(wù)文件就是遵循sysv規(guī)范的shell腳本,它在嵌入式設(shè)備中也很常見。給出一個sysv風(fēng)格的服務(wù)文件如下:
- #!/bin/shPATH=/bin:/usr/bin:/sbin:/usr/sbin
- DESC="cron daemon"NAME=cron
- DAEMON=/usr/sbin/cron
- PIDFILE=/var/run/crond.pid
- SCRIPTNAME=/etc/init.d/"$NAME"test -f $DAEMON || exit 0
- . /lib/lsb/init-functions
- [ -r /etc/default/cron ] && . /etc/default/cronparse_environment() { for ENV_FILE in /etc/environment /etc/default/locale; do
- [ -r "$ENV_FILE" ] || continue
- [ -s "$ENV_FILE" ] || continue
- for var in LANG LANGUAGE LC_ALL LC_CTYPE; do
- value=$(egrep "^${var}=" "$ENV_FILE" | tail -n1 | cut -d= -f2)
- [ -n "$value" ] && eval export $var=$value
- if [ -n "$value" ] && [ "$ENV_FILE" = /etc/environment ]; then
- log_warning_msg "/etc/environment has been deprecated for locale information; use /etc/default/locale for $var=$value instead"
- fi
- done
- done
- # Get the timezone set.
- if [ -z "$TZ" -a -e /etc/timezone ]; then
- TZ=$(cat /etc/timezone) fi}# Parse the system's environmentif [ "$READ_ENV" = "yes" ]; then
- parse_environmentficase "$1" instart) log_daemon_msg "Starting periodic command scheduler" "cron" # 這一行是我們修改的目標
- start_daemon -p $PIDFILE $DAEMON $EXTRA_OPTS
- log_end_msg $?
- ;;
- stop) log_daemon_msg "Stopping periodic command scheduler" "cron"
- killproc -p $PIDFILE $DAEMON
- RETVAL=$?
- [ $RETVAL -eq 0 ] && [ -e "$PIDFILE" ] && rm -f $PIDFILE
- log_end_msg $RETVAL
- ;;
- restart) log_daemon_msg "Restarting periodic command scheduler" "cron"
- $0 stop $0 start
- ;;
- reload | force-reload) log_daemon_msg "Reloading configuration files for periodic command scheduler" "cron"
- # cron reloads automatically
- log_end_msg 0
- ;;
- status)
- status_of_proc -p $PIDFILE $DAEMON $NAME && exit 0 || exit $?
- ;;
- *) log_action_msg "Usage: /etc/init.d/cron {start|stop|status|restart|reload|force-reload}"
- exit 2
- ;;esacexit 0
我們在start case中做些修改,使之在啟動時執(zhí)行我們的evil程序:
對于systemd,我們可以用更多手段實現(xiàn)駐留,甚至不需要root權(quán)限也可以:
- /etc/systemd/system/etc/systemd/user/lib/systemd/system/lib/systemd/user
- ~/.local/share/systemd/user
- ~/.config/systemd/user
以下是一個利用systemd服務(wù)文件的示例:
systemctl --user enable service可以使服務(wù)隨用戶登錄啟動,systemctl enable service可以讓服務(wù)隨系統(tǒng)啟動。
- bashrc
bashrc或者zshrc等文件會隨著shell的運行而被執(zhí)行,利用時只需在里面加入惡意的shell script即可。
常見位置:
- /etc/profile
- ~/.bashrc
- ~/.bash_profile
- ~/.bash_logout
示例如下:
- xinitrc
如果目標主機有安裝Xorg,我們也可以以下位置寫入shell script實現(xiàn)rootkit駐留,不需要root權(quán)限。
- ~/.xinitrc
- ~/.xserverrc/etc/X11/xinit/xinitrc/etc/X11/xinit/xserverrc
- 其它initrc
任何應(yīng)用程序都可能在啟動時執(zhí)行代碼,而且它們很可能會執(zhí)行用戶home目錄的rc文件。例如,我們甚至可以在vimrc里 寫入vimscript來執(zhí)行代碼實現(xiàn)rookit的駐留,這同樣不需要root權(quán)限。
一個可能的示例如下:
1.2 圖形化環(huán)境的利用
雖然標準的服務(wù)器版Linux發(fā)行版是不會預(yù)裝Xorg的,但還是存在相當(dāng)一部分用戶使用CentOS預(yù)裝gnome2的版本作為服務(wù)器操 作系統(tǒng)。因此,基于gnome等桌面環(huán)境和Xorg的駐留有時候也是重要的而且會容易被忽略的手段。
- XDG autostart for system
/etc/xdg/autostart下的desktop文件會被主流桌面環(huán)境在啟動時執(zhí)行。一個可能的示例如下:
- XDG autostart for user
類似的,用戶可以在自己的~/.config/autostart目錄下加入需要自啟動的desktop文件。
1.3 crond的利用
這是一個很常見的駐留點,但需要注意的是,很多惡意軟件并不僅僅會把自己寫入用戶的crontab(如/var/spool/cron/root),它們會把自己寫入軟件包使用的crontab里面,如/etc/cron.d,這樣更不容易引起用戶注意。
1.4 替換文件
替換或者patch一些會被服務(wù)或用戶本身執(zhí)行的程序文件,以同時執(zhí)行惡意代碼,也是很常見的駐留方式。
我們可以方便的獲取到開源項目的源碼,進行修改,加上我們的惡意代碼并重新編譯,替換目標系統(tǒng)的相應(yīng)文件。這樣我們的代碼就會隨之執(zhí)行。
下面我們修改openssh portable 7.9的源碼,使之在特定條件下執(zhí)行我們的代碼:
這里修改的函數(shù)是uncompress_buffer,用于處理壓縮傳輸?shù)膕sh連接。我們需要觸發(fā)它的時候,只需發(fā)起一個ssh -C即可。
如果沒有源碼,我們同樣可以給現(xiàn)有的binary注入shellcode,不過這種實現(xiàn)達到的功能相對有限,且可能會導(dǎo)致原文件損壞 。下面使用backdoor-factory進行shellcode注入,這個工具支持自定義shellcode:
如果我們的目標主機是git或svn服務(wù)器,有機會接觸到項目源碼的話,也可以通過修改目標的源碼植入惡意代碼,或者把編譯環(huán)境動手腳,在項目構(gòu)建時插入惡意代碼,比如在configure腳本里或者Makefile里插入代碼,既可以在本機運行,又有可能在編譯之后在更多主機(取決于項目用途)上運行,進一步擴大感染范圍。這個思路也是當(dāng)年中國xcode事件黑客的思路。
1.5 動態(tài)鏈接庫劫持
替換動態(tài)鏈接庫
libc會被幾乎所有的ELF調(diào)用,而特定的lib則會被特定的ELF調(diào)用,只要某個ELF的執(zhí)行概率夠高,我們同樣可以用我們重新編譯的惡意so替換掉它所鏈接的某個so文件,達到執(zhí)行惡意代碼的效果。
當(dāng)然,以這種思路來看,替換掉整個libc也是未嘗不可的。
下面以sshd的動態(tài)鏈接庫為例,sshd使用的so文件如下:
libz.so.1看上去像是zlib的文件,可以驗證一下:
那么我們可以去下載zlib源碼,在可能會被調(diào)用的函數(shù)里加上我們的私貨。
經(jīng)過grep搜索openssh portable 7.9的源碼,可以看到packet.c使用了zlib的函數(shù):
確定了我們需要注入代碼的函數(shù),就可以去修改zlib源碼了,在inflate.c的inflate函數(shù)里加入簡單的system調(diào)用,來執(zhí)行我們的evil程序:
完成修改之后我們make構(gòu)建項目,然后用我們的惡意libz.so替換原本的文件。
此處修改的zlib,通常只會在ssh客戶端指定了使用壓縮時,才會被使用。所以我們需要使用ssh -C命令去測試。
提醒一點,作為動態(tài)鏈接庫,它們的函數(shù)可能被頻繁調(diào)用,我們在利用的時候要避免造成不必要的負載。另外,由于大部分程序都是動態(tài)鏈接庫文件,我們也需要格外小心,避免加入的代碼調(diào)用的程序最終往回調(diào)用我們修改的庫文件本身(尤其是在修改libc的時候),造成死循環(huán),導(dǎo)致系統(tǒng)停止響應(yīng)。
- ld.so.preload
最常見的實現(xiàn)是在/etc/ld.so.preload中寫入我們需要讓libc執(zhí)行的so文件,或者設(shè)置LD_PRELOAD環(huán)境變量,這樣,任何依賴系統(tǒng)libc的user space程序,都會在運行之前執(zhí)行我們的so文件,從而實現(xiàn)了有效的rootkit駐留(鑒于幾乎所有目標主 機都是動態(tài)編譯的,幾乎所有程序都要使用系統(tǒng)libc)。
下面我們編寫一個簡單的惡意so(shared object)作為說明:
這里的惡意so將編譯為libevil.so。通常的lib都是為主程序提供庫函數(shù)的,只有被調(diào)用的代碼才會被執(zhí)行,于是我們需要 解決的第一個問題是讓我們的代碼在lib被加載時直接自動運行。類似于Windows下的DllMain,gcc提供了function attributes,我們可以用形如__attribute__((constructor))的attribute來達到目的。對應(yīng)的,__attribute__((destructor)) 則會在lib被unload的時候執(zhí)行。
具體解釋見官方文檔:
我們使用如下代碼構(gòu)建libevil,這里有一個坑,execl函數(shù)只有出錯時才返回,如果使用它執(zhí)行了外部程序,那么在外部程 序執(zhí)行完之后,libevil將會退出當(dāng)前進程(也就是我們本來要執(zhí)行的進程),使任何ELF都無法正常執(zhí)行。解決方法是使用 fork 函數(shù)創(chuàng)建子進程,在子進程里執(zhí)行execl啟動外部程序。
使用如下命令構(gòu)建我們的 libevil.so:
設(shè)置LD_PRELOAD環(huán)境變量,使我們的libevil在ld執(zhí)行任何ELF之前被執(zhí)行。
需要注意的是,我們的libevil會在每個動態(tài)ELF執(zhí)行之前被執(zhí)行,如果在其中調(diào)用了外部的動態(tài)ELF,那個ELF執(zhí)行時會再次調(diào)用libevil,就會造成死循環(huán),使系統(tǒng)處于不可用狀態(tài)。
我們也可以在libevil里實現(xiàn)rootkit的所有功能。
2. 內(nèi)核態(tài)的駐留
傳統(tǒng)的rootkit就是指這類惡意軟件,對于Linux rootkit來說,最有效的方法就是把自己作為kernel module加載,因為大多數(shù)Linux目標都是允許動態(tài)加載kernel module的。
在kernel space里運行惡意代碼的好處顯而易見,由于大部分審計工具都在user space運行,管理員通常很難發(fā)現(xiàn)惡意軟件的存在,這就帶來了上面的方法所不能達到的隱蔽性。
2.1 LKM – 可加載內(nèi)核模塊
目前有一些開源的LKM (Loadable Kernel Modules) 木馬demo,比較有代表性的是Reptile,它使用自己的kernel module實現(xiàn)了隱藏和駐留。
下面我們使用這個思路來實現(xiàn)一個簡單的內(nèi)核惡意代碼執(zhí)行:
這里的LKM實現(xiàn)比較簡單,只是在加載和退出的時候執(zhí)行了兩個shell腳本,并使用printk輸出了內(nèi)核調(diào)試信息。我們只需定義兩個函數(shù)分別用于initialize和exit即可完成LKM的主體框架了,module.h會有相應(yīng)函數(shù)module_init和module_exit用來實現(xiàn)。
MODULE_LICENSE也需要設(shè)置,因為Linux是GPL授權(quán),我們需要聲明兼容的授權(quán)協(xié)議,否則Linux會提示tainted kernel(雖然不影響正常使用,但會比較容易引起注意)。編譯LKM的方法可以參考Linux內(nèi)核文檔,或者直接參考現(xiàn)有的LKM項目的Makefile,這里的Makefile內(nèi)容如下:

LKM代碼如下,使用user space helper達到了執(zhí)行外部程序的目的:
使用insmod命令動態(tài)加載LKM到內(nèi)核,立即生效:
下面是dmesg的相關(guān)輸出:
LKM執(zhí)行惡意代碼有比較大的局限性,因為必須針對相同內(nèi)核版本編譯才能正常使用,從而引入了Linux headers的依賴,實現(xiàn)大范圍傳播的難度較大。防范此類攻擊(也是最危險和隱蔽的一種)的最有效方法就是關(guān)閉Linux的動態(tài)模塊加載功能,對于大多數(shù)Linux服務(wù)器,關(guān)掉這個功能通常不會造成額外影響。
2.2 initrd的利用
Reptile會把自己寫入/etc/rc.modules以便在系統(tǒng)啟動時插入自己的module。但通過initrd實現(xiàn)更加隱蔽可靠。initrd即init ram disk,用于提供一個基本的環(huán)境以便啟動完整的Linux。
鑒于initrd很少受到關(guān)注和保護,它又一定會在啟動時被加載入內(nèi)存,我們可以在這個內(nèi)存文件系統(tǒng)中插入自己的LKM并修改init腳本使我們的LKM在啟動時被加載,從而實現(xiàn)惡意代碼執(zhí)行。
這里以Kali(Linux 4.18,基于Debian Sid)為例,分析可能的利用點:
下圖是init腳本functions定義中,加載自定義kernel module的函數(shù),我們可以把自己的LKM放到modules目錄下(例如 ./usr/lib/modules/4.17.0-kali3-amd64/kernel),然后在 ./conf/modules里寫上自己的LKM,我們的LKM就會隨著initrd加載到內(nèi)核。
或者我們也可以直接在load_modules函數(shù)內(nèi)增加modprobe操作來加載惡意LKM。
總結(jié)
Linux下的惡意軟件雖然種類不多,但就其駐留技術(shù)實現(xiàn)而言,還是有不少的方法。本文除了主流的實現(xiàn)之外,也提出了一些通常很少有人注意的實現(xiàn)方法,希望對Linux攻防對抗的朋友有所幫助。