作者: Qixun Zhao(@S0rryMybad) of Qihoo 360 Vulcan Team
博客:https://blogs.projectmoon.pw/2019/01/13/Story1-Mom-What-Is-Zero-Multiplied-By-Infinity/

今天我們文章介紹的是CVE-2018-8391,對應的patch commit. 這是一個關于Loop循環的越界讀寫漏洞,漏洞的成因十分有趣.我們都知道零乘以無限等于零,但是開發人員在寫代碼的時候忽略了這樣的一種特殊情況.

在這里我除了介紹漏洞本身以外,還介紹了在引入了Spectre Mitigation之后的一種通用的Array OOB RW利用方法.關于這個漏洞,我們還有后續的Story2.

實驗環境: chakraCore-2018-8-15附近的commit

0x0 關于Loop的優化

在之前的文章中我們已經簡單介紹過關于Loop的優化,在編譯器的優化過程中,我們需要把很多在Loop中不需要變化的指令hoist到LandingPad中,不然每次循環會執行很多沒必要的指令.而在針對數組的邊界檢查中,有一種特殊的優化處理方法,這種優化是針對在循環inductionVariable并且用inductionVariable進行數組訪問的情況.inductionVariable就是循環中的自變量.舉個例子最直接:

img

這里i就是inductionVariable,并且arr2用i進行數組訪問.優化的詳情在doLowerBoundCheckdoUpperBoundCheck這兩個函數中.這里用doUpperBoundCheck作為例子.

我們可以看到最下面有一個|CreateBoundsCheckInstr|的函數,用于生成一個boundcheck指令,用于檢查|indexSym <= headSegmentLength + offset (src1 <= src2 + dst)|(注釋已經很清楚).只需要通過這個檢查,在下面的循環中就不會再有任何邊界檢查,因為已經hoist到LandingPad中,問題的關鍵就出在這個邊界檢查中.所以關鍵是這個檢查是怎么保證在循環中數組的訪問一定不會發生越界呢?

HeadSegmentLength很清楚就是數組的長度,問題就在于這個indexSym是怎么得來的,通過閱覽代碼我們可以發現是在上面的函數|GenerateSecondaryInductionVariableBound|(生成的hoistInfo.IndexSym最終用于初始化lowerBound這個Opnd).

0x1 GenerateSecondaryInductionVariableBound的計算方法

這個函數根據字面意思已經很清楚,就是計算inductionVariable的取值范圍,只有inductionVariable的最大值少于HeadSegmentLength,循環中的數組訪問必定不會越界.

至于計算的方法其實代碼的注釋已經十分清楚,下面我截取代碼的注釋來解釋:

inductionVariable就是我們i的初始化值,也就是我們上圖的start,而loopCountMinusOne的計算方法在GenerateLoopCount函數中:

img

在這里我用我小學畢業的數學知識把這個公式變換一下,得到如下的等式,當然要注意運算符號的順序: (left - right + offset) / minMagnitudeChange * maxMagnitudeChange + inductionVariable

這里簡單結合js代碼介紹一下各個變量的含義:

函數中:

Left對應的是end變量,right對應的是start,至于offset我們不用太在乎,如果判斷條件是|i<end|,則offset是-1,如果是|i<=end|,則offset是0,對我們影響都不大.minMagnitudeChange是自變量在每一次循環中可能增加的最小值,這里是1(也就是if條件不成立的時候),同理maxMagnitudeChange 是可能增加的最大值,這里是0x1001,也就是if條件成立的時候,inductionVariable我們上面已經提到,也就是start,最終得到的公式與Opcode如下:

結合我們文章的題目,聰明的讀者肯定已經想到問題出在哪里.

0x2 Mom,零乘以無限等于多少?

上述的公式在計算i的取值范圍的時候已經十分保守了,因為沒可能每一次循環i都是增加最大值,但是它忽略了一種特殊情況:zero.當(end- start - 1) / 1等于0的時候,無論它后面乘以多大的數,結果都是0,最后邊界檢查就是只需要start < headSegmentLength即可,而這個邊界檢查是不安全的(試想maxMagnitudeChange 遠遠大于headSegmentLength).

有了越界讀寫的能力,下一步就是如何利用了,chakraCore在這個commit中加入了一個mitigation, 這個commit簡單來說在每一次數組訪問的時候都會再次檢查index是否少于數組的長度,如果不少于就直接crash,本來是用于防御Spectre,但是也把這些越界讀寫漏洞堵住了.換句話說,即使bypass了boundcheck,還要這些mask指令需要bypass.在剛引入的時候,很多人都覺得這種越界讀寫的漏洞不能再利用了.

這些指令的引入是十分拖累速度的,千辛萬苦才消去了boundcheck的檢查,又引入這個措施等于boundcheck的消去毫無意義,特別是在Loop中,每一次的循環都要運行這些沒必要的mask指令,因此微軟很快就引入了一個優化措施,在某些情況下hoist這些mask指令到循環外.由于這個優化措施比較復雜,這里只能簡單介紹一下,它存在于Backward階段的processBlock中,相關代碼如下:

首先遍歷所有的opnd,查看這個opnd的有沒有type-specialized,這里我們可以理解成有沒有針對特定類型的優化,例如Float64等等,如果沒有則記錄下這個Sym的id,記錄下的id最終在這里進行判斷:

如果這里滿足兩個條件,如果是LdElemI_A指令并且之前沒有把Opnd的Sym記錄下來,則把這個指令SetIsSafeToSpeculate(true);意思是不需要添加mask指令,最終在一個air block中加入防御指令:

這個指令是架構相關的,不同架構有不同實現,這里與我們討論的無關,不再展開.

換句話說,第一數組的訪問必須在loop里面,觸發它的loop優化機制,第二我們只能進行數組的load并且數組是int32類型或者float64類型,則我們可以把mask指令hoist到loop外.但是單單有這樣的越界讀(除非再多一個object數組的越界讀)是不夠的,我們需要更多的東西去RCE.

0x3 Hi, MissingValue Again

有了越界讀,我們是可以越界讀取一個missingValue的值的,只要我們首先初始化一個數組,然后把這個數組的length重新設置,例如:

img

則在它的index 4的地方有一個missingValue,同時也滿足了HasNoMissingValue為true,如果不滿足在后續我們JIT取出該值的時候是要bailout的,內存區域如下:

img

這時候如果我們能off by one index,我們就能讀取到這個missingValue,然后我們可以用這個missingValue創建一個evil Array:HasNoMissingValue為true,但是headSegment中帶有missingValue,最終創建evil Array的PoC如下:

img

有了這樣的數組,離RCE還遠嗎,網上已經有大量的利用例子.可以參考我們的第一篇文章或者project-zero 或者 From zero to zero day

剩下的就作為讀者的練習吧.

0x4 總結

零乘以無限等于零


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