# 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` 到 `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 釋放掉。