一個補丁引發(fā)的RCE: 對CVE-2019-1208的深入分析
前言
CVE-2019-1208是趨勢科技的@elli0tn0phacker在今年6月發(fā)現(xiàn)的一個vbscript漏洞,報告中提到這個漏洞是通過補丁比對發(fā)現(xiàn)的,這引起了筆者的興趣。最近,筆者花了一些時間對該漏洞進行了比較詳細的研究。在這篇文章中,筆者將從漏洞成因、修復方案、利用編寫三個方面對該漏洞進行介紹。
讀者將會看到,代碼開發(fā)者是如何在修復舊漏洞時不經(jīng)意間引入新漏洞。在這個例子中,引入的還是一個非常嚴重的遠程代碼執(zhí)行漏洞。通過這個例子讀者也會發(fā)現(xiàn),有時候通過補丁比對就可以發(fā)現(xiàn)新漏洞。
該漏洞從2019年6月更新被引入,到2019年9月更新被修復,只存活了短短3個月,因此編寫這個漏洞的利用并無價值,筆者寫這個漏洞的利用只是為了概念驗證。
盡管微軟已經(jīng)在2019年8月的IE更新中全面禁用了vbscript,但出于安全性考慮,完整利用代碼不予公開。
?
漏洞成因
這是一個vbscript的UAF(Use After Free)漏洞,漏洞成因還要從微軟今年6月的補丁說起。
漏洞成因
微軟在2019年6月的vbscript更新中引入了下面幾個函數(shù):
?SafeArrayAddRef
?SafeArrayReleaseData
?SafeArrayReleaseDescriptor
?
引入SafeArrayAddRef的作用是為SafeArray提供一種類似引用計數(shù)的機制。
?
源碼中通過使用STL的 map將一些對象/數(shù)據(jù)指針(如pSafeArray和pvData)與一個int型的計數(shù)器進行綁定。
?
在VbsFilter和VbsJoin這兩個函數(shù)中,在調(diào)用實際的rtJoin和rtFilter前,會調(diào)用SafeArrayAddRef對相關指針的引用計數(shù)+1。調(diào)用完畢后,再調(diào)用SafeArrayReleaseData和SafeArrayReleaseDescriptor在map中將指針對應的計數(shù)-1,并將指針所對應的key從map中刪除。
?
開發(fā)者應該是用這種方式修復了一些UAF問題。但修復方案中沒有考慮到當Join/Filter傳入的數(shù)組中有類對象時,在Public Default Property Get這一潛在的回調(diào)中可以對數(shù)組進行操作(比如ReDim)。這樣,當調(diào)用完 rtJoin/rtFilter后返回VbsJoin/VbsFilter時,對應的pSafeArray/pvData指針已被更新,原先的設計是將之前已在Map中“注冊”的指針傳入后續(xù)的SafeArrayReleaseData/SafeArrayReleaseDescriptor進行引用計數(shù)減操作,但現(xiàn)在傳入SafeArrayReleaseData/SafeArrayReleaseDescriptor的指針均不在map中(因為被重新創(chuàng)建了)。這導致在調(diào)用RefCountMap
?
具體地,開發(fā)者借助RefCountMap類實現(xiàn)了一個“偽引用計數(shù)機制”,通過一個map
?
相關操作函數(shù)的聲明如下:
RefCountMap
了解了這些知識后,回過頭去理解@elli0tn0phacker報告中的Figure 5就會容易多了。
?
PoC分析
@elli0tn0phacker給出的poc大致如下:
由于漏洞的存在,我們知道arr(0) = 1語句執(zhí)行前arr已被釋放,而且從代碼中可以看到arr是在回調(diào)中被ReDim的。那么arr到底存在哪里?為什么arr(0) = 1索引的是ReDim后被釋放的SafeArray,而不是Redim前的SafeArray?
這就涉及到 vbscript虛擬機的相關知識。
卡巴斯基實驗室的Boris Larin曾寫過一篇關于vbscript虛擬機的文章,并且開源了相關的調(diào)試插件。
在文章中,作者對vbscript虛擬機進行了比較細致的介紹。vbscript的所有代碼都會先被編譯為P-Code,隨后通過CScriptRuntime::RunNoEH對所有P-Code進行解釋執(zhí)行,CScriptRuntime對象的成員變量中存儲著解釋所需的許多信息,比較重要的幾個如下:
借助調(diào)試插件,我們可以得到 PoC代碼編譯后的P-Code:
?以下是上述用到的部分指令對應的字節(jié)碼(全部指令請參考Boris的插件源碼):?
從P-Code中可以看出, arr(0) = 1這句對應的指令索引的是本地變量棧(OP_CallLclSt, 0x25),Call Join(arr)這句對應的指令索引的也是本地變量棧(OP_LocalAdr, 0x19),從兩個指令名稱中我們可以猜測arr被存儲在本地變量棧上。?
?
在IDA Pro中對vbscript!CScriptRuntime::RunNoEH進行逆向,我們來看一下上述兩個指令解釋分支的匯編代碼:?
上述兩個分支都調(diào)用了CScriptRuntime::PvarLocal方法,再來看一下CScriptRuntime::PvarLocal方法的實現(xiàn):
可以看到CScriptRuntime::PvarLocal接收一個索引,并且基于CScriptRuntime對象+0x28或0x2C處的值進行偏移運算。調(diào)試時發(fā)現(xiàn)PoC兩處對arr的操作索引均為1,所以存儲arr的地址為:
poi(pCScriptRuntime + 0x28) - 0x10*1? ?
上述分析驗證了上面對于指令作用的猜想,PoC中每次使用arr變量時,都會傳入對應的索引去本地變量棧中進行訪問。
?
明白了arr的存取原理后,我們可以清晰地在調(diào)試器中觀察arr的變化過程,從而理解整個UAF的過程。
?
筆者在開啟頁堆后對PoC進行了調(diào)試。我們先將斷點下到OP_LocalAdr指令的解釋分支,可以看到Join(arr)執(zhí)行時訪問到的arr,命中斷點時ebx即為CScriptRuntime,調(diào)試時arr從本地變量棧(ebx+0x28)進行索引,讀者請留意下圖中藍色高亮的指針,ReDim語句執(zhí)行后它會發(fā)生變化。
我們對上圖中高亮數(shù)據(jù)(SafeArray指針)所在的內(nèi)存下一個寫入斷點,觀察這個位置上數(shù)據(jù)的幾次變化過程。
?
第一次是在ReDim(OP_ArrNamReDim)執(zhí)行時,對之前arr的清理階段(OP_ArrNamReDim指令的解釋流程在后面“修復方案”一節(jié)中會進一步說明。):
第二次是在OP_ArrNamReDim執(zhí)行時,將新創(chuàng)建的arr復制到本地變量棧的對應內(nèi)存處,可以看到藍色高亮處的指針已經(jīng)發(fā)生變化,此時的SafeArray已經(jīng)變?yōu)閯倓倓?chuàng)建的二維數(shù)組。
最后,我們將斷點下到OP_CallLclSt的解釋分支,目的是斷在arr(0) = 1這句對arr的訪問過程,由于“漏洞成因”所描述的設計上的問題,此時本地變量棧上的arr已經(jīng)被釋放:
追蹤到的釋放棧回溯如下圖,讀者可以看到,這個不當?shù)尼尫耪怯捎赟afeArrayReleaseDescriptor傳入了未在map注冊的指針所導致。
通過以上調(diào)試,讀者應該可以清晰感受到整個Use After Free過程。
?
修復方案
清楚漏洞成因后,我們來看一下微軟在9月更新中是如何修復該漏洞的。筆者用Bidiff工具比對了8月更新和9月更新兩個vbscript.dll,發(fā)現(xiàn)在rtJoin(rtFilter均類似,下面只以rtJoin進行說明)函數(shù)中,在對數(shù)組內(nèi)的元素進行操作前后,加了一對SafeArrayLock/SafeArrayUnlock函數(shù):?
微軟采用對SafeArray加鎖的方式來修補這個由之前的補丁引入的問題。SafeArrayLock會令pSafeArr->cLocks的值+1。這樣,當在安裝9月補丁后再次打開PoC。由于前面的+1操作,就可以令ReDim指令無法得到正常執(zhí)行,我們來看一下具體的邏輯。
?
這里再引述一下上面提到的P-Code,可以看到ReDim arr(1, 1)這句語句對應的P-Code如下:?
筆者在調(diào)試器中跟了一遍OP_ArrNamReDim指令(0x0A) 的執(zhí)行邏輯,發(fā)現(xiàn)有如下幾個關鍵點:
有意思的是,調(diào)試前筆者以為這里的ReDim最終會調(diào)用oleaut32!SafeArrayRedim函數(shù),結(jié)果并沒有。
?
結(jié)合上述邏輯,當補丁中在操作Join傳入的數(shù)組前,SafeArrayLock令pSafeArr->cLocks從0變?yōu)?,從而在執(zhí)行ReDim arr(1, 1)對應的指令時,無法通過3.1.1這一步,新數(shù)組無法被創(chuàng)建,Join函數(shù)執(zhí)行完后本地變量棧中的數(shù)組指針不會得到更新,之前的UAF問題也就無從談起了。Filter函數(shù)的修復方案同上。
?
以下為上述過程中涉及到的函數(shù)調(diào)用及說明:
這個修復方案和CVE-2016-0189的修復方案思路一致。
?
利用編寫
@elli0tn0phacker在他的報告中已經(jīng)給出了這個漏洞的exploit編寫思路,但沒有公布完整代碼。作為概念驗證,筆者親手編寫了對應的exploit,以下對部分細節(jié)進行說明。
?
偽造超長數(shù)組
通過觸發(fā)漏洞,可以得到一塊大小為0x30的空閑內(nèi)存。借助堆的特性,如果在Join函數(shù)執(zhí)行完后立即申請一些字符串長度為(0x30 - 4)的BSTR對象,就可以實現(xiàn)對被釋放內(nèi)存的占位。減4是因為BSTR的字符串前面還有4字節(jié)的長度域,會一并申請。
實踐證明這里的操作還是比較簡單的,并不需要過多的堆風水技巧,下面是一個可以成功占位的代碼示例:
占位后,因為筆者已經(jīng)在字符串中構造了假的超長數(shù)組,當下次訪問arr時,成功占位的字符串會被解釋為SafeArray結(jié)構體,從而得到一個基地址為0,元素個數(shù)為0x7fffffff,元素大小為1的超長數(shù)組。
?
任意地址讀取
這部分,以及如何構造一塊可讀寫內(nèi)存的步驟請參考@elli0tn0phacker的報告,相關步驟實現(xiàn)起來非常簡單,這里不再重復敘述。
?
Bypass ASLR
在前面的基礎上,就可以泄露一個指針對象以繞過ASLR,這里筆者采用的方法和和CVE-2019-0752一樣,泄露一個Scripting.Dictionary對象的虛表指針,具體操作如下:
?
虛函數(shù)劫持
若PoC要在windows 10上執(zhí)行,必須要繞過CFG。筆者最終采用了@elli0tn0phacker在他報告中提到的方法,即對CVE-2019-0752的利用方式稍作改動:
1.借助BSTR復制并偽造一個假的Dictionary虛表(fake_vtable),并改寫Dictionary.Exists函數(shù)指針為kernel32!WinExec,由于kernel32!WinExec是系統(tǒng)自帶函數(shù),因此可以繞過CFG檢測
2.借助BSTR復制并偽造一個假的Dictionary對象(fake_dict),將虛表替換為上述的假虛表,將WinExec的命令行參數(shù)寫入虛表指針后4字節(jié)開始的地址
3.將假的Dictionary對象所對應BSTR的type設為0x09,使之成為一個對象(VT_DISPATCH)
4.調(diào)用fake_dict.Exists,使控制流導向WinExec函數(shù),命令行參數(shù)在步驟2中已經(jīng)構造好
?
這個過程的示例代碼如下:
利用約束
這個漏洞利用在任意地址寫上有一些受限條件,@elli0tn0phacker已在他的報告中提到,這里也不再重復敘述。
?
這里提一個筆者編寫利用時遇到的問題,筆者一開始是在windows7 sp1 x86環(huán)境下寫的利用,代碼全部寫完后發(fā)現(xiàn)計算器無法彈出,一番調(diào)試后發(fā)現(xiàn),傳入WinExec函數(shù)的命令行參數(shù)無法得到正常解釋,原因也很簡單,來看一下某次win7調(diào)試時最終傳給WinExec的參數(shù):
出于利用構造的約束條件,命令行參數(shù)的前4個字符是由前面?zhèn)卧斓奶摫淼牡刂方忉尪鴣恚@種情況下很容易造成前4個字符里面有多余字符,因此WinExec也就不能按預期執(zhí)行后續(xù)的命令行。筆者一開始想到的將虛表偽造到0x20202020這個地址,這樣命令行參數(shù)的前4個字符可以被解釋為空格,不會影響整個命令行的解釋。但該漏洞中對指定地址的連續(xù)寫是受限的,筆者最終放棄了這個思路。
?
后來筆者將未加修改的exploit在win10環(huán)境試了一下,發(fā)現(xiàn)計算器可以成功彈出,以下為某次在win10下調(diào)試得到的參數(shù)及偽造的虛函數(shù)表:
筆者推測win10和win7下進程創(chuàng)建相關函數(shù)對命令行參數(shù)的處理存在一些差異,win10上的容錯性更高一點。
?
代碼執(zhí)行
最終,筆者成功在windows 10 1709 x86系統(tǒng)的2019年8月全補丁環(huán)境上彈出一個計算器:
?
參考資料
《Delving deep into VBScript》
《From BinDiff to Zero-Day: A Proof of Concept Exploiting CVE-2019-1208 in Internet Explorer》
《RCE WITHOUT NATIVE CODE: EXPLOITATION OF A WRITE-WHAT-WHERE IN INTERNET EXPLORER》