CVE-2024-22373
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.
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
Grassroot DICOM - https://sourceforge.net/projects/gdcm/
8.1 - CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H
CWE-119 - Improper Restriction of Operations within the Bounds of a Memory Buffer
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(¶meters);
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.
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
The vendor has fixed the code on their sourceforge site.
2024-02-15 - Initial Vendor Contact
2024-02-20 - Vendor Disclosure
2024-02-21 - Vendor Patch Release
2024-04-25 - Public Release
Discovered by Emmanuel Tacheau of Cisco Talos.