# 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; } ```