Related Weaknesses
CWE-ID |
Weakness Name |
Source |
CWE-119 |
Improper Restriction of Operations within the Bounds of a Memory Buffer The product performs operations on a memory buffer, but it reads from or writes to a memory location outside the buffer's intended boundary. This may result in read or write operations on unexpected memory locations that could be linked to other variables, data structures, or internal program data. |
|
Metrics
Metrics |
Score |
Severity |
CVSS Vector |
Source |
V2 |
9.3 |
|
AV:N/AC:M/Au:N/C:C/I:C/A:C |
[email protected] |
EPSS
EPSS is a scoring model that predicts the likelihood of a vulnerability being exploited.
EPSS Score
The EPSS model produces a probability score between 0 and 1 (0 and 100%). The higher the score, the greater the probability that a vulnerability will be exploited.
EPSS Percentile
The percentile is used to rank CVE according to their EPSS score. For example, a CVE in the 95th percentile according to its EPSS score is more likely to be exploited than 95% of other CVE. Thus, the percentile is used to compare the EPSS score of a CVE with that of other CVE.
Exploit information
Exploit Database EDB-ID : 39379
Publication date : 2016-01-27 23h00 +00:00
Author : Google Security Research
EDB Verified : Yes
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
Products Mentioned
Configuraton 0
Apple>>Iphone_os >> Version To (including) 9.1
Configuraton 0
Apple>>Tvos >> Version To (including) 9.0
Configuraton 0
Apple>>Watchos >> Version To (including) 2.0
Configuraton 0
Apple>>Mac_os_x >> Version To (including) 10.11.1
References