1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 |
Source: https://code.google.com/p/google-security-research/issues/detail?id=542 The IOHIDLibUserClient allows us to create and manage IOHIDEventQueues corresponding to available HID devices. Here is the ::start method, which can be reached via the IOHIDLibUserClient::_startQueue external method: ************ SNIP ************** void IOHIDEventQueue::start() { if ( _lock ) IOLockLock(_lock); if ( _state & kHIDQueueStarted ) goto START_END; if ( _currentEntrySize != _maxEntrySize ) <--- (a) { mach_port_t port = notifyMsg ? ((mach_msg_header_t *)notifyMsg)->msgh_remote_port : MACH_PORT_NULL; // Free the existing queue data if (dataQueue) { <-- (b) IOFreeAligned(dataQueue, round_page_32(getQueueSize() + DATA_QUEUE_MEMORY_HEADER_SIZE)); } if (_descriptor) { _descriptor->release(); _descriptor = 0; } // init the queue again.This will allocate the appropriate data. if ( !initWithEntries(_numEntries, _maxEntrySize) ) {(c) <---- goto START_END; } _currentEntrySize = _maxEntrySize; // RY: since we are initing the queue, we should reset the port as well if ( port ) setNotificationPort(port); } else if ( dataQueue ) { dataQueue->head = 0; dataQueue->tail = 0; } _state |= kHIDQueueStarted; START_END: if ( _lock ) IOLockUnlock(_lock); } ************ SNIP ************** If _currentEntrySize is not equal to _maxEntrySize then the start method will attempt to reallocate a better-sized queue; if dataQueue (a member of IODataQueue) is non-zero its free'd then initWithEntries is called with the new _maxEntrySize. Note that the error path on failure here jumps straight to the end of the function, so it's up to initWithEntries to clear dataQueue if it fails: ************ SNIP ************** Boolean IOHIDEventQueue::initWithEntries(UInt32 numEntries, UInt32 entrySize) { UInt32 size = numEntries*entrySize; if ( size < MIN_HID_QUEUE_CAPACITY ) size = MIN_HID_QUEUE_CAPACITY; return super::initWithCapacity(size); } ************ SNIP ************** There's a possible overflow here; but there will be *many* possible overflows coming up and we need to overflow at the right one... This calls through to IOSharedDataQueue::initWithCapacity ************ SNIP ************** Boolean IOSharedDataQueue::initWithCapacity(UInt32 size) { IODataQueueAppendix * appendix; vm_size_t allocSize; if (!super::init()) { return false; } _reserved = (ExpansionData *)IOMalloc(sizeof(struct ExpansionData)); if (!_reserved) { return false; } if (size > UINT32_MAX - DATA_QUEUE_MEMORY_HEADER_SIZE - DATA_QUEUE_MEMORY_APPENDIX_SIZE) { return false; } allocSize = round_page(size + DATA_QUEUE_MEMORY_HEADER_SIZE + DATA_QUEUE_MEMORY_APPENDIX_SIZE); if (allocSize < size) { return false; } dataQueue = (IODataQueueMemory *)IOMallocAligned(allocSize, PAGE_SIZE); ************ SNIP ************** We need this function to fail on any of the first four conditions; if we reach the IOMallocAligned call then dataQueue will either be set to a valid allocation (which is uninteresting) or set to NULL (also uninteresting.) We probably can't fail the ::init() call nor the small IOMalloc. There are then two integer overflow checks; the first will only fail if size (a UInt32 is greater than 0xfffffff4), and the second will be impossible to trigger on 64-bit since round_pages will be checking for 64-bit overflow, and we want a cross-platform exploit! Therefore, we have to reach the call to initWithCapacity with a size >= 0xfffffff4 (ie 12 possible values?) Where do _maxEntrySize and _currentEntrySize come from? When the queue is created they are both set to 0x20, and we can partially control _maxEntrySize by adding an new HIDElement to the queue. _numEntries is a completely controlled dword. So in order to reach the exploitable conditions we need to: 1) create a queue, specifying a value for _numEntries. This will allocate a queue (via initWithCapacity) of _numEntries*0x20; this allocation must succeed. 2) add an element to that queue with a *larger* size, such that _maxEntrySize is increased to NEW_MAX_SIZE. 3) stop the queue. 4) start the queue; at which point we will call IOHIDEventQueue::start. since _maxEntrySize is now larger this will free dataQueue then call initWithEntries(_num_entries, NEW_MAX_SIZE). This has to fail in exactly the manner described above such that dataQueue is a dangling pointer. 5) start the queue again, since _maxEntrySize is still != _currentEntrySize, this will call free dataQueue again! The really tricky part here is coming up with the values for _numEntries and NEW_MAX_SIZE; the constraints are: _numEntries is a dword (_numEntries*0x20)%2^32 must be an allocatable size (ideally <0x10000000) (_numEntries*NEW_MAX_SIZE)%2^32 must be >= 0xfffffff4 presumable NEW_MAX_SIZE is also reasonably limited by the HID descriptor parsing code, but I didn't look. This really doesn't give you much leaway, but it is quite satisfiable :) In this case I've chosen to create a "fake" hid device so that I can completely control NEW_MAX_SIZE, thus the PoC requires root (as did the TAIG jailbreak which also messed with report descriptors.) However, this isn't actually a requirement to hit the bug; you'd just need to look through every single HID report descriptor on your system to find one with a suitable report size. In this case, _numEntries of 0x3851eb85 leads to an initial queue size of (0x3851eb85*0x20)%2^32 = 0xa3d70a0 which is easily allocatable, and NEW_MAX_SIZE = 0x64 leads to: (0x3851eb85*0x64)%2^32 = 0xfffffff4 To run the PoC: 1) unzip and build the fake_hid code and run 'test -k' as root; this will create an IOHIDUserDevice whose cookie=2 IOHIDElementPrivate report size is 0x64. 2) build and run this file as a regular user. 3) see double free crash. There's actually nothing limiting this to a double free, you could go on indefinitely free'ing the same pointer. As I said before, this bug doesn't actually require root but it's just *much* easier to repro with it! Testing on: MacBookAir5,2 10.10.5 14F27 Guessing that this affects iOS too but haven't tested. Proof of Concept: https://gitlab.com/exploit-database/exploitdb-bin-sploits/-/raw/main/bin-sploits/39379.zip |