http://wordpress.click3.org/garakuta/th135_ai.zip
キャラ別能力に関するメソッド群の追加とキャラ日紐づくobjectの取得とrequireの追加。
こいしさんだけあまりにも難解だったので妥協していて、セットされている技のIDが取れていない。
いい加減サンプルコードとドキュメントの充実頑張らないとなぁ。
http://wordpress.click3.org/garakuta/th135_ai.zip
キャラ別能力に関するメソッド群の追加とキャラ日紐づくobjectの取得とrequireの追加。
こいしさんだけあまりにも難解だったので妥協していて、セットされている技のIDが取れていない。
いい加減サンプルコードとドキュメントの充実頑張らないとなぁ。
http://wordpress.click3.org/garakuta/th135_ai.zip
ひとまずクラッシュなどの致命的なバグもとれて、必要最低限の機能も実装できた版。
まだまだ足りない機能はあるが、今の時点である程度のAIまでは組めると思われる。
この辺りで機能拡張は一旦休憩にして、ドキュメントの整備とかサンプルコードの追加とかやるべきかもね。
http://wordpress.click3.org/garakuta/th135_ai.zip
土日中に形にしたかっただけなのでかなり強引にリリースした。
ドキュメント系もやっつけ仕事だし、クラッシュしたりユーザーにやさしくない動作もたくさんある。
でもまぁ、そのへんは今後のリリースで良くしていくということで。
前回ようやく心綺楼の解析に目処が付いたので、th135_ai(仮)開発に向けてライブラリの選定などをしていました。
その関係でmrubyに触れる機会があったのですが、いつもの様にマイクロスレッドもどきを作ってみたところ、200回以上resumuするとクラッシュというよくわからない事態に遭遇。
最初は自分のコードにバグがあるかと思ったのですが、最小再現コードを作ってみればrubyコードだけでクラッシュしていることが判明。
どうやらmrubyのバグを踏んだようです。
というわけで報告して来ました。
https://github.com/mruby/mruby/issues/1434
最小再現コード
t = Fiber.new do while true do 1.times do Fiber.yield end end end while true do [t].each do |t| t.resume end end
無意味そうなwhileやtimesがありますが、削ると発生しなくなります、謎。
windows8.1 previewという非正規な環境ではありますが、発生率は100%
mruby.exeで発生するので、自作コードによるものではないことは確実。
原因は何なんでしょうね?
Fiberによる状態の保存と復帰処理が、ある程度以上複雑なコールスタック上で行われると壊れるとかですかね?
まぁどっちにしろ、これで転けられるとマイクロスレッド中毒な自分からしたら話にならないので
すぐ治るようでなければ、th135_aiでは別言語を採用しないといけなそうです。
何がいいだろう、個人的にはSquirrelとかLuaの貧弱さが気に入らないので
もっと高機能の載せたいんですが、そうすると今度は組み込みコスト高くてなぁ……
まぁ次はpythonでも試してみましょうかね。
追記:作者さんからリプ飛んできて速攻直りました。
Matzさんすごいなー。
東方心綺楼の体験版が頒布された当時自分は、東方緋想天と同様に数々のツールが作られ、単純に対戦する以上の楽しみが待っていると疑いませんでした。
しかしそれから待てど暮らせど、ツールの一つ出てくる気配はありません。
その謎は数カ月後に明かされることになります。
Web体験版の公開、意気揚々と逆アセンブラをかけた自分の目の前に広がっていたのは、経験したことのない難解なコードでした。
というわけで、東方心綺楼がなぜ解析しづらいのか、また自分がここまで解析してきた成果をまとめておこうと思います。
結論から言えば、C++などのネイティブ言語だけで作られたものではないから、という一言につきます。
通常CやC++などネイティブな言語のみで作られている場合、難読化をかけたとしても似通った処理は一箇所にまとめられることになるため、ある程度知識があれば読み解くことはそう難しいことではありません。
例えば、暗号化されたファイルを読む場合、ファイルを読むReadFileのAPIをフックして場所を特定し、そこから何度かリターンを経たあたりに復号化処理が書かれているわけです。
しかし、今回の心綺楼はSquirrelというアプリ内組み込みの言語によって大部分が実装されており、上記のようには行きません。
単純に変数から読み上げるというだけで幾つもの関数を呼び出したりリターンしたりと処理があっちこっちへと行き来し、人間がコードの意味を理解しながら本質的な部分まで追う労力は生半可なものではないからです。
つまり、逆アセンブルなど機械語に翻訳される言語用に特化した解析手法ではツール作成はかなり難しいことになります。
※綺録帖のようになんとかしたツールも存在するので、完全に不可能というわけではありません
ではどうするのか
Squirrelで書かれているならば、こちらもSquirrelと同じ土俵で戦えばいいのです。
前の章でSquirrelと同じ土俵で戦えばいいと書きました。
ではSquirrelとは何なのかといえば、アプリ内組み込みで用いられるスタックベースの小さな言語になります。
th123_aiで使われているluaと同種の言語ですね。
軽くリファレンスを読んでみればわかりますが、SquirrelのCインターフェースは、基本HSQUIRRELVMを第一引数に受けて動作するようになっているため、HSQUIRRELVMさえ自由に出来れば乗っ取れそうです。
HSQUIRRELVMを取得するため、HSQUIRRELVMを受けていて、なおかつ解析しやすいように特徴的な文字列を扱っている関数を探します。
するとSQGenerator::Yieldがよさそうだということがわかりました。
bool SQGenerator::Yield(SQVM *v,SQInteger target) { if(_state==eSuspended) { v->Raise_Error(_SC("internal vm error, yielding dead generator")); return false;} if(_state==eDead) { v->Raise_Error(_SC("internal vm error, yielding a dead generator")); return false; } SQInteger size = v->_top-v->_stackbase; ……以下略……
※SQVM *とHSQUIRRELVMはtypedefされているだけで同じ型
では実際に、OllyDbgを使って一行目の文字列で検索し、使用している関数先頭にbreakpointを設置。
その状態で心綺楼を実行し、breakpointに引っかかった時点でスタックからHSQUIRRELVMの値を取得。
さらにメモリーエディタを用いてその値を格納しているメモリーを検索すると、どうやらスレッドローカル領域に格納しているようでth135.exeの.tlsセクション内がヒットしました。
これでSquirrelの世界へ攻め入ることができます。
解析してツールを作ろうというのにHelloWorldというのも変かと思われるかもしれませんが、まだ前の章で取得した値が本当にHSQUIRRELVMなのか確認できていません。
急がば回れ、ここは慎重に動作確認から行きましょう。
C++サイド
sq_setprintfunc(v, printfunc, printfunc); // printfuncの実装については省略 sq_pushroottable(v); sqstd_dofile(v, "main.nut", 0, true); sq_pop(v,1);
main.nut
print("HelloWorld");
ひとまずこんなコードをdllインジェクションして実行してみました、クラッシュしました。
……やはり確認は重要ですね。
VisualStudioでスタックトレースをみてみるとsq_dofile内のreallocでランタイムチェックに失敗してクラッシュしているようです。
なぜreallocでクラッシュするのかというと、メモリーの効率化のためランタイムライブラリの内部でmallocやfreeやreallocする対象のメモリーについて管理を行なっており
にも関わらず自作コード内と心綺楼本体という別々に管理されているものを混ぜてしまったことが原因となります。
話は簡単、自作コード内と心綺楼内部に存在するsq_dofileのうち心綺楼内部の方を使えばいいのです。
というわけでHSQUIRRELVMを探してきた時と同じように、ユニークな文字列を探し、そこからサーチすることにします。
sqstd_loadfileにユニークな文字列があったので、そこを呼び出している関数一覧を出し、それらの逆アセンブラコードを軽く読んでsqstd_dofileを特定しました。
ではsqstd_dofileをそのアドレスに置き換えてみて実行してみましょう。
マルチスレッド対応でもないのにメインではないスレッドから使用しているためたまにクラッシュしますが、一応HelloWorldと表示されることもあるはずです。
ちょっと不安になる結果ではありますが、HSQUIRRELVMはこの値で正しいようです。
前の章でとりあえずHelloWorldは出来ました。
しかしたまにクラッシュしたり、表示文字列が化けるようなのはなんとも消化不良な感が漂います。
そこで、解析を次のステップに進める前に、動作を安定化させようと思います。
動作が不安定なのは別スレッドから内部のスタックを触っているからにほかなりません。
ではメインスレッドで自分を実行させる方法はないのか、リファレンスをひっくり返して探して来ました。
その結果、sq_setnativedebughookで関数ポインターを設定すれば、Squirrelの関数が実行されたタイミングで自作コードを走らせることが可能らしいとの記述が。
念のためsq_setnativedebughookの実装も読んできましたが、特にマルチスレッドで操作して落ちる恐れも無さそうです。
というわけで現状のコードはこちら
#include <stdio.h> #include <algorithm> #include <boost/version.hpp> #include <boost/static_assert.hpp> #include <boost/filesystem.hpp> #include <Windows.h> #include <org/click3/dll_hack_lib.h> #include "squirrel.h" unsigned int baseAddr; SQRESULT (*dofile)(HSQUIRRELVM, const SQChar *, SQBool, SQBool); void printfunc(HSQUIRRELVM v, const SQChar* s, ...) { va_list arglist; va_start(arglist, s); vprintf(s, arglist); va_end(arglist); } void hook(HSQUIRRELVM v, SQInteger type, const SQChar * const sourcename, SQInteger line, const SQChar * const funcname) { static bool first = true; if (!first) { return; } first = false; sq_pushroottable(v); dofile(v, "main.nut", 0, true); sq_pop(v,1); } void main() { org::click3::DllHackLib::SetupConsole(); baseAddr = reinterpret_cast<unsigned int>(::GetModuleHandleW(NULL)); dofile = reinterpret_cast<SQRESULT (*)(HSQUIRRELVM, const SQChar *, SQBool, SQBool)>(baseAddr + 0x0034AAF0); unsigned int addr = 0; while(addr == 0) { Sleep(1000); addr = *reinterpret_cast<const unsigned int *>(baseAddr + 0x004D7984); } HSQUIRRELVM v = reinterpret_cast<HSQUIRRELVM>(addr); sq_setprintfunc(v, printfunc, printfunc); sq_setnativedebughook(v, hook); }
※インジェクション部分は省いています
さて、前の章でSquirrelの中にコードをインジェクションしたり、内部構造を自由に操作することができるようになりました。
では次はキャラのxy座標の取得を目標に、Squirrelの内部データサーチを行……う前にちょっと脇道にそれましょう。
まずSquirrelのグローバルな名前空間のものを一覧表示してみます。
色々面白いものは見つかりますが、キャラ情報らしきものは見つかりません。
そこでテーブル要素に限り1階層だけ下まで中身を表示するようにしてみます。
おや、save_dataの下にenable_kokoroという好奇心を刺激されるものがありますね。
ちょっと有効にしてみましょう。
※2013/08/03現在「秦こころ」は未実装であり、ストーリー以外で使用することはできません。
void hookEnableKokoro(HSQUIRRELVM v, SQInteger type, const SQChar * const sourcename, SQInteger line, const SQChar * const funcname) { static bool first = true; if (!first) { return; } first = false; sq_pushroottable(v); sq_pushstring(v, "save_data", -1); sq_get(v, -2); sq_pushstring(v, "enable_kokoro", -1); sq_pushbool(v, true); sq_set(v, -3); sq_pop(v, 2); }
さて、この状態で心綺楼を起動してみるとどうなるのでしょうか……
キター
はい、ここまで本筋と関係のない脱線でした。
ちなみにこの状態で設定をいじったりプロファイルをいじったりするとセーブされてしまい、enable_kokoroへの操作をやめてもこころさんは有効になったままになってしまいます。
試す人は注意。
とりあえず対戦中にしてから前述のグローバル名前空間内の物を列挙してみます。
すると、actor以下にplayer1といういかにもなものが増えていました。
試しにplayer1以下を表示させてみると、lifeやら何やら色々出てきて、もうこれに間違いないという気分になりますが、なぜかxyだけ見当たりません。
どういうことかと思い調べてみると__gettable以下にxとyという関数が。
これは何らかの抽象化の臭いがします。
なのでとりあえず以下の様なコードで取得を試みてみました。
void hookPrintPos(HSQUIRRELVM v, SQInteger hookType, const SQChar * const sourcename, SQInteger line, const SQChar * const funcname) { sq_pushroottable(v); sq_pushstring(v, "actor", -1); sq_get(v, -2); const unsigned int size = sq_getsize(v, -1); if (size < 2) { sq_pop(v, 2); return; } sq_pushstring(v, "player1", -1); sq_get(v, -2); sq_pushstring(v, "x", -1); if (SQ_FAILED(sq_get(v, -2))) { printf("error\n"); sq_pop(v, 3); } int x; sq_getinteger(v, -1, &x); sq_pop(v, 1); sq_pushstring(v, "y", -1); if (SQ_FAILED(sq_get(v, -2))) { printf("error\n"); sq_pop(v, 3); } int y; sq_getinteger(v, -1, &y); sq_pop(v, 2); sq_pushstring(v, "player2", -1); sq_get(v, -2); sq_pushstring(v, "x", -1); if (SQ_FAILED(sq_get(v, -2))) { printf("error\n"); sq_pop(v, 3); } int ex; sq_getinteger(v, -1, &ex); sq_pop(v, 1); sq_pushstring(v, "y", -1); if (SQ_FAILED(sq_get(v, -2))) { printf("error\n"); sq_pop(v, 3); } int ey; sq_getinteger(v, -1, &ey); printf("%d:%d %d:%d\n", x, y, ex, ey); sq_pop(v, 4); }
すると、予想通りXY情報が取得出来ました。
後から知ったことなのですが、__gettableなどはSqratというバインダーを使っていると増えるメンバーで、メソッドが見つからなかった場合の処理をフックしてC側の実態にひもづける何かなのだとか。
とにもかくにもxy情報も無事取得できました、めでたしめでたし。
如何だったでしょうか?
主にSquirrelレイヤーに乗り入れる方法がメインで、そのあとは大体蛇足だった感じがします。
最後に上記のコードを利用して、ひたすら近づいてA連するだけのAIをおいておきます。
DLLインジェクション方法まで含んでいるので、試しに動かしてみたい方はどうぞ。
http://wordpress.click3.org/garakuta/th135hack.zip