Chrome零日漏洞(CVE-2019-5786)分析
1.介紹
3月1日,谷歌釋出 安全公告 , 指出chrome瀏覽器的實現過程FileReader API存在一個use-after-free漏洞。根據谷歌威脅分析小組的 報告 ,該漏洞已在野外利用,針對目標是32位的window7作業系統,漏洞利用主要分為兩部分,一是在Renderer(瀏覽器渲染程序)程序中執行程式碼,二是用於完全接管破壞目標主機系統。本文是一篇技術文章,主要聚焦於該漏洞利用的第一個部分,即如何在Renderer程序中實現程式碼執行,並通過研究分析發現更多的技術資訊。 在本文攥寫時, 谷歌漏洞報告 尚未公開。預設安裝情況,Chrome會自動更新補丁,目前最新版本的Chrome已不受該漏洞影響。請確保您的Chrome處於安全狀態,可通過chrome://version命令查詢,檢視Chrome版本是否已是72.0.3626.121或更高版本。
2. 資訊收集
2.1 漏洞修復
大多數的Chrome程式碼庫都基於Chromium開源專案。由於漏洞程式碼包含在開原始碼中,因此我們可以更直接的檢視更新補丁對FileReader API做了哪些修復動作。此外,谷歌分享了其修復版本的更新日誌為我們的分析工作提供了更大的便利。
我們看到更新檔案中只有一個與FileReader API相關,並帶有以下訊息內容:
該訊息暗示對同一個底層ArrayBuffer多次引用是一件非常糟糕的事情。雖然目前尚不清楚段話意味著什麼,下面的工作將致力於尋找隱藏於該條資訊之下的真實細節。
首先,我們可以比較 GitHub 上新舊兩個版本的差異。,看看其中到底發生了哪些變化。為了便於閱讀,下面展示的是補丁前後兩個版本差異的具體情況。
修復前老版本 :
補丁後新版本 :
這兩個版本在GitHub上都可以找到。從圖中我們可以看出,主要對ArrayBufferResult函式進行了修復。ArrayBufferResult函式在使用者呼叫訪問FileReader.result時用於響應並返回資料。
上圖中,我們可以看到前後版本不同就在於對DOMArrayBuffer物件是如何處理的。在新版本中,增加了對資料尚未載入完成(finished_loading_標誌)情況下對DOMArrayBuffer物件的處理。在老版本中,如果資料尚未載入完成的情況下,ArrayBufferResult函式直接呼叫DOMArrayBuffer::Create(raw_data_->ToArrayBuffer())函式並返回結果,在新版本中,則是呼叫DOMArrayBuffer::Create(ArrayBuffer::Create(raw_data_->Data(),raw_data_->ByteLength()))函式並返回結果。
我們來看補丁後的版本,因為該版本更容易理解。新版本中,DOMArrayBuffer::Create引數中包含了一個ArrayBuffer::Create函式呼叫。該函式包含兩個引數,一個是指向資料的指標型別,一個是資料的長度(該函式在/third_party/blink/renderer/platform/wtf/typed_arrays/array_buffer.h檔案中定義)。
函式定義如下圖:
函式主要建立一個新的ArrayBuffer物件,將其置於scoped_refptr<ArrayBuffer>中並將資料複製到其中。 scoped_refptr 在Chromium專案中用於處理引用計數,也就是跟蹤一個物件被引用了多少次。當建立一個新的scoped_refptr例項,底層目標物件的引用次數會遞增。當該物件退出時,該計數會遞減。當引用數目為0的時候,該物件將被刪除(好玩的是,當引用計數溢位後,Chrome將終止程序)。
老版本程式碼中則沒有呼叫ArrayBuffer::Create,而是呼叫ArrayBufferBuilder::ToArrayBuffer函式並返回值。(該函式在third_party/blink/renderer/platform/wtf/typed_arrays/array_buffer_builder.cc中定義)
函式定義如下:
我們可以看到,在這可能還存在另外一個問題。根據bytes_used_值,函式將返回buffer_自身,或者只是buffer_的一部分。(即一個較小空間的ArrayBuffer,其內包含一份資料副本)
到目前為止,我們看到的所有修復的程式碼中,都是直接返回資料副本,而不是返回實際緩衝區地址。除非是在執行老版本程式碼,並且我們試圖訪問的緩衝區是處於“完全佔用”的狀態的情況下。
但是,在FileReaderLoader物件的實現過程,buffer_->ByteLength()獲取的是預先分配的緩衝區大小,它對應於我們要載入的資料的大小(稍後將會相關)。
目前看來,在finished_loading標誌被設定為true之前,且在資料已經完全載入之後,多次訪問呼叫ArrayBufferBuilder::ToArrayBuffer()函式,將是利用該漏洞的唯一條件,這個時間點是漏洞觸發的最佳交換時期。
為了總結程式碼檢查這一部分,我們在看一下新老版本中都會呼叫的DOMArrayBuffer::Create函式,讓我們感興趣的是DOMArrayBuffer::Create(raw_data_->ToArrayBuffer())這個函式呼叫。該函式定義在third_party/blink/renderer/core/typed_arrays/dom_array_buffer.h標頭檔案中。
定義如下圖:
有趣的是,它呼叫了std::move,該函式具有所有權轉換的語義。
例如,在以下程式碼中:
std::move函式呼叫後,‘b’取得屬於‘a’的所有權(‘b’現在包含的內容是“hello”),而‘a’現在處於某種未定義的狀態(C++ 11規範以更精確的術語解釋)。
當前情況下,有些事情令人感到困惑,具體請檢視 連結1 和 連結2 。ArrayBufferBuilder::ToArrayBuffer()返回的物件已經是一個scoped_refptr<ArrayBuffer>。我相信,當呼叫ToArrayBuffer()函式時,ArrayBuffer的引用計數將增加1,然後std::move函式又獲取該引用物件例項的所有權(而非ArrayBufferBuilder擁有那個物件)。呼叫10次ToArrayBuffer()函式,引用計數將增加10,但所有的返回值將是有效的(與前面提到的‘a’,‘b’的例子不同,該例子中‘a’將導致無法預期的行為發生)。
如果在上面描述的最佳交換期間我們多次呼叫ToArrayBuffer()函式,這將會觸發生成一個明顯的use-after-free漏洞,ArrayBufferBuilder物件中的buffer_物件將被破壞。
2.2 FileReader API
我們也可以通過檢視JavaScript中的API呼叫,看我們能否找到另一個方法找到我們所要尋找的漏洞觸發的最佳交換時期。
在 Mozilla Web 文件中,我們能獲取所有的資訊。其實,操作十分簡單,我們可以在Blob物件或檔案物件中呼叫諸如readAsXXX的函式,其後我們可以中斷讀取操作,此外,還有幾個事件我們可以註冊回撥函式(比如onloadstart、onprogress,、onloadend …等)。
其中onprogress處理事件聽起來是最有趣的一個,它是在資料正在載入並載入完成之前被呼叫。如果我們檢視FileReader.cc原始檔,我們可以看到該事件背後的邏輯,在收到資料時,每隔50毫秒(或更長)該事件觸發一次。下面,讓我們來看看在一個真實的系統中它時如何表現的…
3. web瀏覽器環境測試
3.1 準備工作
我們要做的第一件事是下載存在漏洞的程式碼版本。有一些非常有用的網路資源,在 那裡 我們可以獲取老的版本,而無需自己再去重新編譯原始碼獲得。
資源如下圖所示。
值得注意的是,圖中資源還包含了一個檔名稱包含‘syms’字串的zip檔案,內含.pdb除錯符號檔案,你可以直接匯入到各偵錯程式和反彙編軟體,有助於更易分析。
3.2 偵錯程式附加
Chromium是一個複雜的、支援多程序通訊的軟體,對其除錯比較困難。最有效的除錯方法是正常啟動Chromium,然後將偵錯程式附加到你要進行漏洞測試的那個程序中。我們要除錯的程式碼執行在renderer程序中,並且關注的函式是由chrome_child.dll匯出的(這些資訊通過反覆實驗發現,比如附加到Chrome每個程序,尋找感興趣的函式名等)
如果要在x64dbg中匯入除錯符號,一個可行的方法是在Symbol欄,右鍵選中要匯入調式符號的.dll或.exe,然後選擇下載除錯符號。如果沒有正確設定除錯符號下載伺服器,可能會失敗,但是它仍將在x64dbg的‘symbols’目錄下建立相應的目錄結構,你也可以在該目錄下直接放置之前已下載的.pdb檔案。
3.3 尋找漏洞觸發點
現在,我們已經下載了一個尚未補丁的Chromium版本,並且已經知道如何用偵錯程式去附加除錯它。接下來編寫一段JavaScript程式碼,來看看是否能夠到達我們所關注的程式碼位置。
程式碼如下:
為了總結這將發生的事情,上述程式碼中我們建立了一個Blog物件用於傳遞給FileReader。此外,我們還註冊了progress事件的一個回撥函式onProgress,並當該事件觸發的時候,我們還試圖多次訪問FileReader返回值。在之前的文中,我們已經知道,資料需要被完全載入(這就是我們檢查緩衝區大小的原因),並且如果我們使用同一個ArrayBuffer獲得多次DOMArrayBuffer, JavaScript中他們看起來應該是多個分離物件(相等測試)。最後,為確認我們已經有兩個指向同一個緩衝區的不同物件,我們建立了兩個檢視物件並修改資料,結果驗證瞭如果修改一個物件,另一個物件也隨之改變。
在此,我們沒有預見到,發生了一個令人非常遺憾的問題:progress事件其實並不是經常性的會被呼叫,導致我們必須載入一個非常大的陣列,用以強制程序花銷一些時間並多次觸發progress事件。也許,會存在比上述做法更好的技術方法(可能谷歌的漏洞報告中會揭露一個好方法)。但是,所有建立慢速載入的物件的嘗試都失敗了(比如使用Proxy,擴充套件Blob類等…)。或者我們可以把資料載入與一個Mojo管道繫結,因此使用MojoJS看起來是一個擁有更多控制的好方法,但是在實際的攻擊場景中卻似乎不切實際。有關該方法的示例,可檢視 連結 。
3.4 導致崩潰
到此,既然我們已清楚瞭如何進入存在漏洞的程式碼路徑,那麼我們又該如何來利用這個漏洞呢?這絕對是最難回答的問題,本段旨在分享找到該問題答案的過程。
我們已經看到底層的ArrayBuffer會被引用計數,所以如果只是通過從已獲得的一些DOMArrayBuffer中進行垃圾記憶體蒐集的方法,我們無法對其進行釋放。使引用計數溢位,這聽起來是一個非常有趣的想法。但是,如果我們通過手動修改引用計數值為接近其最大值(比如通過x64dbg),再來看看會發生什麼…。好吧,程序崩潰了。最終,我們無法對這些ArrayBuffer做更多的事情;我們能修改它們的內容,卻不能修改它們的大小,除非我們能手動釋放它們…
如果對這些程式碼庫不是很熟悉的話,最好的方法就是去查閱各種提及了use-after-free、ArrayBuffer等關鍵詞的漏洞報告。去看看別人都做了什麼,談論了什麼。必須假設在某個地方一定存在一個擁有底層記憶體的DOMArrayBuffer物件,這也是一個我們知道的並努力實現的假設。
經過一些網路搜尋,我們發現了一些很有趣的評論,比如 連結1 和 連結2 。這兩個連結討論了DOMArrayBuffer被外部化(externalized)、被轉移(transferred)以及被閹割(neutered)的各種情況。對上述這些術語我們不是很清楚,但是從上下文結合來看,當上述情況發生時,記憶體的所屬權就轉移到了其他人身上。這聽起來非常完美,因為我們希望底層緩衝區能被釋放(就像我們急切的在尋找一個use-after-free漏洞一樣)
WebAudio中存在的use-after-free漏洞向我們展示瞭如何讓我們的ArrayBuffer發生“轉移”,所以讓我們試試吧!
在偵錯程式下可以看到:
圖中我們可以看到,被解除的記憶體引用的地址儲存在ECX中(我們看到EAX=0,這是因為我們正在該檢視物件中尋找第一個選項)。該地址看起來有效,但是事實卻並非如此。ECX指向陣列緩衝區的原始資料(AAAAA…)的地址,但是由於其被釋放,系統取消了儲存它的頁面對映,從而導致了記憶體訪問衝突(我們試圖訪問一個未對映的記憶體地址)。因此,我們找到了一個一直在尋找的use-after-free漏洞。
4. 漏洞利用和下一步工作思考
4.1 漏洞利用
本文重點不是展示如何通過use-after-free漏洞獲取完整程式碼執行許可權(事實上,在本文釋出的同時,Exodus已釋出了一篇 部落格文章 及一個可用的 漏洞利用 程式碼)。
根據我們觸發該use-after-free漏洞的方法,我們最終獲得了一個非常大的未分配的記憶體緩衝區。use-after-free漏洞通常利用方法是在釋放區域之上分配一個新物件從而產生某種混淆。文中,我們釋放了用於備份ArrayBuffer物件資料的原始記憶體。很好的是,我們可以讀取/寫入一個大記憶體區域。但是,這種方法也存在問題,就是由於該記憶體區域確實太大了,導致沒有一個物件可以符合該要求。假如我們有一個小一點的記憶體區域,我們就能建立大量特定大小的物件,希望可能有一個物件正好在該區域被分配。不過這個更難,我們需要去等待,一直要到堆為不相關的物件回收記憶體。在64位windows 10系統,由於記憶體隨機分配方式以及隨機地址可用等機制導致很難做到這一點。在32的windows 7系統中,由於地址空間要小的多,因此相對而言堆的分配更具確定性。分配10K左右的物件可能足以讓我們控制一些地址空間中的元資料。
另外有趣的是,由於要取消引用一個未對映的記憶體區域。如果上面提到的10K分配方法無法在我們控制的那個區域中分配至少一個物件,那麼我們未免就太不走運了。我們將由於記憶體訪問衝突而導致程序崩潰。此外,也有一些方法可以使得這一步更穩定,比如有些文章描述的利用 iframe的 方法 ,以及Javascript物件元資料被破壞的 示例 。
4.2 下一步工作
即使攻擊者在瀏覽器渲染程序中獲得程式碼執行許可權,但是依然受到沙箱機制的限制。本漏洞發現的野外利用,攻擊者使用了另外一個零日漏洞用於躲避沙箱機制。最近360CoreSec釋出了一篇描述該野外漏洞利用的 技術文章 。
5. 結論
通過研究提交的漏洞修復方式,查詢提示和類似的修復,我們有可能恢復漏洞利用路徑。再一次的,我們可以看到,在windows新近作業系統版本中引入的安全防護機制使得攻擊者的日子愈發困難,從防守方的角度來說,我們應該對此表示慶祝。此外,谷歌在漏洞修補策略方面非常高效、積極,其大部分使用者群的Chrome瀏覽器已經及時更新到最新版本。
Links
[1] https://chromereleases.googleblog.com/2019/03/stable-channel-update-for-desktop.html
[2] https://security.googleblog.com/2019/03/disclosing-vulnerabilities-to-protect.html
[2b] https://bugs.chromium.org/p/chromium/issues/detail?id=936448
[3] https://chromium.googlesource.com/chromium/src/+log/72.0.3626.119..72.0.3626.121?pretty=fuller
[3b] https://github.com/chromium/chromium/commit/ba9748e78ec7e9c0d594e7edf7b2c07ea2a90449
[5] https://www.chromium.org/developers/smart-pointer-guidelines
[6b] https://www.chromium.org/rvalue-references
[7] https://developer.mozilla.org/en-US/docs/Web/API/FileReader
[9] https://www.exploit-db.com/exploits/46475
[10a] https://bugs.chromium.org/p/v8/issues/detail?id=2802
[10b] https://bugs.chromium.org/p/chromium/issues/detail?id=761801
[11] https://blog.exodusintel.com/2019/01/22/exploiting-the-magellan-bug-on-64-bit-chrome-desktop/
[12] https://halbecaf.com/2017/05/24/exploiting-a-v8-oob-write/
[13] http://blogs.360.cn/post/RootCause_CVE-2019-0808_EN.html