1 ポイント 投稿者 GN⁺ 2024-07-15 | まだコメントはありません。 | WhatsAppで共有
  • 2001年のRTS Emperor: Battle for Dune は、現代のWindowsでは実行・インストール・オンラインプレイのすべてが不安定だったが、EmperorLauncher はそれを再びプレイ可能な状態へ復活させるパッチ
  • 主な改善点は、高解像度対応、60 FPS制限、直接IPでのオンラインマルチプレイ、協力キャンペーンモード、壊れたインストール手順の回避に集中している
  • 実装は、Emperor.exe の置き換えランチャー、Game.exe を対象にした DLL注入、Microsoft Detours による関数パッチ、Direct3D 7 レンダリングフック、winsock の横取り、簡易WOLサーバーで構成される
  • オンラインプレイでは、既存のP2P・任意ポート・NAT punching構造を単一のクライアント→サーバー接続へトンネリングし、サーバーホストだけがネットワーク設定を気にすればよいように変更した
  • 元のCDファイルのコピー、.cab 展開、公式 v1.09 パッチの適用、WOLAPI.DLLCOM登録回避、Win32 ランチャーUIまでをまとめ、インストールから起動まで処理する

Emperor: Battle for Duneが行き詰まっていた点

  • Emperor: Battle for Dune は 2001年に Westwood Studios が制作したリアルタイムストラテジーゲームで、Dune 2000 の続編
  • 現代のシステムでは、プレイを妨げる問題が各所に残っている
    • 現代の画面に合った 高解像度 で実行できない
    • マルチプレイではゲームシミュレーション速度が制限されず、速すぎる
    • Westwood Online(WOL) がすでに動作しないため、LAN外でのマルチプレイが難しい
    • 協力キャンペーンはオンライン専用機能のため、LANでは使えない
    • ディスクに含まれるインストーラーが壊れている
    • 現代PCの高いフレームレートでは、さまざまな視覚効果が崩れる
  • EmperorLauncher はこれらの問題を解決するためのパッチで、ダウンロードファイルとソースコードが公開されている

Emperor.exe の置き換えと Game.exe の初期化

  • ゲームの Emperor.exe は実際のゲーム実行ファイルではなく、Game.exe を起動する薄いラッパー
  • Game.exe を直接実行しても何も起こらないため、Emperor.exe が行っていた初期化手順を解析し、置き換えランチャーで再現する必要があった
  • 解析には IDA が使われた
    • IDA は実行ファイルを逆アセンブルし、一部のコードをC形式にデコンパイルできる
    • 型情報や構造体情報が失われたバイナリから、関数呼び出しや Windows API の使用を追跡しなければならない
  • Emperor.exeGame.exe 実行前に mutex と匿名ファイルマッピングハンドルを作成し、Emperor.dat から読み込んだデータを処理したあと、マッピングへコピーする
  • 親プロセスは CreateProcessA で得た子プロセスのメインスレッドIDへ Windows メッセージを送り、ファイルマッピングハンドル値を渡す
    • カスタムメッセージIDとして 0xBEEF が使われる
    • ファイルマッピングデータは "UIDATA,3DDATA,MAPS" の3つの文字列で、ゲームのアセット読み込みコードへ渡される
  • 復号コードを直接再実装する代わりに、Game.exe の位置にダンプツールを置いて受け渡しデータをディスクへ保存し、その後ランチャー側で同じシーケンスを実行した

DLL注入と関数パッチの方式

  • パッチを適用するには Game.exe プロセス内でユーザーコードを実行する必要があり、そのために CreateRemoteThread + LoadLibrary 方式が使われた
  • 注入手順は次の順序で進む
    • 対象プロセスのメモリに VirtualAllocEx でバッファを確保する
    • WriteProcessMemory で DLL パス文字列をコピーする
    • LoadLibrary のアドレスと DLL パスバッファを CreateRemoteThread に渡し、対象プロセスで DLL をロードする
    • DLL の DllMain が実行され、パッチコードが動作する
  • プロセスを suspended 状態で開始してから DLL を注入し、main 実行前にコードを走らせられるようにした
  • 既存関数の修正には Microsoft Detours が使われた
    • Detours は元の関数先頭命令をジャンプ命令に書き換え、呼び出しを置き換え関数へ送る
    • 上書きした元の命令は別メモリへコピーし、その後で元の関数の残り位置へジャンプするラッパーを作ることで、元関数の呼び出しも可能にする
  • 関数コードページはセキュリティ上書き込み不可なので、VirtualProtect で権限を変更し、修正後に FlushInstructionCache を呼ぶ必要がある

デバッグログの復元

  • バイナリ内にはデバッグログらしき呼び出しがあったが、実際の対象関数は ret しかない空関数だった
  • リリースビルドでは複数の空関数が同じコードにまとめられたようで、そのうちの1つが デバッグロガー だった
  • 当初は、最初の引数を文字列ポインタとして解釈し、表示可能な ASCII 文字かどうかを調べるヒューリスティックを使った
    • 誤ったポインタアクセスは Windows の SEH例外 で捕捉して無視した
    • この方法はある程度動作したが、false positive と false negative が残った
  • その後、IDA のパッチ機能と Python スクリプトを使い、ログ呼び出し箇所を別の空関数へ移した
    • 一部はヒューリスティックで見つけ、一部は文字列定数を push してから呼ぶパターンで見つけた
    • 残る数百の呼び出し箇所は手動で注釈を付けた
  • 復元されたログは WOL マルチプレイのデバッグに役立った
    • SC_MESSAGE_YOUR_DETAILS 処理中の "MyId == INVALID_ID" assert ログを見て、GAMEOPT コマンドを全プレイヤーに誤って送っていたことを Wireshark ダンプで確認した

Direct3D 7 グラフィックパッチ

  • Emperor は Direct3D 7 ベースのゲームで、現代Windowsの Direct3D 7 サポートは完全ではない
  • 高解像度問題は Direct3D 7 ラッパー層の最大テクスチャサイズ 2048 制限と関係しており、UCyborg の LegacyD3DResolutionHack のコードを活用して解決した
  • ゲームは 4:3 比率以外の画面を正しく処理できない
    • レンダリング自体はできるが、UI が過度に拡大されたように崩れる
    • ゲーム内マウス描画のオフセットも、画面中央からの距離に応じてずれる
  • 解決策は 4:3 レターボックス化
    • ゲームをウィンドウモードで実行すると、任意の解像度を使える
    • ウィンドウ枠のスタイルを外し、全画面の黒いウィンドウ上にゲームウィンドウを再親設定する
    • マウスキャプチャを追加し、マルチモニターやウィンドウモードでも端スクロールが崩れないようにした
  • フレームレート制限は IDirect3DDevice7::EndScene をパッチし、60 FPS に合わせた
    • EndScene はフレーム末尾で1回呼ばれるため、待ち時間を計算してスレッドを sleep させるのに適している
    • EndScene ポインタは直接 export されていないため、DirectDrawCreateExIDirect3D7::CreateDevice の呼び出しを順にフックして vtable から関数ポインタを取得した

オンラインマルチプレイと WOL 代替

  • 目標は、ロビーやホスティング基盤なしで、ポートフォワーディングと IP 入力だけで接続する 直接IPマルチプレイ だった
  • LAN モードは動作するが、UDP broadcast でサーバーを探すため、インターネットプレイには向かない
    • LAN メニューには手動IP入力機能がない
    • 当初は LAN チャットをパッチして IP を指定する方法を試したが、協力キャンペーンが WOL 専用だと分かって中止した
  • WOL を復活させるには2つが必要だった
    • ゲームがどこに接続し、どのゲームを開始するかを把握できるようにする偽の WOLマスターサーバー
    • 直接IP接続の上でゲームパケットが動作するようにするプロキシ
  • 既存の WOL 構造には master server とともに “mangler” サーバーがあり、mangler は NAT punching の調整役だったようだ
    • 元の mangler サーバーは消滅しており、ゲームはその応答を待って停止する
    • パッチでは mangler 呼び出しを削除した
  • Emperor は P2P ネットワークモデルを使用し、プレイヤーのペアごとに双方向接続を開いて任意ポートを選ぶ
    • すべてのクライアントが開放ポートを受け付ける必要がある構造で、現代のインターネット環境には合わない
  • 解決策は、winsock 関数を横取りしてすべての接続を 単一のクライアント→サーバー接続 へトンネリングする方法
    • クライアントがサーバーや他クライアントへ送ろうとするメッセージを横取りし、ヘッダーで包んで単一接続へ送信する
    • サーバー側スレッドがメッセージを受け取り、宛先へ振り分ける
    • ゲームは依然として P2P のように動作していると認識するが、実際にはサーバーホストだけがネットワーク設定を扱えばよい
  • この構成で協力キャンペーンゲームの開始と参加が可能になった

簡易WOLサーバーの実装

  • WOL マスターサーバーは IRC サーバーに近い構造だった
  • xwis.net にはファン運営と思われるサーバーがあり、執筆時点ではゲーム本来の DNS エントリ servserv.westwood.com にもアクセス権を持っているようだった
    • Emperor が xwis でそのまま正常動作するわけではなかったが、ロビー作成と参加の参考になった
  • 公開されている WOL サーバー実装である pvpgn-serverの handle_wol.cpp も参考資料として使われた
  • 独自サーバーを作った理由は、外部サーバーが今後も維持される保証がないため
    • 目的は競合コミュニティの運営ではなく、マルチプレイゲーム実行に必要な最小機能の提供だった
  • WOL は標準 IRC とカスタム動作が混在している
    • ゲームロビーは特殊チャンネル
    • ロビー情報は IRC topic を使う
    • ロビーチャットは PRIVMSG ではなく PAGE を使う
    • ゲーム設定同期には ASCII ではない内容を含む GAMEINFO メッセージを使う
  • 基本的な WOL サーバー実装は trial and error で完成しており、正常経路を外れると堅牢ではないが動作する

インストーラーと v1.09 パッチ適用

  • 元のインストーラーは壊れており、ユーザーが CD 内容をハードディスクへコピーし、代替 setup を上書きする回避策が必要だった
  • EmperorLauncher には、元のCDからファイルをコピーし、.cab ファイルを展開するインストール機能が含まれる
    • .cab は zip に似たアーカイブ形式で、Windows には展開用インターフェースがある
  • 最後の公式パッチである v1.09 の適用はさらに厄介だった
    • EM109EN.EXE を 7zip で単純展開して最新バイナリを得る方法は通用しない
    • Windows resource 内で実行ファイルヘッダーが見える大きなリソースを発見した
    • そのリソースの先頭4バイトがファイルサイズで、その後ろが実際のファイルだった
  • EM109EN.EXE は内蔵 DLL を一時ファイルとして展開してロードし、その後 DLL 内部の関数 RTPatch32@12 を実行する
    • RTPatch はバイナリ diff パッチツールだった
    • myRTP ツールを参考に、内蔵 DLL を直接ロードして実行した
  • DLL は引数で渡されたインストールパスではなく registry からパスを読んでいたため、期待する registry key を偽装してパッチを適用した

Westwood Online Shared Internet Components の処理

  • 元のインストールには Emperor 本体とは別に Westwood Online Shared Internet Components がある
  • この構成要素がないと WOL は動作せず、主要ファイルは WOLAPI.DLL
  • WOLAPI.DLL は COM class library で、Emperor は CoCreateClass により DLL 内の COM オブジェクトを生成する
  • 一般的な COM 登録では HKEY_LOCAL_MACHINE\SOFTWARE\Classes\CLSID 配下に CLSID と DLL パスをシステム全体へ登録する
    • この方式には管理者権限が必要
    • ゲームプロセスの範囲を超えてシステム全体に影響する
  • パッチでは registry redirect と OaEnablePerUserTLibRegistration を使い、ユーザー単位の登録方式で処理した
    • 単一プロセスだけに class library を登録する方法は見つからなかった
    • DllGetClassObject を直接使う試みは動作しなかった

ランチャーUIと最終結果

  • 最後の段階は、IP入力と基本設定変更のための簡単なランチャーUIだった
  • UI は raw Win32 controls で書かれている
    • 静的で単純なUIには十分だったが、Win32 UI を直接作る体験はかなり荒削りだった
  • EmperorLauncher は最終的に、現代システムでの実行、高解像度、60 FPS制限、直接IPマルチプレイ、協力キャンペーン、インストールとパッチ適用までを含むツールになった

まだコメントはありません。

まだコメントはありません。