以下はSebastian Stamm ( Twitter / GitHub / Webサイト )による記事、This Image Is Also a Valid Javascript Fileの日本語訳です。
This Image Is Also a Valid Javascript File
画像は普通はバイナリファイルであり、Javascriptファイルは基本的にテキストファイルです。
いずれも固有のルールに従わなければなりません。
画像ファイルは、データをエンコードするための具体的なフォーマット形式が決まっています。
Javascriptファイルは、実行するためには特定の構文に従わなければなりません。
ところでふと気になりました。
Javascriptとして実行可能な有効な構文を持っている画像ファイルを作成することはできるでしょうか?
ここより先を見る前に、実験の結果を以下のサンドボックスで確認してみることをお勧めします。
自身で画像をダウンロードして確認してみたければ、こちらからダウンロードできます。
Choosing the Right Image Type
残念ながら画像には大量のバイナリデータが含まれているため、そのままJavascriptとして解釈しようとするとエラーになります。
そこで私がまず考えたのは、次のようなものでした。
以下のように、全ての画像データをコメントに入れたらよいのではないかということです。
/*ALL OF THE BINARY IMAGE DATA*/
これは有効なJavascriptファイルになります。
しかし、画像ファイルは特定のバイト列で開始される必要があります。
たとえばPNGファイルの先頭は常に89 50 4E 47 0D 0A 1A 0A
です。
最初が/*
で始まっていたら、それは有効な画像ファイルではありません。
このヘッダを見ていて次のアイデアを思いつきました。
バイト列を変数名にして、バイナリは文字列として代入できればよいのではないかということです。
PNG=`ALL OF THE BINARY IMAGE DATA`;
バイナリデータには改行コードが含まれている可能性があり、改行コードの扱いはテンプレートリテラルのほうが優れているので、通常の"
や'
ではなくテンプレートリテラルを用いることにしました。
残念ながら、ほとんどの画像ファイルはヘッダ部のバイト列に変数名として許されない文字を含んでいます。
しかし、それが可能な画像フォーマットをひとつ発見しました。
GIFです。
GIFファイルのヘッダは47 49 46 38 39 61
で、これは文字列GIF89a
をASCIIで綴ったものであり、すなわち完全に合法ということです。
Choosing the Right Image Dimensions
有効な変数名で始めることができる画像フォーマットを見つけたので、次はそこに等号=
とバックティック`
が必要です。
従って、ここではファイルの次の4バイトを3D 09 60 04
とすることにします。
GIFフォーマットでは、ヘッダの次の4バイトは画像のサイズを表します。
この中に等号の3D
とバックティックの60
を埋め込まなければなりません。
GIFはリトルエンディアンであるため、画像サイズにはそれぞれ2バイト目が大きな影響を与えます。
何万ピクセルもある画像にならないように、3D
と60
は下位バイトに格納することにしました。
画像サイズの残ったバイトには、GIF89a= `
という有効文字列を残すために空白文字を入れます。
最も画像幅を小さくできる有効な空白文字は水平タブ09
であり、従って画像幅は3D 09
、リトルエンディアンでは2365になります。
思ったよりは大きいですが、まだ妥当なサイズです。
画像の高さについては、ちょうどよいアスペクト比になりそうなものを選ぶことができます。
今回は04
を選択したので、画像の高さは60 04
、すなわち1120です。
Getting our own script in there
このJavascriptは今のところ何もしません。
グローバル変数GIF89a
に文字列を代入しているだけです。
せっかくなので何か面白いことがおこるようにしたいですよね。
しかし、GIFファイル内のほとんどは画像をエンコードするためのデータなので、そこにJavascriptを入れようとすると、それは単に壊れた画像になってしまうでしょう。
ところで何故か、GIFフォーマットにはComment Extensionという機能が含まれています。
これはGIFデコーダで解釈されないメタデータを保存する場所であり、すなわちJavascriptのロジックを配置するのに最適な場所ということです。
Comment ExtensionはColor Tableのすぐ後ろに配置されています。
ここには任意の文字列を入れることができるため、変数GIF89a
の文字列を閉じることも簡単です。
そこで任意のJavascriptを記述し、最後にコメント開始を入れることで、残りの画像の部分をJavascriptパーサに解釈されないようにします。
全体として、ファイルの中身は以下のようになります。
GIF89a= ` BINARY COLOR TABLE DATA ... COMMENT BLOCK:
`;alert("Javascript!");/*
REST OF THE IMAGE */
ただし、少しばかり制限があります。
Comment Extensionは複数のサブブロックで構成する必要があり、ブロックごとの最大サイズは255です。
そしてサブブロックの終わりには、次のサブブロックの長さを表すバイトを記述しなければなりません。
従って、大きなスクリプトを入れたい場合は、以下のように小さな部品に分解していく必要があります。
alert('Javascript');/*0x4A*/console.log('another subblock');/*0x1F*/...
コメント中に書かれているhexcodeは、次のサブブロックの長さを表しています。
これはJavascriptには不要ですが、GIFフォーマットとしては必要です。
従って、Javascriptの邪魔にならないようにJavascriptコメント中に書かなければなりません。
この問題を解決するため、スクリプトをチャンクに分けるための小さなスクリプトを書きました。
Cleaning up the Binary
基本的な構造が分かったので、次は画像データのバイナリがJavascript構文を壊さないようにする必要があります。
上で説明したように、ファイルには3つのセクションがあります。
ひとつめは変数GIF89a
への代入部分、ふたつめがJavascriptコード、最後は複数行コメントです。
まずは最初の変数代入部分を見てみましょう。
GIF89a= ` BINARY DATA `;
バイナリデータに`
、もしくは${
が入っていたら、そこでテンプレート文字列が終了したり無効な式が生成されてしまうため困ったことになります。
この修正はとても簡単です。
すなわち、バイナリを変更するだけです。
たとえばバイナリ中にASCIIコード60があったら、それを61、すなわち文字a
に変更します。
ここはカラーパレットの定義部分のデータを変更することになるため、結果として一部のカラーコードが、たとえば#286048
から#286148
へと微妙に変化することになります。
しかし、この違いに誰かが気付く可能性は非常に低いでしょう。
Fighting the corruption
Javascriptコードの最後に、バイナリデータがJavascriptに影響しないようにコメント開始コードを書きました。
alert("Script done");/*BINARY IMAGE DATA ...
もし画像のバイナリに*/
が含まれていたら、そこでコメントが終了してしまい、不正なJavascriptファイルになってしまいます。
従って、ここでも同じように2文字のうちどちらかを変更することで、コメントが終了しないようにすることにします。
しかし、ここは既にエンコードされた画像セクションであるので、単純に書き換えるだけではこのように画像が壊れてしまいます。
極端な場合、画像が全く表示されなくなることすらありました。
しかしどの文字を変更するかを慎重に選択することで、画像の破損を最小限に抑えることができました。
幸いなことに、問題になるような*/
はほんの少しだけでした。
最終的な画像は、Valid Javascript File
文字列の下のほうなどに僅かな破損が見えますが、概ね満足のいく仕上がりになっています。
Ending the File
最後にしなければならない問題は、ファイルの最後にあります。
ファイルの末尾は00 3B
で終わらせなければならないため、最後までコメントで埋めることはできません。
ファイルの終わりであるため、バイナリデータの変更は目に見えるような画像の破損を起こしません。
従って、Javascriptが正常に動くように、コメントブロック終了後に1行コメントを追加しました。
/* BINARY DATA*/// 00 3B
Convincing the Browser to Execute an Image
ここまできて、ついにJavascriptとして有効な画像ファイルが完成しました。
しかし、最後にまたひとつ問題があります。
この画像をサーバにアップロードし、scriptタグで読み込もうとすると、次のようなエラーが発生する可能性があります。
Refused to execute script from 'http://localhost:8080/image.gif' because its MIME type ('image/gif') is not executable.
ブラウザは『こいつは画像ファイルだから実行なんてしないぞ』と拒否します。
大抵の場合、これは適切な動作と言えましょう。
しかし、私はこれを実行したいのです。
ということで、その解決策はそのファイルが画像であることをブラウザに伝えないことです。
そのためだけに、ヘッダ情報を送らず画像ファイルだけを返すサーバを作りました。
ヘッダのMIME typeがなければ、ブラウザはそのファイルの正体が何であるかわからず、コンテキストに即したものを実行します。
すなわち、<img>
タグでは画像として表示し、<script>
タグではJavascriptとして実行します。
But ... why?
なんのため?
それは、私がまだ解明できていないことです。
これを作るのは精神的によい挑戦でした。
もし、これが実際に役立つようなシナリオを思いついたら、ぜひ教えてください!
コメント欄
「面白かった!」
「🤯」
「こいつはクールだ……ぜ?🤔」
「画像ファイルにトラッカーを仕込むとか?もうひとつ思いついたのは、単純にハッキングとか。」「画像に情報を仕込む技術は既にある。ステガノグラフィでぐぐれ。」
「セキュリティへの影響について考えてる。imgタグに入ってる画像をブラウザが勝手にJavaScriptとして実行したら最悪だ。」「imgタグはJavaSctiptと解釈されないから問題ない。」
感想
img src
とscript src
どちらで読み込んでも正しく動作する、面白ファイルが出来上がりました。
手法自体はコメントにバイナリを埋め込むというもので、古来より幾度となく行われているものですが、今回はJavaScriptを動くようにしたうえで画像としても破綻無く仕上げているところが面白いですね。
この手法の実用的な例というとCutwailがありますが、これは別に正しい画像である必要はないからちょっと違うかな。
今はセキュリティ上できないようになっているはずですが、この手法を突き詰めたらいずれ、
・Webサイトにアクセスしたら、一枚の画像ファイルが落ちてきた
・画像の表示が終わったと思ったら何故かHTMLになっていた
みたいなこともできるかもしれませんね。
そんなことをやって何のメリットがあるのかさっぱりわかりませんが。