古今東西のROクライアントのPacketLengthを取得できる技術というネタ
Developmentのカテゴリの方で用意している教材にPacketLengthを取得できるコードとフリーマウスカーソルを搭載。
SimpleROHook RCXの跡地 | Planetleaf.com Lab.
https://lab.planetleaf.com/memory-of-rcx
皆さんの知るPacketLengthという値はCRagConnection::GetPacketSize(int opcode)というコード内を調べると出てくるわけですが、
現在のROのクライアントはCRagConnectionという内部の中間層のC++クラス以降がスクランブルされてしまい、パターンマッチングではPacketLength@リプレイ機能が搭載されたROのexeのコードスクランブル | Planetleaf.com Lab.の記事で書いたようにまず不可能です。
実の所、代替DLL方式であればPacketLengthのツリーを初期化するコードからインジェクション関数を挟むことによりPacketLengthが取得できるのですが、この初期化はウインドウ作成前に実行されるためCBTでのフック設計ではこの技は使えません。
そこで別のアプローチを考えます。やる気促進のため2012/11/24現在のjROのコードを提示しましょう。
肝になるのは以下のコードです。このコードはアイテムを装備するパケットを送信するものですが、このコードの形状はコンパイラの変更やインデックスサイズの拡大で多少変化はあるものの、リニューアル以前からほとんど変 わりません。つまり元のC++のコードは10年不変という事です。(流石にBeta2からはずいぶんコードが変化していますが)
2012-10-17aRagexe_jROdump.exe 0048E69B B950D28800 mov ecx,88D250h ; CModeMgr::g_modeMgr (CModeMgr Instance) 0048E6A0 E8CB690C00 call 00555070 ; ecx->GetGameMode 0048E6A5 668B4E70 mov cx,word ptr [esi+70h] 0048E6A9 668B5674 mov dx,word ptr [esi+74h] 0048E6AD B8A9000000 mov eax,0A9h ; Packet opcode 0048E6B2 6689442408 mov word ptr [esp+8],ax 0048E6B7 8D442408 lea eax,[esp+8] 0048E6BB 50 push eax 0048E6BC 68A9000000 push 0A9h ; Packet opcode 0048E6C1 66894C2412 mov word ptr [esp+12h],cx 0048E6C6 6689542414 mov word ptr [esp+14h],dx 0048E6CB E8D0251E00 call 00670CA0 ; CRagConnection::instanceR 0048E6D0 8BC8 mov ecx,eax ; ecx = CRagConnection Instance 0048E6D2 E8C9201E00 call 006707A0 ; ecx->GetPacketSize( opcode ) 0048E6D7 50 push eax 0048E6D8 E8C3251E00 call 00670CA0 ; CRagConnection::instanceR 0048E6DD 8BC8 mov ecx,eax ; 0048E6DF E8EC261E00 call 00670DD0 ; ecx->SendPacket 0048E6E4 688A000000 push 8Ah 0048E6E9 B9D8C38B00 mov ecx,8BC3D8h ; UIWindowMgr::g_windowMgr 0048E6EE E8CDE10900 call 0052C8C0 ; ecx->DeleteWindow
これに着目すると、GetPacketSizeのアドレスが取得できることが分かると思います。便宜上ecx->GetPacketSizeなどと書いていますが、内部でインスタンスは使用されていないのでスタティックに呼び出しが可能なメンバーファンクションです。
つまり、
typedef int (__stdcall *tCRagConnection__GetPacketSize)(DWORD opcode);
と定義すればプロセス内であれば呼び出せてしまうという事です。
これで解決すればいいのですが、そうも行かず、このファンクションが返すのは純粋な値ではなく4以下のデータが全て4で帰ってくるという仕様で可変長パケットの判断等には使えません。
ファンクション内の捜索は前述のとおり諦めなくてはならないので、ここでは関数の呼び終わりにレジスターに入っている値に注目します。前準備としてツリーの構造体を定義しておきます。
struct p_std_map_packetlen { struct p_std_map_packetlen *left, *parent, *right; DWORD key; // opcode int value; // length };
GetPacketSizeはパケットのサイズが4以上であればstd::mapのファンクションよりデータを取得して純粋なパケットサイズを返すという動きになります。ファンクションの呼び出しに使われる規約はMS stdcallであり、これはeaxに戻り値、ecx、edxは保存しなくてよいという約束事があり、ecx、edxには処理の過程に代入された値が残っているという事になります。
一般にeax = アキュムレータ、ebx = ベースレジスタ、ecx = カウンター、edx = データレジスタなどという住み分けがあるわけですが、基本的に汎用レジスタです。ただし、使用されていないレジスタを優先して使うという作法を考えると最適化されたコードはある程度予測は立てられるのでファンクションから戻ってきた時のレジスターの状態に着目してみます。
ecxがC++のインスタンスとして使用されますが、これは内部処理に於いて保障されません。ebxに関しては保存対象という事で除外、用途としてはファンクション内の変数を参照するためベースレジスタとして使用されます。eaxはそもそも戻り値なので除外とすると、作業過程、つまりツリーを辿るためのポインタがecxかedxに入っている事になります。
そしてecx、edxを調べてアドレスと思えるデータが入っていれば恐らくそれがp_std_map_packetlenのポインタです。
コードで書くとこんな感じに、
__asm push 0x0A9 __asm call GetPacketSizeAddr __asm mov g_temp_eax,eax __asm mov g_temp_ecx,ecx __asm mov g_temp_edx,edx p_std_map_packetlen *plen; if( g_temp_eax == g_temp_edx ){ plen = (p_std_map_packetlen*)g_temp_ecx; }else{ plen = (p_std_map_packetlen*)g_temp_edx; } while(1) { if( plen->key > 0xffff || (plen->key == 0 && plen->value == 0) ){ packetLenMap = plen; break; } plen = plen->parent; }
これを最初の親まで辿っていけばパケットツリーのルートポインタが取得できます。
PacketLength+4のアドレスに入ってるデータがこのルートポインタとなるので、実質パケット長が取得可能になる訳です。なお、std::mapなどのSTLの内部定義はSTLのバージョンに依存するので注意が必要です。
ディスカッション
コメント一覧
まだ、コメントがありません