作者:棧長@螞蟻安全實驗室
原文鏈接:https://mp.weixin.qq.com/s/wQbwFYjPzS4mQMAVzc8jOA

在今年的Black Hat Asia上,螞蟻安全實驗室共入選了5個議題和3個工具。本期分享的是螞蟻光年實驗室的議題《Safari中的新攻擊面:僅使用一個Web音頻漏洞來統治Safari》。

螞蟻安全光年實驗室從2020年4月份開始專注到 Apple 產品漏洞挖掘中,僅用了三個季度的時間,就累計拿下蘋果47次致謝——致謝數排名2020年Apple漏洞致謝數全球第一。

47次漏洞致謝中,包含了系統庫、瀏覽器、內核等多個維度層面,幾乎都是高危漏洞,部分漏洞評分達到了“嚴重”級別,挖掘的數量和質量都處于全球領先位置。

2020年各大公司獲得的蘋果致謝次數排名

以往對蘋果Safari瀏覽器的漏洞研究往往聚焦于DOM或者JS引擎,但是像Safari所使用的一些系統庫,例如音頻庫,視頻庫,字體庫等等很少受到關注,鮮有這些模塊的漏洞在Safari中利用成功的案例。

部分原因是由于Safari內置一些緩解措施,導致這些模塊中的漏洞很難單獨利用,故而外界對這些模塊的關注度較低。我們在對Safari的安全機制做了整體分析后判斷,這些系統庫中的洞是完全可以繞過Safari內置的緩解措施,從而控制Safari瀏覽器,攻擊者進而可以在用戶的機器上執行惡意代碼,竊取瀏覽器cookie、歷史記錄、用戶名密碼等敏感信息。

我們在20年4月份左右開始投入到對這些系統庫的漏洞挖掘當中,采用的是專家經驗和Fuzz相結合的方式。光年實驗室自研了AntFuzz引擎,該引擎是用rust語言編寫,穩定性和性能與同類工具相比都有顯著提升。

AntFuzz對當今主流的Fuzz方法體系進行了吸收融合,在易用性和接入能力上面也有很大的改善。在安全研究員篩選出一些可能的攻擊面的基礎上,AntFuzz會針對特定攻擊面自動化生成高質量的Fuzz Driver,再通過定制化的種子以及變異算法的選取,來進行高效漏洞挖掘。AntFuzz的這些關鍵特性支持我們取得了非常豐富的戰果,挖掘出了大量高危漏洞。

在2020年天府杯中,光年實驗室是全場唯一實現Safari full-chain exploit的參賽團隊(即從瀏覽器入口到獲取用戶目標機器上的最高權限)。在這個攻擊中,我們僅依托發現的一個WebAudio漏洞就實現了Safari瀏覽器的遠程代碼執行,繞過了Safari的所有安全緩釋措施。

該漏洞CVE編號為CVE-2021-1747,蘋果官方已在最新的macOS系統、iOS系統中修復了該漏洞。這也是國內頂尖軟硬件破解大賽中,首次通過系統庫API來攻破Safari瀏覽器。下面我們會分享相關的漏洞利用技巧。

01 漏洞成因

漏洞存在于WebAudio模塊當中,在解析CAF音頻文件的時候會產生越界寫。漏洞存在于ACOpusDecoder::AppendInputData函數中,(1)處有一個類似于邊界檢查的代碼,但是最終被繞過了,(2)處調用memcpy函數,造成了越界寫。

__int64 __fastcall ACOpusDecoder::AppendInputData(ACOpusDecoder *this, const void *a2, unsigned int *a3, unsigned int *a4, const 

AudioStreamPacketDescription *a5)

{

  ...



  if ( a5 )

  {

    v8 = a5->mDataByteSize;

    if ( !a5->mDataByteSize || !*a4 || (v9 = a5->mStartOffset, (a5->mStartOffset + v8) > *a3) || this->buf_size ) // (1). 繞過這里的邊界檢查

    {

      result = 0LL;

      if ( !v8 )

      {

        this->buf_size = 0;

LABEL_19:

        v13 = 1;

        v12 = 1;

        goto LABEL_20;

      }

      goto LABEL_16;

    }

    if ( v9 >= 0 )

    {

      memcpy(this->buf, a2 + v9, v8);   //(2). 越界寫發生的位置

      v14 = a5->mDataByteSize;

      this->buf_size = v14;

      result = (LODWORD(a5->mStartOffset) + v14);

      goto LABEL_19;

    }

    ...

}

先簡單介紹一下CAF文件格式,我這里畫了一幅簡化版的CAF文件格式圖。CAF文件開頭是File Header,之后是由各種不同類型的Chunk組成,每個Chunk都有一個Chunk Header,記錄了該Chunk的大小。

Desc Chunk主要存儲了文件的一些元數據,Data Chunk里面存儲了所有的Packet,Packet Table Chunk則記錄了每一個Packet的size。在解析的時候會先讀取Packet Table Chunk,獲取每一個Packet的大小,然后再去Data Chunk里面讀取。

為了分析這個漏洞,我特意編寫了一個010 Editor模板來對CAF文件進行解析。

然后我們分析一下造成crash的CAF文件,用010 editor的模板文件跑一下,可以看到如下輸出:

第一列是packet的序號,第二列是packet的size。可以看到,第114個packet的size是負數,可以推測程序在處理size為負的packet的時候出了問題。接下來就是如何利用這個漏洞了。

02 將越界寫漏洞轉化為任意地址寫

這里我首先對相關代碼做了逆向分析,被越界寫的buffer是存在于ACOpusDecoder這個結構體的內部,這個結構體的字段如下所示:

被越界寫的是buf字段,共有1500個字節,后面的buf_sizecontroled_field, log_obj, controled 這幾個字段都是我們可以控制的。通過一定的調試加逆向,可以發現log這個對象在后面有用到,而且可以造成任意地址寫。

接下來我們的目標有兩個,一是走到任意地址寫的位置,并且寫的值要滿足一定的條件;二是在造成任意地址寫之后程序不會立馬崩潰。第一步的話我們通過控制一些變量的值就可以做到。

第二步發生了點波折。任意地址寫之后,會發現程序總會在opus_decode_frame中崩潰,按照常規的思路分析,如果造成了任意地址寫就會導致崩潰,如果不崩潰,又沒法造成任意地址寫。但是我在逆向的過程中發現,opus_packet_parse_impl這個函數在解析packet的時候沒有判斷packet的長度,會越界解析到packet+4的位置。所以我構造了兩個互相重疊的packet。

Packet 1是兩個字節, 在解析的時候會越界解析到Packet 2中,把Packet 2中的0xf8當成是Packet 1中的TOC字段,最后繞過opus_decode_frame中會導致崩潰的邏輯,具體細節不表。

03 堆噴,攻破ASLR!

通常即使有了任意地址寫的能力,如果程序的ASLR防護做的比較好的話,想要利用該漏洞還得找一個信息泄漏。但是Safari的堆的實現上有些問題,導致我們可以通過堆噴的手段在某個固定的地址噴上我們控制的值。

有了任意地址寫,首先想到的就是覆蓋JSArray中的length字段,或者是ArrayBuffer中的length字段,ArrayBuffer由于Safari的Gigacage機制,即使覆蓋了length字段也無法越界讀寫到有用的內容,所以我最后選擇了JSArray。

Safari中JSArray使用了Butterfly來存儲JSArray的長度以及內容,如果覆蓋掉其中一個JSArray的長度,那么就可以越界讀寫到下一個JSArray的內容,就可以構造fakeobj以及addrof兩個原語,用于后續的漏洞利用。

我先嘗試噴了2個G的內存,發現我的Butterfly有時噴射在 0x800000000 - 0x1000000000 之間,有時噴射在 0x1800000000 - 0x1c00000000 之間。Safari由于堆隔離機制,不同類型的對象在不同的堆,Butterfly是在Safari中一個叫做Gigacage的堆里面的,對Gigacage堆做了一些研究發現,Gigacage的基地址是可以預測的,Gigacage的類型有兩種,一種可以存儲Bufferfly,一種可以存儲ArrayBuffer

對于這兩種類型的堆,Gigacage做了一個小小的隨機化,一種情況是Bufferfly在上面,另一種情況是ArrayBuffer在上面。如下圖所示。情況一下,從0x800000000開始,會隨機生成一塊0-4G的未映射的區域,之后就是Bufferfly的堆了。第二種情況是從0x1800000000開始,會隨機生成一塊0-4G的未映射的區域,之后就是Bufferfly的堆。無論是哪種情況,基地址的隨機化程度都很小。

我一開始測試的時候是在16G內存的機器上,為了提高成功率,噴了4個G,但是后來發現Safari對每個render進程占用的內存有監控,如果內存過大,會把他殺掉。所以最后我選擇噴2.5個G,但是這會導致成功率有一定程度的下降。解決方法是多次觸發任意地址寫來提高成功率。

04 感謝多線程!在程序崩潰前讓利用代碼有足夠的時間執行

下面這張時序圖解釋了整個漏洞利用過程,剛開始只有一個JS線程,我們先堆噴,并且在內存中構造音頻文件,隨后調用decodeAudioData函數,由于Safari是在單獨的線程里解碼音頻的,所以這里會啟動Audio A線程,我們先假設堆噴后的內存布局是上面的情況1,那么Audio A線程在解碼音頻文件的時候就會往0x80開頭的地方寫數據,JS線程在2s之后檢測JSArray的length是否被改掉,如果被改掉,說明堆布局確實是情況1,接著就可以執行后續的exploit代碼了,如果沒有被改掉,說明堆布局是情況2,那么第二次調用decodeAudioData()函數,啟動Audio B線程解碼音頻,這次是往0x180開頭的地址寫數據。JS線程循環檢查JSArray的length是否被改掉,如果成功,則調用執行后續的exploit,如果失敗,說明整個利用失敗。

此外還有一個問題需要解決,就是音頻文件解碼完之后,調用free函數對資源進行清理的時候,會觸發崩潰。有幾種方式可以解決這一問題,一種就是對損壞的堆進行修復,第二種就是讓音頻解碼的時間非常非常的長,在解碼結束之前我們的利用過程就結束了。

第一種由于需要對堆進行搜索,過于復雜,而且其實你要修復堆,也是需要一定的時間的,并且還是要和第二種手段結合起來,那還不如直接粗暴一點,就選取第二種方法。我構造了一個600M的CAF文件,里面有七千多萬個packet,要全部把這些packet解碼完大概要花費50s左右的時間,完全足夠我的漏洞利用了。

05 Old school,任意地址讀寫原語到任意代碼執行

當覆蓋了JSArray的長度字段后,我們就可以構造fakeobj和addrof原語,然后就可以用這兩個原語構造任意地址讀寫原語,再將shellcode寫入JIT區域就可以任意代碼執行了。這些都是屬于瀏覽器利用的常規套路,對此感興趣的讀者可以閱讀google的saelo寫的文章《Attacking JavaScript Engines》,在這里我們就不細細展開了。


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