CVE-2023-40163
An out-of-bounds write vulnerability exists in the allocate_buffer_for_jpeg_decoding functionality of Accusoft ImageGear 20.1. A specially crafted malformed file can lead to memory corruption. An attacker can provide a 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.
Accusoft ImageGear 20.1
ImageGear - https://www.accusoft.com/products/imagegear-collection/
9.8 - CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
CWE-787 - Out-of-bounds Write
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.
There is a vulnerability in the allocate_buffer_for_jpeg_decoding function, due to a buffer overflow caused by a missing buffer size check. A specially crafted JPG file can lead to an out-of-bounds write, which can result in memory corruption.
Trying to load a malformed JPG file, we end up in the following situation:
===========================================================
VERIFIER STOP 0000000F: pid 0x1DE8: corrupted suffix pattern
04DC1000 : Heap handle
0D86EFF8 : Heap block
00000001 : Block size
0D86EFF9 : corruption address
===========================================================
This verifier stop is not continuable. Process will be terminated
when you use the `go' debugger command.
===========================================================
(1de8.1394): Break instruction exception - code 80000003 (first chance)
eax=00252000 ebx=00000000 ecx=00000001 edx=0019f370 esi=73f8aa40 edi=00000000
eip=73f8dab2 esp=0019f310 ebp=0019f318 iopl=0 nv up ei pl nz na po nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000202
verifier!VerifierBreakin+0x42:
73f8dab2 cc int 3
Inspecting the heap block metadata will tell us more about what happened:
0:000> dt _DPH_BLOCK_INFORMATION c0D86EFF8-20
verifier!_DPH_BLOCK_INFORMATION
+0x000 StartStamp : 0xabcdbbbb
+0x004 Heap : 0x04dc1000 Void
+0x008 RequestedSize : 1
+0x00c ActualSize : 0x1000
+0x010 Internal : _DPH_BLOCK_INTERNAL_INFORMATION
+0x018 StackTrace : 0x03e0d674 Void
+0x01c EndStamp : 0xdcbabbbb
We can see the RequestedSize
is 1 byte only, and the StackTrace
points to 0x03e0d674.
After parsing the address, StackTrace
will indicate the allocation chain for the heap chunk as follows:
0:000> dds 0x03e0d674
03e0d674 00000000
03e0d678 0000c003
03e0d67c 00190000
03e0d680 73f8a8b0 verifier!AVrfDebugPageHeapAllocate+0x240
03e0d684 77cdf22e ntdll!RtlRegisterSecureMemoryCacheCallback+0xa0e
03e0d688 77c47100 ntdll!RtlAllocateHeap+0x1340
03e0d68c 77c46e5c ntdll!RtlAllocateHeap+0x109c
03e0d690 77c45dfe ntdll!RtlAllocateHeap+0x3e
03e0d694 731e1fa6 igCore20d!IG_GUI_page_title_set+0x3e4d6
03e0d698 7302661d igCore20d!AF_memm_alloc+0x1d
03e0d69c 730e966f igCore20d!IG_mpi_page_set+0xbd54f
03e0d6a0 730e2a13 igCore20d!IG_mpi_page_set+0xb68f3
03e0d6a4 730fa065 igCore20d!IG_mpi_page_set+0xcdf45
03e0d6a8 730f9fa7 igCore20d!IG_mpi_page_set+0xcde87
03e0d6ac 730f7dc1 igCore20d!IG_mpi_page_set+0xcbca1
03e0d6b0 730f970a igCore20d!IG_mpi_page_set+0xcd5ea
03e0d6b4 730015b9 igCore20d!IG_image_savelist_get+0xb29
03e0d6b8 73151552 igCore20d!IG_mpi_page_set+0x125432
03e0d6bc 730015b9 igCore20d!IG_image_savelist_get+0xb29
03e0d6c0 730408bc igCore20d!IG_mpi_page_set+0x1479c
03e0d6c4 73040239 igCore20d!IG_mpi_page_set+0x14119
03e0d6c8 72fd5bc7 igCore20d!IG_load_file+0x47
03e0d6cc 00402399 Fuzzme!fuzzme+0x19
03e0d6d0 004026c0 Fuzzme!fuzzme+0x340
03e0d6d4 00408407 Fuzzme!fuzzme+0x6087
03e0d6d8 76bd00c9 KERNEL32!BaseThreadInitThunk+0x19
03e0d6dc 77c67b1e ntdll!RtlGetAppContainerNamedObjectPath+0x11e
03e0d6e0 77c67aee ntdll!RtlGetAppContainerNamedObjectPath+0xee
Parsing this chain tells us the allocation was made into the function allocate_buffer_for_jpeg_decoding
at 730e967a.
730e9410 int32_t allocate_buffer_for_jpeg_decoding(struct jpeg_dec* jpeg_dec, struct SOF_object* jpeg_object, enum SOF_type type_of_sof,
730e9410 struct jpeg_component_table* jpeg_component_table)
730e9410 {
730e941a int32_t var_10 = 0;
730e941d struct jpeg_dec* l_jpeg_dec = jpeg_dec;
730e9420 SIZE_T size_malloc = 0;
730e9422 uint32_t x_MAX_sampling_factor = ((uint32_t)l_jpeg_dec->_related_to_horizontalSamplingFactor);
730e9426 uint32_t y_MAX_sampling_factor = ((uint32_t)l_jpeg_dec->_related_to_verticalSamplingFactor);
730e942d enum SOF_type SOF_type = l_jpeg_dec->enum_SOF_type;
730e943b int32_t l_subsampling_Y;
730e943b int32_t subsampling_X;
730e943b struct jpeg_component_table* edi;
730e943b if ((SOF_type == Lossy || SOF_type == Progressive))
730e9438 {
[...]
730e9492 }
730e943b if ((SOF_type != Lossy && SOF_type != Progressive))
730e9438 {
730e94a3 label_730e94a3:
730e94a3 edi = jpeg_component_table;
730e94a6 subsampling_X = edi->struct_dfh.subsampling_X;
730e94a9 l_subsampling_Y = edi->struct_dfh.subsampling_Y;
730e94ac edi->subsampling_X = subsampling_X;
730e94af edi->subsampling_Y = l_subsampling_Y;
730e94b2 edi->maybe_per_component_bits = 8;
730e94b2 }
730e94bf if (l_jpeg_dec->load_save_dct != 0)
730e94b9 {
730e950a edi->pointer_function = sub_730c3100;
730e950a }
730e94c7 else if (jpeg_object->SOF_Header.precision == 0xc)
730e94c4 {
730e94c9 edi->pointer_function = sub_730c41f0;
730e94c9 }
730e94d2 else
730e94d2 {
730e94d2 int32_t eax_13 = edi->maybe_per_component_bits;
730e94d8 if (eax_13 == 8)
730e94d5 {
730e94da edi->pointer_function = sub_730c2660;
730e94da }
730e94e6 else if (eax_13 == 4)
730e94e3 {
730e94e8 edi->pointer_function = sub_730c3150;
730e94e8 }
730e94f5 else
730e94f5 {
730e94f5 void* eax_14 = sub_730c35e0;
730e9501 if (edi->maybe_per_component_bits == 2)
730e94f1 {
730e9501 eax_14 = sub_730c3470;
730e9501 }
730e9505 edi->pointer_function = eax_14;
730e9505 }
730e9505 }
730e951a struct SOF_object* eax_20;
730e951a if (type_of_sof == Losseless)
730e9519 {
730e9558 int32_t l_word_product_x_image_subsampling;
730e9558 int32_t h_word_product_x_image_subsampling;
730e9558 h_word_product_x_image_subsampling = HIGHD(((int64_t)(((l_jpeg_dec->x_image * subsampling_X) - 1) + x_MAX_sampling_factor)));
730e9558 l_word_product_x_image_subsampling = LOWD(((int64_t)(((l_jpeg_dec->x_image * subsampling_X) - 1) + x_MAX_sampling_factor)));
730e955c int32_t xplus_y_subsampling = (edi->struct_dfh.subsampling_X + l_subsampling_Y);
730e955e edi->standardized_width = (COMBINE(h_word_product_x_image_subsampling, l_word_product_x_image_subsampling) / x_MAX_sampling_factor);
730e9561 int32_t ebx_1 = (xplus_y_subsampling * edi->standardized_width);
730e9572 int32_t eax_30;
730e9572 int32_t edx_4;
730e9572 edx_4 = HIGHD(((int64_t)(((l_jpeg_dec->y_image * l_subsampling_Y) - 1) + y_MAX_sampling_factor)));
730e9572 eax_30 = LOWD(((int64_t)(((l_jpeg_dec->y_image * l_subsampling_Y) - 1) + y_MAX_sampling_factor)));
730e9576 size_malloc = (ebx_1 + ebx_1);
730e9578 edi->standardized_heigth = (COMBINE(edx_4, eax_30) / y_MAX_sampling_factor);
730e957b eax_20 = jpeg_object;
730e957b }
730e951d if ((type_of_sof == Lossy || type_of_sof == Progressive))
730e951c {
[...]
730e95f9 }
[...]
730e967a edi->buffer_1 = AF_memm_alloc(l_jpeg_dec->heap_ptr, size_malloc, "..\..\..\..\Common\Formats\jpeg_…", 0xeeb);
730e967f char* eax_70 = AF_memm_alloc(l_jpeg_dec->heap_ptr, size_malloc, "..\..\..\..\Common\Formats\jpeg_…", 0xeec);
730e9684 bool cond:2 = edi->buffer_1 == 0;
730e9688 edi->buffer_2 = eax_70;
730e968f int32_t esi_2;
730e968f if ((cond:2 || ((!cond:2) && eax_70 == 0)))
730e968d {
730e96ac esi_2 = AF_err_record_set("..\..\..\..\Common\Formats\jpeg_…", 0xef0, 0xfffffc18, 0, size_malloc, l_jpeg_dec->heap_ptr, nullptr);
730e9691 }
730e968f if (((!cond:2) && eax_70 != 0))
730e968d {
730e96b0 esi_2 = var_10;
730e96b0 }
730e96b7 if (type_of_sof == Losseless)
730e96b3 {
730e96cf *(int16_t*)(edi->buffer_1 + (((size_malloc >> 1) - edi->standardized_width) << 1)) = (1 << (((int8_t)jpeg_object->SOF_Header.precision) - 1));
730e96c6 }
730e96d6 edi->struct_dfh.buffer_working_ptr = edi->buffer_1;
730e96db edi->field_0 = 0;
730e96e6 return esi_2;
730e96e6 }
At 730e967a our interesting heap buffer is represented here in the pseudo-code by edi->buffer_1
. It’s created by calling AF_memm_alloc
, which is somehow a wrapper for malloc with a parameter for the size as size_malloc
. size_malloc
was computed earlier at 730e9576
to be the double of ebx1
. ebx1
was computed at 730e9561
to be the result of the product xplus_y_subsampling * edi->standardized_width
.
Going backward into the code, we see at 730e9558 l_jpeg_dec->x_image
influences standardized_width
, and l_jpeg_dec->x_image
is directly read from the file and under control.
Setting x_image
to null will in some circumstance result in a null product. Whenever one member of the product xplus_y_subsampling * edi->standardized_width
is null, it will produce a null result, ending in a size_malloc
to be null. A call to malloc with a null value gives back a 1 byte length heap and gives back a buffer that is too small.
Later at 730e96cf
, we can see the buffer edi->buffer_1
cast into an int16_t *
, meaning the buffer length must be at least 2 bytes. The memory corruption happens there, as the buffer is too small to accept the data stored in it.
To get into theses circumstances, we need to understand where the type_of_sof
must be Losseless
came from.
Investigating the call stack leads to a function I named jpeg_process_FrameHeader
with following pseudo-code:
730e2730 int32_t __stdcall jpeg_process_FrameHeader(int32_t arg1, void* arg2)
730e2730 {
730e2772 while (edi == 0)
730e2770 {
730e2782 int32_t eax_1 = kind_of_look_for_marker_data(jpeg_dec, SOFx, &var_8, &var_2c);
730e2787 int32_t ecx_1 = var_8;
730e278a edi = eax_1;
730e278e if (ecx_1 == 0)
730e278c {
730e2790 SOFx = (SOFx + 1);
730e2796 var_28 = SOFx;
730e279c if (SOFx <= SOF3)
730e2799 {
730e279c continue;
730e279c }
730e2799 }
730e27a0 if (edi != 0)
730e279e {
730e27a0 break;
730e27a0 }
730e27a8 int32_t eax_4;
730e27a8 if (ecx_1 != 0)
730e27a6 {
730e27b5 if ((((uint32_t)SOFx) - SOF0) > 3)
730e27b2 {
730e280b jpeg_dec->enum_SOF_type = Lossy;
730e2814 eax_4 = parse_SOF(jpeg_dec, ecx_1, &var_64);
730e2811 }
730e27aa else
730e27aa {
730e27aa if (SOFx == SOF3)
730e27b7 {
730e27c3 var_1c = 1;
730e27c6 jpeg_dec->enum_SOF_type = Losseless;
730e27cf eax_4 = parse_SOF(jpeg_dec, ecx_1, &var_64);
730e27cc }
730e27aa if (SOFx == SOF2)
730e27b7 {
730e27de var_1c = 2;
730e27e1 jpeg_dec->enum_SOF_type = Progressive;
730e27ea eax_4 = parse_SOF(jpeg_dec, ecx_1, &var_64);
730e27e7 }
730e27aa if ((SOFx == SOF0 || SOFx == SOF1))
730e27b7 {
730e27f6 var_1c = 0;
730e27f9 jpeg_dec->enum_SOF_type = Lossy;
730e2802 eax_4 = parse_SOF(jpeg_dec, ecx_1, &var_64);
730e27ff }
730e27aa }
730e27b2 goto label_730e2880;
730e27b2 }
[...]
730e2d64 }
We can see at 730e27aa
, having a maker identified as SOF3
will set the enum_SOF_type
to Losseless
. So, having a jpeg malformed file containing a marker S0F3
with a ‘X_image’ value set to null will lead to a null memory size length allocation and lead to a memory corruption.
0:000> !analyze -v
*******************************************************************************
* *
* Exception Analysis *
* *
*******************************************************************************
APPLICATION_VERIFIER_HEAPS_CORRUPTED_HEAP_BLOCK_SUFFIX (f)
Corrupted suffix pattern for heap block.
Most typically this happens for buffer overrun errors. Sometimes the application
verifier places non-accessible pages at the end of the allocation and buffer
overruns will cause an access violation and sometimes the heap block is
followed by a magic pattern. If this pattern is changed when the block gets
freed you will get this break. These breaks can be quite difficult to debug
because you do not have the actual moment when corruption happened.
You just have access to the free moment (stop happened here) and the
allocation stack trace (!heap -p -a HEAP_BLOCK_ADDRESS)
Arguments:
Arg1: 04dc1000, Heap handle used in the call.
Arg2: 0d86eff8, Heap block involved in the operation.
Arg3: 00000001, Size of the heap block.
Arg4: 0d86eff9, Corruption address.
KEY_VALUES_STRING: 1
Key : AVRF.Code
Value: f
Key : AVRF.Exception
Value: 1
Key : Analysis.CPU.mSec
Value: 1937
Key : Analysis.Elapsed.mSec
Value: 2040
Key : Analysis.IO.Other.Mb
Value: 14
Key : Analysis.IO.Read.Mb
Value: 1
Key : Analysis.IO.Write.Mb
Value: 32
Key : Analysis.Init.CPU.mSec
Value: 4281
Key : Analysis.Init.Elapsed.mSec
Value: 2756959
Key : Analysis.Memory.CommitPeak.Mb
Value: 102
Key : Failure.Bucket
Value: BREAKPOINT_AVRF_80000003_verifier.dll!VerifierBreakin
Key : Failure.Hash
Value: {59a738c4-b581-efeb-feb5-548af1fa6817}
Key : Timeline.OS.Boot.DeltaSec
Value: 15203
Key : Timeline.Process.Start.DeltaSec
Value: 2756
Key : WER.OS.Branch
Value: vb_release
Key : WER.OS.Version
Value: 10.0.19041.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: 73f8dab2 (verifier!VerifierBreakin+0x00000042)
ExceptionCode: 80000003 (Break instruction exception)
ExceptionFlags: 00000000
NumberParameters: 1
Parameter[0]: 00000000
FAULTING_THREAD: 00001394
PROCESS_NAME: Fuzzme.exe
ERROR_CODE: (NTSTATUS) 0x80000003 - {EXCEPTION} Breakpoint A breakpoint has been reached.
EXCEPTION_CODE_STR: 80000003
EXCEPTION_PARAMETER1: 00000000
STACK_TEXT:
0019f318 73f8dbb0 c0000421 00000000 00000000 verifier!VerifierBreakin+0x42
0019f640 73f8dead 0000000f 04dc1000 0d86eff8 verifier!VerifierCaptureContextAndReportStop+0xf0
0019f684 73f8b945 0000000f 73f81e58 04dc1000 verifier!VerifierStopMessage+0x2bd
0019f6f0 73f8bc2c 04dc1000 00000000 0d86eff8 verifier!AVrfpDphReportCorruptedBlock+0x285
0019f760 73f8893a 04dc1000 0d752c64 00000000 verifier!AVrfpDphCheckPageHeapBlock+0x1bc
0019f78c 73f88ae0 04dc1000 0d86eff8 0019f81c verifier!AVrfpDphFindBusyMemory+0xda
0019f7a8 73f8aad0 04dc1000 0d86eff8 00c46e5c verifier!AVrfpDphFindBusyMemoryAndRemoveFromBusyList+0x20
0019f7c4 77cdfa86 04dc0000 01000002 0d86eff8 verifier!AVrfDebugPageHeapFree+0x90
0019f82c 77c43d66 0d86eff8 096c50b5 00000000 ntdll!RtlDebugFreeHeap+0x3e
0019f988 77c87acd 00000000 0d86eff8 0d86eff8 ntdll!RtlpFreeHeap+0xd6
0019f9e4 77c43c36 00000000 00000000 00000000 ntdll!RtlpFreeHeapInternal+0x783
0019fa00 731e1f3f 04dc0000 00000000 0d86eff8 ntdll!RtlFreeHeap+0x46
WARNING: Stack unwind information not available. Following frames may be wrong.
0019fa14 73026dbc 0d86eff8 0fb5af60 00000000 igCore20d!IG_GUI_page_title_set+0x3e46f
0019fa2c 730e2cfd 1000001f 0d86eff8 7325e380 igCore20d!AF_memm_alloc+0x7bc
0019fab0 730fa065 00000003 730f5dd0 0b166720 igCore20d!IG_mpi_page_set+0xb6bdd
0019facc 730f9fa7 0b166720 0fb5af60 0000ffda igCore20d!IG_mpi_page_set+0xcdf45
0019faf0 730f7dc1 0b166720 0fb5af60 0019fb18 igCore20d!IG_mpi_page_set+0xcde87
0019fb10 730f970a 0019ffc3 1000001d 0a6faf70 igCore20d!IG_mpi_page_set+0xcbca1
0019fb50 730015b9 1000001d 0a6faf70 00000001 igCore20d!IG_mpi_page_set+0xcd5ea
0019fb88 73151552 00000000 00000000 0019fc3c igCore20d!IG_image_savelist_get+0xb29
0019fbb4 730015b9 0019fc3c 0b16afd8 00000001 igCore20d!IG_mpi_page_set+0x125432
0019fbec 730408bc 00000000 0b16afd8 0019fc3c igCore20d!IG_image_savelist_get+0xb29
0019fe68 73040239 00000000 052a5fd0 00000001 igCore20d!IG_mpi_page_set+0x1479c
0019fe88 72fd5bc7 00000000 052a5fd0 00000001 igCore20d!IG_mpi_page_set+0x14119
0019fea8 00402399 052a5fd0 0019febc 76bcfb80 igCore20d!IG_load_file+0x47
0019fec0 004026c0 052a5fd0 0019fef8 05209f50 Fuzzme!fuzzme+0x19
0019ff28 00408407 00000005 05202f80 05209f50 Fuzzme!fuzzme+0x340
0019ff70 76bd00c9 00252000 76bd00b0 0019ffdc Fuzzme!fuzzme+0x6087
0019ff80 77c67b1e 00252000 096c56e1 00000000 KERNEL32!BaseThreadInitThunk+0x19
0019ffdc 77c67aee ffffffff 77c88c03 00000000 ntdll!__RtlUserThreadStart+0x2f
0019ffec 00000000 0040848f 00252000 00000000 ntdll!_RtlUserThreadStart+0x1b
STACK_COMMAND: ~0s ; .cxr ; kb
SYMBOL_NAME: verifier!VerifierBreakin+42
MODULE_NAME: verifier
IMAGE_NAME: verifier.dll
FAILURE_BUCKET_ID: BREAKPOINT_AVRF_80000003_verifier.dll!VerifierBreakin
OS_VERSION: 10.0.19041.1
BUILDLAB_STR: vb_release
OSPLATFORM_TYPE: x86
OSNAME: Windows 10
IMAGE_VERSION: 10.0.19041.1
FAILURE_ID_HASH: {59a738c4-b581-efeb-feb5-548af1fa6817}
Followup: MachineOwner
---------
Release notes from the vendor can be found here:
https://help.accusoft.com/ImageGear/v20.3/Windows/DLL/webframe.html#release-notes.html
https://help.accusoft.com/ImageGear/v20.3/Linux/webframe.html#release-notes.html
2023-08-28 - Vendor Disclosure
2023-09-20 - Vendor Patch Release
2023-09-25 - Public Release
Discovered by Emmanuel Tacheau of Cisco Talos.