Headline
CVE-2021-21938: TALOS-2021-1367 || Cisco Talos Intelligence Group
A heap-based buffer overflow vulnerability exists in the Palette box parser functionality of Accusoft ImageGear 19.10. A specially-crafted file can lead to code execution. An attacker can provide a malicious file to trigger this vulnerability.
Summary
A heap-based buffer overflow vulnerability exists in the Palette box parser functionality of Accusoft ImageGear 19.10. A specially-crafted file can lead to code execution. An attacker can provide a malicious file to trigger this vulnerability.
Tested Versions
Accusoft ImageGear 19.10
Product URLs
ImageGear - https://www.accusoft.com/products/imagegear-collection/
CVSSv3 Score
9.8 - CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
CWE
CWE-193 - Off-by-one Error
Details
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 2000 file can lead to a heap-based buffer overflow in the Palette box parser, due to a wrongly-sized heap buffer caused by an off-by-one error.
Trying to load a malformed JPEG 2000 file, we end up in the following situation:
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
eax=0adacffc ebx=00000003 ecx=0000000f edx=00000000 esi=0adacfc0 edi=0ace9000
eip=6ebce13d esp=0019fac0 ebp=0019fae0 iopl=0 nv up ei pl nz na pe nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00010206
MSVCR110!memcpy+0x21e:
6ebce13d f3a5 rep movs dword ptr es:[edi],dword ptr [esi]
Where the destination buffer has the following information:
0:000> !ext.heap -p -a edi
address 0af11000 found in
_DPH_HEAP_ROOT @ 2cb1000
in busy allocation ( DPH_HEAP_BLOCK: UserAddr UserSize - VirtAddr VirtSize)
d6b0d34: af10fc0 40 - af10000 2000
? Fuzzme!fuzzme+11f00
6f08ab40 verifier!AVrfDebugPageHeapAllocate+0x00000240
7793a65b ntdll!RtlDebugAllocateHeap+0x00000039
778dff98 ntdll!RtlpAllocateHeap+0x00051808
7788e5e0 ntdll!RtlpAllocateHeapInternal+0x00001280
7788d34e ntdll!RtlAllocateHeap+0x0000003e
6ebcdaff MSVCR110!malloc+0x00000049
6ebcdba7 MSVCR110!operator new+0x0000001d
6eda130b igCore19d!IG_mpi_page_set+0x000052db
6ed656f1 igCore19d!IG_comm_is_comp_exist+0x000036a1
6ed48aca igCore19d!IG_warning_set+0x000018da
6e30e16e igJPEG2K19d!CPb_JPEG2K_init+0x000094ae
6e30fe07 igJPEG2K19d!CPb_JPEG2K_init+0x0000b147
6e31106c igJPEG2K19d!CPb_JPEG2K_init+0x0000c3ac
6e3070a6 igJPEG2K19d!CPb_JPEG2K_init+0x000023e6
6e30711e igJPEG2K19d!CPb_JPEG2K_init+0x0000245e
6ed713d9 igCore19d!IG_image_savelist_get+0x00000b29
6edb08d7 igCore19d!IG_mpi_page_set+0x000148a7
6edb0239 igCore19d!IG_mpi_page_set+0x00014209
6ed45757 igCore19d!IG_load_file+0x00000047
00402219 Fuzzme!fuzzme+0x00000019
00402524 Fuzzme!fuzzme+0x00000324
0040668d Fuzzme!fuzzme+0x0000448d
75330419 KERNEL32!BaseThreadInitThunk+0x00000019
778b72ed ntdll!__RtlUserThreadStart+0x0000002f
778b72bd ntdll!_RtlUserThreadStart+0x0000001b Instead, the source buffer:
0:000> !ext.heap -p -a esi
address 0ae4efc0 found in
_DPH_HEAP_ROOT @ 2cb1000
in busy allocation ( DPH_HEAP_BLOCK: UserAddr UserSize - VirtAddr VirtSize)
adb1e6c: ae4ef80 7c - ae4e000 2000
? Fuzzme!fuzzme+11f00
6f08ab40 verifier!AVrfDebugPageHeapAllocate+0x00000240
7793a65b ntdll!RtlDebugAllocateHeap+0x00000039
778dff98 ntdll!RtlpAllocateHeap+0x00051808
7788e5e0 ntdll!RtlpAllocateHeapInternal+0x00001280
7788d34e ntdll!RtlAllocateHeap+0x0000003e
6ebcdaff MSVCR110!malloc+0x00000049
6ed964de igCore19d!AF_memm_alloc+0x0000001e
6e30e7ed igJPEG2K19d!CPb_JPEG2K_init+0x00009b2d
6e30e5eb igJPEG2K19d!CPb_JPEG2K_init+0x0000992b
6e310e34 igJPEG2K19d!CPb_JPEG2K_init+0x0000c174
6e3047be igJPEG2K19d+0x000747be
6e37e2ad igJPEG2K19d!CPb_JPEG2K_init+0x000795ed
The information above clearly shows that the destination buffer is smaller than the source one. The access violation takes place during the parsing of the Palette box, in the following function:
int FUN_73ebe120(HIGDIBINFO *LPHIGDIBINFO,int enumIGColorSpaceIDs,int width,int heigth,
int channelCount,AT_INT *channelDepths,IGDIBStd *IGDIBStd,int sizePalette,
AT_RESOLUTION *lpResolution,undefined4 lphdib)
{
int iVar1;
size_t _Size;
void *_Dst;
if ((((enumIGColorSpaceIDs != 0) && (0 < width)) && (0 < heigth)) && (0 < channelCount)) {
iVar1 = DIB_info_create(LPHIGDIBINFO,width,heigth,enumIGColorSpaceIDs,channelCount,channelDepths
);
if (iVar1 == 0) {
DIB_resolution_set(*LPHIGDIBINFO,lpResolution);
if ((char)enumIGColorSpaceIDs == '\x03') {
iVar1 = DIB_palette_alloc(*LPHIGDIBINFO); [1]
if (iVar1 != 0) {
return iVar1;
}
_Size = sizePalette << 2;
_Dst = (void *)DIB_palette_pointer_get(*LPHIGDIBINFO);
memcpy(_Dst,IGDIBStd,_Size); [3]
}
iVar1 = (*(code *)PTR_73f8874c)(*LPHIGDIBINFO,lphdib);
}
return iVar1;
}
return 0;
}
The memory violation takes place in [3], and the allocation of the wrongly-sized buffer takes place in [1] in the function DIB_palette_alloc:
undefined4 call_IGDIB::DIB_palette_alloc(HIGDIBINFO hDIB)
{
[...]
size_buffer_palette = compute_size_palette(*hDIB->bits_depth_table_by_channel); [4]
(*hDIB->igdibstd_vftable->IGDIB::createIGPalette)((IGDIB *)hDIB,size_buffer_palette);
*in_FS_OFFSET = local_10;
return 0;
}
The size for the buffer is size_buffer_palette, which is calculated in [4]. The argument of the function called in [4] corresponds to the number of bits required for representing the Palette box’s NE field. The compute_size_palette function is simply:
int __cdecl compute_size_palette(int bit_required)
{
if (bit_required < 9) {
return 1 << ((byte)bit_required & 0x1f);
}
return 0;
}
The compute_size_palette function returns, if the argument does not exceed the value 8, the biggest representable value, using bit_required bits, plus one (e.g., for bit_required = 1 the result would be 2; for bit_required = 7 the result would be 128).
This number is later used in [5] to allocate the required space to parse the Palette box:
undefined ** __thiscall IGPalette::IGPalette(undefined **param_1_00,undefined *size_palette)
{
undefined *_Dst;
param_1_00[1] = (undefined *)0x0;
param_1_00[2] = size_palette;
if ((int)size_palette < 1) {
param_1_00[1] = (undefined *)0x0;
}
else {
_Dst = (undefined *)operator_new((int)size_palette << 2); [5]
param_1_00[1] = _Dst;
if (_Dst != (undefined *)0x0) {
memset(_Dst,0,(int)param_1_00[2] << 2);
*param_1_00 = (undefined *)vftable;
return param_1_00;
}
}
param_1_00[2] = (undefined *)0x0;
*param_1_00 = (undefined *)vftable;
return param_1_00;
}
So, the Palette box parser uses the buffer allocated in [5] in the memcpy at [3]. The size of the allocated buffer can be simplified as (1 << bit_required) << 2. The problem resides in the calculation of the bit_required value. An off-by-one calculation exists during the computation of that value. The function that calculates the bit_required value is show here:
void __cdecl FUN_73ebfbc0(undefined4 *param_1,size_t *LPHIGDIBINFO)
{
[...]
iVar3 = 0
if (0 < iVar2) {
do {
if (*(int *)(param_1[0x18] + 4) == 0) {
NE_field = param_1[0x13];
bit_required = 0;
while (NE_field = NE_field >> 1, NE_field != 0) { [6]
bit_required = bit_required + 1;
}
if ((*(char *)(param_1 + 0x16) != '\x03') || (iVar3 != 0)) {
bit_required = *(int *)(param_1[5] + 4 + iVar3 * 0x20);
}
}
else {
bit_required = 8;
}
channelDepths[iVar3] = bit_required;
iVar3 = iVar3 + 1;
} while (iVar3 < iVar2);
}
[...]
}
This is the while loop in assembly:
MOV ECX,dword ptr [EBX + NE_field]
XOR bit_required,bit_required
SAR ECX,1 [7]
JZ LAB_B
LAB_A
INC bit_required
SAR ECX,1
JNZ LAB_A
LAB_B
In [6]/[7] there is the actual calculation for computing the bits required for representing a value. The problem is that the computation starts with the right shift of one rather than the comparison with zero. This would count one bit less than the required one (e.g., with NE_field = 1(0b…001) the bit_required value would be 0, with NE_field = 0x1f(0b…00011111) the bit_required value would be 4). This leads to an off-by-one that can result in a heap-based buffer overflow in [3] because the calculated size, using the wrongly-calculated bits required, could be smaller than the real required size.
Crash Information
0:000> !analyze -v
*******************************************************************************
* *
* Exception Analysis *
* *
*******************************************************************************
KEY_VALUES_STRING: 1
Key : AV.Fault
Value: Write
Key : Analysis.CPU.mSec
Value: 3218
Key : Analysis.DebugAnalysisManager
Value: Create
Key : Analysis.Elapsed.mSec
Value: 10243
Key : Analysis.Init.CPU.mSec
Value: 468
Key : Analysis.Init.Elapsed.mSec
Value: 11601
Key : Analysis.Memory.CommitPeak.Mb
Value: 143
Key : Timeline.OS.Boot.DeltaSec
Value: 166871
Key : Timeline.Process.Start.DeltaSec
Value: 11
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: 2000000
APPLICATION_VERIFIER_FLAGS: 0
APPLICATION_VERIFIER_LOADED: 1
EXCEPTION_RECORD: (.exr -1)
ExceptionAddress: 6ebce13d (MSVCR110!memcpy+0x0000021e)
ExceptionCode: c0000005 (Access violation)
ExceptionFlags: 00000000
NumberParameters: 2
Parameter[0]: 00000001
Parameter[1]: 0af11000
Attempt to write to address 0af11000
FAULTING_THREAD: 00002848
PROCESS_NAME: Fuzzme.exe
WRITE_ADDRESS: 0af11000
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: 0af11000
STACK_TEXT:
0019fac4 6e30e18a 0af10fc0 0ae4ef80 0000007c MSVCR110!memcpy+0x21e
WARNING: Stack unwind information not available. Following frames may be wrong.
0019fae0 6e30fe07 0019fb6c 00000003 00000001 igJPEG2K19d!CPb_JPEG2K_init+0x94ca
0019fb54 6e31106c 0a996f50 0019fb6c 00000000 igJPEG2K19d!CPb_JPEG2K_init+0xb147
0019fb70 6e3070a6 0a996f50 0ac5ff50 a5d44a84 igJPEG2K19d!CPb_JPEG2K_init+0xc3ac
0019fb9c 6e30711e 0019fc3c 0ae2afa0 00000000 igJPEG2K19d!CPb_JPEG2K_init+0x23e6
0019fbb4 6ed713d9 0019fc3c 0ae2afa0 00000001 igJPEG2K19d!CPb_JPEG2K_init+0x245e
0019fbec 6edb08d7 00000000 0ae2afa0 0019fc3c igCore19d!IG_image_savelist_get+0xb29
0019fe68 6edb0239 00000000 0019ff10 00000001 igCore19d!IG_mpi_page_set+0x148a7
0019fe88 6ed45757 00000000 0019ff10 00000001 igCore19d!IG_mpi_page_set+0x14209
0019fea8 00402219 0019ff10 0019febc 00000001 igCore19d!IG_load_file+0x47
0019fec0 00402524 0019ff10 052c7fe0 0522df50 Fuzzme!fuzzme+0x19
0019ff28 0040668d 00000005 05226f68 0522df50 Fuzzme!fuzzme+0x324
0019ff70 75330419 0027b000 75330400 0019ffdc Fuzzme!fuzzme+0x448d
0019ff80 778b72ed 0027b000 7d8b826c 00000000 KERNEL32!BaseThreadInitThunk+0x19
0019ffdc 778b72bd ffffffff 778d65c2 00000000 ntdll!__RtlUserThreadStart+0x2f
0019ffec 00000000 00406715 0027b000 00000000 ntdll!_RtlUserThreadStart+0x1b
STACK_COMMAND: ~0s ; .cxr ; kb
SYMBOL_NAME: MSVCR110!memcpy+21e
MODULE_NAME: MSVCR110
IMAGE_NAME: MSVCR110.dll
FAILURE_BUCKET_ID: INVALID_POINTER_WRITE_STRING_DEREFERENCE_AVRF_c0000005_MSVCR110.dll!memcpy
OS_VERSION: 10.0.17763.1
BUILDLAB_STR: rs5_release
OSPLATFORM_TYPE: x86
OSNAME: Windows 10
IMAGE_VERSION: 11.0.50727.1
FAILURE_ID_HASH: {77975e19-9d4d-daf1-6c0e-6a3a4c334a80}
Followup: MachineOwner
---------
Timeline
2021-08-30 - Initial contact
2021-08-31 - Vendor acknowledged and created support ticket
2021-09-10 - Vendor closed support ticket 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 Emmanuel Tacheau and Francesco Benvenuto of Cisco Talos.