CVE-2024-47796
An improper array index validation vulnerability exists in the nowindow functionality of OFFIS DCMTK 3.6.8. A specially crafted DICOM file can lead to an out-of-bounds write. 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.
OFFIS DCMTK 3.6.8
DCMTK - https://dicom.offis.de/dcmtk.php.en
8.4 - CVSS:3.1/AV:L/AC:L/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
DCMTK is a collection of libraries and applications implementing large parts the DICOM standard.
It includes software …
for examining, constructing and converting DICOM image files
handling storage media
sending and receiving images over a network connection
as well as demonstrative image storage and worklist servers
DCMTK is is written in a mixture of ANSI C and C++. It comes in complete source code and is made available as open source software.
DCMTK has been used at numerous DICOM demonstrations to provide central, vendor-independent image storage and worklist servers (CTNs - Central Test Nodes).
It is used by hospitals and companies all over the world for a wide variety of purposes ranging from being a tool for product testing to being a building block for research projects, prototypes and commercial products.
In order to highlight the reason for the crash, we utilize tools such as ASAN compilation. This helps us gain better insights into the cause of the crash. AddressSanitizer is a runtime memory error detector designed to find various types of bugs in C/C++ programs. It can help you identify issues such as:
- Use after free (dangling pointer dereference)
- Heap buffer overflow
- Stack buffer overflow
- Use after scope
- Initialization order bugs
- Memory leaks
Below are more details when ASAN is enabled.
==4121250==ERROR: AddressSanitizer: heap-use-after-free on address 0x60300007ead1 at pc 0x556e925985bf bp 0x7ffc5dc90110 sp 0x7ffc5dc90100
READ of size 1 at 0x60300007ead1 thread T0
#0 0x556e925985be in DiMonoOutputPixelTemplate<unsigned char, unsigned int, unsigned char>::window(DiMonoPixel const*, unsigned int, DiLookupTable const*, DiDisplayFunction*, double, double, unsigned char, unsigned char) /home/manu/dcmtk/dcmimgle/include/dcmtk/dcmimgle/dimoopxt.h:1100
#1 0x556e9259cc9b in DiMonoOutputPixelTemplate<unsigned char, unsigned int, unsigned char>::DiMonoOutputPixelTemplate(void*, DiMonoPixel const*, DiOverlay**, DiLookupTable const*, DiLookupTable const*, DiDisplayFunction*, EF_VoiLutFunction, double, double, unsigned int, unsigned int, unsigned short, unsigned short, unsigned long, unsigned long, int) /home/manu/dcmtk/dcmimgle/include/dcmtk/dcmimgle/dimoopxt.h:129
#2 0x556e92422a04 in DiMonoImage::getDataUint8(void*, DiDisplayFunction*, int, unsigned long, int, unsigned int, unsigned int) /home/manu/dcmtk/dcmimgle/libsrc/dimoimg3.cc:55
#3 0x556e9223765d in DiMonoImage::getData(void*, unsigned long, unsigned long, int, int, int) /home/manu/dcmtk/dcmimgle/libsrc/dimoimg.cc:1508
#4 0x556e9221cd8b in DiMono2Image::getOutputData(unsigned long, int, int) /home/manu/dcmtk/dcmimgle/libsrc/dimo2img.cc:141
#5 0x556e917bb40a in DicomImage::getOutputData(int, unsigned long, int) /home/manu/dcmtk/dcmimgle/include/dcmtk/dcmimgle/dcmimage.h:425
#6 0x556e917bb40a in DVPresentationState::getPixelData(void const*&, unsigned long&, unsigned long&) /home/manu/dcmtk/dcmpstat/libsrc/dvpstat.cc:1831
#7 0x556e9120ee12 in main /home/manu/dcmtk/dcmpstat/apps/dcmp2pgm.cc:532
#8 0x7f3610629d8f in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
#9 0x7f3610629e3f in __libc_start_main_impl ../csu/libc-start.c:392
#10 0x556e91244f64 in _start (/usr/local/bin/dcmp2pgm+0x3b2f64)
0x60300007ead1 is located 17 bytes inside of 25-byte region [0x60300007eac0,0x60300007ead9)
freed by thread T0 here:
#0 0x556e912d7527 in operator delete[](void*, std::nothrow_t const&) (/usr/local/bin/dcmp2pgm+0x445527)
#1 0x556e92eae559 in DcmElement::~DcmElement() /home/manu/dcmtk/dcmdata/libsrc/dcelem.cc:262
previously allocated by thread T0 here:
#0 0x556e912d6a47 in operator new[](unsigned long, std::nothrow_t const&) (/usr/local/bin/dcmp2pgm+0x444a47)
#1 0x556e92eab86c in DcmElement::DcmElement(DcmElement const&) /home/manu/dcmtk/dcmdata/libsrc/dcelem.cc:103
SUMMARY: AddressSanitizer: heap-use-after-free /home/manu/dcmtk/dcmimgle/include/dcmtk/dcmimgle/dimoopxt.h:1100 in DiMonoOutputPixelTemplate<unsigned char, unsigned int, unsigned char>::window(DiMonoPixel const*, unsigned int, DiLookupTable const*, DiDisplayFunction*, double, double, unsigned char, unsigned char)
Shadow bytes around the buggy address:
0x0c0680007d00: 00 01 fa fa 00 00 00 00 fa fa 00 00 00 03 fa fa
0x0c0680007d10: 00 00 00 00 fa fa fd fd fd fd fa fa fd fd fd fd
0x0c0680007d20: fa fa fd fd fd fd fa fa fd fd fd fa fa fa fd fd
0x0c0680007d30: fd fa fa fa 00 00 00 02 fa fa fd fd fd fa fa fa
0x0c0680007d40: fd fd fd fa fa fa 00 00 00 03 fa fa fd fd fd fd
=>0x0c0680007d50: fa fa fd fd fd fd fa fa fd fd[fd]fd fa fa 00 00
0x0c0680007d60: 00 00 fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c0680007d70: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c0680007d80: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c0680007d90: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c0680007da0: 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
Shadow gap: cc
==4121250==ABORTING
In order to better understand where the crash is happening, we have to go through the call stack up to the incrimined code line 748
:
In file: /home/manu/dcmtk/dcmimgle/include/dcmtk/dcmimgle/dimoopxt.h:748
724 createDisplayLUT(dlut, disp, inter->getBits());
725 const double gradient = outrange / (inter->getAbsMaxRange() - 1);
726 if (initOptimizationLUT(lut, ocnt))
727 {
728 q = lut;
729 if (dlut != NULL) // perform display transformation
730 {
731 DCMIMGLE_TRACE("monochrome rendering: VOI NONE #5");
732 if (low > high) // inverse
733 {
734 for (i = ocnt; i != 0; --i) // calculating LUT entries
735 *(q++) = OFstatic_cast(T3, dlut->getValue(OFstatic_cast(Uint16, i - 1)));
736 } else { // normal
737 for (i = 0; i < ocnt; ++i) // calculating LUT entries
738 *(q++) = OFstatic_cast(T3, dlut->getValue(OFstatic_cast(Uint16, i)));
739 }
740 } else { // don't use display: invalid or absent
741 DCMIMGLE_TRACE("monochrome rendering: VOI NONE #6");
742 for (i = 0; i < ocnt; ++i) // calculating LUT entries
743 *(q++) = OFstatic_cast(T3, lowvalue + OFstatic_cast(double, i) * gradient);
744 }
745 const T3 *lut0 = lut - OFstatic_cast(T2, inter->getAbsMinimum()); // points to 'zero' entry
746 q = Data;
747 for (i = Count; i != 0; --i) // apply LUT
748 *(q++) = *(lut0 + (*(p++)));
which is part of the nowindow
function.
The vulnerabiity is happening because there is no checking on the value corresponding to *(lut0 + (*(p++)))
against the size allocated for the pointer q
The q
pointer coming from the Data
is a pointer allocated earlier in the same function, with a size of FrameSize
parameter.
627 void nowindow(const DiMonoPixel *inter,
628 const Uint32 start,
629 const DiLookupTable *plut,
630 DiDisplayFunction *disp,
631 const T3 low,
632 const T3 high)
633 {
634 const DiDisplayLUT *dlut = NULL;
635 const T1 *pixel = OFstatic_cast(const T1 *, inter->getData());
636 if (pixel != NULL)
637 {
638 if (Data == NULL) // create new output buffer
639 Data = new T3[FrameSize];
To better understand where this value FrameSize
is computed, thanks to the tool rr
helping us.
#0 0x000060190afb1a7b in DiMonoOutputPixel::DiMonoOutputPixel (this=0x507000001050, pixel=0x506000008660, size=262144, frame=0, max=255)
at /home/manu/dcmtk/dcmimgle/libsrc/dimoopx.cc:38
1 0x000060190ad4141e in DiMonoOutputPixelTemplate<unsigned char, unsigned int, unsigned char>::DiMonoOutputPixelTemplate (this=0x507000001050, buffer=0x0,
pixel=0x506000008660, overlays=0x5120000002b8, vlut=0x0, plut=0x0, disp=0x0, vfunc=EFV_Default, center=0, width=-1, low=0, high=255, columns=512, rows=512, frame=0,
pastel=0) at /home/manu/dcmtk/dcmimgle/include/dcmtk/dcmimgle/dimoopxt.h:104
FrameSize
is computed while the constructor DiMonoOutputPixel::DiMonoOutputPixel
is called with parameters from the constructor DiMonoOutputPixelTemplate
as below :
DiMonoOutputPixelTemplate(void *buffer,
const DiMonoPixel *pixel,
DiOverlay *overlays[2],
const DiLookupTable *vlut,
const DiLookupTable *plut,
DiDisplayFunction *disp,
const EF_VoiLutFunction vfunc,
const double center,
const double width,
const Uint32 low,
const Uint32 high,
const Uint16 columns,
const Uint16 rows,
const unsigned long frame,
#ifdef PASTEL_COLOR_OUTPUT
const unsigned long frames,
#else
const unsigned long /*frames*/,
#endif
const int pastel = 0)
: DiMonoOutputPixel(pixel, OFstatic_cast(unsigned long, columns) * OFstatic_cast(unsigned long, rows), frame,
OFstatic_cast(unsigned long, fabs(OFstatic_cast(double, high - low)))),
Data(NULL),
DeleteData(buffer == NULL),
ColorData(NULL)
we can easily read the FrameSize
is corresponding to the product of OFstatic_cast(unsigned long, columns) * OFstatic_cast(unsigned long, rows)
. Theses values are directly read from the file with DICOM tags respectively
(0028, 0010) Rows
(0028, 0011) Columns
The loop size controlled by the variable Count
in 747
is also corresponding to this value as well.
Now if we take a look into lut0
and p
variables :
p
is computed also earlier in the function nowindow
as follow in line 649
and is computed from the pointer pixel
in 635
Getting further with reverse we can get into the place where the content of pixel
is derived from, part from the convert function :
In file: /home/manu/dcmtk/dcmimgle/include/dcmtk/dcmimgle/diinpxt.h:633
void convert(const DiDocument *document,
const Uint16 bitsAllocated,
const Uint16 bitsStored,
const Uint16 highBit,
DcmFileCache *fileCache,
Uint32 &fragment)
{....
628 ++i;
629 ++p;
630 }
631 if (bits == bitsStored)
632 {
► 633 *(q++) = expandSign(value, sign, smask);
634 value = 0;
635 bits = 0;
636 }
637 }
...}
So the content of the pointer pixel is also somehow derived from the content of the file itself pixel
lut0
which is also a pointer to lut
as indicated by code in 745
is built during the function nowindow
starting line 728
and is a table which is filled with values derived from the content of pixel data throught the formula impacting gradient
and more specifically inter->getAbsMaxRange()
in our case.
This demonstrates that by manipulating the file, we can control the loop size using the Count
variable and modify the value derived from line 747
q
pointer. This manipulation leads to memory corruption or a use-after-free condition, as it allows writing beyond the heap allocation.
==4121250==ERROR: AddressSanitizer: heap-use-after-free on address 0x60300007ead1 at pc 0x556e925985bf bp 0x7ffc5dc90110 sp 0x7ffc5dc90100
READ of size 1 at 0x60300007ead1 thread T0
#0 0x556e925985be in DiMonoOutputPixelTemplate<unsigned char, unsigned int, unsigned char>::window(DiMonoPixel const*, unsigned int, DiLookupTable const*, DiDisplayFunction*, double, double, unsigned char, unsigned char) /home/manu/dcmtk/dcmimgle/include/dcmtk/dcmimgle/dimoopxt.h:1100
#1 0x556e9259cc9b in DiMonoOutputPixelTemplate<unsigned char, unsigned int, unsigned char>::DiMonoOutputPixelTemplate(void*, DiMonoPixel const*, DiOverlay**, DiLookupTable const*, DiLookupTable const*, DiDisplayFunction*, EF_VoiLutFunction, double, double, unsigned int, unsigned int, unsigned short, unsigned short, unsigned long, unsigned long, int) /home/manu/dcmtk/dcmimgle/include/dcmtk/dcmimgle/dimoopxt.h:129
#2 0x556e92422a04 in DiMonoImage::getDataUint8(void*, DiDisplayFunction*, int, unsigned long, int, unsigned int, unsigned int) /home/manu/dcmtk/dcmimgle/libsrc/dimoimg3.cc:55
#3 0x556e9223765d in DiMonoImage::getData(void*, unsigned long, unsigned long, int, int, int) /home/manu/dcmtk/dcmimgle/libsrc/dimoimg.cc:1508
#4 0x556e9221cd8b in DiMono2Image::getOutputData(unsigned long, int, int) /home/manu/dcmtk/dcmimgle/libsrc/dimo2img.cc:141
#5 0x556e917bb40a in DicomImage::getOutputData(int, unsigned long, int) /home/manu/dcmtk/dcmimgle/include/dcmtk/dcmimgle/dcmimage.h:425
#6 0x556e917bb40a in DVPresentationState::getPixelData(void const*&, unsigned long&, unsigned long&) /home/manu/dcmtk/dcmpstat/libsrc/dvpstat.cc:1831
#7 0x556e9120ee12 in main /home/manu/dcmtk/dcmpstat/apps/dcmp2pgm.cc:532
#8 0x7f3610629d8f in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
#9 0x7f3610629e3f in __libc_start_main_impl ../csu/libc-start.c:392
#10 0x556e91244f64 in _start (/usr/local/bin/dcmp2pgm+0x3b2f64)
0x60300007ead1 is located 17 bytes inside of 25-byte region [0x60300007eac0,0x60300007ead9)
freed by thread T0 here:
#0 0x556e912d7527 in operator delete[](void*, std::nothrow_t const&) (/usr/local/bin/dcmp2pgm+0x445527)
#1 0x556e92eae559 in DcmElement::~DcmElement() /home/manu/dcmtk/dcmdata/libsrc/dcelem.cc:262
previously allocated by thread T0 here:
#0 0x556e912d6a47 in operator new[](unsigned long, std::nothrow_t const&) (/usr/local/bin/dcmp2pgm+0x444a47)
#1 0x556e92eab86c in DcmElement::DcmElement(DcmElement const&) /home/manu/dcmtk/dcmdata/libsrc/dcelem.cc:103
SUMMARY: AddressSanitizer: heap-use-after-free /home/manu/dcmtk/dcmimgle/include/dcmtk/dcmimgle/dimoopxt.h:1100 in DiMonoOutputPixelTemplate<unsigned char, unsigned int, unsigned char>::window(DiMonoPixel const*, unsigned int, DiLookupTable const*, DiDisplayFunction*, double, double, unsigned char, unsigned char)
Shadow bytes around the buggy address:
0x0c0680007d00: 00 01 fa fa 00 00 00 00 fa fa 00 00 00 03 fa fa
0x0c0680007d10: 00 00 00 00 fa fa fd fd fd fd fa fa fd fd fd fd
0x0c0680007d20: fa fa fd fd fd fd fa fa fd fd fd fa fa fa fd fd
0x0c0680007d30: fd fa fa fa 00 00 00 02 fa fa fd fd fd fa fa fa
0x0c0680007d40: fd fd fd fa fa fa 00 00 00 03 fa fa fd fd fd fd
=>0x0c0680007d50: fa fa fd fd fd fd fa fa fd fd[fd]fd fa fa 00 00
0x0c0680007d60: 00 00 fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c0680007d70: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c0680007d80: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c0680007d90: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c0680007da0: 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
Shadow gap: cc
==4121250==ABORTING
https://git.dcmtk.org/?p=dcmtk.git;a=commit;h=89a6e399f1e17d08a8bc8cdaa05b2ac9a50cd4f6
2024-12-16 - Vendor Disclosure
2025-01-11 - Vendor Patch Release
2025-01-13 - Public Release
Discovered by Emmanuel Tacheau of Cisco Talos.