Talos Vulnerability Report

TALOS-2024-2122

OFFIS DCMTK nowindow improper array index validation vulnerability

January 13, 2025
CVE Number

CVE-2024-47796

SUMMARY

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.

CONFIRMED VULNERABLE VERSIONS

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

PRODUCT URLS

DCMTK - https://dicom.offis.de/dcmtk.php.en

CVSSv3 SCORE

8.4 - CVSS:3.1/AV:L/AC:L/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

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.

Crash Information

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

https://git.dcmtk.org/?p=dcmtk.git;a=commit;h=89a6e399f1e17d08a8bc8cdaa05b2ac9a50cd4f6

TIMELINE

2024-12-16 - Vendor Disclosure
2025-01-11 - Vendor Patch Release
2025-01-13 - Public Release

Credit

Discovered by Emmanuel Tacheau of Cisco Talos.