Featured image of post openjpeg_09:CVE-2020-27814

openjpeg_09:CVE-2020-27814

CVE-2020-27814

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

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

笔记汇总在CVE_Binary_Reproduction


漏洞卡片

字段 内容
CVE-ID CVE-2020-27814
CWE-ID CWE-122: Heap-based Buffer Overflow
NVD公开日期 2021-01-26
评分 7.8 HIGH (CVSS v3)
影响组件 OpenJPEG (openjp2) ≤ 2.2.0
受影响模块 openjp2(JPEG2000 编码器)
漏洞类型 整数溢出 → 堆缓冲区溢出
利用后果 远程代码执行 / DoS
补丁 Commit eaa098b59b346cb88e4d10d505061f669d7134fc (openjpeg)

背景介绍

  • OpenJPEG(openjp2)是 JPEG 2000(ISO/IEC 15444)的一种开源实现,包含编解码器与命令行工具(opj_compress、opj_decompress 等),广泛用于图像查看器、PDF 渲染器与医疗影像等领域。
  • 由于 JPEG 2000 采用嵌入式块编码(EBCOT),编码器需要为每个 code-block 预先分配压缩缓冲区;在内存受限或特殊压缩参数下,这一分配逻辑容易因“理论上限”估计不足而出现溢出。CVE-2020-27814 正是发生在 code-block 压缩缓冲区上的 off-by-one 写,攻击者只需一张极小图片即可远程触发,危害面覆盖任何自动转码、云端缩略图、PDF 生成等调用场景。

漏洞原理分析

  • 触发点(观察到的行为)

    opj_compress 使用 -M 3 / -M 44 / -M 4 -IMF 2K 等参数时,MQ-Coder 在 opj_mqc_flush() 中会把 mqc->bp 递增后再写入 1 byte;而 opj_tcd_code_block_enc_allocate_data() 仅为压缩数据分配了「理论上限 – 1」字节,于是产生 1 byte 越界写(off-by-one)。

  • 根本原因(两类问题共同导致)

    • 整数溢出:计算缓冲区长度时 (x1-x0)*(y1-y0)*sizeof(UINT32) 未考虑 MQ-Coder 的额外开销。
    • 缺少边界预留:代码仅按「原始估计值」malloc,未给 flush 阶段预留足够余量。
  • 如何被利用(攻击面)

    攻击者构造一张极小 PNG(≤32×32 即可),通过命令行传入特定 -M 值,即可在编码过程中触发 off-by-one 写;在启用 ASan 的构建上直接表现为 heap-buffer-overflow in opj_mqc_byteout,可进一步导致堆元数据破坏或 RCE。


漏洞复现

环境准备

本次复现是在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
# 基于 Ubuntu 18.04(与 2020 年相近的构建环境)
FROM ubuntu:18.04

# 安装 2020 年 master 所需的依赖
RUN apt-get update && apt-get install -y --no-install-recommends \
    build-essential \
    cmake \
    clang \
    git \
    pkg-config \
    libpng-dev \
    libtiff-dev \
    libjpeg-dev \
    zlib1g-dev \
    ca-certificates \
    && rm -rf /var/lib/apt/lists/*

# 创建工作目录,后续宿主机挂载源码
WORKDIR /workspace

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

根据dockerfile,我们创建镜像。

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

至此环境准备就完成了。

编译/触发

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

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

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

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

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

接着我们编译出供漏洞复现使用的组件,注意需要启用 ASan 与 debug 编译标志以获得清晰崩溃信息,以下是我撰写使用的编译脚本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
34
35
#!/usr/bin/env bash
set -e

OPENJPEG_SRC="/workspace/openjpeg"
BUILD_DIR="${OPENJPEG_SRC}/build_ASan"

# 2. 完全清理
rm -rf "${BUILD_DIR}"
mkdir -p "${BUILD_DIR}"

# 3. 强制 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"

# 4. cmake 配置(静态库,避免共享库干扰)
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

# 5. 编译
cmake --build . -- -j$(nproc)

# 6. 结果提示
echo "=== Build finished ==="
echo "Executable: ${BUILD_DIR}/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
5
6
7
8
# opj_compress 编码器路径
OPJ_COMPRESS="/workspace/openjpeg/build_ASan/bin/opj_compress"
# $FILE poc文件路径
POC_RAW="/workspace/poc/2020-27814.png"  
# null.j2k 编码输出文件路径
OUT_FILE="/tmp/null.j2k"

/workspace/openjpeg/build_ASan/bin/opj_compress  -i /workspace/poc/2020-27814.png -o /tmp/null.j2k -M 3 

我使用的poc文件链接附上:2020-27814.png

调整路径并执行后成功触发漏洞,显著标志为 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@c9f0b3f51ef2:/workspace# /workspace/openjpeg/build_ASan/bin/opj_compress  -i /workspace/poc/2020-27814.png -o /tmp/null.j2k -M 3 

[INFO] tile number 1 / 1
=================================================================
==1733==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x602000000097 at pc 0x000000760ff5 bp 0x7ffe38b39d30 sp 0x7ffe38b39d28
WRITE of size 1 at 0x602000000097 thread T0
    #0 0x760ff4  (/workspace/openjpeg/build_ASan/bin/opj_compress+0x760ff4)
    #1 0x760848  (/workspace/openjpeg/build_ASan/bin/opj_compress+0x760848)
    #2 0x739861  (/workspace/openjpeg/build_ASan/bin/opj_compress+0x739861)
    #3 0x6a45da  (/workspace/openjpeg/build_ASan/bin/opj_compress+0x6a45da)
    #4 0x599a27  (/workspace/openjpeg/build_ASan/bin/opj_compress+0x599a27)
    #5 0x6a24f0  (/workspace/openjpeg/build_ASan/bin/opj_compress+0x6a24f0)
    #6 0x662f1c  (/workspace/openjpeg/build_ASan/bin/opj_compress+0x662f1c)
    #7 0x6614f8  (/workspace/openjpeg/build_ASan/bin/opj_compress+0x6614f8)
    #8 0x60426b  (/workspace/openjpeg/build_ASan/bin/opj_compress+0x60426b)
    #9 0x601770  (/workspace/openjpeg/build_ASan/bin/opj_compress+0x601770)
    #10 0x5d975f  (/workspace/openjpeg/build_ASan/bin/opj_compress+0x5d975f)
    #11 0x5d7c48  (/workspace/openjpeg/build_ASan/bin/opj_compress+0x5d7c48)
    #12 0x5a7034  (/workspace/openjpeg/build_ASan/bin/opj_compress+0x5a7034)
    #13 0x527420  (/workspace/openjpeg/build_ASan/bin/opj_compress+0x527420)
    #14 0x797c47c02c86  (/lib/x86_64-linux-gnu/libc.so.6+0x21c86)
    #15 0x42c6b9  (/workspace/openjpeg/build_ASan/bin/opj_compress+0x42c6b9)

0x602000000097 is located 0 bytes to the right of 7-byte region [0x602000000090,0x602000000097)
allocated by thread T0 here:
    #0 0x4ec570  (/workspace/openjpeg/build_ASan/bin/opj_compress+0x4ec570)
    #1 0x5a823c  (/workspace/openjpeg/build_ASan/bin/opj_compress+0x5a823c)
    #2 0x66f842  (/workspace/openjpeg/build_ASan/bin/opj_compress+0x66f842)
    #3 0x65fa9c  (/workspace/openjpeg/build_ASan/bin/opj_compress+0x65fa9c)
    #4 0x65985e  (/workspace/openjpeg/build_ASan/bin/opj_compress+0x65985e)
    #5 0x5d815e  (/workspace/openjpeg/build_ASan/bin/opj_compress+0x5d815e)
    #6 0x5d7679  (/workspace/openjpeg/build_ASan/bin/opj_compress+0x5d7679)
    #7 0x5a7034  (/workspace/openjpeg/build_ASan/bin/opj_compress+0x5a7034)
    #8 0x527420  (/workspace/openjpeg/build_ASan/bin/opj_compress+0x527420)
    #9 0x797c47c02c86  (/lib/x86_64-linux-gnu/libc.so.6+0x21c86)

SUMMARY: AddressSanitizer: heap-buffer-overflow (/workspace/openjpeg/build_ASan/bin/opj_compress+0x760ff4) 
Shadow bytes around the buggy address:
  0x0c047fff7fc0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0c047fff7fd0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0c047fff7fe0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0c047fff7ff0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0c047fff8000: fa fa 00 00 fa fa 00 00 fa fa 00 00 fa fa 00 fa
=>0x0c047fff8010: fa fa[07]fa fa fa 07 fa fa fa 07 fa fa fa 07 fa
  0x0c047fff8020: fa fa 07 fa fa fa 07 fa fa fa 07 fa fa fa 07 fa
  0x0c047fff8030: fa fa 07 fa fa fa 07 fa fa fa 07 fa fa fa 07 fa
  0x0c047fff8040: fa fa 07 fa fa fa 07 fa fa fa 07 fa fa fa 07 fa
  0x0c047fff8050: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c047fff8060: 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
  Freed heap region:       fd
  Stack left redzone:      f1
  Stack mid redzone:       f2
  Stack right redzone:     f3
  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
==1733==ABORTING

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

1
2
3
4
cd openjpeg
git checkout eaa098b59b346cb88e4d10d505061f669d7134fc
cd ..
./setup.sh && /workspace/openjpeg/build_ASan/bin/opj_compress  -i /workspace/poc/2020-27814.png -o /tmp/null.j2k -M 3 

这次就被补丁提前拦截了,检测到缓冲区空间不足,并没有再进行强行分配。 补丁生效

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: 9 ms 

PoC分析

2020-27814.png 文件结构仅 386 字节:IHDR 将画布设为 32×32 并使用 palette 类型,后续 PLTE、tRNS、bKGD、pHYs 等辅助块长度均被压缩到最小,使得实际 IDAT 压缩域不足 90 byte;当 -M 3 把整图切成单一 tile 后,编码器为 code-block 计算的“理论上限”恰好是 7 byte,而 MQ-Coder 在 flush 阶段需要再写 1 byte,于是 mqc->bp 越界写入堆块下一个字节的头部元数据,从而触发 ASan 报错。


补丁分析

修复commit详见Encoder: grow buffer size in opj_tcd_code_block_enc_allocate_data() t… · rouault/openjpeg@eaa098b

  1. 主要改动

    l_data_size = 2 + ... 改为 l_data_size = 26 + ...,并在注释中列出 +1 (issue #835) / +2 (issue #982) / +7 (issue #1283 -M 3) / +26 (issue #1283 -M 44) 的累加预留,保证后续 mqc->bp++ 不会超出边界。

  2. 为什么能修复问题

    26 byte 的 head-room 已覆盖目前所有已知 -M 组合的最坏情况;同时保持向上 4-byte 对齐,防止未来再次出现 off-by-one。

  3. 补丁的局限与建议

    该修复属于「经验式」扩容,并未从理论上证明 upper-bound。若后续引入新编码特性,仍需重新评估缓冲区大小;建议增加运行时断言 assert(mqc->bp <= data + data_size) 以在调试期快速发现类似问题。


复现镜像

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

1
docker pull choser/openjpeg_cve-2020-27814:latest

内含:

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

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

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

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

 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

总结和启示

  • 编码器中的「理论上限」计算必须考虑所有后续阶段(如 flush)的额外开销,否则极易出现 off-by-one。
  • 对安全敏感的图像库,建议默认启用 AddressSanitizer 持续集成,能在第一时间发现类似堆溢出。
  • 该漏洞被分配 CVE-2020-27814,CVSS 3.x 评分 7.8(HIGH),影响 OpenJPEG ≤ 2.2.0,请确保使用 2.4.0 及以上版本。

参考链接

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