home intel cve-2025-64784-dng-sdk-heap-buffer-overflow
CVE Analysis 2025-12-09 · 8 min read

CVE-2025-64784: DNG SDK 1.7.0 Heap Overflow via Malicious Image File

DNG SDK ≤1.7.0 contains a heap-based buffer overflow in its tile/strip data parsing path. A crafted DNG file can expose sensitive heap memory or crash the host application.

#heap-buffer-overflow#memory-disclosure#denial-of-service#dng-sdk#user-interaction-required
Technical mode — for security professionals
▶ Attack flow — CVE-2025-64784 · Buffer Overflow
ATTACKERRemote / unauthBUFFER OVERFLOWCVE-2025-64784Cross-platform · HIGHCODE EXECArbitrary coderuns as targetCOMPROMISEFull accessNo confirmed exploits

Vulnerability Overview

CVE-2025-64784 is a heap-based buffer overflow affecting Adobe's open-source DNG SDK versions 1.7.0 and earlier. The vulnerability lives in the raw image data ingestion pipeline — specifically in the logic that allocates and fills pixel tile or strip buffers from attacker-controlled fields in a DNG/TIFF IFD (Image File Directory). A malformed DNG file can cause the SDK to allocate a buffer sized from one set of IFD tags while copying data sized from a different, larger field, overflowing the heap allocation and corrupting adjacent objects.

CVSS 7.1 (HIGH) reflects the combination of memory disclosure potential and reliable denial-of-service with no authentication barrier — the only prerequisite is that a victim opens the file. Any application embedding the DNG SDK (Lightroom, Camera Raw, third-party raw editors) inherits the exposure.

Root cause: The SDK computes a pixel buffer allocation size from TileWidth × TileLength × SamplesPerPixel × BitsPerSample but copies tile byte data using the raw TileByteCount IFD entry without validating that it fits within the computed allocation, allowing an attacker-controlled count to drive a heap overflow of arbitrary size.

Affected Component

The bug is reachable through dng_tiff_directory::ReadImageData() and its callers in dng_image_writer / dng_read_image, ultimately tracing to the tile-reading helper in dng_stream.cpp and the buffer management in dng_memory.cpp. The SDK is distributed as source and compiled into host applications; there is no shared library boundary.

  • Affected versions: DNG SDK ≤ 1.7.0
  • File format entry point: TIFF/DNG IFD tag 0x0144 (TileOffsets), 0x0145 (TileByteCounts), 0x0142 (TileWidth), 0x0143 (TileLength)
  • Trigger: User opens a malicious .dng file

Root Cause Analysis

The SDK pre-computes expected tile storage from image geometry and then reads each tile's byte count directly from the IFD without cross-checking it against the pre-computed size. Below is reconstructed pseudocode reflecting the SDK's class structure and naming conventions:


// dng_read_image.cpp — reconstructed from DNG SDK 1.7.0 source conventions
// Processes one tile from a tiled DNG/TIFF image.

void dng_read_image::ReadTile(dng_host &host,
                               dng_ifd   &ifd,
                               dng_stream &stream,
                               dng_image  &image,
                               uint32      tileIndex)
{
    // Allocation size derived from image geometry (attacker-controlled IFD fields)
    uint32 tileW      = ifd.fTileWidth;           // tag 0x0142
    uint32 tileH      = ifd.fTileLength;          // tag 0x0143
    uint32 samples    = ifd.fSamplesPerPixel;     // tag 0x0115
    uint32 bitsPerSmp = ifd.fBitsPerSample[0];   // tag 0x0102

    // Expected size in bytes — used to size the allocation
    uint64 expectedBytes = (uint64)tileW * tileH *
                           samples * (bitsPerSmp >> 3);

    // BUG: tileByteCount comes from tag 0x0145 and is NEVER validated
    //      against expectedBytes before the copy below.
    uint64 tileByteCount = ifd.fTileByteCount[tileIndex];  // attacker-controlled

    // Allocation sized to expectedBytes (may be smaller than tileByteCount)
    AutoPtr block(host.Allocate(expectedBytes));
    uint8 *buf = block->Buffer_uint8();

    stream.SetReadPosition(ifd.fTileOffset[tileIndex]);

    // BUG: copies tileByteCount bytes into a buffer of expectedBytes.
    //      If tileByteCount > expectedBytes, heap overflow occurs here.
    stream.Get(buf, tileByteCount);   // <-- OVERFLOW: no bounds check

    // Decompress / unpack pixels from buf ...
    DecodeUncompressed(buf, tileByteCount, image, tileIndex, ifd);
}

The mismatch between the allocation path (expectedBytes) and the copy path (tileByteCount) is the complete bug. Both values are derived from attacker-controlled IFD fields with no relationship enforced between them.

Memory Layout

The SDK allocates tile buffers through dng_memory_block, which wraps a raw malloc/new[] call. Adjacent allocations on the heap are typically the next tile's buffer or an internal dng_memory_block header.


// dng_memory.h — dng_memory_block layout (reconstructed)
struct dng_memory_block {
    /* +0x00 */ dng_memory_allocator *fAllocator;  // back-pointer to allocator
    /* +0x08 */ uint32                fLogicalSize; // requested size (expectedBytes)
    /* +0x0C */ uint32                fPad;         // alignment padding
    /* +0x10 */ uint8                 fData[];      // pixel data starts here
};
// Total header overhead: 0x10 bytes
// Allocator rounds fLogicalSize up to 16-byte alignment before malloc()

HEAP STATE BEFORE OVERFLOW:
  (tile 0 — legitimate, fits)
  [ dng_memory_block hdr  | fLogicalSize=0x3000  | fData[0x3000] ]
  
  (tile 1 — victim allocation)
  [ dng_memory_block hdr  | fLogicalSize=0x3000  | fData[0x3000] ]
    ^--- fAllocator = valid ptr
    ^--- fLogicalSize = 0x00003000

  (next heap object — e.g., dng_linearization_info)
  [ dng_linearization_info | fBlackLevel[...] | fWhiteLevel[...] ]

HEAP STATE AFTER OVERFLOW:
  (tile 1 read with tileByteCount = 0x5F00, allocated only 0x3000)
  [ dng_memory_block hdr  | fLogicalSize=0x3000  | fData[0x3000]
    + 0x2F00 bytes OVERFLOW ---------------------> ]

  (dng_linearization_info — now corrupted)
  [ CORRUPTED: fBlackLevel[0] = attacker bytes ]
  [ CORRUPTED: fBlackLevel[1] = attacker bytes ]
  [ CORRUPTED: fWhiteLevel    = attacker bytes ]
  ...
  
  On heap info-leak path: corrupted struct fields are read back
  and embedded into processed image pixel data → memory disclosure.

Exploitation Mechanics

Two impact classes are realistic here: heap memory disclosure (info leak via pixel output) and denial of service (heap metadata corruption → crash). A full code-execution primitive would require a separate control-flow hijack not demonstrated in this writeup.


EXPLOIT CHAIN — Memory Disclosure (CVE-2025-64784):

1. Craft a DNG file with a tiled IFD:
     TileWidth        = 0x40         (64 px)
     TileLength       = 0x30         (48 px)
     SamplesPerPixel  = 4
     BitsPerSample    = 8
     → expectedBytes  = 64 × 48 × 4 × 1 = 0x3000 (12 288 bytes)

2. Set TileByteCount[0] = 0x5F00    (24 320 bytes — 0x2F00 past allocation)
   Set TileOffset[0]    = offset to a tile data blob in the file body.

3. Tile data blob: first 0x3000 bytes = arbitrary pixel data (filler).
   Remaining 0x2F00 bytes = 0x00 padding (reads heap bytes into "tile").

4. SDK calls stream.Get(buf, 0x5F00) into a 0x3000-byte allocation.
   The trailing 0x2F00 bytes read past the tile buffer, pulling bytes
   from the adjacent heap object (dng_linearization_info, next tile
   block header, allocator metadata, etc.) into the stream buffer.

5. SDK proceeds to DecodeUncompressed() treating all 0x5F00 bytes as
   pixel data. The overread heap bytes are packed into output image rows.

6. Host application writes the processed image (JPEG preview, TIFF export).
   Attacker reads the output file → heap bytes at known relative offsets
   are now embedded in pixel rows, leaking heap pointers and data.

7. With heap pointer leak in hand: ASLR defeated for the host process.
   (Full code-exec requires a second primitive — not demonstrated here.)

DENIAL-OF-SERVICE VARIANT:
1–3. Same setup.
4.   Set TileByteCount[0] = 0xFFFF0000 — massively oversized.
5.   stream.Get() writes far past the tile allocation into allocator
     metadata or another object's vtable pointer.
6.   Next virtual dispatch (e.g., dng_image::Get()) → null/wild deref.
7.   Segmentation fault. Process crashes. Reliable cross-platform DoS.

Patch Analysis

The correct fix enforces that tileByteCount never exceeds the computed allocation before any data is read. A secondary fix caps the allocation itself against a sane maximum to prevent integer-overflow-driven undersized allocations in the geometry calculation.


// BEFORE (vulnerable — DNG SDK ≤ 1.7.0):
void dng_read_image::ReadTile(dng_host &host, dng_ifd &ifd,
                               dng_stream &stream, dng_image &image,
                               uint32 tileIndex)
{
    uint64 expectedBytes  = (uint64)ifd.fTileWidth  *
                                    ifd.fTileLength *
                                    ifd.fSamplesPerPixel *
                                   (ifd.fBitsPerSample[0] >> 3);

    uint64 tileByteCount  = ifd.fTileByteCount[tileIndex]; // unchecked

    AutoPtr block(host.Allocate(expectedBytes));
    stream.SetReadPosition(ifd.fTileOffset[tileIndex]);
    stream.Get(block->Buffer_uint8(), tileByteCount);       // OVERFLOW
}

// AFTER (patched):
void dng_read_image::ReadTile(dng_host &host, dng_ifd &ifd,
                               dng_stream &stream, dng_image &image,
                               uint32 tileIndex)
{
    // Guard against integer overflow in geometry multiplication
    if (ifd.fTileWidth == 0 || ifd.fTileLength == 0 ||
        ifd.fSamplesPerPixel == 0 || ifd.fBitsPerSample[0] == 0)
        ThrowBadFormat();

    uint64 expectedBytes = (uint64)ifd.fTileWidth  *
                                   ifd.fTileLength *
                                   ifd.fSamplesPerPixel *
                                  (ifd.fBitsPerSample[0] >> 3);

    // Sanity cap: no single tile should exceed 256 MB
    if (expectedBytes > 0x10000000ULL)
        ThrowBadFormat();

    uint64 tileByteCount = ifd.fTileByteCount[tileIndex];

    // FIX: reject any tile whose stored byte count exceeds computed size
    if (tileByteCount > expectedBytes)
        ThrowBadFormat();   // abort parse, do not allocate

    AutoPtr block(host.Allocate(expectedBytes));
    stream.SetReadPosition(ifd.fTileOffset[tileIndex]);
    stream.Get(block->Buffer_uint8(), tileByteCount);       // safe
}

The ThrowBadFormat() call unwinds via C++ exception through dng_exception, which the host application catches and surfaces as a file-open error — no memory is corrupted, no data is returned.

Detection and Indicators

Static file-level detection is straightforward: a DNG file is malicious if any TileByteCount entry exceeds the corresponding geometry-derived size.


#!/usr/bin/env python3
# dng_tile_check.py — detect CVE-2025-64784 trigger condition in DNG files
import struct, sys

TIFFTAG_IMAGEWIDTH      = 0x0100
TIFFTAG_IMAGELENGTH     = 0x0101
TIFFTAG_BITSPERSAMPLE   = 0x0102
TIFFTAG_SAMPLESPERPIXEL = 0x0115
TIFFTAG_TILEWIDTH       = 0x0142
TIFFTAG_TILELENGTH      = 0x0143
TIFFTAG_TILEOFFSETS     = 0x0144
TIFFTAG_TILEBYTECOUNTS  = 0x0145

def read_ifd_value(data, offset, type_, count, endian):
    fmt = {1:'B',3:'H',4:'I',16:'Q'}.get(type_,'I')
    sz  = struct.calcsize(fmt)
    if count * sz <= 4:
        vals = [struct.unpack_from(endian+fmt, data, offset)[0]
                for i in range(count)]
    else:
        ptr = struct.unpack_from(endian+'I', data, offset)[0]
        vals = [struct.unpack_from(endian+fmt, data, ptr + i*sz)[0]
                for i in range(count)]
    return vals

def check_dng(path):
    data = open(path,'rb').read()
    endian = '<' if data[:2] == b'II' else '>'
    ifd_off = struct.unpack_from(endian+'I', data, 4)[0]
    num_entries = struct.unpack_from(endian+'H', data, ifd_off)[0]

    tags = {}
    for i in range(num_entries):
        entry_off = ifd_off + 2 + i * 12
        tag, type_, count = struct.unpack_from(endian+'HHI', data, entry_off)
        val_off = entry_off + 8
        tags[tag] = read_ifd_value(data, val_off, type_, count, endian)

    tw  = tags.get(TIFFTAG_TILEWIDTH,  [0])[0]
    th  = tags.get(TIFFTAG_TILELENGTH, [0])[0]
    spp = tags.get(TIFFTAG_SAMPLESPERPIXEL, [1])[0]
    bps = tags.get(TIFFTAG_BITSPERSAMPLE,   [8])[0]
    tbc = tags.get(TIFFTAG_TILEBYTECOUNTS,  [])

    expected = tw * th * spp * (bps >> 3)
    for idx, bc in enumerate(tbc):
        if bc > expected:
            print(f"[MALICIOUS] tile {idx}: TileByteCount={bc:#x} "
                  f"> expectedBytes={expected:#x} — CVE-2025-64784 trigger")
            return True
    print(f"[OK] {path}")
    return False

if __name__ == '__main__':
    check_dng(sys.argv[1])

Crash signatures to look for in production logs:


# macOS / Linux: heap corruption crash in dng_stream::Get
Signal:     SIGSEGV / SIGABRT
Crashed at: dng_stream::Get(void*, unsigned long)
            dng_read_image::ReadTile(...)
            dng_read_image::ReadImage(...)

# AddressSanitizer output (instrumented build):
==ERROR: AddressSanitizer: heap-buffer-overflow
WRITE of size 0x2F00 at offset 0x3000 in region [buf, buf+0x3000)
  #0 dng_stream::Get(void*, uint64)       dng_stream.cpp
  #1 dng_read_image::ReadTile(...)        dng_read_image.cpp
  #2 dng_read_image::ReadImage(...)       dng_read_image.cpp
  Shadow bytes around the overflow:
    [buf+0x2ff0]: 00 00 00 00 00 00 00 00
    [buf+0x3000]: fa fa fa fa fa fa fa fa  ← heap redzone begins
    [buf+0x3010]: OVERWRITTEN

Remediation

  • Upgrade immediately to DNG SDK version 1.7.1 or later when Adobe publishes the patched release. Monitor the Adobe Security Bulletins page.
  • Applications embedding the SDK as source must recompile against the patched SDK — there is no binary-only update path.
  • Mitigating controls (where patched SDK is not yet available):
    • Deploy the detection script above in a pre-ingestion pipeline to reject malformed DNG files before they reach the SDK.
    • Enable heap hardening: compile with -fsanitize=address in QA, and ship with tcmalloc or jemalloc's guard pages in production to convert overflows into hard crashes rather than silent corruption.
    • Sandbox the DNG-parsing process (seccomp-bpf on Linux, App Sandbox on macOS) so that a successful exploit cannot reach the broader file system or network.
  • SBOM / dependency audit: Search your software bill of materials for dng_sdk, dng_read_image, or XMP_SDK companion library bundles — many ship the DNG SDK inline without advertising the version.
CB
CypherByte Research
Mobile security intelligence · cypherbyte.io
// RELATED RESEARCH
// WEEKLY INTEL DIGEST

Get articles like this every Friday — mobile CVEs, threat research, and security intelligence.

Subscribe Free →