您好,登錄后才能下訂單哦!
本篇文章給大家分享的是有關怎樣分析Chrome 1day 漏洞CVE-2021-21224,小編覺得挺實用的,因此分享給大家學習,希望大家閱讀完這篇文章后可以有所收獲,話不多說,跟著小編一起來看看吧。
2021年4月4日 - tr0y4 在 Chromium issue tracker 中提交該漏洞;
2021年4月12日 - Chromium 修復該漏洞,除了補丁外還公開了相關的poc;
2021年4月14日 - 國內的研究員 frust93717815 公開了此漏洞的exp[1][2],影響未開沙箱的穩定版 Chrome 瀏覽器;
2021年4月20日 - Chrome發布更新及致謝,對應的CVE編號為 CVE-2021-21224。
該漏洞發生在v8的優化編譯器 TurboFan 中,具體發生在 JIT 的 Simplified-Lowering 階段。關于TurboFan的介紹可以參考 ”Introduction to TurboFan”[3]。有關 Simplefied-Lowering 階段的具體細節可以參考 CVE-2020-16040 的分析[4]。此外,本文使用Turbolizer 展示 TurboFan 不同優化階段的 sea of nodes graph。
下面簡單介紹一下sea of nodes,它是TurboFan運行時的程序表示形式。TurboFan在優化代碼時,整個代碼是以graph的形式存儲的,每個節點就是一個node,包括數學運算、加載、存儲、調用、常數等形式。每個node的具體信息如下:
每個 node 都有一個 restriction type 和一個 feedback type,前者可以理解為節點初始化后的類型,后者則是在實際優化過程中反饋的真實類型。Node 的representation 則表示節點的表示類型,具體如下:
根據commit信息得知,漏洞發生在 RepresentationChanger:: GetWord32RepresentationFor 函數內。該函數的調用棧如圖 1,對該函數的調用主要發生在 Simplified-Lowering 階段:
圖1. GetWord32RepresentationFor 調用棧
具體補丁如圖 2,output_rep 為傳入節點的 Representation 類型, output_type 為當前節點的 feedback type,use_info 為當前節點的后繼節點信息(主要用于區分32位或64位)。所以修復之前的邏輯是滿足兩種情況之一可以更新op:一是當前節點的 feedback type 是 Signed32 或者 UnSigned32;二是當前節點 feedback type 為 SafeInteger 且其后繼節點被用作32位使用(IsUsedAsWord32)。
修復后的邏輯則是滿足以下三個條件之一便可以更新:一是當前節點的 feedback type是Signed32;二是當前節點feedback type是UnSigned32并且后繼節點類型是None;三是當前節點為SafeInteger并且后繼節點被用作32位使用。
圖2. 補丁信息
對比修復前后的邏輯可以發現,在當前節點的 feedback type 為UnSinged32時,并不能直接修改op,需要保證后繼節點類型為 None 才能修改。漏洞正是利用了這一條件,在當前節點滿足UnSigned32時,其后繼節點類型不為None(exp中后繼節點類型如圖 3所示,為 SingdeSmall,是一個有符號數),最后 op 被賦值為 TruncateInt64ToInt32。
因此,所有傳入到當前 TruncateInt64ToInt32 節點的數字都會直接轉為一個有符號32位數,如果傳入的數字恰好使用到了對應32位數的符號位(比如 exp 中使用的 0xffffffff),那么就會發生整數下溢。
圖3. 后繼節點的 use_info
為了驗證漏洞,本文使用補丁發布前一個版本的v8代碼進行實驗,commit 哈希為 f87baad0f8b3f7fda43a01b9af3dd940dd09a9a3。為了便于理解,本文簡化了 exp 中的 foo 函數如下:
這里我們通過對比 Simplified Lowering 及其前一階段 EscapeAnalysis 的graph結果來展示修復前后turbolizer graph的變化。如圖 1所示,在EscapeAnalysis 階段70號節點對應到Max的返回結果,它作為51號節點的輸入,繼續進行下一步的減法操作(SpeculativeSafeIntegerSubtract)。而經過Simplified Lowering 階段后,70號節點和51號節點之間被插入了一個新的95號節點 TruncateInt64ToInt32,直接將70號節點的輸出轉為一個32位有符號數,再進行減法操作。
此時再來看本節使用的例子,Max的返回值是 0xffffffff,即y=0xffffffff,經過TruncateInt64ToInt32節點后,y被轉成32位有符號數,也就是-1,發生了整數下溢。因此z最后經過 sign 函數得出的結果是1。
圖 4. 修復前EscapeAnalysis階段的turbolizer graph
圖 5. 修復前Simplified Lowering階段的turbolizer graph
修復之后得到的 turbolizer graph如圖 6,可以看出原本的TruncateInt64ToInt32 節點現在變成了 CheckedUint64ToInt32,在將64位無符號數轉為32位有符號數時會進行檢查,避免了整數溢出。
圖6. 修復后Simplified Lowering階段的turbolizer graph
單獨的整數溢出并不能實現 RCE,與之前 CVE-2020-16040[5]和CVE-2021-21220[6]的exploit類似,該 exploit 的作者結合 shift 函數獲得一個長度為-1的數組,實現 OOB。
exp 中觸發漏洞的地方在 foo 函數,下面的代碼只保留了原始 exp 中獲取長度為 -1 數組的部分。注釋中標注出了每一行代碼執行后的結果,可以看到最后聲明的 arr 數組實際長度為 1,但是經過 shift 操作后長度直接變成了-1:
下面通過對 turbolizer graph(Simplified-Lowering階段)中各個節點的分析來詳細說明產生上述情況的原因。
圖7是2-4行代碼中包含的關鍵節點,首先,前兩行代碼決定x的范圍是(-1,4294967295),接著到Math.max函數中,x首先和0比較取最大值,所以更新后的范圍是(0, 4294967295),然后再與-1比較取最大值,范圍沒有變化,仍然是(0, 4294967295)。
圖7. Math.max函數輸出結果
在此之后90號節點的值會先被轉為 Int32 然后進行一個減法操作,對應代碼就是第5行中的0-y,對應的范圍是(-4294967295,0)。需要注意的是這里的范圍并不是真實的范圍,后續還會有 feedback type 的更新會進一步更新這個范圍,但是這里對后續的結果并沒有影響。Math.sign 函數會根據根據參數的正負,返回+1/-1,此外還有+0/-0/NaN,由于這里+-0并不會對最終結果有影響,所以本文不做過多討論。由于0-y的值小于等于零,所以該函數輸出結果只能是(-1,0),即z的范圍是(-1,0)。接著到第6行代碼聲明數組,會對數組的邊界做檢查(長度必須要大于零),所以把z的范圍和0取交集得到最后數組長度的范圍是(0,0)。
Shift 操作會移除數組中的一個元素,并把數組長度減一。由于數組的長度只能是0,所以 jit 在優化過程中直接把shift之后數組的長度固定為0-1 = -1,這就是為什么 shift 之前數組長度是1,shift之后長度直接變成了-1。這里其實利用了shift函數中存在的一個bug,即該函數未對數組的邊界做檢查,無論原始數組的長度如何都直接進行減一操作。由于數組長度為0,所以shift之后長度固定為-1,這一值被直接寫入到jit優化后的代碼中(在圖 9中,長度被直接賦值為0xfffffffe,也就是-2,經過調試發現 jsarry 在內存中存儲的長度數值左移了1位,這是為了保證其內存中所有的數字都是以0結尾,指針以1結尾,具體可以參考JS類型對象的內存布局[8]),因此最終得到數組的長度為-1。
圖 8. 邊界檢查結果
還有一點需要注意的是,由于shift 操作還會移除數組中的的一個元素,所以如果第6行聲明的數組中不包含任何元素,則移除元素操作會導致v8直接崩潰。exp中利用溢出恰好聲明了一個長度為1的數組,滿足了這一限制條件。這也解釋了另一個問題,如果不利用整數溢出漏洞,直接聲明一個長度為0的數組,那么根據優化后的jit代碼是不是也能得到一個長度為-1的數組呢。實際上jit代碼中在shift之前對長度做了驗證,如圖 9所示,rdi為數組的長度,如果長度為0的話會直接跳過數組長度更新而直接返回,所以無法獲得一個長度-1的數組。
圖9. 調用shift前的邊界檢查及長度賦值操作
當然,谷歌也在4月15日的更新中修復了該bug[7]。在該補丁中,shift以及類似的pop函數在計算出新的數組長度后會首先進行邊界檢查,基本上杜絕了類似的利用方式。
圖 10. [turbofan] Harden ArrayPrototypePop and ArrayPrototypeShift補丁
1) 越界訪問rwarr數組
exp在 arr 數組之后又聲明了一個長度為2的數組和一個長度為0x1000的ArrayBuffer,接著修改了corrput_arr[12]的值,這里其實就是利用溢出修改了rwarr數組的長度。
根據調試信息可以得知,corrput_arr中元素的位置在0x18b80828216c,每個元素的長度是4字節,rwarr中元素的位置在0x18b8082821a8,根據二者之間的距離,減去corrput_arr前八個字節(分別為元素的map和長度),就可以計算出corrput_arr數組第13個元素對應的就是rwarr數組的長度。通過將該長度修改為一個較大的值(比如0x22444),可以實現對rwarr數組的越界訪問。
2) 越界訪問corrupt_buff
setbackingStore 就是設置 corrput_buf 中 backing_store 的值,leakObjLow則是泄露地址o處的信息。
rwarr設置的第一個元素值為5.1,長度8字節,所以數組中元素都是8字節,因此rwarr[4]的位置在0x18b8082821a98+8+4*8=0x18b8082821ac0,該地址再加4便是corrupt_buff的backing_store的地址, 因此這里還需要用到rwarr[5],分別修改rwarr[4]的高四個字節和rwarr[5]的低4個字節,就可以實現對backing_store的修改。后續exp會從corrupt_buff中生聲明一個Dataview,而backing_store記錄的就是實際DataView的內存地址。如果我們將這個backing_store指針修改為我們想要寫入的內存地址,那么我們再調用view.setUint32(0, poc, true) 類似指令時,實際上就是向指定內存地址處寫入了poc,從而達到任意地址寫。
leakObjLow函數使用corrupt_buff的slot屬性,修改該屬性為某一對象o,那么o的地址就會被寫入到corrupt_buff所在的內存區間中,然后利用rwarr的溢出訪問該值,實現泄露。
代碼執行的方法利用了WASM的RWX段。通過泄露該RWX段的地址,將shellcode寫入,最后調用WASM的導出函數即可實現代碼執行。
以上就是怎樣分析Chrome 1day 漏洞CVE-2021-21224,小編相信有部分知識點可能是我們日常工作會見到或用到的。希望你能通過這篇文章學到更多知識。更多詳情敬請關注億速云行業資訊頻道。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。