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

前情提要

CVE-2021-1748:從客戶端 XSS 到彈計算器

CVE-2021-1864:訪問控制的蝴蝶效應

在前文中介紹了天府杯 2020 上遠程攻破 iPhone 11 的一套漏洞。首先由一個客戶端的 XSS 繞過瀏覽器沙箱并打開額外的攻擊面,接下來一個訪問控制的邏輯漏洞來讓 js 直接可以調用對象的 dealloc 方法,造成 Use-After-Free。

這是本系列的最終篇,介紹如何在最高 iOS 14.3 和 A14 芯片上利用以上漏洞,完全繞過用戶態 PAC 和 APRR 執行任意 shellcode。


SeLector-Oriented Programming

在前文中已經構造了 fakeobj 原語,讓 objc_msgSend 將一塊完全可控的內存當作 Objective-C 對象處理。

在 A12 以前,可以通過構造一些 runtime 的結構體(注:近期版本的 iOS 針對偽造 isa 做了加固)來控制 pc 寄存器,然后通過 return-oriented programming 執行任意代碼。PAC 引入后,通過 objc_msgSend 控制 pc 寄存器就不可行,更不要說 ROP。

但是在 iOS 14.4 和之前的系統上有一個已知的弱點。

Objective-C 的對象結構中第一個指針為 isa,之后才是對象的私有屬性等成員。由于 Objective-C runtime 本身用到了 isa 指針的高位存儲信息,這就和 PAC 的實現有了沖突——PAC 使用密碼學算法給指針添加的校驗簽名也保存到高位上。

所以在 iOS 14 之前,isa 指針沒有簽名保護。PAC 確保不能偽造 selector 對應的 IMP 函數指針,但可以通過指向已有的 objc_class 結構來調用合法的 selector 方法。結合特殊的 gadget,Project Zero 在 iMessage 的 0click 攻擊演示中使用一種被稱之為 SeLector-Oriented Programming 的技術來繞過 PAC,執行一連串 NSInvocation 調用任意 Objective-C 方法。

經過對 ABI 的調整(例如減少引用計數存儲用的長度),在 iOS 14 beta 上,運行時已經開始對 isa 指針做簽名。不過當時加上簽名之后并沒有開始校驗,而是直接用 xpacd 指令移除簽名位。

直到 2021 年發布的 iOS 14.5,終于上線了強制校驗 isa 的 PAC 的運行時。關于這個改動,請參考之前發布的文章 iOS 14.5 如何用 PAC 保護 Objective-C 對象

不過僥幸的是,在天府杯對應的 iOS 14.2,SLOP 還可以用。大膽猜測,新防御的迅速轉正應該也是比賽被打起了一定的催化作用。

SLOP 的核心思路如下:

  1. 找到一個符合條件的類。這個類需要在 dealloc 方法中解引用 self 指針,將固定偏移上的成員當作 _UIAsyncInvocation,調用其 invoke 方法
  2. 這里隱藏了一個類型混淆,就是把 _UIAsyncInvocation 當作 NSInvocation。由于 Objective-C 是根據具體的 isa 指針和 selector 來確定具體調用的方法,這種 NSObject 之前的類型混淆只要存在對應的 selector,就不會產生異常,而是會順利調用查找到的方法
  3. 于是對象被當作 NSInvocation 處理,調用其 invoke 方法
  4. NSInvocation 具有調用任意運行時方法的能力,這一步作為自舉 gadget,來調用一系列 NSInvocation
  5. 一串的 NSInvocation 保存在 NSArray(數組)對象中。而 NSArray 正好有一個 makeOobjectsPerformSelector: 方法
  6. 給 makeObjectsPerformSelector: 傳入 @selector(invoke) 作為參數,就會按順序遍歷數組內所有的 NSInvocation 并執行

在這次的目標 iTunes Store 里正好有一個 SKStoreReviewViewController 滿足要求。但在正式開啟 SLOP 執行代碼之前,我們還需要準備很多工作。


內存任意讀

雖然在 iOS 14.2 上 PAC 還沒有加到 isa 指針,但在 Project Zero 的 iMessage 利用演示之后加入了一個新的防御措施。Apple 意識到 NSInvocation 過于強大,需要針對內存偽造對象的檢查。

于是 NSInvocation 引入了一個 32 位的隨機數。隨機數在單個進程內全局共享,每次啟動進程時初始化。隨機數保存的地址有一個符號 _magic_cookie.oValue。當 NSInvocation 被調用時,runtime 就會檢查 NSInvocation 的 cookie 是否和全局變量相等。

這樣一來,如果不能讀內存,單純用 fakeobj,是不能通過檢查的。

接下來看看如何讀內存。

WebScripting 將 js 的調用映射到 Objective-C 的函數調用。有一個隱藏的轉換就是,js 里的 toString 函數,最終會調用對象的 description 方法,將得到的 NSString 轉換回 js 的字符串返回。

NSData 正好有一個 hex dump 的特性。調用 -[NSData description] 會將其內存內容的十六進制打印出來。如果數據長度超過 24,則會用省略號截斷內容:

{length=4096, bytes=0x23230a23 1025ff00 7224bfbf … 6e2f4142 5c732510}

那么可以構造如下結構:

通過 ASLR 漏洞獲得 NSConcreteData 的 isa 地址。接下來的字段是 buffer 的長度和指針,可以獲得任意內存讀。最后的 callback 成員必須用 0 填充,否則將會被當作一個函數指針,并將 NSData 標記為 freeWhenDone。這樣可能造成不必要的崩潰。

這個函數可以復用,穩定性取決于 UAF 搶占內存的概率。雖然一次限制了 24 個字節,實際上可以轉儲整個內存空間的內容。


使用 ArrayBuffer 偽造對象

在前文中讓 objc_msgSend 在可控的內存上做了消息發送,實現偽造任意 Objective-C 對象并調用其部分方法的效果。但這時候 fakeobj 用的內存分配原語來自前文的一個解碼 data URI 的業務邏輯,獲得的 NSData 對于 js 是只讀的。

這意味著在需要重復使用對象解引用時,需要觸發多次 Use-After-Free。如果能將內存指向一個 js 的 ArrayBuffer,并能在利用當中動態修改就好了。

要達到這個效果,有兩種思路:用 heap spray 讓 ArrayBuffer 到達一個硬編碼的地址,或者使用內存任意讀直接獲取 ArrayBuffer 的 backing store。Heap Spray 是一個不太優雅的方案,穩定性相對后者低很多,不過比賽的時候用到了。

兩個方案都用到了一種思路,就是 Objective-C 當中存在一類"容器"(或者"集合")對象,例如 NSSet, NSArray 等。這類對象的特點是嵌套結構,在容器中會保存一個或多個元素的地址。這樣就造成了在 fakeobj 上的二次地址解引用,可以再次定向到我們需要的地方。而且 Objective-C 的 dealloc 方法一般有遞歸調用的特點,即集合被釋放時會依次嘗試釋放子元素。即使包上一層容器,也不影響代碼執行等 gadget 的使用。

首先是最簡單暴力的堆噴。

iPhone 物理內存很小,堆噴還是很有效的。用 js 創建多個 ArrayBuffer,確保其中一個能落到固定的地址上。在 UAF 創建的假對象上偽造一個 NSArray,只有一個元素,元素的地址指向固定的堆噴目標地址。

為了區分具體落到目標的 ArrayBuffer,在 ArrayBuffer 里再創建一個嵌套的 NSArray,元素為 NSNumber,用來標記序號。

當調用最外部的對象的 toString 方法時,就會返回類似如下的字符串:

@[[@1234]]

這個 NSNumber 可以繼續用 isa 和內存結構偽造,不過在比賽時直接偽造了 tagged pointer 用來加速內存噴射。有 iOS 開發背景的開發者一般知道,為了節省內存,一部分數字、字符串、日期等對象可以通過指針本身的 bits 保存。

在當前版本的 iOS 上,tagged pointer 被混淆處理。例如之前的

0xb000000000000012,混淆之后看上去完全變成了隨機數

0x93b027f3768c6a51。

這也是 iOS 為了防止遠程攻擊引入的防御。在每個進程初始化時,生成一個隨機數保存到全局變量 objc_debug_taggedpointer_obfuscator。之后的 tagged pointer 會和這個隨機數 xor 處理。在缺乏信息泄露漏洞時,就很難偽造 tagged pointer。

不過還記得之前的 addrof 原語嗎?這個原語可以直接泄露出 js 數字對應的 NSNumber 的地址,也就是一個 tagged pointer。已知 xor 的算法和一對(數字,混淆值)之后,就可以算出任意數字的 tagged pointer:

const tagf64 = (() => {  const mask = 0x800000000000002Bn;  const float64_obfuscator = ((1n << 7n) | mask) ^ addrof(1);  const objc_debug_taggedpointer_obfuscator = float64_obfuscator & (!(7n));  iTunes.log('tagged pointer obfuscator: ' + objc_debug_taggedpointer_obfuscator);  return n => ((BigInt(n) << 7) | mask) ^ float64_obfuscator;})();

使用 tagged pointer 偽造對象可以減少一半以上的內存寫入操作。而 addrof 原語會拋出一次異常,并產生 syslog 輸出。如果直接用 addrof 生成所有的數字 id,程序從毫秒級拖慢到幾秒,性能差距在千倍以上。通過這個偽造 tagged pointer 的算法,極大地提升了堆噴的效率。

不過比賽結束之后我又鼓搗了不用堆噴的方案,穩定性極大提升了,也不需要偽造 tagged pointer。

這一個思路其實更簡單。

WebScripting 會為 js 運行時里的對象創建一個 WebScriptObject。當用 addrof 原語獲取一個 ArrayBuffer 的地址時得到的就是這個對象的地址。在WebScriptObject 里有一個 jsObject 指針,指向 JavaScriptCore 里的對象結構。

ArrayBuffer 的 jsObject 指向 jsc 的 Int8Array,也就是保存 ArrayBuffer 內容的地方。在新版的 WebKit 里 VectorPtr 指針被 PAC 加上了保護,用來防止偽造指針來獲取內存讀寫的利用技術。這便是 PAC-cage。

我們的目的是用 ArrayBuffer 的二進制數據被 Objective-C 當作對象來處理,不會修改 VectorPtr。而 Objective-C 也不管 jsc 里的 PAC 簽名。因此只需要用邏輯 and 運算簡單地移除掉高位即可得到一個 fakeobj 的指針。把這個指針作為 UAF 的之后偽造的 NSArray 的唯一元素即可。


構造 Double Free

SLOP 技術有一個關鍵 gadget 就是調用 dealloc 方法。Project Zero 在實現 iMessage 攻擊的時候利用了一個條件,即數據序列化之后創建出來的對象會隱式調用 dealloc 方法。

在前面一系列步驟之后,我們偽造了一個SKStoreReviewViewController 對象,并在 0x358 偏移處放了一個 bootstrap 用的 NSInvocation。既然第二篇文章說到,Use-After-Free 的核心問題在于 dealloc 可以被 js 調用,那么是不是再調用一次 dealloc 就行了?

并沒有這么簡單。

之所以 dealloc 能被 js 訪問,是 SUScriptObject 在實現 isSelectorExcludedFromWebScript: 方法時返回了 NO。不過 SKStoreReviewViewController 并不是 SUScriptObject 的子類,最后會執行到 +[NSObject isSelectorExcludedFromWebScript:],而默認的實現是一律 YES 拒絕。

要讓偽造的對象再調用一次 dealloc,我們需要找到一個滿足如下條件的類:

  • SUScriptObject 的子類
  • 提供一個 setter 方法,可以將其他對象賦值到這個類的屬性上
  • 在 dealloc 時遞歸釋放成員屬性

SUScriptSegmentedControlItem 便滿足需求。

首先用 js 分配該對象到變量 A:

iTunes.makeSegmentedControl().createSegment()

然后調用 setUserInfo_ 將對象 B 關聯上去。調用對象 B 的 dealloc 釋放后占位,準備好 SLOP 所需的數據結構。這時候調用 A 的 dealloc,就會遞歸調用到 B 的 dealloc 方法。

const deallocator = iTunes.makeSegmentedControl();const seg = deallocator.createSegment(); // for double freeiTunes.log(`dangling pointer: ${addrof(x)}`);window.x = x; // avoid GCseg.setUserInfo_(x);x.dealloc(); // first free// ... exploit the UAFseg.dealloc(); // double free to kickstart the chain


調用任意 C 函數

到這一步已經可以串聯多個 Objective-C 方法,實現相當多功能了。

Project Zero 的 iMessage 攻擊演示用到了兩個特殊的 gadget,可以配合起來調用任意被導出的 C 函數。

-[CNFileServices dlsym::]

-[NSInvocation invokeUsingIMP:]

第一個正如它的名字,等價于 dlsym。在 PAC 環境下,dlsym 返回的函數指針使用進程內共享的 IA key 和 0 作為上下文來簽名,通常用作將函數指針當作 callback 的場景。

而另一個則是 NSInvocation 的私有用法,可以自定義 IMP 指針,也就是函數指針,實現參數可控的函數調用。這個方法利用到的 IMP 正是使用 IA key 和 0 context 簽名。

兩個方法結合起來,就可以調用任意能被 dlsym 找到的函數。

但是 saelo 留下了一個問題沒有解決。NSInvocation 要求函數的第一個參數,也就是 self 指針不能為 nil(0)。在接下來的漏洞利用中,我們需要以 0 為參數調用一些函數。

這個缺陷可以用 CoreFoundation 里的回調函數解決。

例如這個:

void CFSetApplyFunction(CFSetRef theSet, CFSetApplierFunction applier, void *context);

第二個參數的 applier 是一個 IA key 和 0 context 簽名過的函數指針。CFSetApplyFunction 將遍歷 CFSet 所有的元素,將元素和 context 作為參數傳給 applier。

這樣便可以通過如下代碼(對應的 SLOP 鏈)繞過限制:

void *fake[2] = {(__bridge void *)NSClassFromString(@"__NSSingleObjectSetI"), NULL};CFSetApplyFunction((void *)&fake[0], (void*)exit, (void*)0x41414141);

首先創建一個只有一個元素的 __NSSingleObjectSetI,元素指針就是 0(applier 的第一個參數)。接下來調用 CFSetApplyFunction,加上 void *context 一共可以完全控制兩個參數。

另外我沒有實測過一個想法,由于 arm64 使用通用寄存器傳遞參數,如果上層的 CFSetApplyFunction 沒有污染后續的寄存器,那么有可能還可以控制更多的參數。

反正之后的利用鏈條兩個參數夠用了。


繞過 APRR 載入任意 shellcode

JavaScriptCore 用到了 APRR 來動態切換 JIT 代碼頁的權限。對于一個線程而言,頁面的屬性要么可寫要么可執行;而不同線程(編譯線程、執行線程)看到的內存頁屬性可以是不一樣的。

雖然瀏覽器的 just-in-time 允許加載動態的機器碼,漏洞利用程序在獲得內存讀寫之后卻沒辦法簡單將代碼寫入。寫入代碼必須調用特殊的指令切換內存屬性,復制代碼之后再恢復權限為可執行。而 PAC 又將代碼重用攻擊(ROP)阻斷了,使攻擊者不能簡單復用瀏覽器已有的指令。

在 APRR 和 PAC 的協作下,很難單純用內存讀寫載入任意代碼。而繞過技術更是見光死,修一個少一個。不得不說 Apple 使用私有的硬件極大提高了漏洞利用的門檻。

當然凡事無絕對,以下介紹一個已被修復的繞過。

具體來說,WebKit 在生成即時編譯的機器碼會用到一個總是內聯的 performJITMemcpy 函數。這個函數內部流程大致如下:

pthread_jit_write_protect_np(0); // set writablememcpy(jit_function, code, size); // write shellcodepthread_jit_write_protect_np(1); // set executable

pthread_jit_write_protect_np 本該被展開成為 內聯匯編指令。但是 iOS 14.3 之前犯了一個低級錯誤,在 libpthread 里把這個函數設置成了公開導出。這樣結合前文的 SLOP 技術,我們直接構造以上三個函數調用,就可以輕而易舉地繞過 APRR,寫入任意的機器碼。


又一個 PAC 繞過:CVE-2021-1769

這一步在漏洞利用中不是必要的,有些炫技的成分。

前面的步驟中實現了任意 shellcode 的寫入,還差最后一步 pc control,把控制流重定向到 shellcode 上。

可以使用經典的做法,利用 JIT 生成一個足夠大的函數體,用上面的 APRR 繞過覆蓋函數的機器碼。最后在 js 里調用該函數時就會執行被修改過的 shellcode。

在這里使用了另一種思路。可以看到 SLOP 本身就可以實現復雜的功能,即使不寫 shellcode 也能完成很多效果。我的想法是找到一些未被 PAC 保護的間接跳轉指令,由某個函數從已知的內存地址中讀取一個指針,然后不加驗證地跳轉過去。只要使用 SLOP 將指針修改掉,再調用這個具有間接跳轉的函數,就可以控制 PC 運行 shellcode。

通過 IDA Pro 對整個 dyld_shared_cache 搜索,找到了這個 Swift 的運行庫函數:

其中第二條 ldr 指令讀取的就是一個 GOT 表項目,也就是沒有 PAC 保護的函數指針。接下來函數走到 jn(::),再繼續看匯編指令:

最后一行 br x1 即是跳轉到函數的第二個參數,也就是前文 ldr 讀取出來的指針。

以上函數位于 /usr/lib/swift/libswiftDarwin.dylib,默認沒有被 iTunes Store 鏈接到,可以用 SLOP 調用 dlopen 加載之。接著修改 jn_ptr@PAGE 的值為shellcode 的地址,然后直接調用 dlsym 出來的 $s6Darwin2jnySdSi_SdtF 函數即可。

這個 PAC 繞過在 iOS 14.4 修復,通過升級編譯工具鏈移除了所有無保護的 GOT 指針。


最終 shellcode 執行結果如下:

第一篇文章里提到了,這個 App 非常特殊,可能是目前 shellcode 能在 iOS 上獲取的最高權限。所有基于 App 能實現的越獄都可以無縫串聯到這里。iTunes Store 還通過 entitlement 加入了例如攝像頭、通訊錄、Apple Wallet、Apple ID 等敏感信息等訪問權限,直接調用對應的 API 即可。

本研究大量參考了 Samuel Gro? 的 iMessage hack 和 JITSploitation 系列文章,強烈推薦閱讀,也在此表示感謝。可以看到閱讀歷史漏洞利用的報告對展開新的研究具有極大的推動作用。


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