Packet Hacking

翻訳元について


I can not find a way to contact the author, so this translation is unauthorized...
If there are any problems, I delete this page immediately.
My Contact: marshxmallow222[at]gmail.com

パケットハッキング

本誌ではパケット解読とパケットのエミュレートを記しています。このシリーズでは私(g3nuin3)とhunterがWater Margin(水滸伝)をどのようにハックしてどのようにそのパケットの解読に取り組んだか記します。なお、本誌では送信パケットデータのハッシュ1のリバーシングのみカバーしています。その全てを明らかはしているわけではありませんが、おそらく十分カバーされているものと考えています。本チュートリアルでは、読者にアセンブリ言語の知識があることを想定しており、それに加えてデバッグ手法およびいくつかのデバッグ用語の知識があると理解の助けになります。

今日では 多くのMMOがそのデータベースとユーザを保護するための抜本的な対策を行っております。私はこれには反対しないし、私自身は純粋な悪のもと…あるいはその知識や実践…あるいは楽しみのために、ゲームをぶち壊すタイプの人間ではありません。したがって、この記事で明らかになった情報を使用して何か行う際には自己責任でお願いします。

ゲームパケットの解読に着手する上で必要とされるものがいくつかあります。それは多くの失敗と多くの時間とそして多くのスキルです。特にスキルは多いほどよく、アセンブリ言語を理解することは必須スキルです。ゲームがデータを暗号化する方法を知るためには、かなり多くのコードをトレースしなければならないからです。なお、ゲームのパケットを解読する方法は1つではありません。よってこの記事にあるテクニックを他のゲームにそのまま使用できるとは限りません。パケットの解析には、ゲームプロトコル上で独自の暗号化を使用するアンチハッキングプロテクション(XTrap, Hackshied, Nprotect)などの多くの障害があり解読の邪魔になります。また、ゲームによっては重要なデータを操作する方法がサーバに対する攻撃のみとなってしまうものもあります。Water Marginの場合は、そのどちらにも当てはまりません。Water MarginはCRC32テーブルとハッシュを使用してクライアントとサーバ間での単純なプロトコルの暗号化を行っているだけです。

WPE Proを呼ばれるツールを聞いたことがあるかと思います。これはターゲットプロセスにDLLを注入していくつかの既知のwinsock関数をフックする(通信を仲介する)プログラムであり、そのフックはsend/WSASend/recv/WSArecv APIによって使用される生のパケットデータを我々が読解できる簡単なフォーマットに置き換えるものです。また、パケットデータを編集したり送り返したりすることも可能です。あたなはこのツールの利用が効果的であると思うかもしれません。しかし、ゲームパケットの解析にはこのツールだけでは不十分であり、暗号化されていないゲームでの利用以外ではWPEは全く役に立たないでしょう。データが暗号化されている場合、WPEを使用して分かるのはそのゲームが暗号化を使用しているかどうかぐらいです。では、暗号化されたパケットを解読する場合に我々はその内容をどのように知ればよいのでしょうか。

Water Marginをお持ちの場合は実行してWPE Proでそれをターゲットプロセスにしてください。パケットを解読する上での最良ではないが最も簡単な方法はチャットパケットを見ることです(いくつかのゲームではチャットパケットを暗号化しないため、コードをトレースする上での良い出発点となります)。パケットとは単にクライアントからサーバへ送信される16進データであることを理解してください。WPEで現在のデータを分析するためにフィルタリングを行います。今、我々は送信パケットを記録したいので、WPEの「View -> Option」を選択し、Winsock 1.1で「Send」のみにチェックを入れます。また、Winsock 2.0ではWSASendにチェックを入れ、バッファサイズはそのままにしておきます。私の場合パラメータは5000になっています。(一度に5000を超えるバッファを記録することはおそらくないでしょう。)
imageプラグインエラー : ご指定のファイルが見つかりません。ファイル名を確認して、再度指定してください。 (image033.JPG)

OK、今Water Marginが実行中かつサーバにログインしていれば、場所はどこにいても大丈夫です。WPEの「PLAY」ボタンを押すとパケットの記録が開始されます。ゲーム内で、文字「A」をチャットに入力します。さらに、文字「B」を入力した後で文字列「ABC」を入力します。さて、今のところはこれで十分でしょう。得られたパケットを確認してみます。

「A」 の入力時に得られたパケット:
AA 0F 00 00 00 09 00 00 00 52 CB F9 D3 78 9C 13 17 60 66 00 02 47 06 00 01 EF 00 6C

「B」 の入力時に得られたパケット:
AA 0F 00 00 00 09 00 00 00 5B 39 F0 39 78 9C 13 17 60 66 00 02 27 06 00 01 F1 00 6D

「ABC」の入力時に得られたパケット:
AA 11 00 00 00 0B 00 00 00 B3 C5 AD AA 78 9C 13 17 60 65 00 02 47 27 67 06 00 04 25 00 F3

注意:パケットを解読するとき、通常最初の1バイトまたは2バイトの16進データは「パケットシグネチャ(ヘッダ)」であることに気づくはずです。これは一般的に「パケットの種類」を知るために用いられています。その他、可能性が高いのがパケットデータのサイズを示す場合でしょう。そしてパケットシグネチャの後に、いくつかの雑多なデータ(ハッシュやタイムスタンプなど)が続いています。これらは暗号化されている場合もあります。最後に、パケットデータの終わりを意味する「パケットフッタ」のようなものが付与される場合があります。これらはゲームによりけりで必ずしもそうなるとは限りませんので、あなたは慎重に調査を行わなければなりません。

さて、あなたがよく考えられる人なら先のパケットが全ての点で同じにはならないことに気づいたでしょう。

我々が今これらのパケットが何であるかを解読して認識しようと試みる場合、多くの時間を浪費することになりかねません。我々はどのようにして暗号化されたデータを解読すればよいのでしょうか?WPE内のダンプを見てみると何か奇妙なことに気づきませんか?はて、我々が"A"(ASCIIコードで0x41)を入力したはずなのに、ダンプされたパケットにはそれが含まれていないのはなぜでしょうか?「暗号化」がその質問への唯一の答えになります。

OK、我々は先に小さなパケットの記録を済ませたところでした。WPEが正しく動作したのは間違いありませんが(暗号化された状態のままでは)私たちの役にはたたないのです…。あなたは文字を入力してチャットパケットを再送信することができますが、おそらくそのパケットを一体全体どうやって解読したものか見当もつかないことでしょう…もちろんチャット以外でも…キャラクターの移動パケットの再送信と確認を繰り返してみてください…。

本誌における筆者の使命は、読者にパケットの暗号化を追跡する方法を示すことです。チャットパケットは暗号化されていますが、ゲームの次の実行まで有効期限が切れることはありません。このゲームでは、セッションキーと呼ばれるものが使用されます。これは、あなたがゲームにログインするとサーバが送信する、そのゲームセッションのためのパケット暗号化の基となるユニークな鍵のことです。それではこの理論を証明するためにWater Margin を閉じて、WPEから取得したパケットデータを保存しておきます。その後、Water Marginを再起動して以前と同じパケットを再送信してもこれが動作することはありませんよね。

とにかく、我々は今何をすればよいでしょうか? 我々は生のパケットデータを釣り当てに行きます。文字列の入力について考えると、チャットでENTERキーを押したときにあなたが入力したデータが正しく暗号化される上で、どこかに保持されるのではないでしょうか?そして…それを見つけることさえできれば、データが向かう先で暗号化ルーチンなども見つけることができるでしょう。暗号化される前のデータの探索を開始する前に、少しカンニングをしましょう。我々は何種類かの暗号化の仕組みを知っているので、ゲームで既知の暗号化技術が使用されている場合にはこれを確認することができます。KANALプラグインを導入したPEIDを使用することでこれを行うことができます。このプラグインは本誌の終わりに添付されています。Krypto Analyzerプラグインを使用した後に見られるのが以下の画像になります。

フィルタされていない参照は以下になります。

さて、これは一体何を意味するのでしょうか。このゲームがいくつかのチェックサムを計算するためにCRC32を使用していることを意味しているのかもしれませんが、そのCRCテーブルへはかなりの数の参照があります。それでは、これらの後で出くわすかもしれない参照について念のため覚えておくこととしましょう。この最初の「Referenced at 005651D3」という参照に対して特別な注意を払うことになります。

コードのトレース

さて、我々は暗号化される前の生のパケットデータがある場所を発見しようと試みるところでした。これにはあなた自身の直感が入ってくるところであり、トレースするべきルーチンが何であるか推測して、多くのコードのトレースを介することで見つけることができるでしょうが、これは退屈な作業となります。そこで、我々が最初に行うのはデバッガの便利な利用方法について確認することです。私の場合は、OllyDebuggerを利用します。

では、ゲームを実行してOllyでそれをアタッチしてください。我々の目標はWinsockのSend()関数からバックトレースして、あわよくばパケット解読のためのいくつかの興味深いルーチンを見つけることです。Ollyのコマンドバー内に”bp send”と入力します。では、Water Marginで何か入力してみましょう。すぐにsend()への呼び出しにおいてブレークが発生します

Ollyのローカルスタックウインドウでこれを参照してください。今のところこの情報は重要ではありません。しかし、WPE Pro内で参照することのできるパケットデータを保持しているのはこの”Data”という引数で、当然のことながらこれは既に暗号化されたデータとなっています。(これを確認したい場合は、WPEでパケットを記録して、Olly内でsend関数にブレークポイントがセットされている間に何かしらのデータを送信します。すると、Ollyでブレークが発生するので”Data”パラメータを右クリックして”Follow in Dump”を行ってください。その後でF9キーを押して実行を再開すると、ダンプされたのと同じデータがWPEに送り出されます。)

OK、ローカルスタックウインドウで”Call to send from 108Online.xxxxxxxx”を右クリックして”Follow in Disassembler”をクリックしてください。CPUウィンドウが表示されます。

デバッグより、CALL NEAR EDIはsend関数の呼び出しであることが分かっています。このsend関数はこのゲーム内のプロシージャの1つに存在しており、データが送信される前の多くの操作がこのプロシージャの前方で行われています。そこで、まずはこの関数の先頭を見つけましょう。CPUウィンドウをスクロールアップして、この処理の開始部分を見つけます。

一般的な開始処理ではないもののなんとなく気になる部分が…私はこの関数を注意深く観察して、いくつかの怪しい呼び出しや怪しいコードを選んでみました。この種の作業に慣れている私の目はいくつかの理由からこの最初の呼出しに注目しました。F9キーを押してゲームを再開し、アドレス00584ADAの呼び出しにブレークポイントをセットします。ブレークポイントをセットした後、再びゲーム内でランダムで無意味な文字列を入力するとブレークポイントがヒットしました。それでは、そのスタックを見てみましょう。

私の最初の推測の結果は…うーむ…まだ興味深いものではなく、ECXのダンプを行ったものの、今のところ面白い結果は見受けられませんでした。

そこで私は最初の呼び出しに戻ってOllyでF7キー(ステップ・イン)を押して関数の中身を確認すると、以下の非常に小さいが非常に興味深いルーチンを見られました。

005845D0 8B41 04 MOV EAX, DWORD PTR DS:[ECX+4]
005845D3 8B40 08 MOV EAX, DWORD PTR DS:[EAX+8]
005845D6 0341 08 ADD EAX, DWORD PTR DS:[ECX+8]
005845D9 C3 RETN

上記の命令について、それぞれ何が得られるのかを確認しましょう。最初の命令ではECX+4のポインタによって指されるDWORD値がEAXに格納されます。私の場合、EAXは02165C10を保持していました。次の命令では、EAX+8のポインタによって指されるDWORD値がEAXに格納されます。このときEAXは0933BE00を保持していました。ダンプウインドウでこのアドレスを参照したところ驚きの結果が得られました。

ダンプウインドウから私が入力した値を推測することができますか? そうです、私がゲーム内で入力したのは”fff”という文字列でした。私はすぐに、これがオリジナルのデータが保持されていた場所の1つであると確信しました。そして、それがNull終端文字で終わるだろうと仮定して、これが’00’で終わるようハイライトしています。そして、このデータを変更することでそれが向かう場所をテストすることにしました。

私はダンプウインドウの”fff”のテキストをハイライトして、右クリックして”Binary -> Edit”を選択し、文字列のサイズは保ったまま、別の3文字に変更しました。

まずはこれをYYYという文字列に変更して、その後でゲームの実行を再開しました。すると次のような結果が得られました。

OK、予想通りチャット文字列が変更できました、しかし、可能ならばもっと扱いやすいものにしたいですよね…私は考えます…うーむ…そういえばこのデータは複数の場所からアクセスされていましたが、実際にデータが作成されるところを我々は確認できていませんでした。チャットパケットを送信する別の良い代替案としては、このチャットデータ”のみ”がプロシージャに引数として渡される場所を見つけることです。そこさえ見つければ、我々はそれをフックして任意のチャットを送信することができるようになります。

生のパケットデータは先に我々がそのデータを取得した関数内に存在し、それは数種類の構造体であることが分かっています。それでは、何かをチャットパケットの代わりにキャラの移動パケットのダンプがどのように変化するかを参照してみてください。キャラを移動するとブレークが発生し、次のようなデータがダンプされます。

Ollyではどのデータが変更されたかを赤で示してくれます。これを見るといくつかの経験に基づいた推測を得ることが出来ます。チャットパケットのダンプは最初のバイトが17でしたが、今回はそれが19になっています。うーむ、チャットパケットの3バイト目は10以外の数でしたが、これは入力した文字列に応じて変化していたはずです。分別の目的で、複数のチャットパケットを記録してみましょう。今回は4文字(gggg)を入力してみます。結果、ダンプは最初に示した例と1部分を除いて同じになりました。3バイト目は、3文字の場合は05になり、今回4文字入力した場合には06となりました。うーむ、3バイト目はデータのサイズを示しているのでしょうか? 実際のデータを見てみましょう。前のチャットデータは3文字で3バイト長ですが、Null終端文字が文字列の終わりに付加されるのを思い出してください。よって3文字の場合4バイトのデータになります。また、奇妙なことにパケットデータの先に必ず00のデータが付加されていることに気づきました。これはなぜでしょうか。気になりますが、これについては何のためのデータであるのか説明のしようがありません…。このことから、3バイト目はデータサイズであると推定されます。おそらく、データの最初と終わりにNull文字が付加されている、あるいは分かりやすくするために0を置いているのでしょう。そして、最初のWORDはパケットの種類を表しています。

ハッシュの取得

さて、パケット構造体がどのように構成されているかについていくつかの推論を立てましたので、これを証明するための適切な場所が必要でしょう。リバーシングの作業の後、知人の助けもあって、生のデータへのポインタを使用するだけでなく、そのハッシュを返す非常に興味深い関数を見つけました。これが、本誌でエミュレートすることになるハッシュ関数となります。この関数はチャットデータを送信して、そのハッシュを取得するための最適なフックポイントであると確信しました。フックする前に、まずはこの関数の詳細を示します。この関数呼び出しはアドレス005655F1に位置しています。Ollyでは次のようになります。

この関数が何を返すのかを確認しましょう。すぐ下で返り値(EAX)がEBX+30に格納されていることが分かります。今、EBX+30が何であるかは問題ではありません。ここにブレークポイントをセットしてゲーム内で適当な文字列を入力してENTERを押すと、ここ(005655F1)でブレークが発生します。ブレークが発生するとEAXにある値が格納されます。OllyでF8キーを押して関数をステップオーバーしましょう。レジスタウインドウのEAX値が即座に赤になり、新たな値が格納されます。これは奇妙な値であり、この関数は引数で与えられたデータを使用してある値を返すものであると確信しています。そうです、これはハッシュを返します。では、このハッシュを取得してみましょう。

一見すると、このコードはスタックに3つの引数をプッシュしていることが分かります。何がプッシュされているのか調べてみましょう。関数内で数回ブレークさせてその時の引数を解析します。Ollyのローカススタックを確認してみましょう(ローカルスタックではスタックにプッシュあるいはポップされる値を確認することができます)。ローカススタックの値を以下に示します。

3つの値をプッシュしているのでスタック上の現在の3つの値を確認します。その後で、Ollyのダンプ内の2番めのパラメータを確認すると以下が得られました。

これは生のパケット構造体のアドレスであり、最初のパラメータは常に1となっています。最後のパラメータは、パケット構造体のサイズ(バイト単位)を示しています。さて、我々はいくつかのアクションを実行するために必要な生のパケット構造体を使用する関数を発見しました。本誌では、それが何の目的でデータを使用するかは重要ではありません。(全体的には非常に重要な機能では有りますが)。今我々は解析済みのパケット構造を取得しており、これを簡単なコードによってテストすることができます。

ハッシュ取得のためのコーディング

ここでは、多くのリバーシングを行いコードの働きを推測します。しかし、パケット構造体についての我々の仮定が本当に正しいかどうかは確実に証明することはできません。ここではチャットパケットのみ取り扱うので、その情報をまとめましょう。最初の2バイトがパケットIDを示していることが我々の調査から判明しており、チャットパケットの場合は全て"17 10"で始まることも分かっています。インテルのプロセッサはバイトオーダーを逆順(リトルエンディアン)にとるので、これはC++言語では0x1017として表されます。次の4バイトはデータのサイズです。データの最初に0が付加され、最後にもNull終端文字が付加されることを思い出してください。その後で雑多なデータが付属しています。

よって、次のようなパケット構造体が定義できます。
struct Packet {
    short packetID;
    int dataSize;
    char data[50];
};

当該関数内にステップ・インして確認したところ、パケットの最大データサイズは5552(15b0h)であることが分かっています。必要であればこのサイズを変更することも可能ですが、この制限を超えないようすることは特に重要ではありません。今必要とされているのはシンプルなエディットボックスより得られるテキストをパケット構造体に適用させることです。以下にそのコードを示します:
int SendInfo1(Packet *p, char *data)
{
      int rett;//returned hash
      p->PacketID = 0x1017;//chat ID
      strcpy(p->data+1, (data)-1);//copy the data in the data element +1 ( remember the prepended 00)
 
      p->data[0] = 0; //the 0 before the data.
      p->datasize = 2 + strlen(p->data);  // the size of the data is the length of the actual data plus the prepended 0.
      Packet *pp = p;
 
      int sizeofPacket = sizeof(short)+sizeof(int)+ p->datasize;  //  PacketID + PacketSize + data
      unsigned int func = 0x0056D445;
      __asm{
                  push sizeofPacket
                  push pp
                  push 1
                  call func
                  mov rett, eax //this is just that hash we mentioned, not doing anything with it now ;)
      }
      return rett;
}
 

この関数を利用して、0056D445に位置する関数より生成されるハッシュを保持します。面倒くさがりな解析者にとってはこの程度のやり方で十分でしょう。しかし、ゲームパケットのリバーシングのポイントはこれらを自分自身で再構成できるようにすることです。リバーシングのため、このハッシュ関数を解析してパケットのためのハッシュを自身で作成できるようにしてみましょう。

ハッシュ関数のリバーシング

我々はこの関数があった場所を覚えています。これをリバーシングして独自のバージョンのコードを生成するためにOllyと一緒にIDAを使用します。Ollydebugのストリップは次のようになります:

108Onlin_0056D445:                           ;<= Procedure Start
        PUSH    EBP
        MOV     EBP, ESP
        MOV     ECX, [ARG.2]; ECX holds pointer to raw data
        PUSH    ESI
        PUSH    EDI
        MOV     EDI, [ARG.1] ;Holds key. (always 1  :/)
        MOV     ESI, EDI ; temporary ‘key’ holder (first param
        AND     ESI, 0xFFFF ; AND’s the temp key with 0xFFFF(65535)
        SHR     EDI, 0x10 ; right shifts argument 1 with 0x10(16)
        TEST    ECX, ECX ; checks if there is any data to work with
        JNZ     108Onlin_0056D467 ; does check for size parameter if there is.
        XOR     EAX, EAX ; if not
        INC     EAX ; returns 1
        JMP     108Onlin_0056D55A ; exit
 
108Onlin_0056D467:
        CMP     [ARG.3], 0 ; size if zero or below?
        JBE     108Onlin_0056D553 ; jmp
        PUSH    EBX ; ebx is pointer to original data
 
108Onlin_0056D472: //Checks size limit
        MOV     EDX, 0x15B0 
        CMP     [ARG.3], EDX
        JNB     108Onlin_0056D47F; If the size is larger than 5552, truncate it
        MOV     EDX, [ARG.3]
 
108Onlin_0056D47F: //
        SUB     [ARG.3], EDX ; subtracts size from itself.
        CMP     EDX, 0x10  ; jump is the datas size  is lower than 16.
        JL      108Onlin_0056D522 ; takes the jump for our chats
        MOV     EAX, EDX
        SHR     EAX, 4
        MOV     EBX, EAX
        NEG     EBX
        SHL     EBX, 4
        ADD     EDX, EBX
 
108Onlin_0056D499:  // this routine is NOT very important after some tracing.
        MOVZX   EBX, BYTE PTR DS:[ECX]
        ADD     ESI, EBX
        MOVZX   EBX, BYTE PTR DS:[ECX+1]
        ADD     EDI, ESI
        ADD     ESI, EBX
        MOVZX   EBX, BYTE PTR DS:[ECX+2]
        ADD     EDI, ESI
        ADD     ESI, EBX
        MOVZX   EBX, BYTE PTR DS:[ECX+3]
        ADD     EDI, ESI
        ADD     ESI, EBX
        MOVZX   EBX, BYTE PTR DS:[ECX+4]
        ADD     EDI, ESI
        ADD     ESI, EBX
        MOVZX   EBX, BYTE PTR DS:[ECX+5]
        ADD     EDI, ESI
        ADD     ESI, EBX
        MOVZX   EBX, BYTE PTR DS:[ECX+6]
        ADD     EDI, ESI
        ADD     ESI, EBX
        MOVZX   EBX, BYTE PTR DS:[ECX+7]
        ADD     EDI, ESI
        ADD     ESI, EBX
        MOVZX   EBX, BYTE PTR DS:[ECX+8]
        ADD     EDI, ESI
        ADD     ESI, EBX
        MOVZX   EBX, BYTE PTR DS:[ECX+9]
        ADD     EDI, ESI
        ADD     ESI, EBX
        MOVZX   EBX, BYTE PTR DS:[ECX+0xA]
        ADD     EDI, ESI
        ADD     ESI, EBX
        MOVZX   EBX, BYTE PTR DS:[ECX+0xB]
        ADD     EDI, ESI
        ADD     ESI, EBX
        MOVZX   EBX, BYTE PTR DS:[ECX+0xC]
        ADD     EDI, ESI
        ADD     ESI, EBX
        MOVZX   EBX, BYTE PTR DS:[ECX+0xD]
        ADD     EDI, ESI
        ADD     ESI, EBX
        MOVZX   EBX, BYTE PTR DS:[ECX+0xE]
        ADD     EDI, ESI
        ADD     ESI, EBX
        MOVZX   EBX, BYTE PTR DS:[ECX+0xF]
        ADD     EDI, ESI
        ADD     ESI, EBX
        ADD     EDI, ESI
        ADD     ECX, 0x10
        DEC     EAX
        JNZ     108Onlin_0056D499
 
108Onlin_0056D522:
        TEST    EDX, EDX   ; another check to see if it was 0 (size of data)
        JE      108Onlin_0056D531 ; if it was 0 jump.This eventually means that no data will be sent in send()
 
108Onlin_0056D526: // this little routine here adds up the byte data in our packet struct.
        MOVZX   EAX, BYTE PTR DS:[ECX] ; ECX is our packet structs data.
        ADD     ESI, EAX ; ESI is the result of our bytes being added.
        INC     ECX ; move to next byte
        ADD     EDI, ESI ; add this byte to EDI
        DEC     EDX ; EDX is size of packet, decreases till end.
        JNZ     108Onlin_0056D526 ; loop
 
108Onlin_0056D531:
        MOV     EAX, ESI  ; the end result is put into EAX
        XOR     EDX, EDX  ; zeroes out EDX
        MOV     EBX, 0xFFF1 ;
        MOV     ESI, EBX ;
        DIV     ESI
        MOV     EAX, EDI ; EDI held small ‘checksum’ of our data being added.
        MOV     ESI, EDX ;EDX == the end result ESI Held.
        XOR     EDX, EDX ;Zeroes out EDX
        DIV     EBX  ; EBX == EDI.
        CMP     [ARG.3], 0  ; checks third argument, remember it was zeroed out.
        MOV     EDI, EDX ; EDI = EDX
        JA      108Onlin_0056D472 ; no jump.
        POP     EBX ;
 
108Onlin_0056D553:
        MOV     EAX, EDI  ; EAX = EAX << 16 || ESI
        SHL     EAX, 0x10;
        OR      EAX, ESI;
 
108Onlin_0056D55A:
        POP     EDI
        POP     ESI
        POP     EBP
        RETN                                 ;<= Procedure End
 

この出力はOllyプラグインの”CodeRipper”を使用して得られたものです。さて、このルーチンから分かることは… 実際のデータは“MOVZX EAX, BYTE PTR DS:[ECX]”のコードから格納され始めるということです。以下にこの関数のC++の実装を示しますが、ハッシュを作成するために必要ではないジャンクコードの大部分を省いてあります:
unsigned int __cdecl getHashB(int key, void *data,unsigned int size)
{
 
      unsigned char *pData = (unsigned char *)data; //save data in temp as to not corrupt it
 
      unsigned int sum = key & 65535; // temp being the key AND's with 0xFFFF
      key >>= 16; //shr 0X10
      if(pData == 0) {
            return 1;
      } else {
            if(size > 0) { // truncating if size is larger than limit
                  if(size > 5552)
                        size = 5552;
 
                  for (int i=0;i < size;i++) {  //
                        sum += ( pData[i] & 255);
                        key += sum;
                  }
            }
            return (key << 16) | sum;
            /*    MOV     EAX, EDI  ; EAX = EAX << 16 || ESI
                  SHL     EAX, 0x10;
                  OR      EAX, ESI;
                  */
      }
 
}
 

完成したハッシュ関数がゲームと同じハッシュを返しているかどうかをテストする必要があります。そこで私はその比較を行うためにハッシュテストプログラムにいくつかの機能を追加しました。結果はどうでしょうか?

そして、SendInfo関数を修正したSnedInfo2関数になります。
int SendInfo2(Packet *p, char *pdata)
{
      int rett;//returned hash
      p->PacketID = 0x1017;//chat ID
      strcpy(p->data+1, (pdata)-1);//copy the data in the data element +1 ( remember the prepended 00)
 
      p->data[0] = 0; //the 0 before the data.
      p->datasize = 2 + strlen(p->data);  // the size of the data is the length of the actual data plus the prepended 0.
      Packet *pp = p;
 
      int sizeofPacket = sizeof(short)+sizeof(int)+ p->datasize;  //  PacketID + PacketSize + data
      int i = 1; // arg 1 == 1.
 
      rett = getHashB(i, pp, sizeofPacket);
      return rett;
}
 

我々はついに正常にハッシュを返す関数をリバーシングすることができました!コングラッチュレーション!
最終更新:2014年07月10日 16:32