バイナリエディタを駆使して壊れたzipからファイルを救出する


ukyoは激怒した。ukyoにはMsEdgeがわからぬ。けれどもzipに対しては、人一倍に敏感であった

$ unzip MsEdge.Win10.VMware.zip
Archive:  MsEdge.Win10.VMware.zip
warning [MsEdge.Win10.VMware.zip]:  1031667094 extra bytes at beginning or within zipfile
  (attempting to process anyway)
error [MsEdge.Win10.VMware.zip]:  start of central directory not found;
  zipfile corrupt.
  (please check that you have transferred or created the zipfile in the
  appropriate BINARY mode and that you have compiled UnZip properly)

なにやらVirtual Machine (VM), Windows Virtual PC & BrowserStack : Microsoft Edge Devよりダウンロードしたzipアーカイブが展開できない、エラーをみるとどうやらメタデータが壊れているようなのでzipの仕様書を見ながらファイルを救出することにした。

zipに格納された各ファイルのオフセットとサイズを取得する

zipの構造については仕様書にも書かれていますし、いろいろなサイトで解説されてますので詳しくは解説しません。以下のサイト等を参考にして下さい。

通常zipはファイル末尾にあるメタデータからデータを展開していきます。が、それが壊れているようなので、前から読み込んでいきます。リトルエンディアンなのでバイナリエディタの表示とは逆になっていることに注意して下さい。

ファイル先頭から、以下のような情報が読み取れます。

  1. 0x04034B50 local file headerのシグニチャです
  2. 0xFFFFFFFF ファイルサイズを表しますが、この場合は4GBで収まらないファイルということになります。その場合は、拡張フィールドにサイズが書いてあります
  3. 0x0014 拡張フィールドのバイト長
  4. 0x0001 拡張フィールドには複数の要素が格納されている可能性があります。これは要素の種類を表します、この場合は64bitで表されたファイルのサイズが入っています
  5. 0x0010 要素のサイズ
  6. 0x000000013D7DF66C ファイルの圧縮後のサイズ

MsEdge-Win10-VMware/MsEdge-Win10-VMware-disk1.vmdkのオフセットが0x64byte、サイズが0x000000013D7DF66Cbyteだと判明しました。次のファイルのlocal file headerの先頭は0x000000013D7DF6D0byteにあるはずなのでそこまで飛びます。

同様に読み込んでいきます。1画面に収まる範囲にあるので半透明な範囲で覆ってみました。ファイル名がEdge-Win10-VMware/MsEdge-Win10-VMware.mfでlocal file headerの先頭からのオフセットは0x48byteはサイズはサイズは0x7Abyteです。

次のファイルも同様に読み込んでいきます。これは省略。3つ目のファイルを読み終わって次のファイルを読み込もうとしたときシグニチャが0x04034B50でなくて0x02014B50なことに気づきます。これはcentral directory headerのそれ、local file headerのオフセット等のメタデータで、つまりこれ以上ファイルはないということにもなります。

合計3つのファイルがありました。まとめると以下のようになります。

ファイル名 local file header先頭からのオフセット(byte) 圧縮されたファイルのサイズ(byte)
MsEdge-Win10-VMware/MsEdge-Win10-VMware-disk1.vmdk 0x64 0x013D7DF66C
MsEdge-Win10-VMware/MsEdge-Win10-VMware.mf 0x48 0x7A
MsEdge-Win10-VMware/MsEdge-Win10-VMware.ovf 0x49 0x056E

展開する

各ファイルのオフセットとサイズが取れたので各ファイルを展開していきます。jsで書きます。別に言語はなんでもいいんですが、慣れているのでこれで。jsだと浮動小数点のアレがあるんですが、今回のだとセーフですかね。

var fs = require('fs');
var zlib = require('zlib');

var start;
var end;

function unzipFile(name, start, end) {
  var rs = fs.createReadStream('MsEdge.Win10.VMware.zip', {start: start, end: end});
  var inflateStream = zlib.createInflateRaw();
  var ws = fs.createWriteStream(name);
  rs.pipe(inflateStream).pipe(ws);
}

start = 0x6E;
end = start + 0x013D7DF66C;
unzipFile('MsEdge-Win10-VMware-disk1.vmdk', start, end);

start = end + 0x48;
end = start + 0x7A;
unzipFile('MsEdge-Win10-VMware.mf', start, end);

start = end + 0x49;
end = start + 0x056E;
unzipFile('MsEdge-Win10-VMware.ovf', start, end);

適当にdeflateで圧縮されていると決めつけましたが、zipの圧縮アルゴリズムはdeflate以外も使えます。ただ、deflateじゃないのを見たことがないので意識する必要はないかと思います。

その他、救出する方法を考えてみる

zipはストリームで出力するためにlocal file header内のcompressed size(とuncomressed size, crc32)の部分に0x00000000を入れておいて、圧縮されたファイルのバイト列のおしりにdata descriptorとして書くことができます。ただ、それだと前からファイルのサイズが取得できないので詰みます。そんなときどうしたらよいか考えてみます。

ディレクトリ名で検索する

今回の例だとMsEdge-Win10-VMware。案外単一ディレクトリに入っているzipが多い印象があります。ディレクトリ名で検索してちょっと戻ってシグニチャを探すのはありだと思います。

central directory headerが生存しているか確認する

今回の例だとcentral directory headerの先頭バイトが見つからないみたいなエラーだったのでそれ自体は生き残っているかもしれません。実際生き残っていましたし。残っていればそこから普通にオフセットを計算すればOKですね。

local file headerらしきものを探す

これは割りと力技です。local file headerのシグニチャを一個ずつ検索していき、直前の12byte(data descriptorのはず)を解析します。compressed sizeが正しいものなら一個前のlocal file headerの末尾にたどり着くはずです。まぁ、ここまで来たらそういうプログラムを作るべきですね。

zip -F

実はzip -F in --out outでzipファイルを修復することができます。最初からそれ使えよ、というかんじですが、これは終わった後に知ったことです。ただ、これやってもなぜか修復できなかったので結局手動でやるしかなかったのですが。

参考