古今東西のROクライアントのPacketLengthを取得できる技術というネタ

2014年9月17日

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のバージョンに依存するので注意が必要です。

Ragnarok Online

Posted by redchat