標籤:

Google與微軟互懟,看看誰的瀏覽器沙箱更安全?

前言

Google安全團隊Project Zero曾多次揭露微軟產品的安全漏洞,有幾次更在微軟修補前即公布,惹來微軟的不滿。雙方曾為此隔空交戰,微軟在10月18號也揭露了Chrome瀏覽器漏洞,並指責其沙箱安全性不足,修補政策無法有效防止黑客攻擊。

下面就讓我具體分析一下此次事件,看看微軟說的有沒有道理。讓我先從Chrome開始,看看Chrome是如何阻止一個RCE漏洞的,然後進一步探討是否有強大的沙箱模式足以使瀏覽器達到最安全的狀態。

通過取證分析,我發現了Chrome所包含的以下漏洞:

1.CVE-2017-5121漏洞表明在Chrome中有RCE漏洞;

2.Chrome缺乏預防RCE漏洞的技術,這意味著從發現漏洞到使用漏洞的時間會很短;

3.在Chrome沙箱內進行的幾次安全檢查表明,RCE能夠繞過同源策略(SOP),讓攻擊者訪問受害者的在線服務(如電子郵件、文檔和銀行會話)並保存登錄憑據;

4.由於Chrome的漏洞處理流程是先向用戶公開披露漏洞細節,所以很可能使得漏洞在修復之前被濫用。

發現並利用一個RCE漏洞

要發現漏洞,就要首先找到一個異常現象。通常,我們都會找到內存漏洞,如緩衝區溢出或UAF漏洞來來實現此目的。對瀏覽器的攻擊方法多種多樣,包括V8 JavaScript引擎編譯、Blink DOM引擎和pdfium PDF渲染器等,其中V8編譯是本文的重點。

本文所使用的CVE-2017-5121漏洞就是微軟使用基於Azure的漏洞檢查工具ExprGen發現的。ExprGen是通過監測Chrome瀏覽器所使用的V8 JavaScript引擎,從而發現的CVE-2017-5121。

譯者註:Google在2014將其V8JavaScript 解析引擎移到Github地址是:github.com/v8/v8。V8是Google發布的開源 JavaScript引擎,採用C++ 編寫,在Google的Chrome瀏覽器中被使用。V8 引擎可以獨立運行,也可以用來嵌入到C++應用程序中執行。

識別漏洞

與手動審查代碼相比,基於Azure的自動化模糊檢測的缺點就是不能很快地分析出漏洞發生的原因。

由於模糊檢測技術經常產生大量複雜的代碼片段,所以在使用ExprGen檢測時,首先要將測試樣本的代碼進行精簡,只留下一些能夠識別的代碼。

當使用D8運行此代碼時,V8的獨立可執行版本由git標籤6.1.534.32構建,我們會遇到崩潰:

這段代碼的任務就是創建一個奇怪的結構化對象,然後設置一些欄位。雖然從理論上講,這不應該引發任何奇怪的行為,但實際上卻出現了一些運行結果。當使用D8運行這段代碼運行時,發現V8的獨立可執行版本是由git標籤6.1.534.32構建,此時會出現一次崩潰(如下圖)。

譯者註:如果你想了解如何使用D8分析javascript被V8引擎優化,請點此鏈接。

由上圖可以看出,崩潰發生的地址是0x000002d168004f14,因此我可以斷定該崩潰是不會發生在靜態模塊中的。因此,它必須是由V8的just-in-time(JIT)編譯器動態生成的。還能看到,崩潰發生是因為rax寄存器是零。

乍一看,這看起來像空引用異常(null dereference bug),如果是這樣的話就沒有什麼可講的了,因為這些異常通常不是可利用的,因為目前的操作系統會禁止零虛擬地址(zero virtual address)的映射。

我可以從上圖的代碼中發現兩個有用的線索:

首先,可以注意到崩潰發生在一個函數調用之前,該函數看起來像一個JavaScript函數調度器存根,這主要是由於v8::internal::Builtin_FunctionPrototypeToString的地址被載入到該調用之前的一個寄存器中。看看位於0x000002d167e84500的函數代碼,我發現地址0x000002d167e8455f確實包含一個調用rbx指令,這似乎證實了我的懷疑。

調用Builtin_FunctionPrototypeToString很有趣,因為這是Object.toString方法的實現。這似乎表明崩潰發生在func0 Javascript函數的JIT編譯器中。

其次,在崩潰時寄存器rax中包含的零值是從內存中載入的。這個過程看起來像是原本應該被載入的值被傳遞給作為參數的toString函數調用,並且是從[rdi + 0x18]載入的,以下就是內存載入的部分代碼。

以上代碼並沒有包含什麼有價值的信息,因為大多數值都是指針。但是,分析指針的來源卻很有用,因為它可以幫助我弄清楚為什麼這個值一開始就為零。使用WinDbg最新公布的「時間旅程調試(TTD)」 功能,我可以在該位置上放置一個內存寫入斷點(baw 8 0000025e a6845dd0),然後在函數的起始處放置一個執行斷點,最後重新反過來進行跟蹤(g -)。

有趣的是,放置的內存寫入斷點並不會被觸發,這意味著這個內存數據不會在這個函數中被初始化,或者至少在使用之前不會被初始化。但如果我對測試樣本進行一些處理,例如把o.b.bc.bca.bcab = 0;替換成o.b.bc.bca.bcab = 0xbadc0de;,然後就可以發現崩潰值源自於內存區域的變化:

由上圖可以看到, 0xbadc0de常數值最終出現在內存區域中。儘管這並沒有證明什麼,但卻似乎表明,這個內存區域被jit編譯的函數使用過,用來存儲局部變數,因為開始處的代碼看起來像是前面被傳遞給Object.toString作為參數的載入值。

結合TTD確認該內存槽未被該功能初始化的事實,可能的解釋是JIT編譯器無法發出將初始化表示用於訪問o.b.ba.bab欄位的對象成員的指針代碼。

為了證實我的推斷,我可以用-trace-turbo和-trace-turbo-graph參數在D8中運行測試樣本。這樣,D8就會輸出有關TurboFan,V8的JIT編譯器如何構建和優化相關代碼的信息。Javascript 引擎 V8採用的是新的引擎:TurboFan 和 Ignition,其中 Turbofan 是新的優化編譯器,而 Ignition 則是新的解釋器。

TurboFan的工作原理是將各個階段的優化信息都轉化成對象映射圖,如下圖所示。

可以看出優化器將func0嵌入到無限循環中,然後拉出第一個循環進行迭代。這些信息有助於了解這些功能塊之間如何相互關聯。然而,用映射圖進行表示的缺點就是無法表示對應於載入函數調用參數的節點,以及局部變數的初始化。

幸運的是,我們可以使用turbolizer來顯示出這些內容。關注第二個Object.toString調用,我可以看到參數的來源,以及它被分配和初始化的位置:

在優化流程的階段,代碼看起來完全合理:

1.分配存儲塊以存儲局部對象o.b.ba(節點235),並且其欄位baa和bab被初始化;

2.分配存儲塊以存儲局部對象o.b(節點259),並且其欄位都被初始化,其中ba專門用對前一個o.b.ba分配的引用來初始化;

3.分配存儲塊以存儲局部對象o(節點303),並且其欄位都被初始化;

4.局部對象o的欄位b被具有對象o.b(節點185)的引用覆蓋;

5.局部對象欄位o.b.ba.bab被載入(節點199,209和212);

6.調用Object.toString方法,將o.b.ba.bab作為第一個參數傳遞。

不過,優化過程中編譯的代碼不應該顯示未初始化的局部變數行為,所以我可以假設這chrome漏洞發生的根本原因。話雖如此,這只是我的一個假設。查看分別被載入到o.b.ba和o.b.ba.bab的節點209和212,它們被用作函數調用參數,如下圖,我可以在偏移量+ 24和+ 32處看到被反彙編過的崩潰代碼。

0x17和0x1f分別為23和31,考慮到V8標籤的值可以區分實際對象與內聯整數(SMI),可以進行這樣的推測:如果一個表示JavaScript變數的值具有最低有效位設置,則被視為指向對象的指針,否則就是SMI。因此,在取消引用(dereferencing)之前,可以使用V8代碼進行優化,減去一個JavaScript對象的偏移量。

不過到目前我還是沒有找出chrome漏洞的原因,所以我會繼續進行優化以找到原因。之後我進行了逃逸分析(Escape Analysis) ,分析試圖如下所示。

可以看出,有兩個顯著的不同:

1.代碼不需要從o載入到o.b,被優化後直接引用o.b,我想可能是因為該欄位的值從未更改過;

2.代碼不再初始化o.b.ba,從上圖中可以看出,turbolizer輸出了節點264,這意味著它不再是實時的,因此不會被嵌入到最終的代碼中。

此時,查看所有的活動節點似乎都確認這個欄位不會再被初始化了。不過為了保險起見,我還是在這個帶有–no-turbo-escape標誌的測試樣本上運行d8,以跳過此優化階段。結果我發現d8不再崩潰,所以可以確定這就是漏洞的真正原因。其實, Google已在v8 6.1中完全禁用了逃逸分析階段,只有在v8 6.2中才會發生逃逸分析。

現在我已經掌握了該漏洞的所有信息,不過為了深度驗證此漏洞,我還要想辦法利用它,看看它是不是一個攻擊力很大的漏洞,不過這完全取決於我控制未初始化的內存插槽的能力。

獲取泄漏信息

此時,利用漏洞的最簡單的方法是對樣本進行簡單地處理。例如,可以從未初始化的指針中更改正在載入的欄位類型,看看會出現什麼反應:

其結果是,該欄位現在直接被載入為一個FLOAT 數據類型,而不是一個對象指針或SMI:

同樣,我可以嘗試在對象中添加更多欄位:

運行以上的代碼段,可以得到以下崩潰:

這很有趣,因為它看起來像是將欄位添加到經過對象修改的偏移量中,其中該偏移量的位置位於被載入的對象欄位中。如果你感興趣,可以做個數學運算,你會看到(0x67 – 0x1f)/ 8 = 9,這恰好是我從o.b.ba.bab中添加的欄位數量。這同樣適用於從rbx載入的新偏移量。

經過對測試樣本進行更多的操作後,我可以確認,即使這些欄位沒有被初始化,我還是能對未初始化的指針載入的偏移量進行控制。現在,就讓我檢測一下,看看是否可以將任意的數據放到這個內存區域中發生反應。使用0xbadc0de進行的早期測試似乎肯定了我的檢測,不過每個測試樣本的每次運行所檢測出的偏移量都不一樣。通常情況下,我會利用噴洒數值(spray value)的方式來解決這些問題。其原理是這樣的:如果我不能準確地將目標鎖定到給定的位置,那我就可以讓目標範圍擴大。在實踐中,我經常使用內嵌數組(inline array)來擴大數值範圍。

發生的故障轉儲(crash dump)如下所示:

可以看出,崩潰與以前基本相同,但如果我查看來自內存的未初始化數據,你會發現如下的情況。

在r11的偏移位置處,我可以看到一大塊由任意腳本控制的值。將這個發現與之前的偏移量相結合進行分析,我可以得出更靠譜的結論。

由上圖可以看出,我取消了任意地址的float值:

當然,這是非常強大的,因為它會立即導致任意讀取原語。但不幸的是,沒有初始信息泄漏的任意讀取原語在實踐中並不是很有用的,因為我需要知道要讀取哪些地址才能使用它。

變數v可以替換成任何我想要的東西,比如,我可以用一個對象替換它,然後讀取它的內部欄位。再比如,我可以使用自定義的回調來替換對Object.toString()的調用,並將V替換為DataView對象,從而讀取該對象的備份存儲地址,這就為我提供了一種找到完全由腳本控制的數據的方法。

以上就是返回的代碼(以ASLR為模型):

使用WinDbg,就可以驗證這是我的緩衝區的後備存儲器。

由於這是一個非常強大的原語,我可以使用它來泄漏任何來自對象的欄位,以及任何JavaScript對象的地址,因為這些對象有時會存儲為其他對象中的欄位。

構建任意的讀/寫原語

能夠在已知地址處放置任意數據,就意味著我可以構建一個更強大的原語,因為此時我已經具備了創建任意JavaScript對象的能力。只需改變從float讀取到的對象的欄位類型,我就可以從內存中的任何地方讀取對象指針,比如已知地址的緩衝區。我可以使用WinDbg在已知的地址(上面發現的原語)中使用以下命令來進行測試:

這將在我的任意對象指針的載入位置放置一個表示整數0xbadc0de的SMI,由於我沒有設置最低有效位,它將被V8解釋為內聯整數:

正常情況下,V8會列印以下輸出:

鑒於此,我就可以創建任意對象,比如,我可以通過創建假的DataView和ArrayBuffer對象來組合一個我使用起來很方便的任意讀/寫原語。現在,我會再次使用WinDbg將我的假對象數據放在已知位置。

然後我會用以下JavaScript進行測試:

如預期的那樣,調用DataView.prototype.setUint32會觸發崩潰,因為調用會嘗試將0xdeadcafe值寫入0x00000badbeefc0de地址。

可以看出,數據寫入或讀取的地址被控制了,該問題只是修改通過WinDbg填充的obj.arraybuffer.backing_store插槽所出現的問題之一。因為在實際的漏洞中,內存將成為真正的ArrayBuffer對象的後備存儲器的一部分,要實現這點很容易,例如,寫入如下所示的原語。

這樣,我就可以在JavaScript的Chrome渲染程序中可準確地讀寫任意內存位置。

實現任意代碼執行

只要有任意讀/寫原語,就可以很容易地在Chrome渲染程序中實現代碼執行。由於V8會將其JIT代碼頁分配給讀寫執行(RWX)許可權,這意味著可以通過定位一個JIT代碼頁,然後覆蓋該頁面,最後通過調用來完成代碼執行。在實際操作過程中,可以通過使用我的信息泄漏來定位JavaScript函數對象的地址並讀取其函數entrypoint欄位來實現。一旦我將代碼放在了入口點,就可以調用JavaScript函數來執行代碼。

值得注意的是,即使V8沒有使用RWX頁面,由於缺乏對控制流程的完整性檢查,仍然很容易觸發返嚮導向編程(ROP)鏈的執行。如果發生這種情況,可以通過重寫JavaScript函數對象的entrypoint欄位來指向所需的gadget,然後進行函數調用。

不過,這些技術都不能直接用於具有CFG和ACG功能的Microsoft Edge。在Windows 10 Creators Update中引入的ACG會執行嚴格的數據執行保護(DEP),並將JIT編譯器移動到外部進程。這就形成了一個強大的防禦,即攻擊者在不破壞JIT進程的情況下,是不能重寫可執行代碼的,這就需要發現和開發其它的漏洞。

其次,CFG保證了間接調用站點只能跳轉到某一組函數,這意味著該函授不能用於直接啟動ROP執行。另外, win10 creators update還引入了CFG導出抑制,通過將大部分導出的函數從有效的目標集合中刪除,從而大大減少了有效的CFG間接調用目標集,所有這些緩解措施都使得黑客很難利用Microsoft Edge中的RCE漏洞。

RCE漏洞的危險

Chrome採用多進程模式,涉及到多種流程類型,比如瀏覽器進程,GPU進程和渲染器進程。

每個渲染器都會負責解析和解釋對應網頁的的HTML,JavaScript等。沙箱模型使得這些進程只能訪問其需要的功能。因此,我無法通過渲染器中對受害者的系統進行攻擊。

如此一來,我就要考慮攻擊者有沒有可能使用了其它的漏洞完成攻擊。雖然大多數選項卡的進程中都是進行單獨隔離的,但還是有些特殊情況,例如,如果在bing.com上使用JavaScript開發者控制台(可以通過按F12打開該控制台)來運行window.open(microsoft.com),則會打開一個新的選項卡,但新的選項卡通常會與原始標籤的進程完全相同。這可以通過使用Chrome的內部任務管理器來看到,你可以通過按Shift + Esc來打開它。

這是一個有趣的觀察,因為它表明渲染器進程沒有被鎖定到任何一個來源。這意味著在渲染器進程中實現任意代碼執行就可以使攻擊者訪問其他來源的進程。雖然攻擊者以這種方式繞過同源策略(SOP)的可能性並不大,但如果一旦發生,則後果不堪設想:

1.攻擊者可以通過劫持PasswordAutofillAgent界面從任何網站竊取用戶保存的密碼;

2.攻擊者可以將任意JavaScript注入任何頁面,例如通過劫持 blink::ClassicScript::RunScript 的方法;

3.攻擊者可以在用戶沒有注意的情況下導航到後台的任何網站,例如,通過創建隱藏的彈出式窗口。因為許多用戶交互檢查是發生在渲染器進程中的,無法讓瀏覽器進行進程驗證。這樣,類似ChromeContentRendererClient:AllowPopup的進程就會被劫持。這樣在不需要用戶交互的情況下, 攻擊者就可以隱藏新的窗口。他們還可以在關閉的時候繼續打開新的彈出窗口,例如,通過連接到onbeforeunload窗口事件。

這種攻擊更好地研究並實施了渲染器和瀏覽器進程如何相互通信並直接模擬相關消息,但這表明這種攻擊可以在有限的努力下實現。雖然雙重身份驗證的民主化減輕了密碼盜竊的危險,但是由於可以讓攻擊者在已經登錄的網站上欺騙用戶的身份,所以不可認為雙重認證就是絕對可靠的。

這表明只要在渲染器和瀏覽器進程的通信中做點文章,即使用戶採用了雙因素身份驗證,攻擊者還是能夠達到盜取賬號的目的的。

總結

客觀地講,Google與微軟互懟的結果,會讓普通的用戶受益,因為每個瀏覽器都有其長板和短板,互相競爭的結果迫使它們不得不努力改善自己以吸引用戶。就Microsoft Edge而言,目前它正在繼續改進隔離技術,並使得任意代碼執行難以實現。就Google而言,Google正在開發一個網站隔離功能,一旦該功能實現,Chrome就可以通過保證任何給定的渲染器進程只能與單一來源進行交互,這樣Chrome就能輕鬆應對RCE攻擊。

本文翻譯自 blogs.technet.microsoft.com ,如若轉載,請註明原文地址: 4hou.com/binary/8131.ht 更多內容請關注「嘶吼專業版」——Pro4hou

推薦閱讀:

AirHopper:使用無線電突破獨立網路和手機物理隔離(上)
刺風有道,吳翰清的雲端飛揚
揭秘無文件惡意軟體的前生今世
如何與人工智慧和睦相處?它已經掌控了你的一切
濫用系統Token實現Windows本地提權

TAG:信息安全 |