Grassroot DICOM RLE解码功能越界读取漏洞(CVE-2025-48429)深度分析

本文深入分析了Grassroot DICOM 3.024库中RLECodec::DecodeByStreams函数存在的越界读取漏洞(CVE-2025-48429)。漏洞源于代码未对`NumSegments`变量进行有效边界检查,导致在解析恶意DICOM文件时可读取超出预定范围的内存,可能泄露堆数据。文章详细解释了触发漏洞的代码路径、崩溃时的寄存器状态及根本原因。

思科Talos漏洞报告 TALOS-2025-2214

Grassroot DICOM RLECodec::DecodeByStreams 越界读取漏洞

CVE编号: CVE-2025-48429

发布日期: 2025年12月16日

摘要

在Grassroot DICOM 3.024版本的 RLECodec::DecodeByStreams 功能中,存在一个越界读取漏洞。特制的DICOM文件可导致堆数据泄露。攻击者可以通过提供恶意文件来触发此漏洞。

受影响的版本

经过测试或由供应商确认受影响的版本如下:

  • Grassroot DICOM 3.024

产品链接: Grassroot DICOM

CVSSv3 评分: 7.4 - CVSS:3.1/AV:L/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H

CWE 分类: CWE-119 - 对内存缓冲区范围内的操作限制不当

技术详情

Grassroots DiCoM 是一个用于处理DICOM医学文件的C++库,可从Python、C#、Java和PHP访问。它支持RAW、JPEG、JPEG 2000、JPEG-LS、RLE和压缩传输语法。

特制的DICOM文件可以在 RLECodec::DecodeByStreams 函数中触发越界读取。此漏洞是由于缺乏适当的边界检查,意味着该函数在处理过程中不会验证内存访问是否保持在源缓冲区的范围内。因此,当加载格式错误的DICOM文件时,会发生以下情况:

1
2
3
4
5
6
7
8
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 )

在调试期间检查变量时,立即可以明显看出存在越界问题。查看导致崩溃的代码并检查诸如 frame.Header.Offset 和控制for循环的 numSegments 等变量,可以进一步说明问题:

1
2
3
4
5
6
7
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

代码由于读取了范围之外的内存而崩溃。以下是 RLECodec::DecodeByStreams 函数的部分代码片段:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
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 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 98. }

为了理解为何第43行的 frame.Header.Offset[i] 会导致访问违规,需要更详细地检查源代码。在第8行,frame 被定义为 RLEFrame 类型,并从 Internals->Frame 赋值。通过检查 Internals 变量,我们可以收集到关于根本问题的重要线索:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
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
}

我们可以观察到,该对象中与 frame.Header.Offset 对应的 Offset 数量是15个。以下是 RLEFrame 的声明:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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.     }
...
121. //private:
122.   RLEHeader Header;
123.   std::vector<char> Bytes;
124. };

在第109行,我们看到 numSegments 来自私有变量 Header.NumSegments。检查 Header 类可以提供更多上下文:

1
2
3
4
5
6
7
126. class RLEHeader
127. {
128. public:
129.   uint32_t NumSegments;
130.   uint32_t Offset[15];
...
140. };

我们可以观察到 Offset 是一个固定大小的 uint32_t 数组,长度为15。如果 NumSegments 变量(来自第11行的 frame.Header.NumSegments)被设置为大于 Offset 数组(第130行)最大条目数的值,就会导致越界访问并引发崩溃。

格式错误的DICOM文件可以操纵 NumSegments 的值来控制for循环的行为,可能导致敏感信息泄露。

崩溃信息

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
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 )
...
───────────────────────────────────────────────────────────────────────────────────────────[ 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 - 向供应商披露
  • 2025-08-04 - Talos 跟进
  • 2025-09-01 - Talos 跟进
  • 2025-10-06 - Talos 跟进
  • 2025-12-16 - 公开披露

致谢

该漏洞由思科Talos的Emmanuel Tacheau发现。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计