2021q3 Homework1 (quiz1) === contributed by < `dmefs` > ###### tags: `linux2021` 環境: windows 10 使用 multipass ,透過 vscode ssh 連到 vm 寫程式 ```bash= $ uname -r 5.4.0-80-generic $ multipass list Name State IPv4 Image hideproc Running 172.24.248.146 Ubuntu 20.04 LTS $ multipass shell hideproc ``` ## 前置作業 首先看 [Linux 核心模組運作原理](https://hackmd.io/@sysprog/linux-kernel-module) 根據文章的思路學習並筆記 #### strace 利用 strace 追蹤執行 insmod fibdrv.ko 的過程有哪些系統呼叫被執行 執行指令 `sudo strace insmod fibdrv.ko` 有 103 行,摘錄最後幾行,可以發現 `finit_module` 在 99 行 ```bash=92 getcwd("/home/ubuntu/workspace/fibdrv", 4096) = 30 stat("/home/ubuntu/workspace/fibdrv/fibdrv.ko", {st_mode=S_IFREG|0664, st_size=9952, ...}) = 0 openat(AT_FDCWD, "/home/ubuntu/workspace/fibdrv/fibdrv.ko", O_RDONLY|O_CLOEXEC) = 3 read(3, "\177ELF\2\1", 6) = 6 lseek(3, 0, SEEK_SET) = 0 fstat(3, {st_mode=S_IFREG|0664, st_size=9952, ...}) = 0 mmap(NULL, 9952, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f0214210000 finit_module(3, "", 0) = 0 munmap(0x7f0214210000, 9952) = 0 close(3) = 0 exit_group(0) = ? +++ exited with 0 +++ ``` #### 透過以下指令看到經過前置處理後的程式碼 `$ gcc -E fibdrv.c -I $TREE/include/ -I $TREE/arch/x86/include/ -I $TREE/uapi -I $MODULE_PTAH | less` `$TREE` = /usr/src/linux-headers-\$(uname -r) `$MODULE_PTAH`=/lib/modules/$(uname -r)/build/arch/x86/include/generated/ 使用 / 搜尋 可以找到 ```c static initcall_t __initcall_init_fib_dev6 __used __attribute__((__section__(".initcall6" ".init"))) = init_fib_dev; ``` 如同老師說的使用的是 "沒定義 MODULE 的" #### 使用別名 將老師的實驗修改一下 ```c= #include <stdio.h> int __func() { printf("In __func\n"); return 0; } int __func() __attribute__((alias("func"))); int main(int argc, char const* argv[]) { func(); return 0; } ``` 產生錯誤訊息 ```shell= fun.c:8:5: error: redefinition of ‘__func’ 8 | int __func() __attribute__((alias("func"))); | ^~~~~~ fun.c:3:5: note: previous definition of ‘__func’ was here 3 | int __func() { | ^~~~~~ fun.c: In function ‘main’: fun.c:11:5: warning: implicit declaration of function ‘func’ [-Wimplicit-function-declaration] 11 | func(); | ^~~~ fun.c: At top level: fun.c:8:5: error: ‘__func’ aliased to undefined symbol ‘func’ 8 | int __func() __attribute__((alias("func"))); | ^~~~~~ ``` 所以 line 7 的意義應該是 “宣告一個 function __func,__func 是 func 的別名“,但 func 不存在,就產生了錯誤 使用 readelf 觀察 fibdrv.ko `$ readelf -a fibdrv.ko | less` 搜尋 `modinfo` 發現以下資訊 ```shell= [17] .modinfo PROGBITS 0000000000000000 000005c8 00000000000000ed 0000000000000000 A 0 0 1 ``` 使用 hex editor 開啟 fibdrv.ko 可以確認 offset 是 0x5c8 ### 學習 linux rootkits 在 [Linux 核心模組運作原理](https://hackmd.io/pfRzP9sxRYqYbXS6Tm0Ynw?view) 下方有延伸閱讀 - Hiding Kernel Modules from Userspace 這有一系列文章,學習一下 #### [Linux Rootkits Part 1: Introduction and Workflow](https://xcellerator.github.io/posts/linux_rootkits_01/) 解釋如何 build kernel module 這已經會了,看過即可 #### [Linux Rootkits Part 2: Ftrace and Function Hooking](https://xcellerator.github.io/posts/linux_rootkits_02/) 介紹如何使用 ftrace hook system call ,他的例子 hook mkdir。這也是作業一 hook 所使用的方法。 只有包含在 `/proc/kallsyms` 的 function,才能夠使用 hook 的方法 #### [Linux Rootkits Part 3: A Backdoor to Root](https://xcellerator.github.io/posts/linux_rootkits_03/) 展示了 hook 了強大之處, hook kill system call,藉以取得 root 權限 看到這寫作業的先備知識應該夠了,開始看作業 ## 解釋上述程式碼運作原理,包含 ftrace 的使用 ### _hideproc_init 首先從進入點`_hideproc_init`切入,這個 function 主要的工作是註冊 device 及 class,最後是 hook _hideproc_init 並沒有做好 error handle,需要改進 ```c= static int _hideproc_init(void) { int err, dev_major; dev_t dev; printk(KERN_INFO "@ %s\n", __func__); err = alloc_chrdev_region(&dev, 0, MINOR_VERSION, DEVICE_NAME); dev_major = MAJOR(dev); hideproc_class = class_create(THIS_MODULE, DEVICE_NAME); cdev_init(&cdev, &fops); cdev_add(&cdev, MKDEV(dev_major, MINOR_VERSION), 1); device_create(hideproc_class, NULL, MKDEV(dev_major, MINOR_VERSION), NULL, DEVICE_NAME); init_hook(); return 0; } ``` line 6: 註冊 device 有兩種方式 `alloc_chrdev_region` 跟 `register_chrdev_region`,除非確定 major number 才使用 `register_chrdev_region`,否則用 `alloc_chrdev_region`讓系統幫你配置 line 7: macro`MAJOR` ,定義在`linux/kdev_t.h`,可以得到 major number ```c= #define MINORBITS 20 #define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS)) ``` line 9: class_create(THIS_MODULE, DEVICE_NAME); 定義在 `linux/device.h` ```c= /* This is a #define to keep the compiler from merging different * instances of the __key variable */ #define class_create(owner, name) \ ({ \ static struct lock_class_key __key; \ __class_create(owner, name, &__key); \ }) ``` __class_create 建立一個 可以被 device_create 使用的 class 並回傳,不需要時記得呼叫 class_destroy 回收 ```c= struct class * __class_create(struct module * owner, const char * name, struct lock_class_key * key); ``` line 11: `cdev_init`, `cdev_add`,init 一個 cdev line 13: `device_create`,在 sysfs 建立一個 device ```shell= $ ls /sys/module/hideproc/ coresize holders initsize initstate notes refcnt sections srcversion taint uevent $ ls -l /sys/class/hideproc/ total 0 lrwxrwxrwx 1 root root 0 Jul 28 17:51 hideproc -> ../../devices/virtual/hideproc/hideproc ``` line 16: `init_hook` 下方介紹 ### _hideproc_exit 目前沒做任何事,但是`_hideproc_init`有呼叫`class_create`,這邊要有對應的`class_destory`才對,還有 unregister_chrdev ### init_hook 作業希望 hook `find_ge_pid` 達到我們想做的事情,於是透過`kallsyms_lookup_name`可以取得有 export 的 function address ,包含 system call 及 kernel function 定義一個 struct 紀錄 hook 的資訊,並宣告一個 function pointer type,signatrue 跟我們想要 hook 的 function 一樣 ```c= struct ftrace_hook { const char *name; void *func, *orig; unsigned long address; struct ftrace_ops ops; }; typedef struct pid *(*find_ge_pid_func)(int nr, struct pid_namespace *ns); static find_ge_pid_func real_find_ge_pid; ``` 下方是 init_hook 的程式碼,line 2 是多餘的,因為 hook_install 會再做一次 ```c= static void init_hook(void) { real_find_ge_pid = (find_ge_pid_func)kallsyms_lookup_name("find_ge_pid"); hook.name = "find_ge_pid"; hook.func = hook_find_ge_pid; hook.orig = &real_find_ge_pid; hook_install(&hook); } ``` 定義一個 macro 方便初始化 ftrace_hook ```c= #define HOOK(_name, _hook, _orig) \ { \ .name = SYSCALL_NAME(_name), \ .function = (_hook), \ .original = (_orig), \ } ``` 修改 hook variable 的初始化 ```c= static struct ftrace_hook hook = HOOK("find_ge_pid", hook_find_ge_pid, &real_find_ge_pid); ``` ### hook_install 可分為 4 部份: 1. `hook_resolve_addr`利用`kallsyms_lookup_name`尋找並紀錄 function address 2. 設定 ftrace 攔截到 original function 時,要做什麼動作,實做在 `hook_ftrace_thunk`,以及 ftrace 的設定,此次使用了三個設定 3. `ftrace_set_filter_ip`設定要追蹤的 function 4. `register_ftrace_function` enables trace ` ```c= static int hook_install(struct ftrace_hook *hook) { int err = hook_resolve_addr(hook); if (err) return err; hook->ops.func = hook_ftrace_thunk; hook->ops.flags = FTRACE_OPS_FL_SAVE_REGS | FTRACE_OPS_FL_RECURSION_SAFE | FTRACE_OPS_FL_IPMODIFY; err = ftrace_set_filter_ip(&hook->ops, hook->address, 0, 0); if (err) { printk("ftrace_set_filter_ip() failed: %d\n", err); return err; } err = register_ftrace_function(&hook->ops); if (err) { printk("register_ftrace_function() failed: %d\n", err); ftrace_set_filter_ip(&hook->ops, hook->address, 1, 0); return err; } return 0; } ``` ### hook_remove disable trace and clear filter ```c= void hook_remove(struct ftrace_hook *hook) { int err = unregister_ftrace_function(&hook->ops); if (err) printk("unregister_ftrace_function() failed: %d\n", err); err = ftrace_set_filter_ip(&hook->ops, hook->address, 1, 0); if (err) printk("ftrace_set_filter_ip() failed: %d\n", err); } ``` ### hook_ftrace_thunk 當 rip 是我們所要追蹤的 function address 時會被呼叫。 將 rip 替換成 hook function,使用 within_module 判斷有無 recursion 的情況 ```c= static void notrace hook_ftrace_thunk(unsigned long ip, unsigned long parent_ip, struct ftrace_ops *ops, struct pt_regs *regs) { struct ftrace_hook *hook = container_of(ops, struct ftrace_hook, ops); if (!within_module(parent_ip, THIS_MODULE)) regs->ip = (unsigned long) hook->func; } ``` ### device_read 將目前 hide pid 傳回 user ```c= static ssize_t device_read(struct file *filep, char *buffer, size_t len, loff_t *offset) { pid_node_t *proc, *tmp_proc; char message[MAX_MESSAGE_SIZE]; if (*offset) return 0; list_for_each_entry_safe (proc, tmp_proc, &hidden_proc, list_node) { memset(message, 0, MAX_MESSAGE_SIZE); sprintf(message, OUTPUT_BUFFER_FORMAT, proc->id); copy_to_user(buffer + *offset, message, strlen(message)); *offset += strlen(message); } return *offset; } ``` ### device_write 根據 使用者的指令 hide /unhide process ### hide_process 將 hide pid 加入 list ### unhide_process 清空 list,但應該是要只清掉指定 pid 的 node ## 允許其 PPID 也跟著隱藏 問題在於:如何從 PID 找到 PPID ? 首先要暸解 [PID namespace](https://titanwolf.org/Network/Articles/Article?AID=9f7206f6-f50a-4a45-bbf3-8743beb1e3e1#gsc.tab=0) 的架構,便知道尋找的路徑如下 1. 已知 child PID,利用 `find_get_pid` 找到 struct pid 2. 再從 struct pid 找到 tast_strct ,使用`get_pid_task` 3. 透過 tast_struct::real_parent 取得 parent task_struct 4. 利用 task_pid_vnr(parent) 取得 parent PID 取得 parent 之後,記得呼叫 `put_task` 及 `put_pid` 釋放資源 `get_parent_pid` 即是上述的實作 ```c= static pid_t get_parent_pid(pid_t vnr) { struct pid *pid; struct task_struct *p; pid_t ppid = 0; /* Find parent pid */ pid = find_get_pid(vnr); if (!pid) { goto out_err; } p = get_pid_task(pid, PIDTYPE_PID); if (!p) { goto out_pid; } ppid = task_pid_vnr(p->real_parent); if (!ppid) { goto out_task; } out_task: put_task_struct(p); out_pid: put_pid(pid); out_err: return ppid; } ``` hide_process 隱藏 PID 及 parent PID do_hide_process 將 PID 加入 hide list 中 ```c= static int do_hide_process(pid_t pid) { printk(KERN_INFO "@ %s pid: %d\n", __func__, pid); pid_node_t *proc = kmalloc(sizeof(pid_node_t), GFP_KERNEL); if (!proc) { printk(KERN_ERR "%s: kmalloc() failed!\n", __func__); return -ENOMEM; } proc->id = pid; list_add_tail(&proc->list_node, &hidden_proc); return SUCCESS; } static int hide_process(pid_t vnr) { pid_t ppid; int err = SUCCESS; if (!vnr) return -EAGAIN; err = do_hide_process(vnr); if (err) { return err; } ppid = get_parent_pid(vnr); if (!ppid) return -ESRCH; err = do_hide_process(ppid); if (err) return err; return err; } ``` unhide_process 回復 PID 及 parent PID do_unhide_process 將 PID 從 hide list 去除 ```c= static int unhide_process(pid_t pid) { pid_t ppid; int err = SUCCESS; err = do_unhide_process(pid); if (err) return err; ppid = get_parent_pid(pid); if (!ppid) return -ESRCH; err = do_unhide_process(ppid); return err; } static int do_unhide_process(pid_t pid) { pid_node_t *proc, *tmp_proc; list_for_each_entry_safe (proc, tmp_proc, &hidden_proc, list_node) { if (proc->id == pid) { list_del(&proc->list_node); kfree(proc); } } return SUCCESS; } ``` 測試: 撰寫一個簡單的測試程式,程式名稱為 `forever_sleep.c` 功能是 fork 之後就不停 sleep ```c= #include <stdio.h> #include <unistd.h> int main(int argc, char const *argv[]) { fork(); while (1) { sleep(10); } return 0; } ``` 測試流程 ```shell= $ sudo insmod hideproc.ko $ gcc forever_sleep.c -o forever_sleep $ ./forever_sleep & [1] 10345 $ pidof forever_sleep 10346 10345 $ echo "add 10346" | sudo tee /dev/hideproc # add child PID $ pidof forever_sleep # show nothing $ echo "del 10346" | sudo tee /dev/hideproc $ pidof forever_sleep 10346 10345 ``` ## 指出程式碼可改進的地方,並動手實作 ### _hideproc_init error handle 使用 goto 讓程式更簡潔 reference:https://github.com/torvalds/linux/blob/5bfc75d92efd494db37f5c4c173d3639d4772966/drivers/s390/char/tape_class.c ```c= static int _hideproc_init(void) { int err; struct device* device; printk(KERN_INFO "@ %s\n", __func__); err = alloc_chrdev_region(&dev, 0, MINOR_VERSION, DEVICE_NAME); if (err) { printk(KERN_ERR "hideproc: Couldn't alloc_chrdev_region, error=%d\n", err); goto out_chrdev; } hideproc_class = class_create(THIS_MODULE, DEVICE_NAME); if (IS_ERR(hideproc_class)) { err = PTR_ERR(hideproc_class); printk(KERN_ERR "hideproc: Couldn't class_create, error=%d\n", err); goto out_class; } cdev_init(&cdev, &fops); err = cdev_add(&cdev, dev, 1); if (err) { printk(KERN_ERR "hideproc: Couldn't cdev_add, error=%d\n", err); goto out_cdev; } device = device_create(hideproc_class, NULL, dev, NULL, DEVICE_NAME); if (IS_ERR(device)) { err = PTR_ERR(device); goto out_device; } init_hook(); return 0; out_device: cdev_del(&cdev); out_cdev: class_destroy(hideproc_class); out_class: unregister_chrdev_region(dev, MINOR_VERSION); out_chrdev: return err; } ``` ### release resources release init 時所註冊/建立的結構 ```c= static void _hideproc_exit(void) { printk(KERN_INFO "@ %s\n", __func__); /* FIXME: ensure the release of all allocated resources */ release_hide_list(); device_destroy(hideproc_class, dev); cdev_del(&cdev); class_destroy(hideproc_class); unregister_chrdev_region(dev, MINOR_VERSION); } ``` ## unhide certain pid 刪除指定的 node,原本的實作會刪除 list 中所有的 node ```c= static int unhide_process(pid_t pid) { pid_node_t *proc, *tmp_proc; list_for_each_entry_safe (proc, tmp_proc, &hidden_proc, list_node) { if (proc->id == pid) { list_del(&proc->list_node); kfree(proc); } } return SUCCESS; } ``` 測試: ```shell= $ sudo insmod hideproc.ko $ pidof cron 660 $ echo "add 660" | sudo tee /dev/hideproc add 660 $ pidof multipathd 465 $ echo "add 465" | sudo tee /dev/hideproc add 465 $ cat /dev/hideproc pid: 660 pid: 465 $ echo "del 465" | sudo tee /dev/hideproc del 465 $ sudo cat /dev/hideproc pid: 660 ```