Try   HackMD

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() 使用 next_tgid() 遍尋所有程序,而其實作則使用 find_ge_pid()),所以若我們改變 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 隱藏

$ echo "add <pid>" | sudo tee /dev/hideproc

實際上進行處理的函式是在 device_write(),經由命令分析後,使我們能夠動態修改 hidden list。

允許其 PPID 也跟著隱藏

我參考 next_tgid() 的方式來進行實作

因為 parent 的資訊保存在 task_struct 結構裡面,所以我們需要依序經由以下步驟取得 ppid

  • find_get_pid(): 利用 pid 取得 struct pid 的位址
  • get_task_pid(): 透過 struct pid 的位址取得 struct task_struct 的位址
  • task->parent->pid: 得到 ppid

為了方便使用我將其包裝成 get_ppid():

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

這邊為了驗證模組行為,我準備了一個簡單的程式:

#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

編譯後,執行結果大致如下

$ 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 檢查二者是否可以被看見

$ 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 中

$ echo "addwp 25736" | sudo tee /dev/hideproc

透過讀取 /dev/hideproc 檢查二者是否被加入到 list 之中

$ sudo cat /dev/hideproc
pid: 25736
pid: 25735

此時若使用 ps aux 則無法查到兩個 test_proc 的狀態

$ 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 --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 對於 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 --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 --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 中

$ echo "add 589" | sudo tee /dev/hideproc
$ echo "add 589" | sudo tee /dev/hideproc
$ echo "add 589" | sudo tee /dev/hideproc

接著查看隱藏的清單即可見三個 pid node

$ sudo cat /dev/hideproc
pid: 589
pid: 589
pid: 589

我們在 hide_process() 中加入檢查機制以避免將相同的 pid 加入 list 中

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 時,就會產生以下的錯誤提示

$ 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