contributed by < dmefs
>
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 追蹤執行 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 核心模組運作原理 下方有延伸閱讀
這有一系列文章,學習一下
解釋如何 build kernel module 這已經會了,看過即可
介紹如何使用 ftrace hook system call ,他的例子 hook mkdir。這也是作業一 hook 所使用的方法。
只有包含在 /proc/kallsyms
的 function,才能夠使用 hook 的方法
展示了 hook 了強大之處, hook kill system call,藉以取得 root 權限
看到這寫作業的先備知識應該夠了,開始看作業
首先從進入點_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_region
跟 register_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_init
有呼叫class_create
,這邊要有對應的class_destory
才對,還有 unregister_chrdev
作業希望 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);
可分為 4 部份:
hook_resolve_addr
利用kallsyms_lookup_name
尋找並紀錄 function addresshook_ftrace_thunk
,以及 ftrace 的設定,此次使用了三個設定ftrace_set_filter_ip
設定要追蹤的 functionregister_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;
}
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);
}
當 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;
}
將目前 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;
}
根據 使用者的指令 hide /unhide process
將 hide pid 加入 list
清空 list,但應該是要只清掉指定 pid 的 node
問題在於:如何從 PID 找到 PPID ?
首先要暸解 PID namespace 的架構,便知道尋找的路徑如下
已知 child PID,利用 find_get_pid
找到 struct pid
再從 struct pid 找到 tast_strct ,使用get_pid_task
透過 tast_struct::real_parent 取得 parent task_struct
利用 task_pid_vnr(parent) 取得 parent PID
取得 parent 之後,記得呼叫 put_task
及 put_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
使用 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 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);
}
刪除指定的 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