摘要:隨著我們贏得2017年P(guān)wn2Own比賽,賽場(chǎng)上包括我們的多只參賽隊(duì)伍都使用了Chakra JIT相關(guān)的漏洞。所以近年來腳本引擎的JIT相關(guān)的漏洞引起了大家的廣泛關(guān)注。但這并不代表Chakra腳本引擎中其它的邏輯就是安全的。宋凱將詳細(xì)介紹一個(gè)Chakra腳本引擎,非JIT相關(guān)的漏洞,以及這個(gè)漏洞利用的詳細(xì)過程。在最開始寫這個(gè)漏洞利用的時(shí)候,曾一度認(rèn)為其是不可用的(比如Array對(duì)象的size變大可能導(dǎo)致安全威脅,但是變小該如何利用呢?)。最后通過組合多種技巧,成功實(shí)現(xiàn)在Edge瀏覽器中的任意代碼執(zhí)行。
宋凱(exp-sky)????騰訊安全玄武實(shí)驗(yàn)室高級(jí)安全研究員
今天和大家主要談一下Charka引擎漏洞細(xì)節(jié),在Edge瀏覽器里繞過所有系統(tǒng)保護(hù)機(jī)制的技術(shù)與技巧。現(xiàn)在各大廠商因?yàn)楣シ缹?duì)抗的不斷升級(jí),都在不斷地加入這些musication(音108:33)來增加攻擊者的攻擊成本,這也應(yīng)該是全球首發(fā)的,繞過迄今為止Edge瀏覽器,在Windows平臺(tái)最新版本中有效的漏洞應(yīng)對(duì)方案。
我首先介紹一下Charka漏洞,這是我們團(tuán)隊(duì)為了2017年Defcon所準(zhǔn)備的備選方案之一。為什么說這個(gè)漏洞非常有趣呢?一開始我們寫這個(gè)漏洞的時(shí)候,一度認(rèn)為這個(gè)漏洞是不可用的。如果你能修改它的address讓它可以越界訪問,那么它是可以用的,可以產(chǎn)生越界的圖或者寫,都會(huì)對(duì)瀏覽器安全造成危害。如果我們只能變小怎么辦?非但沒有緩沖區(qū)越界訪問,反而可訪問的空間變小了,這樣的情況下如何利用漏洞呢?后面針對(duì)性地介紹在IE瀏覽器里保護(hù)機(jī)制,設(shè)計(jì)方法和繞過思路。
這是我經(jīng)歷多個(gè)步驟之后才能實(shí)現(xiàn)屏幕中的效果,大家仔細(xì)看一下標(biāo)紅里的值,是完全可控的,現(xiàn)在所嘗試的一個(gè)動(dòng)作實(shí)際是個(gè)內(nèi)存寫,這實(shí)際上實(shí)現(xiàn)了大范圍可控的越界寫的能力,要實(shí)現(xiàn)這一步,我們要經(jīng)歷比較多的步驟。這個(gè)漏洞在2016年5月份被我們發(fā)現(xiàn)了,在2017年,它往前一個(gè)月的補(bǔ)丁被撞或者被修補(bǔ)。詳細(xì)講這個(gè)漏洞過程中,先看一下所需要的基礎(chǔ)知識(shí)。
只要了解一個(gè)對(duì)象就可以了,就是NativelntArray,Array設(shè)計(jì)之初就是為了提高效率,與正常的,能存儲(chǔ)數(shù)據(jù)以及對(duì)象組成區(qū)分開,它里面只能存儲(chǔ)純的、整形的數(shù)據(jù)。Array設(shè)計(jì)本身它是個(gè)始初數(shù)據(jù),Array里所描述的空間范圍不不一定全是被映射狀態(tài),可以存在空洞。我初始化了哪一部分就對(duì)哪一部分進(jìn)行賦值。它的具體實(shí)現(xiàn)方式是用了Segment的結(jié)構(gòu),這幾個(gè)域,Left表示當(dāng)前Segment的位置,是從那里開始的,length是表示當(dāng)前segment初始化元素的長度,Size是用當(dāng)年segment的buffer用多大長度來進(jìn)行初始化,就是我buffer的大小,后面用size來申請(qǐng)內(nèi)存buffer。因?yàn)橄∈钄?shù)組支持從任意位置開始,所以用Next segment指針維護(hù)單向列表。
現(xiàn)在我們就知道了這樣的結(jié)構(gòu),在內(nèi)存中大致是這樣的狀態(tài),前面Array頭里是虛函數(shù)表的指針,后面是segment指針,也就是Head,Head里有Left、segment length、segment size等等這樣。這樣的設(shè)計(jì),其中有兩個(gè)設(shè)計(jì)者所認(rèn)為的約定,這兩個(gè)違法會(huì)會(huì)產(chǎn)生問題的。第一個(gè)約定是,這個(gè)NativeInt的頭里面有個(gè)lenghth,這個(gè)length表示的是我整個(gè)Array的空間到底有多大。里面每一個(gè)segment表示,我當(dāng)前segment的空間有多大。第一個(gè)約定就是,我Array頭里length所代表的空間(大小)一定要大于最后一個(gè)segment,也就是當(dāng)前Array中初始化的的,最后一個(gè)segment的Left+Length,最后一個(gè)segment被初始化元素的位置。這是第一個(gè)假設(shè)。
第二個(gè)假設(shè),在segment里,length一定要小于等于size,因?yàn)樗胹ize初始化了緩沖區(qū),length表示的意思是我往里面哪個(gè)位置,最后的位置賦過值。所以,length一定要小于size,否則它就越界了。這是兩個(gè)設(shè)計(jì)者所遵循的假設(shè)。
我們這個(gè)漏洞最經(jīng)典的POC是這幾行代碼,實(shí)際也就四五行。它能實(shí)現(xiàn)怎樣的效果呢?首先,穿越一個(gè)1024長度的Array,默認(rèn)就會(huì)創(chuàng)建成一個(gè)Native的Array,我們對(duì)它進(jìn)行回調(diào),創(chuàng)建了一個(gè)get回調(diào),在回調(diào)里我們修改了Array的length,把它變成零,我們?cè)?x2d處寫了一個(gè)1,都是非常簡單的操作。之后我們調(diào)用一個(gè)Array的reverse函數(shù),reverse函數(shù)的意義也很簡單,將我剛才所介紹的Array頭里的length,以這個(gè)維空間來進(jìn)行反轉(zhuǎn),將里面所有的segment以及segment里所有的元素進(jìn)行翻轉(zhuǎn)。
它會(huì)導(dǎo)致什么樣的效果呢?違反了我剛才所介紹的兩個(gè)約定其中的一個(gè)。剛才我在0x2d處設(shè)置了1,現(xiàn)在Array length變成了0x2e,這沒有問題。現(xiàn)在看一下它的Head segment,這是一個(gè)未初始化默認(rèn)的segment,left是0表示往里寫過任何值,它的size是0x2e,這是默認(rèn)的segment大小,buffer也是用0xe2×4這樣的D-world(音)空間來申請(qǐng)內(nèi)存。問題就出現(xiàn)在后面的next第二個(gè)segment里,Next是0x3d2,length是0x2e,size也是0x2e。這就違反了我剛才介紹的第一個(gè)約定,Array頭的length所表示的空間,現(xiàn)在已經(jīng)小于Left+length,現(xiàn)在我們并不能造成實(shí)際任何安全威脅,但它已經(jīng)違反了第一個(gè)約定。
在內(nèi)存中,NativeIntArray的頭里,length是0x2e,是head segment的指針。在這個(gè)指針里,Head segment就是一位觸發(fā)的,但是Next的segment指針,length是0x2(音)3d2,它的length以及size都是0x2。為什么會(huì)出現(xiàn)這樣的狀態(tài),為什么會(huì)產(chǎn)生這樣的不一致,這最起碼違背了設(shè)計(jì)者的約定。在ReverseHelper函數(shù)里,typed函數(shù)里其實(shí)想要過渡到當(dāng)前Array里所有回調(diào)所產(chǎn)生的動(dòng)態(tài)值,把它們都整理好了放在自己的位置里。它比會(huì)回調(diào)獲取值。問題就出現(xiàn)在,回調(diào)之前,外面闖進(jìn)來一個(gè)length。后面在回調(diào)之后,它沒有做任何檢查,依然沿用了這個(gè)length。這就導(dǎo)致前面所產(chǎn)生的不一致,在回調(diào)里把Array length變小了,但它依然用之前大的length進(jìn)行Array空間的翻轉(zhuǎn),這就導(dǎo)致Array頭里的length要小于它最后面segment所認(rèn)為的空間,產(chǎn)生這樣的不一致。
這有什么用呢?它能產(chǎn)生什么危害呢?如何利用這樣細(xì)微的威脅,細(xì)微的缺陷來是實(shí)現(xiàn)在Edge瀏覽器里整套的攻擊。
第一步,非常簡單,先把剛才所設(shè)計(jì)的回調(diào)刪掉,防止影響到后面的利用。然后將Array0A處寫個(gè)1,再調(diào)一次Reverse,會(huì)產(chǎn)生怎樣的后果呢?它變成這樣(圖),可以看到,Head size由剛才的0x2e變成了0x23,剛才通過Array head length變小,現(xiàn)在可以把segment的size變小。
看一下Head next的Left也是0x23,也會(huì)變成0x23。這里回到一個(gè)segment的length如果大于size的話,這個(gè)漏洞怎么利用?看一下內(nèi)存,已經(jīng)由它的size從0x2e變成了0x23,變小了。最開始我們寫漏洞的時(shí)候,甚至認(rèn)為它是不可用的。一個(gè)size讓它變大可以產(chǎn)生越界,變小我們應(yīng)該怎么用?為什么會(huì)變小?首先在ReverseHelper函數(shù)里,segment的Head在計(jì)算Left的時(shí)候,會(huì)調(diào)用后面的EnsureSizeInBound函數(shù),在這個(gè)函數(shù)里,它會(huì)先取Next,剛才的Next是0x23,在翻轉(zhuǎn)的過程中發(fā)現(xiàn),前面segment的left是0x23,到當(dāng)前segment的size是0x2e,也就是兩個(gè)segment出現(xiàn)了重合,它要抵消掉這樣的重合怎么辦呢?它把當(dāng)前的segment size由0x2e變成了0x23,縮小了一點(diǎn),實(shí)際是為安全來進(jìn)行的考慮。最后調(diào)min,取一個(gè)最小的。
基于安全的考慮,但沒有做好,在修改size的時(shí)候length沒有改,所以我們實(shí)現(xiàn)了第二個(gè)不一致,segment的length小于了size,但現(xiàn)在不能造成任何的安全威脅,因?yàn)閎uffer還是由之前的0x2e來申請(qǐng)。怎么產(chǎn)生一個(gè)安全問題呢?創(chuàng)造一個(gè)OOB,只要一行就可以了。這行意義是什么呢?剛才我們所說的是NativeIntArray,它里面存儲(chǔ)一個(gè)純的整形數(shù)據(jù),但我們往往賦值一個(gè)對(duì)象時(shí)會(huì)進(jìn)行類型轉(zhuǎn)化,把它c(diǎn)onverse為一個(gè)正常的JavaScriptArray。這種Array在進(jìn)行類型轉(zhuǎn)換過程當(dāng)中,一定要重新申請(qǐng)segment的buffer,因?yàn)樗臄?shù)據(jù)長度變了。之前NativeIntArray所存儲(chǔ)的數(shù)據(jù)寬度是4個(gè)字節(jié),但JavaScriptArray里所存儲(chǔ)的數(shù)據(jù)寬度是以一個(gè)對(duì)象指針來存儲(chǔ),64位上有8個(gè)字節(jié)。所以,他一定要重新申請(qǐng)buffer,重新申請(qǐng)segment,這樣就產(chǎn)生了一個(gè)真實(shí)的安全問題,它會(huì)用size來重新申請(qǐng)segment的buffer,但length依然沒有動(dòng),就直接復(fù)制過來了。我們現(xiàn)在擁有了用0x23×0x08創(chuàng)建的緩沖區(qū),但segment的length是0x2e,我們擁有了0x0b×0x08越界的能力,通過類型轉(zhuǎn)換。這就是違反了設(shè)計(jì)約定所導(dǎo)致的后果,開發(fā)者覺得沒問題,但實(shí)際上不是的。在內(nèi)存中就會(huì)產(chǎn)生這樣的狀態(tài),segment的length是0x2e,它的size是0x23,我后面標(biāo)紅的框是OOB可以訪問的一部分。
第三步,怎么實(shí)際地越界寫,我們只有寫一個(gè)數(shù)據(jù)改變內(nèi)存中某一個(gè)對(duì)象某一個(gè)域中的狀態(tài)才能進(jìn)一步擴(kuò)大我們的控制力。
我們創(chuàng)建一個(gè)新的Array,并且也往0x2上的一個(gè)對(duì)象,通過創(chuàng)建一個(gè)segment,這個(gè)segment和能產(chǎn)生OOB的segment大小是一致的,一模一樣,類型一模一樣。所以,它一定會(huì)出現(xiàn)在剛才越界的segment后面,我現(xiàn)在擁有了越界的能力,又把一個(gè)Array segment布置在它后面。第二,我們就可以修改它的length和size來實(shí)現(xiàn)真正的越界。
第四步,怎么改?這里面還是有限制,我們應(yīng)用過程中不難應(yīng)用。我直接往0xffffffff這個(gè)數(shù),其實(shí)是不行的(一會(huì)兒告訴大家為什么不行),我們所使用的方法是先調(diào)用一次Reverse函數(shù),然后在0x09的位置上賦值上0,然后減1,在JavaScriptArray里,在Edge里,它賦上0再減1,可以讓我們創(chuàng)造出一個(gè)32位最大的整形。如果直接賦值,超過0x7f的話,它會(huì)把它轉(zhuǎn)成對(duì)象,數(shù)據(jù)前面會(huì)有一個(gè)flag,這是一個(gè)小技巧。再調(diào)用一次Reverse可以寫后面的Array2的segment里的size。成功之后,我們發(fā)現(xiàn)終于可以讓一個(gè)size變大了,通過我們最開始非常細(xì)微的小缺陷,可以看到32位是最大的整形。
為什么說直接賦值不行?稀疏數(shù)值是用segment指針來維持segment之間的關(guān)系,如果往0x24處上寫一個(gè)值,它不會(huì)直接往Head的buffer后面寫,因?yàn)樗l(fā)現(xiàn)有Next的指針,我看一下當(dāng)前的size與我寫的Index是否相同,你大于它,那么我去nextsegment,他會(huì)沒有問題地寫到next segment里,另一個(gè)buffer里沒有越界。這種直接寫的方式不行就是因?yàn)檫@樣。現(xiàn)在我們擁有了一個(gè)Array的segment size是非常大的segment,它實(shí)際已經(jīng)擁有了越界的能力,我們寫什么就很重要了。我們可以往非常大的邊沿處寫上一個(gè)有一定限制的值,而不是所有的值,大于CFF整形的值是不行的,但我們擁有一個(gè)非常大的范圍空間寫的能力。我們經(jīng)過了五步,終于實(shí)現(xiàn)了在內(nèi)存空間中一個(gè)大范圍的,一定寫入值限制的能力,這已經(jīng)是很大的突破了。
第六步,要實(shí)現(xiàn)全內(nèi)存的讀寫,在64位地址空間中如果沒有全內(nèi)存的讀寫,后面的Matevition(音127:43)非常難以繞過。我選了另一個(gè)為什么選擇NativeIntArray,通過一些內(nèi)存布局,可以讓它出現(xiàn)我剛才所越界的,能訪問到的一個(gè)內(nèi)存中,我修改它的Array頭上length、Head.length以及head.size。為什么選擇NativeIntArray呢?這是一個(gè)非常重要的結(jié)構(gòu),并且它是要Inline head創(chuàng)建的這個(gè)NativeIntArray才行。當(dāng)我把這三個(gè)域都修改了之后,通過它就可以進(jìn)一步地實(shí)現(xiàn)一個(gè)的范圍的全數(shù)據(jù)銑鞋,剛才我們說寫的一點(diǎn)數(shù)據(jù)是有限制的,通過這個(gè)可以實(shí)現(xiàn)全數(shù)據(jù)寫入。我們又?jǐn)U大了一個(gè)可控的能力,改完之后就是這樣。
NativeIntArray實(shí)際是相對(duì)位置,4個(gè)G的全數(shù)據(jù)寫和讀,但它并沒有不能實(shí)現(xiàn)64位地址空間中所有內(nèi)存的讀寫,還需要配合上Array buffer,這是可以用于讀取基礎(chǔ)數(shù)據(jù)的類型。我們只要將這兩個(gè)域進(jìn)行修改就可以了,一個(gè)是長度,二個(gè)是緩沖區(qū)的指針。把長度變到最大,指針的高32位改成我們想寫入的地址,通過SetUint32這樣的API,它的第一個(gè)參數(shù)就是Offset,通過它,我們就可以設(shè)置我們要寫數(shù)據(jù)的低32位,后面就是32位完整的數(shù)據(jù),至此,我們就擁有了在64位進(jìn)程地址空間中任意地址的任意數(shù)據(jù)寫的能力,這個(gè)能力就非常得重要。讀就非常簡單,我們用Get Uint32這樣的API,也是傳上我們低32位的地址,就可以讀任意數(shù)據(jù)。
至此,我們通過6步將開始非常微小的缺陷變成了在Edge進(jìn)程中64位地址空間中任意地址的讀和寫任意數(shù)據(jù)的能力。擁有這個(gè)能力的情況下,我們還有很困難要面臨,實(shí)際在通常的漏洞利用過程中,我們會(huì)認(rèn)為,現(xiàn)在已經(jīng)達(dá)到了肯定能實(shí)現(xiàn)遠(yuǎn)程代碼執(zhí)行能力,但后面這些保護(hù)機(jī)制我們還是要有。
ASLR和DEP這兩個(gè)保護(hù)機(jī)制是非常古老的保護(hù)機(jī)制,在早期的軟件對(duì)抗過程中就已經(jīng)出現(xiàn)了,一是地址空間隨機(jī)化,二是數(shù)據(jù)不可執(zhí)行的保護(hù)機(jī)制。為什么我剛才選擇NativeInt就落在這兒,我們通常的信息泄露想要Bypass ASLR需要獲得兩種類型的地址,一種是模塊的遞減,可以通過泄露對(duì)象的虛函數(shù)表來實(shí)現(xiàn);第二是對(duì)象的地址,它在堆里,我們最好擁有一個(gè)結(jié)構(gòu)里,我所泄漏它里面數(shù)據(jù)的時(shí)候,它有個(gè)指向?qū)ο笞罱K的指針。NativeIntArray就恰巧滿足了這兩個(gè)條件,會(huì)為我們后面利用帶來非常大的方便。Show&表(音)的技術(shù)可以通過它來間接泄漏我們模塊的能力,后面的segment head指針可以用來幫助我們泄露object的地址。我們擁有了全內(nèi)存讀寫的能力可以解析進(jìn)程中任意對(duì)象,這是在我后面所講的保護(hù)機(jī)制出現(xiàn)之前,這實(shí)際是早期的應(yīng)用過程中,我們可以通過多層解析找到一個(gè)ShellCode的地址。現(xiàn)在內(nèi)存中可以找到任意對(duì)象,任意我們需要東西的地址。
DEP怎么過?早期非常簡單,可以通過ROP,因?yàn)樵缙趯?duì)這塊沒有保護(hù),我們可以通過ROP調(diào)用Virtual protect或者VirtualAlloc函數(shù)來實(shí)現(xiàn)修改內(nèi)存中的屬性,加上S-build way(音132:40)。這是早期的,后來不行了,因?yàn)楹竺嬗行碌谋Wo(hù)機(jī)制,我回介紹到方法。早期的時(shí)候是可以通過ROP修改內(nèi)存屬性,再執(zhí)行ShellCode,實(shí)際上在Windows10的RS2版本之前到這兒基本就行了。
微軟隨著不斷推出新的meetcation(音)就實(shí)現(xiàn)了后面三種保護(hù)機(jī)制CFG(Control Flow Guard)。
剛才講漏洞利用過程中講實(shí)現(xiàn)執(zhí)行ROP,必須要控制程序的可知性流程RIP,要想控制這個(gè)寄存器,通常早期比較簡單的方法是,我修改虛函數(shù)表,調(diào)用它的虛函數(shù)進(jìn)行間接控制。微軟知道之后推出了相應(yīng)的保護(hù)方案,用于保護(hù)程序間接跳轉(zhuǎn),防止攻擊者控制程序的執(zhí)行流程。因?yàn)殚g接跳轉(zhuǎn),早期的時(shí)候我們會(huì)套個(gè)虛函數(shù),有上面一個(gè)代碼。后面加上保護(hù)機(jī)制之后會(huì)出現(xiàn)后面的調(diào)用,這個(gè)調(diào)用會(huì)對(duì)我們所調(diào)用的函數(shù)進(jìn)行驗(yàn)證。邏輯是這樣,在我們程序編譯的時(shí)候,編譯器會(huì)將所有的剛才形式上的間接跳轉(zhuǎn)所調(diào)用的函數(shù)來創(chuàng)建一個(gè)Bitmap表,這個(gè)表里每一位代表每一個(gè)函數(shù)是否允許調(diào)用。當(dāng)你程序在運(yùn)行過程中,剛才那個(gè)函數(shù)會(huì)通過這個(gè)算法來驗(yàn)證你這個(gè)函數(shù)是否允許調(diào)用,不允許的話直接拋出異常,當(dāng)前進(jìn)程就退出了。我們想用這種方式來保護(hù)瀏覽器進(jìn)程的安全。
它的算法是這樣的,32位過程中,如果我想要調(diào)用一個(gè)函數(shù),它高三個(gè)字節(jié),24個(gè)bit,會(huì)用作我們bitmap的index,最低字節(jié)高5個(gè)bit,會(huì)把它當(dāng)作我dword里的位的index,就是哪一位。這里是01010,實(shí)際是個(gè)十進(jìn)制的10,它表示第10位,在這個(gè)bitmap里,這個(gè)第10位如果是0的話是不允許你調(diào)用的,如果是1它允許調(diào)用。這是CFG驗(yàn)證的一個(gè)方式。
怎么過呢?CFG只保護(hù)了程序中間接調(diào)用,在編譯過程中能分析出來的間接調(diào)用,但沒有保護(hù)棧的返回技術(shù),我們?cè)谡{(diào)用棧的時(shí)候會(huì)把參數(shù)壓棧,把當(dāng)前函數(shù)的代碼指針地址壓棧,函數(shù)返回的時(shí)候它又會(huì)抽回來。我們所使用的方法是,通過泄漏瀏覽器進(jìn)程中的棧地址,找到它的棧的范圍,左邊是我泄漏出來的,右邊是我調(diào)試器里的,一模一樣。把這個(gè)地址泄露出來之后,我們可以做這樣的事情,找到一些特殊的函數(shù),主動(dòng)調(diào)它。在棧中一定會(huì)存在這樣的函數(shù)內(nèi)的返回機(jī)制。因?yàn)槲覀儞碛辛巳珒?nèi)存讀寫信能力,可以在棧中搜索我們想要寫的函數(shù)的地址。我們可以通過修改棧中某一個(gè)特殊我們?cè)O(shè)計(jì)好的函數(shù)返回值,在這個(gè)函數(shù)reture的時(shí)候間接控制這個(gè)程序的執(zhí)行流程。最終Return的時(shí)候,Return的目的和代碼我們就完全可控了。
通過前面一個(gè)非常微小的漏洞,實(shí)現(xiàn)了全內(nèi)存讀寫,又進(jìn)一步繞過了CFG,我們可以獲得程序的控制流程,這已經(jīng)進(jìn)了一大步。因?yàn)闉g覽器都是在沙箱里的,沙箱的防護(hù)越來越強(qiáng)了。兩種方案,要么找沙箱的漏洞Bypass sandbox,要么找conode的漏洞來提權(quán)。實(shí)際現(xiàn)在要想實(shí)現(xiàn)沙箱的穿透,所需要的代碼量可能非常巨大大,特別是內(nèi)核提權(quán)的漏洞利用的開發(fā)。早期我們都會(huì)用ShellCode去load一個(gè)library,在Library里寫,這是一個(gè)比較方便的設(shè)計(jì)過程。現(xiàn)在微軟也知道這種方式,所以,推出了兩個(gè)新的保護(hù)方式,CIG以及ACG,就是要組織攻擊者過于容易地提權(quán),過于容易在瀏覽器內(nèi)執(zhí)行惡意代碼,我們雖然進(jìn)行了程序控制,但并不能非常方便地執(zhí)行惡意代碼。理論上,我們可以用ROP實(shí)現(xiàn)任意功能,但對(duì)純沙箱漏洞或內(nèi)核提權(quán)漏洞來說成本太高了,并且不便于移植,不同的版本更新都會(huì)有影響,包括漏洞利用的影響都會(huì)非常大。
CIG實(shí)際是個(gè)代碼簽名驗(yàn)證保護(hù),當(dāng)你加載到進(jìn)程中,通過LoadLibray這個(gè)API加載到進(jìn)程中,任意的模塊,所有的模塊都會(huì)對(duì)它進(jìn)行簽名驗(yàn)證,只有微軟的有效簽名模塊才被允許加載到進(jìn)程中。這個(gè)保護(hù)機(jī)制是通過這個(gè)函數(shù)SetProcessMitigationPolicy這樣的函數(shù)來進(jìn)行主動(dòng)加入的。
CIG保護(hù)機(jī)制的實(shí)現(xiàn),在用戶態(tài)和內(nèi)核態(tài)都有,用戶態(tài)比較簡單,在LoadLibray里來作為入口,內(nèi)核態(tài),到內(nèi)核的時(shí)候它會(huì)檢查,你所loadlibray鏡像簽名是不是合法,不是的話我就拒絕你。現(xiàn)在問題變成了,我們?cè)趺礃幽躭oadlibray一個(gè)沒有軟簽名的問題。剛才看到的實(shí)現(xiàn)非常簡單,LoadLibray API為入口,我們可以創(chuàng)建一個(gè)ShellCode,我們不調(diào)用Libray,用ShellCode實(shí)現(xiàn)整個(gè)鏡像在內(nèi)存中的加載,通過解決它每個(gè)節(jié),對(duì)齊,加載到內(nèi)存,修復(fù)它的導(dǎo)入到處表以及資源的偏移,在創(chuàng)建新的線程,在UNkey中,實(shí)現(xiàn)我們不調(diào)用它的Libray也能加載鏡像的的能力。這是我們?yōu)?017年Windows RSR準(zhǔn)備的方案之一,通過前面相關(guān)保護(hù)機(jī)制,之后到ShellCode,動(dòng)態(tài)的,任意的,非微軟簽名鏡像加載到當(dāng)前的內(nèi)存中。微軟也知道這樣的攻擊方法,所以緊接著又推出了ACG。這是一個(gè)比較強(qiáng)的保護(hù)。它實(shí)際是為了保護(hù)動(dòng)態(tài)可執(zhí)行代碼的創(chuàng)建,為了防護(hù)這樣的攻擊手段。我實(shí)際一定還是需要ShellCode,所以需要一段可執(zhí)行,并可寫的內(nèi)存,微軟這個(gè)保護(hù)機(jī)制就是為了防御這樣的攻擊方式,讓你沒辦法動(dòng)態(tài)地創(chuàng)建可執(zhí)行的內(nèi)存,并且也不可以修改已經(jīng)存在的這樣的內(nèi)存空間。
WINAPI也是這樣,主動(dòng)對(duì)進(jìn)程進(jìn)行設(shè)置的保護(hù)機(jī)制。它實(shí)現(xiàn)的方式是這樣,我們修改Windows中的內(nèi)存屬性主要是為了API,workalong(音142:20)和workcontect(音),這些作為入口進(jìn)入到內(nèi)核時(shí),會(huì)驗(yàn)證當(dāng)前進(jìn)程是否開了保護(hù)機(jī)制,如果開了,那么你所修改的內(nèi)存是不是允許;如果他發(fā)現(xiàn)你創(chuàng)建了任何可執(zhí)行屬性的內(nèi)存,它都會(huì)給你殺掉。
這樣我們剛才的方案就不行了,我們想創(chuàng)建一個(gè)ShellCode,動(dòng)態(tài)load包含了提權(quán)、沙箱穿越利用的DLL不行了,那么怎么辦呢?現(xiàn)在我們還有一個(gè)能力ROP,不能創(chuàng)建可執(zhí)行內(nèi)存,但我們依然可以依賴于現(xiàn)有可執(zhí)行代碼來完成一部分功能,完成什么功能呢?這是我在XVCore(音143:20)隨便找到的一些buget,通過rbx、rax、rcx,它能幫助實(shí)現(xiàn)什么功能?可以實(shí)現(xiàn)調(diào)用WindowsAPI的功能。為什么ROP實(shí)現(xiàn)完整的提權(quán)和穿沙箱的邏輯比較困難,因?yàn)閃indows版本不斷更新,新的保護(hù)機(jī)制又不斷推出,可能我們的利用都需要不斷地變化,如果你每次都通過修改ROP來實(shí)現(xiàn)的話,這是非常龐大的工作量,打Pwn2own比賽的前一天,微軟會(huì)推出補(bǔ)丁,我們只有不到24個(gè)小時(shí)的時(shí)間,怎么辦呢?我們一定需要通用的,在Windows推出新版本,代碼有比較大修改時(shí),我們依然能實(shí)現(xiàn)快速的適配解決方案,可以通過ROP來實(shí)現(xiàn)Windows API的調(diào)用,那一波的參數(shù),那一類型的參數(shù),以及我們能讀取到它的返回技術(shù),我們ROP只做這樣簡單的事情,別的什么都不干。實(shí)際在Windows平臺(tái)中,我們擁有了調(diào)用任意系統(tǒng)API能力的時(shí)候,就相當(dāng)于獲得了遠(yuǎn)程代碼執(zhí)行的能力,其他的都沒什么問題。
CFG的時(shí)候,我們可以控制程序的返回地址,ROP會(huì)切換到數(shù)據(jù)可控的內(nèi)存空間中,這里面保存了函數(shù)參數(shù),以及函數(shù)的返回,API函數(shù)的遞減,以及我保存這個(gè)API所調(diào)用之后的值,在這一過程,API網(wǎng)絡(luò)安全保存返回值之后再切回到原先的棧,所以,整個(gè)棧的影響非常小,什么數(shù)據(jù)都不被破壞,只是跳了一個(gè)棧堆而已,并且非常穩(wěn)定。在我測(cè)試來看,連續(xù)調(diào)用幾千個(gè)Windows系統(tǒng)API,不會(huì)造成任何的內(nèi)存損壞。我們還是找到了一個(gè)任意比較特殊的函數(shù),知道它在棧中就一份,并且這一份一定是由我們創(chuàng)建的,包括修改返回地址不只是ROP,處理函數(shù)的參數(shù)以及保存函數(shù)的返回值,最后我們?cè)侔褩G袚Q回去,達(dá)到正常代碼執(zhí)行流程繼續(xù)執(zhí)行的效果。
接著還是需要做大量工作,我們要想實(shí)現(xiàn)調(diào)用系統(tǒng)API,每個(gè)API都需要單獨(dú)實(shí)現(xiàn),我們把剛才所說的跨API通用功能先實(shí)現(xiàn)了,通過簡單的代碼就可以實(shí)現(xiàn)ReFill(音146:45)的函數(shù),Windows API函數(shù)的調(diào)用與調(diào)用正常的C++代碼是一樣的。我們通過JavaScript就可以實(shí)現(xiàn)。穿沙箱常見的是使用Com接口的缺陷,實(shí)際com這樣復(fù)雜的,面向?qū)ο蟮恼{(diào)用依然沒有問題,只要符合它的調(diào)用原理就可以了。(視頻)這是我們今年2月份做出的整套應(yīng)用,用的不是這個(gè)洞,因?yàn)檫@個(gè)漏洞已經(jīng)被補(bǔ)上了,是另外一個(gè)漏洞。里面所有madecation bycose(音147:30)在現(xiàn)在依然是有效的,只不過漏洞我們已經(jīng)提交給微軟,現(xiàn)在已經(jīng)修復(fù)了。之所以產(chǎn)生這個(gè)stress(音),是因?yàn)槲覀兇┥诚涞穆┒葱枰麄€(gè)過程十幾秒,不到二十秒的時(shí)間就可以實(shí)現(xiàn)在最新版的Windows10種,Edge瀏覽器也是最新的,只要你訪問一個(gè)鏈接,在十幾秒內(nèi),我就可以拿到當(dāng)前用戶的權(quán)限任意代碼的能力。這是它的權(quán)限,我們已經(jīng)穿透了它的沙箱,這是使用穿沙箱的漏洞,邏輯也非常復(fù)雜,可能需要幾百行C++代碼的能力,我們?nèi)鞘褂肑avaScript調(diào)用任意系統(tǒng),Windows系統(tǒng)中的API處理參數(shù),保存返回值實(shí)現(xiàn)的能力。
我整個(gè)演講到這里就結(jié)束了,大家有什么問題可以提問。
主持人潘柱廷:大家有沒有什么問題,里面可能需要有一定瀏覽器研究基礎(chǔ)的同學(xué)才能深入地聽明白。
Q:ACG的時(shí)候,你最后其實(shí)不調(diào)H-lock(音149:35)?你用的ROP調(diào)任意API效果??(149:40,聽不清)。
宋凱(exp-sky):我們穿沙箱只是舉了一個(gè)API的例子,如果要是他們相關(guān)漏洞,需要指定特定的comp對(duì)象和接口功能,包括組織一些BSTR這種特殊結(jié)構(gòu)的對(duì)象參數(shù),這是一個(gè)非常復(fù)雜的過程,ROP所做的功能,我實(shí)現(xiàn)調(diào)用提前穿沙箱的能力,中間所需要用到的API我全都可以調(diào)用,它只是作為調(diào)用API的小工具。可以理解為是這樣一個(gè)能力。
Q:ALSR內(nèi)存隨機(jī)化,除了看虛函數(shù)的返回地址,還有其他更好的方法嗎?
宋凱(exp-sky):這實(shí)際是看你的漏洞利用場(chǎng)景,因?yàn)槲覀冊(cè)谶@里面需要用ROP實(shí)現(xiàn)后面整套應(yīng)用,如果沒有泄露鏡像地址能力的話,很難找到ROP的具體位置。如果你不適用ROP,早期一段應(yīng)用時(shí)我們可以創(chuàng)建可執(zhí)行內(nèi)存,既然我們可以創(chuàng)建可執(zhí)行內(nèi)存,我們不需要泄漏??(151:29),也不需要這個(gè)表,直接調(diào)用ShellCode就實(shí)現(xiàn)所有功能,我們可以把提權(quán)代碼放在ShellCode里。針對(duì)不同的軟件和利用時(shí)的漏洞環(huán)境可能是不一樣的,我舉的這個(gè)例子只是在我這個(gè)漏洞環(huán)境中需要這樣。在一些漏洞環(huán)境中可能不需要這樣一種功能。
Q:剛才旁邊的朋友想問一下,還要學(xué)多少年才能進(jìn)玄武實(shí)驗(yàn)室?進(jìn)玄武實(shí)驗(yàn)室,需要具備哪些方面的知識(shí)?
宋凱(exp-sky):實(shí)際我們實(shí)驗(yàn)室是做面向?qū)嶋H應(yīng)用攻擊的研究以及防御研究,剛畢業(yè)的同學(xué),大家對(duì)比較感興趣,踏踏實(shí)實(shí)地做一些真正自己喜歡的事情,這樣可以大大提高你學(xué)習(xí)的速度,你越專注,你學(xué)習(xí)的東西越快,可以想到正常人想不到的一些點(diǎn)。要知道微軟或Google這種大廠商他們所雇的開發(fā)者都是業(yè)內(nèi)全球頂尖的,我們要找的漏洞實(shí)際就是要超越這些頂尖開發(fā)者所擁有的知識(shí)。我認(rèn)為,最重要的是找到自己喜歡的(方向),當(dāng)然安全所涉及到的領(lǐng)域太多了,我們實(shí)驗(yàn)室也包括硬件、移動(dòng)、Windows平臺(tái)、Linux平臺(tái)都會(huì)有同事在研究,在這么多領(lǐng)域情況下找到自己最喜歡的,專注地研究下去,一定可以做到比較好的水平。