• 文章/ARTICLE
  • CVE-2015-5774分析及利用
  • 360NirvanTeam 2015-12-23 3863

  盘古团队在博客上放出了太极8.4越狱使用的内核漏洞CVE-2015-5774的简单分析(http://blog.pangu.io/cve-2015-5774/)。本文将结合IOHIDFamily源代码对该漏洞进行分析,并阐述在编写POC的过程中遇到的问题以及解决方法,最后根据较新的源码可以看到苹果官方是如何对这个漏洞进行修补的。

 

一、简要说明

 


        前一段时间,盘古团队在博客上放出了太极8.4越狱使用的内核漏洞CVE-2015-5774的分析(http://blog.pangu.io/cve-2015-5774/)。基于之前分析CVE-2014-4487的基础,这次尝试不逆向太极的二进制文件,直接正向来写POC,最终实现虚表指针的控制。

 

二、漏洞分析

 


        这个漏洞出现在IOHIDFamily中,位于IOHIDResourceDeviceUserClient,其3号调用_postReportResult中,对于IOBufferMemoryDescriptor的长度处理不当,可造成堆溢出并最终导致任意代码执行。下面根据OS X 10.10.2的IOHIDFamily源码(606.10.8)来分析漏洞原因。

typedef enum {
    kIOHIDResourceDeviceUserClientMethodCreate,
    kIOHIDResourceDeviceUserClientMethodTerminate,
    kIOHIDResourceDeviceUserClientMethodHandleReport,
    kIOHIDResourceDeviceUserClientMethodPostReportResponse,  // = 3
    kIOHIDResourceDeviceUserClientMethodCount
} IOHIDResourceDeviceUserClientExternalMethods;

typedef enum {
    kIOHIDResourceUserClientResponseIndexResult = 0,
    kIOHIDResourceUserClientResponseIndexToken,
    kIOHIDResourceUserClientResponseIndexCount
} IOHIDResourceUserClientResponseIndex;

// IOHIDResourceDeviceUserClient::_methods
const IOExternalMethodDispatch IOHIDResourceDeviceUserClient::_methods[kIOHIDResourceDeviceUserClientMethodCount] = {
…
    {   // kIOHIDResourceDeviceUserClientMethodPostReportResult // = 3
        (IOExternalMethodAction) &IOHIDResourceDeviceUserClient::_postReportResult,
        kIOHIDResourceUserClientResponseIndexCount, -1, /* 1 scalar input: the result, 1 struct input : the buffer */
        0, 0
    }
};


        从源码可以看出,_postReportResult接受2个ScalarInput和1个StructInput。两个ScalarInput参数分别为Result和Token。造成漏洞的原因是输入的StructInput的大小。_postReportResult最终会调用IOHIDResourceDeviceUserClient::postReportResult,代码如下:

//----------------------------------------------------------------------------------------------
// IOHIDResourceDeviceUserClient::postReportResult
//----------------------------------------------------------------------------------------------
IOReturn IOHIDResourceDeviceUserClient::postReportResult(IOExternalMethodArguments * arguments)
{
    OSObject * tokenObj = (OSObject*)arguments->scalarInput[kIOHIDResourceUserClientResponseIndexToken];

    if ( tokenObj && _pending->containsObject(tokenObj) ) {
        OSData * data = OSDynamicCast(OSData, tokenObj);
        if ( data ) {
            __ReportResult * pResult = (__ReportResult*)data->getBytesNoCopy();
            
            // RY: HIGHLY UNLIKELY > 4K
            if ( pResult->descriptor && arguments->structureInput ) {
                pResult->descriptor->writeBytes(0, arguments->structureInput, arguments->structureInputSize);

                // 12978252:  If we get an IOBMD passed in, set the length to be the # of bytes that were transferred
                IOBufferMemoryDescriptor * buffer = OSDynamicCast(IOBufferMemoryDescriptor, pResult->descriptor);
                if (buffer)
                    buffer->setLength((vm_size_t)arguments->structureInputSize); // here!!
            }
                
            pResult->ret = (IOReturn)arguments->scalarInput[kIOHIDResourceUserClientResponseIndexResult];

            _commandGate->commandWakeup(data);
        }     
    }
    return kIOReturnSuccess;
}


        tokenObj中包含一个IOBufferMemoryDescriptor,是在IOHIDDevice::updateElementValues中初始化的,并且调用一次updateElementValues只初始化一个新的IOBufferMemoryDescriptor,多个cookie在调用getReport时,使用的都是同一个IOBufferMemoryDescriptor,而postReportResult在使用这个IOBufferMemoryDescriptor时会调用setLength(structureInputSize)修改其大小。

IOReturn IOHIDDevice::updateElementValues(IOHIDElementCookie *cookies, UInt32 cookieCount) {
    maxReportLength = max(_maxOutputReportSize, max(_maxFeatureReportSize, _maxInputReportSize));
    // Allocate a mem descriptor with the maxReportLength.
    // This way, we only have to allocate one mem discriptor
    report = IOBufferMemoryDescriptor::withCapacity(maxReportLength, kIODirectionNone);

    // Iterate though all the elements in the
    // transaction.  Generate reports if needed.
    for (index = 0; index < cookieCount; index++) { //多个cookie getReport使用同一个IOBufferMemoryDescriptor
    …
        ret = getReport(report, reportType, reportID);
    …
        if (ret == kIOReturnSuccess) {
            // If we have a valid report, go ahead and process it.
            ret = handleReport(report, reportType, kIOHIDReportOptionNotInterrupt);
        }
        if (ret != kIOReturnSuccess)
            break;
    }
    …
    return ret;
}


structureInput的数据和structureInputSize是我们可控的,就可以通过下述步骤进行攻击。


1. IOBufferMemoryDescriptor::withCapacity(maxReportLength,kIODirectionNone);
      初始化IOBufferMemoryDescriptor时,大小为maxReportLength,这是我们在创建HID设备时可以通过PropertyData指定的,例如maxReportLength = 128。这样,就会从zone.128中分配一个堆块。
 
2. 第一次postReportResult
      第一次postReportResult时,传入的structureInputSize大小为512。在writeBytes时并不会出问题,因为writeBytes会根据descriptor的最大长度128将数据进行截断。但是接着,setLength就会将descriptor的长度设置为structureInputSize=512,即原来只有128字节的空间长度却被设置成512字节,而其本身所占用的堆块的大小却仍然是128字节!

void IOBufferMemoryDescriptor::setLength(vm_size_t length)
{
    assert(length <= _capacity); //在发行版并没有这个断言,导致length可以大于capacity。

    _length = length;
    _ranges.v64->length = length;
}

 
3. 第二次postReportResult
      由于descriptor的大小被设置成了512,因此第二次调用postReportResult时再次传入512字节大小的StructureInput的数据,就不会发生截断,导致堆溢出的发生,可以覆盖descriptor所占用的堆块后的数据,结合堆风水就可以实现内核读和覆盖内核对象虚表实现任意代码执行。

 

 

三、POC编写

 



        根据上面的漏洞分析,如何编写POC也就有了一个大概的思路,启动一个线程调用updateElementValues,然后在主线程中调用两次postReportResult。在POC的编写过程中,有两个需要注意的地方。

 

(1)创建的HID设备的属性

        根据前面的分析,我们可以知道IOBufferMemoryDescriptor在初始化时的大小是maxReportLength,大小如下,是根据后面三个Size取最大值。

maxReportLength = max(_maxOutputReportSize, max(_maxFeatureReportSize, _maxInputReportSize));

        在创建HID设备时,我们传入的PropertyData中包含了相关的字段,我创建HID设备时参照的是CVE-2014-4487使用的PropertyData,通过修改下图中的对应大小,实现控制maxReportLength。具体每个字段的含义见IOHIDDevice::parseReportDescriptor,或者通过http://eleccelerator.com/usbdescreqparser/进行解析。通过下面的propertydata_5774中的0x3e0,就可以使得maxReportLength为128。

 

pasted-image-17

 

(2)获取postReportResult时使用的tokenObj

        在调用postReportResult时,需要传入正确的tokenObj,才能访问IOBufferMemoryDescriptor。

IOReturn IOHIDResourceDeviceUserClient::postReportResult(IOExternalMethodArguments * arguments)
{
    //tokenObj参数是我们通过ScalarInput作为参数传入
    OSObject * tokenObj = (OSObject*)arguments->scalarInput[kIOHIDResourceUserClientResponseIndexToken];

    //当tokenObj在_pending中时在访问report descriptor
    if ( tokenObj && _pending->containsObject(tokenObj) ) {
        OSData * data = OSDynamicCast(OSData, tokenObj);
        if ( data ) {
            __ReportResult * pResult = (__ReportResult*)data->getBytesNoCopy();
         …
        }       
    }
    return kIOReturnSuccess;
}

        因此我们需要得到tokenObj,那么是否可以从用户空间获取?根据源码进行分析,调用updateElementValues时,会最终调用IOHIDResourceDeviceUserClient::getReportGated

//----------------------------------------------------------------------------------------------
// IOHIDResourceDeviceUserClient::getReport
//----------------------------------------------------------------------------------------------
IOReturn IOHIDResourceDeviceUserClient::getReportGated(ReportGatedArguments * arguments)
{
    …
    result.descriptor = arguments->report;  //report descriptor
    
    retData = OSData::withBytesNoCopy(&result, sizeof(__ReportResult));
    require_action(retData, exit, ret=kIOReturnNoMemory);
    
    header.direction   = kIOHIDResourceReportDirectionIn;
    header.type        = arguments->reportType;
    header.reportID    = arguments->options&0xff;
    header.length      = (uint32_t)arguments->report->getLength();
    header.token       = (intptr_t)retData;   // retData就是tokenObj

    _pending->setObject(retData);  //将tokenObj放入_pending
    
    //将header放入_queue队列中,即_queue队列中包含tokenObj的值
    require_action(_queue && _queue->enqueueReport(&header), exit, ret=kIOReturnNoMemory);
    …

exit:
    if ( retData ) {
        _pending->removeObject(retData);
        _commandGate->commandWakeup(&_pending);
        retData->release();
    }
    return ret;
}

        _queue又该如何访问呢?在源码中查找后,发现IOHIDResourceDeviceUserClient::clientMemoryForTypeGated中,_queue进行了初始化,并且可以被映射到用户空间,可以直接从映射内存中得到tokenObj!

//----------------------------------------------------------------------------------------------
// IOHIDResourceDeviceUserClient::clientMemoryForTypeGated
//----------------------------------------------------------------------------------------------
IOReturn IOHIDResourceDeviceUserClient::clientMemoryForTypeGated(IOOptionBits * options, IOMemoryDescriptor ** memory )
{
    IOReturn ret;
    IOMemoryDescriptor * memoryToShare = NULL;
    
    require_action(!isInactive(), exit, ret=kIOReturnOffline);
    
    if ( !_queue ) {
        _queue = IOHIDResourceQueue::withCapacity(kHIDQueueSize); //初始化_queue
    }
    
    require_action(_queue, exit, ret = kIOReturnNoMemory);
    
    memoryToShare = _queue->getMemoryDescriptor();
    require_action(memoryToShare, exit, ret = kIOReturnNoMemory);

    memoryToShare->retain();

    ret = kIOReturnSuccess;

exit:
    // set the result
    *options = 0;
    *memory  = memoryToShare; // _queue可以通过IOConnectMapMemory映射到用户空间!

    return ret;
}

 

pasted-image-20

 

        上图是通过IOConnectMapMemory得到的_queue队列的数据,可以看到两次postReportResult所使用的tokenObj都在其中。

 

        在ipad mini 1,iOS 8.1.2上测试POC。POC为的是验证5774这个漏洞,因此堆风水以及内核信息泄露借助的是8.1.2上的老漏洞mach_port_kobject,崩溃后寄存器的状态如下,覆盖溢出的堆块后的对象虚表,使虚表指针r1可控。基础的POC完成。

 

pasted-image-26

 

pasted-image-23

 

 

四、官方修复

 


        根据xnu的源码,看一下官方是如何修复这个漏洞的。

// RY: HIGHLY UNLIKELY > 4K
if ( pResult->descriptor && arguments->structureInput ) {
    pResult->descriptor->writeBytes(0, arguments->structureInput, arguments->structureInputSize);
    
    // 12978252:  If we get an IOBMD passed in, set the length to be the # of bytes that were transferred
    IOBufferMemoryDescriptor * buffer = OSDynamicCast(IOBufferMemoryDescriptor, pResult->descriptor);
    if (buffer)
        buffer->setLength(MIN((vm_size_t)arguments->structureInputSize, buffer->getCapacity()));
    
}

        可以看到,在setLength之前,会将structure input size与capacity进行比较,这样就避免了堆溢出的发生。

 

References



1. 盘古博客:http://blog.pangu.io/cve-2015-5774/
2. IOHIDFamily源码:http://www.opensource.apple.com/source/IOHIDFamily/IOHIDFamily-606.10.8/