# 2021q3 Homework1 (quiz1)
contributed by < `OscarShiang` >
## 解釋 hideproc 程式碼運作原理,包含 ftrace 的使用
`hideproc` 的原理在於使用 ftrace hook 進行 live patching 進而更改 `find_ge_pid()` 函式的行為。
當我們利用 `ps` 或是列出 `/proc/` 目錄底下的程序時,實際上會使用到 `find_ge_pid()` 函式來進行查找 (在 `/fs/proc/base.c` 中的 [`proc_pid_readdir()`](https://elixir.bootlin.com/linux/v5.4.80/source/fs/proc/base.c#L3313) 使用 [`next_tgid()`](https://elixir.bootlin.com/linux/v5.4.80/source/fs/proc/base.c#L3275) 遍尋所有程序,而其實作則使用 [`find_ge_pid()`](https://elixir.bootlin.com/linux/v5.4.80/source/fs/proc/base.c#L3284)),所以若我們改變 `find_ge_pid()` 的行為,就可以達到隱藏程序的效果。
但是我們的目的並不是要完全取代原本的實作,而是偏移部份程序查照的結果。我們使用的方式是利用 ftrace 註冊 `hook_ftrace_thunk()` 函式,當有程序想要呼叫 `find_ge_pid()` 時, ftrace 會將其跳轉到我們先前指定的 `hook_ftrace_thunk()` 函式,透過更改 Instruction Pointer 位址從而跳轉到 `hook_find_ge_pid()` ,利用原先的 `find_ge_pid()` 函式進行操作後,如果預期的 pid 是我們想要隱藏者時,我們將代入到 `find_ge_pid()` 的 `pid` + 1 讓其無法取得預期的結果,將列表中的 pid 隱藏。
而我們操作 `hidden_proc` 的 hidden list 的方式是使用 VFS 來進行。
在範例程式中,我們使用以下命令將指定 pid 隱藏
```shell
$ echo "add <pid>" | sudo tee /dev/hideproc
```
實際上進行處理的函式是在 `device_write()`,經由命令分析後,使我們能夠動態修改 hidden list。
## 允許其 PPID 也跟著隱藏
我參考 [`next_tgid()`](https://elixir.bootlin.com/linux/v5.4.80/source/fs/proc/base.c#L3275) 的方式來進行實作
因為 parent 的資訊保存在 `task_struct` 結構裡面,所以我們需要依序經由以下步驟取得 ppid
- [`find_get_pid()`](https://elixir.bootlin.com/linux/v5.4.80/source/kernel/pid.c#L393): 利用 pid 取得 `struct pid` 的位址
- [`get_task_pid()`](https://elixir.bootlin.com/linux/v5.4.80/source/kernel/pid.c#L381): 透過 `struct pid` 的位址取得 `struct task_struct` 的位址
- `task->parent->pid`: 得到 ppid
為了方便使用我將其包裝成 `get_ppid()`:
```cpp
static pid_t get_ppid(pid_t pid)
{
struct pid *pid_struct;
struct task_struct *task;
pid_struct = find_get_pid(pid);
task = get_pid_task(pid_struct, PIDTYPE_PID);
return task->parent->pid;
}
```
接著在模組中新增一個新的命令 `addwp` (add with parent) 用以將指定 pid 以及其 parent pid 加入隱藏的 list 中
> 相關的 commit 可以參考 [`50f8eb9`](https://github.com/OscarShiang/hideproc/commit/50f8eb9c86b57b917759f6d41bb2b731139fa76b)
這邊為了驗證模組行為,我準備了一個簡單的程式:
```cpp
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
int main(void)
{
printf("My pid is \t %d\n", getpid());
pid_t child = fork();
if (!child) {
pause();
} else {
printf("Child's pid is \t %d\n\n", child);
printf("(Use Ctrl + C to exit)\n");
wait(0);
}
return 0;
}
```
上述這段程式碼的作用就是產生一組 process 與 child process,並印出兩者的 pid
編譯後,執行結果大致如下
```shell
$ gcc -o test_proc test_proc.c
$ ./test_proc
My pid is 25735
Child's pid is 25736
(Use Ctrl + C to exit)
```
我們可以透過 `ps aux` 檢查二者是否可以被看見
```shell
$ ps aux | grep test_proc
ubuntu 25735 0.0 0.0 2488 588 pts/1 S+ 21:17 0:00 ./test_proc
ubuntu 25736 0.0 0.0 2488 84 pts/1 S+ 21:17 0:00 ./test_proc
ubuntu 25738 0.0 0.0 8160 736 pts/0 S+ 21:21 0:00 grep --color=auto test_proc
```
接著我們使用 `addwp` 命令將 child process pid 加入到 hidden list 中
```shell
$ echo "addwp 25736" | sudo tee /dev/hideproc
```
透過讀取 `/dev/hideproc` 檢查二者是否被加入到 list 之中
```shell
$ sudo cat /dev/hideproc
pid: 25736
pid: 25735
```
此時若使用 `ps aux` 則無法查到兩個 `test_proc` 的狀態
```shell
$ ps aux | grep test_proc
ubuntu 25771 0.0 0.0 8160 668 pts/0 S+ 21:25 0:00 grep --color=auto test_proc
```
## 允許給定一組 PID 列表,而非僅有單一 PID
> TODO
## 改進實作
### 離開模組時釋放相關資源
在原本的實作中,因為沒有 exit function 中實作資源釋放。所以如果我們將模組載入後移除,使用 `insmod` 重新載入模組時會得到 `Killed` 的輸出,並無法再次載入模組。
解決的方式就是在 `_hideproc_exit()` 加入與 `_hideproc_init()` 成對的資源釋放即可。
```diff
diff --git a/main.c b/main.c
index 45922af..c9e42b2 100644
--- a/main.c
+++ b/main.c
@@ -63,7 +63,6 @@ static int hook_install(struct ftrace_hook *hook)
return 0;
}
-#if 0
void hook_remove(struct ftrace_hook *hook)
{
int err = unregister_ftrace_function(&hook->ops);
@@ -73,7 +72,6 @@ void hook_remove(struct ftrace_hook *hook)
if (err)
printk("ftrace_set_filter_ip() failed: %d\n", err);
}
-#endif
typedef struct {
pid_t id;
@@ -212,10 +210,11 @@ static const struct file_operations fops = {
#define MINOR_VERSION 1
#define DEVICE_NAME "hideproc"
+static dev_t dev;
+
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);
@@ -235,7 +234,21 @@ static int _hideproc_init(void)
static void _hideproc_exit(void)
{
printk(KERN_INFO "@ %s\n", __func__);
- /* FIXME: ensure the release of all allocated resources */
+
+ /* Destroy the hidden list */
+ 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);
+ }
+
+ /* Unregister the device */
+ device_destroy(hideproc_class, MKDEV(MAJOR(dev), MINOR_VERSION));
+ cdev_del(&cdev);
+ class_destroy(hideproc_class);
+ unregister_chrdev_region(MKDEV(dev, MINOR_VERSION), MINOR_VERSION);
+ hook_remove(&hook);
}
module_init(_hideproc_init);
```
### `kstrtol` 回傳值檢查
根據 [Kernel API Doc](https://www.kernel.org/doc/htmldocs/kernel-api/API-kstrtol.html) 對於 return value 的描述
> Returns 0 on success, -ERANGE on overflow and -EINVAL on parsing error. Used as a replacement for the obsolete simple_strtoull. Return code must be checked.
但在 `device_write()` 處並沒有針對 `kstrtol()` 的回傳值做檢查,
因此需要在這邊加上檢查
```diff
diff --git a/main.c b/main.c
index 9593cf2..c969b29 100644
--- a/main.c
+++ b/main.c
@@ -170,6 +170,7 @@ static ssize_t device_write(struct file *filep,
size_t len,
loff_t *offset)
{
+ int ret;
long pid;
char *message;
@@ -181,11 +182,17 @@ static ssize_t device_write(struct file *filep,
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);
+ ret = kstrtol(message + sizeof(add_message), 10, &pid);
+ if (!ret)
+ hide_process(pid);
+ else
+ return ret;
} else if (!memcmp(message, del_message, sizeof(del_message) - 1)) {
- kstrtol(message + sizeof(del_message), 10, &pid);
- unhide_process(pid);
+ ret = kstrtol(message + sizeof(del_message), 10, &pid);
+ if (!ret)
+ unhide_process(pid);
+ else
+ return ret;
} else {
kfree(message);
return -EAGAIN;
```
因為 `kstrtol` 錯誤原因有兩種,所以不直接回傳 `-EINVAL`,而是將 `kstrtol` 產生的回傳值回傳回去。
### 取消隱藏指定 pid
在原本的實作中,`hideproc` 在收到 `del <pid>` 的命令時並不只會將我們指定的 pid 從隱藏列表中刪除,而是會將所有列表中的 node 全部刪除
因此我在 `unhide_process()` 中加上核對 pid 的機制,避免其將所有節點刪除
```diff
diff --git a/main.c b/main.c
index 5b63b66..f529c1e 100644
--- a/main.c
+++ b/main.c
@@ -133,8 +133,11 @@ 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)
{
- list_del(&proc->list_node);
- kfree(proc);
+ if (proc->id == pid) {
+ list_del(&proc->list_node);
+ kfree(proc);
+ break;
+ }
}
return SUCCESS;
}
```
### 重複加入已被隱藏的 pid
我們可以透過對 `/dev/hideproc` 寫入命令來增刪我們想要隱藏的 pid,但是在 `hide_process()` 函式中我們並不會對 pid 進行任何的檢查,因此就會有 list 中有重複好幾個 pid 的情況產生
我們可以利用下列的命令重複將 pid 589 加入 list 中
```shell
$ echo "add 589" | sudo tee /dev/hideproc
$ echo "add 589" | sudo tee /dev/hideproc
$ echo "add 589" | sudo tee /dev/hideproc
```
接著查看隱藏的清單即可見三個 pid node
```shell
$ sudo cat /dev/hideproc
pid: 589
pid: 589
pid: 589
```
我們在 `hide_process()` 中加入檢查機制以避免將相同的 pid 加入 list 中
```diff
diff --git a/main.c b/main.c
index c969b29..e99f137 100644
--- a/main.c
+++ b/main.c
@@ -115,7 +115,14 @@ static void init_hook(void)
static int hide_process(pid_t pid)
{
- pid_node_t *proc = kmalloc(sizeof(pid_node_t), GFP_KERNEL);
+ pid_node_t *proc;
+
+ /* Check if the pid is in the list */
+ if (is_hidden_proc(pid))
+ return -EAGAIN;
+
+ /* insert pid node into hidden_proc */
+ proc = kmalloc(sizeof(pid_node_t), GFP_KERNEL);
proc->id = pid;
list_add_tail(&proc->list_node, &hidden_proc);
return SUCCESS;
```
若我們這時嘗試重複加入一個 pid 589 時,就會產生以下的錯誤提示
```shell
$ echo "add 589" | sudo tee /dev/hideproc # first add
add 589
$ echo "add 589" | sudo tee /dev/hideproc # second add
add 589
tee: /dev/hideproc: File exists
```
### 避免 `device_read()` 發生 buffer overflow
> TODO
###### tags: `linux2021`