Talos Vulnerability Report

TALOS-2023-1836

Accusoft ImageGear allocate_buffer_for_jpeg_decoding out-of-bounds write vulnerability

September 25, 2023
CVE Number

CVE-2023-40163

SUMMARY

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.

CONFIRMED VULNERABLE VERSIONS

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

PRODUCT URLS

ImageGear - https://www.accusoft.com/products/imagegear-collection/

CVSSv3 SCORE

9.8 - CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

CWE

CWE-787 - Out-of-bounds Write

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.

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. ebx1was 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_sofmust 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.

Crash Information

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
---------
VENDOR RESPONSE

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

TIMELINE

2023-08-28 - Vendor Disclosure
2023-09-20 - Vendor Patch Release
2023-09-25 - Public Release

Credit

Discovered by Emmanuel Tacheau of Cisco Talos.