# 2021q3 Homework1 (quiz1) contributed by < `foxhoundsk` > ## 實驗環境 ``` Linux ubuntu 5.4.0-80-generic #90-Ubuntu SMP Fri Jul 9 22:49:44 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux ``` ## 運作原理 解釋 hideproc 運作原理之前,需先理解 [ftrace](https://www.kernel.org/doc/Documentation/trace/ftrace.txt) 的運作方式。 在有開啟 ftrace 相關 [config](https://www.kernel.org/doc/html/latest/kbuild/kconfig.html) 的 kernel 中,所有非 `inline` 或使用 [notrace](https://elixir.bootlin.com/linux/v5.4.136/source/include/linux/compiler_types.h#L114) 巨集宣告的函式的開頭都會放置一 nop 命令,後者在指定函式套用 ftrace 相關設施時,會被替換為一 `call` 指令,其跳轉目標為 ftrace 的 [trampoline](https://en.wikipedia.org/wiki/Trampoline_(computing)),後者將再跳轉至使用者定義的函式執行。相關解說可見此[簡報](https://blog.linuxplumbersconf.org/2014/ocw/system/presentations/1773/original/ftrace-kernel-hooks-2014.pdf)第 46 至 49 張。 在 hideproc 中,kernel API `register_ftrace_function()` 在做的即是透過 ftrace 將特定 nop 指令做 [live patching](https://www.kernel.org/doc/html/latest/livepatch/livepatch.html#kprobes-ftrace-livepatching),也就是說,此函式結束後,所有對 `find_ge_pid` 的呼叫都會執行到使用者定義的 hook。 以下開始介紹 hideproc 之運作,在模組初始化函式 `_hideproc_init` 執行完後,目錄 `/dev` 下預期可見到名為 hideproc 的檔案,後者為一 character device,也是 hideproc 提供給 userland 操作的介面。此外,hideproc 使用到的 ftrace hook 也已置入核心函式 `find_ge_pid` 中。 稍後,當執行命令 `echo "add 644" | sudo tee /dev/hideproc ` 時,hideproc 中,對應 write 操作的 handler 函式 `device_write` 將執行。此函式將使用者的輸入做處理,判斷是否為新增或移除對指定 process 的隱藏。兩者對應的操作分別為將對應節點 新增至/移除出 一名為 `hidden_proc` 的 list。 假設現在 pid 644 已被加入 list 中。此時,當有 process 對目錄 `/proc` 進行讀取操作時,可以發現到,特定 pid 無法被該讀取找到,但倘若直接 `ls /proc/644`,其實還是可以找到 process 644 對應的 process dir,這是因為直接對單一 process dir 操作時,原先被置入 hook 的函式 `find_ge_pid` 並不會被執行到。後者包裝於 [proc_root_operations](https://elixir.bootlin.com/linux/v5.4.136/source/fs/proc/root.c#L271) 的 [iterate_shared](https://elixir.bootlin.com/linux/v5.4.136/source/fs/proc/root.c#L273) 的對應 handler [proc_root_readdir](https://elixir.bootlin.com/linux/v5.4.136/source/fs/proc/root.c#L254) 之中。 回頭來探討為什麼 pid 644 會被隱藏。在 hideproc 程式碼中,可以看到被掛到核心函式 `find_ge_pid` 的 hook 為 `hook_ftrace_thunk`: ```cpp 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; } ``` 其中巨集 `notrace` 用於避免 ftrace 在執行過程發生無限 recursion。而函式 within_module 也是同樣用途,它用於保證稍後核心模組在執行透過 `regs->ip = (unsigned long) hook->func` 執行的函式 `hook_find_ge_pid` 在執行時不會發生 recursion。 由於在 kernel code (不在模組中) 執行 hook 函式 `hook_ftrace_thunk` 時,IP 暫存器被存放 (倘若要在 ftrace 執行的 callback 中,對通用暫存器 (pt_regs) 做修改,需在註冊 ftrace 時使用旗標 `FTRACE_OPS_FL_SAVE_REGS`) 了函式 `hook_find_ge_pid` 的位置,所以,下一瞬間(下一條指令)將開始執行函式 `hook_find_ge_pid`: ```cpp= 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; } ``` 其中可以發現此函式呼叫了被放置 hook 的核心函式 `find_ge_pid`。首先,將 struct `pid` 取出,並且只要找出的 `pid` 儲存的 process id 存在於 list `hidden_proc` 中,就繼續往下找,直到 `pid` 為 NULL 或其儲存的 process id 不在 list `hidden_proc` 中。此處要注意的是,執行函式 `hook_find_ge_pid` 時,我們使用的 argument 即是待會 `find_ge_pid` 實際執行時所使用的。換句話說,我們正在操作稍後 `find_ge_pid` 會用到的儲存 function argument (即 `int nr, struct pid_namespace *ns`) 的暫存器。 至此,我們可以理解為什麼指定 process id 會在被 hideproc 隱藏後無法透過讀取目錄 `/proc` 來找到,原因就是我們為函式 `find_ge_pid` 加了點「料」(ftrace)。也就是說,只要我們發現被隱藏的 process id 會被找到,我們就盡可能的往下一個 process id 前進,直到 struct pid 為 NULL 或 process id 不是我們所隱藏的 process id。如此一來,`find_ge_pid` 實際在結束時就不會回傳預期的指向結構體 struct pid 的指標。 ## 對應資源釋放 相關修改可見 commit [9ca893](https://github.com/foxhoundsk/linux-kernel-internals-hw/commit/9ca893b40bc1811deaf917faa1f292ad7a0c83df?branch=9ca893b40bc1811deaf917faa1f292ad7a0c83df&diff=unified)。 ## 修正 unhide_process 邏輯 由此函式的 parameter 可得知,其功能是用於將指定 process id 移出 list `hidden_proc`,但目前實做卻是將 list 中的所有節點移除。 相關修正可見 commit [ae5b34](https://github.com/foxhoundsk/linux-kernel-internals-hw/commit/ae5b34e5bbd582235bf782c71f02d6ec1a87eb53#diff-07b5327b04a05f56af9f4248ed9eb092128e1f00f8fdf8af341858cb398cc433)。 ## 擴充功能 ### 允許隱藏 ppid 將一部分 hideproc 程式碼修改為: ```cpp= static int hide_process(pid_t pid, bool hide_parent) { struct pid *p; pid_node_t *proc; if (hide_parent) { p = find_get_pid(pid); if (p) { /* prevent invalid @pid */ proc = kmalloc(sizeof(pid_node_t), GFP_KERNEL); /* turns out `child_reaper` is the parent of the target process */ proc->id = p->numbers[p->level].ns->child_reaper->pid; list_add_tail(&proc->list_node, &hidden_proc); } } proc = kmalloc(sizeof(pid_node_t), GFP_KERNEL); proc->id = pid; list_add_tail(&proc->list_node, &hidden_proc); return SUCCESS; } static ssize_t device_write(struct file *filep, const char *buffer, size_t len, loff_t *offset) { long pid; char *message; bool hide_parent = false; char add_message[] = "add", del_message[] = "del", hide_parent_opt[] = "-p"; 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) + sizeof(hide_parent_opt), 10, &pid); if (!memcmp(message + sizeof(add_message), /* start of the opt */ hide_parent_opt, sizeof(hide_parent_opt) - 1)) hide_parent = true; hide_process(pid, hide_parent); } 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; } ``` 判斷使用者寫入 `/dev/hideproc` 的字串是否包含 `-p`,倘若有,在執行 `hide_process` 時,透過 `find_get_pid` 將 `struct pid *p` 取出,並透過 `->numbers[p->level].ns->child_reaper->pid` 取得 parent process 的 process id,將其加入 list `hidden_proc` 中。 ### 為 list @hidden_proc 加入同步機制 以避免 list 的存取存在 race condition。[目前以 spinlock 實做](https://github.com/foxhoundsk/linux-kernel-internals-hw/commit/7bea0449e50999676690453de2deef3c7591b575#diff-07b5327b04a05f56af9f4248ed9eb092128e1f00f8fdf8af341858cb398cc433),但目標為 lock-free 的 list。