An out-of-bounds read vulnerability exists in the RLECodec::DecodeByStreams functionality of Grassroot DICOM 3.024. A specially crafted DICOM file can lead to leaking heap data. 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.024
Grassroot DICOM - https://sourceforge.net/projects/gdcm/
7.4 - CVSS:3.1/AV:L/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 trigger an out-of-bounds read in the RLECodec::DecodeByStreams function. This vulnerability is due to the absence of proper size checks, which means the function does not verify whether memory accesses stay within the bounds of the source buffer during processing. As a result, when a malformed DICOM file is loaded, the following situation occurs:
Program received signal SIGSEGV, Segmentation fault.
In file: /src/gdcm-3.0.24/Source/MediaStorageAndFileFormat/gdcmRLECodec.cxx:804
799 length /= numSegments;
800 for(unsigned long i = 0; i<numSegments; ++i)
801 {
802 unsigned long numberOfReadBytes = 0;
803 std::streampos pos = is.tellg() - start;
► 804 if ( frame.Header.Offset[i] - pos != 0 )
While inspecting variables during debugging, it becomes immediately apparent that an out-of-bounds issue is present. Reviewing the code responsible for the crash and examining variables such as frame.Header.Offset and numSegments, which controls the for-loop, provides further indication of the problem:
pwndbg> p frame.Header.Offset[0]
$2 = 64
pwndbg> p frame.Header.Offset[i]
Cannot access memory at address 0x5d4eb2150000
pwndbg> p/x numSegments
$5 = 0x800002
The code crashes due to a read memory outside of scope. Below is an excerpt from the RLECodec::DecodeByStreams function:
LINE 1. bool RLECodec::DecodeByStreams(std::istream &is, std::ostream &os)
LINE 2. {
LINE 3. std::streampos start = is.tellg();
LINE 4. // FIXME: Do some stupid work:
LINE 5. char dummy_buffer[256];
LINE 6. std::stringstream tmpos;
LINE 7.
LINE 8. RLEFrame &frame = Internals->Frame;
LINE 9. if( !frame.Read(is) )
LINE 10. return false;
LINE 11. unsigned long numSegments = frame.Header.NumSegments;
LINE 12.
LINE 13. unsigned long length = Length;
LINE 14. assert( length );
LINE 15. // Special case:
LINE 16. assert( GetPixelFormat().GetBitsAllocated() == 32 ||
LINE 17. GetPixelFormat().GetBitsAllocated() == 16 ||
LINE 18. GetPixelFormat().GetBitsAllocated() == 8 );
LINE 19. if( GetPixelFormat().GetBitsAllocated() > 8 )
LINE 20. {
LINE 21. RequestPaddedCompositePixelCode = true;
LINE 22. }
LINE 23.
LINE 24. assert( GetPixelFormat().GetSamplesPerPixel() == 3 || GetPixelFormat().GetSamplesPerPixel() == 1 );
LINE 25. // A footnote:
LINE 26. // RLE *by definition* with more than one component will have applied the
LINE 27. // Planar Configuration because it simply does not make sense to do it
LINE 28. // otherwise. So implicitly RLE is indeed PlanarConfiguration == 1. However
LINE 29. // when the image says: "hey I am PlanarConfiguration = 0 AND RLE", then
LINE 30. // apply the PlanarConfiguration internally so that people don't get lost
LINE 31. // Because GDCM internally set PlanarConfiguration == 0 by default, even if
LINE 32. // the Attribute is not sent, it will still default to 0 and we will be
LINE 33. // consistent with ourselves...
LINE 34. if( GetPixelFormat().GetSamplesPerPixel() == 3 && GetPlanarConfiguration() == 0 )
LINE 35. {
LINE 36. RequestPlanarConfiguration = true;
LINE 37. }
LINE 38. length /= numSegments;
LINE 39. for(unsigned long i = 0; i<numSegments; ++i)
LINE 40. {
LINE 41. unsigned long numberOfReadBytes = 0;
LINE 42. std::streampos pos = is.tellg() - start;
LINE 43. if ( frame.Header.Offset[i] - pos != 0 )
LINE 44. {
LINE 45. // ACUSON-24-YBR_FULL-RLE.dcm
LINE 46. // D_CLUNIE_CT1_RLE.dcm
LINE 47. // This should be at most the \0 padding
LINE 48. //gdcmWarningMacro( "RLE Header says: " << frame.Header.Offset[i] <<
LINE 49. // " when it should says: " << pos << std::endl );
LINE 50. std::streamoff check = frame.Header.Offset[i] - pos;//should it be a streampos or a uint32? mmr
LINE 51. // check == 2 for gdcmDataExtra/gdcmSampleData/US_DataSet/GE_US/2929J686-breaker
LINE 52. //assert( check == 1 || check == 2);
LINE 53. (void)check; //warning removal
LINE 54. is.seekg( frame.Header.Offset[i] + start, std::ios::beg );
LINE 55. }
[...]
LINE 98. }
To understand why frame.Header.Offset[i] at LINE 43 leads to an access violation, it’s necessary to examine the source code in more detail. At LINE 8, frameis defined as a type of RLEFrame and is assigned values from Internals->Frame.
By inspecting the Internals variable, we can gather important clues about the underlying issue:
pwndbg> p *Internals
$6 = {
Frame = {
Header = {
NumSegments = 8388610,
Offset = {64, 30490, 0 <repeats 13 times>}
},
Bytes = std::vector of length 0, capacity 0
},
SegmentLength = std::vector of length 0, capacity 0
}
We can observe that the number of Offset in this object corresponding to frame.Header.Offset is 15.
Below is the declaration of an RLEFrame:
99. class RLEFrame
100. {
101. public:
102. bool Read(std::istream &is)
103. {
104. // read Header (64 bytes)
105. is.read((char*)(&Header), sizeof(uint32_t)*16);
106. assert( sizeof(uint32_t)*16 == 64 );
107. assert( sizeof(RLEHeader) == 64 );
108. SwapperNoOp::SwapArray((uint32_t*)&Header,16);
109. uint32_t numSegments = Header.NumSegments;
110. if( numSegments >= 1 )
111. {
112. if( Header.Offset[0] != 64 ) return false;
113. }
114. // We just check that we are indeed at the proper position start+64
115. return true;
116. }
117. void Print(std::ostream &os)
118. {
119. Header.Print(os);
120. }
121. //private:
122. RLEHeader Header;
123. std::vector<char> Bytes;
124. };
At LINE 109, we see that numSegments is derived from the private variable Header.NumSegments. Examining the Header class provides further context:
126. class RLEHeader
127. {
128. public:
129. uint32_t NumSegments;
130. uint32_t Offset[15];
131.
132. void Print(std::ostream &os)
133. {
134. os << "NumSegments:" << NumSegments << "\n";
135. for(int i=0; i<15; ++i)
136. {
137. os << i << ":" << Offset[i] << "\n";
138. }
139. }
140. };
We can observe that Offset is a fixed-size array of uint32_t with a length of 15. If the NumSegments variable—sourced from frame.Header.NumSegments at LINE 11—is set to a value larger than the maximum number of entries in the Offset array LINE 130, this leads to an out-of-bounds access and causes the crash.
A malformed DICOM file can manipulate the value of NumSegments to control the behavior of the for-loop, potentially resulting in the exposure of sensitive information.
Program received signal SIGSEGV, Segmentation fault.
0x00005d4e91546831 in gdcm::RLECodec::DecodeByStreams (this=0x7ffd85430080, is=..., os=...) at /src/gdcm-src/Source/MediaStorageAndFileFormat/gdcmRLECodec.cxx:804
804 if ( frame.Header.Offset[i] - pos != 0 )
LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA
────────────────────────────────────────────────────────────────────[ REGISTERS / show-flags off / show-compact-regs off ]─────────────────────────────────────────────────────────────────────
*RAX 0x5d4eb2107db0 ◂— 0x4000800002
RBX 0
*RCX 0
*RDX 0x12093
*RDI 0x7ffd8542f610 ◂— 0xffffffffffffffff
*RSI 0xffffffffffffffff
R8 0
*R9 8
*R10 0x71e3875bfdd0 ◂— 0
*R11 0
*R12 1
*R13 0x5d4e914f76b9 (main) ◂— endbr64
*R14 0x5d4e918b3798 (__do_global_dtors_aux_fini_array_entry) —▸ 0x5d4e914f7430 (__do_global_dtors_aux) ◂— endbr64
*R15 0x71e387bb9040 (_rtld_global) —▸ 0x71e387bba2e0 —▸ 0x5d4e912f6000 ◂— 0x10102464c457f
*RBP 0x7ffd8542fa70 —▸ 0x7ffd85430000 —▸ 0x7ffd85430100 —▸ 0x7ffd85430140 —▸ 0x7ffd85430170 ◂— ...
*RSP 0x7ffd8542f590 ◂— 0
*RIP 0x5d4e91546831 (gdcm::RLECodec::DecodeByStreams(std::istream&, std::ostream&)+729) ◂— mov eax, dword ptr [rax + rdx*4 + 4]
─────────────────────────────────────────────────────────────────────────────[ DISASM / x86-64 / set emulate on ]──────────────────────────────────────────────────────────────────────────────
► 0x5d4e91546831 <gdcm::RLECodec::DecodeByStreams(std::istream&, std::ostream&)+729> mov eax, dword ptr [rax + rdx*4 + 4] <Cannot dereference [0x5d4eb2150000]>
0x5d4e91546835 <gdcm::RLECodec::DecodeByStreams(std::istream&, std::ostream&)+733> mov ebx, eax
0x5d4e91546837 <gdcm::RLECodec::DecodeByStreams(std::istream&, std::ostream&)+735> lea rax, [rbp - 0x460]
0x5d4e9154683e <gdcm::RLECodec::DecodeByStreams(std::istream&, std::ostream&)+742> mov rdi, rax
0x5d4e91546841 <gdcm::RLECodec::DecodeByStreams(std::istream&, std::ostream&)+745> call std::fpos<__mbstate_t>::operator long() const <std::fpos<__mbstate_t>::operator long() const>
0x5d4e91546846 <gdcm::RLECodec::DecodeByStreams(std::istream&, std::ostream&)+750> cmp rbx, rax
0x5d4e91546849 <gdcm::RLECodec::DecodeByStreams(std::istream&, std::ostream&)+753> setne al
0x5d4e9154684c <gdcm::RLECodec::DecodeByStreams(std::istream&, std::ostream&)+756> test al, al
0x5d4e9154684e <gdcm::RLECodec::DecodeByStreams(std::istream&, std::ostream&)+758> je gdcm::RLECodec::DecodeByStreams(std::istream&, std::ostream&)+873 <gdcm::RLECodec::DecodeByStreams(std::istream&, std::ostream&)+873>
0x5d4e91546850 <gdcm::RLECodec::DecodeByStreams(std::istream&, std::ostream&)+760> mov rax, qword ptr [rbp - 0x498]
0x5d4e91546857 <gdcm::RLECodec::DecodeByStreams(std::istream&, std::ostream&)+767> mov rdx, qword ptr [rbp - 0x4b0]
───────────────────────────────────────────────────────────────────────────────────────[ SOURCE (CODE) ]───────────────────────────────────────────────────────────────────────────────────────
In file: /src/gdcm-3.0.24/Source/MediaStorageAndFileFormat/gdcmRLECodec.cxx:804
799 length /= numSegments;
800 for(unsigned long i = 0; i<numSegments; ++i)
801 {
802 unsigned long numberOfReadBytes = 0;
803 std::streampos pos = is.tellg() - start;
► 804 if ( frame.Header.Offset[i] - pos != 0 )
805 {
806 // ACUSON-24-YBR_FULL-RLE.dcm
807 // D_CLUNIE_CT1_RLE.dcm
808 // This should be at most the \0 padding
809 //gdcmWarningMacro( "RLE Header says: " << frame.Header.Offset[i] <<
───────────────────────────────────────────────────────────────────────────────────────────[ STACK ]───────────────────────────────────────────────────────────────────────────────────────────
00:0000│ rsp 0x7ffd8542f590 ◂— 0
01:0008│-4d8 0x7ffd8542f598 —▸ 0x7ffd8542fe70 —▸ 0x71e387b4d870 (vtable for std::__cxx11::basic_stringstream<char, std::char_traits<char>, std::allocator<char> >+64) —▸ 0x71e387a6b830 (non-virtual thunk to std::__cxx11::basic_stringstream<char, std::char_traits<char>, std::allocator<char> >::~basic_stringstream()) ◂— endbr64
02:0010│-4d0 0x7ffd8542f5a0 —▸ 0x7ffd8542fcd0 —▸ 0x71e387b4d848 (vtable for std::__cxx11::basic_stringstream<char, std::char_traits<char>, std::allocator<char> >+24) —▸ 0x71e387a6b8d0 (std::__cxx11::basic_stringstream<char, std::char_traits<char>, std::allocator<char> >::~basic_stringstream()) ◂— endbr64
03:0018│-4c8 0x7ffd8542f5a8 —▸ 0x7ffd85430080 —▸ 0x5d4e919e4fd8 (vtable for gdcm::RLECodec+16) —▸ 0x5d4e91543388 (gdcm::RLECodec::~RLECodec()) ◂— endbr64
04:0020│-4c0 0x7ffd8542f5b0 ◂— 0
05:0028│-4b8 0x7ffd8542f5b8 —▸ 0x71e387b8a37c (check_match+316) ◂— test eax, eax
06:0030│-4b0 0x7ffd8542f5c0 ◂— 0x12093
07:0038│-4a8 0x7ffd8542f5c8 ◂— 0
─────────────────────────────────────────────────────────────────────────────────────────[ BACKTRACE ]─────────────────────────────────────────────────────────────────────────────────────────
► 0 0x5d4e91546831 gdcm::RLECodec::DecodeByStreams(std::istream&, std::ostream&)+729
1 0x5d4e91545425 gdcm::RLECodec::Decode(gdcm::DataElement const&, gdcm::DataElement&)+295
2 0x5d4e9157d43a gdcm::Bitmap::TryRLECodec(char*, bool&) const+578
3 0x5d4e9157d6d5 gdcm::Bitmap::GetBufferInternal(char*, bool&) const+247
4 0x5d4e9157be08 gdcm::Bitmap::ComputeLossyFlag()+52
5 0x5d4e91586d85 gdcm::PixmapReader::ReadImageInternal(gdcm::MediaStorage const&, bool)+14433
6 0x5d4e91583522 gdcm::PixmapReader::ReadImage(gdcm::MediaStorage const&)+44
7 0x5d4e915004be gdcm::ImageReader::ReadImage(gdcm::MediaStorage const&)+70
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
2025-07-15 - Vendor Disclosure
2025-08-04 - Talos Follow-up
2025-09-01 - Talos Follow-up
2025-10-06 - Talos Follow-up
2025-12-16 - Public Release
Discovered by Emmanuel Tacheau of Cisco Talos.