メモ代わり

Feed Rss

ブラウザゲーム時代のチートとセキュリティ

12.22.2011, know how, by .

主にオンラインゲームの話です。

最近ではトリプルエークラスのオンラインゲームも減り、主戦場はブラウザに移ってきたようです。
あらゆるロジックをサーバ内のコントローラに配置し、クライアントはビューワに徹すれば不正行為の殆どは防げますが、ゲームデザイナーやプログラマがあらかじめそれを設計するのは非常に難しいと考えられます。

難しいことを考えずに作られた(と思われる)アプリを対象に、過去の手口や、ありきたりな手口がどのぐらい通用し、どのように対策すべきかを考えます。

注:ググってダウンロード、もしくは購入できるツールは絶対に使わないでください。それ間違いなくウィルスですから!

スピードハック系

スピードハックは非常にシンプルです。ブラウザが参照するタイマー API をフックし、実時間より多くの時間を返すだけでできてしまいます。汎用ツールも多くあるため、一番最初に対策を考えるべきです。


このゲームで、赤丸で括った部分は敵を攻撃すると出るアイテム、青で括った部分はエネルギーが回復するまでの残り時間。描画に関係する処理は「高精度系タイマー」を使い、ゲーム進行は「カレンダー系タイマー」を使っています。高精度タイマーのほうだけ数倍で進めることで、数秒に一度しか攻撃できないところを、連続で攻撃し、沢山のアイテムを短時間で出し、実時間で回収することができてしまいます。
またエネルギーの回復も高精度系タイマーを使っているため、簡単に多く回復できてしまい、画面遷移するまで動作します。その後結果がサーバへコミットされ不正と判断されると切断されます。良い結果が出ない場合は不正行為をコミットしてしまい弾かれるのを期待し、良い結果がでたときだけ実時間で辻褄が合うように粘るという行為ができるようです。

というか、この戦争ゲームの進行トロすぎると思いませんか?悪気がなくても二倍速ぐらいがちょうどいいです。

クライアントアプリから定期的に実時間を送信する

クライアントから定期的に時間を送る方法は若干手間かかりますが、後から仕組みを追加することが比較的容易です。 Ping のようなパケットに載せるのも有効です。
サーバで受信したデータと、サーバ内での実時間を比較するだけで対応できます。

複数のタイマー取得 API を使い比較する

複数のタイマーで比較することも有効です。一般的にどのプラットフォームでも「高精度タイマー」と「カレンダー系タイマー」の二つが最低用意されています。たとえばアニメーション動作やフレームレートの調整などは前者、秒単位で十分な日付などは後者の API がよく使われます。この二つの API の片方だけ早めている場合などに二つの API で取得した時間を比較することで対応できます。

クライアントのタイマーをゲームロジックに依存させない

サーバ内の時計を操作することは非常に困難なので、クライアントのタイマーにゲームロジックを依存させないことはもっとも有効です。もしこの仕組みが十分に活用できていれば上記のクライアントでの対策は不要です。

しかし…

このゲームでは、青で括った部分のタイマー(戦闘終了までの時間)はサーバから定期的に送られてきますが、ゲームの進行そのものはプラットフォームに備わった API を利用しています。この時計もプラットフォームの API を使っておけば RTS として難易度が上がるだけで攻撃者の利益にはならなかったことでしょう…

おそらく戦闘時間改ざん対策のための仕組みが、スピードハックという原始的な手法を許し、裏目に出ていることがうかがえます。戦闘時間(パケット)改ざんはプロキシなどで対応できてしまうため判断を鈍らせたのでしょうか。

送信パケット改ざん系

送信パケット改ざんについてはもっとも気をつける必要があります。 JavaScript な HTTP の場合はコンソールを開くだけで簡単にリクエストが送信できてしまいます。クロスサイトスクリプティングの対応を期待するのもユーザを守るための仕組みであり、ユーザからの攻撃には無力です。

アイテム個数や価格を範囲でしっかり判定する

if( request.value < inventory.value )...;

残念!マイナスの値も通ってしまいます。値は必ず符号付を選び、範囲でチェックしましょう。ユーザ間のトレードなどで特に気をつけます。

プレイヤーの手腕を頼った調整は慎重に考える

オンラインゲームではスキルのレベルによる威力の調整が出来るゲームが多くあります。プレイヤーの手腕によりスキルを調整することでプレイヤースキルに差が出ることを目指したゲームデザインですが、これが自動計算できてしまう場合は格好の標的になります。

// パーティーメンバーの残り HP に応じてスキルレベルを調整する例
for(int i=0;i<st.ptnum;i++)
{
	if(st.ptm[i].id==id)
	{
		int shp=st.ptm[i].mhp-st.ptm[i].hp;
		shp-=intes/8*4;
		short lv=(shp/(intes/8))/8+1;
		if(lv>10)lv=10;
		if(lv<1)lv=1;
		level=lv;
	}
}

ターゲットが有効であるか必ずサーバで確認する

何かアクションを行うときに、ターゲットの ID を書き換えてしまうことでもっとも手軽にそれに関する判定をすり抜けることができます。クライアントアプリでは隣の敵を攻撃したつもりが、対象を書き換えられて、サーバでは遠くの敵が攻撃できてしまった!と。

当たり判定や使用可否に関する判定は必ずサーバで行い、余裕があればクライアントにも実装するようにしましょう。

有効な対策?

上記テクニックはハッシュを加えるだけで多くの攻撃者の心を折ることができます。しかし、ハッシュを計算するアルゴリズムをサーバとクライアントに実装し、その関数の位置もバレないようにしなければなりません。
公開鍵暗号でも、暗号鍵と復号鍵が別々のため、別プロセスでの改ざんをある程度防げます。どんなに頑張ろうが、ハッシュでも暗号でも抜けられてしまう可能性はあるのでお手軽で効果的な対策を行いましょう。

そもそも Web 系エンジニアであれば送信パケット改ざんは折込済みでしょうから普段の「常識」を素直に守るようにしましょう。

受信パケット改ざん系

受信パケットに関する改ざんは見落とされがちですが、手軽にできることや、サーバ側にはバレにくいこと、面白い結果が出てしまうことから、攻撃者はよく標的にします。

戦場の状態変更

たとえば強力な防御用のタワーを、最弱のタワーに書き換えてしまうことや、自分の保持ユニットの数を多くしてしまうなど。手抜きゲーム相手では枚挙にいとまがありません!

エフェクトの削除、変更

たとえばステルス状態で、カメレオン風の見た目になるところを、そのパケットを改ざんしてしまい、なにごともなく可視化することや、逆に煙幕などの視覚を妨害するエフェクトを削ってしまうような行為。

見た目や音などがゲームロジックに大きく影響するようなデザインは避けましょう。こまめにサーバと通信し、状態に不整合がないか問い合わせるのも手です。(時間調整は慎重に!)

自動処理系

いわゆる BOT といったプログラムはクライアントプログラムの殆どをエミュレーションする必要があり、開発も比較的困難です。ブラウザゲームなどではプロトコルが HTTP などの細めで、コネクションレスなものもあります。よって単発的なプロトコルは簡単にエミュレーションできてしまいます。

  • 定期的に指定したリクエストを出す。例えば5分置きに収穫するなど
  • ライフが減ったら回復アイテムを利用する
  • タイミングよくボタンを押させる

不正に作られたリクエストに対応するには、プロトコル内にリクエスト番号などを乗せることで対応できます。連番であることが知られてしまった場合は擬似乱数を使い、値を読まれにくいようにするのも非常に良い手になります。

そもそも自動化プログラムに、プログラムで対応するのは非常に難しい行為なので、自動化されても困らないゲームデザインが重要になります。

メモリ改ざん系

メモリ改ざんは受信パケット改ざんと似たようなことができますが、より簡単です。

射程距離、当たり判定

その射程距離、クライアントアプリで距離の計算や障害物判定をして、範囲内のときにターゲットの ID を送っていませんか?判定部分のプログラムの改変は比較的容易にできてしまいます。

ユニット構成、地形など

メモリ改変よりは受信パケット改ざんのほうがお手軽なため、直接書き換えられることは少ないようです。 Flash の場合直接メモリに状態が配置されないことがあったり、メモリマップが読みにくかったりなど予め改ざんしにくい構成になっているようです。

暗号化について

SSL(HTTPS) を使うことで第三者によるデータの覗き見や改ざんは実質不可能になり、よい対策となりえます。しかし、対応できるのはあくまでも第三者からであり、利用者本人が攻撃者となるときはあまり効果がありません。

  • hosts を書き換え、自前の HTTPS サーバへリダイレクトし、 rewrite などして本物サーバへ中継する
  • ブラウザの SSL 送信関数をフックする

特に前者は簡単です。リバースプロキシを用意した仮想マシンへ飛ばすだけでできてしまいます。ブラウザの関数をフックするのは容易ではありませんが、スピードハックよりは簡単にできてしまいます。


他人にやられたらシャレにならないけど、自分でやるのは OK?

SSL は第三者からユーザを守るためであって、サーバを守るためにはあまり使い物にならないと思いましょう。

独自暗号

独自の暗号化アルゴリズムは非常に有効です。暗号鍵と適当に xor するだけでも攻撃者の心を折ることができます。しかし、一般的なプログラマであれば暗号と復号処理を関数にしてしまうため、その関数をコールされてしまった場合は突破されてしまいます。特にゲームクライアントの実装が JavaScript の場合、全く意味が無いと考えるべきです。

//暗号化関数の場所がバレてしまったネイティブコードの例
int csend(void *p,int size)
{
	DWORD f=*(WORD*)p;  // 暗号化したいデータ
	int s=size; // 暗号化するデータ量
	__asm
	{
		push p
		push f
		mov ecx,this  // this ポインタ代入
		call encadr // 暗号化関数呼び出し
		push eax
		mov ecx,this
		call sendadr // 送信関数呼び出し 
	}
}

データマイニング系

多くの場合、プレイヤーアカウントとその識別子は密接に関連付けられています。サーバに対して「 プレイヤー ID=xxxx を攻撃する」とリクエストしたときに、エラーによって「射程外」なのか、「存在していない」のかを返してしまうと、対象のプレイヤーがログインしているかどうかなど些細な状態が分かってしまいます。網羅的に集める事によって、統計を取り、優位に立つことが出来てしまいます。

他にも

  • 露天などの、プレイヤー間のマーケットで最安値を検索する。陳列の減り具合を集め売れる価格を調べる
  • どのぐらいの攻撃で倒せたか?を集め敵の強さを知る
  • 所属するグループの ID やアカウントの ID から所持する別キャラ、交友関係を探る
  • ttp://mari.kukulu.erinn.biz/ とかまだ残ってるんだ...
  • ttp://picopico2.dip.jp/app/charque/query.html なんてのもまだ残ってる。

一人では難しくても多数で調査されると恐ろしいですね。

まとめ

強固な対策をしても抜けられてしまうが、無防備はダメ。

実装すべき判断基準を考えるに、

  1. その行為は他のプレイヤーに直接影響を及ぼすか?
  2. その行為の対策はサーバロジックで可能か?
  3. その行為でプレイヤーは優位に立てるか?
  4. その行為は自動化できてしまうか?

上から順に当てはまったら対応すべきかもしれません。自動化は仕方ないですね、ゲームデザインを曲げる必要があるかどうかだと思ってます。

そもそもブラウザゲームはそこまで気合いれてやりこまないよね。