H.264 Bitstream Manipulation for Steganographic Payload Embedding

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 0000 00 00
  • 00 00 03 0100 00 01
  • 00 00 03 0200 00 02
  • 00 00 03 0300 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 integer
  • se(v): signed exponential-Golomb-coded integer

For ue(v) encoding of value v:

  1. Compute v + 1
  2. Write its binary representation
  3. Prepend floor(log2(v+1)) zero bits

Example: v = 77 + 1 = 81000₂ (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 to 11 (B-frame) breaks playback but remains syntactically legal.
  • Non-IDR slices can be uniformly mutated from P (1) or I (2) to B (1) using bitwise OR with mask 0x04, since ue(1) = 0b001 and ue(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:

  1. Parsing all NALUs and identifying IDRs and subsequent SEIs.
  2. Extracting and decoding SEI payloads to retrieve original frame-type sequence.
  3. Reverting slice_type bits in corresponding slices using XOR masking (^ 0x04).
  4. 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.

Tags: h264 Steganography video-forensics nal-unit exponential-golomb

Posted on Tue, 28 Jul 2026 16:27:45 +0000 by demonicfoetus