← Back to Writeups
CUSTOMMediumForensics

Современный формат (Modern Format)

XESXOR9/2/20264 min read
#forensics#custom#medium#av1_obu_concatenation#avif_sequence_recovery#hidden_track_sample_extraction#isobmff_box_parsing#linux

Современный формат (Modern Format)

Platform: bug-makers | Category: Forensics | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2025-10-04 | Status: Solved Techniques: av1_obu_concatenation, avif_sequence_recovery, hidden_track_sample_extraction, isobmff_box_parsing

Summary

Task: a static site serves a modern AVIF image that does not display in most browsers; the file is an AVIF image sequence with a faked stsz sample_count hiding a second frame. Solution: parse the ISOBMFF boxes to find the real track samples, concatenate both AV1 temporal units from mdat, and decode the raw OBU stream with ffmpeg to reveal the flag frame.

Современный формат (Modern Format) — bug-makers

Description

Я делаю статичный сайт с описанием персонажа аниме. Но у меня есть проблема с отображением изображения в большинстве браузеров. Я использую современный формат AVIF для оптимизации, но похоже что этот формат не кроссбраузерный. Адрес: http://31.207.77.216:4020

English summary: A static site describes the anime character Megumin (KonoSuba) and embeds <img src="/megumin-flag.avif">. The author complains the modern AVIF image "doesn't display in most browsers." The filename ("flag") and the cross-browser hint point to a hidden frame inside the AVIF. The goal is to recover that hidden frame containing the flag. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Recon

Fetch the page and download the image:

bash
curl -s http://31.207.77.216:4020/ | grep -i avif
# <img src="/megumin-flag.avif">

curl -s -o megumin-flag.avif http://31.207.77.216:4020/megumin-flag.avif
file megumin-flag.avif
# megumin-flag.avif: ISO Media, AVIF Image Sequence

Key insight: the file is an AVIF Image Sequence (a 4-second, 1920x1080 animation), not a single still. Browsers lacking AVIF-sequence support only render the primary still item — exactly the "not cross-browser" symptom described. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Default decoders only emit the first frame:

bash
ffmpeg -i megumin-flag.avif frame_%03d.png   # -> only ONE frame (standing Megumin, no flag)
avifdec megumin-flag.avif out.png            # -> "BMFF parsing failed / Truncated sample table"

Analysis

The file is ISOBMFF (MP4-family). Parsing the box tree with a small Python script reveals:

  • ftyp — brand avif
  • meta — pitm (primary item id=1), iloc, iinf, iprp/ipco (ispe, pixi, av1C, colr), ipma
  • moov — a video track (the AVIF sequence) with mdia/minf/stbl → stsd, stts, stsc, stsz, stco, stss
  • uuid — an XMP block with an ezgif.com software tag (red herring)
  • mdat — the actual AV1 payload ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

The primary item (what browsers show) is located via iloc:

extent_offset = 0x0eea = 3818
extent_length = 0xbfa0 = 49056   ->  bytes [3818:52874] = standing Megumin still

The trick is in the track's stsz (sample size) box:

raw stsz: 0000001c 7374737a 00000000 00000000 00000001 0000bfa0 0000b1c6
                                      ^sample_size=0  ^count=1  ^entry1   ^entry2

sample_size = 0 means per-sample sizes follow, but sample_count is faked to 1 while two entries are actually present: ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

  • entry1 = 0x0000bfa0 = 49056 (the standing still / primary item)
  • entry2 = 0x0000b1c6 = 45510 (the hidden FLAG frame)

Cross-checking the other tables confirms two real samples:

  • stts: 1 entry, sample_count=2, delta=0xc8 (200)
  • stsc: 1 chunk, 2 samples per chunk
  • stco: single chunk offset = 0xeea = 3818

The faked stsz sample_count=1 is exactly what produces avifdec's "Truncated sample table" and makes naive decoders stop after the first frame.

Offsets line up perfectly:

sample1 @ 3818, len 49056  -> ends at 52874  (= primary item)
sample2 @ 52874, len 45510 -> ends at 98384  (= exact file size)
3818 + 49056 + 45510 = 98384  ✓

‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Solution

Both samples sit contiguously in mdat and each begins with an AV1 temporal-delimiter OBU (12 00). Sample 1 carries the sequence-header OBU; sample 2 is a frame-only OBU that reuses it. The reliable recovery is to dump both temporal units as one raw AV1 OBU stream and decode it:

python
#!/usr/bin/env python3
# Extract both AVIF-sequence samples from mdat as a continuous AV1 OBU stream.
data = open('megumin-flag.avif', 'rb').read()
# chunk offset from stco = 3818; both samples are contiguous to EOF (98384)
both = data[3818:98384]          # sample1 (seq header + frame) + sample2 (frame)
open('both.obu', 'wb').write(both)
print('wrote', len(both), 'bytes')

‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

bash
ffmpeg -i both.obu both_%03d.png -y
# -> both_001.png : Megumin standing (still)
# -> both_002.png : Megumin collapsed in a grassy field, flag overlaid bottom-left

both_002.png reveals the flag.

What Failed (for future solvers)

  • Default ffmpeg / avifdec / magick decode only the first (primary) frame because of the faked stsz sample_count.
  • Patching stsz sample_count from 1 to 2 alone did not make ffmpeg emit the second frame.
  • Rebuilding a fresh minimal AVIF container around only the extracted second sample failed with "Error parsing OBU data" / "getNextImage() failed": sample 2 is a FRAME-only OBU (12 00 32 ..., OBU type 6) that depends on sample 1's sequence-header OBU (0a 0b ...).
  • The uuid/XMP box (ezgif.com software tag) is standard metadata — a non-issue.
  • The winning method: concatenate both temporal units (sample1 provides the sequence header, sample2 is the dependent frame) and decode as one continuous raw AV1 OBU stream. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍