--- tags: linux-summer-2021 --- # 2021q3 Homework1 (quiz1) contributed by < `RinHizakura` > > [第 1 週測驗題](https://hackmd.io/@sysprog/linux2021-summer-quiz1?fbclid=IwAR3ttnqOehKTUa0lZoFAQgtFq3ec3YZ20woXR2rq72AGIwltTeA01GksFzQ) > [GitHub](https://github.com/RinHizakura/hideproc) ## 執行環境 ``` $ uname -r 5.4.0-77-generic ``` ## kernel module ### 實作 #### `_hideproc_init` ```cpp 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; } ``` 由 `module_init(_hideproc_init)` 載入 module 的起始函數,這裡主要包含 3 個部份: 1. char device driver 的註冊 * [`alloc_chrdev_region`](https://www.kernel.org/doc/htmldocs/kernel-api/API-alloc-chrdev-region.html) 註冊一個 char device number,該函數呼叫後 kernel 會自動分配 char device number,保存在 `dev` 中 * `cdev` 是一個描述一個 char device 的關鍵結構,先透過 [`cdev_init`](https://www.kernel.org/doc/htmldocs/kernel-api/API-cdev-init.html) 初始化並定義其相關操作的對應行為(`open` / `read` / `write` 等 file operations, `fops`),再透過 [`cdev_add`](https://www.kernel.org/doc/htmldocs/kernel-api/API-cdev-add.html) 根據之前得到的 `dev` 向 kernel 註冊此 char device 2. 在 /dev 目錄下建立 device file 並註冊到 sysfs,因此可以從 user space 存取該 device * `class_create` 建立 `struct class` 結構,其持有者為 `THIS_MODULE` 且 該 class 之名稱為 `DEVICE_NAME` * [`device_create`](https://www.kernel.org/doc/html/latest/driver-api/infrastructure.html?highlight=device_create#c.device_create) 根據該 class 將 device 註冊到 sysfs > [Diffrences between cdev_add and device_create function?](https://stackoverflow.com/questions/50377327/diffrences-between-cdev-add-and-device-create-function) 3. hook 的建立,詳見 [ftrace hook](#ftrace-hook) #### `device_write` `device_write` 被註冊為對 char device 的寫操作(`.write`) 之對應行為: ```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; ``` * 寫入內容的長度需滿足一定長度 ```cpp 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; } ``` 為了使用 memcmp 將 write 要寫入的字串在 kernel 中與 "add" / "del" 做比對,這裡需要先透過 `copy_from_user` 將 buffer 內容複製一份到 user space,並做出對應的隱藏 / 解隱藏行為。 * [`kstrtol`](https://www.kernel.org/doc/htmldocs/kernel-api/API-kstrtol.html) #### `hide_process` ```cpp static int hide_process(pid_t pid) { pid_node_t *proc = kmalloc(sizeof(pid_node_t), GFP_KERNEL); proc->id = pid; CCC; return SUCCESS; } ``` 對照 [ftrace hook](#ftrace-hook) 章節的想法,`hide_process` 的任務即為將欲隱藏的 pid 之 `pid_node_t` 結構加入到 linked list `hidden_proc` 之下。 * CCC = `list_add_tail(proc, hidden_proc)` :::warning :warning: 尚未釐清老師提出 `list_add` 會無法運行的原因? ::: #### `unhide_process` ```cpp static int unhide_process(pid_t pid) { pid_node_t *proc, *tmp_proc; BBB (proc, tmp_proc, &hidden_proc, list_node) { DDD; kfree(proc); } return SUCCESS; } ``` 反之,`unhide_process` 的工作是要將目標 pid 所對應的節點從 linked list 中刪除。 * BBB = `list_for_each_entry_safe` * DDD = `list_del(proc)` :::info 這部份有實作錯誤,參見 [unhide_process 的錯誤](#unhide_process-的錯誤) ::: ## ftrace hook > * [Using ftrace to hook to functions](https://www.kernel.org/doc/html/latest/trace/ftrace-uses.html) > * [ilammy/ftrace-hook](https://github.com/ilammy/ftrace-hook) > * [Ftrace Hook (Linux内核热补丁) 详解](https://blog.csdn.net/pwl999/article/details/107426138) > * [Hooking Linux Kernel Functions, Part 1: Looking for the Perfect Solution](https://www.apriorit.com/dev-blog/544-hooking-linux-functions-1) > * [Hooking Linux Kernel Functions, Part 2: How to Hook Functions with Ftrace](https://www.apriorit.com/dev-blog/546-hooking-linux-functions-2) ftrace 的技術使得可以將 callback 附加在 kernel function 的開頭,使得可以記錄和跟踪 kernel 的運作流程,甚至是達到即時修補 Linux 中的安全漏洞和錯誤,而無須重新啟動的 [live kernel patching](https://en.wikipedia.org/wiki/Kpatch) 目的。儼然是 Linux kernel 中相當重要的技術,下面針對此測驗題中對 ftrace hook 之運用方式與流程進行討論。 ### 實作 #### `init_hook` ```cpp 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); } ``` `init_hook` 先將 `struct ftrace_hook` 的相關成員進行初始化: * `kallsyms_lookup_name` 可以根據給定的 symbol 得到其位址,這裡的目標是 [`find_ge_pid`](https://github.com/torvalds/linux/blob/master/kernel/pid.c#L518),其回傳 `struct pid *` 而輸入參數為 `int nr, struct pid_namespace *ns`,也因此和 typedef 對應 ```cpp typedef struct pid *(*find_ge_pid_func)(int nr, struct pid_namespace *ns); ``` 而 `struct ftrace_hook` 的結構如下 ```cpp struct ftrace_hook { const char *name; void *func, *orig; unsigned long address; struct ftrace_ops ops; }; ``` * `name`: ftrace 所要 hook 的目標函數之 symbol 名稱 * `func`: 用來替換 symbol 原本對應之函數 * `orig`: 指向 symbol 原本對應之函數 * `address`: 被 hook 的函數之地址 * `ops`: 使 ftrace 提供服務之相關操作 :::warning 1. `MODULE_LICENSE` 的設置會影響實際可以存取的 export symbol 2. 較新版的 Linux 將不再可以使用此查詢 symbol,詳見 [Unexporting kallsyms_lookup_name()](https://lwn.net/Articles/813350/) ::: #### `hook_install` ```cpp 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_resolve_addr` 查找要被hook函數之地址(`hook->address`),並使得 `hook->orig` 也指向該位址 * 將 `ftrace_ops` 結構初始化 * `hook->ops.func` 設定為 `hook_ftrace_thunk`,後者是 ftrace 追蹤的 callback * `hook->ops.flags` 調整 ftrace 時對 register * `FTRACE_OPS_FL_SAVE_REGS`: 允許對 pt_regs 的讀寫 * `FTRACE_OPS_FL_IPMODIFY`: 同時設置 `FTRACE_OPS_FL_SAVE_REGS` 才有效,使得可以修改 pt_regs->ip,把原本要追蹤的函式替換成其他函式 * 首先用 `ftrace_set_filter_ip()` 為跟蹤函數打開 ftrace(第 3 個參數設為 0) * 再透過 `register_ftrace_function()` 對被 hook 之函數進行註冊,允許 ftrace 執行我們給定的 callback * 如果註冊失敗,需要 `ftrace_set_filter_ip` (第 3 個參數設為 1) 關閉對其 ftrace #### `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; } ``` 在 `hook_install` 之後,`hook_ftrace_thunk` 是 ftrace 所呼叫的 callback,會插入追蹤函數的入口點中。而要達到替換掉追蹤函數,則要將 `hook_ftrace_thunk` 做為中繼點,替換 `regs->ip`。 這裡的替換函數取得是透過 `ops`。因為 `ops` 屬於 struct hook 結構的成員之一,可以藉由 `container_of` 拿到先拿到 hook 再取得其成員 hook->func。 * 如果 `hook->func` 中再次調用追蹤函數,會造成無窮的遞迴呼叫。因此要先用 `within_module` 檢查 `parent_ip`(調用 hook 的返回地址)是否等於 `THIS_MODULE`,只有在第一次呼叫時執行 `hook->func` #### `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` 是 `hook->func` 所指向之函數,作為替代原本的 `find_ge_pid` 去做到隱藏的效果。其行為是多次調用原本的函數(`real_find_ge_pid`) 去略過所要隱藏的 pid,達到隱藏 pid 的效果。 #### `is_hidden_proc` ```cpp static bool is_hidden_proc(pid_t pid) { pid_node_t *proc, *tmp_proc; AAA (proc, tmp_proc, &hidden_proc, list_node) { if (proc->id == pid) return true; } return false; } ``` `is_hidden_proc` 檢查此 pid 是否與 module 所維護的 linked list `hidden_proc` 中其一節點之 id 相符 * AAA = `list_for_each_entry_safe` ## 程式碼改進 ### 資源釋放 顯然 `_hideproc_exit` 之處需要進行必要的資源釋放(~~不然一 remove 就當機,哭啊~~),原則上就是照著取得資源的相反步驟釋放回去。需要進行的工作包含: 1. 將 linked list 上的節點釋放(如果存在) 2. 解除 hook 3. `device_create` 對應 `device_destroy`: 將在 /dev 目錄下建立的 device file 刪除,從 sysfs 取消註冊 4. `cdev_add` 對應 `cdev_del` 將 char device 刪除 5. `class_create` 對應 `class_destroy` 釋放 `struct class` 相關資源 6. `alloc_chrdev_region` 對應 `unregister_chrdev_region` 釋放 device number ```cpp static void _hideproc_exit(void) { pid_node_t *proc, *tmp_proc; /* free pid_node_t allocated form kmalloc */ list_for_each_entry_safe (proc, tmp_proc, &hidden_proc, list_node) { list_del(&proc->list_node); kfree(proc); } hook_remove(&hook); device_destroy(hideproc_class, dev); cdev_del(&cdev); class_destroy(hideproc_class); unregister_chrdev_region(dev, MINOR_VERSION); printk(KERN_INFO "@ %s\n", __func__); } ``` ### `unhide_process` 的錯誤 unhide_process 的行為與期待不一致,將所有 linked list 中的 node 對應之 `pid` 都解除隱藏了,修改如下: ```cpp 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; } ``` ### `kallsyms_lookup_name` 的替代方案 雖然我的 kernel 版本可以直接使用 `kallsyms_lookup_name`,然而未雨綢繆,不妨先整理相關的知識起來放。 [ilammy/ftrace-hook](https://github.com/ilammy/ftrace-hook/blob/master/ftrace_hook.c) 已經展示了一種可行的解法是透過 kprobe: ```cpp #if LINUX_VERSION_CODE >= KERNEL_VERSION(5,7,0) static unsigned long lookup_name(const char *name) { struct kprobe kp = { .symbol_name = name }; unsigned long retval; if (register_kprobe(&kp) < 0) return 0; retval = (unsigned long) kp.addr; unregister_kprobe(&kp); return retval; } #else static unsigned long lookup_name(const char *name) { return kallsyms_lookup_name(name); } #endif ``` ## 延伸功能 ### 隱藏 ppid 我們可以從一個 `pid` 去取得其 `task_struct` 來得知其 parent 之 pid。如下為從 child pid 取得其 parent pid 的函式封裝: ```cpp #define find_task_by_pid(pid) pid_task(find_vpid(pid), PIDTYPE_PID) static inline pid_t get_ppid_by_pid(pid_t pid) { struct task_struct *task = find_task_by_pid(pid); if (task->parent) return task->parent->pid; return 0; } ``` :::warning 另一個版本是取得 `task_struct` 的 instance (會將 reference count + 1)。由於對 kernel codes 的撰寫不甚熟悉,尚需釐清兩者的具體差異和適合的使用情境 ```cpp #define get_task_by_pid(pid) get_pid_task(find_get_pid(PID), PIDTYPE_PID) ``` ::: 然後改寫 device_write 如下: ```diff static ssize_t device_write(struct file *filep, const char *buffer, size_t len, loff_t *offset) { ... + pid_t ppid; ... if (!memcmp(message, add_message, sizeof(add_message) - 1)) { kstrtol(message + sizeof(add_message), 10, &pid); hide_process(pid); + // hide also the parent process + ppid = get_ppid_by_pid(pid); + if (ppid) + hide_process(ppid); } else if (!memcmp(message, del_message, sizeof(del_message) - 1)) { kstrtol(message + sizeof(del_message), 10, &pid); unhide_process(pid); + // unhide also the parent process + ppid = get_ppid_by_pid(pid); + if (ppid) + unhide_process(ppid); } ... } ``` 並藉由以下實驗初步的驗證其 parent process 之資訊確實被隱藏: ``` $ pidof bash 7939 $ pstree -s -p 7939 systemd(1)───systemd(1883)───gnome-shell(2125)───terminator(7932)───bash(7939)───vim(8029) $ ps -f 7932 UID PID PPID C STIME TTY STAT TIME CMD rin 7932 2125 0 19:07 ? Sl 0:33 /usr/bin/python3 /usr/bin/terminator $ echo "add 7939" | sudo tee /dev/hideproc $ ps -f 7932 UID PID PPID C STIME TTY STAT TIME CMD $ echo "del 7939" | sudo tee /dev/hideproc $ ps -f 7932 UID PID PPID C STIME TTY STAT TIME CMD rin 7932 2125 0 19:07 ? Sl 0:34 /usr/bin/python3 /usr/bin/terminator ``` :::warning 需釐清之問題: * unhide 時會再次 access `find_task_by_pid`,其成本是否可以承受? 或者需要在 kernel module 中透過資料結構去 cache 住 parent pid * 一個 process 的 parent 可能會在系統運行中途改變嗎? 此設計是否有不足之處? :::