代表の小竹(aka tkmru)です。
2026年3月19日に開催された勉強会「TECH BATON in 東京 〜ゲームアンチチートに学ぶ セキュリティ設計と攻防の舞台裏 〜 - connpass」にて、「10分で知る ゲームが『チートされる』仕組み 〜通信・API・メモリから見る攻撃と防御〜」というタイトルで発表しました。スライドは Speaker Deck で公開しています。
本記事では、登壇内容のうち「ゲームサーバにおける API の脆弱性」のパートから、特にゲームアプリでよく見かける TOCTOU(Time of Check to Time of Use)という脆弱性に絞って解説します。
TOCTOU は厳密にはゲーム特有の脆弱性ではありません。一般的な Web アプリケーションや OS レベルの処理でも発生しうる古典的な競合状態の問題です。しかし、ゲームには「1日1回限定ガチャ」「イベント期間中 N 回まで受け取れる報酬」「スタミナ消費」など、回数制限を前提とした機能が多いため、TOCTOU が起きやすい箇所も多く、実際の脆弱性診断でもよく見つかります。
想定シナリオ ― 1日1回限定のガチャ API
次のような仕様の API を例に考えます。
- エンドポイント: 1日1回だけ引ける限定ガチャ
- サーバは「ユーザごとの残り回数」を DB に保持
- ガチャを引くたびに残回数を1減らす
ごく素朴にサーバ側の処理を書くと、以下のような流れになります。
- DB から残回数を読む
- 残回数 > 0 ならガチャを実行し、報酬を付与する
- 残回数を1減らして DB に書き戻す
一見、問題なさそうに見えます。しかし、この実装は 同時に複数のリクエストを送られると破綻します。
攻撃方法 ― 同時並列リクエスト
攻撃者は単純に、同じ API に対して複数のリクエストをほぼ同時に並行送信します。curl を並列実行する、Burp Suite の Turbo Intruder などのツールを使う、といった方法で簡単に再現できます。
ゲームの脆弱性診断の現場で特に便利なのが、DeNA が OSS として公開している PacketProxy の send x 20ボタンです。PacketProxy は HTTP/HTTPS だけでなく TCP/UDP 上の任意のプロトコル(ゲーム独自の暗号化通信プロトコルを含む)に対応したローカルプロキシツールで、捕捉したリクエストをsend x 20 ボタンひとつで 同一リクエストを20個まとめて同時に送信できます。まさに TOCTOU のような競合状態の検証のために用意された機能で、README でも「同期・ロック不備に起因する race condition や不整合状態のテスト」が用途として挙げられています。
診断時のワークフローは概ね以下のようになります。
- 対象 API(例: ガチャ、交換所、クーポン利用)のリクエストを PacketProxy で捕捉する
- ペイロードを必要に応じて書き換える(ユーザー入力値などの調整)
send x 20ボタンを押して一斉送信する- サーバのレスポンスおよび DB の状態(所持数・残回数など)を確認する
単一リクエストでは正常に弾かれる回数制限が、x20 送信では抜けてしまうことが確認できたら TOCTOU が起きていると判断できます。PacketProxyでは、ゲーム独自のバイナリプロトコルでも Encode Module を実装すれば同様に扱えるため、「HTTPS の内側をさらに独自暗号化しているモバイルゲーム」のような題材でも応用が効きます。
なぜ同時に送ると危ないのか
ひとことで言うと、2つのリクエストが、どちらも「まだ1回残っている」と勘違いしたまま処理を進めてしまう からです。
片方が「使った」と DB に書き戻す前に、もう片方が DB を読みにいくと、古い「残り1回」という値がそのまま見えてしまいます。どちらのリクエストも自分が正当だと信じてガチャを実行するので、結果として1回の権利で2回引けてしまいます。
図にすると次のようになります。
時刻 リクエストA リクエストB │ │ 残回数を読む(1) │ 残回数を読む(1) ← Aの更新前に読んでしまう │ ガチャ実行 │ 残回数を0に更新 │ ガチャ実行 ▼ 残回数を0に更新
リクエスト A が残回数を 0 に書き戻す前に、リクエスト B も「残り1回」と読んでしまうため、本来1回しか引けないはずのガチャが2回(並列数を増やせば N 回)引けてしまいます。
この問題の名前 ― TOCTOU
この種の問題にはTOCTOU(Time of Check to Time of Use)という名前が付いています。発音は「トックトゥー」のようです。
「値を確認(Check)してから、その値を使う(Use)までの間に状態が変わってしまう」ことに起因する不具合の総称です。OS のファイルシステムに対する攻撃(シンボリックリンク競合など)で有名ですが、Web API における「残数チェック → 更新」の処理でも全く同じ構造で発生します。
ゲーム以外でも、
- EC サイトの在庫管理(購入個数に制限があるもの、在庫1の商品など)
- ポイント消費・残高からの引き落とし
- クーポン・シリアルコードの利用(使用済みフラグを立てる前に複数回使われる)
など、「残数を見てから減らす」処理全般で起こり得ます。冒頭でも触れたとおり、ゲーム固有の問題ではないものの、回数制限のある機能が多いゲームでは特に頻出するのが実情です。ガチャ、デイリーミッション報酬受け取り、イベントの交換所、スタミナ回復アイテムの使用など、開発者が「このエンドポイントは回数制限で守られているから大丈夫」と思い込んでいる箇所ほど穴になりがちです。
対策 ― 悲観的ロックで DB 側に整合性を寄せる
アプリケーションコードで頑張って排他制御しようとすると、ロジックが複雑になるうえに抜け漏れが発生します。基本方針は 「DB 側で整合性を保証する」 ことです。
最もシンプルな対策は、RDB の 悲観的ロック(SELECT ... FOR UPDATE)を使う方法です。悲観的ロックとは、他の処理が同じ行を同時に触れないよう、一時的に順番待ちにする仕組みだと思ってください。
BEGIN; SELECT remaining FROM gacha_limit WHERE user_id = ? FOR UPDATE; -- 他のリクエストはここで待たされる -- 残回数チェック & ガチャ実行 & 残回数の更新を一連の処理として実施 COMMIT;
BEGIN 〜 COMMIT で囲まれた範囲は トランザクション(途中で失敗したら全部なかったことにできる、ひとまとまりの処理の単位)と呼ばれます。FOR UPDATE を付けて該当行にロックを取ることで、同じユーザに対する他のリクエストはこのトランザクションが完了するまで待たされます。結果、「確認 → 使用」の間に別リクエストが割り込むことがなくなり、TOCTOU が成立しなくなります。
より軽量な代替として、条件付き UPDATE を先に実行し、影響行数で成否を判定する方式もあります。
UPDATE gacha_limit SET remaining = remaining - 1 WHERE user_id = ? AND remaining > 0; -- affected_rows が 1 ならガチャ実行、0 なら残回数切れとして弾く
UPDATE 文自体は、DB が アトミックに(= 途中で他の処理が割り込めない、一まとまりの処理として)実行してくれます。そのため、同時に複数リクエストが飛んできても残回数を超えて成功することはありません。明示的なトランザクション管理が不要なぶんシンプルで、リトライや長いロック待ちも避けやすいため、ガチャのように短時間で高競合になるユースケースではこちらを採用するケースも多くあります。
ただしひとつ注意点があります。上の UPDATE 文だけでアトミックなのは「残回数を1減らす」処理そのものに限られます。実際のガチャ API では「残回数を1減らす」のあとに「報酬アイテムをユーザーに付与する」という処理が続くはずで、両者を合わせて初めて「1回ガチャを引いた」という一貫した状態になります。したがって、残回数の消費と報酬付与は同一トランザクションで行う、あるいは別システム・別テーブルを跨ぐ場合は同等の整合性設計(冪等なジョブキュー、補償トランザクションなど)で扱う ことが必要です。カウンタだけ先に減って報酬付与に失敗すると、ユーザーから見れば「引けていないのに回数だけ消えた」ということになり、サポート対応案件に直結します。
いずれの方式でも大事なのは、「アプリ側で読んでから書く」のではなく、「DB が持つアトミックな操作に任せる」ことです。「読み取り → アプリ側で判定 → 書き戻し」という流れを残している限り、TOCTOU の余地は消えません。
おわりに
TOCTOU はセキュリティ脆弱性として古くから知られた題材ですが、ゲーム API の文脈では今も実際に見つかりやすい脆弱性です。特に、イベントやキャンペーン期間中に追加される「○回まで」系の機能は、短い開発期間で実装されることが多く、排他制御が甘いまま本番投入されるケースをよく見かけます。
診断・レビューの際には、「残数を扱うエンドポイントで、同時並列で実行したら何が起きるか」をチェックリストに入れておくことをおすすめします。
登壇スライドでは本記事で扱った TOCTOU 以外にも、
- HTTPS ペイロードのさらなる暗号化と鍵の保護
- マイナス値を送ることで所持数が増えるバリデーション不備
- ガチャ ID とアイテム ID の整合性欠如による課金アイテムの不正取得
/proc/<pid>/memを用いた Android のメモリ改ざんと対策
など、ゲーム特有のセキュリティトピックを扱っています。興味があれば公開スライドもあわせてご覧ください。