Try   HackMD

2021q3 Homework1 (quiz1)

contributed by < Hao-yu-lin >

tags: linux2021

2021 年暑期 Linux 核心 第 1 週測驗題

Linux 核心版本:GNU/Linux 5.4.0-80-generic x86_64

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

依照執行流程分成三部分,探討程式碼

Part1:Module insert

當使用下方的指令後,便會開始載入 module

sudo insmod hideproc.ko 

/include/linux/module.h 內容得知

module_init(_hideproc_init);
module_exit(_hideproc_exit);

為程式的進入點

module_init():driver initialization entry point
module_exit():driver exit entry point

觀察_hideproc_init 得知,此程式主要在建立 hideproc 裝置環境,建立後執行init_hook() 建立 hook function

static void init_hook(void)
{
    real_find_ge_pid = (find_ge_pid_func) kallsyms_lookup_name("find_ge_pid");
    ...
    hook_install(&hook);
}

hook_install 此處做的事情是初始化 hook_ops ,主要目的是將 hook 內的 func 成員指向 hook_ftrace_thunk

static int hook_install(struct ftrace_hook *hook)
{   
    // 取得 Address 
    int err = hook_resolve_addr(hook);
    ...
    
    // 將 ops 取代為 hook_ftrace_thunk 為 ftrace 再進入 kernel 內時,真正執行 callback 的 func
    hook->ops.func = hook_ftrace_thunk;
    hook->ops.flags = FTRACE_OPS_FL_SAVE_REGS | FTRACE_OPS_FL_RECURSION_SAFE |
                      FTRACE_OPS_FL_IPMODIFY;
    
    // 使用 ftrace_set_filter_ip 建立一個 fliter 來篩選出需要的函數後才執行 hook
    err = ftrace_set_filter_ip(&hook->ops, hook->address, 0, 0);
    ...
    
    // 使用 register_ftrace_function 允許 ftrace 調用 hook_ftrace_thunk
    err = register_ftrace_function(&hook->ops);
    ...
    return 0;
}

hook_resolve_addr 在此處的目的就是找到 find_ge_pid 的函數 address
並保存在 hook->orig

static int hook_resolve_addr(struct ftrace_hook *hook)
{
    
    hook->address = kallsyms_lookup_name(hook->name);
    ...
    *((unsigned long *) hook->orig) = hook->address;
    return 0;
}

接續探討 hook_ftrace_thunk,將 自定義的 hook_find_ge_pid 透過更改 %rip 的下一步指令,強制跳轉到 hook function

static void notrace hook_ftrace_thunk(unsigned long ip,
                                      unsigned long parent_ip,
                                      struct ftrace_ops *ops,
                                      struct pt_regs *regs)
{
    // 利用 container_of 來獲得 原始 hook 的 address
    struct ftrace_hook *hook = container_of(ops, struct ftrace_hook, ops);
    // 避免重複調用
    if (!within_module(parent_ip, THIS_MODULE))
        regs->ip = (unsigned long) hook->func;
}

Part2:(Un)hide Process

當使用下方指令後,device 便會開始執行 device_write 指令
device_write 會根據你輸入在 buffer 內的指令,來執行 hide_process 或是 unhide_process

echo "add 644" | sudo tee /dev/hideproc 
echo "del 644" | sudo tee /dev/hideproc 

建立一個 proc,設定 id 為 PID,並將此 proc 連接到 hidden_proc list 串的最尾巴

static int hide_process(pid_t pid)
{
    pid_node_t *proc = kmalloc(sizeof(pid_node_t), GFP_KERNEL);
    proc->id = pid;
    
    list_add_tail(&proc->list_node, &hidden_proc);
    return SUCCESS;
}

Part3:Show PID

當使用下方指令後

pidof cron

由於 callback function 作用會導向執行 hook_find_ge_pid,透過 is_hidden_proc 來遍歷 hidden_proc 若有符合的 pid,就會 pid ++ 直到不相符的 pid,此時就會因為找不到相符的 pid 而輸出找不到

static struct pid *hook_find_ge_pid(int nr, struct pid_namespace *ns)
{
    struct pid *pid = real_find_ge_pid(nr, ns);
    while (pid && is_hidden_proc(pid->numbers->nr))
        pid = real_find_ge_pid(pid->numbers->nr + 1, ns);
    return pid;
}

本核心模組只能隱藏單一 PID,請擴充為允許其 PPID 也跟著隱藏,或允許給定一組 PID 列表,而非僅有單一 PID

透過 find_get_pid 取得 child 的 pid struct,藉由 get_pid_task 取得 task struct,再利用 real_parent,來得到 parent 的 pid 即可

#include <linux/sched.h>

static pid_t get_ppid(long cpid)
{
    struct task_struct *child = NULL;
    struct pid *child_pid = NULL;
    struct task_struct *parent = NULL;
    

    child_pid = find_get_pid(cpid);
    child = get_pid_task(child_pid, PIDTYPE_PID);
    parent = child -> real_parent;

    return parent -> pid;
    
}
static ssize_t device_write(struct file *filep,
                            const char *buffer,
                            size_t len,
                            loff_t *offset)
{
    
    ...
    if (!memcmp(message, add_message, sizeof(add_message) - 1)) {
        kstrtol(message + sizeof(add_message), 10, &pid);
        hide_process(pid);
        ppid = get_ppid(pid);
        hide_process(ppid);

    } else if (!memcmp(message, del_message, sizeof(del_message) - 1)) {
        kstrtol(message + sizeof(del_message), 10, &pid);
        unhide_process(pid);
        ppid = get_ppid(pid);
        unhide_process(ppid);
    } else {
        kfree(message);
        return -EAGAIN;
    }

    *offset = len;
    kfree(message);
    return len;
}

以下是原始尚未新增同時刪除 PPID 時的操作

$ pidof cron        # 輸出 590
$ pstree -s -p 590    # 輸出 systemd(1)───cron(590) 得知 ppid = 1
$ echo "add 590" | sudo tee /dev/hideproc
$ pstree -s -p 590    # 發現 pid:590 被忽略找不到了
systemd(1)─┬─accounts-daemon(586)─┬─{accounts-daemon}(594)
           │                      └─{accounts-daemon}(618)
           ├─atd(612)
           ├─dbus-daemon(592)
           ├─irqbalance(598)───{irqbalance}(635)
           ├─login(629)───bash(1187)
           ├─multipathd(475)─┬─{multipathd}(476)
           │                 ├─{multipathd}(477)
           │                 ├─{multipathd}(478)
           │                 ├─{multipathd}(479)
           │                 ├─{multipathd}(480)
           │                 └─{multipathd}(481)
            ......    省略


$ echo "add 1" | sudo tee /dev/hideproc
$ pstree -s -p 590    # 此時(1) 這地方直接變成 ?
?(1)─┬─accounts-daemon(586)─┬─{accounts-daemon}(594)
     │                      └─{accounts-daemon}(618)
     ├─atd(612)
     ├─dbus-daemon(592)
     ├─irqbalance(598)───{irqbalance}(635)
     ├─login(629)───bash(1187)
     ├─multipathd(475)─┬─{multipathd}(476)
     │                 ├─{multipathd}(477)
     │                 ├─{multipathd}(478)
     │                 ├─{multipathd}(479)
     │                 ├─{multipathd}(480)
     │                 └─{multipathd}(481)
     ├─networkd-dispat(600)
     ├─polkitd(647)─┬─{polkitd}(648)
     │              └─{polkitd}(650)
     ├─sh(1328)───node(1335)─┬─node(1385)─┬─bash(1437)───pstree(7285)
     │                       │            ├─{node}(1386)
     │                       │            ├─{node}(1387)
     │                       │            ├─{node}(1388)
     │                       │            ├─{node}(1389)
    

$ echo "del 1" | sudo tee /dev/hideproc 
$ echo "del 590" | sudo tee /dev/hideproc 
$ pstree -s -p 590 # 輸出 systemd(1)───cron(590) 變正常了

若新增同時刪除 PPID

因為要重新編譯 code 此時發現 linux 當機了,此問題留到下一題解決

$ pidof cron        # 輸出 584
$ pstree -s -p 584    # 輸出 systemd(1)───cron(584) 得知 ppid = 1
$ echo "add 584" | sudo tee /dev/hideproc
$ pstree -s -p 584    # ppid 成功消失了
?(1)─┬─accounts-daemon(581)─┬─{accounts-daemon}(586)
     │                      └─{accounts-daemon}(615)
     ├─atd(605)
     ├─dbus-daemon(587)
     ├─irqbalance(593)───{irqbalance}(630)
     ├─login(613)───bash(888)
     ├─multipathd(472)─┬─{multipathd}(473)
     │                 ├─{multipathd}(474)
     │                 ├─{multipathd}(475)
     │                 ├─{multipathd}(476)
     │                 ├─{multipathd}(477)
     │                 └─{multipathd}(478)


指出程式碼可改進的地方,並動手實作

每當我 sudo rmmod hideproc,linux 就會直接當機

_hideproc_exit() 沒有釋放相關資源

首先要把 MKDEV(dev_major, MINOR_VERSION),不然一直輸入看起來很不簡潔,首先釋放 hook 資源,利用已經寫好的 hook_remove 來釋放,接著對照 _hideproc_init 反向操作的回收資源

  • device_create / device_destroy
  • cdev_add / cdev_del
  • class_create / class_destroy
  • alloc_chrdev_region / unregister_chrdev_region
static void _hideproc_exit(void)
{
    
    printk(KERN_INFO "@ %s\n", __func__);
    hook_remove(&hook);
    device_destroy(hideproc_class, MK_DEV);
    class_destroy(hideproc_class);
    cdev_del(&cdev);
    unregister_chrdev_region(MK_DEV, MINOR_VERSION);
    /* FIXME: ensure the release of all allocated resources */
}

unhide_process() 全部刪除

在實作嘗試刪除其他 pid,然後將其復原時發現,復原時他會一併復原,因此對 unhide_process 加點判斷式

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(pid == proc->id){
            list_del(&proc->list_node);
            kfree(proc);
        }
    }
    return SUCCESS;
}

參考資料

Using ftrace to hook to functions
ftrace來進行內核hook,part2