XP3ファイルを読もう

ここのところarcktyのほうで新しいプロジェクトが動き出した影響で趣味に割く時間が減ってツラい感じですが、そのプロジェクトに関係していて何となく面白そうだったことをメモっておきます

まずXP3ファイルって何?っていう話ですが、XP3ファイルは吉里吉里で利用されているアーカイブファイルです

吉里吉里で利用されるコンテンツデータ、例えばシナリオデータだとか、音楽データ、画像データなど多くのファイルを保存するアーカイブです

吉里吉里って何?って方はこの辺りをどうぞ
吉里吉里 ダウンロードページ
吉里吉里2 - Wikipedia


さて、今回はXP3ファイルをバイナリエディタと電卓と、後はちょっとしたコーディングでテキトーに漁ってみようって話です

XP3ファイルの展開・解析に関しては多くの前例がありますので、テキトーにググって貰えばこの記事より分かりやすく、正確なものが出てきます

これを読んで「よく分からん」と思った方はそちらをご覧下さい…



以下はWindows + 吉里吉里2 SDK version 2.32 revision 2 / KAG 3 3.32 revision 2の環境で動作を行っています

また、主にこのSourceを参考にさせて頂いています
吉里吉里2.32のRelSettingsUnit.cpp



XP3ファイルを展開・解析すると言っても、勿論著作権の問題などが絡んできますので、吉里吉里2 SDKについてくるtemplateというサンプルを利用させて頂きます

templateフォルダを見ると分かるとおり、中は殆ど何もありません
このままXP3を作成して、展開・解析しても面白く無いので、テキトーに画像とかを置いておいて下さい

で、リリーサを使ってXP3ファイルを作成して下さい
今回はオプション等を一切弄らず、デフォルトのままで作成しました





今回利用したXP3と、元のデータはこちらにおいて置きます
http://c.arckty.org/krkr.zip





まずバイナリエディタで眺めてみましょう
(以下は僕の場合です)

f:id:kkrnt:20150225223934p:plain

58 50 33 0D 0A 20 0A 1A 8B 67 01 17 00 00 00 00
00 00 00 01 00 00 00 80 00 00 00 00 00 00 00 00
44 33 02 00 00 00 00 00


Sourceを見ると先頭8Bytesがヘッダーになっているようです

stream.WriteBuffer("XP3\r\n \n\x1a ", 8);

次の3Bytesも固定のヘッダーらしいです

stream.WriteBuffer("\x8b\x67\x01", 3);

次にcushion headerという固定値が入るようです

stream.WriteInt64(11+4+8);

バイナリエディタ上では8Bytesの16進数表記で、かつリトルエンディアンで格納されているので

17 00 00 00 00 00 00 00

となっているはずです

その次にheader minor versionという固定値が入ります

stream.WriteBuffer("\x01\x00\x00\x00", 4);


ここまでの23Bytesがヘッダーで、固定値です

58 50 33 0D 0A 20 0A 1A 8B 67 01 17 00 00 00 00
00 00 00 01 00 00 00


その後の17Bytesがcushion headerです

stream.WriteBuffer("\x80", 1); // continue
stream.WriteBuffer("\0\0\0\0\0\0\0\0", 8); // index size = 0
unsigned __int64 index_pointer_pos = stream.GetPosition();
stream.WriteBuffer("        ", 8); // to real header

バイナリエディタでは以下の9Bytesまでが固定値で、その後の8Bytesがファイルを管理している部分の場所になっています

80 00 00 00 00 00 00 00 00

僕の場合は

44 33 02 00 00 00 00 00

つまり0x23344というアドレスから管理部が始まるということですね

では0x23344に飛びましょう
飛んでみると、恐らくですが、ファイル全体から見るとかなり最後のほうになるのではないでしょうか?

その辺りを見てみると

01 42 04 00 00 00 00 00 00 0C 11 00 00 00 00 00 00

0x23344の1Bytesは圧縮処理を行うかどうかを判断しているようです
今回は0x01なので圧縮するということですね

その後の8Bytes×2は圧縮後のサイズ、圧縮前のサイズを示しているようです

そこから最後までがファイルを管理している部分になります
僕の場合だと0x23355からということになります

0x23355から最後までのデータを抜き出して、別のファイルに保存しておきます

で、抜き出したデータなんですが、Sourceを読むのが面倒くさくなってきたのでテキトーに読んでみますが、全く分かりません

そりゃ圧縮フラグが立ってるんだから当然なんですが…
そこでfileコマンドで見てみます

file ファイル名
ファイル名: zlib compressed data

ということでzlibで圧縮されていることが分かります


さて、データを伸張してみましょう
Pythonの場合はzlib.decompress()で簡単に出来ます
こんな感じ

import zlib
a = open("input.bin", "rb")
b = a.read()
c = zlib.decompress(b)
d = open("output.bin", "wb")
d.write(c)

伸張して出てきたものがこれです
f:id:kkrnt:20150225230424p:plain

これをSourceを参考に読んでいけば良いみたいです

// for each file
memcpy(index_buf, "File", 4);
index_buf += 4;
*(__int64*)(index_buf) = 4 + 8 + 4 + 8 + 8 + 2 + str.Length()*2 + 4 + 8 +
        4 + 8 + 4 +     it->Segments.size()*28;
index_buf += 8;

となっているので、最初の4Bytesが「File」で、その後の8Bytesがこのデータのサイズのようです

僕の場合は

46 69 6C 65 78 00 00 00 00 00 00 00

となっているので、サイズは0x78ということになります

次にinfoセクションが入ります

// write "info"
memcpy(index_buf, "info", 4);
index_buf += 4;
*(__int64*)(index_buf) = 4 + 8 + 8  + 2 + str.Length()*2;
index_buf += 8;
*(__int32*)(index_buf) = it->Flags;
index_buf += 4;
*(__int64*)(index_buf) = it->FileSize;
index_buf += 8;
*(__int64*)(index_buf) = it->StoreSize;
index_buf += 8;
*(__int16*)(index_buf) = str.Length();
index_buf += 2;

wchar_t *tstr = new wchar_t[str.Length() +1];
try
{
        // change path delimiter to '/'
        wcscpy(tstr, str.c_bstr());
        wchar_t *p = tstr;
        while(*p)
        {
                if(*p == '\\') *p = '/';
                p++;
        }
        memcpy(index_buf, tstr, str.Length() * 2);
        index_buf += str.Length() * 2;
}
catch(...)
{
        delete [] tstr;
        throw;
}
delete [] tstr;


はじめの4Bytesで「info」となり、その後の8Bytesで次のセクションまでのサイズ、その次の4Bytesがプロテクトのフラグで0の時はプロテクトしていないことを示します
その次に8Bytesでファイルサイズ、同じく8Bytesでストアサイズを示します
その次の2Bytesはファイル名の長さですね

そこからファイル名が入ります
注意して欲しいのはファイル名はwcharで格納されているということです
f:id:kkrnt:20150225232011p:plain


次からがセグメントセクションです

// write "segm"
memcpy(index_buf, "segm", 4);
index_buf += 4;
*(__int64*)(index_buf) = it->Segments.size() * 28;
index_buf += 8;
std::vector<TFileSegment>::iterator sit;
for(sit = it->Segments.begin(); sit != it->Segments.end(); sit++)
{
        *(__int32*)(index_buf) = sit->Flags;
        index_buf += 4;
        *(__int64*)(index_buf) = sit->Offset;
        index_buf += 8;
        *(__int64*)(index_buf) = sit->OrgSize;
        index_buf += 8;
        *(__int64*)(index_buf) = sit->StoreSize;
        index_buf += 8;
}


最初の4Bytesが「segm」で固定値ですね
次にセグメントのサイズが8Bytesで入ります
セグメントの数*28で計算されています

僕の場合は

1C 00 00 00 00 00 00 00

なので0x1C、つまり28なのでセグメント数は1つということですね

そこからセグメント数分繰り返されるのですが
4Bytesで圧縮フラグ、0の時が圧縮されていなく、1の時が圧縮されていることを示します
次の8Bytesがoffsetで、このデータが保存されている開始アドレスを示します
その次の8Bytesがデータのサイズで、その次の8Bytesがストアサイズです


つまり、offsetからoffset+ストアサイズまでを引っ張ってくることでデータを抜き出すことが可能になります


僕の場合は

28 00 00 00 00 00 00 00 69 52 00 00 00 00 00 00 69 52 00 00 00 00 00 00

となっているので、offsetが0x28でストアサイズは0x5269です

つまり0x28から0x5291まで抜き出すことで元のデータが手に入るということです

実際にやってみると...
f:id:kkrnt:20150225233303p:plain
f:id:kkrnt:20150225233336p:plain


ということでデータを抜き出すことが出来ました


実際にはセグメントセクションの後にadlrセクションがあり、ここでファイルのチェックを行えるようになっています

// write "adlr" (adler32 check sum)
memcpy(index_buf, "adlr", 4);
index_buf += 4;
*(__int64*)(index_buf) = 4;
index_buf += 8;
*(unsigned __int32*)(index_buf) = it->Adler32;
index_buf += 4;


これで今回は終わりなんですが、実際のところ、はじめにXP3を開いた瞬間に「臼NG」とか見えていてですね、まぁフツーに抜き出したりすることは簡単です

今回はXP3を読もうということでこんな面倒な方法でやりましたが、中身を取り出したいだけだったら幾らでもツールがあります

著作権の問題などがあるので、データを抽出したりする際は注意して下さい


また、実際に市場で出回っているゲームなどはこんな簡単に行かなくて、暗号化が施されていたりします

暗号化に関してはxp3enc.dllなどを参考にして下さい
これはこれで色々な手段で復号化を試みることが可能です
今度そういったことについて書こうと思っていますが…


吉里吉里オープンソースなのでSourceを読めば大体のことは分かります
分からないことはググれば大体出てきます
なので非常に楽しく遊ぶことが出来るんじゃないかなーって思います


最近では吉里吉里Zであったり、ノベルスフィア、Almightなどがあり、面白そうですね^^;
吉里吉里Z