2021q3 Homework1 (quiz1)
===
contributed by < `dmefs` >
###### tags: `linux2021`
環境: windows 10 使用 multipass ,透過 vscode ssh 連到 vm 寫程式
```bash=
$ uname -r
5.4.0-80-generic
$ multipass list
Name State IPv4 Image
hideproc Running 172.24.248.146 Ubuntu 20.04 LTS
$ multipass shell hideproc
```
## 前置作業
首先看 [Linux 核心模組運作原理](https://hackmd.io/@sysprog/linux-kernel-module)
根據文章的思路學習並筆記
#### strace
利用 strace 追蹤執行 insmod fibdrv.ko 的過程有哪些系統呼叫被執行
執行指令 `sudo strace insmod fibdrv.ko`
有 103 行,摘錄最後幾行,可以發現 `finit_module` 在 99 行
```bash=92
getcwd("/home/ubuntu/workspace/fibdrv", 4096) = 30
stat("/home/ubuntu/workspace/fibdrv/fibdrv.ko", {st_mode=S_IFREG|0664, st_size=9952, ...}) = 0
openat(AT_FDCWD, "/home/ubuntu/workspace/fibdrv/fibdrv.ko", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1", 6) = 6
lseek(3, 0, SEEK_SET) = 0
fstat(3, {st_mode=S_IFREG|0664, st_size=9952, ...}) = 0
mmap(NULL, 9952, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f0214210000
finit_module(3, "", 0) = 0
munmap(0x7f0214210000, 9952) = 0
close(3) = 0
exit_group(0) = ?
+++ exited with 0 +++
```
#### 透過以下指令看到經過前置處理後的程式碼
`$ gcc -E fibdrv.c -I $TREE/include/ -I $TREE/arch/x86/include/ -I $TREE/uapi -I $MODULE_PTAH | less`
`$TREE` = /usr/src/linux-headers-\$(uname -r)
`$MODULE_PTAH`=/lib/modules/$(uname -r)/build/arch/x86/include/generated/
使用 / 搜尋 可以找到
```c
static initcall_t __initcall_init_fib_dev6 __used __attribute__((__section__(".initcall6" ".init"))) = init_fib_dev;
```
如同老師說的使用的是 "沒定義 MODULE 的"
#### 使用別名
將老師的實驗修改一下
```c=
#include <stdio.h>
int __func() {
printf("In __func\n");
return 0;
}
int __func() __attribute__((alias("func")));
int main(int argc, char const* argv[]) {
func();
return 0;
}
```
產生錯誤訊息
```shell=
fun.c:8:5: error: redefinition of ‘__func’
8 | int __func() __attribute__((alias("func")));
| ^~~~~~
fun.c:3:5: note: previous definition of ‘__func’ was here
3 | int __func() {
| ^~~~~~
fun.c: In function ‘main’:
fun.c:11:5: warning: implicit declaration of function ‘func’ [-Wimplicit-function-declaration]
11 | func();
| ^~~~
fun.c: At top level:
fun.c:8:5: error: ‘__func’ aliased to undefined symbol ‘func’
8 | int __func() __attribute__((alias("func")));
| ^~~~~~
```
所以 line 7 的意義應該是 “宣告一個 function __func,__func 是 func 的別名“,但 func 不存在,就產生了錯誤
使用 readelf 觀察 fibdrv.ko
`$ readelf -a fibdrv.ko | less`
搜尋 `modinfo` 發現以下資訊
```shell=
[17] .modinfo PROGBITS 0000000000000000 000005c8
00000000000000ed 0000000000000000 A 0 0 1
```
使用 hex editor 開啟 fibdrv.ko 可以確認 offset 是 0x5c8
### 學習 linux rootkits
在 [Linux 核心模組運作原理](https://hackmd.io/pfRzP9sxRYqYbXS6Tm0Ynw?view) 下方有延伸閱讀
- Hiding Kernel Modules from Userspace
這有一系列文章,學習一下
#### [Linux Rootkits Part 1: Introduction and Workflow](https://xcellerator.github.io/posts/linux_rootkits_01/)
解釋如何 build kernel module 這已經會了,看過即可
#### [Linux Rootkits Part 2: Ftrace and Function Hooking](https://xcellerator.github.io/posts/linux_rootkits_02/)
介紹如何使用 ftrace hook system call ,他的例子 hook mkdir。這也是作業一 hook 所使用的方法。
只有包含在 `/proc/kallsyms` 的 function,才能夠使用 hook 的方法
#### [Linux Rootkits Part 3: A Backdoor to Root](https://xcellerator.github.io/posts/linux_rootkits_03/)
展示了 hook 了強大之處, hook kill system call,藉以取得 root 權限
看到這寫作業的先備知識應該夠了,開始看作業
## 解釋上述程式碼運作原理,包含 ftrace 的使用
### _hideproc_init
首先從進入點`_hideproc_init`切入,這個 function 主要的工作是註冊 device 及 class,最後是 hook
_hideproc_init 並沒有做好 error handle,需要改進
```c=
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);
hideproc_class = class_create(THIS_MODULE, DEVICE_NAME);
cdev_init(&cdev, &fops);
cdev_add(&cdev, MKDEV(dev_major, MINOR_VERSION), 1);
device_create(hideproc_class, NULL, MKDEV(dev_major, MINOR_VERSION), NULL,
DEVICE_NAME);
init_hook();
return 0;
}
```
line 6:
註冊 device 有兩種方式 `alloc_chrdev_region` 跟 `register_chrdev_region`,除非確定 major number 才使用 `register_chrdev_region`,否則用 `alloc_chrdev_region`讓系統幫你配置
line 7:
macro`MAJOR` ,定義在`linux/kdev_t.h`,可以得到 major number
```c=
#define MINORBITS 20
#define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))
```
line 9:
class_create(THIS_MODULE, DEVICE_NAME);
定義在 `linux/device.h`
```c=
/* This is a #define to keep the compiler from merging different
* instances of the __key variable */
#define class_create(owner, name) \
({ \
static struct lock_class_key __key; \
__class_create(owner, name, &__key); \
})
```
__class_create 建立一個 可以被 device_create 使用的 class 並回傳,不需要時記得呼叫 class_destroy 回收
```c=
struct class * __class_create(struct module * owner, const char * name, struct lock_class_key * key);
```
line 11:
`cdev_init`, `cdev_add`,init 一個 cdev
line 13:
`device_create`,在 sysfs 建立一個 device
```shell=
$ ls /sys/module/hideproc/
coresize holders initsize initstate notes refcnt sections srcversion taint uevent
$ ls -l /sys/class/hideproc/
total 0
lrwxrwxrwx 1 root root 0 Jul 28 17:51 hideproc -> ../../devices/virtual/hideproc/hideproc
```
line 16:
`init_hook` 下方介紹
### _hideproc_exit
目前沒做任何事,但是`_hideproc_init`有呼叫`class_create`,這邊要有對應的`class_destory`才對,還有 unregister_chrdev
### init_hook
作業希望 hook `find_ge_pid` 達到我們想做的事情,於是透過`kallsyms_lookup_name`可以取得有 export 的 function address ,包含 system call 及 kernel function
定義一個 struct 紀錄 hook 的資訊,並宣告一個 function pointer type,signatrue 跟我們想要 hook 的 function 一樣
```c=
struct ftrace_hook {
const char *name;
void *func, *orig;
unsigned long address;
struct ftrace_ops ops;
};
typedef struct pid *(*find_ge_pid_func)(int nr, struct pid_namespace *ns);
static find_ge_pid_func real_find_ge_pid;
```
下方是 init_hook 的程式碼,line 2 是多餘的,因為 hook_install 會再做一次
```c=
static void init_hook(void) {
real_find_ge_pid = (find_ge_pid_func)kallsyms_lookup_name("find_ge_pid");
hook.name = "find_ge_pid";
hook.func = hook_find_ge_pid;
hook.orig = &real_find_ge_pid;
hook_install(&hook);
}
```
定義一個 macro 方便初始化 ftrace_hook
```c=
#define HOOK(_name, _hook, _orig) \
{ \
.name = SYSCALL_NAME(_name), \
.function = (_hook), \
.original = (_orig), \
}
```
修改 hook variable 的初始化
```c=
static struct ftrace_hook hook = HOOK("find_ge_pid", hook_find_ge_pid, &real_find_ge_pid);
```
### hook_install
可分為 4 部份:
1. `hook_resolve_addr`利用`kallsyms_lookup_name`尋找並紀錄 function address
2. 設定 ftrace 攔截到 original function 時,要做什麼動作,實做在 `hook_ftrace_thunk`,以及 ftrace 的設定,此次使用了三個設定
3. `ftrace_set_filter_ip`設定要追蹤的 function
4. `register_ftrace_function` enables trace
`
```c=
static int hook_install(struct ftrace_hook *hook)
{
int err = hook_resolve_addr(hook);
if (err)
return err;
hook->ops.func = hook_ftrace_thunk;
hook->ops.flags = FTRACE_OPS_FL_SAVE_REGS | FTRACE_OPS_FL_RECURSION_SAFE |
FTRACE_OPS_FL_IPMODIFY;
err = ftrace_set_filter_ip(&hook->ops, hook->address, 0, 0);
if (err) {
printk("ftrace_set_filter_ip() failed: %d\n", err);
return err;
}
err = register_ftrace_function(&hook->ops);
if (err) {
printk("register_ftrace_function() failed: %d\n", err);
ftrace_set_filter_ip(&hook->ops, hook->address, 1, 0);
return err;
}
return 0;
}
```
### hook_remove
disable trace and clear filter
```c=
void hook_remove(struct ftrace_hook *hook)
{
int err = unregister_ftrace_function(&hook->ops);
if (err)
printk("unregister_ftrace_function() failed: %d\n", err);
err = ftrace_set_filter_ip(&hook->ops, hook->address, 1, 0);
if (err)
printk("ftrace_set_filter_ip() failed: %d\n", err);
}
```
### hook_ftrace_thunk
當 rip 是我們所要追蹤的 function address 時會被呼叫。
將 rip 替換成 hook function,使用 within_module 判斷有無 recursion 的情況
```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;
}
```
### device_read
將目前 hide pid 傳回 user
```c=
static ssize_t device_read(struct file *filep,
char *buffer,
size_t len,
loff_t *offset)
{
pid_node_t *proc, *tmp_proc;
char message[MAX_MESSAGE_SIZE];
if (*offset)
return 0;
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;
}
```
### device_write
根據 使用者的指令 hide /unhide process
### hide_process
將 hide pid 加入 list
### unhide_process
清空 list,但應該是要只清掉指定 pid 的 node
## 允許其 PPID 也跟著隱藏
問題在於:如何從 PID 找到 PPID ?
首先要暸解 [PID namespace](https://titanwolf.org/Network/Articles/Article?AID=9f7206f6-f50a-4a45-bbf3-8743beb1e3e1#gsc.tab=0) 的架構,便知道尋找的路徑如下
1. 已知 child PID,利用 `find_get_pid` 找到 struct pid
2. 再從 struct pid 找到 tast_strct ,使用`get_pid_task`
3. 透過 tast_struct::real_parent 取得 parent task_struct
4. 利用 task_pid_vnr(parent) 取得 parent PID
取得 parent 之後,記得呼叫 `put_task` 及 `put_pid` 釋放資源
`get_parent_pid` 即是上述的實作
```c=
static pid_t get_parent_pid(pid_t vnr)
{
struct pid *pid;
struct task_struct *p;
pid_t ppid = 0;
/* Find parent pid */
pid = find_get_pid(vnr);
if (!pid) {
goto out_err;
}
p = get_pid_task(pid, PIDTYPE_PID);
if (!p) {
goto out_pid;
}
ppid = task_pid_vnr(p->real_parent);
if (!ppid) {
goto out_task;
}
out_task:
put_task_struct(p);
out_pid:
put_pid(pid);
out_err:
return ppid;
}
```
hide_process 隱藏 PID 及 parent PID
do_hide_process 將 PID 加入 hide list 中
```c=
static int do_hide_process(pid_t pid)
{
printk(KERN_INFO "@ %s pid: %d\n", __func__, pid);
pid_node_t *proc = kmalloc(sizeof(pid_node_t), GFP_KERNEL);
if (!proc) {
printk(KERN_ERR "%s: kmalloc() failed!\n", __func__);
return -ENOMEM;
}
proc->id = pid;
list_add_tail(&proc->list_node, &hidden_proc);
return SUCCESS;
}
static int hide_process(pid_t vnr)
{
pid_t ppid;
int err = SUCCESS;
if (!vnr)
return -EAGAIN;
err = do_hide_process(vnr);
if (err) {
return err;
}
ppid = get_parent_pid(vnr);
if (!ppid)
return -ESRCH;
err = do_hide_process(ppid);
if (err)
return err;
return err;
}
```
unhide_process 回復 PID 及 parent PID
do_unhide_process 將 PID 從 hide list 去除
```c=
static int unhide_process(pid_t pid)
{
pid_t ppid;
int err = SUCCESS;
err = do_unhide_process(pid);
if (err)
return err;
ppid = get_parent_pid(pid);
if (!ppid)
return -ESRCH;
err = do_unhide_process(ppid);
return err;
}
static int do_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;
}
```
測試:
撰寫一個簡單的測試程式,程式名稱為 `forever_sleep.c` 功能是 fork 之後就不停 sleep
```c=
#include <stdio.h>
#include <unistd.h>
int main(int argc, char const *argv[])
{
fork();
while (1) {
sleep(10);
}
return 0;
}
```
測試流程
```shell=
$ sudo insmod hideproc.ko
$ gcc forever_sleep.c -o forever_sleep
$ ./forever_sleep &
[1] 10345
$ pidof forever_sleep
10346 10345
$ echo "add 10346" | sudo tee /dev/hideproc # add child PID
$ pidof forever_sleep # show nothing
$ echo "del 10346" | sudo tee /dev/hideproc
$ pidof forever_sleep
10346 10345
```
## 指出程式碼可改進的地方,並動手實作
### _hideproc_init error handle
使用 goto 讓程式更簡潔
reference:https://github.com/torvalds/linux/blob/5bfc75d92efd494db37f5c4c173d3639d4772966/drivers/s390/char/tape_class.c
```c=
static int _hideproc_init(void) {
int err;
struct device* device;
printk(KERN_INFO "@ %s\n", __func__);
err = alloc_chrdev_region(&dev, 0, MINOR_VERSION, DEVICE_NAME);
if (err) {
printk(KERN_ERR "hideproc: Couldn't alloc_chrdev_region, error=%d\n",
err);
goto out_chrdev;
}
hideproc_class = class_create(THIS_MODULE, DEVICE_NAME);
if (IS_ERR(hideproc_class)) {
err = PTR_ERR(hideproc_class);
printk(KERN_ERR "hideproc: Couldn't class_create, error=%d\n", err);
goto out_class;
}
cdev_init(&cdev, &fops);
err = cdev_add(&cdev, dev, 1);
if (err) {
printk(KERN_ERR "hideproc: Couldn't cdev_add, error=%d\n", err);
goto out_cdev;
}
device = device_create(hideproc_class, NULL, dev, NULL, DEVICE_NAME);
if (IS_ERR(device)) {
err = PTR_ERR(device);
goto out_device;
}
init_hook();
return 0;
out_device:
cdev_del(&cdev);
out_cdev:
class_destroy(hideproc_class);
out_class:
unregister_chrdev_region(dev, MINOR_VERSION);
out_chrdev:
return err;
}
```
### release resources
release init 時所註冊/建立的結構
```c=
static void _hideproc_exit(void) {
printk(KERN_INFO "@ %s\n", __func__);
/* FIXME: ensure the release of all allocated resources */
release_hide_list();
device_destroy(hideproc_class, dev);
cdev_del(&cdev);
class_destroy(hideproc_class);
unregister_chrdev_region(dev, MINOR_VERSION);
}
```
## unhide certain pid
刪除指定的 node,原本的實作會刪除 list 中所有的 node
```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;
}
```
測試:
```shell=
$ sudo insmod hideproc.ko
$ pidof cron
660
$ echo "add 660" | sudo tee /dev/hideproc
add 660
$ pidof multipathd
465
$ echo "add 465" | sudo tee /dev/hideproc
add 465
$ cat /dev/hideproc
pid: 660
pid: 465
$ echo "del 465" | sudo tee /dev/hideproc
del 465
$ sudo cat /dev/hideproc
pid: 660
```