Grassroot DICOM JPEG编码器内存越界读取漏洞深度分析

本文详细分析了Grassroot DICOM 3.024库中JPEGBITSCodec::InternalCode函数存在的越界读取漏洞。该漏洞源于处理恶意DICOM文件时缺乏缓冲区大小检查,攻击者可利用此漏洞导致信息泄露。

TALOS-2025-2210 || Cisco Talos情报组 - 综合威胁情报

Talos漏洞报告

TALOS-2025-2210 Grassroot DICOM JPEGBITSCodec::InternalCode越界读取漏洞 2025年12月16日

CVE编号 CVE-2025-53619, CVE-2025-53618

摘要 Grassroot DICOM 3.024的JPEGBITSCodec::InternalCode功能中存在一个越界读取漏洞。特制的DICOM文件可导致信息泄露。攻击者可通过提供恶意文件来触发此漏洞。

确认受影响版本 以下版本经过Talos测试或验证,或由供应商确认为易受攻击。 Grassroot DICOM 3.024

产品URL Grassroot DICOM - https://sourceforge.net/projects/gdcm/

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和deflated传输语法。 它带有一个超快的扫描器实现,可快速扫描数百个DICOM文件。 它支持SCU网络操作(C-ECHO、C-FIND、C-STORE、C-MOVE)。PS 3.3和3.6作为XML文件分发。 它还提供基于PS 3.15证书和密码的机制来匿名化和去标识化DICOM数据集。

特制的DICOM文件可以在多个压缩例程(如grayscale_convertnull_convert)中触发越界读取。此漏洞源于处理过程中缺少对源内存缓冲区的大小检查。

根据为压缩提供的输入文件,稍后将调用各种函数来处理颜色空间。相关信息将存储在一个名为cinfo的JPEG压缩对象中,该对象在一个名为jinit_color_converter的函数中设置。

例如,根据编码格式,颜色转换的函数指针可能按如下方式分配: cconvert->pub.color_convert = grayscale_convert; (用于灰度转换) cconvert->pub.color_convert = null_convert; (用于无转换)

jinit_color_converter函数负责根据需要配置color_convert对象。以下是处理此设置的实现。

 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
LINE 1. /*
LINE 2.  * Module initialization routine for input colorspace conversion.
LINE 3.  */
LINE 4. 
LINE 5. GLOBAL(void)
LINE 6. jinit_color_converter (j_compress_ptr cinfo)
LINE 7. {
[...]
LINE 47. 
LINE 48.   /* Check num_components, set conversion method based on requested space */
LINE 49.   switch (cinfo->jpeg_color_space) {
LINE 50.   case JCS_GRAYSCALE:
LINE 53.     if (cinfo->in_color_space == JCS_GRAYSCALE)
LINE 54.       cconvert->pub.color_convert = grayscale_convert;
[...]
LINE 62.     break;
LINE 63. 
LINE 64.   case JCS_RGB:
[...]
LINE 67.     if (cinfo->in_color_space == JCS_RGB && RGB_PIXELSIZE == 3)
LINE 68.       cconvert->pub.color_convert = null_convert;
[...]
LINE 71.     break;
[...]
LINE 112.   }
LINE 113. }

为了更深入地了解底层过程,检查其构造非常重要。在这种情况下,使用rr record工具被证明是非常有效的,因为它有助于识别处理JPEG压缩的函数。

在压缩过程中,会调用一个名为JPEGBITSCodec::InternalCode的函数。该函数在管理JPEG压缩中起着关键作用。其输入参数是一个向量,该向量以文件中记录的像素数据大小确定的固定长度进行分配。这个长度称为len,被相应地设置并作为参数传递给函数。

 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
LINE 114. /*
LINE 115.  * Sample routine for JPEG compression.  We assume that the target file name
LINE 116.  * and a compression quality factor are passed in.
LINE 117.  */
LINE 118. 
LINE 119. bool JPEGBITSCodec::InternalCode(const char* input, unsigned long len, std::ostream &os)
LINE 120. {
LINE 121.   int quality = 100; (void)len;
LINE 122.   (void)quality;
LINE 123.   JSAMPLE * image_buffer = (JSAMPLE*)(void*)const_cast<char*>(input);  /* Points to large array of R,G,B-order data */
LINE 124.   const unsigned int *dims = this->GetDimensions();
LINE 125.   int image_height = dims[1];  /* Number of rows in image */
LINE 126.   int image_width = dims[0];    /* Number of columns in image */
LINE 127. 
[...]
LINE 277.    row_stride = image_width * cinfo.input_components;  /* JSAMPLEs per row in image_buffer */
LINE 278.  
LINE 279.    if( this->GetPlanarConfiguration() == 0 )
LINE 280.      {
LINE 281.      while (cinfo.next_scanline < cinfo.image_height) {
LINE 282.        /* jpeg_write_scanlines expects an array of pointers to scanlines.
LINE 283.         * Here the array is only one element long, but you could pass
LINE 284.         * more than one scanline at a time if that's more convenient.
LINE 285.         */
LINE 286.        row_pointer[0] = & image_buffer[cinfo.next_scanline * row_stride];      <---- 此处存在越界读取      
LINE 287.        (void) jpeg_write_scanlines(&cinfo, row_pointer, 1);
LINE 288.      }
LINE 289.      }
[...]
LINE 328.  }

在代码的较早部分,由于缺少边界检查,可以在第286行观察到漏洞。具体来说,对row_pointer的赋值可能导致潜在的越界(OOBO)值,因为没有验证image_buffer[cinfo.next_scanline * row_stride]是否保持在image_buffer或其长度(len)的边界内。

计算出的row_pointer缓冲区随后作为参数传递给jpeg_write_scanlines,在那里用作scanlines参数。在jpeg_write_scanlines内部,我们可以在第361行看到对cinfo->main->process_data的调用,其中scanlines(对应于前面提到的row_pointer)作为第二个参数传递。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
LINE 329. GLOBAL(JDIMENSION)
LINE 330. jpeg_write_scanlines (j_compress_ptr cinfo, JSAMPARRAY scanlines,
LINE 331.           JDIMENSION num_lines)
LINE 332. {
[...]
LINE 355.   /* Ignore any extra scanlines at bottom of image. */
LINE 356.   rows_left = cinfo->image_height - cinfo->next_scanline;
LINE 357.   if (num_lines > rows_left)
LINE 358.     num_lines = rows_left;
LINE 359. 
LINE 360.   row_ctr = 0;
LINE 361.   (*cinfo->main->process_data) (cinfo, scanlines, &row_ctr, num_lines);
LINE 362.   cinfo->next_scanline += row_ctr;
LINE 363.   return row_ctr;
LINE 364. }

在这种情况下,第361行的函数指针cinfo->main->process_data指向process_data_simple_main函数。当调用此函数时,scanlines参数作为input_buf传入。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
LINE 365. METHODDEF(void)
LINE 366. process_data_simple_main (j_compress_ptr cinfo,
LINE 367.         JSAMPARRAY input_buf, JDIMENSION *in_row_ctr,
LINE 368.         JDIMENSION in_rows_avail)
LINE 369. {
LINE 370.   my_main_ptr mainPtr = (my_main_ptr) cinfo->main;
LINE 371.   JDIMENSION data_unit = (JDIMENSION)(cinfo->data_unit);
LINE 372. 
LINE 373.   while (mainPtr->cur_iMCU_row < cinfo->total_iMCU_rows) {
LINE 374.     /* Read input data if we haven't filled the main buffer yet */
LINE 375.     if (mainPtr->rowgroup_ctr < data_unit)
LINE 376.       (*cinfo->prep->pre_process_data) (cinfo,
LINE 377.           input_buf, in_row_ctr, in_rows_avail,
LINE 378.           mainPtr->buffer, &mainPtr->rowgroup_ctr,
LINE 379.           (JDIMENSION) data_unit);
LINE 380. 
[...]
LINE 412. }

在第376行,函数指针(*cinfo->prep->pre_process_data)指向pre_process_data函数。此函数以input_buf作为其参数之一被调用。

 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
LINE 413. METHODDEF(void)
LINE 414. pre_process_data (j_compress_ptr cinfo,
LINE 415.       JSAMPARRAY input_buf, JDIMENSION *in_row_ctr,
LINE 416.       JDIMENSION in_rows_avail,
LINE 417.       JSAMPIMAGE output_buf, JDIMENSION *out_row_group_ctr,
LINE 418.       JDIMENSION out_row_groups_avail)
LINE 419. {
LINE 420.   my_prep_ptr prep = (my_prep_ptr) cinfo->prep;
LINE 421.   int numrows, ci;
LINE 422.   JDIMENSION inrows;
LINE 423.   jpeg_component_info * compptr;
LINE 424. 
LINE 425.   while (*in_row_ctr < in_rows_avail &&
LINE 426.    *out_row_group_ctr < out_row_groups_avail) {
LINE 427.     /* Do color conversion to fill the conversion buffer. */
LINE 428.     inrows = in_rows_avail - *in_row_ctr;
LINE 429.     numrows = cinfo->max_v_samp_factor - prep->next_buf_row;
LINE 430.     numrows = (int) MIN((JDIMENSION) numrows, inrows);
LINE 431.     (*cinfo->cconvert->color_convert) (cinfo, input_buf + *in_row_ctr, 
LINE 432.                prep->color_buf,
LINE 433.                (JDIMENSION) prep->next_buf_row,
LINE 434.                numrows);
[...]
LINE 470.   }
LINE 471. }

最终,调用了前面提到的颜色转换函数(*cinfo->cconvert->color_convert)(第431行)。该函数根据恶意DICOM文件的内容动态调用不同的例程,可能导致各种崩溃。

CVE-2025-53618 - grayscale_convert

以下是grayscale_convert函数的摘录,崩溃发生在第495行:

 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
LINE 473. /*
LINE 474.  * Convert some rows of samples to the JPEG colorspace.
LINE 475.  * This version handles grayscale output with no conversion.
LINE 476.  * The source can be either plain grayscale or YCbCr (since Y == gray).
LINE 477.  */
LINE 478. 
LINE 479. METHODDEF(void)
LINE 480. grayscale_convert (j_compress_ptr cinfo,
LINE 481.        JSAMPARRAY input_buf, JSAMPIMAGE output_buf,
LINE 482.        JDIMENSION output_row, int num_rows)
LINE 483. {
LINE 484.   register JSAMPROW inptr;
LINE 485.   register JSAMPROW outptr;
LINE 486.   register JDIMENSION col;
LINE 487.   JDIMENSION num_cols = cinfo->image_width;
LINE 488.   int instride = cinfo->input_components;
LINE 489. 
LINE 490.   while (--num_rows >= 0) {
LINE 491.     inptr = *input_buf++;
LINE 492.     outptr = output_buf[0][output_row];
LINE 493.     output_row++;
LINE 494.     for (col = 0; col < num_cols; col++) {
LINE 495.       outptr[col] = inptr[0];                             // <----- 此处崩溃
LINE 496.       inptr += instride;                                  // <----- 此处越界读取
LINE 497.     }
LINE 498.   }
LINE 499. }

变量cinfo->image_widthcinfo->input_componentsnum_rows直接受到从恶意DICOM文件中提取的值的影响。 此外,在第491行获取的指针inptr是从作为函数参数传入的input_buf指针派生和计算出来的。经过分析,我们发现在第496行发生了越界读取问题。

崩溃信息

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
NumberOfDimensions: 2
Dimensions: (256,58112,1)
SamplesPerPixel    :1
BitsAllocated      :16
BitsStored         :16
HighBit            :15
PixelRepresentation:0
ScalarType found   :UINT16
PhotometricInterpretation: MONOCHROME2 
PlanarConfiguration: 0
TransferSyntax: 1.2.840.10008.1.2.1
Origin: (0,0,0)
Spacing: (2.21,2.21,1)
DirectionCosines: (1,0,0,0,1,0)
Rescale Intercept/Slope: (0,1)

Program received signal SIGSEGV, Segmentation fault.
grayscale_convert (cinfo=<optimized out>, input_buf=0x7fffffffda30, output_buf=<optimized out>, output_row=1, num_rows=<optimized out>) at /src/gdcm-git/Utilities/gdcmjpeg/jccolor.c:295
295      outptr[col] = inptr[0];  /* don't need GETJSAMPLE() here */

(后续寄存器、反汇编、源代码和堆栈跟踪详情已省略以保持简洁)

CVE-2025-53619 - null_convert

以下是null_convert函数的摘录,崩溃发生在第526行:

 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
LINE 502. /*
LINE 503.  * Convert some rows of samples to the JPEG colorspace.
LINE 504.  * This version handles multi-component colorspaces without conversion.
LINE 505.  * We assume input_components == num_components.
LINE 506.  */
LINE 507. 
LINE 508. METHODDEF(void)
LINE 509. null_convert (j_compress_ptr cinfo,
LINE 510.         JSAMPARRAY input_buf, JSAMPIMAGE output_buf,
LINE 511.         JDIMENSION output_row, int num_rows)
LINE 512. {
LINE 513.   register JSAMPROW inptr;
LINE 514.   register JSAMPROW outptr;
LINE 515.   register JDIMENSION col;
LINE 516.   register int ci;
LINE 517.   int nc = cinfo->num_components;
LINE 518.   JDIMENSION num_cols = cinfo->image_width;
LINE 519. 
LINE 520.   while (--num_rows >= 0) {
LINE 521.     /* It seems fastest to make a separate pass for each component. */
LINE 522.     for (ci = 0; ci < nc; ci++) {
LINE 523.       inptr = *input_buf;
LINE 524.       outptr = output_buf[ci][output_row];
LINE 525.       for (col = 0; col < num_cols; col++) {
LINE 526.         outptr[col] = inptr[ci];                             // <----- 此处崩溃                               
LINE 527.         inptr += nc;                                         // <----- 此处越界读取
LINE 528.       }
LINE 529.     }
LINE 530.     input_buf++;
LINE 531.     output_row++;
LINE 532.   }
LINE 533. }

变量cinfo->image_widthcinfo->input_componentsnum_rows直接受到从恶意DICOM文件中提取的值的影响。 此外,在第523行获取的指针inptr是从作为函数参数传入的input_buf指针派生和计算出来的。经过分析,我们观察到在第527行发生了越界读取问题。

崩溃信息

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
NumberOfDimensions: 2
Dimensions: (256,58112,1)
SamplesPerPixel    :1
BitsAllocated      :16
BitsStored         :16
HighBit            :0
PixelRepresentation:0
ScalarType found   :UINT16
PhotometricInterpretation: RGB 
PlanarConfiguration: 0
TransferSyntax: 1.2.840.10008.1.2.1
Origin: (0,0,0)
Spacing: (1,1,1)
DirectionCosines: (1,0,0,0,1,0)
Rescale Intercept/Slope: (0,1)

Program received signal SIGSEGV, Segmentation fault.
null_convert (cinfo=<optimized out>, input_buf=0x7fffffffda28, output_buf=0x555555f58ae8, output_row=0, num_rows=<optimized out>) at /src/gdcm-git/Utilities/gdcmjpeg/jccolor.c:326
326  outptr[col] = inptr[ci]; /* don't need GETJSAMPLE() here */

(后续寄存器、反汇编、源代码和堆栈跟踪详情已省略以保持简洁)

时间线

  • 2025-07-15 - 向供应商披露
  • 2025-08-04 - Talos跟进
  • 2025-09-01 - Talos跟进
  • 2025-10-06 - Talos跟进
  • 2025-12-16 - 公开披露

致谢 由Cisco Talos的Emmanuel Tacheau发现。

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