# 2021q3 Homework1 (quiz1) contributed by < `demonsome` > ###### tags: `2021 Summer - Linux kernel` > [第 1 週測驗題題目](https://hackmd.io/@sysprog/linux2021-summer-quiz1) :::warning 注意書寫規範: 中英文間用一個空白字元區隔。唯有掌握各項細節,方可征服浩瀚的核心。 :notes: jserv ::: ## 延伸問題 **1. 解釋上述程式碼運作原理,包含 `ftrace` 的使用** #### ftrace 以及 hook 此程式碼一開始即定義了包含 `ftrace_ops` 物件的結構 `ftrace_hook` ,整個程式碼的基本功能是由 `ftrace` 功能的延伸,詳細資訊可參考[連結](https://www.kernel.org/doc/html/latest/trace/ftrace-uses.html)。 我們可將 `ftrace_op` 附在 [callback function](https://en.wikipedia.org/wiki/Callback_(computer_programming)) 上,用以追蹤程式在 kernel 中的流程。 `ftrace` 通常被運用於系統的安全監控中,尤其在一些工作負載重的科學系統可以在不重啟系統的情形下,對系統核心的工作執行追蹤。 這裡是 `ftrace_ops` 的結構: ```cpp struct ftrace_ops ops = { .func = my_callback_func, .flags = MY_FTRACE_FLAGS .private = any_private_data_structure, }; ``` `ftrace_op` 需事先註冊才能開始追蹤函式的呼叫,註冊的動作定義在 `hook_install` 函式之中(如下),函式會透過 `hook_resolve_addr` 去找尋 kernel's symbol table 中 hook 名稱對應的位址,並指派給變數`err`。一旦 `err` 變數值非零,則回傳 hook 安裝失敗的訊息。 :::info `hook_resolve_addr` 函式中的 `kallsyms_lookup_name` 的函式在較新的linux版本需要進行更新,才能運作。 ::: ```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; } ``` 其中 `hook_ftrace_thunk` 函式為 `ftrace` 的callback function,注意到這裡的`notrace`識別字是為了讓 `hook_ftrace_thunk` 無法被`ftrace`追蹤。就此情況來說,函式加上 `notrace` 識別字是為了避免不必要的遞迴呼叫。 kernel 會在此 callback function 找到結構 `ops` 所對應的起始位址,並指派給指標 hook。而一旦我們發現 `parent_ip` 的位址不在這個模組記憶體區塊的範圍內,則結構 [`pt_regs`](https://github.com/spotify/linux/blob/master/arch/x86/include/asm/ptrace.h) 中的指令位址(instruction pointer, ip)會被改成 `hook` 的 callback function 起始位址。 ```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; } ``` #### 隱藏 process 的機制 這裡藉由一個 doubly-linked list 存放要隱藏的 process ID ,這個 list 的第一個節點被指標 `hidden_proc` 所指向,每個節點的結構叫做 `pid_node_t` ,包含了一個 `list_head` 結構以及一個 process ID 值。 ```c= typedef struct { pid_t id; struct list_head list_node; } pid_node_t; LIST_HEAD(hidden_proc); ``` 示意圖: ![](https://i.imgur.com/rjlAnUj.png) 為了欺騙 kernel ,讓 kernel 以為某個 process 不存在,我們需要取的 kernel 中的 process ID 資訊。 作法上,使用 `ftrace` 去追蹤 `find_ge_pid_func` 結構,這個結構是一個 [`pid`](https://github.com/torvalds/linux/blob/master/include/linux/pid.h) 的結構,藉由傳入`int nr` 以及 `struct pid_namespace *ns` ,我們可取得結構 `pid` 的 process ID。 接著我們需要對管理 process 的結構進行追蹤。可以使用 `ftrace` 機制建立一個 hook 。在初始化一個 hook 時,我們將 hook 結構中的函式指標指向 `hook_find_ge_pid` 函式。 ```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); } ``` 而一個 hook 的結構長這樣: ```c= struct ftrace_hook { const char *name; void *func, *orig; unsigned long address; struct ftrace_ops ops; }; ``` `hook_find_ge_pid` 函式會先去查找目前存在於記憶體的 process 有哪些,接著去比對這些 process 的 ID 是否存在於要隱藏 process 的 doubly-linked list 。若有,則回傳要隱藏的 process ID。 ```c= static struct pid *hook_find_ge_pid(int nr, struct pid_namespace *ns) { struct pid *pid = real_find_ge_pid(nr, ns); while (pid && is_hidden_proc(pid->numbers->nr)) pid = real_find_ge_pid(pid->numbers->nr + 1, ns); return pid; } ``` 所以我在腦海中整理了一下,具體來說,他會看起來有點像這樣: ![](https://i.imgur.com/kGhzRuG.png) #### Device file 初始化及 process 隱藏 當我們插入 kernel module 到 kernel 時, kernel 會先初始化一個 device file 並等待終端的輸入。 接著準備一個 `hook` , `hook` 的函式`hook_find_ge_pid` 會在 `hidden_proc` 所指向的 doubly linked list 中找出要隱藏 process ID。 最後, callback function -- `hook_ftrace_thunk` 將 `regs` 的 instruction point 做更改,使 kernel 無法辨別隱藏的 process ID。 ```cpp #define MINOR_VERSION 1 #define DEVICE_NAME "hideproc" 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; } static void _hideproc_exit(void) { printk(KERN_INFO "@ %s\n", __func__); /* FIXME: ensure the release of all allocated resources */ } module_init(_hideproc_init); module_exit(_hideproc_exit); ``` 當我們在 terminal 輸入 `echo "add 644" | sudo tee /dev/hideproc` ,註冊的裝置會讀取字串,並將 `add 644` 解讀成將 process ID 為 644 的行程加到要隱藏的 doubly-linked list 之中。反之, `"del 644"` 則是實作移除動作,使 process ID 不再被隱藏,可以t再次被核心辨別。 ```cpp 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)) { kstrtol(message + sizeof(add_message), 10, &pid); hide_process(pid); } else if (!memcmp(message, del_message, sizeof(del_message) - 1)) { kstrtol(message + sizeof(del_message), 10, &pid); unhide_process(pid); } else { kfree(message); return -EAGAIN; } *offset = len; kfree(message); return len; } ``` **2. 本程式僅在 Linux v5.4 測試,若你用的核心較新,請試著找出替代方案 2020 年的變更 Unexporting kallsyms_lookup_name() Access to kallsyms on Linux 5.7+** 我使用的核心版本為 `5.8.0-63-generic` ,編譯時問題就發生了。 ![](https://i.imgur.com/eUwVI4y.png) 2020 年 [Unexporting kallsyms_lookup_name()](https://lwn.net/Articles/813350/) 討論 `kallsyms_lookup_name()` 這個函式存在的漏洞。 文中提到,每個模組已經有足夠的機制可以去 kernel 找出一個 symbol 對應的位址。 `kallsyms_lookup_name()` 像是一個後門,筆者擔心被濫用。因此 Will Deacon 發布了一系列的 patch 以移除 `kallsyms_lookup_name()` 的輸出。 雖然,取消了這個函式讓許多正當使用的(如: kernel live patching)工程師以及業餘開發者感到不便 :upside_down_face: ,但事實上還是有許多方法可以在 kernel 中找到 symbol 所對應的位址。 Masami Hiramatsu 舉了[幾個方法](https://lwn.net/ml/linux-kernel/20200221232746.6eb84111a0d385bed71613ff@kernel.org/),一個是先去取得 symbol map 再用它去找位址,另一個則是使用 `kprobes` 去取得位址。 若不考慮執行時間的話,也可以使用 "%pF" 格式的 `snprintf()`去查詢。 我這裡使用 `kprobes` 去取得位址,並參考 [`hankluo6`](https://hackmd.io/@hankluo6/hideproc) 同學的作法,程式碼請參[連結](https://github.com/demonsome/linux2021q3/blob/main/quiz1/main.c)。 process 可以被正確隱藏如下圖: ![](https://i.imgur.com/S9KvJdS.png) **3. 本核心模組只能隱藏單一 PID,請擴充為允許其 PPID 也跟著隱藏,或允許給定一組 PID 列表,而非僅有單一 PID 指出程式碼可改進的地方,並動手實作** ```clike= $ pidof firefox 3066 2999 2956 2881 $ pstree -p | grep "firefox" | |-firefox(2881)-+-Privileged Cont(2956)-+-{Privileged Cont}(2962) | | |-{firefox}(2888) | | |-{firefox}(2889) | | |-{firefox}(2894) | | |-{firefox}(2895) | | |-{firefox}(2896) . . . . . . . . . ```