Static Image
Static Image
Platform: Broncoctf2026 | Category: Steganography | Type: Challenge | Difficulty: Medium | OS: NA | Author: D3v0o0Nu11 | Date: 2026-07-11 | Status: Solved Techniques: ffmpeg_rawvideo_dump, majority_vote_glyph_extraction, near_duplicate_pair_xor, telecine_cadence_detection
Summary
Task: a 300x300 60fps monochrome TV-static MP4 with no audio; the pun 'now that's dynamic' points to inter-frame motion, not per-pixel temporal aggregates. Solution: detect the telecine cadence where frame[i] and frame[i+2] are bit-identical except inside a fixed banner box, XOR the thresholded near-duplicate pairs to cancel the frozen noise and reveal one flag glyph per ~10-frame block, majority-vote and concatenate 25 glyphs.
Recon
Port scan
nmap -p- -sV -sC <TARGET> --min-rate 1000 -Pn
| Port | Service | Version | Notes |
|---|---|---|---|
| <PORT> | <SVC> | <VER> | <notes> |
Enumeration highlights
- Event:
broncoctf2026| ID:20260711_broncoctf2026_static_image - Tags: ffmpeg, mp4, video_steganography, telecine, frame_differencing, xor_frame_diff, monochrome_noise, temporal_analysis
- Indicators: monochrome TV-static MP4 with no audio, 60fps with very high bitrate to preserve noise near-losslessly, title/description pun on 'static' vs 'dynamic, frame[i] bit-identical to frame[i+2] (telecine cadence), near-duplicate frame pairs differ only inside a small fixed rectangular box
- Source:
20260711_broncoctf2026_static_image.md
Foothold
Vulnerability / Misconfiguration
- Ffmpeg_rawvideo_dump
- Majority_vote_glyph_extraction
- Near_duplicate_pair_xor
- Telecine_cadence_detection
<command>
Exploitation
- See original writeup content for detailed exploitation.
Privilege Escalation
Enumeration
sudo -l find / -perm -4000 2>/dev/null getcap -r / 2>/dev/null cat /etc/crontab ps aux
Exploitation
- N/A for challenge-type writeup; see exploitation above.
- Flag obtained via challenge solve.
<command>
Flags
| Flag | Location | Value |
|---|---|---|
| flag | REDACTED |
Key Takeaways / Lessons
- ffmpeg_rawvideo_dump
- majority_vote_glyph_extraction
- near_duplicate_pair_xor
- telecine_cadence_detection
- Tags: ffmpeg, mp4, video_steganography, telecine, frame_differencing, xor_frame_diff, monochrome_noise, temporal_analysis
Original Writeup
<details><summary>Click to expand original content</summary>Description
"It's not often that you do static analysis outside of Reversing, is it?"
Artifact:
static.mp4
English summary: We are given static.mp4, a 300x300, 60fps, 1500-frame,
yuv420p, monochrome "TV static" white-noise video with no audio and a single
video stream (~58 MB, ~18 Mbps — a bitrate high enough to preserve the noise
nearly losslessly). The flag is hidden somewhere in the noise; the goal is to
recover it.
Analysis
Semantic clue — the whole challenge is a pun
- "static" = TV static (visual noise) AND static analysis.
- The intended read of the description is a play on dynamic: "now that's
dynamic". The flag itself is
bronco{REDACTED}. - Takeaway: the signal is not in any static/averaged image of the noise — it is in the motion between frames (the dynamic part).
Why per-pixel temporal aggregation fails
The obvious first attack on a stack of noise frames is to reduce them per pixel over time (mean, median, min/max, mode, std, white-probability, bit-plane counts, temporal FFT). Every single one of these produced pure noise:
- The per-pixel white-probability across 1500 frames ranges 0.43–0.57, which is exactly the spread of 1500 fair Bernoulli samples. There is no embedded per-pixel bias image.
- Temporal FFT, 2D spatial FFT magnitude/phase, phase-coherent complex average
- inverse, global XOR of all frames, transition counts, first-vs-last diff — all noise, no watermark.
Conclusion: no information is carried by the marginal distribution of any single pixel. The data must live in the temporal correlation structure between frames.
The telecine cadence
Comparing thresholded frames revealed that most frames are bit-identical to
the frame two positions later: threshold(frame[i]) == threshold(frame[i+2]).
The video is a telecine-style cadence — a frozen noise background is repeated on
a fixed period. Treated as a bitstream (i == i+2 or not) this cadence is a
perfectly periodic square wave (period 60, "111111100000..."); it is pure
structure and carries no data by itself. This is a decoy: it tells you the
video is built from repeated frozen frames, nothing more.
The real signal — near-duplicate-but-different pairs
In 25 evenly spaced blocks of ~10 consecutive frames each (block start frames
33, 93, 153, ..., 1473; period 60, hence 25 blocks), the pair (i, i+2) is
not identical — but the difference is confined to a small fixed
rectangular box (e.g. around x=88–215, y=232–263 for one block). Outside that
box the two frames are the same frozen noise.
Because the background noise is identical in both frames of a near-duplicate
pair, threshold(frame[i]) ^ threshold(frame[i+2]) cancels the frozen
background and leaves only the box — which contains a single flag glyph
(one character). 25 blocks → 25 characters → the full flag.
Averaging the XOR mask over all ~10 pairs in a block (majority vote) removes the residual per-pixel noise flicker inside the box and yields a clean glyph.
Solution
- Dump all frames to raw grayscale with ffmpeg.
- Threshold each frame at 127 to a boolean image.
- Find every
(i, i+2)pair whose Hamming distance is nonzero but small (0 < d < 20000) — these are the near-duplicate-but-different pairs. - Group consecutive such pairs (step ≤ 3) into per-character blocks.
- Per block, accumulate
threshold(frame[i]) ^ threshold(frame[i+2])and majority-vote to a clean glyph. - Crop each glyph and concatenate all 25 in order → the flag.
#!/usr/bin/env python3
import subprocess, numpy as np, cv2
# 1. dump all 1500 frames as raw 8-bit grayscale
raw = subprocess.check_output(
['ffmpeg', '-v', 'error', '-i', 'static.mp4',
'-f', 'rawvideo', '-pix_fmt', 'gray', '-'])
a = np.frombuffer(raw, np.uint8).reshape(-1, 300, 300)
th = a > 127 # 2. threshold -> boolean
# 3. near-duplicate telecine pairs (i, i+2): differ, but only slightly
pairs = []
for i in range(len(a) - 2):
d = np.count_nonzero(th[i] ^ th[i + 2])
if 0 < d < 20000:
pairs.append((i, i + 2, d))
# 4. group consecutive pairs (step 3) into per-character blocks
blocks, cur = [], [pairs[0]]
for p in pairs[1:]:
if p[0] - cur[-1][0] <= 3:
cur.append(p)
else:
blocks.append(cur); cur = [p]
blocks.append(cur)
# 5. per block: majority-vote XOR mask -> one glyph
crops = []
for blk in blocks:
acc = np.zeros((300, 300), int)
for (i, j, d) in blk:
acc += (th[i] ^ th[j])
g = (acc > len(blk) // 2).astype('uint8') * 255
ys, xs = np.where(g > 0)
crops.append(g[ys.min():ys.max() + 1, xs.min():xs.max() + 1])
# 6. crop + concatenate all 25 glyphs in order
H = max(c.shape[0] for c in crops)
row = [np.pad(c, ((0, H - c.shape[0]), (3, 3))) for c in crops]
cv2.imwrite('flag_strip.png', np.hstack(row)) # reads: bronco{REDACTED}
The output image flag_strip.png spells the flag left-to-right across its 25
glyphs.
Dead ends (conclusively ruled out — do not repeat)
All of the following produced only noise:
- Per-pixel temporal aggregates: mean, median, min, max, mode, std, range,
white-probability, counts of
==0/==255/>=240. (White-prob range 0.43–0.57 = expected spread of 1500 Bernoulli samples; no embedded bias image.) - Frequency domain: temporal FFT per pixel; averaged 2D spatial FFT magnitude/phase; phase-coherent complex average + inverse.
- Whole-stack ops: global XOR of all frames, transition counts, first-vs-last diff.
- Exact-duplicate cadence alone (
i == i+2): a perfectly periodic square-wave (period 60) — telecine structure, carries no data by itself. - Per-frame metrics: brightness, entropy, neighbor-similarity — no single frame is a clean image.
- Chroma / channels: U/V constant ~127 (monochrome), R=G=B.
- Byte/container level: spatial LSB within frames; raw MP4 byte bit-planes;
packet sizes as an image; container atoms clean (
ftyp/free/mdat/moov, no trailing/appended data, no extra tracks/metadata); MPEG-4user_dataonly holds the encoder string; codec QP uniform.
The signal is only in the near-duplicate-but-different (i, i+2) pairs.
Auto-tracked: saved to WriteUps; run
/xesor-reviseto fold lessons into XESXor_Methodology.md.
signed by XESXOR