H.264 bitstreams provide a rich, structured surface for steganographic embedding due to their hierarchical syntax and redundancy in encoding conventions. This article details how to locate, parse, and modify syntactically valid NAL units—specifically targeting slice headers and SEI messages—to embed recoverable metadata without breaking decodability.
Video Data Abstraction Layers
Common container formats (e.g., MP4, FLV, MKV) encapsulate compressed elementary streams. The actual visual information resides in the encoded video bitstream, typically conforming to H.264/AVC standards. Transport protocols (RTMP, HTTP-FLV, HLS) deliver these containers but do not alter the underlying bitstream semantics.
Extracting Raw H.264 Bitstream
To isolate the raw byte sequence without re-encoding, use ffmpeg with stream copying:
ffmpeg -vcodec copy -i input.mp4 -f h264 output.h264
Omitting -vcodec copy would trigger transcoding, altering payload integrity. The resulting .h264 file contains Annex B–formatted NAL units: each prefixed by a start codde (00 00 01 or 00 00 00 01).
NAL Unit Structure and Byte Stream Processing
An Annex B bitstream is segmented into NAL units. To reconstruct original RBSP (Raw Byte Sequence Payload), escape sequences must be reversed:
00 00 03 00→00 00 0000 00 03 01→00 00 0100 00 03 02→00 00 0200 00 03 03→00 00 03
Given a sample NAL unit:
00 00 00 01 67 64 00 1F AC D9 40 50 ...
- Full NALU (after start code removal):
67 64 00 1F ... - EBSP (header stripped):
64 00 1F ... - RBSP (escape bytes removed):
64 00 1F ... 00 00 00 ...
The first byte (0x67) encodes NAL unit properties:
forbidden_zero_bit = (byte & 0x80) >> 7
nal_ref_idc = (byte & 0x60) >> 5
nal_unit_type = byte & 0x1F
Here, nal_unit_type == 7 indicates an SPS, 5 an IDR, and 1 a P-slice.
Parsing RBSP Syntax Elements
RBSP payloads follow syntax tables defined in ITU-T H.264 (e.g., Section 7.3). Key descriptors include:
ue(v): unsigned exponential-Golomb-coded integerse(v): signed exponential-Golomb-coded integer
For ue(v) encoding of value v:
- Compute
v + 1 - Write its binary representation
- Prepend
floor(log2(v+1))zero bits
Example: v = 7 → 7 + 1 = 8 → 1000₂ (4 bits) → prepend 3 zeros → 0001000.
A typical IDR RBSP begins with 0x88, whose binary 10001000 decodes as ue(7) followed by further fields.
Critical NAL Unit Types
- SPS/PPS: Contain global decoding parameters (resolution, profile, level). Must precede any IDR frame.
- IDR (nal_unit_type = 5): Self-contained I-frame; resets decoder state. All IDRs are I-frames, but not all I-frame are IDRs.
- Non-IDR I/P/B slices (types 1–3): Reference-dependent frames used for inter-frame compression.
- SEI (type = 6): User-data carrier with no impact on decoding logic—ideal for steganographic payloads.
Embedding Strategy via Slice Header Mutation
A robust embedding method modifies slice_type within slice layer RBSPs while preserving structural validity:
- IDR slices encode
slice_type = 2(I-frame) by default; changing it to11(B-frame) breaks playback but remains syntactically legal. - Non-IDR slices can be uniformly mutated from
P (1)orI (2)toB (1)using bitwise OR with mask0x04, sinceue(1)=0b001andue(3)=0b00011— both share leading bits alllowing safe low-bit toggling.
def mutate_slice_type(rbsp_bytes: bytes, new_type_ue: int) -> bytes:
if len(rbsp_bytes) < 2:
return rbsp_bytes
# Parse first_mb_in_slice (ue)
bs = BitStream(rbsp_bytes[1:])
_ = bs.ue() # consume first_mb_in_slice
old_type_ue = bs.ue()
# Replace type field in-place using bit-level injection
# Assuming ue(new_type_ue) fits within same byte boundary as old_type_ue
new_bits = encode_ue(new_type_ue)
# Implementation omitted for brevity — requires bit-level overwrite
return inject_bits(rbsp_bytes, bs.bit_pos, new_bits)
Payload Storage Using SEI Messages
To enable recovery, original slice_type values are serialized and embedded in custom SEI messages following each IDR:
def build_sei_payload(types: list[str]) -> bytes:
uuid_bytes = os.urandom(16)
payload = uuid_bytes + ''.join(types).encode('ascii')
payload_len = len(payload)
sei_header = b'\x00\x00\x01\x06' # NALU start + SEI type
sei_size_field = bytes([min(payload_len, 255)])
# Handle payload > 255 bytes via multiple SEI NALUs (omitted)
return sei_header + b'\x05' + sei_size_field + payload + b'\x80'
Each SEI NALU is inserted immediately after an IDR NALU in the bitstream.
Reconstruction Workflow
Recovery involves:
- Parsing all NALUs and identifying IDRs and subsequent SEIs.
- Extracting and decoding SEI payloads to retrieve original frame-type sequence.
- Reverting
slice_typebits in corresponding slices using XOR masking (^ 0x04). - Reassembling the corrected bitstream and remuxing.
ffmpeg -i repaired.h264 -c:v copy -f mp4 restored.mp4
This approach ensures decoder compatibility before and after tampering, enabling CTF-style challenges where visual corruption is reversible solely through bitstream analysis.