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