AGTHを逆にして代替GUIを再作成する

リバースエンジニアリングのトピックはハブで非常に人気があるため、このトピックに関するベストプラクティスを共有することにしました。 私は、 ビジュアルノベルの多くのファンのように、 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 // SizeOf = 8168 bytes Context: Cardinal; Subcontext: Cardinal; ProcessID: Cardinal; UpTime: Cardinal; TextLength: Cardinal; HookName: array [0 .. 23] of ansichar; Text: array [0 .. 4061] of widechar; end;
      
      





アプリケーションとゲーム間の通信を理解しました。次に、テキストキャプチャモジュールがゲームに入り、フックを設定する場所と方法に関する情報を受信する方法を見つける必要があります。



ブートローダーの研究



ゲーム(または他のアプリケーション)を実行し、最終ダウンロードを待って、デバッガーでフックします。 次に、モジュールのリストを開いて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を作成した後、正しく終了し、次に変数-パラメーターも終了します。



もう一度、簡単に:





ちなみに、このようなスタックを持つゲームは、RETの後の関数がそれを呼び出したコードではなく別の関数に入る場合、リターン指向プログラミングテクニックまたは単にROP(Return-Oriented Programming)と呼ばれます。



さて、ターゲットプロセスへのパラメーターの実装と転送を理解しました。すべてのパラメーターは、「AGTH」という名前の環境変数を介して渡されます。 独自のブートローダーを作成する場合、環境変数を設定してdllをロードするだけで十分であることがわかります。



ローダー:
 //       TInject = packed record // code cmd0: BYTE; cmd1: BYTE; cmd1arg: DWORD; cmd2: BYTE; cmd2arg: DWORD; cmd3: WORD; cmd3arg: DWORD; cmd4: BYTE; cmd4arg: DWORD; cmd5: WORD; cmd5arg: DWORD; cmd6: BYTE; cmd6arg: DWORD; cmd7: WORD; cmd7arg: DWORD; // data pLoadLibrary: Pointer; pExitThread: Pointer; pSetEnvironmentVariableW: Pointer; ENVName: array [0 .. 4] of WideChar; ENVValue: array [0 .. MAX_PATH] of WideChar; LibraryPath: array [0 .. MAX_PATH] of WideChar; end; const //     PUSH: BYTE = $68; CALL_DWORD_PTR: WORD = $15FF; INT3: BYTE = $CC; NOP: BYTE = $90; {  Dll   } class function THooker.InjectDll(Process: DWORD; ModulePath, HCode: WideString): boolean; var Memory: Pointer; CodeBase: DWORD; BytesWritten: SIZE_T; ThreadId: DWORD; hThread: DWORD; hKernel32: DWORD; Inject: TInject; function RebasePtr(ptr: Pointer): DWORD; //      //    begin Result := CodeBase + DWORD(ptr) - DWORD(@Inject); end; begin Result := false; //      //        Memory := VirtualAllocEx(Process, nil, sizeof(Inject), MEM_TOP_DOWN or MEM_COMMIT, PAGE_EXECUTE_READWRITE); if Memory = nil then Exit; CodeBase := DWORD(Memory); hKernel32 := GetModuleHandle('kernel32.dll'); //   : //  Inject       FillChar(Inject, sizeof(Inject), 0); with Inject do begin // code cmd0 := NOP; cmd1 := PUSH; cmd1arg := RebasePtr(@ENVValue); cmd2 := PUSH; cmd2arg := RebasePtr(@ENVName); cmd3 := CALL_DWORD_PTR; cmd3arg := RebasePtr(@pSetEnvironmentVariableW); cmd4 := PUSH; cmd4arg := RebasePtr(@LibraryPath); cmd5 := CALL_DWORD_PTR; cmd5arg := RebasePtr(@pLoadLibrary); cmd6 := PUSH; cmd6arg := 0; cmd7 := CALL_DWORD_PTR; cmd7arg := RebasePtr(@pExitThread); // data //      , //  ImageBase kernel32.dll     //         //  -      //     kernel32.dll  //     //       pLoadLibrary := GetProcAddress(hKernel32, 'LoadLibraryW'); pExitThread := GetProcAddress(hKernel32, 'ExitThread'); pSetEnvironmentVariableW := GetProcAddress(hKernel32, 'SetEnvironmentVariableW'); lstrcpy(@LibraryPath, PWideChar(ModulePath)); lstrcpy(@ENVName, PWideChar('AGTH')); lstrcpy(@ENVValue, PWideChar(HCode)); end; //       WriteProcessMemory(Process, Memory, @Inject, SIZE_T(sizeof(Inject)), BytesWritten); //    hThread := CreateRemoteThread(Process, nil, 0, Memory, nil, 0, ThreadId); if hThread = 0 then Exit; //      WaitForSingleObject(hThread, INFINITE); CloseHandle(hThread); VirtualFreeEx(Process, Memory, 0, MEM_RELEASE); // -      Result := true; end;
      
      







ここで、パラメータ、より正確には、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|#ordinal}]] - select OK for more help /NC - don't hook child processes /NH - no default hooks /NJ - use thread code page instead of Shift-JIS for non-unicode text (should be specified for capturing non-japanese text) /NS - don't use subcontexts /S[IP_address] - send text to custom computer (default parameter: local computer) /V - process text threads from system contexts /X[sets_mask] - extended sets of hooked functions (default parameter: 1; number of available sets: 2)
      
      





次に、ランダムなコマンドラインパラメーターを入力し、それらが最終結果にどのように影響するかを確認します。

たとえば、キーセット「/ 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; //    /h        U i := pos('/h', lcmd); if i > 0 then begin uFlag := copy(AGTHcmd, i, length(AGTHcmd) - (i - 1)); // /h -> endstr delete(uFlag, 1, 2); // del /h i := pos(' ', uFlag); if i > 0 then delete(uFlag, i, length(uFlag) - (i - 1)); uFlag := 'U' + uFlag; end else uFlag := ''; //    /s        S0: i := pos('/s', lcmd); if i > 0 then begin sFlag := copy(AGTHcmd, i, length(AGTHcmd) - (i - 1)); delete(sFlag, 1, 2); // del /s i := pos(' ', sFlag); if i > 0 then delete(sFlag, i, length(sFlag) - (i - 1)); sFlag := 'S0:' + sFlag; end else sFlag := ''; Result := IntToHex(flags, 1) + sFlag + uFlag; end;
      
      







したがって、ライブラリのパラメーター形式が解析されました。



終わり



以上です。 残っているのは、独自のインターフェイスを実装し、必要な機能を追加することだけです。 行われたこと:



コードの残りの部分を書くことは十分に簡単なので、ここでは説明しません。Githubへのリンクを残してください。



All Articles