SWFバイナリ編集のススメ第一回

こんにちは。メディア開発部のよやと申します。バイナリ編集エンジニアです。

はじめに

GREE では携帯向けコンテンツに Flash Lite を利用していますが、Lite には様々な制限(*1)があり、SWF(スウィフ) の動的生成技術を活用しています。

ツール(ming, swfmill, swftools, etc…)を用いた SWF 生成の記事は世間に溢れてますので、SWFバイナリの生編集をテーマに記事を何回かに分けて進めようと思います。
ツールを使う場合でも何かしら問題に遭遇した際の一助になるかもしれません。

第一回の当記事は、SWF仕様書の読み方ガイドです。

SWF仕様

Adobe公式の SWF仕様書は一般公開されています。 英語です。

理解の助けになる日本語のサイトを列挙します。

以下の SWFファイル(orz.swf)をサンプルとして、公式仕様書を読み進めます。

SWFヘッダ

仕様書の SWF Structure Summary – The SWF header で以下のように定義されています。

SWF File Header
Field       Type  Comment
Signature   UI8   Signature byte:
                 “F” indicates uncompressed
                 “C” indicates compressed (SWF 6 and later only)
Signature   UI8   Signature byte always “W”
Signature   UI8   Signature byte always “S”
Version     UI8   Single byte file version (for example, 0x06 for SWF 6)
FileLength  UI32  Length of entire file in bytes
FrameSize   RECT  Frame size in twips
FrameRate   UI16  Frame delay in 8.8 fixed number of frames per second
FrameCount  UI16  Total number of frames in file

hexdump コマンドで実際の SWFファイルの先頭を16進数で表示して仕様書と比較してみます。

% hexdump -C orz.swf | head -1
00000000  46 57 53 04 77 1b 00 00  70 00 09 60 00 00 96 00  |FWS.w...p..`....|
初めの8bytes
  • 先頭3bytesの FWS は Signature。一文字目が F なので圧縮なし。
  • 4byte目は 0x04 で、SWF の version
  • 5~8byte目の 77 1b 00 00 は FileLength。SWF は基本 LittleEndian なのでbyte列を逆方向に読んで、0x1b77 = 7031 これが SWFファイルのファイル長に相当します。
% ls -l orz.swf
-rwxr--r-- 1 yoya yoya 7031 2010-08-04 16:41 orz.swf

合ってますね。
といった具合に読み解けます。

RECT

9byte目以降は少々やっかいです。

ここで出てくる RECT(Rectangle Record)は仕様書の Basic Data Types で定義されます。

RECT
Field  Type       Comment
Nbits  UB[5]      Bits used for each subsequent field
Xmin   SB[Nbits]  x minimum position for rectangle in twips
Xmax   SB[Nbits]  x maximum position for rectangle in twips
Ymin   SB[Nbits]  y minimum position for rectangle in twips
Ymax   SB[Nbits]  y maximum position for rectangle in twips

初めの5bitにフィールド長が入っていて、その長さのフィールドが4つ後ろに続きます。
具体的には、

% hexdump -C orz.swf | head -1
00000000  46 57 53 04 77 1b 00 00  70 00 09 60 00 00 96 00  |FWS.w...p..`....|

この中の

70 00 09 60 00 00 96 00  

のbyte列が対応します。bit で分けると以下のようになります。

   7   0    0   0    0   9    6   0    0   0    0   0    9   6    0   0
01110000 00000000 00001001 01100000 00000000 00000000 10010110 00000000
<-5-><-- 14 bits  --><-- 14 bits  --><-- 14 bits --><-- 14 bits  -->
NBits      Xmin            Xmax            Ymin           Ymax

初めの 5 bits を読むと 14 という値が入っているので、その後ろに 14bits ずつ Xmin, Xmax, Ymin, Ymax の4つの値が並ぶという訳です。

各々の値は次のようになります。

NBits = 01110 = 14
Xmin = 00000000000000 =    0 twips =   0 pixel
Xmax = 01001011000000 = 4800 twips = 240 pixel
Ymin = 00000000000000 =    0 twips =   0 pixel
Ymax = 01001011000000 = 4800 twips = 240 pixel

ここで出てくる twips は長さの単位で(本来は DPI 依存ですが)、SWF では logical pixel(100% 表示での pixel) の 1/20 の長さと定められています。つまり twips の値を 20 で割ると丁度 pixel 値になります。

ヘッダの残り
% hexdump -C orz.swf | head -2
00000000  46 57 53 04 77 1b 00 00  70 00 09 60 00 00 96 00  |FWS.w...p..`....|
00000010  00 0a 28 00 43 02 ff ff  ff 7f 05 e4 08 00 00 01  |..(.C...........|

丁度二行目(offset 0x10)から続いています。

  • FrameRate の 00 0a は LittleEndian なのでひっくり返して 0a 00 。8.8 fixed number (8bit整数.8bit少数 の固定小数点)なので、10.00 [frames/秒] (*2)
  • FrameCount の 28 00 も LittleEndian なので 0x0028 => 40 frames (=コマ数)

以上で、SWF ヘッダが一通り読めました。

  • イメージ図


SWFタグ (short形式)

SWFヘッダの後ろに SWFタグのリストが続きます。(図は仕様書から抜粋)

仕様書の SWF Structure Summary – SWF file structure に以下の定義があります。

RECORDHEADER (short)
Field             Type  Comment
TagCodeAndLength  UI16  Upper 10 bits: tag type
                        Lower  6 bits: tag length

SWFタグはSWFヘッダの後ろに続くので、先程の FrameCount の続きから読んでいきます。

% hexdump -C orz.swf | head -2
00000000  46 57 53 04 77 1b 00 00  70 00 09 60 00 00 96 00  |FWS.w...p..`....|
00000010  00 0a 28 00 43 02 ff ff  ff 7f 05 e4 08 00 00 01  |..(.C...........|
43 02 ff ff  ff 7f 05 e4 08 00 00 01 

タグの先頭2bytes にタグの種類とタグの長さが入っていますが、LittleEndian と bit packing の合わせ技なので、読むのに少しコツがいります。

まず。43 02 を逆方向に読んで 0243 として、これを bit に分解します。

   0   2    4   3
00000010 01000011
<-10 bits-><-6 ->
 tag type  length

初めの10bits がタグの種類(type)で、続く 6bits が(TagCodeAndLengthを除いた)タグの長さです。

tag type = 0000001001 = 9 = SetBackgroundColor
length = 000011 = 3 = (RGBの) 3 byte

tag type の 9 と SetBackgroundColor の対応は仕様書(swf_file_format_spec_v10.pdf)をテキスト検索して確認してください。

以上から、SetBackgroundColor のタグは以下の byte列になる事が分かります。

43 02 ff ff  ff 

背景が (R,G,B) = (0xff, 0xff, 0xff)= 白色となります。

  • イメージ図

SWFタグ (long形式)

6 bits では111111 = 0x3f = 63 byte までしか表現できないので、この 6 bits に 111111 = 0x3f が入っている時は、long 形式が適用されます。

画像や動画等のマルチメディアコンテンツを含むタグはこの形式を利用します。

RECORDHEADER (long)
Field             Type  Comment
TagCodeAndLength  UI16  Tag type and length of 0x3F
                        Packed together as in short header
Length            SI32  Length of tag

先程のタグ(SetBackgroundColor)の後ろに丁度、JPEG画像のタグが並んでいるので、それを例にします。

% hexdump -C orz.swf | head -2
00000000  46 57 53 04 77 1b 00 00  70 00 09 60 00 00 96 00  |FWS.w...p..`....|
00000010  00 0a 28 00 43 02 ff ff  ff 7f 05 e4 08 00 00 01  |..(.C...........|

先程の続きなので以下のbyte列が相当します。

 7f 05 e4 08 00 00 

まず TagCodeAndLength の 2 byte を読みだします。

7f 05 => 0x057f => 00000101 01111111
                   <---------><---->
                    tag type  length
 tag type = 10101 = 0x15 = 21 = DefineBitsJPEG2 
 length = 0x3f = long形式

Length が 0x3f なので、その後ろに続く 4bytes を本当の Length として読み出します。

 e4 08 00 00  = 0x8e4 = 2276 byte
  • イメージ図

PHPで読みだし

以上で SWF のヘッダとタグの読み方を一通り説明しました。
参考までに今まで説明した処理を行う PHP プログラムを以下に付けます。

swfdump

input($swfdata);

/* SWF Header */
echo 'Signature: '.$reader->getData(3).PHP_EOL;
echo 'Version: '.$reader->getUI8().PHP_EOL;
echo 'FileLength: '.$reader->getUI32LE().PHP_EOL;
echo 'FrameSize: '.PHP_EOL;
$NBits = $reader->getUIBits(5);
echo "\tXmin: ".($reader->getUIBits($NBits) / 20).PHP_EOL;
echo "\tXmax: ".($reader->getUIBits($NBits) / 20).PHP_EOL;
echo "\tYmin: ".($reader->getUIBits($NBits) / 20).PHP_EOL;
echo "\tYmax: ".($reader->getUIBits($NBits) / 20).PHP_EOL;
$reader->byteAlign();
echo 'FrameRate: '.($reader->getUI16LE() / 0x100).PHP_EOL;
echo 'FrameCount: '.$reader->getUI16LE().PHP_EOL;

/* SWF Tags */
echo 'Tag:'.PHP_EOL;
while (true) {
    $tagAndLength = $reader->getUI16LE();
    $type = $tagAndLength >> 6;
    $length = $tagAndLength & 0x3f;
    if ($length == 0x3f) { // long format
        $length = $reader->getUI32LE();
    }
    echo "\ttype: $type  length: $length".PHP_EOL;
    $contents = $reader->getData($length);
    if ($type == 0) { // END Tag
        break;
    }
}

exit(0);

BitReader

class BitReader {
    var $_data; // input_data
    var $_byte_offset;
    var $_bit_offset;

    function input($data) {
        $this->_data = $data;
        $this->_byte_offset = 0;
        $this->_bit_offset = 0;
    }
    function byteAlign() {
        if ($this->_bit_offset > 0) {
            $this->_byte_offset ++;
            $this->_bit_offset = 0;
        }
    }
    function getData($length) {
        $this->byteAlign();
        $data = substr($this->_data, $this->_byte_offset, $length);
        $data_len = strlen($data);
        $this->_byte_offset += $data_len;
        return $data;
    }
    function getUI8() {
        $this->byteAlign();
        $value = ord($this->_data{$this->_byte_offset});
        $this->_byte_offset += 1;
        return $value;
    }
    function getUI16LE() {
        $this->byteAlign();
        $ret = unpack('v', substr($this->_data, $this->_byte_offset, 2));
        $this->_byte_offset += 2;
        return $ret[1];
    }
    function getUI32LE() {
        $this->byteAlign();
        $ret = unpack('V', substr($this->_data, $this->_byte_offset, 4));
        $this->_byte_offset += 4;
        return $ret[1];
    }
    function getUIBit() {
        $value = ord($this->_data{$this->_byte_offset});
        $value = 1 & ($value >> (7 - $this->_bit_offset));
        $this->_bit_offset ++;
        if (8 <= $this->_bit_offset) {
            $this->_byte_offset++;
            $this->_bit_offset = 0;
        }
        return $value;
    }
    function getUIBits($width) {
        $value = 0;
        for ($i = 0 ; $i < $width ; $i++) {
            $value <<= 1;
            $value |= $this->getUIBit();
        }
        return $value;
    }
}

実行結果

% php swfdump.php orz.swf
Signature: FWS
Version: 4
FileLength: 7031
FrameSize:
        Xmin: 0
        Xmax: 240
        Ymin: 0
        Ymax: 240
FrameRate: 10
FrameCount: 40
Tag:
        type: 9  length: 3
        type: 21  length: 2276
<略>
        type: 0  length: 0

次回予告

次回は SWFバイナリの編集まで話しを進める予定です。
それでは失礼いたします。

*1: Lite の制限については、こちらのURLが分かり易いです => http://d.hatena.ne.jp/hanageman/20080922

*2: 2 byte まとめて一つの値として読んだ上で 0x100 で割ると楽です。

Author: よや