mp4を読んでみる

はじめに

こんにちは!Garoon開発チームのYukimiチームにいます、てきめん です。

Yukimiチームは、より安全なGaroonをユーザーに届けることを目標とし、将来に渡って継続的にGaroonを安定提供できるように活動しているチームです。Yukimiチームのことについては以下を参照してみてください。

blog.cybozu.io

さて、今日は、ファイルフォーマットの一つで動画のフォーマットである、mp4について見ていきたいと思います。

この記事は、CYBOZU SUMMER BLOG FES '24 (Garoon Stage) DAY 6の記事です。

mp4を読んでみる
mp4を読んでみる

あらまし

最近、GoPro HERO12 Blackを購入したのですが、そのGoProのmp4ファイルがよく壊れていて再生できない問題に遭遇しました。そこで、mp4ファイルってどういう仕組みになっているのかを規格から見ていこうと思いました。

アプローチについて

最初は、GoProのmp4ファイルを修復しようと試みたのですが、修復できるサイト(あえて紹介はしません)で17ドル払ったら修復してくれたり、GoPro Premiumに加入したほうがトラブルが少ないため、GoPro Premiumに加入することにしました。

ネタバレにはなってしまうものの、前者で修復してもらったとしても、今回のファイルフォーマットを調べるのに一苦労したことを考えると、17ドルは破格の値段だったのかなと思っています。

ファイルを読んでみる

参考サイトとして、 MP4 · uupaa/H264.js Wiki · GitHub を参考にさせていただきました。簡単に言うと、1個のboxに情報が入っていたり、情報の情報が入っていたり(つまり、入れ子要素にできる)します。

ファイルを読み込み、box名とバイト数を出力して、そのboxが更に子要素を持っていたらインデントを付けて出力するプログラムをPythonで書きました。参考サイトのとおりであれば、再帰を使って書けますね。Pythonなのは特に大きな理由はないのですが、バイナリを16進数の \xff などとprint文で出力してくれるのでこういうとき便利ですね。本職で使ってないのでお手柔らかにお願いします(^_^;)

#!/usr/bin/env python
import os
import sys
import struct

udta_skip = False

def read_box(f, depth=0, parent_length=None):
  global udta_skip
  field_length = struct.unpack(">i", f.read(4))[0]
  print("{0}{1}".format(" "*depth, field_length))

  if field_length == 0:
    print("{0}reach end of file".format(" "*depth))
    while True:
      b = f.read(1)
      if not b:
        break
    return 0

  b = f.read(4)
  try:
    box_type = b.decode("utf-8")
    print("{0}{1}".format(" "*depth, box_type))
  except UnicodeDecodeError as e:
    box_type = b
    print(b)

  if field_length == 1:
    field_length = struct.unpack(">q", f.read(8))[0]
    print("{0}{1}".format(" "*depth, field_length))

  if box_type == "udta" and udta_skip:
    print(f.read(field_length - 8))

  if box_type in ["moov", "trak", "mdia", "minf", "dinf", "stbl", "stsd", "avc1", "mvex", "moof", "traf", "mfra", "skip", "cprt", "srtk", "meta", "ipro", "sinf", "fiin", "pean", "meco", "udta"]:
    parent_length_count = 0
    parent_length = field_length

    while parent_length_count < parent_length - 8:
      child_length = read_box(f, depth+1, parent_length)
      if child_length == 0:
        return 0
      parent_length_count += child_length

    return field_length

  if box_type in ["ftyp"]:
    print(f.read(field_length - 8).decode("utf-8"))
  else:
    #print("{1}seek: {0} bytes".format(field_length - 8, " "*depth))
    f.seek(field_length - 8, 1)

  return field_length

if __name__ == "__main__":
    if len(sys.argv) < 2 or os.path.isfile(sys.argv[1]) != True:
      print("usage: python3 {0} filename".format(sys.argv[0]))
      exit(0)

    if len(sys.argv) == 3 and sys.argv[2] == '--udta-skip':
      udta_skip = True

    with open(sys.argv[1], "rb") as f:
      while True:
        l = read_box(f)
        if l == 0:
          break

これの出力結果として、GoProが作った再生できるmp4ファイルを読んでみます。

$ python3 parse_mp4.py GX010020.MP4
20
ftyp
mp41 mp41
8
free
2989086
mdat
29244
moov
 108
 mvhd
 26309
 udta
  30
  free
  23
  FIRM
  24
  LENS
  24
  CAME
  20
  SETT
  40
  MUID
  332
  HMMT
  44
  BCID
  24
  GUMI
  25608
  GPMF
  132
  free
 24
 iods
 977
 trak
  92
  tkhd
  20
  tref
  36
  edts
  821
  mdia
   32
   mdhd
   44
   hdlr
   737
   minf
    20
    vmhd
    36
    dinf
     28
     dref
    673
    stbl
     241
     stsd
      0
      reach end of file

このように、boxの内容を読んでみました。その結果、MP4 · uupaa/H264.js Wiki · GitHub であったように、moov > trak > mdia > minf > stbl > stsd に動画の内容があるように思えます。なお、GoProの動画コーデックがH.265なためか、avc1とavcCはなかったです。このスクリプトを使って他のデバイスで作成されたmp4を読んでみると、違いを見つけられて面白いですね。

これって仕事のなにかに役に立つの

さて、こういった一見するとただファイルを読んでみただけの虚無の記事ではありますが、業務でなにか役に立つものなのでしょうか。

一応役に立つか、と言われると役に立つと考えています。

Yukimiチームがセキュリティのことを気にするならば、こういったファイルがどのようなフォーマットに基づいて作成されているのかを調べることで、例えばわざとudta(user-data)の子要素にスクリプトを入れてみたらブラウザでJavaScriptが実行されてしまったなどがあるかもしれないかもなと思うとなくもないかなと思いました。画像ですとかなり昔ではあるものの https://blog.tokumaru.org/2007/12/image-xss-summary.html のような事例がありましたね。

CTF(Capture The Flag)をやられてる方ならばudtaなどを調べてフラグを探す、なんかがわかりやすいのでしょうか。

まとめ

今回は動画フォーマットであるmp4をさらっと見ていきました。ファイルフォーマットの構造を知ることで、何ができるだろうと想像するのも楽しいですね。 今回は以上になります。読んでいただきありがとうございました。