Forza Horizon 6 / クリーン化プロジェクト 第2報
「せっかく安全なプロキシを作ったのに、ライセンスエラーでゲームが起動しない」──もしあなたが同じ状況に遭遇しているなら、この記事がそのまま答えになるはずです。本記事では、Forza Horizon 6の「クリーン化」を目指したプロジェクトで、自作のAMD AGSプロキシが実際には正常に動作していたにもかかわらず、並行して進めていた別の実験がライセンスエラーを引き起こした経緯を記録します。原因の切り分けに丸一日を費やした末にたどり着いた結論は、意外なほど単純なものでした。
結論を先にお伝えします。ライセンスエラーの原因は、自作プロキシではありませんでした。並行して実施していたxgameruntime.dllのダミー化実験(後述する「アプローチA」)が真犯人です。これを元に戻すだけで正常な動作が確認されています。同じエラーに悩んでいる方は、まず自分のxgameruntime.dllが正規のエミュレータファイル(約800KB)かどうかを確認してください。
自作プロキシは正常に機能していました
まず、原因の切り分け結果から説明します。RUNE版のインラインフック(バイナリの直接書き換え)を排除するために開発した自作のamd_ags_x64.dllプロキシは、実際にはすべての要件を満たして動作していました。エラーが出たとき真っ先にこのプロキシを疑ってしまいましたが、それは誤りでした。
なぜ「成功していた」と断言できるのか。以下の3つの機構が、それぞれ独立して正しく機能していることを確認したからです。
| 機構 | MinHookによるレジストリフック:ゲームがRegQueryValueExWでSteamClientDll64の場所を照会した瞬間だけ反応し、ゲームディレクトリ配下のSteam\steamclient64.rneのパスを返します。ゲームは「本物のSteamクライアントを見つけた」と判断して、自らエミュレータを読み込んでくれます。 |
|---|---|
| 機構 | _ReturnAddressを活用したファイル可視性の制御:CreateFileWをフックし、steam_appid.txtへのアクセスについて、呼び出し元がforzahorizon6.exe(ゲーム本体のDRM側)であれば「ファイルが存在しない」と応答し、steam_api64.dll(エミュレータ側)であれば正常にファイルを返します。これにより、DRMにはSteam環境を意識させず、エミュレータには必要な設定ファイルを正しく供給する、という高度な二面性を実現しています。 |
| 機構 | #pragma comment(linker, "/export:...")による全37関数の転送:AMD AGSのグラフィックスAPI呼び出しはすべて本物のamd_ags_x64_rne.dllへ透過的に転送されるため、描画性能や機能への影響は生じません。 |
「ライセンスエラー」を引き起こした真犯人:アプローチAの失敗
ここからが本題です。プロキシが正しく動作しているにもかかわらず、テスト時に「このゲームの起動にエラーが発生しました。ライセンスがありません」というMicrosoft Store特有のエラーダイアログが発生し、ゲームが起動できなくなりました。
この時点では、直前に導入した自作プロキシが原因だと考えるのが自然でしょう。実際、私たちもそう判断してしまい、一時的にプロキシをロールバックする騒ぎになりました。しかし、真相はまったく別のところにありました。ここに記録するのは、「疑わしいものを順番に外していったら、最後に残ったのが意外なやつだった」という、トラブルシューティングの典型的な教訓です。
アプローチAとは何だったのか
「アプローチA」と聞くと大げさに聞こえるかもしれませんが、やったことは単純です。xgameruntime.dll──ゲームがXbox LiveやMicrosoft Storeのライセンス処理に使うDLL──を、わずか9KBの軽量なダミースタブに差し替える実験でした。すべてのエクスポート関数が「成功しました」(S_OK)を返すだけの、中身のない実装です。
「全部成功を返せば騙せるのでは?」という発想自体は理解できます。しかし、ゲームは起動時にこのDLLを通じてXbox LiveおよびGaming Servicesの複雑なライセンス初期化を行っています。元のRUNE版xgameruntime.dllは800KBを超えるサイズがあり、この通信やメモリ構造の偽装を精巧に実装しています。どのくらい精巧か、数字で示すとこうなります──静的解析ツールGhidraによると、XGameSave(ゲームセーブ)関連だけで93件のコード参照・40関数、XStore(ストア/DLC)関連で34件、licenseToken(ライセンストークン)関連で4件が確認されています。
9KBのダミーが、これほどの規模のAPI群を肩代わりできるはずがありません。結果として何が起きたかというと、ゲームエンジン側でライセンスコンテキスト(「このユーザーはゲームを所有している」という内部状態)が正しく生成されず、Xbox Appなど外部の本来のライセンス検証が走ってしまい、「ライセンスがありません」というエラーを誘発していたのです。
ロールバックと正常動作環境の確立
原因がアプローチAにあると判明した時点で、修正はごく簡単でした。同じ状況の方は、以下の2ステップだけで復旧できます。
- ダミー化していた
xgameruntime.dllを破棄し、バックアップから本来のRUNE版xgameruntime.dll(精巧なGDKエミュレータ)へロールバックしました。 - ゲームディレクトリの
amd_ags_x64.dllは、自作のクリーンなMinHookプロキシをそのまま維持しました。
これにより、ゲームはクラッシュ(PreCrashReport出力)を起こさず、ライセンスエラーのダイアログも出現せず、正常に起動することが確認されました。エミュレータの初期化ログ(Setting breakpad minidump AppID = 2483190等)も正常に出力され、プロセスは安定してRUNNING状態を維持しています。
この検証を通じて得た最大の教訓は、「安全にできる部分」と「触ってはいけない精巧な実装」の境界を見極めることの重要性です。AMD AGSプロキシは安全に自作できました。しかしxgameruntimeは、800KB超のコードが緻密に連携する精巧な実装であり、9KBのスタブでは代替不可能でした。では、なぜGoldbergのようなOSSでも代替できないのか?──その答えは、次の記事でGhidra静的解析の定量データとともに示します。
結論。「自作のクリーンなAMD AGSプロキシ」+「RUNE版の精巧なxgameruntimeエミュレータ」のハイブリッド構成が、安全性と確実性を両立する最適解でした。ライセンスエラーの原因は自作プロキシではなく、ダミースタブによるGDK初期化の破綻にありました。もし同じエラーに遭遇した方がいたら、まずxgameruntime.dllのファイルサイズを確認してください。数KB〜数十KBしかなければ、それがダミーです。
旧DLL検証メモの統合
別記事になっていたDLL互換性検証メモは、Forza検証の一部として本記事に統合扱いにしました。重複していた実装コード断片は再掲せず、AGSプロキシ、ライセンスエラー、旧DLL検証が同じ切り分け記録であることを本文上でも明示します。