CVE-2021-21949
An improper array index validation vulnerability exists in the JPEG-JFIF Scan header parser functionality of Accusoft ImageGear 19.10. A specially-crafted file can lead to an out-of-bounds write and potential code exectuion. An attacker can provide a malicious file to trigger this vulnerability.
Accusoft ImageGear 19.10
ImageGear - https://www.accusoft.com/products/imagegear-collection/
9.8 - CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
CWE-129 - Improper Validation of Array Index
The ImageGear library is a document-imaging developer toolkit that offers image conversion, creation, editing, annotation and more. It supports more than 100 formats such as DICOM, PDF, Microsoft Office and others.
A specially-crafted JPEG file can lead to a stack-based buffer overflow in the JPEG-JFIF progressive image parser, due to a improper array index validation vulnerability, which leads to a type confusion, combined with a numeric range comparison without minimum check.
Trying to load a malicious JPEG file, we end up in the following situation:
(2894.2098): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
eax=ffffac2c ebx=80000000 ecx=6f57e46c edx=0000ff20 esi=ffffed84 edi=0000f896
eip=6f4451e9 esp=0019f924 ebp=0019fa8c iopl=0 nv up ei ng nz na po cy
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00010283
igCore19d!IG_mpi_page_set+0xb91b9:
6f4451e9 668994457cffffff mov word ptr [ebp+eax*2-84h],dx ss:002b:00195260=????
The access violation take place at [4]
in the process_jpeg_progressive
function:
void process_jpeg_progressive
(jpeg_dec *jpeg_dec,SOF_object *SOF,short restart_interval,int max_X_sampling,
int max_Y_sampling)
{
[...]
LOOP_COMPONENT_START:
if (0 < (int)comp_idx) {
comp_idx_ = 0;
cur_SOF = SOF;
do {
temp_var__ = comp_idx_ * 0x50;
component_obj = *cur_SOF->nr_component_buffer_data + comp_idx_;
y_idx = 0;
compo_size_times_comp_idx = temp_var__;
if (0 < *(int *)(&component_obj->field_0x0 + 0xe)) {
do {
/* subsampling_Y ^ */
x_idx = 0;
if (0 < (int)(component_obj->component_values).subsampling_X) {
local_EDI_2224 = component_array[comp_idx_];
local_fc = local_EDI_2224;
do {
OS_memcpy(stack_image_row_temp,
(void *)(((local_EDI_2224->Y_times_height_idx_div_Y_MAX + y_idx) *
local_EDI_2224->related_standardized_width + x_idx +
local_EDI_2224->probably_counter) * 0x80 +
local_EDI_2224->probably_data_ptr),0x80);
SOS_Ss__ = (short)SOS_Ss;
if ((ushort)SOS_Ah == 0) {
[...]
SOS_Ss_related = (short)SOS_Ss_plus_done;
zig_zag_plus_SOS_Ss = (word *)(JPEG_ZIGZAG_MAP + SOS_Ss_related);
if (flag_to_enter_the_crash_branch == 0) {
huffman_AC_table_obj = *(huffman_table_struct **)
((int)&(*SOF->nr_component_buffer_data)[0].component_values.
huffman_AC_Table + compo_size_times_comp_idx); [1]
PRE_huffman_code_idx_buff = &huffman_AC_table_obj->PRE_huffman_code_idx_buff;
PRE_huffman_code_length_buff = &huffman_AC_table_obj->PRE_huffman_code_length_buff;
PRE_huffman_code_next_elem_length = (byte *)&huffman_AC_table_obj->PRE_huffman_code_next_elem_length;
raw_AC_table_values = (ushort *)huffman_AC_table_obj->raw_values;
parsed_huffman_code = huffman_AC_table_obj->parsed_huffman_code;
while ((local_EDI_2224 = local_fc, SOS_Ss_related < 0x40 &&
((short)SOS_Ss_plus_done <= (short)SOS_Se))) {
if (((int)bit_read < 0x10) &&
(temp_var_ = (byte *)::read_n_bytes((io_buffer *)&io_buff_,8,&read_n_bytes),
temp_var_ != (byte *)0x0)) {
[... read the scan data and update a variable called dword_parsed_sum ...]
}
huffman_code = (ushort)parsed_huffman_code[dword_parsed_sum >> 0x15];
[... update variables and based on the huffman table the dword_parsed_sum and temp_var__ values ...]
if ((dword_parsed_sum & 0xffffff00) == 0) {
huffman_code = raw_AC_table_values[dword_parsed_sum];
}
else {
huffman_code = raw_AC_table_values[PRE_huffman_code_length_buff[temp_var__]];
}
offset_source_shifted = (short)huffman_code >> 4; [2]
_offset_buffer_idx = (uint)offset_source_shifted;
local_a8 = huffman_code & 0xf;
local_a4 = comp_idx;
[... calculate calcualted_value and other values ...]
if (flag_to_enter_the_crash_branch != 0) goto SKIP_WRITE;
zig_zag_accessing_offset = (short)((int)SOS_Ss_plus_done + _offset_buffer_idx);
if ((zig_zag_accessing_offset <= (short)SOS_Se) &&
(zig_zag_accessing_offset < 0x40)) { [3]
stack_image_row_temp[(short)zig_zag_plus_SOS_Ss[(short)_offset_buffer_idx]] =
(ushort)calcualted_value << ((byte)SOS_Al & 0x1f); [4]
}
SOS_Ss_plus_done = (byte *)((int)SOS_Ss_plus_done + _offset_buffer_idx + 1);
zig_zag_plus_SOS_Ss = zig_zag_plus_SOS_Ss + (short)_offset_buffer_idx + 1;
SOS_Ss_related = (short)SOS_Ss_plus_done;
dword_parsed_sum = local_a4;
}
}
else {
[...]
}
}
else {
[...]
}
OS_memcpy((void *)(((local_EDI_2224->Y_times_height_idx_div_Y_MAX + y_idx) *
local_EDI_2224->related_standardized_width + x_idx +
local_EDI_2224->probably_counter) * 0x80 +
local_EDI_2224->probably_data_ptr),stack_image_row_temp,0x80);
x_idx = x_idx + 1;
dword_parsed_sum = local_a4;
} while (x_idx < *(int *)((int)&(*SOF->nr_component_buffer_data)[0].
component_values.subsampling_X +
compo_size_times_comp_idx));
bit_after_SOS_read_2 = bit_read;
cur_SOF = SOF;
temp_var__ = compo_size_times_comp_idx;
}
[...]
} while (y_idx < (int)(component_obj->component_values).subsampling_Y);
}
[...]
}
This access violation is originated in the parse_SOS_SOF
function:
AT_ERRCOUNT
parse_SOS_SOF(jpeg_dec *jpeg_dec,SOS_From_FILE *SOS_From_FILE,SOF_object *param_3,int param_4,
SOS_object *output,jpeg_component_table_SOS **param_6,SOS_From_FILE **param_7,
dword *param_8)
{
[...]
SOS_data = SOS_From_FILE->data_marker;
[...]
nr_comp = (uint)SOS_data->nr_comp;
output->nr_comp = nr_comp;
[...]
jpeg_component_table = (jpeg_component_table_SOS *)AF_memm_alloc(uVar1,nr_comp * 0x50);
if (jpeg_component_table == (jpeg_component_table_SOS *)0x0) {
AVar4 = AF_err_record_set("..\\..\\..\\..\\Common\\Formats\\jpeg_dec.c",0x536,-1000,0,0,0,
(LPCHAR)0x0);
return AVar4;
}
OS_memset(jpeg_component_table,0,output->nr_comp * 0x50);
compnents_data = (SOS_parsed_comp *)AF_memm_alloc(uVar1,output->nr_comp * 0xc);
output->parsed_comp = compnents_data;
if (compnents_data == (SOS_parsed_comp *)0x0) {
local_1c = AF_err_record_set("..\\..\\..\\..\\Common\\Formats\\jpeg_dec.c",0x53d,-1000,0,0,0,
(LPCHAR)0x0);
pSVar10 = (SOS_From_FILE *)0x0;
}
else {
OS_memset(compnents_data,0,output->nr_comp * 0xc);
if (0 < (int)output->nr_comp) {
parsed_comp_idx = 0;
iVar11 = 0;
current_SOS_data = (SOS_entry *)SOS_data;
do {
SOS_entry_shifted = ¤t_SOS_data->SOS_ENTRY+1;
iVar11 = iVar11 + 1;
*(uint *)(&output->parsed_comp->component_id + parsed_comp_idx) = (uint)current_SOS_data->component_id;
*(uint *)(&output->parsed_comp->DC_table_idx + parsed_comp_idx) =
(uint)(SOS_entry_shifted->DC|AC >> 4); [5]
*(uint *)(&output->parsed_comp->AC_table_idx + parsed_comp_idx) =
SOS_entry_shifted->DC|AC & 0xf; [6]
parsed_comp_idx = parsed_comp_idx + 0xc;
current_SOS_data = (SOS_entry *)SOS_entry_shifted;
} while (iVar11 < (int)output->nr_comp);
}
iVar8 = 0;
SOS_comp_num = output->nr_comp;
SOF_comp_num = (param_3->SOF_header).size_ImageTableComponent;
[...]
comp_idx = 0;
if (0 < (int)SOS_comp_num) {
jpeg_component = &jpeg_component_table->component_values;
SOS_comp_idx = 0;
do {
[...]
jpeg_component[-1].huffman_DC_Table =
(dword)jpeg_dec->HuffmanDC_TableSymbols_
[*(int *)(&output->parsed_comp->DC_table_idx + SOS_comp_idx)]; [7]
jpeg_component->huffman_AC_Table =
(dword)jpeg_dec->HuffmanAC_TableSymbols
[*(int *)(&output->parsed_comp->AC_table_idx + SOS_comp_idx)]; [8]
jpeg_component->subsampling_X =
(*(param_3->SOF_header).ImageTableComponent)[dVar5].horizontalSamplingFactor;
jpeg_component->subsampling_Y =
(*(param_3->SOF_header).ImageTableComponent)[dVar5].verticalSamplingFactor;
[...]
SOS_comp_idx = SOS_comp_idx + 0xc;
jpeg_component = jpeg_component + 4;
comp_idx = comp_idx + 1;
} while (comp_idx < (int)output->nr_comp);
}
[...]
}
This function, among other things, is responsible for associating the image components with the correct AC
and DC
parsed huffman tables, called from now on, respectively, AC_t
and DC_t
. The function that parses the huffman tables, from this point called parse_huffman
, parses the AC_t
and DC_t
checking that the several specification constraints are respected. For instance, in the parse_huffman
function is ensured that, per each table type, the identifier can only range from 0
to 3
. After the tables are parsed, if any, their pointer are placed sequentially into a structure.
Following a schematization of the structure’s memory layout:
0x00 XXXX YYYY XXXX YYYY
...
0x28 DC_t0 DC_t1 DC_t2 DC_t3
0x38 AC_t0 AC_t1 AC_t2 AC_t3
...
The number after DC_t
and AC_t
is the table’s specified identifier. This struct, that also contains data and pointers not related to the huffman tables, is used at [7]
and [8]
to associate the parsed component element with the correct parsed DC_t
and AC_t
. This association is perfomed using the parsed SOS
’s DC
and AC
component values, taken respectively at [5]
and [6]
. These values specify the element indexes of the huffman tables to be used. The problem is that these variables are four bits long, which means they can range from 0
to 15
. These values are used as an array index to get the specified element, but because no check is perfomed on the value of AC
or DC
it is possible to select elements that are not parsed huffman tables. This will break the assumption ensured by the various checks in parse_huffman
.
The instruction used at [8]
is mov eax,dword ptr [esi+eax*4+38h]
:
0:000> dd esi+0x38
0a300f98 00000000 00000000 00000000 00000000
0a300fa8 0a960720 6f453880 00000000 00000000
0a300fb8 00000000 00000000 00000002 00000000
0a300fc8 00000000 00000f0f 00000000 00000000
0a300fd8 00000002 0ae30fd0 00000001 00000000
0a300fe8 00000000 00000000 00000000 00000015
0a300ff8 00000000 d0d0d0d0 ???????? ????????
0a301008 ???????? ???????? ???????? ????????
At esi+0x38
the first of the four possible parsed AC_t
is located. No AC_t
were specified in this example. Instead, eax
contains the specified SOS
’s AC
value:
0:000> r eax
eax=00000005
The eax
value is used, starting from esi+0x38
, as an element index. So, the fifth element is taken. In this case the element is 0x6f453880
, a function pointer.
At [1]
the associated huffman pointer is loaded and used to calculate, among the other thing, the index value at [2]
used for accesing the JPEG_ZIGZAG_MAP
buffer, offset by the SOS
’s Ss
value. The JPEG_ZIGZAG_MAP
is a buffer of short with 64 elements, and the biggest value is 0x3F
. The JPEG_ZIGZAG_MAP
buffer is used to get the correct index to access stack_image_row_temp
, a stack buffer with 64 short elements. Because the biggest element in JPEG_ZIGZAG_MAP
is 0x3F
(decimal 63
) this would ensure that the stack_image_row_temp
buffer is accessed at most to its last element. But, because the huffman table assumptions are broken it is possible for _offset_buffer_idx
, the pointer that accesses the JPEG_ZIGZAG_MAP
, to have a negative value. The negative value would then bypass the checks perfomed at [3]
because they are a signed comparison that only check the maximum range values. This allows us to obtain values outside the JPEG_ZIGZAG_MAP
buffer range, and consequentially write out of the stack_image_row_temp
bounds.
Here are the relevant assembly instructions related to the check at [3]
and the point at which the crash happens at [4]
:
mov edx, dword ptr [ebp-0B0h] ; _offset_buffer_idx
[...]
mov ecx, dword ptr [ebp-0A8h] ; JPEG_ZIGZAG_MAP + Ss
mov edi, dword ptr [ebp-9Ch] ; SOS_Ss_plus_done, it should be equal to Ss
movsx eax, dx ; off_idx_16_sign_ext = (take 16 sign extended bits) _offset_buffer_idx
add edi, edx ; calculated_offset = _offset_buffer_idx + SOS_Ss_plus_done
lea ecx, [ecx+eax*2] ; zig_zag_value_ptr =
; JPEG_ZIGZAG_MAP + Ss + off_idx_16_sign_ext * 2
mov eax, dword ptr [ebp-11Ch] ; SOS_Se
mov dword ptr [ebp-0A8h], ecx ;
cmp di, ax ; (calculated_offset & 0xffff) < SOS_Se
jg SKIP_WRITE ; if false go to SKIP_WRITE
cmp di, 40h ; (calculated_offset & 0xffff) <= 0x40, otherwise SKIP_WRITE
jge SKIP_WRITE ; if false go to SKIP_WRITE
mov cx, word ptr [ebp-10Ch] ; SOS_Al
mov dx, word ptr [ebp-94h] ; calculated_value = value to be written into the buffer
shl dx, cl ; calculated_value = calculated_value << SOS_Al
mov ecx, dword ptr [ebp-0A8h] ; load zig_zag_value_ptr
movsx eax, word ptr [ecx] ; zig_zag_index_value = (take 16 sign extended bits) dereference zig_zag_value_ptr
mov word ptr [ebp+eax*2-84h], dx ; stack_image_row_temp[zig_zag_index_value*2] = calculated_value
In the assembly above, it is possible to see that the perfomed checks are signed, and only the maximum of the allowed ranges are checked. This allows negative indexes to pass the check and reach the JPEG_ZIGZAG_MAP
access. Furthermore, because the buffer are shorts, the index value is multiplied by two in order to seek the correct element. This would allow a negative index to transform into a positive one, if the index provided is small enough to cause an overflow. A potential attacker would be able to control the index used to access JPEG_ZIGZAG_MAP
, and thus be able to obtain as index for stack_image_row_temp
a value outside the range of the buffer iteself, giving the capability to write outside that stack buffer.
0:000> !analyze -v
*******************************************************************************
* *
* Exception Analysis *
* *
*******************************************************************************
KEY_VALUES_STRING: 1
Key : AV.Fault
Value: Write
Key : Analysis.CPU.mSec
Value: 2952
Key : Analysis.DebugAnalysisManager
Value: Create
Key : Analysis.Elapsed.mSec
Value: 10202
Key : Analysis.Init.CPU.mSec
Value: 375
Key : Analysis.Init.Elapsed.mSec
Value: 27637
Key : Analysis.Memory.CommitPeak.Mb
Value: 135
Key : Timeline.OS.Boot.DeltaSec
Value: 155289
Key : Timeline.Process.Start.DeltaSec
Value: 27
Key : WER.OS.Branch
Value: rs5_release
Key : WER.OS.Timestamp
Value: 2018-09-14T14:34:00Z
Key : WER.OS.Version
Value: 10.0.17763.1
Key : WER.Process.Version
Value: 1.0.1.1
NTGLOBALFLAG: 2100000
APPLICATION_VERIFIER_FLAGS: 0
APPLICATION_VERIFIER_LOADED: 1
EXCEPTION_RECORD: (.exr -1)
ExceptionAddress: 6f4451e9 (igCore19d!IG_mpi_page_set+0x000b91b9)
ExceptionCode: c0000005 (Access violation)
ExceptionFlags: 00000000
NumberParameters: 2
Parameter[0]: 00000001
Parameter[1]: 00195260
Attempt to write to address 00195260
FAULTING_THREAD: 00002098
PROCESS_NAME: Fuzzme.exe
WRITE_ADDRESS: 00195260
ERROR_CODE: (NTSTATUS) 0xc0000005 - The instruction at 0x%p referenced memory at 0x%p. The memory could not be %s.
EXCEPTION_CODE_STR: c0000005
EXCEPTION_PARAMETER1: 00000001
EXCEPTION_PARAMETER2: 00195260
STACK_TEXT:
WARNING: Stack unwind information not available. Following frames may be wrong.
0019fa8c 6f4400a0 0adb6f60 0019fab4 00000000 igCore19d!IG_mpi_page_set+0xb91b9
0019fb14 6f4574d5 00000002 6f453160 0a27c720 igCore19d!IG_mpi_page_set+0xb4070
0019fb30 6f45734a 0a27c720 0adb6f60 0000ffda igCore19d!IG_mpi_page_set+0xcb4a5
0019fb54 6f455161 0a27c720 0adb6f60 0019fb7c igCore19d!IG_mpi_page_set+0xcb31a
0019fb74 6f456b7a 0019ffc2 1000001d 0a921f70 igCore19d!IG_mpi_page_set+0xc9131
0019fbb4 6f3613d9 1000001d 0a921f70 00000001 igCore19d!IG_mpi_page_set+0xcab4a
0019fbec 6f3a08d7 00000000 0a921f70 0019fc3c igCore19d!IG_image_savelist_get+0xb29
0019fe68 6f3a0239 00000000 0019ff10 00000001 igCore19d!IG_mpi_page_set+0x148a7
0019fe88 6f335757 00000000 0019ff10 00000001 igCore19d!IG_mpi_page_set+0x14209
0019fea8 00402219 0019ff10 0019febc 00000001 igCore19d!IG_load_file+0x47
0019fec0 00402524 0019ff10 05267fe0 051cdf50 Fuzzme!fuzzme+0x19
0019ff28 0040668d 00000005 051c6f70 051cdf50 Fuzzme!fuzzme+0x324
0019ff70 76f20419 00265000 76f20400 0019ffdc Fuzzme!fuzzme+0x448d
0019ff80 772f72ed 00265000 369f943e 00000000 KERNEL32!BaseThreadInitThunk+0x19
0019ffdc 772f72bd ffffffff 773165d3 00000000 ntdll!__RtlUserThreadStart+0x2f
0019ffec 00000000 00406715 00265000 00000000 ntdll!_RtlUserThreadStart+0x1b
STACK_COMMAND: ~0s ; .cxr ; kb
SYMBOL_NAME: igCore19d!IG_mpi_page_set+b91b9
MODULE_NAME: igCore19d
IMAGE_NAME: igCore19d.dll
FAILURE_BUCKET_ID: INVALID_POINTER_WRITE_AVRF_c0000005_igCore19d.dll!IG_mpi_page_set
OS_VERSION: 10.0.17763.1
BUILDLAB_STR: rs5_release
OSPLATFORM_TYPE: x86
OSNAME: Windows 10
IMAGE_VERSION: 19.10.0.0
FAILURE_ID_HASH: {39ff52ad-9054-81fd-3e4d-ef5d82e4b2c1}
Followup: MachineOwner
---------
2021-09-22 - Initial contact
2021-09-23 - Vendor acknowledged and and confirmed under review with engineering team
2021-11-30 - 60 day follow up
2021-12-01 - Vendor advised release planned for Q1 2022
2021-12-07 - 30 day disclosure extension granted
2022-01-06 - Final disclosure notification
2022-02-23 - Public disclosure
Discovered by Francesco Benvenuto of Cisco Talos.