Talos Vulnerability Report

TALOS-2024-1935

Grassroot DICOM JPEG2000Codec::DecodeByStreamsCommon out-of-bounds write vulnerability

April 25, 2024
CVE Number

CVE-2024-22373

SUMMARY

An out-of-bounds write vulnerability exists in the JPEG2000Codec::DecodeByStreamsCommon functionality of Mathieu Malaterre Grassroot DICOM 3.0.23. A specially crafted DICOM file can lead to a heap buffer overflow. 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.

Grassroot DICOM 3.0.23

PRODUCT URLS

Grassroot DICOM - https://sourceforge.net/projects/gdcm/

CVSSv3 SCORE

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

CWE

CWE-119 - Improper Restriction of Operations within the Bounds of a Memory Buffer

DETAILS

Grassroots DiCoM is a C++ library for DICOM medical files. It is accessible from Python, C#, Java and PHP. It supports RAW, JPEG, JPEG 2000, JPEG-LS, RLE and deflated transfer syntax. It comes with a super fast scanner implementation to quickly scan hundreds of DICOM files. It supports SCU network operations (C-ECHO, C-FIND, C-STORE, C-MOVE). PS 3.3 & 3.6 are distributed as XML files. It also provides PS 3.15 certificates and password based mecanism to anonymize and de-identify DICOM dataset

A specially-crafted DICOM file can lead to a heap-based buffer overflow in gdcm::JPEG2000Codec::DecodeByStreamsCommon, due to a buffer overflow caused by a missing size check for a buffer memory.

Below some extract of the function and the crash is happening in LINE234 :

LINE1   std::pair<char *, size_t> JPEG2000Codec::DecodeByStreamsCommon(char *dummy_buffer, size_t buf_size)
LINE2   {
    [...]
LINE27    /* set decoding parameters to default values */
LINE28    opj_set_default_decoder_parameters(&parameters);
LINE29  
LINE30    const char jp2magic[] = "\x00\x00\x00\x0C\x6A\x50\x20\x20\x0D\x0A\x87\x0A";
LINE31    if( memcmp( src, jp2magic, sizeof(jp2magic) ) == 0 )
LINE32      {
LINE33      /* JPEG-2000 compressed image data ... sigh */
LINE34      // gdcmData/ELSCINT1_JP2vsJ2K.dcm
LINE35      // gdcmData/MAROTECH_CT_JP2Lossy.dcm
LINE36      gdcmWarningMacro( "J2K start like JPEG-2000 compressed image data instead of codestream" );
LINE37      parameters.decod_format = JP2_CFMT;
LINE38      assert(parameters.decod_format == JP2_CFMT);
LINE39      }
    [...]
LINE49    /* get a decoder handle */
LINE50    switch(parameters.decod_format)
LINE51      {
LINE52    case J2K_CFMT:
LINE53      dinfo = opj_create_decompress(CODEC_J2K);
LINE54      break;
LINE55    case JP2_CFMT:
LINE56      dinfo = opj_create_decompress(CODEC_JP2);
LINE57      break;
    [...]
LINE97    bResult = opj_read_header(
LINE98      cio,
LINE99      dinfo,
LINE100     &image);
    [...]
LINE186   // Copy buffer
LINE187   unsigned long len = Dimensions[0]*Dimensions[1] * (PF.GetBitsAllocated() / 8) * image->numcomps;
LINE188   char *raw = new char[len];
LINE189   //assert( len == fsrc->len );
LINE190   for (unsigned int compno = 0; compno < (unsigned int)image->numcomps; compno++)
LINE191     {
LINE192     opj_image_comp_t *comp = &image->comps[compno];
LINE193 
LINE194     int w = image->comps[compno].w;
LINE195     int wr = int_ceildivpow2(image->comps[compno].w, image->comps[compno].factor);
LINE196 
LINE197     //int h = image.comps[compno].h;
LINE198     int hr = int_ceildivpow2(image->comps[compno].h, image->comps[compno].factor);
   [...]
LINE228     if (comp->prec <= 8)
LINE229       {
LINE230       uint8_t *data8 = (uint8_t*)raw + compno;
LINE231       for (int i = 0; i < wr * hr; i++)
LINE232         {
LINE233         int v = image->comps[compno].data[i / wr * w + i % wr];
LINE234         *data8 = (uint8_t)v;
LINE235         data8 += image->numcomps;
LINE236         }
LINE237       }
    [...]
LINE271 }
LINE272 

The for loop at LINE231 is controlled by the product of wr * hr. Theses two variables are derived from image at LINE195 and LINE198 respectively. The image variable is computed from the openjpeg function opj_read_header at LINE97 The target heap buffer *data8, indexed by compno at LINE230 is corresponding to raw buffer and the raw buffer length is allocated with a length set to len derived from Dimensions at LINE187 The Dimensions is a table of three integers where the first two values are read from the file directly and corresponds to tags dicom (0x0028, 0x0010) and (0x0028, 0x0011). The data written also into the heap represented by the v variable is also extracted from the file itself too. The issue is happening when PF.GetBitsAllocated() is equal to 8, the product of Dimensions[0]*Dimensions[1] may be less than wr * hr and there is no bounds checking nor correlation regarding the allocation of the buffer length and the values returned by the openjpeg opj_read_header function, causing the heap-based buffer overflow leading to memory corruption.

Crash Information

NumberOfDimensions: 2
Dimensions: (400,400,1)
SamplesPerPixel    :3
BitsAllocated      :8
BitsStored         :8
HighBit            :7
PixelRepresentation:0
ScalarType found   :UINT8
PhotometricInterpretation: YBR_RCT
PlanarConfiguration: 0
TransferSyntax: 1.2.840.10008.1.2.4.90
Origin: (0,0,0)
Spacing: (0.352778,0.352778,1)
DirectionCosines: (1,0,0,0,1,0)
Rescale Intercept/Slope: (0,1)
=================================================================
==3379==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x7fe5799b4b00 at pc 0x7fe57b65df54 bp 0x7ffc8a631150 sp 0x7ffc8a631148
WRITE of size 1 at 0x7fe5799b4b00 thread T0
    #0 0x7fe57b65df53 in gdcm::JPEG2000Codec::DecodeByStreamsCommon(char*, unsigned long) /home/manu/gdcm-3.0.23/Source/MediaStorageAndFileFormat/gdcmJPEG2000Codec.cxx:860:16
    #1 0x7fe57b65e903 in gdcm::JPEG2000Codec::DecodeByStreams(std::istream&, std::ostream&) /home/manu/gdcm-3.0.23/Source/MediaStorageAndFileFormat/gdcmJPEG2000Codec.cxx:908:43
    #2 0x7fe57b65b9c1 in gdcm::JPEG2000Codec::Decode(gdcm::DataElement const&, gdcm::DataElement&) /home/manu/gdcm-3.0.23/Source/MediaStorageAndFileFormat/gdcmJPEG2000Codec.cxx:552:14
    #3 0x7fe57b3f18cf in gdcm::Bitmap::TryJPEG2000Codec(char*, bool&) const /home/manu/gdcm-3.0.23/Source/MediaStorageAndFileFormat/gdcmBitmap.cxx:878:20
    #4 0x7fe57b3f15bd in gdcm::Bitmap::GetBufferInternal(char*, bool&) const /home/manu/gdcm-3.0.23/Source/MediaStorageAndFileFormat/gdcmBitmap.cxx:1003:28
    #5 0x7fe57b3f3311 in gdcm::Bitmap::GetBuffer(char*) const /home/manu/gdcm-3.0.23/Source/MediaStorageAndFileFormat/gdcmBitmap.cxx:993:10
    #6 0x7fe57b41c4c4 in gdcm::ImageChangeTransferSyntax::Change() /home/manu/gdcm-3.0.23/Source/MediaStorageAndFileFormat/gdcmImageChangeTransferSyntax.cxx:420:21
    #7 0x5584f2a61954 in main /home/manu/gdcm-3.0.23/Examples/Cxx/CompressImage.cxx:63:19
    #8 0x7fe57a229d8f in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16
    #9 0x7fe57a229e3f in __libc_start_main csu/../csu/libc-start.c:392:3
    #10 0x5584f29a14d4 in _start (/home/manu/gdcm_3_0_23_builds/asan/bin/CompressImage+0x1f4d4) (BuildId: 185df4d685b8d63866de5c20436d6884ca503318)

0x7fe5799b4b00 is located 0 bytes to the right of 480000-byte region [0x7fe57993f800,0x7fe5799b4b00)
allocated by thread T0 here:
    #0 0x5584f2a5f1fd in operator new[](unsigned long) (/home/manu/gdcm_3_0_23_builds/asan/bin/CompressImage+0xdd1fd) (BuildId: 185df4d685b8d63866de5c20436d6884ca503318)
    #1 0x7fe57b65d505 in gdcm::JPEG2000Codec::DecodeByStreamsCommon(char*, unsigned long) /home/manu/gdcm-3.0.23/Source/MediaStorageAndFileFormat/gdcmJPEG2000Codec.cxx:814:15
    #2 0x7fe57b65e903 in gdcm::JPEG2000Codec::DecodeByStreams(std::istream&, std::ostream&) /home/manu/gdcm-3.0.23/Source/MediaStorageAndFileFormat/gdcmJPEG2000Codec.cxx:908:43
    #3 0x7fe57b65b9c1 in gdcm::JPEG2000Codec::Decode(gdcm::DataElement const&, gdcm::DataElement&) /home/manu/gdcm-3.0.23/Source/MediaStorageAndFileFormat/gdcmJPEG2000Codec.cxx:552:14
    #4 0x7fe57b3f18cf in gdcm::Bitmap::TryJPEG2000Codec(char*, bool&) const /home/manu/gdcm-3.0.23/Source/MediaStorageAndFileFormat/gdcmBitmap.cxx:878:20
    #5 0x7fe57b3f15bd in gdcm::Bitmap::GetBufferInternal(char*, bool&) const /home/manu/gdcm-3.0.23/Source/MediaStorageAndFileFormat/gdcmBitmap.cxx:1003:28
    #6 0x7fe57b3f3311 in gdcm::Bitmap::GetBuffer(char*) const /home/manu/gdcm-3.0.23/Source/MediaStorageAndFileFormat/gdcmBitmap.cxx:993:10
    #7 0x7fe57b41c4c4 in gdcm::ImageChangeTransferSyntax::Change() /home/manu/gdcm-3.0.23/Source/MediaStorageAndFileFormat/gdcmImageChangeTransferSyntax.cxx:420:21
    #8 0x5584f2a61954 in main /home/manu/gdcm-3.0.23/Examples/Cxx/CompressImage.cxx:63:19
    #9 0x7fe57a229d8f in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16

SUMMARY: AddressSanitizer: heap-buffer-overflow /home/manu/gdcm-3.0.23/Source/MediaStorageAndFileFormat/gdcmJPEG2000Codec.cxx:860:16 in gdcm::JPEG2000Codec::DecodeByStreamsCommon(char*, unsigned long)
Shadow bytes around the buggy address:
  0x0ffd2f32e910: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0ffd2f32e920: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0ffd2f32e930: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0ffd2f32e940: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0ffd2f32e950: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x0ffd2f32e960:[fa]fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0ffd2f32e970: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0ffd2f32e980: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0ffd2f32e990: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0ffd2f32e9a0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0ffd2f32e9b0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
  Addressable:           00
  Partially addressable: 01 02 03 04 05 06 07
  Heap left redzone:       fa
  Freed heap region:       fd
  Stack left redzone:      f1
  Stack mid redzone:       f2
  Stack right redzone:     f3
  Stack after return:      f5
  Stack use after scope:   f8
  Global redzone:          f9
  Global init order:       f6
  Poisoned by user:        f7
  Container overflow:      fc
  Array cookie:            ac
  Intra object redzone:    bb
  ASan internal:           fe
  Left alloca redzone:     ca
  Right alloca redzone:    cb
==3379==ABORTING
VENDOR RESPONSE

The vendor has fixed the code on their sourceforge site.

TIMELINE

2024-02-15 - Initial Vendor Contact
2024-02-20 - Vendor Disclosure
2024-02-21 - Vendor Patch Release
2024-04-25 - Public Release

Credit

Discovered by Emmanuel Tacheau of Cisco Talos.