Try   HackMD

2021q3 Homework1 (quiz1)

contributed by < hankluo6 >

第 1 週測驗題

ftrace hook

The ftrace infrastructure was originially created to attach callbacks to the beginning of functions in order to record and trace the flow of the kernel. But callbacks to the start of a function can have other use cases. Either for live kernel patching, or for security monitoring.

- Using ftrace to hook to functions

struct ftrace_hook {
    const char *name;
    void *func, *orig;
    unsigned long address;
    struct ftrace_ops ops;
};

自訂 ftrace_hook 方便我們建立 ftrace hook。其中 name 為 kernel 內要被取代的原始函式名字;orig 為原始函式地址;func 為要 hook 的函式而 ops 則儲存 ftrace 內部執行 callback 需要的資料。

透過 init_hook 開始建立 hook function:

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

kallsyms_lookup_name 可以取得對應函式在 kernel 內的位址,並設置相關參數。

static int hook_resolve_addr(struct ftrace_hook *hook)
{
    hook->address = kallsyms_lookup_name(hook->name);
    if (!hook->address) {
        printk("unresolved symbol: %s\n", hook->name);
        return -ENOENT;
    }
    *((unsigned long *) hook->orig) = hook->address;
    return 0;
}

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_resolve_addr 給定 hook-address 要被取代的函式在 kernel 內的位置(與 hook.orig 相同);hook->ops.func 則為 ftrace 在進入 kernel 內函式時,真正會執行的 callback function;ftrace_set_filter_ipregister_ftrace_function 皆為讓 ftrace 知道我們需要註冊的函數資訊。

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

ftrace 在追蹤時主要會呼叫此 callback function,而內部通過 regs->ip (其意義等同於 x86 內的 %rip 暫存器,儲存下個指令的位置)直接將下次指令改為我們想要執行的 function (此處為 hook_find_ge_pid)。within_module 用來防止遞迴呼叫此函式,parent_id 會指向呼叫此參數的地址。

透過上述的 ftrace hook,我們可以把 kernel 內的 find_ge_pid 改為我們撰寫的 hook_find_ge_pid 來執行。而 pidof 內部應會使用 find_ge_pid 來實現,而被改成執行 hook_find_ge_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;
}

hook_find_ge_pid 利用 real_find_ge_pid 取得原本的 pid,如果此 process 為我們隱藏的 process,則強制搜尋其 pid 值加一的 process,便能防止回傳到我們 process 的 pid。

Fix bug

module_exit 時,要正確的解除 ftrace 的綁定,否則下次 insmod 時可能會出現錯誤,將 hook_remove 的 condition build 移除,並在 _hideproc_exit 增加 hook_remove(&hook);

此時呼叫 insmod 後再透過 rmmod 卸載後,要再重新安裝一次 driver 時會出現以下錯誤:

kobject_add_internal failed for hideproc with -EEXIST, don't try to register things with the same name in the same directory.

這是因為原本 hook_remove 內沒有完全釋放所有註冊的裝置,可以透過以下命令查看:

$ sudo find / -name hideproc

/sys/class/hideproc
/sys/class/hideproc/hideproc
/sys/devices/virtual/hideproc
/sys/devices/virtual/hideproc/hideproc

添加對應的釋放函式即可解決問題

static dev_t dev; //move to global

static void _hideproc_exit(void)
{
    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);
    }
    device_destroy(hideproc_class, MKDEV(dev_major, MINOR_VERSION));
    class_destroy(hideproc_class);
    cdev_del(&cdev);
    hook_remove(&hook);
}

MINOR_VERSION

從原始碼的註解可以理解 alloc_chrdev_region 的參數意義:

/**
 * alloc_chrdev_region() - register a range of char device numbers
 * @dev: output parameter for first assigned number
 * @baseminor: first of the requested range of minor numbers
 * @count: the number of minor numbers required
 * @name: the name of the associated device or driver
 *
 * Allocates a range of char device numbers.  The major number will be
 * chosen dynamically, and returned (along with the first minor number)
 * in @dev.  Returns zero or a negative error code.
 */
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,
			const char *name)

其中 baseminor 為指定 range of minor numbers 的第一個數,count 為需要 minor numbers 的數量。

而在原本 hideproc 中指定 0 為 minor numbers 的第一個數,range 則設定為 1

#define MINOR_VERSION 1

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

應要改成 alloc_chrdev_region(&dev, MINOR_VERSION, 1, DEVICE_NAME); 才正確。

但如果按照原本程式執行時好像也能得到正確的結果,可以從原始碼中看出端倪,alloc_chrdev_region 內部會呼叫 __register_chrdev_region 分配區域:

/* * Register a single major with a specified minor range. * * If major == 0 this function will dynamically allocate an unused major. * If major > 0 this function will attempt to reserve the range of minors * with given major. * */ static struct char_device_struct * __register_chrdev_region(unsigned int major, unsigned int baseminor, int minorct, const char *name) { ... cd = kzalloc(sizeof(struct char_device_struct), GFP_KERNEL); if (major == 0) { ret = find_dynamic_major(); if (ret < 0) { pr_err("CHRDEV \"%s\" dynamic allocation region is full\n", name); goto out; } major = ret; } i = major_to_index(major); for (curr = chrdevs[i]; curr; prev = curr, curr = curr->next) { if (curr->major < major) continue; if (curr->major > major) break; if (curr->baseminor + curr->minorct <= baseminor) continue; if (curr->baseminor >= baseminor + minorct) break; goto out; } ... out: mutex_unlock(&chrdevs_lock); kfree(cd); return ERR_PTR(ret); }

第 16 ~ 24 行從沒使用的 devices 內找到一個可用的 device number,重點在 27 ~ 41 行的 for 迴圈。在用過得 devices 中,如果 major 為相同時,其對應的 minor numbers 範圍便不能重疊,第 34 ~ 38 行便是在檢查是否有重疊的情況發生。

但因為 hideproc 中 alloc_chrdev_region 內的 major 參數為 0,表示會由 find_dynamic_major 找到合適的 major number 來使用,意味著不會有兩個 devices 使用相同的 major number,也就不會有重疊的問題產生,所以程式能如期執行。

kallsyms

符號 kallsyms_lookup_name 在 Linux 5.7+ 以上的版本不再對外揭露 (Unexporting kallsyms_lookup_name()),故需要利用其他方法來取得核心內的符號地址。

這邊我使用 Kprobes,添加以下程式碼:

static unsigned long lookup_name(const char *name)
{
    struct kprobe kp = {
        .symbol_name = name
    };
    unsigned long retval;

    if (register_kprobe(&kp) < 0) return 0;
    retval = (unsigned long) kp.addr;
    unregister_kprobe(&kp);
    return retval;
}

並將原本的 kallsyms_lookup_name 改為 lookup_name 即可。

Kretprobes

[GitHub]

要使用 kprobes 取得 symbol address 後再用 ftrace 去 hook 有點多此一舉的感覺,我們可以使用 kretprobes (kprobes 的一種包裝) 來達到與 ftrace hook 類似的效果。

static struct kretprobe hide_kretprobe = {
    .entry_handler      = entry_handler,
    .handler            = ret_handler,
    .maxactive          = 20,
    .kp.symbol_name     = "find_ge_pid",
};

先宣告一個 kretprobe 物件,其中註冊兩個 function: entry_handler 會在函式 (即 find_ge_pid) 進入前呼叫;ret_handler 則在函式執行完呼叫。

我們的目標是要更改 find_ge_pid 回傳的內容,所以 entry_handler 可以不用特別改動,參考範例

static int entry_handler(struct kretprobe_instance *ri, struct pt_regs *regs)
{
    if (!current->mm)
        return 1;	/* Skip kernel threads */

    return 0;
}

重點在於 ret_handler,我們的目標是在函式結束後,想辦法更改回傳值:

static int ret_handler(struct kretprobe_instance *ri, struct pt_regs *regs)
{
    struct pid *pid = (struct pid *)regs_return_value(regs);
    if (pid && is_hidden_proc(pid->numbers->nr))
    {	
        regs->ax = (unsigned long)0; //x86 only
    }	

    return 0;
}

pt_regs 儲存目前在 userspace 下的 stack register,所以可以透過
regs_return_value 回傳剛剛執行的 function 內的 return value。此時就可以透過檢查 find_ge_pid 的回傳是否在我們程式內的 hidden process 中,如果該 process 是我們隱藏的 process 時,直接修改 regs 內對應到 return value 的 register ax 即可。

只有在 x86 架構中, regs->ax 為 return value,其他架構可自行參考 arch/alpha/include/asm/ptrace.h