SWFバイナリのパース

はじめに

今までバイナリに触れる機会がなかったのでとりあえずPythonでバイナリを読めるようになろうかなって思った
とりあえず仕様が公開されてるSWFの画像・音声(動画)をバイナリから引っこ抜くところまでを目標にする

ちなみにバイナリについてバイナリエディタで開いてほーん…ってなるくらいしか知らない

仕様書

Adobeがファイルフォーマットの仕様書を公開しているので基本はこれを読みながらやる
http://wwwimages.adobe.com/content/dam/Adobe/en/devnet/swf/pdf/swf-file-format-spec.pdf
読み方としては15ページのInteger Typesは先に目を通してから27ページのSWFのフォーマットについて読むと良い
それ以外は読み飛ばして必要に応じて読めば良い

レポジトリ

とりあえずの作業スペース
https://github.com/skyblue3350/swf

作業

最低限のバイナリの読み込み

とりあえずバイナリエディタみたいに16進数で表示してみる
structモジュールとかあるけど使わないで書いてみる

1fp = open("test.swf", "rb")
2while True:
3    b = fp.read(1)
4    if b == "": break
5    print hex(ord(b))
6fp.close()

1バイトずつ読み出してordで10進数にしてそれをhexで16進数に変換する

ヘッダの読み込み

ヘッダは以下で構成される

  • Signature
    圧縮されているかどうか
  • Version
    このSWFファイルのバージョン情報
  • FileLength
    このSWFファイルの大きさ
  • FrameSize
    フレームの縦横の大きさ
  • FrameRate
    FPS
  • FrameCount
    フレームの数

1個ずつ読んでいく

Signature

Sinatureは3バイトで表される
1バイト目が圧縮 2バイト目(W)と3バイト目(S)は固定(仕様書27ページ参照)
1バイト目がFの時 未圧縮 Cの時 zlib圧縮 Zの時 LZMA圧縮
UI8が8bit→8*3で24ビット→8ビット1バイトで3バイトなのでreadで3バイト読み出す

1fp = open("test.swf", "rb")
2
3signature = fp.read(3)
4print "Signature", signature
5fp.close()
1FWS

未圧縮であることが分かる
もしCWSなら9バイト目以降をzlibモジュールをインポートして

1zlib.decompress(fp.read())

する

Version

この辺から差分だけ

1print ord(fp.read(1))

FileLength

これはリトルエンディアンで表される
1バイト:A
2バイト:B
だったら
BAにしてこれを2つつなげて10進数に直す

1little = [ hex(ord(fp.read(1)))[2:].zfill(2) for x in range(4)][::-1]
2filelen = int("".join(little), 16)
3print filelen

今後もリトルエンディアンは出てくるので関数化しておくと良い
10進数→16進数→10進数ってやり方しか思いつかなかったけどもっと簡単にできそう

ここまで読み込んだらCWSなどの圧縮時はこれ以降のバイナリが圧縮されているので展開する

1if signature == "CWS":
2    b = fp.read()
3    fp.close()
4    fp = StringIO(zlib.decompress(b))

とかしておくと未圧縮と圧縮の差を考えなくて良くなるので多少楽
こんな感じでヘッダの残りも読む

Tagの読み込み

SWFファイルは[Headter][Tag][Tag]...[Tag](28ページ)
とデータが続くようになっている
Tagには種類が設定されていてENDというTagが出て来るまでひたすら読み込んでいけば良い

typeとlength

2バイト読み込んで最初の10ビットを10進に直したものがTagの種類
残りの6ビットがそのTagのサイズ

1info = "".join([ bin(ord(f.read(1)))[2:].zfill(8) for x in range(2) ][::-1])
2tagtype = int(info[:10], 2)
3taglength = int(info[-6:], 2)

またtaglength変数の中身が6ビットで表せる最大の数(2進:111111、10進:63)の時は更に4バイトリトルエンディアンで読み込んだものが実際のサイズになります
これを反映すると以下のようになります

1info = "".join([ bin(ord(fp.read(1)))[2:].zfill(8) for x in range(2) ][::-1])
2tagtype = int(info[:10], 2)
3taglength = int(info[-6:], 2)
4
5# サイズが63なら追加4ビットを使用する
6if taglength == 63:
7    taglength = int("".join([ hex(ord(fp.read(1)))[2:].zfill(2) for x in range(4)][::-1]), 16)
8
9tagbody = fp.read(taglength)

Tag全体の読み込み

tagtypeがSWFにおけるタグの種類です(仕様書235ページに一覧あり)
ファイルの末尾はEND(id:0)となるはずなのでtagtypeが0になるまでループします

 1while True:
 2    fileinfo = "".join([ bin(ord(fp.read(1)))[2:].zfill(8) for x in range(2) ][::-1])
 3    tagtype = int(fileinfo[:10], 2)
 4    taglength = int(fileinfo[-6:], 2)
 5
 6    # TagType:0がデータの末尾
 7    if tagtype == 0:
 8        break
 9
10    if taglength == 63:
11        taglength = int("".join([ hex(ord(fp.read(1)))[2:].zfill(2) for x in range(4)][::-1]), 16)
12
13    tagbody = fp.read(taglength)

これでひとまずファイルの全体の読み込みが出来ました
あとは必要なデータに応じてtagbodyを良い感じに加工するとデータが抽出出来ます

実装済みの部分はレポジトリの方に反映してあるので興味があればどうぞ
この記事の解説はいい加減なので以下の参考サイトを参考に自分で書いた方が分かりやすいと思います

参考記事

以下2サイトは大変お世話になりました