http://events.php.gr.jp/events/show/96

はじめに

PHP でバイナリ処理の話をあまり聞かないので、あえてニッチな所を狙って発表させて頂きます。

  • キーワード: Binary、Format, Byte, Bit, Endian, Signed,
  • デモ: JPEGからGPS抜き出し、Flash SWF 改変… (pure PHP で)

PHP というより、バイナリの勉強会といった要素が強いですが、 多分…、いつかお役に立てると思いますので、どうか、ご容赦ください。

一応、自己紹介

  • http://d.hatena.ne.jp/yoya/ でプログラミングしてて困った事とか書いてます。
  • 一昨年位まで、数十万ユーザ規模の携帯サイトでアプリ開発をしていました。
    • その時に、PHP で主に動画や画像のフォーマットを弄るお仕事をしていました。フレームワークとかよく知りません。
  • 今も、一応 PHP でお仕事してます。あと、C 言語もたまに使います。

まずは、バイナリの定義

  • Wikipedia より
    通常バイナリとテキストは対比して用いられる。
    テキストとはデータの内容すべてを人間が読んで理解できる (human-readable) 表現形式を指し、
    バイナリとはそうでない表現形式を指すことが多い。

なので、本発表では、バイナリファイルの事を、

テキストエディタで開いて読めない文字とか記号が表示されるようなファイル。

という事にしておきます。

バイナリの実例

% hexdump -C aria.gif
00000000  47 49 46 38 39 61 c8 00  96 00 f7 00 00 00 00 00  |GIF89a..........|
00000010  ff ff ff 96 53 58 29 1b  1c e6 b0 b8 b2 69 76 37  |....SX)......iv7|
00000020  26 29 d6 96 a1 cb c6 c7  34 1c 22 48 31 38 2b 21  |&)......4."H18+!|
<略>

GIF ファイルですね。

$ hexdump -C kuriboo.png
00000000  89 50 4e 47 0d 0a 1a 0a  00 00 00 0d 49 48 44 52  |.PNG........IHDR|
00000010  00 00 00 c0 00 00 00 e0  08 06 00 00 00 55 70 69  |.............Upi|
00000020  31 00 00 00 04 73 42 49  54 08 08 08 08 7c 08 64  |1....sBIT....|.d|
<略>

PNG です。

先頭の4文字を見ると何か分かる事が多いのですが、その後ろにはよく分からないデータが続いてます。

このよく分からないデータを PHP で改変する方法について、今回はお話します。

PHP とバイナリ

PHP の string 型でバイナリ処理が簡単に出来るよ!

というのが今回、紹介する Tips の肝です。

(注) PHP6 では UTF-8 対応したり取りやめたりと怪しいので、今回の発表はとりあえず、PHP5(PHP4 も多分大丈夫) only での話しだと思ってください。

本当にバイナリ処理できるの?

\0 は?

C言語が典型ですが、\0 を文字列の(最後を表す)終端マークとして使う処理系も 結構あるので、string型をバイナリデータとして使う場合、途中で切れないか。

  • 簡単に確認
$s = "This is TEST\n";
$s{3} = "\0";
echo strlen($s)."\n";
echo $s;
13
Thi is TEST
  • 確認done \0 で切れない。

8bitスルー?

  • US-ASCII は 7bit で表現できるので、先頭1bit が特別扱いされたりしない?
$s = ' ';
$s{3} = 'A';
$s{4} = chr(ord('A') | 0x80); // chr, ord は後で説明します
var_dump(bin2hex($s));
string(10) "20202041c1"
  • 8bit スルーです。日本語とかも入れられるし当然ですね。

バイナリを取り込んでそのまま出力

  • でも、細かい事考えなくても、バイナリファイルを入力して、何も変えずに出力して同じデータが生成されれば、OKですよね。
$data = file_get_contents($argv[1]);
echo $data;
% php echo.php saitama.jpg > output.dat
% md5sum saitama.jpg output.dat
06f741dca38937df3702f6759aead28b  saitama.jpg
06f741dca38937df3702f6759aead28b  output.dat
  • 入力をそのまま出力して、同じデータになりました。
  • 後は、この途中でデータを分割したり結合したり、入れ替えたりすれば編集できる事になります。

byte処理

  • String 関数が使えます http://php.net/manual/ja/ref.strings.php
    • strlen (データサイズを調べる。呼ぶたびに文字数を数えたりしないので安心)
    • substr (データから一部を抜き出す)
    • substr_replace (データの一部を入れ替える)
    • strrev (データの前後を逆にする) Endian 処理に便利
    • chr 数値 => 文字(バイナリ1byte)
    • ord 文字(バイナリ1byte) => 数値
    • bin2hex ダンプするのに便利です

これだけ分かれば大丈夫。(pack 関連は難しいので次回発表の機会があれば…)

さて、試してみましょう。

JPEG の解析

初めの一歩

  • まずファイルを開いてみる
% hexdump -C aria.jpg
00000000  ff d8 ff e0 00 10 4a 46  49 46 00 01 01 01 00 60  |......JFIF.....`|
00000010  00 60 00 00 ff e1 00 22  45 78 69 66 00 00 49 49  |.`....."Exif..II|
00000020  2a 00 08 00 00 00 01 00  00 51 04 00 01 00 00 00  |*........Q......|
<略>
マーカ・コード  	長さ  	データ
FFxx(16bit) 	16bit 	可変サイズ
  • 16bit は 2 byteに相当 、あと、ffXX で区切るらしい。
00000000  ff d8 ff e0 00 10 ...
          ~~~~~ ~~~~~~~~~~~~~~~
          SOI   APP0
  • 眺めていると、何となくピンと来るはず。

JPEG の chunk を分割してみる

  • まず、Byte を切り出すクラスを作成
    class ByteSteam {
        private $_data;
        private $_cursor;
        function getBytes($size); // バイナリ列を取り出す
        function getValue($size); // 数値として取り出す
         function getCursol();     // カーソル位置を知る
    }
  • marker 一個目
    $jpegdata = file_get_contents($argv[1]); // 引数で指定したファイル取り込み
    $bs = new ByteStream($jpegdata);
    $marker = $bs->getBytes(2); // 先頭2バイト取り出す
    var_dump(bin2hex($marker)); // 16進ダンプ
ffd8
  • marker 逐次処理
    function marker($a, $b) { return chr($a).chr($b); }
while($marker = $bs->getBytes(2)) {
    switch ($marker) {
     case marker(0xFF, 0xD8): // SOS
       $length = $data = null;
       break;
     case marker(0xFF, 0xE0): // APP0
     case marker(0xFF, 0xE1): // APP1
       $length = $bs->getValue(2);
       $data = $bs->getBytes($length - 2);
       break;
     case marker(0xFF, 0xDA): // SOS include RST
       $length = strlen($jpegdata) - $bs->getCursor() - 2;
       $data = $bs->getBytes($length);
       break;
     case marker(0xFF, 0xDA): // EOI
       $length = $data = null;
       $done = true;
       break;
}
  • 分割できた
    string(4) "ffd8"
    string(4) "ffe1"
    string(4) "ffdb"
    string(4) "ffc0"
    string(4) "ffc4"
    string(4) "ffda"
    string(4) "ffd9"

欲しい情報を探してみる

iPhone で写真を撮ると、GPS 情報が付くらしいので、抽出してみよう。

bit処理

その他Tips

  • PHP の閉じタグ ?> は使わない。?> の後ろに改行やゴミ文字があった場合に、テキストなら最後にゴミが付くだけで大きな問題になりにくいが、バイナリだと致命的。

Reload   Diff   Front page List of pages Search Recent changes Backup Referer   Help   RSS of recent changes