Featured image of post openjpeg_04:CVE-2016-10504

openjpeg_04:CVE-2016-10504

CVE-2016-10504

本文将介绍CVE-2016-10504这一漏洞的背景、原理和复现方式,仅为个人学习笔记,供大家学习参考。

申明:本工作是A.S.E (AI Code Generation Security Evaluation)开源项目的一部分,很荣幸能作为contributor参与这一开源项目,为大模型的安全评估做出贡献;

笔记汇总在CVE_Binary_Reproduction


漏洞卡片

字段 内容
CVE-ID CVE-2016-10504
CWE-ID CWE-119:Memory Buffer Overflow
NVD公开日期 2017-08-30
评分 6.5 MEDIUM (CVSS v3)
影响组件 OpenJPEG (openjp2) ≤ 2.2.0
受影响模块 openjp2(JPEG2000 编码器)
漏洞类型 缓冲区越界写(heap-buffer-overflow)
利用后果 远程代码执行 / DoS
补丁 Commit 397f62c0a838e15d667ef50e27d5d011d2c79c04 (openjpeg)

背景介绍

  • OpenJPEG(openjp2)是 JPEG 2000(ISO/IEC 15444)的一种开源实现,包含编解码器与命令行工具(opj_compress、opj_decompress 等),广泛用于图像查看器、PDF 渲染器与医疗影像等领域。
  • 该漏洞出现在编码流程中(opj_compress),触发器是一个特制/损坏的 BMP 文件(作为输入),在编码过程中导致 opj_mqc_byteout 写出越界。
  • 触发链条(简要):
    • 编码器为编码块分配缓冲(tcd.c -> opj_tcd_code_block_enc_allocate_data)。
    • MQ 熵编码(mqc.c)在 byteout/flush 过程中写入字节流。
    • 在某些畸形输入下,MQ 编码器需要输出比预估多出的字节(典型为 1 字节),但分配的缓冲区大小不足,导致写越界(ASan 报告为越界 1 字节)。

漏洞原理分析

  • 触发点(观察到的行为)
    • 在对输入图像进行 JPEG2000 编码时,mqc 模块用于熵编码(MQ 编码)。opj_mqc_byteout 负责把编码输出字节写入已分配的输出缓冲区。
    • 当输入为某些畸形/特制的 BMP 文件时,编码流程会导致输出字节数比为编码分配的缓冲区大 1(或更多),最终在 opj_mqc_byteout 做写入时触发越界写。
  • 根本原因
    • 写入逻辑没有对输出缓冲区剩余空间做充分检查(或缓冲区大小估算/计算存在问题),导致在某些边界条件下发生越界写(典型表现为“边界检查遗漏 / off-by-one”或“分配长度不足”)。
    • 从 ASan 栈可见,越界发生在 mqc.c 的 byteout 序列,分配发生在编码块数据分配路径上(tcd.c 中 allocate_data),说明问题出在编码/计数与分配之间的不一致。
  • 如何被利用(攻击面)
    • 任何使用受影响版本的 OpenJPEG 对不受信任图像进行编码(opj_compress)或在需要对外部图像进行转码的服务,都可能被触发导致崩溃或进一步的利用(视其它内存安全漏洞链而定)。

漏洞复现

环境准备

本次复现是在docker容器环境下进行的,保证了环境的精确、纯粹,我们可以随意指定依赖版本,而不会被主机的环境干扰。

首先,拉取openjpeg官方github仓库。

1
git clone https://github.com/uclouvain/openjpeg.git

然后,根据编译所需相关依赖创建docker镜像,注意依赖要尽量贴合当年的环境,以下是dockerfile。

 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
FROM ubuntu:16.04

# 设置非交互式安装
ENV DEBIAN_FRONTEND=noninteractive

# 安装基础依赖
RUN apt-get update && apt-get install -y \
    build-essential \
    cmake \
    git \
    clang \
    gcc \
    g++ \
    libc6-dev \
    libtiff5-dev \
    libpng-dev \
    libjpeg-dev \
    zlib1g-dev \
    libssl-dev \
    pkg-config \
    wget \
    vim \
    && rm -rf /var/lib/apt/lists/*

# 安装 AddressSanitizer(ASan)支持的编译器(clang)
RUN apt-get update && apt-get install -y \
    clang-3.8 \
    && rm -rf /var/lib/apt/lists/*

# 设置默认编译器为 clang(用于 ASan)
ENV CC=clang-3.8
ENV CXX=clang++-3.8

# 设置工作目录
WORKDIR /workspace

# 保持容器运行
CMD ["tail", "-f", "/dev/null"]

根据dockerfile,我们创建镜像。

1
2
3
4
5
6
7
# 在dockerfile所在目录下执行
docker build -t openjpeg_cve-2016-10504 .
# 检查是否创建成功
docker images
# 返回的images中含openjpeg_cve-2016-10504即创建完成
REPOSITORY                       TAG       IMAGE ID       CREATED        SIZE
openjpeg_cve-2016-10504          latest    07ad62f9e5e6   4 weeks ago    800MB

至此环境准备就完成了。

编译/触发

首先,使用先前创建的镜像启动容器,建议使用docker容器挂载宿主机目录,方便进行文件的观测和修改。

1
2
3
4
5
6
7
# 挂载目录替换成自己宿主机的实际路径,保证openjpeg项目文件夹也在其下
docker run -it --rm --name openjpeg_cve-2016-10504 \
  -v /mnt/d/A.S.E/benchmark-project/openjpeg:/workspace \
  openjpeg_cve-2016-10504   /bin/bash
# -rm 选项表示容器退出后自动删除
# --name 指定容器名字
# /bin/bash 指定命令行环境

宿主机目录会被挂载到容器的/workspace目录下,注意所有改变也会同步到宿主机目录上。

进入容器后,我们先将项目切换到修复前版本。

1
2
3
cd openjpeg
# 切换到修复前一个commit
git checkout 397f62c0a838e15d667ef50e27d5d011d2c79c04^

接着我们编译出供漏洞复现使用的组件,注意需要启用 ASan 与 debug 编译标志以获得清晰崩溃信息(添加 -fsanitize=address 到 CFLAGS/CXXFLAGS),以下是我撰写使用的编译脚本setup.sh。

 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
#!/usr/bin/env bash
set -e

cd /workspace/openjpeg
BUILD_DIR="build_ASan"

# 1. 完全清理
rm -rf "$BUILD_DIR"
mkdir "$BUILD_DIR"

# 2. 强制 clang + ASan 渗透到所有阶段
export CC=clang
export CXX=clang++
export CFLAGS="-fsanitize=address -fno-omit-frame-pointer -g -O0"
export CXXFLAGS="$CFLAGS"
export LDFLAGS="-fsanitize=address"

# 3. 配置 + 编译
cd "$BUILD_DIR"
cmake .. \
  -DCMAKE_BUILD_TYPE=Debug \
  -DCMAKE_C_COMPILER="$CC" \
  -DCMAKE_CXX_COMPILER="$CXX" \
  -DCMAKE_C_FLAGS="$CFLAGS" \
  -DCMAKE_CXX_FLAGS="$CXXFLAGS" \
  -DCMAKE_EXE_LINKER_FLAGS="$LDFLAGS" \
  -DBUILD_SHARED_LIBS=OFF \
  -DBUILD_THIRDPARTY=ON

cmake --build . -- -j$(nproc)

echo "=== Build finished ==="
echo "Executable: $(pwd)/bin/opj_compress"

编译成功

执行完毕后,需要用到的编码器组件opj_compress应当在以下路径。

1
2
3
# 检查opj_compress是否存在
root@1877c59a83ec:/workspace# ls /workspace/openjpeg/build_ASan/bin/opj_compress
/workspace/openjpeg/build_ASan/bin/opj_compress

接下来就可以结合poc触发文件,参考bug_report的中的漏洞触发方式进行漏洞复现。

在report中,触发命令格式如下:

1
2
3
4
# opj_compress 编码器路径
# $FILE poc文件路径
# null.j2k 编码输出文件路径
/workspace/openjpeg/build_ASan/bin/opj_compress -i /workspace/poc/2016-10504.bmp -o /tmp/null.j2k

我使用的poc文件链接附上:2016-10504.bmp

调整路径并执行后成功触发漏洞,显著标志为 heap-buffer-overflow,具体结果如下:

触发漏洞

 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
64
65
66
67
68
69
root@3ae83b1d330a:/workspace# /workspace/openjpeg/build_ASan/bin/opj_compress -i /workspace/poc/2016-10504.bmp -o /tmp/null.j2k 

[INFO] tile number 1 / 1
=================================================================
==1853==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x60200000ef55 at pc 0x72ec6c440abc bp 0x7fff615fe3b0 sp 0x7fff615fe3a8
WRITE of size 1 at 0x60200000ef55 thread T0
    #0 0x72ec6c440abb  (/workspace/openjpeg/build_ASan/bin/libopenjp2.so.7+0xb1abb)
    #1 0x72ec6c4403cc  (/workspace/openjpeg/build_ASan/bin/libopenjp2.so.7+0xb13cc)
    #2 0x72ec6c46ee47  (/workspace/openjpeg/build_ASan/bin/libopenjp2.so.7+0xdfe47)
    #3 0x72ec6c46d658  (/workspace/openjpeg/build_ASan/bin/libopenjp2.so.7+0xde658)
    #4 0x72ec6c523b8d  (/workspace/openjpeg/build_ASan/bin/libopenjp2.so.7+0x194b8d)
    #5 0x72ec6c5221e6  (/workspace/openjpeg/build_ASan/bin/libopenjp2.so.7+0x1931e6)
    #6 0x72ec6c417fdf  (/workspace/openjpeg/build_ASan/bin/libopenjp2.so.7+0x88fdf)
    #7 0x72ec6c415c90  (/workspace/openjpeg/build_ASan/bin/libopenjp2.so.7+0x86c90)
    #8 0x72ec6c3f1305  (/workspace/openjpeg/build_ASan/bin/libopenjp2.so.7+0x62305)
    #9 0x72ec6c3ef867  (/workspace/openjpeg/build_ASan/bin/libopenjp2.so.7+0x60867)
    #10 0x72ec6c446064  (/workspace/openjpeg/build_ASan/bin/libopenjp2.so.7+0xb7064)
    #11 0x4fab6d  (/workspace/openjpeg/build_ASan/bin/opj_compress+0x4fab6d)
    #12 0x72ec6b49d83f  (/lib/x86_64-linux-gnu/libc.so.6+0x2083f)
    #13 0x427788  (/workspace/openjpeg/build_ASan/bin/opj_compress+0x427788)

0x60200000ef55 is located 0 bytes to the right of 5-byte region [0x60200000ef50,0x60200000ef55)
allocated by thread T0 here:
    #0 0x4c78b8  (/workspace/openjpeg/build_ASan/bin/opj_compress+0x4c78b8)
    #1 0x72ec6c52e5bc  (/workspace/openjpeg/build_ASan/bin/libopenjp2.so.7+0x19f5bc)
    #2 0x72ec6c529c9f  (/workspace/openjpeg/build_ASan/bin/libopenjp2.so.7+0x19ac9f)
    #3 0x72ec6c520ad9  (/workspace/openjpeg/build_ASan/bin/libopenjp2.so.7+0x191ad9)
    #4 0x72ec6c51aba6  (/workspace/openjpeg/build_ASan/bin/libopenjp2.so.7+0x18bba6)
    #5 0x72ec6c3efd73  (/workspace/openjpeg/build_ASan/bin/libopenjp2.so.7+0x60d73)
    #6 0x72ec6c3ef32d  (/workspace/openjpeg/build_ASan/bin/libopenjp2.so.7+0x6032d)
    #7 0x72ec6c446064  (/workspace/openjpeg/build_ASan/bin/libopenjp2.so.7+0xb7064)
    #8 0x4fab6d  (/workspace/openjpeg/build_ASan/bin/opj_compress+0x4fab6d)
    #9 0x72ec6b49d83f  (/lib/x86_64-linux-gnu/libc.so.6+0x2083f)

SUMMARY: AddressSanitizer: heap-buffer-overflow (/workspace/openjpeg/build_ASan/bin/libopenjp2.so.7+0xb1abb) 
Shadow bytes around the buggy address:
  0x0c047fff9d90: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c047fff9da0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c047fff9db0: fa fa 05 fa fa fa 00 01 fa fa 05 fa fa fa 00 01
  0x0c047fff9dc0: fa fa 05 fa fa fa 00 01 fa fa 05 fa fa fa 00 01
  0x0c047fff9dd0: fa fa 05 fa fa fa 00 01 fa fa 05 fa fa fa 00 01
=>0x0c047fff9de0: fa fa 05 fa fa fa 00 01 fa fa[05]fa fa fa 00 01
  0x0c047fff9df0: fa fa 00 fa fa fa 00 00 fa fa 00 00 fa fa 00 00
  0x0c047fff9e00: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c047fff9e10: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c047fff9e20: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c047fff9e30: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
  Addressable:           00
  Partially addressable: 01 02 03 04 05 06 07 
  Heap left redzone:       fa
  Heap right redzone:      fb
  Freed heap region:       fd
  Stack left redzone:      f1
  Stack mid redzone:       f2
  Stack right redzone:     f3
  Stack partial redzone:   f4
  Stack after return:      f5
  Stack use after scope:   f8
  Global redzone:          f9
  Global init order:       f6
  Poisoned by user:        f7
  Container overflow:      fc
  Array cookie:            ac
  Intra object redzone:    bb
  ASan internal:           fe
  Left alloca redzone:     ca
  Right alloca redzone:    cb
==1853==ABORTING

当我们切换到修复后版本尝试漏洞复现:

1
2
3
4
cd openjpeg
git checkout 397f62c0a838e15d667ef50e27d5d011d2c79c04
cd ..
./setup.sh && /workspace/openjpeg/build_ASan/bin/opj_compress -i /workspace/poc/2016-10504.bmp -o /tmp/null.j2k

编码能成功或在出现空间不足时被拦截,不再触发 ASan 报错。 补丁生效

1
2
3
4
5
6
=== Build finished ===
Executable: /workspace/openjpeg/build_ASan/bin/opj_compress

[INFO] tile number 1 / 1
[INFO] Generated outfile /tmp/null.j2k
encode time: 7 ms 

PoC分析

通过xxd -g 1 -l 512 2016-10504.bmp命令我们可以对poc文件进行分析:

PoC 文件 2016-10504.bmp 在 BMP 层故意设置「3 bit 色深 + 百万像素级高度 + 伪造压缩类型」,导致以下连锁反应:

  1. 解码阶段 OpenJPEG 的 BMP 读取器按 biSizeImage = 0x8c8c8c8c(≈ 2.3 GB)预估总像素数据,但文件实际仅有 80 kB; 在「行字节数 → 总像素大小」换算中发生整数下溢,最终为 tcd_code_block_enc_allocate_data() 计算出远小于真实需求的缓冲区长度(典型值比理论值少 1 字节)。
  2. 编码阶段 进入 MQ 熵编码后,当前 code-block 的比特流在 mqc_flush() 收尾时需要多输出 1 个对齐/进位字节opj_mqc_byteout() 直接将这额外字节写入堆块末尾,恰好越过已分配边界 1 Byte,被 ASan 捕获为 WRITE of size 1 at address 0x60200000ef55 … located 0 bytes to the right of 5-byte region
  3. 根因归纳 漏洞并非 BMP 解析本身崩溃,而是**「BMP 畸形头 → 分配长度低估 → MQ flush 多 1 字节 → 堆越界写」**的跨模块级联; +1 字节补丁即在 l_data_size 计算时预留裕量,抵消该「flush 额外字节」场景。

小结:PoC 通过头字段造假让 OpenJPEG 自己「算错」内存,再在熵编码收尾时「多写 1 字节」,从而稳定触发 heap-buffer-overflow。


补丁分析

修复commit详见: https://github.com/uclouvain/openjpeg/commit/397f62c0a838e15d667ef50e27d5d011d2c79c04

  1. 主要改动

    补丁核心变化(摘录):

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    
    @@ -1182,7 +1182,8 @@ static OPJ_BOOL opj_tcd_code_block_enc_allocate_data(opj_tcd_cblk_enc_t *
     {
         OPJ_UINT32 l_data_size;
    
    -    l_data_size = (OPJ_UINT32)((p_code_block->x1 - p_code_block->x0) *
    +    /* The +1 is needed for https://github.com/uclouvain/openjpeg/issues/835 */
    +    l_data_size = 1 + (OPJ_UINT32)((p_code_block->x1 - p_code_block->x0) *
                                         (p_code_block->y1 - p_code_block->y0) * (OPJ_INT32)sizeof(OPJ_UINT32));
    
         if (l_data_size > p_code_block->data_size) {
    
    • 说明:补丁在计算需要分配的字节数时增加了“+1”字节的裕量,确保在极端情况下 MQ 输出额外的一个字节时不会写出分配区域之外,从而避免 heap-buffer-overflow。
  2. 为什么能修复问题

    • 直接原因:分配大小估算存在 off-by-one 风险(或未考虑 flush/进位时可能产生的额外字节)。通过在计算中加入 1 字节的保底(安全裕量),可以避免在常见的“多输出 1 字节”情形下出现越界写。
  3. 补丁的局限与建议

    • 虽然 +1 修复了这类 off-by-one 的触发路径,但从安全稳健性角度更推荐:
      • 在写入点(opj_mqc_byteout / opj_mqc_flush)增加严格的边界检查:在每次写入之前验证缓冲剩余空间,若不足则报错并返回失败,避免任何假设性的分配足够。
      • 在分配处(tcd.c)根据上游最大可能输出(例如基于最坏情况计算)分配,而非基于平均/最常见情况的估算。
      • 添加单元/回归测试,覆盖 MQ 编码边界(flush/进位/标记插入等)以防回归。
    • 综上所述,补丁是合理且直接的缓解(带来最小改动),但一个更完善的防御应在输出写入处做双重边界检查。

复现镜像

以上复现过程已打包为docker镜像,可通过以下命令拉取:

1
docker pull choser/openjpeg_cve-2016-10504:latest

内含:

  • openjpeg(项目文件夹)
  • setup.sh
  • image_status_check.sh
  • test_case.sh
  • poc.sh
  • poc文件

首先进入项目文件夹,按需切换到修复前/后版本:

1
2
3
4
5
cd openjpeg
# 切换到修复前一个commit
git checkout 397f62c0a838e15d667ef50e27d5d011d2c79c04^
# 切换到修复commit
git checkout 397f62c0a838e15d667ef50e27d5d011d2c79c04

然后按顺序执行四个脚本,即可复现和验证漏洞,预期结果为:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# 在修复前/后两个版本
./setup.sh && ./image_status_check.sh && ./test_case.sh
# 都应成功编译,并且可执行文件通过基本功能验证
=== Build finished ===
Executable: /workspace/openjpeg/build_ASan/bin/opj_compress
[A.S.E] image startup successfully
[A.S.E] test case passed
# 在修复前/后版本
./poc.sh
# 修复前版本
[A.S.E] vulnerability found
# 修复后版本
[A.S.E] vulnerability not found

总结和启示

  • 该漏洞是典型的“估算分配大小不足(off-by-one)导致的越界写”,在涉及复杂编码/压缩流程时尤其容易出现(因为编码器内部状态会影响输出字节数)。
  • 修复比较直接(为分配加上 1 字节裕量),但更稳健的做法是:在写入端做界限检查、在分配端采用最坏情况估算,并用单元测试覆盖这些边界场景。
  • 对图像处理等面向外部输入的库,建议将内存安全工具(ASan、UBSan)纳入常规 CI,以便早期发现类似问题并在合并前修复。

参考链接

Licensed under CC BY-NC-SA 4.0
© 2023-2025 Ch0ser. All Rights Reserved.
使用 Hugo 构建
主题 StackJimmy 设计