# 2021q3 Homework1 (quiz1) contributed by < `yian02` > ###### tags: `linux2021` Linux 核心版本: `5.4.0-80-generic` ## 1. 解釋程式碼運作原理 ### 建立裝置 從新模組被 init 的地方開始看起: ```c dev_t dev; err = alloc_chrdev_region(&dev, 0, MINOR_VERSION, DEVICE_NAME); ``` 宣告一個 `dev_t` 的變數,並透過呼叫 `alloc_chardev_region` 取得一個**動態分配**的、目前**未被其他 driver 使用**的 major number,此函式被定義在 [include/linux/](https://github.com/torvalds/linux/blob/master/include/linux/fs.h),而其說明在 [Kernel Documentation](https://www.kernel.org/doc/html/latest/core-api/kernel-api.html?highlight=alloc_chrdev_region#c.alloc_chrdev_region) 中如下: ```c // Return value: 正常執行後回傳 0,錯誤發生時回傳一個負數 int alloc_chrdev_region ( dev_t * dev, // 用來回傳被系統分配到的 device number unsigned baseminor, // 所需要的 minor number 範圍中的第一個 unsigned count, // 需要的 minor number 數量 const char * name // 此 device driver 的名字 ); ``` 此函式的傳入參數 `dev` 在函式回傳時會被設定成系統給定的 device number,此 device number 是一個 32 位元 unsigned 整數,定義在 [include/linux/types.h](https://github.com/torvalds/linux/blob/master/include/linux/types.h) 中: ```c typedef u32 __kernel_dev_t; typedef __kernel_dev_t dev_t; ``` 測驗題程式碼中,接下來的 ```c dev_major = MAJOR(dev); ``` 使用到以下巨集: ```c #define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS)) ``` 而根據在 [include/linux/kdev_t.h](https://github.com/torvalds/linux/blob/master/include/linux/kdev_t.h) 中所定義的 ```c #define MINORBITS 20 #define MINORMASK ((1U << MINORBITS) - 1) #define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS)) #define MINOR(dev) ((unsigned int) ((dev) & MINORMASK)) ``` 從 `dev_t` 被使用的方法可以推敲出,用來儲存 device number 的 32 位元 unsigned 整數的後面 20 個位元代表的是 minor number,前面剩下的 12 個位元是 major number,可以透過 **bit operations** (**shift right** or **bit mask**) 來取得 device number 中儲存的 major number 和 minor number 資訊。 而接下來的程式碼: ```c hideproc_class = class_create(THIS_MODULE, DEVICE_NAME); ``` 則是在建立一個指向 struct class 的指標,並將回傳的 struct class 的 `OWNER` 指定為 `THIS_MODULE`,且將其命名為 `DEVICE_NAME`。這個函式呼叫完成後,會在 `/sys/class/` 目錄下創建一個 `DEVICE_NAME` 的資料夾,在 module 移除階段,若沒有呼叫 `class_destroy()` 函式來移除該目錄,則會造成下一次要將此模組掛載進核心時失敗。 關於 `struct class` 的意義,可以在 [Linux Kernel Documentation](https://www.kernel.org/doc/html/v4.12/driver-api/infrastructure.html#c.class) 中看到: > A class is a higher-level view of a device that abstracts out low-level implementation details. class 在實作裝置如何運作的 level 上,提供了一層抽象層,讓 user space 可以較簡單地和裝置互動,而不用知道裝置到底是如合運作的等等底層細節。 而 `class_create(owner, name)` 是個巨集,定義在 [include/linux/device/class.h]([http](https://github.com/torvalds/linux/blob/master/include/linux/device/class.h#L273)) ```c #define class_create(owner, name) \ ({ \ static struct lock_class_key __key; \ __class_create(owner, name, &__key); \ }) ``` 上述巨集真正呼叫的函式 `__class_create(owner, name, &__key)` 則被實作在 [/drivers/base/class.c](https://github.com/torvalds/linux/blob/master/drivers/base/class.c#L226) 中,其目的是產生一個指向 struct class 的 pointer,在之後可以傳入 `device_create()` 中來向系統註冊裝置。 在測驗題程式碼中,接下來的 ```c cdev_init(&cdev, &fops); cdev_add(&cdev, MKDEV(dev_major, MINOR_VERSION), 1); ``` 則是指定事先定義好的 `fops` 到 `cdev`,並將 `cdev` 註冊到核心。 接下來的: ```c device_create(hideproc_class, NULL, MKDEV(dev_major, MINOR_VERSION), NULL, DEVICE_NAME); ``` 則是會在 `/dev/` 目錄中建立一個 `DEVICENAME` 的檔案。到此為止,新增一個裝置的步驟結束了。 ### ftrace > 以下為閱讀 [Using ftrace to hook to functions](https://www.kernel.org/doc/html/v4.17/trace/ftrace-uses.html#the-callback-function) 、閱讀測驗題程式碼、肉眼觀察程式碼運作得到的結論,還未使用真正的工具去觀察使用 ftrace 後函式呼叫的真實情況,因此可能與真實發生的事情有出入。 上面在系統中新增裝置,為的應只是建立一個能跟核心溝通的橋樑,但真正讓此程式碼能夠隱藏 process 的,則是 ftrace 的功勞。 ```c hook->ops.func = hook_ftrace_thunk; //... err = ftrace_set_filter_ip(&hook->ops, hook->address, 0, 0); //... err = register_ftrace_function(&hook->ops); ``` 透過上述程式碼向系統註冊 hook 後,只要系統執行到 `find_ge_pid()` 函式時,就會先執行我們註冊的 `hook_ftrace_thunk` 函式。 ```c 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; } ``` 在此函式中,會透過修改 ip register,來達到 hijack 函式的作用,也就是説,原本要呼叫 `find_ge_pid()` 時,會先進入到我們註冊的 callback 函式中,執行完畢即會返回原本的 `find_ge_pid()`。 但是在這個程式碼中,透過修改 `regs->ip` 暫存器中的值, `hook_find_ge_pid()` 的位置後,就可以將原本要執行的 `find_ge_pid()` 替換成 `hook_find_ge_pid()`,達到核心函式攔截的作用。 > TODO: 使用系統工具觀察 ftrace 運作的真實情形 (還在學習) ### 運作原理 當此核心模組收到 `add PID` 的輸入時,就會把收到的 `PID` 加入 `hidden_proc` 的 linked list 中。當系統呼叫 `find_ge_pid` 函式時,會被 ftrace 攔截成呼叫 `hook_find_ge_pid`,此函式會先呼叫真正的 `find_ge_pid` 取得其函式回傳的 pid 後,去 `hidden_proc` 中尋找其是否是被隱藏起來的 process,若是的話,則會將其 `pid + 1` 後,再呼叫: ```c pid = real_find_ge_pid(pid->numbers->nr + 1, ns); ``` 來達成讓在 `hidden_proc` 中存在的 process 被隱藏起來的效果。 :::warning 採用 `pid + 1` 的策略,是否會造成 PID 衝突? :notes: jserv ::: ### /proc 目錄、 Pseudo File System 與 `pidof` 寫測驗題時,很好奇 `find_ge_pid()` 這個函式到底是在 `pidof` 哪裡被使用到,又為何 'hijack' 這個函式可以改變 `pidof` 這個程式的執行結果,因此就去找 `pidof` 的原始碼來看,希望能發現一點線索。 #### `pidof` :::warning 注意用語: * command 是「命令」,如 shell commands (執行人類的意志) * instruction 是「指令」,如 CPU instructions (機械性操作) :notes: jserv ::: 在 `Linux ubuntu 5.4.0-80-generic` 中,輸入以下命令: ```shell $ dpkg -S pidof > sysvinit-utils: /bin/pidof > sysvinit-utils: /usr/share/man/man8/pidof.8.gz ``` 找出 `pidof` 是 `sysvinit-utils` 這個套件的一部份,使用以下命令來取的其原始碼: ```shell $ apt-get source sysvinit-utils ``` 執行後出現的目錄並不是 `sysvinit-utils`,而是 `sysvinit-2.96`,在這個目錄中的 `/src` 下並沒有發現和 `pidof` 有關的檔案名稱,搜尋一下原始碼後,原來, `pidof` 隱身在 `killall5.c` 檔案中。到此,先確認一下平常用的 `pidof` 到底是在執行哪個程式: ```shell $ which pidof > /usr/bin/pidof $ cd /usr/bin $ ll pidof > lrwxrwxrwx 1 root root 14 Feb 13 2020 pidof -> /sbin/killall5* ``` 平常使用的 `pidof` 真的是透過 symbolic link 來執行 `killall5` 這個程式,和我們在原始碼中發現的相同。 在 `killall5` 的程式原始碼 `killall5.c` 的 `main()` 函式中,可以看到以下程式碼: ```c /* Get program name. */ if ((progname = strrchr(argv[0], '/')) == NULL) progname = argv[0]; else progname++; //... /* Were we called as 'pidof' ? */ if (strcmp(progname, "pidof") == 0) return main_pidof(argc, argv); ``` 當使用 `pidof` 命令來執行 `killall5` 時,`argv[0]` 為 `pidof`,透過 `argv[0]` 即可判斷被執行的到底是 `pidof` 還是真正的 `killall5`,若是執行 `pidof` 的話,就會呼叫 `main_pidof()` 函式,開始執行 `pidof`,且在此函式回傳後隨即結束程式。 讀完 `killall5.c` 的程式碼後,簡單歸納一下 `pidof` 的運作原理。 在 `killall5.c` 中,使用以下 struct 來儲存單一一個 process 的資訊: ```c /* Info about a process. */ typedef struct proc { char *pathname; /* full path to executable */ char *argv0; /* Name as found out from argv[0] */ char *argv0base; /* `basename argv[1]` */ char *argv1; /* Name as found out from argv[1] */ char *argv1base; /* `basename argv[1]` */ char *statname; /* the statname without braces */ ino_t ino; /* Inode number */ dev_t dev; /* Device it is on */ pid_t pid; /* Process ID. */ pid_t sid; /* Session ID. */ char kernel; /* Kernel thread or zombie. */ char nfs; /* Name found on network FS. */ struct proc *next; /* Pointer to next struct. */ } PROC; ``` 而上面這個描述單一一個 process 的結構體,會透過 linked list 串起來成為一個 PID queue: ```c /* pid queue */ typedef struct pidq { PROC *proc; struct pidq *next; } PIDQ; typedef struct { PIDQ *head; PIDQ *tail; PIDQ *next; } PIDQ_HEAD; ``` 讀取系統中所有 process 的資訊並將其串成 PID queue 的是 `readproc()` 這個函式: ```c int readproc(int do_stat) { //... /* Open the /proc directory. */ if (chdir("/proc") == -1) { nsyslog(LOG_ERR, "chdir /proc failed"); return -1; } if ((dir = opendir(".")) == NULL) { nsyslog(LOG_ERR, "cannot opendir(/proc)"); return -1; } //... /* Walk through the directory. */ while ((d = readdir(dir)) != NULL) { //... } //... } ``` 原來, `pidof` 是用遍歷 `/proc` 目錄下的 `/proc/$(PID)/` 目錄來獲得所有運行中的 process 資訊,將其串為 PID queue。取得所有 process 資訊後,再呼叫 `pidof()` 函式從 PID queue 中一一比對是否為我們要尋找的 process。 在此原始碼中,並沒有發現 `find_ge_pid()` 的身影,不過,用來遍歷 `/proc` 目錄的 `readdir()` 函式是決定我們找到哪些 process 的關鍵,看起來很可疑,因此就繼續往 `readdir()` 函式中探索。 在往 `readdir()` 繼續探索前,先確認 `find_ge_pid()` 和 `/proc` 目錄下的內容有什麼關係,測試使用 `hideproc` 模組是不是真的會影響到 `/proc` 目錄下的內容,執行以下命令: ```shell $ pidof cron > 686 $ echo "add 686" | sudo tee /dev/hideproc $ pidof cron # 無法見到 cron 的 pid $ ls /proc/ #程式印出的目錄中無法找到 686 這個目錄 ``` 發現若使用 `ls` 列出 `/proc/` 下的目錄,真的看不到 `686` 這個目錄,根據 [How does `ls` work?](https://gist.github.com/amitsaha/8169242) 文章中所述,`ls` 也是使用 `readdir()` 來列出目錄,因此讓問題出在 `readdir()` 這個函式的可能性大幅增加。 `readdir()` 是一個在 glibc 函式庫中的函式,其實作在 [glibc/sysdeps/unix/sysv/linux/readdir64.c](https://github.com/bminor/glibc/blob/master/sysdeps/unix/sysv/linux/readdir64.c#L29) 中: ```c /* Read a directory entry from DIRP. */ struct dirent64 * __readdir64 (DIR *dirp) { struct dirent64 *dp; int saved_errno = errno; #if IS_IN (libc) __libc_lock_lock (dirp->lock); #endif do { size_t reclen; if (dirp->offset >= dirp->size) { /* We've emptied out our buffer. Refill it. */ size_t maxread = dirp->allocation; ssize_t bytes; bytes = __getdents64 (dirp->fd, dirp->data, maxread); if (bytes <= 0) { /* On some systems getdents fails with ENOENT when the open directory has been rmdir'd already. POSIX.1 requires that we treat this condition like normal EOF. */ if (bytes < 0 && errno == ENOENT) bytes = 0; /* Don't modifiy errno when reaching EOF. */ if (bytes == 0) __set_errno (saved_errno); dp = NULL; break; } dirp->size = (size_t) bytes; /* Reset the offset into the buffer. */ dirp->offset = 0; } dp = (struct dirent64 *) &dirp->data[dirp->offset]; reclen = dp->d_reclen; dirp->offset += reclen; dirp->filepos = dp->d_off; /* Skip deleted files. */ } while (dp->d_ino == 0); #if IS_IN (libc) __libc_lock_unlock (dirp->lock); #endif return dp; } ``` 在上面這段 `readdir()` 的程式碼中,使用了 `__getdents64()` 這個函式來取得 directory entries,這個函式實作在 [/glibc/sysdeps/unix/sysv/linux/getdents64.c]() 中: ```c /* The kernel struct linux_dirent64 matches the 'struct dirent64' type. */ ssize_t __getdents64 (int fd, void *buf, size_t nbytes) { /* The system call takes an unsigned int argument, and some length checks in the kernel use an int type. */ if (nbytes > INT_MAX) nbytes = INT_MAX; return INLINE_SYSCALL_CALL (getdents64, fd, buf, nbytes); } ``` 從上述程式碼可看出,`__getdents64()` 這個函式其實就是在呼叫 `getdents64()` 這個 syscall,從這邊開始,程式就從 **user space** 進入到 **kernel space** 。 #### Virtual File System > 參考資料:[Linux 核心設計: 檔案系統概念及實作手法](https://hackmd.io/@sysprog/linux-file-system?type=view) `getdents64()` 這個函式實作在 [/linux/fs/readdir.c](https://github.com/torvalds/linux/blob/master/fs/readdir.c#L354) 中: ```c SYSCALL_DEFINE3(getdents64, unsigned int, fd, struct linux_dirent64 __user *, dirent, unsigned int, count) { struct fd f; struct getdents_callback64 buf = { .ctx.actor = filldir64, .count = count, .current_dir = dirent }; int error; f = fdget_pos(fd); if (!f.file) return -EBADF; error = iterate_dir(f.file, &buf.ctx); if (error >= 0) error = buf.error; if (buf.prev_reclen) { struct linux_dirent64 __user * lastdirent; typeof(lastdirent->d_off) d_off = buf.ctx.pos; lastdirent = (void __user *) buf.current_dir - buf.prev_reclen; if (put_user(d_off, &lastdirent->d_off)) error = -EFAULT; else error = count - buf.count; } fdput_pos(f); return error; } ``` 上述程式碼中,在取得目錄資訊的,是 `iterate_dir()` 函式,也實作在同一個檔案中: ```c int iterate_dir(struct file *file, struct dir_context *ctx) { struct inode *inode = file_inode(file); bool shared = false; int res = -ENOTDIR; if (file->f_op->iterate_shared) shared = true; else if (!file->f_op->iterate) goto out; res = security_file_permission(file, MAY_READ); if (res) goto out; if (shared) res = down_read_killable(&inode->i_rwsem); else res = down_write_killable(&inode->i_rwsem); if (res) goto out; res = -ENOENT; if (!IS_DEADDIR(inode)) { ctx->pos = file->f_pos; if (shared) res = file->f_op->iterate_shared(file, ctx); else res = file->f_op->iterate(file, ctx); file->f_pos = ctx->pos; fsnotify_access(file); file_accessed(file); } if (shared) inode_unlock_shared(inode); else inode_unlock(inode); out: return res; } ``` 上述程式碼中,就看到了我們要找的重點:`file->f_op->iterate_shared(file, ctx);`。 這個函式和 Linux Virtual File System (VFS) 有關, VFS 的用意就是提供一個介面,可以將檔案系統真正運作、存取的方式抽象化,讓我們可以不用知道不同檔案系統真正運作的方式,也可以透過 VFS 提供的介面來操作檔案系統的讀、寫等等操作。 而 `iterate_shared` 這個函式就是 VFS 中的一個介面,在 [Overview of the Linux Virtual File System](https://www.kernel.org/doc/Documentation/filesystems/vfs.txt) 可找到其對應的說明: > iterate_shared: called when the VFS needs to read the directory contents when filesystem supports concurrent dir iterators. 也就是說,`iterate_shared` 這個函式是在 VFS 需要讀取一個目錄底下的內容時,會去呼叫的函式,而這個函式,是由我們要讀取的目錄的**檔案系統**來提供的,例如今天讀取一個 EXT2 格式的檔案系統下的目錄,要獲得該目錄中的內容,就會呼叫 EXT2 檔案系統中實作的 `iterate` 或是 `iterate_shared` 函式。VFS 中的函式會根據檔案系統的類型自動去呼叫對應的函式,在我們的題目中,我們所在的檔案系統是 `procfs`,因此 `iterate_shared` 函式是呼叫 `procfs` 中的實作,而 `procfs` 在 Linux 中和一般的檔案系統不同,它是 Linux 中的 Pseudo File System。 #### Pseudo File System > 老師在 [Linux 核心設計: 檔案系統概念及實作手法](https://hackmd.io/@sysprog/linux-file-system?type=view) 中將其翻譯為「不是真的檔案系統」,我覺得可以翻譯為「擬」檔案系統? 這樣可以同時表達 「像真的」但「不是真的」的概念。且 pseudo 在有些科學名詞中也會被翻譯成「擬」。 Pseudo File System 就是「很像檔案系統」但「不是真的是」,我們可以透過向存取一般檔案系統的方式來存取 Pseudo File System,中的內容,但我們所存取的內容並不是真的在磁碟中的內容,可能只是儲存在記憶體中的一些資料,例如 `procfs`,就是將 process 的資訊,變成可以用檔案系統的操作來讀取,但我們讀取的資料只是在記憶體中儲存的各種資訊而已。 Pseudo File System 會根據 VFS 的介面去定義各種操作需要使用的函式,像是 `read()` 、 `write()` 、 `iterate_shared()`,等等,這樣我們透過 VFS 的介面來操作時,就會根據對應的操作呼叫對應的函式。 我們關注的 `procfs` 實作在 [linux/fs/proc](https://github.com/torvalds/linux/tree/master/fs/proc) 目錄中,而我們所感興趣、用來取得 `/proc` 目錄下的函式,實作在 [/linux/fs/proc/root.c](https://github.com/torvalds/linux/blob/master/fs/proc/root.c#L340) 中: ```c /* * The root /proc directory is special, as it has the * <pid> directories. Thus we don't use the generic * directory handling functions for that.. */ static const struct file_operations proc_root_operations = { .read = generic_read_dir, .iterate_shared = proc_root_readdir, .llseek = generic_file_llseek, }; ``` 上述程式碼定義了對 `procfs` 根目錄 `/proc` 的操作,其中也包括我們前面看到在 `getdents64()` 中被呼叫到的 `iterate_shared` 函式,在 `procfs` 中,呼叫 `iterate_shared` 其實就是在呼叫 `proc_root_readdir` 函式。 `proc_root_readdir` 函式實作在 [linux/fs/proc/root.c](https://github.com/torvalds/linux/blob/master/fs/proc/root.c#L328) 中: ```c static int proc_root_readdir(struct file *file, struct dir_context *ctx) { if (ctx->pos < FIRST_PROCESS_ENTRY) { int error = proc_readdir(file, ctx); if (unlikely(error <= 0)) return error; ctx->pos = FIRST_PROCESS_ENTRY; } return proc_pid_readdir(file, ctx); } ``` 這個函式的前半段是在處理 `/proc` 目錄中和 process 沒有關係的內容,例如 cpuinfo 等等資訊,而在最後 return 才呼叫的 `proc_pid_readdir()` 函式,就是在 `procfs` 中為每個 process 建立一個 directory entries 的函式。這個函式實作在 [/linux/fs/proc/base.c](https://github.com/torvalds/linux/blob/master/fs/proc/base.c#L3429) 中: ```c /* for the /proc/ directory itself, after non-process stuff has been done */ int proc_pid_readdir(struct file *file, struct dir_context *ctx) { struct tgid_iter iter; struct proc_fs_info *fs_info = proc_sb_info(file_inode(file)->i_sb); struct pid_namespace *ns = proc_pid_ns(file_inode(file)->i_sb); loff_t pos = ctx->pos; if (pos >= PID_MAX_LIMIT + TGID_OFFSET) return 0; if (pos == TGID_OFFSET - 2) { struct inode *inode = d_inode(fs_info->proc_self); if (!dir_emit(ctx, "self", 4, inode->i_ino, DT_LNK)) return 0; ctx->pos = pos = pos + 1; } if (pos == TGID_OFFSET - 1) { struct inode *inode = d_inode(fs_info->proc_thread_self); if (!dir_emit(ctx, "thread-self", 11, inode->i_ino, DT_LNK)) return 0; ctx->pos = pos = pos + 1; } iter.tgid = pos - TGID_OFFSET; iter.task = NULL; for (iter = next_tgid(ns, iter); iter.task; iter.tgid += 1, iter = next_tgid(ns, iter)) { char name[10 + 1]; unsigned int len; cond_resched(); if (!has_pid_permissions(fs_info, iter.task, HIDEPID_INVISIBLE)) continue; len = snprintf(name, sizeof(name), "%u", iter.tgid); ctx->pos = iter.tgid + TGID_OFFSET; if (!proc_fill_cache(file, ctx, name, len, proc_pid_instantiate, iter.task, NULL)) { put_task_struct(iter.task); return 0; } } ctx->pos = PID_MAX_LIMIT + TGID_OFFSET; return 0; } ``` 上述程式碼中最重要的部分是最後的 for 迴圈,該迴圈會找出所有正在運行中的 process 並取得其資訊,其中找出運行中 process 的關鍵函式就是 `next_tgid()` 函式,這個函式會搜尋下一個 **thread group ID**,我們可以在同一個檔案中找到其實作: ```c static struct tgid_iter next_tgid(struct pid_namespace *ns, struct tgid_iter iter) { struct pid *pid; if (iter.task) put_task_struct(iter.task); rcu_read_lock(); retry: iter.task = NULL; pid = find_ge_pid(iter.tgid, ns); if (pid) { iter.tgid = pid_nr_ns(pid, ns); iter.task = pid_task(pid, PIDTYPE_TGID); if (!iter.task) { iter.tgid += 1; goto retry; } get_task_struct(iter.task); } rcu_read_unlock(); return iter; } ``` 終於,我們看到了我們 hijack 的函式: `find_ge_pid` ,`next_tgid` 透過 `find_ge_pid` 函式,來找出系統中所有運行的 process,並為其建立 `/proc` 目錄下的 directory entry,當我們 hijack `find_ge_pid` 時,會讓 `next_tgid` 找不到我們藏起來的 process,也因此 `procfs` 在列出 `/proc` 目錄下的內容時會找不到我們所隱藏的 process。而 `pidof` 這類的程式,就是透過讀取 `/proc` 目錄下的 process 目錄來獲得 process 的資訊,也因此程式才會找不到被我所隱藏的 process。 ![pidof 函式關係圖](https://i.imgur.com/tTVRpW3.png) 上面這張圖整理了從 `pidof` 到 `find_ge_pid()` 函式之間的關聯,可以更清楚的看出到底我們 "hijack" `find_ge_pid()` 函式和 `pidof` 輸出之間的關聯。 > 我覺得找出 `pidof` 和 `find_ge_pid()` 之間的關係時,讓我理解到 Linux Kernel 架構中的每個部分都習習相關,原本以為只是研究一下和 process 有關的內容就好,即果發現和檔案系統有關,於是開始研究起 virtule file system,後來又發現是和 `procfs` 有關,於是又開始研究 pseudo file system,才總算比較理解這些背後的關係。想要理解 linux kernel 真的會需要對 linux kernel 的「廣」先有所認識啊! --- ## 3. 擴充程式碼 ### 接受多個 PID 作為參數 在 `device_write()` 函式中,原本使用 `kstrtol()` 函式來將 string 轉為 long,但為了要接受多個參數,因此在解析 string 成 long 的步驟就得自己來。 以下程式碼以 `hide_process` 為例,但 `del_process` 也幾乎相同,完整程式碼可見 [GitHub](https://github.com/yian02/linux2021-quiz1-hideproc/blob/6c2eab07e8fbcdba9e6b1edc01c090d9d206aa5c/main.c#L179) (更新:使用 `clang-format` 排版): ```c if (!memcmp(message, add_message, sizeof(add_message) - 1)) { param = message + sizeof(add_message) - 1; max_len = len - sizeof(add_message) + 1; pid = 0; while (*param != '\0' && *param != '\n' && max_len--) { if (*param >= '0' && *param <= '9') { pid = pid * 10 + (*param - '0'); if (*(param + 1) == ' ' || *(param + 1) == '\n' || *(param + 1) == '\0') { printk(KERN_INFO "add %ld\n", pid); hide_process(pid); pid = 0; } } param++; } ``` :::warning TODO: 使用 `clang-format` 排版,設定可見 [lkm-hidden/.clang-format](https://github.com/sysprog21/lkm-hidden/blob/master/.clang-format) :notes: jserv ::: 透過上述程式碼,即可支援以下操作: ```shell $ echo "add <PID1> <PID2> <PID3> ..." | sudo tee /dev/hideproc $ echo "del <PID1> <PID2> <PID3> ..." | sudo tee /dev/hideproc ``` 即可在一次操作內隱藏或釋放多個 process。 --- ## 4. 可改進的地方 ### 釋放系統資源 若直接將原始的測驗程式碼拿來編譯並掛載進核心中,當卸載之後,會讓整個作業系統卡住,完全沒有回應,推測應該是在卸載的過程中,此模組向系統要求的資源沒有被正確釋放有關。 將模組卸載時執行的 `_hideproc_exit` 改寫為: ```c static void _hideproc_exit(void) { dev_t dev; printk(KERN_INFO "@ %s\n", __func__); hook_remove(&hook); dev = cdev.dev; device_destroy(hideproc_class, cdev.dev); cdev_del(&cdev); class_destroy(hideproc_class); unregister_chrdev_region(dev, MINOR_VERSION); } ``` 上述程式碼所做的,其實就是按照在 `_hideproc_init` 的相反順序一一將掛載時向系統要求的資源釋放回去,其步驟為: 1. 解除 ftrace 的 hook 2. 將系統在 `/dev/` 目錄中建立的檔案釋放 3. 將其從 cdev 中刪除 4. 刪除系統建立的 class,包括在 `/sys/class/` 目錄中創建的 `DEVICENAME` 目錄 5. 釋放向系統申請的 device number 將上述程式碼加入測驗程式碼中,編譯並掛載後,卸載時就不會有作業系統當機的問題發生,也可以重複的掛載、卸載。 > 在修改程式時,一開始忘記加入 `class_destroy(hideproc_class)`,此時會發現模組一旦卸載之後,再次掛載會發生問題,查看錯誤訊息才知道 kernel 在掛載時無法在 `/sys/class/` 目錄下建立 `DEVICENAME` 的目錄(因為卸載時未被刪除),因此導致再次掛載時發生錯誤。 ### 修改 `unhide_process` 原始測驗題中的 `unhide_process`,雖然有傳入 `pid` 當作參數,但是並沒有使用到此資訊來釋放對應的 process,而是直接將整個 `hide_proc` 都釋放掉。 因此若將多個 process 都隱藏後,輸入: ```shell $ echo "del PID" | sudo tee /dev/hideproc ``` 這行命令的作用原本應該只是要釋放 `PID` 而已,但會發現所有原本被隱藏的都被釋放出來了。 因此將函式修改如下: ```c 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; } ``` 即可正常完成功能。 ### 釋放 `hideproc` linked list 中的空間 在原始測驗程式碼中,若在卸載模組時,`hideproc` list 中還有 node 存在的話,並不會被正確地釋放出來,因此修改 `_hideproc_exit()` 函式如下: ```c static void _hideproc_exit(void) { dev_t dev; pid_node_t *proc, *tmp_proc; printk(KERN_INFO "@ %s\n", __func__); list_for_each_entry_safe(proc, tmp_proc, &hidden_proc, list_node) { list_del(&proc->list_node); kfree(proc); } hook_remove(&hook); dev = cdev.dev; device_destroy(hideproc_class, cdev.dev); cdev_del(&cdev); class_destroy(hideproc_class); unregister_chrdev_region(dev, MINOR_VERSION); } ``` 即可在卸載模組時一併將 `hide_proc` 中還存在的 node 釋放掉。