Try   HackMD

2021q3 Homework1 (quiz1)

contributed by < dmefs >

tags: linux2021

環境: windows 10 使用 multipass ,透過 vscode ssh 連到 vm 寫程式

$ 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 核心模組運作原理
根據文章的思路學習並筆記

strace

利用 strace 追蹤執行 insmod fibdrv.ko 的過程有哪些系統呼叫被執行
執行指令 sudo strace insmod fibdrv.ko
有 103 行,摘錄最後幾行,可以發現 finit_module 在 99 行

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/

使用 / 搜尋 可以找到

static initcall_t __initcall_init_fib_dev6 __used __attribute__((__section__(".initcall6" ".init"))) = init_fib_dev;

如同老師說的使用的是 "沒定義 MODULE 的"

使用別名

將老師的實驗修改一下

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

產生錯誤訊息

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 發現以下資訊

[17] .modinfo PROGBITS 0000000000000000 000005c8 00000000000000ed 0000000000000000 A 0 0 1

使用 hex editor 開啟 fibdrv.ko 可以確認 offset 是 0x5c8

學習 linux rootkits

Linux 核心模組運作原理 下方有延伸閱讀

  • Hiding Kernel Modules from Userspace

這有一系列文章,學習一下

Linux Rootkits Part 1: Introduction and Workflow

解釋如何 build kernel module 這已經會了,看過即可

Linux Rootkits Part 2: Ftrace and Function Hooking

介紹如何使用 ftrace hook system call ,他的例子 hook mkdir。這也是作業一 hook 所使用的方法。

只有包含在 /proc/kallsyms 的 function,才能夠使用 hook 的方法

Linux Rootkits Part 3: A Backdoor to Root

展示了 hook 了強大之處, hook kill system call,藉以取得 root 權限
看到這寫作業的先備知識應該夠了,開始看作業

解釋上述程式碼運作原理,包含 ftrace 的使用

_hideproc_init

首先從進入點_hideproc_init切入,這個 function 主要的工作是註冊 device 及 class,最後是 hook
_hideproc_init 並沒有做好 error handle,需要改進

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_regionregister_chrdev_region,除非確定 major number 才使用 register_chrdev_region,否則用 alloc_chrdev_region讓系統幫你配置

line 7:
macroMAJOR ,定義在linux/kdev_t.h,可以得到 major number

#define MINORBITS 20 #define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))

line 9:
class_create(THIS_MODULE, DEVICE_NAME);
定義在 linux/device.h

/* 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 回收

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

$ 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 一樣

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 會再做一次

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

#define HOOK(_name, _hook, _orig) \ { \ .name = SYSCALL_NAME(_name), \ .function = (_hook), \ .original = (_orig), \ }

修改 hook variable 的初始化

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
    `
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

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 的情況

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

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 的架構,便知道尋找的路徑如下

  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_taskput_pid 釋放資源

get_parent_pid 即是上述的實作

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 中

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 去除

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

#include <stdio.h> #include <unistd.h> int main(int argc, char const *argv[]) { fork(); while (1) { sleep(10); } return 0; }

測試流程

$ 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

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 時所註冊/建立的結構

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

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

測試:

$ 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