2012/01/06
KAICHO: s_naray[at]yahoo[dot]co[dot]jp
※SPAM防止中

吉里吉里/KAGでのデータ暗号化方法例

■はじめに

本書では、吉里吉里/KAGを利用したゲームにおいて、データを暗号化する方法について 述べる。

暗号化によるメリットは、「データの不正展開、拡散の防止」と、 (あるならば)「プロテクトの難読化」。製作側としては、あんまデメリットは 存在しない。最近、データ展開されて(特に効果音とかが)拡散しちゃう被害が 増えているので、真面目にこういうの考えた方がいいんじゃないか、と思って 書いてみた次第。あと、こういうのって標準機能なのに誰もまとめて文書化して ないなーって。

■まずはロジカルシンキン

「データ二次頒布防止」を目標とし、まずロジカルシンキンのHOWツリーを使って その方法を考えてみる。え、ロジカルシンキンって知らない? そういう人は、津田先生の「超」MBA式ロジカル問題解決(Amazonリンク) を是非読もう。この本はロジカルシンキン(Logical Thinking ... 論理的思考法)の 基礎の基礎の基礎を教えてくれる良書だ。我輩は直接津田先生にこれを教わったが、 あまりの思考の美しさに衝撃を受けた一冊。サラリーマンなら一読すべき。

さておき、HOWツリーは以下のとおり。太字のものは対策として使えそうなもの。

とりあえず思いつくのはこんなものだろうか。他に画期的でトレビヤンなアイディアを お持ちの方は、是非お知らせ頂きたい。アイディアメンの称号をプレゼント。

実は、吉里吉里には標準で「個々のファイルの暗号化・複合化」方法が 用意されている。なので、本書では、(krkr.exeを再作成することなく)簡単に データを暗号化する方法として、「個々のファイルの暗号化・復号化」の方法を 紹介する。

■実際に個々のファイルを暗号化・復号化する

xp3enc.dll(エンコーダ(暗号化)dll)を作成し、吉里吉里リリーサ(krkrrel.exe)と 同じディレクトリに入れておくと、吉里吉里リリーサを実行した時に、 「オプション」タブに「xp3enc.dllを使う(E)」というチェックボックスが追加で アクティブになる。

これは、xp3dec.tpm(実体はxp3dec.dll、デコーダ(復号化)dll)と対になって、 リリース(.xp3ファイル作成)時に個々のファイルを自動的に暗号化してくれる、 吉里吉里デフォルトの機能だ。 え、そんなのマニュアルに書いてないよ?と思ったキミは正しい。 この機能についての説明はほとんどどこにも書いてない。酷い。単に書き忘れたとか 面倒だから以外の、真っ当な理由(ヘンに解説しちゃうと解析されちゃうからー、の ような)はなんとなく思いつくが、まぁオープンソースでそういうことゆってても 仕方ないでしょ、と思うので、ここに方法をつらつら書く次第。 これを読んだからといって、暗号化をさくさく解除できるようになるわけじゃ ないしね。

実は、吉里吉里のソースリポジトリを覗くと、xp3enc.dllとxp3dec.tpm(dll)の スケルトンが用意されている。 これらのファイルのコメントをまずはよく読もう。よく読めば結構情報書いてあるし。

これが、吉里吉里で使える標準的な暗号化ルーチン(の骨組み)である。ただし、 このソースコードはスケルトンでしかなく、これをそのまま使うと ユーザに簡単に復号化されてしまう。ので、結局自前で暗号化したい時は、 自前でXP3ArchiveAttachFilter_v2()やTVPXP3ArchiveExtractionFilter()を 捻って書いて、VC++などでコンパイルして各dllを自分で作成する必要がある。 エラく敷居が高い。

というところで、以下、具体的な同ソースコードのコンパイル方法を示す。

●VC++プロジェクトの作成

まず、xp3enc.dllとxp3dec.tpm(dll)を生成するためのプロジェクトを それぞれ作成しよう。以下はVC++2008でxp3enc.dllを作成する手順。 なぜVC++(2010ではなく)2008なのかというと、2010だと作成したバイナリが Win2000で動かないから。「Win2000とかサポート終わってんだし切ればいいのに」 とか言わないであげてお願い。

  1. [ファイル]→[新規作成]→[プロジェクト(P)]から[Win32プロジェクト]を作成
  2. 「Win32アプリケーションウィザードへようこそ」画面で[次へ]ボタンを押す
  3. [アプリケーションの種類]→[DLL(D)]選択、[追加のオプション]→[空のプロジェクト(E)]を選択して[完了]ボタンを押す
  4. 吉里吉里2.32のxp3enc.dllのソースコード main.cppをプロジェクトフォルダにコピー
  5. 同じディレクトリのxp3enc.defファイルをプロジェクトフォルダにコピー
  6. プロジェクトのソリューションビュー(左のツリー)の[ソースファイル]を 右クリック→[追加]→[既存の項目] から、上のmain.cppを追加
  7. [プロジェクト]の[プロパティ]から、[構成プロパティ]→[リンカ]→[入力] →[モジュール定義ファイル]に、xp3enc.defを指定
  8. 同[構成プロパティ]→[C/C++]→[コード生成]→[ランタイムライブラリ]を、 「マルチスレッド(/MT)」に変更
これでxp3enc.dllを作成するプロジェクトは完成。

一方、xp3dec.tpm(dll)を作成するプロジェクトは、基本的に上と同じで xp3encをxp3decに置き換えるだけだが、追加で以下の作業が必要になる。

  1. tp_stub.htp_stub.cppをプロジェクト フォルダにコピー
  2. プロジェクトのソリューションビュー(左のツリー)のソースファイル右クリック →[追加]→[既存の項目] から、上のtp_stub.cppを追加
  3. プロジェクトのソリューションビュー(左のツリー)のヘッダファイル右クリック →[追加]→[既存の項目] から、上のtp_stub.hを追加
なお、.defファイルを追加するのが嫌いで、.defで定義する代わりに Exportする関数に__declspec(dllexport)付けるべきだろう! という人(我輩がそうなんだ)が居るのは知っているが、 実はxp3enc.dllだけは.defファイルが絶対必要。 __declspec(dllexport)+__stdcallだと 関数シンボル名の末尾に「@8」とかがくっついちゃうので、krkrrel.exeから 呼び出せなくなってしまい、リリーサで「xp3enc.dllを使う(E)」チェックボックスは 有効になるにも関わらず.xp3ファイルの生成に失敗する(正確にはエラーなく生成できた ように見えるが、サイズがエラい小さいものが出来上がる)。 これを防ぐには、__declspec(dllexport)なしに、.defファイルで関数シンボルを 明示的にExportするしかない。スゲー悩んじゃった上に.defファイル嫌いの我輩は 一体どうすれば!

●エンコード・デコードルーチンの作成

上のmain.cppファイル中で例示されているのは、1byte XORという最も基本的な 暗号化。これをそのまま使うのは簡単すぎて、暗号化としては危険極まりない。 ちょっとバイナリに詳しい人なら、1byte XORはデータを見た瞬間に それと分かる。我輩ですら分かる。だから、あまり使うべきではない。

以下では、より複雑な例として、ファイルハッシュ値の31bitローテート値での XORを挙げる。32bitじゃないのは、基数を素数にすることでより解析を 難しくする(少なくともバイト単位のハッシュ値でなければ、 バイナリ見られても元データを想像するのは非常に困難な)ため。 エンコードルーチンはtp_stub.hをincludeしていないので、tjs_intとかの 型が使えないことに注意。自前で定義してもいいけどさ。

エンコードルーチン(XP3ArchiveAttractFilter_v2()のみ抜粋)
extern "C" void __stdcall XP3ArchiveAttractFilter_v2(
                unsigned __int32 hash,
                unsigned __int64 offset, void * buffer, long bufferlen)
{

    // 暗号化
#define CODEBIT 31
    unsigned __int8 xor[CODEBIT];

    hash &= ((unsigned __int32)1<<CODEBIT)-1;  // 31bitのみ有効にする
    hash |= (hash&0x1)<<CODEBIT;               // 最下位ビットを最上位に移動
    for (long i = 0; i < CODEBIT; i++) {
        xor[i] = hash;
        hash = (hash>>8) | ((hash<<(CODEBIT-8))&0xff000000);
    }
    for (long i = 0; i < bufferlen; i++)
        ((unsigned __int8*)buffer)[i] ^= xor[(i+offset)%CODEBIT];
}

これでエンコードされたバイナリ列はずいぶん難解になっている。 そのバイナリ列見て、「ああなるほどこれは31bitの ローテートだねふむふむ」とか言える人がいたとしたら、 その人はスーパークラッカーであり、そういう人にはもうナニやっても無駄なんで、 畏怖の念を抱きつつすっぱり諦めてよろし。

次はデコードルーチン。実は、XOR を使う場合、エンコードルーチンと デコードルーチンはほぼ同一となる。 構造体なのか引数なのかでちょっと表記が違うが、以下でやってることは エンコードルーチンと完全に同一。

デコードルーチン(TVPXP3ArchiveExtractionFilter() のみ抜粋)
void TVP_tTVPXP3ArchiveExtractionFilter_CONVENTION
	        TVPXP3ArchiveExtractionFilter(tTVPXP3ExtractionFilterInfo *info)
{
    if(info->SizeOfSelf != sizeof(tTVPXP3ExtractionFilterInfo)) {
        // 構造体のサイズが違う場合はここでエラーにした方がよい
        TVPThrowExceptionMessage(TJS_W("Incompatible tTVPXP3ExtractionFilterInfo size"));
    }

    // 復号化
#define CODEBIT 31
    unsigned __int8  xor[CODEBIT];
    unsigned __int32 hash = info->FileHash;

    hash &= ((unsigned __int32)1<<CODEBIT)-1;  // 31bitのみ有効にする
    hash |= (hash&0x1)<<CODEBIT;         // 最下位ビットを最上位に移動
    for (long i = 0; i < CODEBIT; i++) {
        xor[i] = hash;
        hash = (hash>>8) | ((hash<<(CODEBIT-8))&0xff000000);
    }
    for(unsigned long i = 0; i < info->BufferSize; i++)
        ((unsigned __int8*)info->Buffer)[i] ^= xor[(i+info->Offset)%CODEBIT];
}

●コンパイル

[Release]でBuildするだけ。 これでそれぞれxp3enc.dll、xp3dec.dllが作成されるだろう。

●ファイルの配置

xp3enc.dllは、吉里吉里リリーサ (krkrrel.exe)と同じディレクトリにコピーする。 これで、その吉里吉里リリーサを起動すると、 [オプション]タブで[xp3enc.dllを使う(E)]チェックボックスが選択可能になるはずだ。 これを選択すると、xp3ファイル中の個々のファイルごとに、xp3enc.dll内の XP3ArchiveAttractFilter_v2()が呼ばれて、データがエンコード(暗号化)された .xp3ファイルが作成される。

xp3dec.dllは、実際にxp3enc.dllを使ってエンコードした.xp3ファイルを使う ゲームのkrkr.exeと同じディレクトリに、 なんとか.tpmという名前でコピーする。「なんとか」の部分は任意。 なぜ拡張子を変更するかというと、krkr.exeが起動して一番最初に読むdllとして 明示的に指定するためだ。え、そんな機能あったん?という人は、本家の プラグインの自動読み込みを読むこと。なんかソースコード見ると、 自動読み込みするプラグイン(*.tpm)はsystem/ディレクトリの下にあっても よさそうだが、まぁそんなこたどうでもいいや。

なお、暗号化・復号化は、全XP3ファイルに対して同等に実行される(しなければ ならない)ことに注意。即ち、あるファイルには暗号化を施し、こっちには 施さない、とか、そういう器用なことはできない。XP3に含めるファイルは 潔く全部暗号化するか、または全部暗号化しないかを選択することになる。

●どのくらい難読化できたのか

以下に、デフォルトの暗号化と今回作った暗号化、それぞれで暗号化した同じ ファイルのバイナリ列を示す。

まず、デフォルトの暗号化(1byte XOR)後のデータ。元データが想像できるだろうか。 (暗号化ルーチンもう知ってるからというのを差し引いても、)フツーのクラッカーなら 容易に想像できる。我輩ですら想像できる。

CircleLogo.ksの暗号化後のバイナリ列(1byte XOR, 先頭256byteのみ)
00000000  3a 21 82 55 80 5a 82 4f  82 8a 82 8c 82 52 0c 0b  |:!.U.Z.O.....R..|
00000010  3a 21 6d 60 78 64 73 21  31 83 c7 30 83 f1 8f 66  |:!m`xds!1..0...f|
00000020  83 a5 80 43 8e 48 83 c0  83 bc 8f 9f 92 5e 83 c4  |...C.H.......^..|
00000030  96 bd 94 fa 21 62 6d 64  60 73 21 83 b4 83 c5 83  |....!bmd`s!.....|
00000040  a9 83 ac 83 b0 83 c7 80  43 0c 0b 2b 62 68 73 62  |........C..+bhsb|
00000050  6d 64 6d 6e 66 6e 0c 0b  0c 0b 5a 75 64 79 75 5e  |mdmnfn....Zudyu^|
00000060  77 68 72 68 63 6d 64 21  77 68 72 68 63 6d 64 3c  |whrhcmd!whrhcmd<|
00000070  67 60 6d 72 64 5c 0c 0b  5a 62 6d 68 62 6a 72 6a  |g`mrd\..Zbmhbjrj|
00000080  68 71 21 64 6f 60 63 6d  64 65 3c 75 73 74 64 5c  |hq!do`cmde<ustd\|
00000090  0c 0b 0c 0b 5a 64 77 60  6d 21 64 79 71 3c 23 75  |....Zdw`m!dyq<#u|
000000a0  67 2f 62 6d 68 62 6a 62  6e 74 6f 75 72 60 77 64  |g/bmhbjbntour`wd|
000000b0  21 3c 21 6a 60 66 2f 62  6d 68 62 6a 42 6e 74 6f  |!<!j`f/bmhbjBnto|
000000c0  75 23 5c 0c 0b 3a 21 82  4f 82 8b 82 63 82 4f 82  |u#\..:!.O...c.O.|
000000d0  59 82 4d 82 63 82 77 82  7c 82 4f 82 8c 80 43 82  |Y.M.c.w.|.O...C.|
000000e0  4f 82 8b 82 63 82 4f 83  c4 82 59 82 4d 82 63 82  |O...c.O...Y.M.c.|
000000f0  77 83 c4 83 aa 83 e8 80  43 0c 0b 5a 6c 60 62 73  |w.......C..Zl`bs|

既にファイル名(CircleLogo.ks)が分かっているから、ksファイル(テキストファイル)と 仮定してO.K.。しかも、ざっと見ると0x80より小さいデータが並んでいて、文字列中に 「whrhcmd」「bmhbj」のような並びが複数見えるから、こいつぁ多分byte単位XOR、 しかもそのXOR値は0x80より小さいだろうなぁ、ということがすぐに分かる。 加えて、テキストファイルなんだから改行コード(0x0d+0x0a)を XORしたものが入ってるだろ、という推測の下、2byteの連続して出現するバイナリを 探すと…"0c 0b"が怪しいのもピンとくる。あとは、これが "0d 0a" になる XOR 値を 探して、えいっと復号テストしてみればいい。正解は 0x01 でのXORだ。 実は「whrhcmd」は「visible」、「bmhbj」は「click」という文字列だったのだ。

次に、今回作ったエンコーダで同じファイルを暗号化したものを見てみよう。

CircleLogo.ksの暗号化後のバイナリ列(31bit XOR, 先頭256byteのみ)
00000000  3a 02 b3 fd 81 4a 1b 1a  03 83 cf a7 c3 57 2b 1f  |:....J.......W+.|
00000010  1b 22 ff 6b 69 e4 3b 25  b8 42 62 33 c6 90 dc 66  |.".ki.;%.Bb3...f|
00000020  a0 94 28 42 9e d1 d6 41  8a f1 a4 de 97 79 97 e5  |..(B...A.....y..|
00000030  95 2f 9f eb a1 2a 69 ed  a1 d6 22 c6 d5 d0 c5 a0  |./...*i...".....|
00000040  98 2b ad 93 29 d6 46 89  0e 27 4a 2e 45 7c 52 61  |.+..).F..'J.E|Ra|
00000050  ff 6f 7c ee 2e 6a 85 ca  a9 08 1f 14 37 79 56 6f  |.o|..j......7yVo|
00000060  df 69 62 f1 36 ec 6d 6c  5c 29 77 4f 77 4c 67 ae  |.ib.6.ml\)wOwLg.|
00000070  6c 71 ed 3a 60 d5 cd ae  59 27 0c 3b 62 49 43 c2  |lq.:`...Y'.;bIC.|
00000080  69 61 b8 31 ee 69 2e 46  25 60 1b 61 52 77 f6 57  |ia.1.i.F%`.aRw.W|
00000090  1d 8b 44 0f d3 a5 d2 63  28 40 37 79 52 0d 8b 74  |..D....c(@7yR..t|
000000a0  77 b6 37 ec 61 2f 41 23  6b 53 7b 54 71 f2 7c 75  |w.7.a/A#kS{Tq.|u|
000000b0  a1 74 25 e3 a1 c3 2c 27  0c 3b 62 49 73 c6 75 7f  |.t%...,'.;bIs.u.|
000000c0  ec 76 dd 05 46 11 60 87  68 96 aa 81 f1 89 5e 02  |.v..F.`.h.....^.|
000000d0  11 86 c4 43 c6 81 32 e3  2f 82 6c b3 24 81 53 1b  |...C..2./.l.$.S.|
000000e0  1a 03 82 cf 48 c3 4a a4  d0 a3 5a 10 46 93 e3 ca  |....H.J...Z.F...|
000000f0  73 0a 05 26 a9 c6 89 d3  43 2f 3a f2 6d 70 fb 26  |s..&....C/:.mp.&|

…うん、…わかんない。0x80前後で分けてもデータは散らばっているし、2byte以上 連続して現れるような特徴的なデータもない。正解は 0xa9302201 の 下位31bitのXORなんだが、そんなのこのデータから探せる人は多分居ないだろう。 もちろん、XORであることを信じて総当りアタックされる可能性は残るが、 まぁゲーム一本にそんな手間かける人もそうそう居ないだろう。

復号化ルーチンが解析されて復号化方法が露呈してしまうのは仕方ない(後述するが、 そのために復号化ルーチンの難解析化は欠かせない)。 しかし、暗号化アルゴリズムとして、 「暗号化後のデータを人間が精査しただけで復号化されてしまう」のはエニグマも がっかりしちゃうくらいダメのダメダメだ。 暗号化を志すなら、「ちょっと捻る」くらいはしてもバチはあたらないと思うですよ?

●サンプルバイナリとそのソースコード

上で作ったxp3enc.dll、xp3dec.tpm、及び 上のソースコード全部含むサンプルコードをここに。 プロジェクトのプロパティとかは再設定しなきゃイカンけれど、 必要なファイルは全部ある、はず。参考にしてくだされぃ。

■その他の注意点

上のHOWツリーで書いた、実現可能でその他に注意すべきことを以下に述べる。 これらは必須ではないが、実装していくとどんどんクラック難易度が上がるよ、 というヒントまでに…。 市販ゲームでも、さすがにこれらを全部やってるのは見たことがないなぁ。

●暗号化・復号化手法の複雑化

上で述べた例はエンコード・デコードルーチンとしてはまだ簡単すぎる。 もっと複雑にすれば解読難易度はグィっと上がるので、 もっともっと複雑なものを考えればよい。しかし、あまり複雑になると、今度は 復号化に時間がかかり、ゲームの進行を妨げるかもしれない。ので、 「複雑で人間が解析するのは難しいが、機械が復号化するのは高速であること」が 必要になる。注意。

より良いのは、「(ユーザの手元に置かざるをえない)復号化プログラムは 異様に冗長だが、やってることは単純で復号化に時間がかからない デコードルーチン」。冗長であればあるほど、 解析者の気力を萎えさせることができる。例えば数命令毎に意味の無い命令が 混じっているとか、やたらめったらあっちこっちジャンプするとか、 間でグラフィック表示したりディスクアクセスしたりして一見デコードと 関係なさそうに見せるとか。 PC-88末期の市販ゲームのプロテクトや暗号化って、そういうのの境地だったなぁ…。

あ、あと暗号化を複雑にすると、XP3ファイルを作成する時に圧縮が効かなく なっちゃうかもしれないことに注意。今時は大した問題じゃないけれど。

●環境毎の暗号化キーの多様性

暗号化キーがゲームに対して一つ固定的に決まっていた場合、誰かが一度復号化に 成功してその情報が流れると、全ての同ゲームを所有している人が同じ手法で 復号化可能になってしまう。 ヨシくんちで復号化した方法(それを実現するプログラム)は、 みっちゃんちでもそのまま使えるはずだ。これだと、暗号化の意味が薄れてしまう。

一応、上のエンコード・デコードルーチンでは、「ファイルハッシュ値の 下位31bitでxor」という方法を採っている。これだと、ファイル毎に暗号化キーが 異なるため、根性XOR(総当りでXOR値を探し出す)であるファイルを復号化できても、 同じXOR値で別のファイルを復号化することはできない。ある意味賢い。

で、それをもう少し進めて、「インストールされたハードウェアの情報を 使って暗号化する」というのを考えてみるとよい。この方法を使えば、 あるハードウェア上で動作するゲームバイナリはそのまま他のハードウェアに コピーしても動かなくなるわけで、より解析側からの難易度が上がる。

例えば、そのPCのマザーボードのUUIDを使う、ネットワークカードのMACアドレスを 使う、なんてのが考えられる。UUIDやMACアドレスはハードウェアに付随して 必ずユニーク(※VM上のマザーボードUUIDってどうなんかね)だし、 推測はかなり不可能だ。

で、これらの暗号化キーを入手したら、そのハードウェアにインストールする時に 「インストーラを使ってXP3ファイルをこれらのキーで暗号化しなおす」という作業を 敢行する。そうすれば、インストールされたものはそのPC上でしか復号化できない データを持つことになる。これで、たとえヨシくんが31bitローテートの XOR値を根性XORで探して復号化に成功しても、 みっちゃんちでは同じキーでは復号化できないことになる。うっひっひ。

この方法を採る時には、以下の四点に注意しなければならない。

  1. 元々インストール前のDVD上のデータが復号化困難になっていること
  2. 前述のようにインストーラが暗号化機能を持っていること
  3. 正規ユーザが別のPC上にゲームを移行する手段を用意すること
  4. ファイルが破損した時に回復する手段を用意すること
1.で思いつくのは、インストール時にアクティベーションのような仕組みがあって、 ゲームメーカーとの通信中にSSHみたいな公開・秘密キーをやりとりし、それらを 用いないと元データを復号化できないというもの。アクティベーションの時だけ インターネットに接続する必要があるが、それ以外ではユーザに制限もないし、 強度も高いと思う。…というか、究極的には、ゲームに必要ないくつかの ファイルをメーカー側に置いておいて、アクティベーション時に、それらを ユーザに合わせてエンコードしつつユーザのディスク上にコピーするようにすれば いいんだろうけどね。

●正規の方法以外での復号化ルーチン利用禁止

ちょっと気が利いたクラッカーは、「復号化の方法なんか探すの面倒くせぇ、 どーせxp3dec.tpm(dll)があるんだから、これをそのまま自作ツールから使って、 データ復号化してセーブすりゃいいじゃん」と考える。うん、実に賢い。 なので、そういうのを予め禁止しておく方法を考える。

あんま難しく考えてもアレなので、ここでは、復号化ルーチン TVPXP3ArchiveExtractionFilter()の中で、以下の項目をチェックするようにして 「自身を利用しようとしているプロセスの正当性を考える」ことくらいは してもいい。

  1. 自身のプロセスIDから、呼び出したプロセス名をチェック(krkr.exeか?)
  2. 呼び出したプロセスの実行パスは正常か?(インストールパス以外から呼ばれてないか?)
  3. そのプロセスのサイズ・MD5SUM値・signatureは正しいか?(改竄されていないか?)

●複号化コードの保護

まっとうに作ってしまうと、複合化コード(xp3dec.tpm)は暗号化されていない。 ということは、やる気のあるクラッカーがディスアセンブルして見れば、 さくさくと解析されてしまうということだ。 それでは困るので、このdllも暗号化しておこう。とはいえ、 全体を暗号化することはできないので、「暗号化した復号化ルーチンを データとして持ち、それを復号化するルーチンを追加する」形にする。つまり、 「xp3dec.dllは、暗号化されたデータ部分に実際の復号化処理を含むもの」とする。 このようにすることで、クラッカーの手間を増大させ、クラックを断念させる ことができる。…かもしれない。

こういうのもPC-88の市販ゲームのプロテクトに使われてたアナクロ技術。 世界は繰り返すということか…ッ!

■おまけ1:.tpmファイル、プラグイン追加を禁止、各ファイルのMD5sum・signature チェック

吉里吉里はプラグインをホイホイ追加できる。.tpmファイルに至っては、存在したら それを優先的に読み込んでしまう。当然、クラッカーが解析のために これらのファイルを自作し、追加すると、無条件で読んでしまう。 禁止しておかないと自作dllは追加され放題だ。

ところが、実は「禁止」は難しい。吉里吉里にそういう仕組みがないためだ。 一番いいのは吉里吉里本体のソースコード(のTVPLoadPlugins()関数)をそのように 書き換えてリコンパイルすることだが、それはかなりハードルが高い。 次善の策として、「ゲーム中でファイルリストとそのチェックサムを動的に逐次 チェックするようなdllを自前で書くこと」が挙げられる。パッチ適用のことも考える 必要がある(正規パッチはちゃんと受け入れなければならない)ため、このdllは 少々複雑になるのが玉に瑕。

なお、「ファイル破損チェックツール」というものが標準で用意されているが、アレは あくまで「能動的に動作させたときに初めてファイル破損をチェックする」だけ なので、この用途には使えないことに注意。

実際には、メモリ上に読み込んだ後にOllyDbg.exeなどで「えいや」で 自作プラグインへ飛ぶように改変したりもされちゃうので、ファイルだけじゃなくて メモリ上の改変も禁止できると嬉しい。いたちごっこなのは判るけどさ。

■おまけ2:xp3dec.tpm(dll)ファイルが独立しているということ

そもそもxp3dec.tpm(dll)ファイルが独立していると、普通のクラッカーなら これをどっこいしょとディスアセンブルしてさくさく解析してしまうだろう。 我輩ならそうする。で、こうなると少々複雑な暗号化でも復号化阻止は困難で、 時間さえかけられればバッチリ復号化突破されちゃうことになる。 これはもう仕組み上防げない。

なんとかするんなら、(結局本体のリコンパイルが必要になるけれど、) やっぱりデコードルーチンを埋め込んだ吉里吉里本体(krkr.exe)をリビルドするのが 一番安全かなーと。その時は関数名を完全に隠匿(ローカル関数みたくして 名前を詳らかにしない)するか、少なくとも「TVPXP3ArchiveExtractionFilter」なんて わかりやすいのではなくて「AllLayerVisible」みたいな一見関係なさそうな名前に 変更するなどして、解析者を翻弄するくらいの心意気は欲しいところ。

案外そういうアホみたいで地味なトラップが、真面目なクラッカーには効くのですよ。

■おまけ3:xp3dec.tpm(dll)中で、展開中のアーカイブ名(.xp3)を得る

2013/03/04追記。前からやりたいと思ってた、展開中のアーカイブ名を xp3dec.tpm(dll)のTVPXP3ArchiveExtractionFilter()から得る方法を調べたので 追記しておく。

TVPXP3ArchiveExtractionFilter()は、tTVPXP3ArchiveStream::Read()から 呼ばれる。で、アーカイブ名はtTVPXP3ArchiveStream->Owner->Nameに格納されている。 tTVPXP3ArchiveStream::Read()はtTVXP3ArchiveStreamクラスのメンバなので、 関数として呼ばれる時にはEBPがフレームポインタとして使われており、 ヘンに最適化されていなければ、[FramePointer+8]にThisポインタが 収められているはず。…というのを念頭に探すと、This.Owner.Name(の中の WCHARの文字列へのポインタ)は、以下のようにすれば取得できることがわかった。

void TVP_tTVPXP3ArchiveExtractionFilter_CONVENTION
       TVPXP3ArchiveExtractionFilter(tTVPXP3ExtractionFilterInfo *info)
{
    TCHAR *fnam;
    __asm {
	mov eax,  [ebp]        ; eax=FramePointer
	mov eax,  [eax+8]      ; eax=This
	mov eax,  [eax+4]      ; eax=&This->Owner
	mov eax,  [eax+0x801c] ; eax=&This->Owner->Name
	mov eax,  [eax+4]      ; eax=WSTR(This->Owner->Name)
	mov fnam, eax          ; fnam = WSTR(This->Owner->Name)
    }
    (続く)

これで、fnamに"file://./c/program files/ゲーム名/data.xp3"のように、 アーカイブ名のフルパス文字列が入る。 ただし、これは今のバイナリを逆アセンブルして調べた方法なので、これが 未来永劫使えるとは限らないことに注意。とりあえず、krkr.exeの 2.30.2.416と2.32.2.426では両方正しくアーカイブ名を得ることができた。 コンパイラやコンパイルスイッチが変わらなければずっと使えるんじゃないかしらん。 あとクラスメンバの構成が変わったりしない限り。…ハードル高いな…。

なお、アーカイブ中のファイル名は.Owner.ItemVector[index].Nameに 格納されているが、「今展開中の(アーカイブ内の)ファイル名」を求めるのは ちょっと難しい。さすがにそれは要らんかなという気もする…。

なんでこんな方法書いてるかというと、「アーカイブごとに暗号化・展開 アルゴリズムを変更する」というのを実現するため。なんでそんなことを しなければならんのかというと、もちろん難展開化というのもあるが、主には 別項で述べる、コピープロテクトのためだ。

■カンペキな暗号化は存在しない

解析しづらく、復号化を困難にすることはできる。 しかし、ハードウェア(例:ドングル)を使ったロックでない限り、 ある程度能力を持ったクラッカーが時間をかければ、全ての暗号化は解除可能だ。 これを承知した上で、可能な限り復号化しづらいものを作らねばならない。

何度か出てきたように、結局、こういうのを突き詰めていくと、自分で吉里吉里の ソースコードに手を入れてコンパイルして、専用の実行バイナリを生成するのが 最も製作側の自由度が高くなり、解析側の難度を上げることができる。このバイナリが さらに暗号化されているともっと難易度が上がる。まぁ…そこまではやりすぎだと 思わなくもないけれど。

デコードやアクティベーションなどのルーチンは隠さねばならないし、 暗号化強度を上げるならもっとエンコーダ・デコーダを複雑にせねばならない。 どんどんその作業量は増えてくる。だんだん、自分がなんでこんなことを やってるのかわかんなくなってくるだろう。

そういう時は初心に立ち返る。「自分が作りたいものはなんだったっけ?」。 プロテクトや暗号化に心血を注ぐのもそれはそれで面白いが、 自分がやりたいのはそういうことじゃなかったなぁ、と気づけば、 「ある程度割れを防止できればいいや」という割り切りをつけて、 どこかで線引きが可能になるだろう。

こういうときはどうすんだ!とか、こうやれば復号化できちゃうじゃん!とか もっとトレビヤンな方法あるぜー、とかご意見ある方は是非教えて下さい。 是非是非参考にしたいです。ホントに。

■正直、ビジネスにならんかコレ

暗号化とプロテクト、ネット経由のアクティベーションなどをセットにして、 エロゲ(別にエロゲじゃなくてもいいけど)メーカーに売る、というビジネスに ならんかと真剣に考えている。メーカー側も、直接利益にはつながらない(損失量が 明確に定義できない)ことに金と時間をかけたくはないだろうし、そういうのを 請け負えば正にWin-Winの関係になるのでは。 ユニークIDの発行作業まで請け負えば、結構な収入が見込めるんじゃなかろうか。 誰か会社起こして、我輩にアイディア使用量を払ってくれんか(自分でやる気 さらさら無し)。

ちょっと真面目に考えてみた。 ひと月に発売されるエロゲ本数が50本くらい、そのうち20%が吉里吉里ベース、 その20%がこのビジネスに乗っかると考えると、2本/月が売り上げ対象。 サポートフィーのように月掛けでアクティベーションサーバへの登録料を もらうようにし、一ヶ月10万円、平均で半年維持するとすると、一本あたり 60万円、一ヶ月で120万円の売り上げ。…うーん、もう少し裾野を広げないと、 ちょっとビジネスにするのは難しいかー…。5人が働くなら月商500万円くらい ないと厳しいので、対象を吉里吉里に絞らないか、採用率を上げるかして 売り上げを4倍に上げないと厳しいよなー。

人はこれを「取らぬ狸の皮算用」という。 いいんだよ!これこそがビジネスマインドなんだよ!(カッコよく言ったつもり)