# 2021q3 Homework1 (quiz1)
contributed by < `demonsome` >
###### tags: `2021 Summer - Linux kernel`
> [第 1 週測驗題題目](https://hackmd.io/@sysprog/linux2021-summer-quiz1)
:::warning
注意書寫規範: 中英文間用一個空白字元區隔。唯有掌握各項細節,方可征服浩瀚的核心。
:notes: jserv
:::
## 延伸問題
**1. 解釋上述程式碼運作原理,包含 `ftrace` 的使用**
#### ftrace 以及 hook
此程式碼一開始即定義了包含 `ftrace_ops` 物件的結構 `ftrace_hook` ,整個程式碼的基本功能是由 `ftrace` 功能的延伸,詳細資訊可參考[連結](https://www.kernel.org/doc/html/latest/trace/ftrace-uses.html)。
我們可將 `ftrace_op` 附在 [callback function](https://en.wikipedia.org/wiki/Callback_(computer_programming)) 上,用以追蹤程式在 kernel 中的流程。 `ftrace` 通常被運用於系統的安全監控中,尤其在一些工作負載重的科學系統可以在不重啟系統的情形下,對系統核心的工作執行追蹤。
這裡是 `ftrace_ops` 的結構:
```cpp
struct ftrace_ops ops = {
.func = my_callback_func,
.flags = MY_FTRACE_FLAGS
.private = any_private_data_structure,
};
```
`ftrace_op` 需事先註冊才能開始追蹤函式的呼叫,註冊的動作定義在 `hook_install` 函式之中(如下),函式會透過 `hook_resolve_addr` 去找尋 kernel's symbol table 中 hook 名稱對應的位址,並指派給變數`err`。一旦 `err` 變數值非零,則回傳 hook 安裝失敗的訊息。
:::info
`hook_resolve_addr` 函式中的 `kallsyms_lookup_name` 的函式在較新的linux版本需要進行更新,才能運作。
:::
```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;
}
```
其中 `hook_ftrace_thunk` 函式為 `ftrace` 的callback function,注意到這裡的`notrace`識別字是為了讓 `hook_ftrace_thunk` 無法被`ftrace`追蹤。就此情況來說,函式加上 `notrace` 識別字是為了避免不必要的遞迴呼叫。
kernel 會在此 callback function 找到結構 `ops` 所對應的起始位址,並指派給指標 hook。而一旦我們發現 `parent_ip` 的位址不在這個模組記憶體區塊的範圍內,則結構 [`pt_regs`](https://github.com/spotify/linux/blob/master/arch/x86/include/asm/ptrace.h) 中的指令位址(instruction pointer, ip)會被改成 `hook` 的 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;
}
```
#### 隱藏 process 的機制
這裡藉由一個 doubly-linked list 存放要隱藏的 process ID ,這個 list 的第一個節點被指標 `hidden_proc` 所指向,每個節點的結構叫做 `pid_node_t` ,包含了一個 `list_head` 結構以及一個 process ID 值。
```c=
typedef struct {
pid_t id;
struct list_head list_node;
} pid_node_t;
LIST_HEAD(hidden_proc);
```
示意圖:

為了欺騙 kernel ,讓 kernel 以為某個 process 不存在,我們需要取的 kernel 中的 process ID 資訊。
作法上,使用 `ftrace` 去追蹤 `find_ge_pid_func` 結構,這個結構是一個 [`pid`](https://github.com/torvalds/linux/blob/master/include/linux/pid.h) 的結構,藉由傳入`int nr` 以及 `struct pid_namespace *ns` ,我們可取得結構 `pid` 的 process ID。
接著我們需要對管理 process 的結構進行追蹤。可以使用 `ftrace` 機制建立一個 hook 。在初始化一個 hook 時,我們將 hook 結構中的函式指標指向 `hook_find_ge_pid` 函式。
```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 的結構長這樣:
```c=
struct ftrace_hook {
const char *name;
void *func, *orig;
unsigned long address;
struct ftrace_ops ops;
};
```
`hook_find_ge_pid` 函式會先去查找目前存在於記憶體的 process 有哪些,接著去比對這些 process 的 ID 是否存在於要隱藏 process 的 doubly-linked list 。若有,則回傳要隱藏的 process ID。
```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 file 初始化及 process 隱藏
當我們插入 kernel module 到 kernel 時, kernel 會先初始化一個 device file 並等待終端的輸入。
接著準備一個 `hook` , `hook` 的函式`hook_find_ge_pid` 會在 `hidden_proc` 所指向的 doubly linked list 中找出要隱藏 process ID。
最後, callback function -- `hook_ftrace_thunk` 將 `regs` 的 instruction point 做更改,使 kernel 無法辨別隱藏的 process ID。
```cpp
#define MINOR_VERSION 1
#define DEVICE_NAME "hideproc"
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;
}
static void _hideproc_exit(void)
{
printk(KERN_INFO "@ %s\n", __func__);
/* FIXME: ensure the release of all allocated resources */
}
module_init(_hideproc_init);
module_exit(_hideproc_exit);
```
當我們在 terminal 輸入 `echo "add 644" | sudo tee /dev/hideproc` ,註冊的裝置會讀取字串,並將 `add 644` 解讀成將 process ID 為 644 的行程加到要隱藏的 doubly-linked list 之中。反之, `"del 644"` 則是實作移除動作,使 process ID 不再被隱藏,可以t再次被核心辨別。
```cpp
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)) {
kstrtol(message + sizeof(add_message), 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);
} else {
kfree(message);
return -EAGAIN;
}
*offset = len;
kfree(message);
return len;
}
```
**2. 本程式僅在 Linux v5.4 測試,若你用的核心較新,請試著找出替代方案 2020 年的變更 Unexporting kallsyms_lookup_name() Access to kallsyms on Linux 5.7+**
我使用的核心版本為 `5.8.0-63-generic` ,編譯時問題就發生了。

2020 年 [Unexporting kallsyms_lookup_name()](https://lwn.net/Articles/813350/) 討論 `kallsyms_lookup_name()` 這個函式存在的漏洞。
文中提到,每個模組已經有足夠的機制可以去 kernel 找出一個 symbol 對應的位址。 `kallsyms_lookup_name()` 像是一個後門,筆者擔心被濫用。因此 Will Deacon 發布了一系列的 patch 以移除 `kallsyms_lookup_name()` 的輸出。
雖然,取消了這個函式讓許多正當使用的(如: kernel live patching)工程師以及業餘開發者感到不便 :upside_down_face: ,但事實上還是有許多方法可以在 kernel 中找到 symbol 所對應的位址。
Masami Hiramatsu 舉了[幾個方法](https://lwn.net/ml/linux-kernel/20200221232746.6eb84111a0d385bed71613ff@kernel.org/),一個是先去取得 symbol map 再用它去找位址,另一個則是使用 `kprobes` 去取得位址。 若不考慮執行時間的話,也可以使用 "%pF" 格式的 `snprintf()`去查詢。
我這裡使用 `kprobes` 去取得位址,並參考 [`hankluo6`](https://hackmd.io/@hankluo6/hideproc) 同學的作法,程式碼請參[連結](https://github.com/demonsome/linux2021q3/blob/main/quiz1/main.c)。 process 可以被正確隱藏如下圖:

**3. 本核心模組只能隱藏單一 PID,請擴充為允許其 PPID 也跟著隱藏,或允許給定一組 PID 列表,而非僅有單一 PID 指出程式碼可改進的地方,並動手實作**
```clike=
$ pidof firefox
3066 2999 2956 2881
$ pstree -p | grep "firefox"
| |-firefox(2881)-+-Privileged Cont(2956)-+-{Privileged Cont}(2962)
| | |-{firefox}(2888)
| | |-{firefox}(2889)
| | |-{firefox}(2894)
| | |-{firefox}(2895)
| | |-{firefox}(2896)
. . .
. . .
. . .
```