# 2021q3 Homework1 (quiz1) ###### contributed by <`JyunD`> ###### tags: `linux 2021` - [ ] 解釋程式碼運作原理,包含 ftrace 的使用 - [ ] 本核心模組只能隱藏單一 PID,請擴充為允許其 PPID 也跟著隱藏 - [ ] 允許給定一組 PID 列表,而非僅有單一 PID - [ ] 指出程式碼可改進的地方,並動手實作 ## Development environment * VM * 2 Vcpu | 2048 MB Ram | 40.0 GB Disk * OS: Ubuntu 20.04 LTS * Kernel: Linux 5.4.0-80-generic #90-Ubuntu ## 程式碼運作原理 定義好一個 character device 讓對它讀寫時有特定行為,對它寫入會新增想要隱藏或想要取消隱藏的 pid,對它讀取會印出當前想要隱藏的 pid 能夠達到這樣的功能全都是仰賴 `ftrace`,`ftrace` 將原先 pidof 會 call 的 `find_ge_pid` hook 成自定義的 function - `hook_find_ge_pid`,`hook_find_ge_pid` 除了會 call 原本的 `find_ge_pid`,還會在檢查這 pid 有沒有在 `hidden_proc` 之中,有的話就不會印出 pid,沒有的話才會正常印出 ### 程式碼詳解 * `_hideproc_init` 為 kernel module 進入點 (insmod 時觸發) 1. `alloc_chrdev_region` 註冊 device 2. 初始化 `cdev` 3. 將 char device 加入 system 4. 創建 device 並 register in 檔案系統 5. 初始化 ftrace_hook hook ```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; } ``` * 初始化 ftrace_hook hook * name: 要被 hooked 的原 function 名稱 * function: hook fuction 的 address * original: 原 function 的 pointer * address: 原 function 的 address * ops: type 是 ftrace_ops * ops.func: callback function,call ftrace 時會執行 * ops.flags: ```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 * 初始化 `hook->ops.func` 和 `hook->ops.flags` ```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 的 function address (用 `kallsyms_lookup_name`) ```c= static int hook_resolve_addr(struct ftrace_hook *hook) { hook->address = kallsyms_lookup_name(hook->name); if (!hook->address) { printk("unresolved symbol: %s\n", hook->name); return -ENOENT; } *((unsigned long *) hook->orig) = hook->address; return 0; } ``` * ftrace 的 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; } ``` * hook function 拿來 hijack 掉 `find_ge_pid` 的 function 這裡要先簡單探討 pidof 這個 command 是如何找到 pid 的 因為 `find_ge_pid` 是找到大於等於特定 pid 的 function,可以推論 pidof 在呼叫這個函數的時候應該期望的是找到相符合的 pid,如果找不到還是會回傳比較大的,但是最後結果不會印出,所以應該是有在比對是不是目標 process,不是的話還是不會印出 那只要我們故意將找到的 pid 加 1,那回傳的就不會是符合的 pid (若剛好還是的話,繼續加 1),pidof 就不會印出我們要的 pid,達到隱藏 pid 的效果 ```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; } ``` * 定義 device 各個 member * .owner * 設定 owner 為自己避免用到一半被 unload * .open * device_open * 回傳成功 * .release * device_close * 回傳成功 * .read * device_read * 讀取 `hidden_proc` 中的內容 copy 到 user space 後印出 * .write * device_write * 將 pid 加到 `hidden_proc` 的 tail `_hideproc_exit` 為 kernel module 離開點 (rmmod 時觸發) ### 補充 #### alloc_chrdev_region ```c= int alloc_chrdev_region ( dev_t * dev, unsigned baseminor, unsigned count, const char *name); ``` kernel 提供了 3 個函數來註冊 character device * register_chrdev_region * 註冊一直範圍的 device number * alloc_chrdev_region * 讓 kernel 主動分配未使用的 device number * register_chrdev * 註冊指定 device number #### class_create 建立 class structure #### cdev_init 初始化 cdev structure #### cdev_add 把 character device 加到 system #### device_create 建立 device 並將他註冊到 sysfs ## 隱藏 PID 列表 修該 `device_write` 讓他可以處理多個 pid,用 `strsep` 做分割 ```diff= static ssize_t device_write(struct file *filep, const char *buffer, size_t len, loff_t *offset) { ... + char *message, *number, *tmp; ... if (!memcmp(message, add_message, sizeof(add_message) - 1)) { - kstrtol(message + sizeof(add_message), 10, &pid); - hide_process(pid); + number = message + sizeof(del_message); + while ((tmp = strsep(&number, " ")) != NULL) \\ 用空白做分割,直到無法分割 + if(strlen(tmp) && !kstrtol(tmp, 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); + number = message + sizeof(del_message); + while ((tmp = strsep(&number, " ")) != NULL) + if(strlen(tmp) && !kstrtol(tmp, 10, &pid)) + unhide_process(pid); ... return len; } ``` ## 隱藏 PPID 找 PPID 我主要使用到以下 3 個 fucntion * struct pid *find_get_pid(int nr); * 從 number 找 struct pid * struct task_struct *get_pid_task(struct pid *pid, enum pid_type); * 從 struct pid 和 pid_type 找其 task_struct * task_ppid_nr(const struct task_struct *tsk) * 從 task_struct 找到其 PPID 修該 `device_write` 讓他在寫入的時候將其 PPID 也加進去 ```diff= static ssize_t device_write(struct file *filep, const char *buffer, size_t len, loff_t *offset) { + struct pid *tmp_pid; + struct task_struct *tmp_task_struct; + pid_t tmp_pid_t; ... if (!memcmp(message, add_message, sizeof(add_message) - 1)) { number = message + sizeof(del_message); while ((tmp = strsep(&number, " ")) != NULL) - if(strlen(tmp) && !kstrtol(tmp, 10, &pid)) - hide_process(pid); + if(strlen(tmp) && !kstrtol(tmp, 10, &pid)) { + tmp_pid = find_get_pid(pid); + tmp_task_struct = get_pid_task(tmp_pid, PIDTYPE_PID); + tmp_pid_t = task_ppid_nr(tmp_task_struct); + hide_process(pid); + hide_process(tmp_pid_t); + } } else if (!memcmp(message, del_message, sizeof(del_message) - 1)) { number = message + sizeof(del_message); while ((tmp = strsep(&number, " ")) != NULL) - if(strlen(tmp) && !kstrtol(tmp, 10, &pid)) - unhide_process(pid); + if(strlen(tmp) && !kstrtol(tmp, 10, &pid)) { + tmp_pid = find_get_pid(pid); + tmp_task_struct = get_pid_task(tmp_pid, PIDTYPE_PID); + tmp_pid_t = task_ppid_nr(tmp_task_struct); + unhide_process(pid); + unhide_process(tmp_pid_t); + } ... return len; } ``` ## 改進 `unhide_process` 實質上是清空整個 `hidden_proc`,並不是刪除指定 pid ```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); break; } } return SUCCESS; } ``` module 在 exit 時並沒有將 device 取消註冊,也沒有把該釋放的資源釋放掉,hook 也要拔掉不然會出問題 ```c= static void _hideproc_exit(void) { pid_node_t *proc, *tmp_proc; dev_t dev; printk(KERN_INFO "@ %s\n", __func__); /* FIXME: ensure the release of all allocated resources */ dev = cdev.dev; 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, cdev.dev); class_destroy(hideproc_class); cdev_del(&cdev); unregister_chrdev_region(dev, MINOR_VERSION); } ``` 在 `hide_process` 中檢查 pid 是不是本來就在裡面,避免一個 pid 有多筆紀錄 ```diff= static int hide_process(pid_t pid) { pid_node_t *proc = kmalloc(sizeof(pid_node_t), GFP_KERNEL); + if (is_hidden_proc(pid)) { + kfree(proc); + return SUCCESS; + } proc->id = pid; list_add_tail(&proc->list_node, &hidden_proc); return SUCCESS; } ``` * sprintf overflow