# 2021q3 Homework1 (quiz1)
contributed by < `ChinYikMing` >
###### tags: `Linux 作業系統`
> [GitHub](https://github.com/ChinYikMing/Linux-2021-summer/blob/main/main.c)
## 此Bash Script簡化了新增和刪除
Note: 使用此 script 之前,必須先 insmod module_name
```shell
#!/bin/bash
usage() {
echo "$0 add|del PID[,...]"
echo "add for adding PID[,...] to list to hide"
echo "del for deleting PID[,...] from list to show"
echo "Note:"
echo " del -1 will delete all PID from list to show"
}
dev="/dev/hideproc"
re='^(-?[0-9]+,?)*$'
if [ "$#" -ne 2 ] || [[ "$1" != "add" && "$1" != "del" ]] || ! [[ $2 =~ $re ]]; then
usage
else
echo "$1 $2" | sudo tee "$dev"
fi
```
## 延伸問題
### 1.0 解釋程式碼運作原理
目的 :隱藏特定的 PID
流程 :
1. 掛載核心模組時,建立 device file(`/dev/hideproc`) 且初始化 ftrace hook。ftrace hook可以註冊我們自定義的 function 來 hook 原本的 function。在這裡,我們是 hook find_ge_pid。get_ge_pid 均被 ps(1), pidof(1), top(1), pstree(1), pgrep(1)等其他查詢 process 狀態的 cmd 使用,所以 hook 這個 function,就可以達到以上的目的。使用 ftrace 來 hook get_ge_pid 的細節會在下面 1.1 討論。
2. 當我們使用 `add pid` 或 `del pid` 命令和 device file( /dev/hideproc ) 互動的時候,能夠分別顯示或隱藏特定的 PID。例如`echo "add 1" | sudo tee /dev/hideproc` 就會隱藏 process id 為 1 的 process; `echo "del 1" | sudo tee /dev/hideproc` 就會顯示 process id 為 1 的 process。實作方法是使用 Linux kernel API 的 List Management Functions 維護一個 doubly circular linked list。`add pid` 會增加這個 linked list 的節點;`del pid` 會從這個 linked list 刪除節點。由此可見,這個 linked list 的所有節點是要被隱藏的process id
3. 在 rmmod module 的時候,只需要把原本註冊的 ftrace hook 釋放掉,即可恢復 ps(1), pidof(1), top(1), pstree(1), pgrep(1)等其他查詢 process 狀態的 cmd 使用回 `find_ge_pid`
### 1.1 使用 ftrace 來 hook find_ge_pid
```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;
}
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_install 的時候,要使用的 callback function 是 hook_ftrace_thunk,這個 callback function 會在 process 每次使用 `get_ge_pid` 的時候把暫存器的 ip 改成 hook 的 func(`hook_find_ge_pid`),進而攔截 `get_ge_pid`
* 要能夠修改暫存器的 ip,struct `ftrace_ops` 的 flags 必須有 `FTRACE_OPS_FL_IPMODIFY`,要使用 `FTRACE_OPS_FL_IPMODIFY` flag,還必須有 `FTRACE_OPS_FL_SAVE_REGS` flag,否則 callback function 的 regs 會是 garbage value 或 NULL
* struct `ftrace_ops` 初始化之後,就可以使用 `ftrace_set_filter_ip` 指定我們要追蹤的函式(這裡是 `get_ge_pid`),然後使用 `register_ftrace_function` 註冊 hook
參考: [Using ftrace to hook to functions](https://www.kernel.org/doc/html/latest/trace/ftrace-uses.html)
Note:在查資料的時候,有看到類似 ftrace 的 cmd 工具:trace-cmd
* trace PID: `sudo trace-cmd record -p function -P PID`
* trace PID(比較美的輸出格式): `sudo trace-cmd record -p function_graph -P PID`
* 查看 report: `sudo trace-cmd report`
* 參考: [ftrace: trace your kernel functions!](https://jvns.ca/blog/2017/03/19/getting-started-with-ftrace/)
### 2 本核心模組只能隱藏單一 PID,請擴充為允許其 PPID 也跟著隱藏,或允許給定一組 PID 列表,而非僅有單一 PID
- 允許給定一組 PID 列表跟著隱藏的 kernel module
Note: PID 和 PID 之間要用 ,隔開。e.g., `echo "add 1,2,3" | sudo tee /dev/hideproc` 會隱藏 PID 為 1, 2 和 3 的 process 或者可以使用上面一開始提供的 bash script: ./scriptname add 1,2,3
```cpp
typedef int (*hide_handler)(pid_t);
void device_write_handler(char *message,
size_t message_len,
size_t cmd_len,
hide_handler handler)
{
long pid;
char *ptr, *qtr, *end;
ptr = message + cmd_len; /* skip cmd */
end = message + message_len;
while(1){
qtr = memchr(ptr, ',', end - ptr);
if(!qtr){
kstrtol(ptr, 10, &pid);
handler(pid);
break;
}
*qtr = '\0';
kstrtol(ptr, 10, &pid);
handler(pid);
ptr += (qtr - ptr + 1);
}
}
static ssize_t device_write(struct file *filep,
const char *buffer,
size_t len,
loff_t *offset)
{
char *message;
char add_message[] = "add", del_message[] = "del";
if (len < sizeof(add_message) - 1 && len < sizeof(del_message) - 1)
goto err;
/* guaratees message contains PID */
if(len >= 5){
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)) {
device_write_handler(message, len, sizeof(add_message), hide_process);
} else if (!memcmp(message, del_message, sizeof(del_message) - 1)) {
device_write_handler(message, len, sizeof(del_message), unhide_process);
} else {
kfree(message);
goto err;
}
*offset = len;
kfree(message);
return len;
}
err:
return -EAGAIN;
}
```
- 允許 PID 的 PPID 跟著隱藏的 kernel module
把以上例子的 device_write_handler 改成以下即可`
`Note: 刪除 PID 的時候,PPID 也會跟著刪除, 若 PID 為 -1 會刪除所有 PID
```cpp
void device_write_handler(char *message,
char *cmd,
hide_handler handler)
{
struct task_struct *tsk;
pid_t ppid;
long pid;
char *ptr, *qtr, *end;
ptr = message + strlen(cmd) + 1; /* skip cmd */
end = message + strlen(message);
while(1){
qtr = memchr(ptr, ',', end - ptr);
if(!qtr){
kstrtol(ptr, 10, &pid);
tsk = pid_task(find_vpid(pid), PIDTYPE_PID);
if(!tsk){
if(!memcmp(cmd, "del", 3) && pid == -1) /* remove all hideproc */
handler(pid);
break;
}
if(tsk->parent){
ppid = tsk->parent->pid;
handler(ppid);
}
handler(pid);
break;
}
*qtr = '\0';
kstrtol(ptr, 10, &pid);
tsk = pid_task(find_vpid(pid), PIDTYPE_PID);
if(!tsk)
goto next;
if(tsk->parent){
ppid = tsk->parent->pid;
handler(ppid);
}
handler(pid);
next:
ptr += (qtr - ptr + 1);
}
}
```
參考:<s>https://www.programmersought.com/article/8956801558/</s>
:::warning
不要參照 programmersought 網站,其內容不完整。儘量找第一手材料
:notes: jserv
:::
### 3 指出程式碼可改進的地方,並動手實作
* `unhide_process` 函式在刪除 PID 的時候會把所有 PID 刪除,我認為可以提供多一點彈性,例如根據給定的 PID 或 一組 PID 來刪除 PID 以及若 PID 為 -1 代表要刪除所有 PID。此外,刪除 PID 的時候,應該要先檢查資料結構(以下以 linked list 為例)是否為空,若是空的就不必再去走訪
```cpp
static int unhide_process(pid_t pid)
{
pid_node_t *proc, *tmp_proc;
if(hash_empty(hidden_proc)){
pr_info("hash table is empty\n");
return -ENOENT;
}
if(pid == -1){
list_for_each_entry_safe (proc, tmp_proc, &hidden_proc, list_node) {
list_del(&proc->list_node);
kfree(proc);
}
} else {
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;
}
```
* 在 rmmod module 的時候,應該要釋放對應的資源
```cpp
static void _hideproc_exit(void)
{
printk(KERN_INFO "@ %s\n", __func__);
/* FIXME: ensure the release of all allocated resources */
hook_remove(&hook);
device_destroy(hideproc_class, MKDEV(dev_major, MINOR_VERSION));
cdev_del(&cdev);
class_destroy(hideproc_class);
}
```
* add PID,PID 可能不是合法的 PID,所以應該要去檢查 PID 是否合法再加到 linked list。(已在 device_write_handler 實作中)
範例請參考:https://github.com/ChinYikMing/Linux-2021-summer/blob/main/main.c
* PPID 可能是很多個 process 共享的,所以 PPID 在資料結構可能會有重複,所以應該要去檢查 PPID 是否已經存在資料結構內,解決方法是修改 `hide_process` function 即可
```cpp
static int hide_process(pid_t pid)
{
...
if(is_hidden_proc(pid)){
pr_info("PID=%d is already hidden\n", pid);
return -EINVAL;
}
...
}
```
* linked list的搜尋時間複雜度可能是 `O(n)`,所以把 linked list 改成 hash table 可能可以加快搜尋的時間 `O(1)`,但要在量大的時候才有明顯差別
主要修改程式碼有:
* `pid_node_t` structure
* hash table initialization
* `is_hidden_proc` function
* `hide_process` function
* `unhide_process` function
* `device_read` function
範例請參考: [main.c](https://github.com/ChinYikMing/Linux-2021-summer/blob/main/main.c)
hash table API參考:https://github.com/torvalds/linux/blob/5bfc75d92efd494db37f5c4c173d3639d4772966/include/linux/hashtable.h