Edited at

ZIPファイルをPNGファイルに偽装する方法

More than 1 year has passed since last update.


よくある質問


「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ファイルの基本構造は、ファイルの最後のほうにある End of Central Directory (以下 EOCD )から追いかけるとわかりやすいと思います。

EOCDCentral 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ファイルフォーマットの詳細な仕様については深入りしません。

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ファイルをもとに、偽装ファイルを生成するものとします。


  1. PNGファイルのシグネチャを出力する

  2. 既存PNGファイルのIHDRチャンクを出力する

  3. 既存ZIPファイルのサイズを出力(ZIPコンテナチャンクの「長さ」)

  4. ZIPコンテナチャンクの「チャンクタイプ」 ziPc を出力

  5. 既存ZIPファイルの CEN の手前までを出力(ここから「チャンクデータ」)

  6. 既存ZIPファイルの CEN を補正しながら出力(繰り返し)

  7. 既存ZIPファイルの CEN の最後と EOCDの間に何かあれば、それをコピー

  8. 既存ZIPファイルの EOCD を補正して出力

  9. 既存ZIPファイルの EOCD の後ろに何かあれば、それをコピー(ここまで「チャンクデータ」)

  10. 上記、「チャンクタイプ」から「チャンクデータ」までのCRC32を計算して出力(ZIPコンテナチャンクの「CRC」)

  11. 既存PNGファイルのIHDRチャンクより後ろをすべて出力する

以上です。


実装例

難易度の高い処理は特にないので(一番難易度が高いのがCRC32の計算ぐらい)、バイナリファイルを操作できれば、どんな言語でも実装可能だろうと思います。

注意点としては、ZIPはリトルエンディアン、PNGはビッグエンディアンであることでしょうか。

とりあえずjava/javascript/pythonで実装した例をgithubにあげました。