# 2021q3 Homework1 (quiz1) contributed by < `OscarShiang` > ## 解釋 hideproc 程式碼運作原理,包含 ftrace 的使用 `hideproc` 的原理在於使用 ftrace hook 進行 live patching 進而更改 `find_ge_pid()` 函式的行為。 當我們利用 `ps` 或是列出 `/proc/` 目錄底下的程序時,實際上會使用到 `find_ge_pid()` 函式來進行查找 (在 `/fs/proc/base.c` 中的 [`proc_pid_readdir()`](https://elixir.bootlin.com/linux/v5.4.80/source/fs/proc/base.c#L3313) 使用 [`next_tgid()`](https://elixir.bootlin.com/linux/v5.4.80/source/fs/proc/base.c#L3275) 遍尋所有程序,而其實作則使用 [`find_ge_pid()`](https://elixir.bootlin.com/linux/v5.4.80/source/fs/proc/base.c#L3284)),所以若我們改變 `find_ge_pid()` 的行為,就可以達到隱藏程序的效果。 但是我們的目的並不是要完全取代原本的實作,而是偏移部份程序查照的結果。我們使用的方式是利用 ftrace 註冊 `hook_ftrace_thunk()` 函式,當有程序想要呼叫 `find_ge_pid()` 時, ftrace 會將其跳轉到我們先前指定的 `hook_ftrace_thunk()` 函式,透過更改 Instruction Pointer 位址從而跳轉到 `hook_find_ge_pid()` ,利用原先的 `find_ge_pid()` 函式進行操作後,如果預期的 pid 是我們想要隱藏者時,我們將代入到 `find_ge_pid()` 的 `pid` + 1 讓其無法取得預期的結果,將列表中的 pid 隱藏。 而我們操作 `hidden_proc` 的 hidden list 的方式是使用 VFS 來進行。 在範例程式中,我們使用以下命令將指定 pid 隱藏 ```shell $ echo "add <pid>" | sudo tee /dev/hideproc ``` 實際上進行處理的函式是在 `device_write()`,經由命令分析後,使我們能夠動態修改 hidden list。 ## 允許其 PPID 也跟著隱藏 我參考 [`next_tgid()`](https://elixir.bootlin.com/linux/v5.4.80/source/fs/proc/base.c#L3275) 的方式來進行實作 因為 parent 的資訊保存在 `task_struct` 結構裡面,所以我們需要依序經由以下步驟取得 ppid - [`find_get_pid()`](https://elixir.bootlin.com/linux/v5.4.80/source/kernel/pid.c#L393): 利用 pid 取得 `struct pid` 的位址 - [`get_task_pid()`](https://elixir.bootlin.com/linux/v5.4.80/source/kernel/pid.c#L381): 透過 `struct pid` 的位址取得 `struct task_struct` 的位址 - `task->parent->pid`: 得到 ppid 為了方便使用我將其包裝成 `get_ppid()`: ```cpp static pid_t get_ppid(pid_t pid) { struct pid *pid_struct; struct task_struct *task; pid_struct = find_get_pid(pid); task = get_pid_task(pid_struct, PIDTYPE_PID); return task->parent->pid; } ``` 接著在模組中新增一個新的命令 `addwp` (add with parent) 用以將指定 pid 以及其 parent pid 加入隱藏的 list 中 > 相關的 commit 可以參考 [`50f8eb9`](https://github.com/OscarShiang/hideproc/commit/50f8eb9c86b57b917759f6d41bb2b731139fa76b) 這邊為了驗證模組行為,我準備了一個簡單的程式: ```cpp #include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <sys/wait.h> #include <unistd.h> int main(void) { printf("My pid is \t %d\n", getpid()); pid_t child = fork(); if (!child) { pause(); } else { printf("Child's pid is \t %d\n\n", child); printf("(Use Ctrl + C to exit)\n"); wait(0); } return 0; } ``` 上述這段程式碼的作用就是產生一組 process 與 child process,並印出兩者的 pid 編譯後,執行結果大致如下 ```shell $ gcc -o test_proc test_proc.c $ ./test_proc My pid is 25735 Child's pid is 25736 (Use Ctrl + C to exit) ``` 我們可以透過 `ps aux` 檢查二者是否可以被看見 ```shell $ ps aux | grep test_proc ubuntu 25735 0.0 0.0 2488 588 pts/1 S+ 21:17 0:00 ./test_proc ubuntu 25736 0.0 0.0 2488 84 pts/1 S+ 21:17 0:00 ./test_proc ubuntu 25738 0.0 0.0 8160 736 pts/0 S+ 21:21 0:00 grep --color=auto test_proc ``` 接著我們使用 `addwp` 命令將 child process pid 加入到 hidden list 中 ```shell $ echo "addwp 25736" | sudo tee /dev/hideproc ``` 透過讀取 `/dev/hideproc` 檢查二者是否被加入到 list 之中 ```shell $ sudo cat /dev/hideproc pid: 25736 pid: 25735 ``` 此時若使用 `ps aux` 則無法查到兩個 `test_proc` 的狀態 ```shell $ ps aux | grep test_proc ubuntu 25771 0.0 0.0 8160 668 pts/0 S+ 21:25 0:00 grep --color=auto test_proc ``` ## 允許給定一組 PID 列表,而非僅有單一 PID > TODO ## 改進實作 ### 離開模組時釋放相關資源 在原本的實作中,因為沒有 exit function 中實作資源釋放。所以如果我們將模組載入後移除,使用 `insmod` 重新載入模組時會得到 `Killed` 的輸出,並無法再次載入模組。 解決的方式就是在 `_hideproc_exit()` 加入與 `_hideproc_init()` 成對的資源釋放即可。 ```diff diff --git a/main.c b/main.c index 45922af..c9e42b2 100644 --- a/main.c +++ b/main.c @@ -63,7 +63,6 @@ static int hook_install(struct ftrace_hook *hook) return 0; } -#if 0 void hook_remove(struct ftrace_hook *hook) { int err = unregister_ftrace_function(&hook->ops); @@ -73,7 +72,6 @@ void hook_remove(struct ftrace_hook *hook) if (err) printk("ftrace_set_filter_ip() failed: %d\n", err); } -#endif typedef struct { pid_t id; @@ -212,10 +210,11 @@ static const struct file_operations fops = { #define MINOR_VERSION 1 #define DEVICE_NAME "hideproc" +static dev_t dev; + 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); @@ -235,7 +234,21 @@ static int _hideproc_init(void) static void _hideproc_exit(void) { printk(KERN_INFO "@ %s\n", __func__); - /* FIXME: ensure the release of all allocated resources */ + + /* Destroy the hidden list */ + 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); + } + + /* Unregister the device */ + device_destroy(hideproc_class, MKDEV(MAJOR(dev), MINOR_VERSION)); + cdev_del(&cdev); + class_destroy(hideproc_class); + unregister_chrdev_region(MKDEV(dev, MINOR_VERSION), MINOR_VERSION); + hook_remove(&hook); } module_init(_hideproc_init); ``` ### `kstrtol` 回傳值檢查 根據 [Kernel API Doc](https://www.kernel.org/doc/htmldocs/kernel-api/API-kstrtol.html) 對於 return value 的描述 > Returns 0 on success, -ERANGE on overflow and -EINVAL on parsing error. Used as a replacement for the obsolete simple_strtoull. Return code must be checked. 但在 `device_write()` 處並沒有針對 `kstrtol()` 的回傳值做檢查, 因此需要在這邊加上檢查 ```diff diff --git a/main.c b/main.c index 9593cf2..c969b29 100644 --- a/main.c +++ b/main.c @@ -170,6 +170,7 @@ static ssize_t device_write(struct file *filep, size_t len, loff_t *offset) { + int ret; long pid; char *message; @@ -181,11 +182,17 @@ static ssize_t device_write(struct file *filep, 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); + ret = kstrtol(message + sizeof(add_message), 10, &pid); + if (!ret) + hide_process(pid); + else + return ret; } else if (!memcmp(message, del_message, sizeof(del_message) - 1)) { - kstrtol(message + sizeof(del_message), 10, &pid); - unhide_process(pid); + ret = kstrtol(message + sizeof(del_message), 10, &pid); + if (!ret) + unhide_process(pid); + else + return ret; } else { kfree(message); return -EAGAIN; ``` 因為 `kstrtol` 錯誤原因有兩種,所以不直接回傳 `-EINVAL`,而是將 `kstrtol` 產生的回傳值回傳回去。 ### 取消隱藏指定 pid 在原本的實作中,`hideproc` 在收到 `del <pid>` 的命令時並不只會將我們指定的 pid 從隱藏列表中刪除,而是會將所有列表中的 node 全部刪除 因此我在 `unhide_process()` 中加上核對 pid 的機制,避免其將所有節點刪除 ```diff diff --git a/main.c b/main.c index 5b63b66..f529c1e 100644 --- a/main.c +++ b/main.c @@ -133,8 +133,11 @@ 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) { - list_del(&proc->list_node); - kfree(proc); + if (proc->id == pid) { + list_del(&proc->list_node); + kfree(proc); + break; + } } return SUCCESS; } ``` ### 重複加入已被隱藏的 pid 我們可以透過對 `/dev/hideproc` 寫入命令來增刪我們想要隱藏的 pid,但是在 `hide_process()` 函式中我們並不會對 pid 進行任何的檢查,因此就會有 list 中有重複好幾個 pid 的情況產生 我們可以利用下列的命令重複將 pid 589 加入 list 中 ```shell $ echo "add 589" | sudo tee /dev/hideproc $ echo "add 589" | sudo tee /dev/hideproc $ echo "add 589" | sudo tee /dev/hideproc ``` 接著查看隱藏的清單即可見三個 pid node ```shell $ sudo cat /dev/hideproc pid: 589 pid: 589 pid: 589 ``` 我們在 `hide_process()` 中加入檢查機制以避免將相同的 pid 加入 list 中 ```diff diff --git a/main.c b/main.c index c969b29..e99f137 100644 --- a/main.c +++ b/main.c @@ -115,7 +115,14 @@ static void init_hook(void) static int hide_process(pid_t pid) { - pid_node_t *proc = kmalloc(sizeof(pid_node_t), GFP_KERNEL); + pid_node_t *proc; + + /* Check if the pid is in the list */ + if (is_hidden_proc(pid)) + return -EAGAIN; + + /* insert pid node into hidden_proc */ + proc = kmalloc(sizeof(pid_node_t), GFP_KERNEL); proc->id = pid; list_add_tail(&proc->list_node, &hidden_proc); return SUCCESS; ``` 若我們這時嘗試重複加入一個 pid 589 時,就會產生以下的錯誤提示 ```shell $ echo "add 589" | sudo tee /dev/hideproc # first add add 589 $ echo "add 589" | sudo tee /dev/hideproc # second add add 589 tee: /dev/hideproc: File exists ``` ### 避免 `device_read()` 發生 buffer overflow > TODO ###### tags: `linux2021`