CVE-2014-4487

CVE-2014-4487 – IOHIDLibUserClient堆溢出漏洞

漏洞描述

通过查看IOHIDFamily的代码,检查IOHIDLibUserClient的15号处理函数_getElements。该函数的入参是1个uint64,指定要获取的element是哪个队列的。出参是一个struct结构,无固定长度。

    { //    kIOHIDLibUserClientGetElements
    (IOExternalMethodAction) &IOHIDLibUserClient::_getElements,
    1, 0,
    0, kIOUCVariableStructureSize
    },

_getElements在处理的时候,如果出参是structureOutputDescriptor,则会先IOMalloc用户指定长度的缓冲区然后再去填充elements数据。

IOReturn IOHIDLibUserClient::_getElements(IOHIDLibUserClient * target, void * reference __unused, IOExternalMethodArguments * arguments)
{
    if ( arguments->structureOutputDescriptor )
        return target->getElements((uint32_t)arguments->scalarInput[0], arguments->structureOutputDescriptor, &(arguments->structureOutputDescriptorSize));
    else
        return target->getElements((uint32_t)arguments->scalarInput[0], arguments->structureOutput, &(arguments->structureOutputSize));
}

IOReturn IOHIDLibUserClient::getElements (uint32_t elementType, void *elementBuffer, uint32_t *elementBufferSize)
{
...
        elementLength = mem->getLength();
        if ( elementLength )
        {
            // 根据用户指定的大小来分配内存
            elementData = IOMalloc( elementLength );

            if ( elementData )
            {
                bzero(elementData, elementLength);

                // 向分配的堆中填充数据,elementLength更新为实际填充的数据长度
                ret = getElements(elementType, elementData, &elementLength);

                if ( elementBufferSize )
                    *elementBufferSize = elementLength;

                mem->writeBytes( 0, elementData, elementLength );

                // 释放刚分配的堆
                IOFree( elementData, elementLength );
...
}


getElements在填充elements的时候根本没有检查输入缓冲区的大小,从而会导致缓冲区溢出。

IOReturn IOHIDLibUserClient::getElements (uint32_t elementType, void *elementBuffer, uint32_t *elementBufferSize)
{
    OSArray *                array;
    uint32_t                i, bi, count;
    IOHIDElementPrivate *    element;
    IOHIDElementStruct *    elementStruct;

    // 仅仅检查了是否有长度
    if (elementBuffer && elementBufferSize && !*elementBufferSize)
        return kIOReturnBadArgument;

    if (!fNub || isInactive())
        return kIOReturnNotAttached;

    // 这里的hierarchElements是可控数据(通过创建IOHIDDevice时指定ReportDescriptor)
    if ( elementType == kHIDElementType )
        array = fNub->_reserved->hierarchElements;
    else
        array = fNub->_reserved->inputInterruptElementArray;

    if ( !array )
        return kIOReturnError;

    if ( elementBuffer )
        bzero(elementBuffer, *elementBufferSize);

    count = array->getCount();
    bi = 0;

    for ( i=0; i<count; i++ )
    {
        element = OSDynamicCast(IOHIDElementPrivate, array->getObject(i));

        if (!element) continue;

        // 填充数据之前根本没有对size进行检查
        // Passing elementBuffer=0 means we are just attempting to get the count;
        elementStruct = elementBuffer ? &(((IOHIDElementStruct *)elementBuffer)[bi]) : 0;

        if ( element->fillElementStruct(elementStruct) )
            bi++;
    }
...
}

利用思路

比较传统的思路是控制堆溢出的数据和长度,来覆盖分配的堆之后的一些内容再进一步转换成内存读写的利用。不过该漏洞的覆盖数据不是非常容易控制,可以换一种利用思路。注意到elementLength在调用getElements后会被改写,并且被传递到IOFree函数用于释放刚刚分配的内存。从而使我们可以分配一个较小的堆,随后让系统释放一个较大的堆,此时我们再通过heap风水分配一个较大的堆时就可以覆盖原来较小的堆之后的堆数据,从而进一步转换成任意内存读写的漏洞。

漏洞可达性

该漏洞的触发还有一些额外的可达性要求:

  • 首先必须要自己创建一个IOHID的设备才能打开对应的IOHIDLibUserClient。而IOHID设备的创建可以通过”IOHIDResource”服务的0号调用函数来完成,注意该调用必须要有Root权限!
  • 此外,前文提到必须要传入structureOutputDescriptor才能走到存在漏洞的代码,而通常调用IOConnectCallMethod方法时对于大于4096长度的缓冲区才会创建memory descriptor。不过通过直接调用更底层的io_connect_method函数可以直接控制。

发表评论

电子邮件地址不会被公开。 必填项已用*标注