~/blog/mp4-for-reversers.md

The MP4 Container for Reverse Engineers

· #reverse-engineering #formats

Every MP4 you’ll ever see, regardless of what camera or app or CDN produced it, starts with the same eight bytes in the same shape: a 4-byte big-endian size, followed by the ASCII tag ftyp. That’s not a coincidence you can exploit once — it’s a guarantee you can lean on every time, because it’s how the format itself is defined.

00 00 00 18  66 74 79 70  69 73 6F 6D  00 00 02 00
└─size=24─┘  └──"ftyp"──┘ └─"isom"───┘ └─version─┘

Boxes are TLV, and TLV is a reverser’s best friend

MP4 (and its relatives — MOV, HEIC, anything built on ISO BMFF) is not a bespoke binary blob. It’s a sequence of boxes (Apple calls them atoms), and every box has the same three-part shape:

  • 4 bytes: box size, big-endian, including the 8-byte header itself
  • 4 bytes: box type, always four printable ASCII characters (ftyp, moov, mdat, free…)
  • the rest: payload, which for container boxes (moov, trak, mdia…) is just more boxes, nested

That’s the whole grammar. A box with size 1 (not 0) means “the real size is a 64-bit value in the next 8 bytes,” for files bigger than 4GB. A size of 0 means “this box runs to the end of the file.” Everything else is exactly what it says on the label.

import struct

def walk_boxes(data: bytes, offset=0, end=None):
    end = len(data) if end is None else end
    while offset < end:
        size, box_type = struct.unpack_from(">I4s", data, offset)
        if size == 1:
            size = struct.unpack_from(">Q", data, offset + 8)[0]
        elif size == 0:
            size = end - offset
        yield box_type.decode("ascii", "replace"), offset, size
        offset += size

Fourteen lines, and you can enumerate every top-level box in any MP4-family file without a library, without knowing anything about video codecs, and without caring whether the payload is H.264, HEVC, or something proprietary.

What actually matters if you don’t care about video

Most reversing tasks against MP4 files have nothing to do with decoding frames. What you usually want:

  • ftyp — the brand tells you what produced the file and what it claims to be compatible with. Cheap fingerprinting, first eight bytes, zero parsing needed.
  • moov — the metadata tree: track count, durations, sample tables (where each frame lives in the file, its size, its timestamp). This is almost always not the obfuscated part, even in protected files, because players need to seek before they decode.
  • mdat — the actual media bytes. This is where an app is most likely to apply its own scrambling on top of the standard container, because it’s the only part a generic player can’t make sense of without cooperating with the app anyway.

That split — cleartext structure around an obfuscated payload — is common precisely because breaking the seekability of a file is expensive for the app to deal with (every player, including their own, needs moov to work at all), while the raw sample bytes inside mdat are opaque to any generic decoder to begin with. So apps scramble mdat and leave moov alone. Once you know that, you know exactly which box to skip past and which one to attack.

Why the fixed header is free plaintext

Here’s the payoff for reversing anything that wraps or scrambles an MP4: you know, with certainty, that a real MP4 has ftyp at a small, predictable offset near the start of the file — the size field ahead of it varies by a few bytes depending on the brand list, but it’s never far from byte zero. If a file that’s supposed to be an MP4 comes out looking like noise, you already have your first eight known-plaintext bytes (66 74 79 70, literally the ASCII for “ftyp”) without opening a debugger, and often the size field ahead of it too, since common brand strings (isom, mp42, M4V ) produce a short, guessable set of size values.

That’s the whole reason this format matters to a reverser who’s never going to touch a video codec: it’s not the box grammar you’re exploiting, it’s the fact that the grammar is fixed enough to hand you a crib for free.

Takeaway

Learn the box walker once — fourteen lines, no dependencies — and you can orient yourself in any ISO BMFF file in under a minute: what’s cleartext, what’s obfuscated, and exactly which eight bytes you already know before you’ve read a single byte of the actual file.

cd .. cd ~ (back to terminal)