---
tags: linux-summer-2021
---
# 2021q3 Homework1 (quiz1)
contributed by < `RinHizakura` >
> [第 1 週測驗題](https://hackmd.io/@sysprog/linux2021-summer-quiz1?fbclid=IwAR3ttnqOehKTUa0lZoFAQgtFq3ec3YZ20woXR2rq72AGIwltTeA01GksFzQ)
> [GitHub](https://github.com/RinHizakura/hideproc)
## 執行環境
```
$ uname -r
5.4.0-77-generic
```
## kernel module
### 實作
#### `_hideproc_init`
```cpp
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;
}
```
由 `module_init(_hideproc_init)` 載入 module 的起始函數,這裡主要包含 3 個部份:
1. char device driver 的註冊
* [`alloc_chrdev_region`](https://www.kernel.org/doc/htmldocs/kernel-api/API-alloc-chrdev-region.html) 註冊一個 char device number,該函數呼叫後 kernel 會自動分配 char device number,保存在 `dev` 中
* `cdev` 是一個描述一個 char device 的關鍵結構,先透過 [`cdev_init`](https://www.kernel.org/doc/htmldocs/kernel-api/API-cdev-init.html) 初始化並定義其相關操作的對應行為(`open` / `read` / `write` 等 file operations, `fops`),再透過 [`cdev_add`](https://www.kernel.org/doc/htmldocs/kernel-api/API-cdev-add.html) 根據之前得到的 `dev` 向 kernel 註冊此 char device
2. 在 /dev 目錄下建立 device file 並註冊到 sysfs,因此可以從 user space 存取該 device
* `class_create` 建立 `struct class` 結構,其持有者為 `THIS_MODULE` 且 該 class 之名稱為 `DEVICE_NAME`
* [`device_create`](https://www.kernel.org/doc/html/latest/driver-api/infrastructure.html?highlight=device_create#c.device_create) 根據該 class 將 device 註冊到 sysfs
> [Diffrences between cdev_add and device_create function?](https://stackoverflow.com/questions/50377327/diffrences-between-cdev-add-and-device-create-function)
3. hook 的建立,詳見 [ftrace hook](#ftrace-hook)
#### `device_write`
`device_write` 被註冊為對 char device 的寫操作(`.write`) 之對應行為:
```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;
```
* 寫入內容的長度需滿足一定長度
```cpp
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;
}
```
為了使用 memcmp 將 write 要寫入的字串在 kernel 中與 "add" / "del" 做比對,這裡需要先透過 `copy_from_user` 將 buffer 內容複製一份到 user space,並做出對應的隱藏 / 解隱藏行為。
* [`kstrtol`](https://www.kernel.org/doc/htmldocs/kernel-api/API-kstrtol.html)
#### `hide_process`
```cpp
static int hide_process(pid_t pid)
{
pid_node_t *proc = kmalloc(sizeof(pid_node_t), GFP_KERNEL);
proc->id = pid;
CCC;
return SUCCESS;
}
```
對照 [ftrace hook](#ftrace-hook) 章節的想法,`hide_process` 的任務即為將欲隱藏的 pid 之 `pid_node_t` 結構加入到 linked list `hidden_proc` 之下。
* CCC = `list_add_tail(proc, hidden_proc)`
:::warning
:warning: 尚未釐清老師提出 `list_add` 會無法運行的原因?
:::
#### `unhide_process`
```cpp
static int unhide_process(pid_t pid)
{
pid_node_t *proc, *tmp_proc;
BBB (proc, tmp_proc, &hidden_proc, list_node) {
DDD;
kfree(proc);
}
return SUCCESS;
}
```
反之,`unhide_process` 的工作是要將目標 pid 所對應的節點從 linked list 中刪除。
* BBB = `list_for_each_entry_safe`
* DDD = `list_del(proc)`
:::info
這部份有實作錯誤,參見 [unhide_process 的錯誤](#unhide_process-的錯誤)
:::
## ftrace hook
> * [Using ftrace to hook to functions](https://www.kernel.org/doc/html/latest/trace/ftrace-uses.html)
> * [ilammy/ftrace-hook](https://github.com/ilammy/ftrace-hook)
> * [Ftrace Hook (Linux内核热补丁) 详解](https://blog.csdn.net/pwl999/article/details/107426138)
> * [Hooking Linux Kernel Functions, Part 1: Looking for the Perfect Solution](https://www.apriorit.com/dev-blog/544-hooking-linux-functions-1)
> * [Hooking Linux Kernel Functions, Part 2: How to Hook Functions with Ftrace](https://www.apriorit.com/dev-blog/546-hooking-linux-functions-2)
ftrace 的技術使得可以將 callback 附加在 kernel function 的開頭,使得可以記錄和跟踪 kernel 的運作流程,甚至是達到即時修補 Linux 中的安全漏洞和錯誤,而無須重新啟動的 [live kernel patching](https://en.wikipedia.org/wiki/Kpatch) 目的。儼然是 Linux kernel 中相當重要的技術,下面針對此測驗題中對 ftrace hook 之運用方式與流程進行討論。
### 實作
#### `init_hook`
```cpp
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);
}
```
`init_hook` 先將 `struct ftrace_hook` 的相關成員進行初始化:
* `kallsyms_lookup_name` 可以根據給定的 symbol 得到其位址,這裡的目標是 [`find_ge_pid`](https://github.com/torvalds/linux/blob/master/kernel/pid.c#L518),其回傳 `struct pid *` 而輸入參數為 `int nr, struct pid_namespace *ns`,也因此和 typedef 對應
```cpp
typedef struct pid *(*find_ge_pid_func)(int nr, struct pid_namespace *ns);
```
而 `struct ftrace_hook` 的結構如下
```cpp
struct ftrace_hook {
const char *name;
void *func, *orig;
unsigned long address;
struct ftrace_ops ops;
};
```
* `name`: ftrace 所要 hook 的目標函數之 symbol 名稱
* `func`: 用來替換 symbol 原本對應之函數
* `orig`: 指向 symbol 原本對應之函數
* `address`: 被 hook 的函數之地址
* `ops`: 使 ftrace 提供服務之相關操作
:::warning
1. `MODULE_LICENSE` 的設置會影響實際可以存取的 export symbol
2. 較新版的 Linux 將不再可以使用此查詢 symbol,詳見 [Unexporting kallsyms_lookup_name()](https://lwn.net/Articles/813350/)
:::
#### `hook_install`
```cpp
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;
}
return 0;
}
```
* `hook_resolve_addr` 查找要被hook函數之地址(`hook->address`),並使得 `hook->orig` 也指向該位址
* 將 `ftrace_ops` 結構初始化
* `hook->ops.func` 設定為 `hook_ftrace_thunk`,後者是 ftrace 追蹤的 callback
* `hook->ops.flags` 調整 ftrace 時對 register
* `FTRACE_OPS_FL_SAVE_REGS`: 允許對 pt_regs 的讀寫
* `FTRACE_OPS_FL_IPMODIFY`: 同時設置 `FTRACE_OPS_FL_SAVE_REGS` 才有效,使得可以修改 pt_regs->ip,把原本要追蹤的函式替換成其他函式
* 首先用 `ftrace_set_filter_ip()` 為跟蹤函數打開 ftrace(第 3 個參數設為 0)
* 再透過 `register_ftrace_function()` 對被 hook 之函數進行註冊,允許 ftrace 執行我們給定的 callback
* 如果註冊失敗,需要 `ftrace_set_filter_ip` (第 3 個參數設為 1) 關閉對其 ftrace
#### `hook_ftrace_thunk`
```cpp
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;
}
```
在 `hook_install` 之後,`hook_ftrace_thunk` 是 ftrace 所呼叫的 callback,會插入追蹤函數的入口點中。而要達到替換掉追蹤函數,則要將 `hook_ftrace_thunk` 做為中繼點,替換 `regs->ip`。
這裡的替換函數取得是透過 `ops`。因為 `ops` 屬於 struct hook 結構的成員之一,可以藉由 `container_of` 拿到先拿到 hook 再取得其成員 hook->func。
* 如果 `hook->func` 中再次調用追蹤函數,會造成無窮的遞迴呼叫。因此要先用 `within_module` 檢查 `parent_ip`(調用 hook 的返回地址)是否等於 `THIS_MODULE`,只有在第一次呼叫時執行 `hook->func`
#### `hook_find_ge_pid`
```cpp
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;
}
```
`hook_find_ge_pid` 是 `hook->func` 所指向之函數,作為替代原本的 `find_ge_pid` 去做到隱藏的效果。其行為是多次調用原本的函數(`real_find_ge_pid`) 去略過所要隱藏的 pid,達到隱藏 pid 的效果。
#### `is_hidden_proc`
```cpp
static bool is_hidden_proc(pid_t pid)
{
pid_node_t *proc, *tmp_proc;
AAA (proc, tmp_proc, &hidden_proc, list_node) {
if (proc->id == pid)
return true;
}
return false;
}
```
`is_hidden_proc` 檢查此 pid 是否與 module 所維護的 linked list `hidden_proc` 中其一節點之 id 相符
* AAA = `list_for_each_entry_safe`
## 程式碼改進
### 資源釋放
顯然 `_hideproc_exit` 之處需要進行必要的資源釋放(~~不然一 remove 就當機,哭啊~~),原則上就是照著取得資源的相反步驟釋放回去。需要進行的工作包含:
1. 將 linked list 上的節點釋放(如果存在)
2. 解除 hook
3. `device_create` 對應 `device_destroy`: 將在 /dev 目錄下建立的 device file 刪除,從 sysfs 取消註冊
4. `cdev_add` 對應 `cdev_del` 將 char device 刪除
5. `class_create` 對應 `class_destroy` 釋放 `struct class` 相關資源
6. `alloc_chrdev_region` 對應 `unregister_chrdev_region` 釋放 device number
```cpp
static void _hideproc_exit(void)
{
pid_node_t *proc, *tmp_proc;
/* free pid_node_t allocated form kmalloc */
list_for_each_entry_safe (proc, tmp_proc, &hidden_proc, list_node) {
list_del(&proc->list_node);
kfree(proc);
}
hook_remove(&hook);
device_destroy(hideproc_class, dev);
cdev_del(&cdev);
class_destroy(hideproc_class);
unregister_chrdev_region(dev, MINOR_VERSION);
printk(KERN_INFO "@ %s\n", __func__);
}
```
### `unhide_process` 的錯誤
unhide_process 的行為與期待不一致,將所有 linked list 中的 node 對應之 `pid` 都解除隱藏了,修改如下:
```cpp
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) {
if(proc->id == pid) {
list_del(&proc->list_node);
kfree(proc);
}
}
return SUCCESS;
}
```
### `kallsyms_lookup_name` 的替代方案
雖然我的 kernel 版本可以直接使用 `kallsyms_lookup_name`,然而未雨綢繆,不妨先整理相關的知識起來放。
[ilammy/ftrace-hook](https://github.com/ilammy/ftrace-hook/blob/master/ftrace_hook.c) 已經展示了一種可行的解法是透過 kprobe:
```cpp
#if LINUX_VERSION_CODE >= KERNEL_VERSION(5,7,0)
static unsigned long lookup_name(const char *name)
{
struct kprobe kp = {
.symbol_name = name
};
unsigned long retval;
if (register_kprobe(&kp) < 0) return 0;
retval = (unsigned long) kp.addr;
unregister_kprobe(&kp);
return retval;
}
#else
static unsigned long lookup_name(const char *name)
{
return kallsyms_lookup_name(name);
}
#endif
```
## 延伸功能
### 隱藏 ppid
我們可以從一個 `pid` 去取得其 `task_struct` 來得知其 parent 之 pid。如下為從 child pid 取得其 parent pid 的函式封裝:
```cpp
#define find_task_by_pid(pid) pid_task(find_vpid(pid), PIDTYPE_PID)
static inline pid_t get_ppid_by_pid(pid_t pid)
{
struct task_struct *task = find_task_by_pid(pid);
if (task->parent)
return task->parent->pid;
return 0;
}
```
:::warning
另一個版本是取得 `task_struct` 的 instance (會將 reference count + 1)。由於對 kernel codes 的撰寫不甚熟悉,尚需釐清兩者的具體差異和適合的使用情境
```cpp
#define get_task_by_pid(pid) get_pid_task(find_get_pid(PID), PIDTYPE_PID)
```
:::
然後改寫 device_write 如下:
```diff
static ssize_t device_write(struct file *filep,
const char *buffer,
size_t len,
loff_t *offset)
{
...
+ pid_t ppid;
...
if (!memcmp(message, add_message, sizeof(add_message) - 1)) {
kstrtol(message + sizeof(add_message), 10, &pid);
hide_process(pid);
+ // hide also the parent process
+ ppid = get_ppid_by_pid(pid);
+ if (ppid)
+ hide_process(ppid);
} else if (!memcmp(message, del_message, sizeof(del_message) - 1)) {
kstrtol(message + sizeof(del_message), 10, &pid);
unhide_process(pid);
+ // unhide also the parent process
+ ppid = get_ppid_by_pid(pid);
+ if (ppid)
+ unhide_process(ppid);
}
...
}
```
並藉由以下實驗初步的驗證其 parent process 之資訊確實被隱藏:
```
$ pidof bash
7939
$ pstree -s -p 7939
systemd(1)───systemd(1883)───gnome-shell(2125)───terminator(7932)───bash(7939)───vim(8029)
$ ps -f 7932
UID PID PPID C STIME TTY STAT TIME CMD
rin 7932 2125 0 19:07 ? Sl 0:33 /usr/bin/python3 /usr/bin/terminator
$ echo "add 7939" | sudo tee /dev/hideproc
$ ps -f 7932
UID PID PPID C STIME TTY STAT TIME CMD
$ echo "del 7939" | sudo tee /dev/hideproc
$ ps -f 7932
UID PID PPID C STIME TTY STAT TIME CMD
rin 7932 2125 0 19:07 ? Sl 0:34 /usr/bin/python3 /usr/bin/terminator
```
:::warning
需釐清之問題:
* unhide 時會再次 access `find_task_by_pid`,其成本是否可以承受? 或者需要在 kernel module 中透過資料結構去 cache 住 parent pid
* 一個 process 的 parent 可能會在系統運行中途改變嗎? 此設計是否有不足之處?
:::