# 2021q3 Homework1 (quiz1)
contributed by < [`RoyWFHuang`](https://github.com/RoyWFHuang) >
###### tags: `linux_class` `kernel`
> [2021 年暑期 第 1 週隨堂測驗題目](https://hackmd.io/@sysprog/linux2021-summer-quiz1)
> [2021 年暑期 Linux 核心 Homework1](https://hackmd.io/@sysprog/linux2021-summer-homework1)
開發環境為:
OS: ubuntu 20.04
kernel ver: 5.4.0-77-generic
CPU arch: x86_64
使用 multipass 開發
```shell
$ multipass info dev
Name: dev
State: Running
IPv4: 10.195.227.125
Release: Ubuntu 20.04.2 LTS
Image hash: 1d3f69a8984a (Ubuntu 20.04 LTS)
Load: 0.00 0.00 0.00
Disk usage: 1.9G out of 4.7G
Memory usage: 136.6M out of 981.2M
Mounts: /home/roy/src-code => /home/roy/src-code
UID map: 1000:default
GID map: 1000:default
In multipass shell
$ uname -a
Linux dev 5.4.0-80-generic #90-Ubuntu
```
---
#### 解釋上述程式碼運作原理,包含 ftrace 的使用
此程式為典型的 Liunx kernel module, 藉由 module_init 作為 insmod 的開始, 以 module_exit 作為結束時相對的動作.
Module 開始的資源需求的確認, 跟結束時的資源釋放, 就不再贅述. 以下將聚焦在 ==cdev_init(&cdev, &fops);== 跟 ==init_hook();== 上
[cdev_init](https://elixir.bootlin.com/linux/v4.2/source/fs/char_dev.c#L538): [character device](https://linux-kernel-labs.github.io/refs/heads/master/labs/device_drivers.html) init. Linux 中, 最主要的兩大 device 另一個則是 [block device](https://linux-kernel-labs.github.io/refs/heads/master/labs/block_device_drivers.html?highlight=register_blkdev)(註冊操作為 [register_blkdev](https://elixir.bootlin.com/linux/v4.2/source/block/genhd.c#L286)), 當然後來因應網路快速的需求, 多了[network device](https://www.kernel.org/doc/html/latest/networking/netdevices.html).
其中帶入的參數 [struct file_operations](https://elixir.bootlin.com/linux/v4.2/source/include/linux/fs.h#L1602) 主要為 read/ write/ open/ release ,會分別對應到操作這個 module 時相對應的動作
init_hook:
在此 function 中, 會透過 kallsyms_lookup_name 去取得 find_ge_pid 在 symbol table 中的 address 並記錄下來, 在將自己的 hook_find_ge_pid hook 上去, 最後透過 hook_install 進行取代
hook_install:
在看這個 function 之前, 要先了解 [ftrace](https://www.kernel.org/doc/html/v4.17/trace/ftrace-uses.html), ftrace 的結構由
```c=
struct ftrace_ops ops = {
.func = my_callback_func,
.flags = MY_FTRACE_FLAGS
.private = any_private_data_structure,
};
```
func: 註冊相對應的 call_back function.
接著透過 ftrace_set_filter_ip/ register_ftrace_function 註冊到kernel 中, 接著透過 container_of 的方式, 找到結構的 start address, 然後透過struct member 將之前 hook_find_ge_pid 給到要追蹤的 ip 上, 接著就可以開始追蹤這個 function 了
若以此範例來說, 值追蹤的是 ==find_ge_pid== 而這個在 ==$ps== 中會用到,
所以當執行 ps 的時候, 因為 hook 的關係就會轉到 hook_find_ge_pid 上, 以此去查找 is_hidden_proc, 如果存在就 return NULL, 如此就不會顯示在 ps 中.
不過此處有一個問題, 就是當 pid = 1 在 hidden list 中, 會造成所有的 process 都不顯示, 在下面會進行修改
[另外 trace 這也有一個命令列工具](https://ithelp.ithome.com.tw/articles/10242035) (感謝 [mfinmuch](https://hackmd.io/@mfinmuch/hideproc))
==sudo apt install trace-cmd==
#### 本程式僅在 Linux v5.4 測試,若你用的核心較新,請試著找出替代方案
我自己的 kernel 是 5.4.0-80-generic 所以沒有遇到這個問題, 解法參考其他同學[linD026](
https://hackmd.io/@linD026/hideproc#ERROR-modpost-%E2%80%9Ckallsyms_lookup_name%E2%80%9D-hideprocko-undefined)
#### 本核心模組只能隱藏單一 PID,請擴充為允許其 PPID 也跟著隱藏,或允許給定一組 PID 列表,而非僅有單一 PID
擴充為允許其 PPID 也跟著隱藏
需考慮 PPID 有可能不存在的情形
透過 [find_get_pid](https://elixir.bootlin.com/linux/v5.8.18/source/include/linux/pid.h#L130) 取得 child 的 pid struct, 在藉由 pid struct 經過 [get_pid_task](https://elixir.bootlin.com/linux/v5.8.18/source/include/linux/pid.h#L94) 取得 task struct, 在由task struct 經過 [task_ppid_nr](https://elixir.bootlin.com/linux/latest/source/include/linux/sched.h#L1507)取得 parent pid
```cpp
static pid_t _getppid(long pid){
struct pid *t_chpid = NULL, *t_ppid = NULL;
struct task_struct *ch_ts = NULL;
pid_t ppid;
t_chpid = find_get_pid(pid);
if (NULL == t_chpid) {
printk(KERN_INFO "@ %ld not exist\n", pid);
return -ENOENT;
}
ch_ts = get_pid_task(t_chpid, PIDTYPE_PID);
ppid = task_ppid_nr(ch_ts);
t_ppid = find_get_pid(ppid);
if (NULL == t_ppid) {
printk(KERN_INFO "@ %d not exist\n", ppid);
return -ENOENT;
}
printk(KERN_INFO "@ %ld parent is %d\n", pid, ppid);
return ppid;
}
```
在 device_write 中 add /del 增加相對應動作, 從隱藏 list 增加 / 刪除 ppid
這邊也一併考慮了 input pid 不存在的時候相對應處理
```cpp
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)) {
kstrtol(message + sizeof(add_message), 10, &pid);
if (SUCCESS != (ret = hide_process(pid)) ) {
kfree(message);
return ret;
}
if (ENOENT != (ppid = _getppid(pid))) {
hide_process(ppid);
}
} else if (!memcmp(message, del_message, sizeof(del_message) - 1)) {
kstrtol(message + sizeof(del_message), 10, &pid);
unhide_process(pid);
ppid = _getppid(pid);
unhide_process(ppid);
}
.....
}
static int hide_process(pid_t pid)
{
pid_node_t *proc = NULL;
struct pid *t_chpid = NULL;
t_chpid = find_get_pid(pid);
if (NULL == t_chpid) {
printk(KERN_INFO "@ %d not exist\n", pid);
return -ENOENT;
}
.....
}
```
```shell
$ echo "add 600" | sudo tee /dev/hideproc
add 600
tee: /dev/hideproc: No such file or directory
$ echo "add 1" | sudo tee /dev/hideproc
add 1
$ sudo cat /dev/hideproc
pid: 1
$ echo "add 601" | sudo tee /dev/hideproc
add 601
$ sudo cat /dev/hideproc
pid: 1
pid: 601
$ echo "del 601" | sudo tee /dev/hideproc
del 601
$ sudo cat /dev/hideproc
$
$ echo "add 601" | sudo tee /dev/hideproc
add 601
$ sudo cat /dev/hideproc
pid: 601
pid: 1
```
#### 指出程式碼可改進的地方,並動手實作
[src code](https://github.com/RoyWFHuang/hide_proc)
[errorno](https://man7.org/linux/man-pages/man3/errno.3.html)
修改處:
1. exit module release resource
```cpp
static void _hideproc_exit(void)
{
printk(KERN_INFO "@ %s\n", __func__);
_clear_all_hideproc();
hook_remove(&hook);
device_destroy(hideproc_class, MKDEV(MAJOR(dev), MINOR_VERSION));
class_destroy(hideproc_class);
cdev_del(&cdev);
unregister_chrdev_region(MKDEV(MAJOR(dev), MINOR_VERSION), 1);
/* FIXME: ensure the release of all allocated resources */
}
```
2. 增加 init 時候確認資源是否取得以及其他 kmalloc 失敗處理
```cpp
static int hide_process(pid_t pid)
{
pid_node_t *proc = kzalloc(sizeof(pid_node_t), GFP_KERNEL);
if(NULL == proc)
return -ENOMEM;
proc->id = pid;
list_add_tail(&proc->list_node, &hidden_proc);
return SUCCESS;
}
```
```c=
static ssize_t device_write(struct file *filep,
const char *buffer,
size_t len,
loff_t *offset)
.....
if (len < sizeof(add_message) - 1 && len < sizeof(del_message) - 1)
return -EAGAIN;
message = kzalloc(len + 1, GFP_KERNEL);
if(NULL == message)
return -ENOMEM;
// 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);
if (SUCCESS != hide_process(pid)) {
kfree(message);
return -ENOMEM;
}
} else if (!memcmp(message, del_message, sizeof(del_message) - 1)) {
.....
}
```
```cpp
static int _hideproc_init(void)
{
int dev_major;
struct device *device = NULL;
printk(KERN_INFO "@ %s\n", __func__);
if (0 != alloc_chrdev_region(&dev, 0, MINOR_VERSION, DEVICE_NAME) )
goto fail_hideproc_init;
dev_major = MAJOR(dev);
hideproc_class = class_create(THIS_MODULE, DEVICE_NAME);
if(NULL == hideproc_class)
goto fail_hideproc_init;
cdev_init(&cdev, &fops);
if(0 != cdev_add(&cdev, MKDEV(dev_major, MINOR_VERSION), 1))
goto fail_hideproc_init_cdev_add;
device = device_create(hideproc_class, NULL, MKDEV(dev_major, MINOR_VERSION), NULL,
DEVICE_NAME);
if (NULL == device)
goto fail_hideproc_init_dev_create;
init_hook();
return 0;
fail_hideproc_init_dev_create:
cdev_del(&cdev);
fail_hideproc_init_cdev_add:
class_destroy(hideproc_class);
fail_hideproc_init:
return -1;
}
```
3. 回看整個程式, 發現似乎會有 memory leak 的情形發生
當 insmod 後, 這時候 echo "add pid" 進去後, 若無執行 echo "del pid", 則被加到 list 中作為紀錄的資料, 似乎不會清除.
本來想要用 valgrind 去檢查, 但只能在 user space 中使用, 於是乎查了一下, 發現 ==CONFIG_DEBUG_KMEMLEAK== ([Kernel Memory Leak Detector](https://www.kernel.org/doc/html/v5.9/dev-tools/kmemleak.html)) 要開啟才能追蹤, 而開啟這個又必須要重新編譯kernel.
於是找到了[kedr](https://github.com/euspectre/kedr), 然後按照上述程序操作
```shell
$ sudo insmod hideproc.ko
$ pidof cron
$ echo "add 601" | sudo tee /dev/hideproc
$ sudo rmmod hideproc
```
檢查 dmesg 果然出現
```
[ 3452.293745] [leak_check] Totals:
[ 3452.293745] [leak_check] Allocations: 2
[ 3452.293746] [leak_check] Possible leaks: 1
[ 3452.293747] [leak_check] Unallocated frees: 0
```
於是多增加了
```cpp
static void _clear_all_hideproc(void)
{
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);
}
}
```
重新確認
```
[ 4774.930412] [leak_check] Totals:
[ 4774.930414] [leak_check] Allocations: 2
[ 4774.930416] [leak_check] Possible leaks: 0
[ 4774.930417] [leak_check] Unallocated frees: 0
```
4. static int unhide_process(pid_t pid) 這個的 pid 沒有用到, 而底下的內容卻是把每個 item 全部移除
```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;
}
```
:::warning
TODO: 思考是否引入其他資料結構,讓處理更有效率
:notes: jserv
:::
> 正如老師說的, 路見不平, 我只好拿 Linux api 來填了
> :notes: Roy
> 改正方式在最後面
5. 在增加 ppid hiden 功能的時候, 發現如果重複輸入同一個 pid 會出現兩筆在 list 中, 佔用多餘的資源
```shell
$ sudo cat /dev/hideproc
pid: 601
pid: 1
$ echo "add 601" | sudo tee /dev/hideproc
add 601
$ sudo cat /dev/hideproc
pid: 601
pid: 1
pid: 601
pid: 1
```
修改 hide_process
```cpp
static int hide_process(pid_t pid)
{
pid_node_t *proc = NULL;
struct pid *t_chpid = NULL;
t_chpid = find_get_pid(pid);
if (NULL == t_chpid) {
printk(KERN_INFO "@ %d not exist\n", pid);
return -ENOENT;
}
if (is_hidden_proc(pid))
return SUCCESS;
proc = kzalloc(sizeof(pid_node_t), GFP_KERNEL);
if(NULL == proc)
return -ENOMEM;
proc->id = pid;
list_add_tail(&proc->list_node, &hidden_proc);
return SUCCESS;
}
```
6. 提昇效能
List 搜尋速度太慢, 那就換成直接存取, 讓時間變成 O(1) 直接使用 [bitmap](https://www.kernel.org/doc/htmldocs/kernel-api/ch02s03.html)
起因: 本來想說用 [hash](https://github.com/torvalds/linux/blob/5bfc75d92efd494db37f5c4c173d3639d4772966/include/linux/hashtable.h) 去節省一些時間, 但後來想想, 不是只要紀錄 pid 而已嗎? 而這些 pid 不就是一個整數的數列, bitmap 不就是剛好可以用來表示這些 pid 是否存在, 於是乎就翻了 kernel bitmap 出來用了
> ref :
> [DECLARE_BITMAP](https://docs.huihoo.com/doxygen/linux/kernel/3.7/include_2linux_2types_8h.html)
> [Kernel doc](https://www.kernel.org/doc/html/latest/core-api/kernel-api.html?highlight=bitmap_alloc)
以下是修改部份, 註解是保留原本 list 的作法
```cpp=
DECLARE_BITMAP(hid_prc_bmap, PID_MAX_LIMIT);
// typedef struct {
// pid_t id;
// struct list_head list_node;
// } pid_node_t;
// LIST_HEAD(hidden_proc);
```
```cpp=
static bool is_hidden_proc(pid_t pid)
{
return test_bit(pid, hid_prc_bmap);
// pid_node_t *proc, *tmp_proc;
// list_for_each_entry_safe (proc, tmp_proc, &hidden_proc, list_node) {
// if (proc->id == pid)
// return true;
// }
// return false;
}
```
```cpp=
static int hide_process(pid_t pid)
{
// pid_node_t *proc = NULL;
struct pid *t_chpid = NULL;
if (1 == pid)
return SUCCESS;
t_chpid = find_get_pid(pid);
if (NULL == t_chpid) {
printk(KERN_INFO "@ %d not exist\n", pid);
return -ENOENT;
}
if (is_hidden_proc(pid))
return SUCCESS;
// proc = kzalloc(sizeof(pid_node_t), GFP_KERNEL);
// if(NULL == proc)
// return -ENOMEM;
// proc->id = pid;
// list_add_tail(&proc->list_node, &hidden_proc);
set_bit(pid, hid_prc_bmap);
return SUCCESS;
}
```
```cpp=
static int unhide_process(pid_t pid)
{
clear_bit(pid, hid_prc_bmap);
// 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;
}
```
```cpp=
static ssize_t device_read(struct file *filep,
char *buffer,
size_t len,
loff_t *offset)
{
// pid_node_t *proc, *tmp_proc;
int idx = 0;
char message[MAX_MESSAGE_SIZE];
if (*offset)
return 0;
for (idx = 0; idx < PID_MAX_LIMIT; idx++) {
if(test_bit(idx, hid_prc_bmap)){
memset(message, 0, MAX_MESSAGE_SIZE);
sprintf(message, OUTPUT_BUFFER_FORMAT, idx);
copy_to_user(buffer + *offset, message, strlen(message));
*offset += strlen(message);
}
}
// 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;
}
```