よくある質問
「ZIPファイルをPNGファイルに偽装」とはどんな意味ですか?
一見、PNGファイルのようであり、実際にPNGデコーダーで画像の表示などが問題なくできるが、(拡張子でファイルの種類を判断するOSの場合)拡張子をZIPに書き換えるとZIPファイルとして扱えるファイルのことを、PNGファイルに偽装したZIPファイルを呼ぶことにします。
また、このようなファイルを作ることを「ZIPファイルをPNGファイルに偽装した」ということにします。
つまり、PNGファイルとしても、ZIPファイルとしても扱えるファイルを作ることです。
基本的に、既存の小さなPNGファイルと既存の小さなZIPファイルを組み合わせて偽装することを想定しています。
見方を変えれば、PNGファイルをZIPファイルに偽装したとみることも可能です。
どんなZIPでも偽装できますか?
いくつかの制約があります。
- ZIP64は対象外です
- 分割ZIPもとりあえず対象外です
- ZIPのサイズが2GB未満である必要があります。
また、PNGとして、あるいはZIPとしてファイルの変更(PNGで画像のフィルタ処理など。ZIPでファイルの追加・削除など)をした場合には、その後は偽装ファイルとして使えない可能性が高いです。
ほかにも、実装上の制約がいろいろあるかもしれません。
偽装することでどんなメリットがありますか?
知りません。
ZIPファイルの基本構造と偽装方法その1
ZIPファイルフォーマットの詳細な仕様については深入りしません。
また、ZIP64と分割ZIPのことは無視します。
- .ZIP File Format Specification
- Wikipedia: ZIP (ファイルフォーマット)
ZIPファイルの基本構造は、ファイルの最後のほうにある End of Central Directory
(以下 EOCD
)から追いかけるとわかりやすいと思います。
EOCD
は Central Directory
(以下 CEN
)の位置、つまりファイル先頭からのオフセットを持っています。
また、CEN
はアーカイブ内のファイル数分存在しており、それぞれの CEN
には Local file header
(以下 LOC
)の位置(オフセット)があります。
LOC
に続いてファイルの本体(圧縮されたり、非圧縮だったり、暗号化されたり、平文だったり)などが格納されています。いろいろ説明を省略していますが。
このような構造になっているため、ZIPファイルの先頭や末尾、あるいは各データ間に何かのデータがあったとしても、ZIPファイルとして正常に扱えます。
具体例としては、自己解凍型ZIPファイル(EXEファイル)は、たいていのZIPアーカイバで通常のZIPファイルとして扱うことが可能です。
見方を変えると、自己解凍ZIPファイルはEXEファイルとしてもZIPファイルとしても扱うことができるわけです。
このような特性があるため、別のファイル内に丸々ZIPファイルを格納しても、通常のZIPファイルとして扱うことが可能です。
ただし既存のZIPファイルを別のファイルに埋め込む場合には、ファイル先頭からのオフセットを持っている個所を適切に補正する必要があります。
具体的な補正個所は、EOCD
内のCEN
のオフセットと、各CEN
内のLOC
のオフセットがそれにあたります。
(ZIP64用の拡張データがあると、そこにもオフセットがあるかもしれないのですが、今回は無視します)
ちなみに、ZIPファイルのバイトオーダーはリトルエンディアンです。
PNGファイルの基本構造と偽装方法その2
PNGファイルフォーマットの詳細な仕様については深入りしません。
- RFC2083 PNG (Portable Network Graphics) Specification Version 1.0
- Wikipedia: Portable Network Graphics
- ISO/IEC 15948:2004
PNGの基本構造は、先頭に8バイトのPNGシグネチャ(PNG file signature)があり、その後に複数個のチャンク(Chunk)が存在します。チャンクには必須のチャンクもあれば、任意のチャンク(補助チャンク)もあります。
また一部順番が決まっているチャンクもありますが(例: IHDR
チャンクは先頭、 IEND
チャンクは一番最後、など)、それ以外は順番も任意です。
各チャンクは「長さ(4バイト)」「チャンクタイプ(4バイト)」「チャンクデータ(任意バイト数)」「CRC(4バイト)」により構成されます。
バイトオーダーはビッグエンディアンです。
「長さ」は「チャンクデータ」の長さを示します(「長さ」自身、「チャンクタイプ」「CRC」の分は含まれません)。また最大値は $2^{31} - 1$ バイトです。
「CRC」は、「チャンクタイプ」と「チャンクデータ」の範囲についてCRC32を求めた値です。
チャンクタイプは、ASCIIコードでの英字4文字です。大小文字を区別します。
(仕様書では、文字として扱うのではなくバイナリデータとして扱えとありますが、説明の簡便さから文字ベースで記載します)
また、各桁の大文字・小文字で意味が異なります(仕様書では5bit目のon/offで判定しろ、とのこと)。
桁 | 5bit目の名前 | 大文字(off) | 小文字(on) |
---|---|---|---|
1桁目 | 補助ビット | 必須チャンク | 補助チャンク |
2桁目 | プライベートビット | 公開チャンク | プライベートチャンク |
3桁目 | 予約済(将来拡張用)ビット | (大文字固定) | (小文字は使用しない) |
4桁目 | 複写可能ビット | 画像が変更されたとき複写不可 | 画像が変更されたとき複写可能 |
必須チャンクは画像を表示するために必須なので、PNGデコーダが未知の必須チャンクに遭遇した場合には、エラーとします。逆に未知の補助チャンクに遭遇した場合には、無視することが可能です。
公開チャンクは仕様書や公開チャンクリストなどに登録されたチャンクで、プライベートチャンクはアプリ独自仕様のチャンクとして使用するものです。一般的にはプライベートチャンク同士の名称の衝突があっても大丈夫なようなガードは必要だろうと思いますが。
複写可能ビットは他のビットとは異なり、PNGデコーダではなくPNGエディタ(PNGファイルをフィルタ処理などをするプログラム)で画像を加工した際に、未知のチャンクをそのままコピーしてよいかを示すものです。
今回は、補助チャンクとして「ZIPファイルを格納するチャンク」を独自に作ってしまおうと思います。チャンクタイプはなんでもいいのですが、「ZIPコンテナチャンク」ということで、ziPc
にしてみます。場所は IHDR
チャンクのすぐ後ろに作成することにします。チャンクにZIPファイルを丸々格納する必要があるため、偽装するZIPファイルのサイズは2GB未満である必要があります。
また IHDR
チャンクの直後に「ZIPコンテナチャンク」を配置したのは、IHDR
チャンクは固定長でありPNGエディタで加工された後でもZIPファイルのoffsetがずれない可能性が高いと判断したためです。
もちろん、IHDR
チャンクと ziPc
チャンクの間に何か別のチャンクを挿入されたり、ziPc
チャンクを削除された場合にはダメなのですが。
偽装手順
最後に、偽装の手順を整理します。
既存のZIPファイルとPNGファイルをもとに、偽装ファイルを生成するものとします。
- PNGファイルのシグネチャを出力する
- 既存PNGファイルのIHDRチャンクを出力する
- 既存ZIPファイルのサイズを出力(ZIPコンテナチャンクの「長さ」)
- ZIPコンテナチャンクの「チャンクタイプ」
ziPc
を出力 - 既存ZIPファイルの
CEN
の手前までを出力(ここから「チャンクデータ」) - 既存ZIPファイルの
CEN
を補正しながら出力(繰り返し) - 既存ZIPファイルの
CEN
の最後とEOCD
の間に何かあれば、それをコピー - 既存ZIPファイルの
EOCD
を補正して出力 - 既存ZIPファイルの
EOCD
の後ろに何かあれば、それをコピー(ここまで「チャンクデータ」) - 上記、「チャンクタイプ」から「チャンクデータ」までのCRC32を計算して出力(ZIPコンテナチャンクの「CRC」)
- 既存PNGファイルのIHDRチャンクより後ろをすべて出力する
以上です。
実装例
難易度の高い処理は特にないので(一番難易度が高いのがCRC32の計算ぐらい)、バイナリファイルを操作できれば、どんな言語でも実装可能だろうと思います。
注意点としては、ZIPはリトルエンディアン、PNGはビッグエンディアンであることでしょうか。
とりあえずjava/javascript/pythonで実装した例をgithubにあげました。