リバースエンジニアリングのトピックはハブで非常に人気があるため、このトピックに関するベストプラクティスを共有することにしました。 私は、
ビジュアルノベルの多くのファンのように、
AGTH(Anime-Game-Text-Hooker)などのプログラムに精通しています。 それはあなたがその後の翻訳のために短編小説からテキストを抽出することを可能にします(ほとんどのゲームは日本語です)。 どうやら、このプログラムの開発は2011年に中止されたため、ソースコードが見つかりませんでした。魂は追加機能を必要としていたため、このプログラムを元に戻し、受け取ったデータに基づいて、私にとって欠けていたすべての機能を備えた代替シェルを再作成することにしました
元のプログラムは2つの部分で構成されています-実行可能ファイルと、動的ライブラリの形式で作成されたインターセプトモジュールです。 このプログラムはゲームプロセスでこのライブラリを実装し、その助けを借りてそこからテキストを受け取ります。
実行可能ファイルの変換と書き換えのみを行い、元のモジュールは傍受のために残します。 これにはいくつかの理由があります。 モジュールの明らかな複雑さと私の怠に加えて、いわゆるHコードとの開発の互換性を確保する必要があります。 Hコードは、デフォルトのフックが無効な場合にインターセプターがフックを正しく設定するために必要なデータセットです。 メモリアドレス、レジスタ番号、およびゲーム内のテキストの場所に関するその他の情報が含まれています。 個々のゲームごとに、このコードは一意であり、愛好家によって発見されます。 したがって、「に基づいて」モジュールを記述することは機能しません。 これらのコードの完全な互換性を確保する必要がありますが、これはまったく異なるレベルの複雑さです。 また、追加の利点はありません。
傍受モジュールとAGTHの通信プロトコルの分析
明らかに、ゲーム内の傍受モジュールとAGTHはどういうわけか相互に作用します。代替シェルを作成するには、その方法を調べる必要があります。 ウィンドウメッセージからソケットに至るまで、あるプログラムから別のプログラムにデータを転送する方法はたくさんあります。 実際に使用された方法は、偶然に学びました。
プロセスエクスプローラーで agth.exeプロセスのプロパティに移動し、このプログラムに含まれる行を確認することにしました。
"\\。\ Pipe \ agth"という行がすぐに目を引きました-これは
名前付きチャンネルの名前です。つまり、AGTHはパイプを使用してゲームと通信していると想定できます。 これで、検索を開始する方向が決まりました。 デバッグには、非常に愛されている
OllyDbgデバッガーを使用します。
AGTHを「Olya」にロードし、すぐに
kernel32モジュール内の
CreateNamedPipe *関数にブレークを設定します。 これらのブレークの1つは、プログラムが名前付きパイプを作成しようとするとすぐに機能し、この時点からこれらのパイプで機能するコードに到達できるようになります。
実行を継続し、ブレーカーの2回目の作動から適切な場所に到達します。 スタック上の文字列「\\。\ Pipe \ agth」の存在は、この場所が必要であることを示しています。
ここで、スタックの一番上にあり、
CreateNamedPipeWを呼び出した直後にコードを指すアドレス
0x00AF3A64に移動します。
001B3A43 > 56 PUSH ESI ; 0x0 00AF3A44 . 6A 00 PUSH 0 00AF3A46 . 68 00000200 PUSH 20000 00AF3A4B . 6A 00 PUSH 0 00AF3A4D . 68 FF000000 PUSH 0FF 00AF3A52 . 6A 06 PUSH 6 00AF3A54 . 68 01000840 PUSH 40080001 00AF3A59 . 68 A026AF00 PUSH agth.00AF26A0 ; UNICODE "\\.\pipe\agth" 00AF3A5E . FF15 4010AF00 CALL DWORD PTR DS:[<&KERNEL32.CreateName>; kernel32.CreateNamedPipeW 00AF3A64 . 8BF8 MOV EDI,EAX 00AF3A66 . EB 03 JMP SHORT agth.00AF3A6B
ここで、パイプが作成されたパラメータ、すなわち、
CreateNamedPipeW("\\.\pipe\agth", 40080001, 6, 0xFF, 0, 0x20000, 0, NULL);
ドキュメントを使用して、マジックナンバーを名前付き定数に拡張します。 次のようになります。
CreateNamedPipeW("\\.\pipe\agth", PIPE_ACCESS_INBOUND | FILE_FLAG_OVERLAPPED | FILE_FLAG_FIRST_PIPE_INSTANCE, PIPE_WAIT | PIPE_READMODE_MESSAGE | PIPE_TYPE_MESSAGE, 0xFF, 0, 0x20000, 0, NULL)
コードを少し実行すると、
ConnectNamedPipeおよび
WaitForMultipleObjects関数の呼び出しが表示され、作成されたパイプからのイベントが期待されます。
さて、今、あなたはデータがどのように読み取られるか、あるいはゲームからアプリケーションに送信されるデータブロックのサイズを知る必要があります。 データが連続したバイトストリームではなくブロックで送信されるという事実は、チャネルの作成時に使用される
PIPE_TYPE_MESSAGEフラグの存在によって示されます。
WaitForMultipleObjectsが制御を返した後、新しいスレッドが作成され、おそらく接続されたばかりのパイプでイベントを処理することに気付くのは簡単
です 。 アドレス
0x00CC5080に行きましょう。
以下は、パラメーターを指定して呼び出される目的の
ReadFile関数です。
0291D9B4 00000104 |hFile = 00000104 (window) 0291D9B8 0291DA78 |Buffer = 0291DA78 0291D9BC 00001FE8 |BytesToRead = 1FE8 (8168.) 0291D9C0 0291DA14 |pBytesRead = 0291DA14 0291D9C4 004C4168 \pOverlapped = 004C4168
ReadFileを呼び出すために事前に設定した、クラックが壊れた瞬間にスタックから取得しました。 一般に、8168バイトの
BytesToReadパラメーターのみに関心があります。 おそらく-これは、ゲームがプログラムに送信するテキストを含む構造のサイズです。
その結果、ゲームとのやり取りがどのように行われるかについて十分な情報が収集されました。AGTHは、8168バイトのデータを受信するパイプサーバーを実装します。 これで、これらのバイトの意味を分析することができます。
プログラム内でデータ形式の分析を行うことにしました。 その中で、以前に取得したデータを使用して独自のサーバーを実装し、それでゲームからメッセージを受信しました。 非常に便利-適切なサイズの構造を取得し、その中にデータを直接読み込むことができます。 これまたはそのバイトグループの意味を分析するプロセスで、この構造を変更し、最後にすべてのフィールドの完全な説明を取得できます。
それはゲームからプログラムに来るように見えるものです。 UserHookQとKotarouの行はすぐに印象的で、1つ目は元のプログラムに表示される関数の名前、2つ目はゲームのUTF-16エンコーディングのテキストです。 また、数字7(青いハイライト)が表示されます。これは、判明したとおり、常にゲームテキストの行の文字数と同じです。 さまざまなデータセットを調べてみると、関数名は最大24文字のnullで終わる文字列であることがわかりました。 つまり、上記のスクリーンショットの場合、緑と青の強調表示の間のすべてのバイトは単なるゴミです。 構造の先頭には、さらに16個のデータバイトが残っています。 最初の2つの変数は簡単に特定できました。これらはコンテキストとサブコンテキストであり、元のプログラムウィンドウでも確認できます。 3番目のパラメーターを見つけるのはもう少し難しくなりました。常に小さい値で、ゲームを再起動したときにのみ変更されました。 ゲームのProcessIDであることが判明しました。 4つの最後の値は常に変化しており、かなり大きな値を持ちました。 唯一の手がかりは、この値が常に時間とともに増加し、決して減少しないことです。 これは時間であり、
GetTickCount関数を呼び出した結果です。
結果は次の構造になります。
TAGTHRcPckt = packed record
アプリケーションとゲーム間の通信を理解しました。次に、テキストキャプチャモジュールがゲームに入り、フックを設定する場所と方法に関する情報を受信する方法を見つける必要があります。
ブートローダーの研究
ゲーム(または他のアプリケーション)を実行し、最終ダウンロードを待って、デバッガーでフックします。 次に、モジュールのリストを開いて
kernel32を選択し、
LoadLibrary *で開始するすべての関数の関数リストにブレークポイントを設定します。 これは、これらの関数のいずれかを呼び出すことで最終的なdllの読み込みが行われ、呼び出しをインターセプトすると、スタックに沿ってさまよい、ブートローダー自体に移動できるためです。
プログラムを継続します。 次に、AGTHを実行してゲームプロセスを表示します。
agth /PN_.exe
デバッガーはそこで動作します。 私の場合、内訳は
LoadLibraryW関数で機能しました。
スタックを見てみましょう:
上から2番目は関数の引数ですが、最初は戻りアドレスであり、
kernel32の腸にどこかにつながります。 奇妙なことに、ゲームに埋め込まれたローダーコードのアドレスが表示されると予想していました。 それでは、
LoadLibraryW引数を使用して次にあるものを見てみましょう。 アドレス
0x7EF80022に行きましょう。ここにあります!
ちなみに、これは非常に
注意が必要なブートローダーです。コマンドは4つしかありません(アドレス
0x7EF80014から
始まり 、データが送られます)。
7EF80000 68 1E00F87E PUSH 7EF8001E ; UNICODE "0" 7EF80005 68 1400F87E PUSH 7EF80014 ; UNICODE "AGTH" 7EF8000A 68 121E4D75 PUSH kernel32.LoadLibraryW 7EF8000F -E9 CE9755F6 JMP kernel32.SetEnvironmentVariableW
最初に、
SetEnvironmentVariableW関数のパラメーター
(「AGTH」、「0」)がスタックされ 、次に
LoadEnvironmentVariableW関数の戻りアドレスとして機能する
LoadLibraryW関数のアドレス
がスタックされます。 「だからこそ、
LoadLibraryWが、ローダーではなく
kernel32の腸内のどこかから呼び出されたのです!」-そう思いました。 しかし、
LoadLibraryが機能した後に何が起こるかという考えに
悩まされてきました。 そこで、呼び出し後に同じコントロールがどこに戻るかを見てみることにしました。 アドレス
0x754D3677にアクセスして、
以下を確認します。
754D3677 50 PUSH EAX 754D3678 FF15 F0064D75 CALL DWORD PTR DS:[<&ntdll.RtlExitUserThread>] ; ntdll.RtlExitUserThread
どうやら、
LoadLibraryWを呼び出した後、
LoadLibraryWを返すパラメーターを指定して
RtlExitUserThreadが呼び出されるため、リモートスレッドは正常に完了します。 すべてがうまくいくように思えますが、考えは私を置き去りにしませんでした。「このアドレスはスタックのどこから来たのでしょうか。 確かに、ローダーコードには種類はありません!」 最初のブートローダー命令が呼び出される前でさえ、誰かがこれらのアドレスをスタックに置いていることがわかりました。 そして、それは私に気付きました:リモートスレッドは
CreateRemoteThread関数を使用して作成され、関数ポインターに加えて、この関数のパラメーターも取ります。 つまり
、アドレス
RtlExitUserThreadを最初にスタックに
プッシュするため、スレッドは
RETを作成した後、正しく
終了し、次に変数-パラメーターも
終了します。
もう一度、簡単に:
- CreateRemoteThreadは、 RtlExitUserThreadのアドレスをスタック、dllへのパスにプッシュし 、ブートローダーを開始します
- ローダーは、 LoadLibraryWのアドレスであるSetEnvironmentVariableWの引数をスタックし、SetEnvironmentVariableWへの無条件ジャンプを行います。
- SetEnvironmentVariableWはスタックから引数を取り、スタックから戻ると、ストリームはLoadLibraryWの先頭にあります
- LoadLibraryWは、スタックからdllへのパスを取得し 、そこから戻ると、スレッドはRtlExitUserThreadに移動します
- RtlExitUserThreadはスレッドを終了します
ちなみに、このようなスタックを持つゲームは、RETの後の関数がそれを呼び出したコードではなく別の関数に入る場合、リターン指向プログラミングテクニックまたは単に
ROP(Return-Oriented Programming)と呼ばれます。
さて、ターゲットプロセスへのパラメーターの実装と転送を理解しました。すべてのパラメーターは、「AGTH」という名前の環境変数を介して渡されます。 独自のブートローダーを作成する場合、環境変数を設定してdllをロードするだけで十分であることがわかります。
ここで、パラメータ、より正確には、Hコードを設定するプログラムのコマンドラインが同じ環境変数の値にどのように変わるかを理解する必要があります。
デバッガーで絶え間なく動き回らないようにするために、スタブライブラリが作成されました。
スタブコード:
library AGTH; uses windows; var buffer: array [0 .. 255] of widechar; begin GetEnvironmentVariableW('AGTH', buffer, 256); MessageBoxW(0, buffer, buffer, 0); end.
次に、元のdllを置き換えて、考えられるすべてのコマンドラインキーを並べ替え、それらが環境変数にどのようにマッピングされるかを確認し始めました。 簡単なことがわかりました。
すべてのコマンドのリストは、元のプログラムに組み込まれているヘルプに記載されています。 これらのコマンドのうち、私はフックオプションにのみ興味がありました。
フックオプション: /H[X]{A|B|W|S|Q}[N][data_offset[*drdo]][:sub_offset[*drso]]@addr[:module[:{name|
次に、ランダムなコマンドラインパラメーターを入力し、それらが最終結果にどのように影響するかを確認します。
たとえば、キーセット
「/ HQN54 @ 48693e / NH / Slocalhost」は
「20S0:localhostUQN54 @ 48693e」に変わり 、キー
/ Hおよび
/ Sの値がそのまま送信されることがすぐにわかります。 また、接頭辞
Uおよび
S0:は、対応するキー
/ Hおよび
/ Sがない場合にのみ変更され、完全に消えないこともわかりました
。 他のすべてのキーは、最初の2つの16進数にのみ影響します。 キーで遊んだ後、これらがビットフラグであることがもう少しわかりました。各キーは、これら2つの数値が表すバイトに個別のビットを設定する役割を果たします。
結果はタブレットでした:
/nh - 20 - 10 0000 /nc - 10 - 01 0000 /nj - 08 - 00 1000 /x3 - 06 - 00 0110 // /x2 /x /x2 - 04 - 00 0100 /x - 02 - 00 0010 /V - 01 - 00 0001
コマンドラインをHコード関数に変換する const PROCESS_SYSTEM_CONTEXT = $01; HOOK_SET_1 = $02; HOOK_SET_2 = $04; USE_THREAD_CODEPAGE = $08; NO_HOOK_CHILD = $10; NO_DEF_HOOKS = $20; class function THooker.GenerateHCode(AGTHcmd: string): string; var i: Integer; lcmd, uFlag, sFlag: string; flags: BYTE; begin lcmd := lowercase(AGTHcmd); flags := 0; if pos('/nh', lcmd) > 0 then flags := flags or NO_DEF_HOOKS; if pos('/nc', lcmd) > 0 then flags := flags or NO_HOOK_CHILD; if pos('/nj', lcmd) > 0 then flags := flags or USE_THREAD_CODEPAGE; if pos('/v', lcmd) > 0 then flags := flags or PROCESS_SYSTEM_CONTEXT; if pos('/x3', lcmd) > 0 then flags := flags or (HOOK_SET_1 or HOOK_SET_2) else if pos('/x2', lcmd) > 0 then flags := flags or HOOK_SET_2 else if pos('/x', lcmd) > 0 then flags := flags or HOOK_SET_1;
したがって、ライブラリのパラメーター形式が解析されました。
終わり
以上です。 残っているのは、独自のインターフェイスを実装し、必要な機能を追加することだけです。 行われたこと:
- 階層化されたウィンドウの助けを借りて、ゲーム上の字幕出力が実装されました
- グーグル翻訳者との統合を追加
- 翻訳前にテキストを前処理するためのJSユーザースクリプト
コードの残りの部分を書くことは十分に簡単なので、ここでは説明しません
。Githubへのリンクを残してください。