iOS 9.2/9.2.1修補的內核漏洞 - PanguTeam
來自: http://blog.pangu.io/race_condition_bug_92/
蘋果在 iOS 9.2 和 iOS 9.2.1 中陸續修補了大量漏洞,其中Google Project Zero團隊的Ian Beer報告了多個內核漏洞,并且在蘋果修補后給出了 漏洞細節 。
條件競爭漏洞
通過查看公告可以發現除了熟知的UAF類型漏洞外(例如Pangu9中使用的即是UAF漏洞),還包含了多個條件競爭類型的漏洞。通過分析漏洞的細節可以發現蘋果在許多情況下都沒有考慮用戶態多線程調用導致的競爭問題,因此不排除在別的模塊也有類似的漏洞(例如在未開源的內核擴展中)。
此次被修補的漏洞包含數個能被實際利用的漏洞,其中有一個漏洞能夠繞過地址隨機等保護機制完全攻破內核(可被用于越獄)。下面會簡單分析兩個漏洞的細節,討論編寫利用的一些思路。
double free in IOHIDEventQueue::start
通過查看 542報告 可以知道漏洞的主要原因在于IOFreeAligned釋放dataQueue后沒有置空。雖然大多數情況下隨后的initWithEntries函數會對dataQueue重新賦值,但是如果initWithEntries失敗的話,dataQueue并不會被賦值。如果再次調用start函數就會導致double free的問題。
void IOHIDEventQueue::start() { ... if (dataQueue) { 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) ) { goto START_END; }</pre>
如何使IOHIDEventQueue::initWithEntries失敗?如果滿足”numEntries > UINT32_MAX / entrySize”即能導致函數失敗。而其中_numEntries是我們在創建IOHIDEventQueue時可以指定的。_maxEntrySize則會在IOHIDEventQueue::addElement函數中根據event的size來修改,也是可控的(通過創建設備時輸入特殊的ReportDescriptor)。如果將_maxEntrySize設置成非常大的值即能導致函數失敗。
Boolean IOHIDEventQueue::initWithEntries(UInt32 numEntries, UInt32 entrySize) { UInt32 size = numEntries*entrySize;if ( numEntries > UINT32_MAX / entrySize ) return false; if ( size < MIN_HID_QUEUE_CAPACITY ) size = MIN_HID_QUEUE_CAPACITY; return super::initWithCapacity(size);
}</pre>
由于實際上dataQueue的分配是頁面對齊的,因此兩次釋放很難直接利用來控制PC。通過進一步分析IOHIDEventQueue::start的調用者IOHIDLibUserClient可以發現該類重寫了clientMemoryForType方法并允許映射queue到用戶態(type設置為_createQueue函數返回的token),從而能夠直接在用戶態下讀寫dataQueue指向的內核內存區域。因此當dataQueue被釋放后需要想辦法用內核對象來占位,但由于需要分配較大的對象(頁面對齊),可以考慮通過OSArray來分配一組內核對象嘗試占位。從而在用戶態可以修改OSArray中的內核對象的地址,從而控制vtable來控制PC執行代碼。
IOReturn IOHIDLibUserClient::clientMemoryForTypeGated( UInt32 token, IOOptionBits options, IOMemoryDescriptor ** memory ) { IOReturn ret = kIOReturnNoMemory; IOMemoryDescriptor memoryToShare = NULL; IOHIDEventQueue *queue = NULL;// if the type is element values, then get that if (token == kIOHIDLibUserClientElementValuesType) { // if we can get an element values ptr if (fValid && fNub && !isInactive()) memoryToShare = fNub->getMemoryWithCurrentElementValues(); } // otherwise, the type is token else if (NULL != (queue = getQueueForToken(token))) { memoryToShare = queue->getMemoryDescriptor(); }
... </pre>
double free in iokit registry iterator
查看 598報告 中提到IORegistryIterator對象由于沒有線程互斥的保護,導致對成員進行操作的時候可能會出錯。例如兩個線程同時調用exitEntry可能會觸發double free。顯然通過條件競爭觸發二次釋放來控制PC的穩定性不高,也沒有辦法繞過內核地址隨機化的保護。
bool IORegistryIterator::exitEntry( void ) { IORegCursor * gone;if( where->iter) { where->iter->release(); where->iter = 0; if( where->current)// && (where != &start)) where->current->release(); } if( where != &start) { gone = where; where = gone->next; IOFree( gone, sizeof(IORegCursor)); return( true); } else return( false);
}</pre>
是否存在其它穩定的條件競爭利用并且能夠泄露內核地址?觀察IORegistryIterator中其它操作成員的函數,enterEntry函數中會分配一個新的where,并將這個where的next指向之前的where(在單向列表的頭部插入一個結點)。而之前分析的exitEntry函數則會嘗試釋放where->iter并釋放where,之后將next指向的結點賦值給新的where(把單向列表的頭部結點移除并釋放)。
void IORegistryIterator::enterEntry( const IORegistryPlane enterPlane ) { IORegCursor prev;prev = where; where = (IORegCursor *) IOMalloc( sizeof(IORegCursor)); assert( where); if( where) { where->iter = 0; where->next = prev; where->current = prev->current; plane = enterPlane; }
}</pre>
在兩個線程中分別調用exitEntry和enterEntry可能導致新創建的where的next指針指向一個已經被釋放了的內存區域。執行序列:
- enterEntry: prev = where | exitEntry: gone = where
- enterEntry: where = IOMalloc(sizeof(IORegCursor)) | exitEntry: IOFree(gone, sizeof(IORegCursor)); // 此時prev指向的where實際已經被釋放
- enterEntry: where->next = prev; // 新創建的where->next指向已經被釋放的內存區域
再次調用exitEntry后where將會指向被釋放的內存區域,而該區域的內容我們可以通過堆風水控制。進一步獲取代碼執行相對容易,”where->iter->release();”這個虛函數調用完全可以控制。內核信息泄露則可以通過占位再釋放后,再用一個同樣的大小的object去占位,然后讀出vtable來獲取內核地址。
值得注意的是這個漏洞可以在iOS的沙盒內觸發,因此在APP內就可以直接攻擊內核,獲取內核代碼執行權限。建議用戶盡快升級到最新版本,并且避免安裝來歷不明的APP。