はじめに
こんにちは!Garoon開発チームのYukimiチームにいます、てきめん です。
Yukimiチームは、より安全なGaroonをユーザーに届けることを目標とし、将来に渡って継続的にGaroonを安定提供できるように活動しているチームです。Yukimiチームのことについては以下を参照してみてください。
さて、今日は、ファイルフォーマットの一つで動画のフォーマットである、mp4について見ていきたいと思います。
この記事は、CYBOZU SUMMER BLOG FES '24 (Garoon Stage) DAY 6の記事です。
あらまし
最近、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をさらっと見ていきました。ファイルフォーマットの構造を知ることで、何ができるだろうと想像するのも楽しいですね。 今回は以上になります。読んでいただきありがとうございました。