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_convert和null_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_width、cinfo->input_components和num_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_width、cinfo->input_components和num_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发现。