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