2012年12月15日土曜日

技術メモ: 実践 目grep


ゲーム日本語化 Advent Calendar 2012


実際に解析してみる。
The Real Texasを例にする。

(以下数値は16進数)



フォルダを見るとglobというサイズの大きいファイルがある。
他にゲームデータらしきファイルは無い。globがアーカイブと思われる。

軽くダンプしてみる


0000010に 78 9c が見える。たぶんzlibヘッダ。

2つのアーカイブを見比べる。



共通のシグネチャやバージョン番号らしきヘッダはないことがわかる。

サイズの一番小さいworld.globを調べる

先頭付近と末尾付近をざっと眺める




どこにもファイル名らしきものは見当たらない。

先頭の

e5 f9 2b 00
be 00 00 00

はよくわからない。後者をファイル数と仮定する。


zlibヘッダの直前に
4c 00 00 00
2c 4e 00 00
が見える。前者がファイルサイズ、後者が展開後サイズとあたりをつける。

ここからバイナリエディタでまじめに見る

int size
int origsize
char data[size]

と仮定して、dataの先頭にsizeを足して飛んでみる


5c にきた。

なんかズレてるので、sizeはデータサイズではなく、origsizeの4byte分を含めたサイズとわかる

int size
int origsize
char data[size-4] // zlib compressed

とりあえずこの仮定でunpackerを作ってみる。
ファイル名は適当に連番をつける。

$fp = fopen($file,'r');

$unk = fgetint($fp);
$num = fgetint($fp);


for($i=0;$i<$num;$i++) {
  $size = fgetint($fp);
  $origsize = fgetint($fp);
  $data = fread($fp,$size-4);
  $data = gzuncompress($data);
  $outfile = $dir.sprintf("%04d.dat",$i);
  file_put_contents($outfile,$data);
  printf("%d %x %d %d %x\n",$i,$pos,$size,$origsize,ftell($fp));
}
fclose($fp);

実行してみる。



うまく展開されているようだ。
アーカイブ名がworld.globなのでマップデータか何かだろう。

ただし、最終位置がアーカイブのサイズと会わない。ヘッダの値をファイル数と仮定したのは間違いだったようだ。
最後まで処理するようにする。

for($i=0;true/*$i<$num*/;$i++) {
  $pos = ftell($fp);
  if ($pos>=$filesize) break;


最後のファイルだけなんか大きいので覗いてみる。




どう見てもファイル名っぽい。

先頭の4a 1e 00 00はファイル数ぽい。
次の20 00 00 00は、ファイル名の長さっぽい。

解説してなかったが、文字列はたいてい

長さ 文字....
文字.....0

のいずれか。

長さ 文字...0

の場合もある。
あるいは、長さちょうどのサイズではなく、4byte等でalignされるように末尾にpaddingが付くこともある。

20 00 00 00 の直後の world... 文字列に20を加えてみると、うまく文字列の末尾になった。


次の文字列開始30 までの間に、
e3 0d 10 00
cc 00 00 00

30からの文字列末尾の次にも、
19 0a 10 00
24 00 00 00

が見える。

ためしに 100de3に飛んでみる


size origsize zlibヘッダ

と圧縮されたファイルの先頭ぽいので、これはオフセットとわかった。
もう一つの値 cc 00 00 00 はよくわからん

int num

struct entry[num]
  int len
  char name[len]
  int ofs  // offset to file
  int unk

改めてファイルの先頭を見てみると



E5 F9 2B 00
BE 00 00 00

と、ofs,unkと同じ構造をしていることがわかる。

2BF9E5に飛んでみると、これは先ほど解析した最後のファイルである、エントリ情報を指しているとわかる。




2BF9E5 はファイルサイズより小さいので、アーカイブ内の何かを指すオフセットだ、と 最初に気づいていれば、先にファイルエントリ情報を見つけられたはずだが、まあこういうこともある。


ここまでの情報をもとに、アーカイブを先頭から読んで連番ファイルに出力するのではなく、ファイルエントリ情報を読んでファイル名を付けて出力するように変更する。

$fp = fopen($file,'r');

$entpos = fgetint($fp);
$unk0 = fgetint($fp);

$data = read_data($fp,$entpos);
$st = new BinStream($data);

$num = $st->getint();

echo $num,"\n";

for($i=0;$i<$num;$i++) {
  $namelen = $st->getint();
  $name = $st->read($namelen);
  $ofs  = $st->getint();
  $unk  = $st->getint();
  printf("%x %x %s\n",$ofs,$unk,$name);
  $data = read_data($fp,$ofs);
  savefile($dir.$name,$data);
}
fclose($fp);

function read_data($fp,$pos)
{
  fseek($fp,$pos,SEEK_SET);
  $size = fgetint($fp);
  $origsize = fgetint($fp);
  $data = fread($fp,$size-4);
  $data = gzuncompress($data);
  return $data;
}

うまくいった。

data.globを展開してみる

なんかエラーだって

エラー部を見てみると

sizeが0の場合はorigsize,dataが続かず、すぐ次のファイルになっていることがわかる。

int size // if 0 , no origsize,data
int origsize
char data[size-4] // zlib compressed

size == 0 の特殊処理を入れる。

function read_data($fp,$pos)
{
  fseek($fp,$pos,SEEK_SET);
  $size = fgetint($fp);
  if ($size==0) return '';

もう一度展開してみる。

うまくいった。完成。

The Real Texas unpacker

0 件のコメント:

コメントを投稿