Magic: The Gathering Arenaをハックして勝率100%を達成
(mayer.cool)- Magic: The Gathering Arenaで、任意のタイミングで相手を投了扱いにでき、マッチメイキングのゲームで負けない状態を作ることができた
- カードゲームは通常、サーバーが全状態を管理するサーバー権威型アーキテクチャに適しているが、MTGAのボット対戦相手であるSparkyはローカルクライアントのロジックとして実装されていた
- C#ベースのMTGAクライアントはリフレクションによりランタイムオブジェクトやprivateフィールドへアクセスでき、デコンパイルしたコードから
JoinMatch、ConnectAndJoinMatch、HeadlessClientといった名前が確認できた - ボットマッチでは同じアカウントの
PersonaIDとJWTで両方の席に接続する構造が使われており、これを通常マッチに適用して相手側の席にヘッドレスクライアントを接続したうえでConcedeGame()を呼び出せた - その後サーバーには、マッチメイキングゲームで両方の席が同じアカウントとJWTを使えないようパッチが適用され、クライアント側ボット実装と席の認証検証の境界が実際のセキュリティに直接影響した事例となった
カードゲームのハックが難しい理由
- カードゲームはターン制で、クライアントとサーバーの間でやり取りされる情報が多くないため、サーバー権威型アーキテクチャに向いている
- サーバーがゲーム全体の状態を管理し、クライアントには必要な情報だけを渡せばよい
- 相手の手札やデッキのような非公開情報はローカルに存在しない
- デッキ順を変えたり、引くカードを操作したりする試みは、サーバーが処理を実行して結果だけを通知するため難しい
- 一人称シューティングとは異なり、カードゲームではプレイヤーの行動が限定され、決まったタイミングで発生する
- FPSでは敵モデルの位置などの情報がクライアントに事前キャッシュされることがあり、wallhackのような形が可能になる
- Riotのanti-wallhackブログでは、クライアント側のプレイヤー位置データを減らすアプローチを扱っている
- 不正な行動の検知も比較的容易
- 手札にないカードを出す行動
- 自分のターンではないのに行動する場合
分析の出発点:ネットワークとC#クライアント
- ゲームハックの出発点として、ネットワーク通信を調べるアプローチを選んだ
- MMOハック事例を扱ったManfredのDEF CON発表は、ネットワークプロトコルのリバースエンジニアリングが複数のバグ分析の核心であることを示している
- MTGAはC#で書かれているため、トラフィック送受信関数をフックする代わりに、ランタイム上でオブジェクトを操作できた
- .NETリフレクションにより、privateフィールドやメソッドにもアクセスできる
- 関連する基礎例として、Unityゲームのリフレクションハックチュートリアルがリンクされている
- 難読化されていない.NETアセンブリはmetadata tokenのおかげで、関数、変数、クラス名を人間が読める形で確認できる
- そのため、デコンパイル結果は実質的にソースコードレビューに近いものになる
JoinMatchで見つかったSparkyの構造
- マッチ参加の初期化処理を探すため関連する関数名を検索していたところ、
JoinMatch関数を発見した JoinMatchは200行を超える長い関数で、下部でConnectAndJoinMatchの呼び出しを確認した- この関数はマッチ設定情報を受け取り、ゲームサーバーに接続する流れに見えた
- 同じコードフローには
MatchType.NPEとMatchType.Familiarの分岐があったNPEは新規プレイヤー体験と推測されるチュートリアル系マッチFamiliarは標準のボットマッチ
- この分岐で呼び出されるロジックは、MTGAのボット対戦相手であるSparkyにつながっていた
- Sparkyはチュートリアルとボット対戦で使われる、MTGAのマスコット的な対戦相手
- ボットマッチでは、ボットロジックがローカルマシンのゲームクライアント内で実行される
ローカルボットとHeadlessClient
- ボットの実際のロジックハンドラーは
HeadlessClientクラス内にあった HeadlessClientはゲームボードをレンダリングしないヘッドレスクライアントで、サーバーに接続してゲームを進行する- ローカルで生成されたボットクライアントは、ゲームクライアントと同じユーザー認証情報を使用する
- ユーザーIDの役割を持つ
PersonaID - ログイン後にゲームへ付与されるJSON Web Token
- ユーザーIDの役割を持つ
- ボットマッチでは、同じクライアントが実質的にマッチの両側へ接続しても、サーバーが問題視しない構造だった
- 席(seat)はゲーム内でどのプレイヤーかを区別する方式として使われる
- ボットマッチでは、同じ認証情報で異なる席を埋める構成が可能だった
通常マッチを乗っ取る方法
- ボットマッチのロジックを通常マッチに適用し、両方の席に接続できるかをテストした
- 必要な情報はランタイム上のゲームオブジェクトから取得した
- 現在のマッチ設定
- マッチサーバーのホストとポート
controllerFabricUrimatchIdPersonaIDJwt- カードデータベース
- マッチマネージャー
- アセット検索システム
- 自分の席を基準に相手の席を計算する
- コードでは
man.LocalPlayerSeatId % 2U + 1Uで別の席を求めている
- コードでは
UnityFamiliar.SpawnFamiliar_DEBUG(...)で相手側の席にボットを接続する- 生成された
UnityFamiliarオブジェクトを探し、cheatbot.Client.Gre.ConcedeGame()を呼び出して即座に投了させる
結果とパッチ
- この方法は通常マッチでも動作した
- 相手がすでに接続していてゲームが進行中の状態でも動作する
- マッチメイキングゲームだったため、人間の相手に勝ったかのように報酬を受け取れた
- 脆弱性の核心は、通常マッチサーバーが同じアカウントとJWTで両方の席に接続する状況を許容していた点にある
- その後サーバーには、マッチメイキングゲームで両方の席が同じアカウントとJWTを使えないようパッチが適用された
- 付録の
InstaWinコードには、UnityのMonoBehaviourでGUIボタンを作成し、ボタンクリック時に現在のマッチ情報を収集してボットを相手側の席に接続し、投了させる流れが含まれている
1件のコメント
Hacker News の意見
Linux に初めて本格的にのめり込むきっかけになったのは、EverQuest 用の ShowEQ でネットワークトラフィックをのぞいたことだった
当時のトラフィックは暗号化されておらず、有用な情報が大量に含まれていた。ハブでトラフィックを Linux マシンに複製し、ゾーンのリアルタイム地図を描画して、モンスター・NPC・ユーザーの位置を表示し、さらにモンスターが持っている戦利品まで見せてくれたので、特定のモンスターだけを狙って狩ることができた。手動方式なので検知不能だったのが利点で、最終的に SOE が気づき、トラフィックの暗号化を始めた
すると相手は鍵ベースの署名を導入し、こちらはまたクライアントから鍵を盗もうとして暗号化を破り、その後チート対策システムが入ってきて、いたちごっこが始まる
ゲーム内で得をするほど価値があったかというと、そうではなかった。真剣にプレイしていなかったので、インストールに2週間かけて1週間ほど使ってみただけだったが、学習経験としては素晴らしかった
例えば hell level の存在、人間ではなくハーフリングが経験値ボーナスを受けること、実際に種族/クラスごとの経験値差があること、初期の Shaman の錬金術が本当に壊れていたこと、などだ。そしてこれが eqemulator.org につながったのではないかと思う
現在の EQ の所有者はあまり気にしていないようなので、使っても比較的問題ない。ただし、どちらのアプリも、見えている装備を除けばモンスターが持つ戦利品を表示したことはない。年月が経つにつれて一部のデータも変わり、以前は正確なモンスターの体力が送信されていたが、今はパーセンテージだけが送られる
「Magic: The Gathering の任意のゲームをプレイできるほぼ完全なボットが、ローカルマシンで動くほど小さい」という部分がよく理解できない
MTG AI が顧客のマシンで動かすには負担になるほど重いなら、サーバーでも動かさない気がする。カードゲームのボット対戦は通常、1局ごとの課金ではないのでコストが大きくなる。サーバーも魔法ではなく、ほとんどはローカルマシンと同じ x86 CPU を使い、デスクトップよりクロックが低い場合も多い。ローカルよりボットのターン時間を短くするには、顧客よりはるかに多くのコアを使う必要があるはずで、プレイヤー1人あたり 8〜16 個の割り当てコアとなると、同時接続のピーク時には悪夢になりそうだ。CPU プレイヤーがマルチコアに対応していないなら、いずれにせよローカル実行のほうが速いはず
ボットのルールエンジンが、古い iPhone や Android 端末のメモリに収まるほど小さいという意味だ。サーバーなら状態機械やルールエンジンをメモリ上に大量に載せておけるし、特定のリクエストを実行するのに処理性能をほとんど使わない場合もある
Daniel、改めてトップ記事入りおめでとう。この記事が出た後で MTGA をハックしてみて、GitHub でも少し話した [0]
興味のある人向けに言うと、今はほとんど機能のない MTGA の非公式クライアントを、休止状態で作業している。目標はランク戦プレイの自動化と、より強いボット相手の提供だ。最近は別のことに押されていて、見やすい良い UI をどう作るかもまだ見えていないので、最低限使えるものになる時期すら予想しにくい。そのほかにも、Daniel や他の人たちのゲームハック話には引き続き興味がある。こういうバグをどう見つけるのか、ハック後にBANを心配しないで済む方法、@aethros が言っていたようなバグ公開のやり方、非公式カードゲームクライアントの構造化といった話をもっと聞きたい
[0] https://github.com/MayerDaniel/mayerdaniel.github.io/issues/...
MTG のような複雑なゲームで AI 相手を作るならオーバーヘッドが大きいだろう、という言い方はかなり大きな過小評価だ
このゲームはほぼチューリング完全で、無限ループを除いても人々はそれでかなり遊んでいるからだ。それでも AI 戦略方面の研究はすでにかなり進んでいそうだ。さらに筆者本人が投稿しているのだから、タイトルに「Show HN:」が付いてもよさそうだ
ボット作者にとって難しいのは後者のように見える
例えば、入場/退場/アンタップ効果でトークン・クリーチャーを作って殺す、といったものだ。こうしたループが常にどちらか一方にダメージを与えるわけでもない
MTG をプレイしたことはないが、この主張は勝つ目的ではなく、意図的に状態を持つ構造を作れば技術的には可能、という意味のように見える
息子と一緒にオールドスクール Magic 93/94を物理カードでプレイするのが本当に楽しい。
毎年Madridへ行き、7pts Singleton世界選手権に参加している。今年の夏は息子が9位になって、とても誇らしかった。7pts Singletonは多様なデッキ構築が可能で、比較的手頃な費用でバランスの取れたゲームプレイを提供する素晴らしいフォーマットだ(https://7pts-singleton.com)
7pts Singletonでは全部を使えるわけではないにしても、1枚だけでも数千〜数万ドルする。それでも息子さんとゲームを楽しんでいるのは良いことだし、上位入賞もおめでとう。
純粋にクライアント側のゲームロジックだなんて。昔、小さなゲームを作ったとき、レスポンスのためにクライアントにゲームロジックを入れなければならないことはあったが、サーバーで同じロジックを再実行してはいけない決まりはないので、そうしていた。
FPSやRTSのようなリアルタイムゲームでは難しいかもしれないが、カードゲームでは言い訳の余地がない。こういうカードゲームなら、実際のプレイヤーが見られる情報以上のものをクライアントに送らないのが正しい。たとえば相手の手札のカード内容は送らず、枚数だけ送るべきだ。サーバーに送る行動も自分自身に関するものだけであるべきなので、相手の投了を宣言できてはいけない。「投了!」と言ったら、サーバーはそれを自分が投了したものとして解釈すべきだ。
脆弱性は、2つ目のクライアントを起動して、現在進行中のゲームに相手側の席として接続できた点にある。いったんそうなると、投了を含めて相手の行動を送ることができた。
筆者が利用したのはクライアント側のゲームロジックではなく、認可とゲーム接続コードの問題だった。「相手の代わりに投了を宣言できてはいけない」というのは、筆者が見つけたエクスプロイトがどう動作したかをそのまま繰り返しているだけだ。
League of Legendsには、特定のチャンピオンとアイテムの組み合わせでゼロ除算バグがあり、サーバーが全プレイヤーを追い出したあとクラッシュすることがあった。
悪用者が最後に追い出されたため、そのチームは勝利扱いになり、相手チームは通常の結果ではなくLoss Preventedを受け取った。
この記事は、取っつきやすく、それでいて洞察のある細部が良い。
ただ、実際の対戦中にボットを接続するケースが理解できない。なぜゲームが対戦途中の参加を許し、ボットが投了したときにそれがなぜ相手の投了として処理されるのだろうか? 3人ゲームを作るのであれば、3番プレイヤーが投了しても2番プレイヤーまで投了したことになってはいけない。
コードは自分のアカウントの席インデックスを見つけたうえで、ボットを別の席インデックスとして参加させる方式だ。問題は、参加するユーザーがその席にいるべき正しいユーザーなのかを検証していない点にある。すでに接続済みの席への再接続を許可すべきではない、という考え方もできるが、モバイルにも対応するゲームなら切断タイムアウトはかなり長めに取りたいはずだ。少し切断されたあと、タイムアウトが以前の接続を死んだと認識する前に再接続したプレイヤーをブロックしたくはないからだ。他のゲームでも、再接続しようとすると10秒ほど「進行中のゲームに参加できません」と表示され、その後ようやく再接続されるのを見たことがある。たとえるなら、地元のカードショップのMTG大会に現れて、誰かを席から押しのけて座り「投了します!」と叫んだら、ジャッジがその椅子に座っている人が宣言したのだからプレイヤーBが棄権したと認めるような状況だ。
そのため、相手の代わりに投了できたように見える。開発者たちは、わざわざそんなことを試す理由がある人がいるとは考えず、チェックを入れなかったのかもしれない。
Diablo 2の時代、同じ接続パケットを再利用してオープンサーバーキャラクター(LAN)を公式bnetインターネットサーバーに接続できた頃を思い出す。
オープンサーバーのデータはすべてローカルに保存されていたため、本来存在してはいけないあらゆるアイテムを作ることができ、公式サーバーもそれを受け入れていた。
最近MTGAで再びMTGに入門した。Unityのゲームで、il2cppではなかったので素早くデコンパイルしてみたが、たとえil2cppだったとしても大した保護にはならなかっただろうし、かなり面白いものを見つけた。
Epic Launcherビルド用のキーや、文書化されていないAPIなどがあった。不正行為に使いたいわけではなく、ただ戦闘記録がほしいだけだ。どのマッチに勝って負けたのか、終了したゲームの戦場の状態を見直す機能などだ。この記事も確認するつもりで、早くパッチされることを願っている。
記録を見たいなら、https://untapped.gg/enを確認してみるといい。そこと少し話したが、実質的に望んでいることをやってくれる。情報の大半はMTGAアプリケーションディレクトリにあるMTGデバッグログから取得しているので、望むなら自分でトラッカーを作ることもできる。サイトでもこの内容を説明している: https://help.hearthsim.net/en/articles/3620440-how-do-i-supp...
そのログを使って追跡するアプリがかなりあったと記憶している。