9log

アウトプットの練習

ゴ魔乙のフェスでプルメリアをめっちゃ走らせた話

ゴ魔フェス Goma×Mushihimesama 萌える全力タウン

ケイブ社のソシャゲ「ゴシックは魔法乙女」という奴のキャンペーンで、キャンペーンサイトのクリック数を一定期間で積んでいって規定量に達するとユーザーに還元します的な事をやってたのでハックしてみた話。

この手の企画ってソシャゲとかでたまにあるんだけど、オレもWeb業界で働いている以上こういう攻撃も可能だしどう防御していけばいいか考える良いきっかけになった。
実際この手のハックに興味を持ったのはこないだのYAPC::ASIA2015の銅鑼パーソンの件で、あの時オレはそこまでガチで叩いて無かったとは言え運用側だったらどうやったら負荷とかに耐えられるかモヤモヤは考えてた。

キャンペーンの概要

ケイブ虫姫さまiOS版が出るのでハロウィンコラボで、お菓子を持って逃げたカブタンを追いかけるプルメリア(ゴ魔乙のキャラ)をクリックして走らせろ!っていう設定。
クリックする度におっぱいがプルプルする。
一定間隔以上でクリックするとプルメリアの心拍数が上がって一定時間走れなくなる。

とりあえずブラウザからJavaScriptで叩いてみる

Element見てみると「id="plumTouch"」の要素のmouseDownイベントを拾っておっぱいがプルプルしているようだ。

f:id:kyuu1999:20151102111534p:plain

幸いページはjQueryバリバリ使ってるので#plumTouchのjQueryオブジェクトにmouseDownイベントを送ってやればクリック1回扱い。

ブラウザのJavascriptコンソールを開いて以下を打ち込めば

setInterval(function(){$('#plumTouch').trigger('mousedown')},10);

一瞬で数百回叩いてるのと同じになる。 f:id:kyuu1999:20151102111535p:plain

ブラウザからイベント送信する手法はサーバーサイドからは手で叩いてるのと見分けがつかないので制限するのは非常に難しいと思う。あまりに効率良く長時間叩いてくる奴は弾くぐらいか。
ただ、攻撃側もサイトの作りに依存するのである程度は企画に沿った範囲でしかリクエストは送信できない。 今回の場合は一定回数叩くとプルメリアが疲れてクリックを集計しない時間(5,6秒ぐらい)があるのでどうがんばっても1タブで精々33クリック/7秒毎が限界だった。
複数タブでJavascriptを高速で回すので実行するPCの負荷も結構高い。

ちなみにYAPC::ASIA2015で銅鑼を叩いてたのはオレの場合だとこの手法だった。

パッと見の仕様

ブラウザからJavascriptで叩くのは遅い&重いのでもっとガンガン叩ける手法を考える為にどういうリクエストを送っているのかざっと調べてみる。 動かした感じは以下

  • 最終クリックから5,6秒経つか、累計のクリック回数が50回になるとサーバーにクリック回数をPOST
    • http://promo.ghm.cave.co.jp/gomafes4/count がリクエスト先
    • その時、ページに含まれたワンタイムトークンがリクエストパラメータに含まれていないといけない
    • 最速でクリック連打すると33クリック目でプルメリアの息が上がるので普通に連打するとここで送信される
    • 息が上がらないようにゆっくり叩いても50カウント目で送信される f:id:kyuu1999:20151102111537p:plain トークンこれ
  • HTTPヘッダには特殊な情報は含まれてないような感じ
  • ポストが完了すると新しいワンタイムトークンと現在の総クリック数がjsonで返ってくる
    • これを用いてページに埋め込まれたトークンと走行距離を最新に更新する

ってことはページに埋め込まれたワンタイムトークンさえ取得できればカウント用のPOST先に直接データを送信できそうだな。 クリック毎にリクエストを生成していないので連打にも耐えられるし、ページ自体のギミックであんまり連打出来ないようになっているのでその分通信部分は簡素に出来ているようだ。 こういうほうが通常の運用は耐えられそうだし良くできてると思う。

スクリプトにしてみる@Perl

gist.github.com

  1. ページを取得してTreeBuilderでパースしてワンタイムトークンを抜く
  2. トークンを使ってクリック数を送信(50回)
  3. 帰ってきたトークンを使ってまたクリック数を送信(50回)
  4. 2−3を0.2秒間隔で繰り返す
  5. 2がタイムアウトすると戻りが無くてjsonエンコードでコケるからとりあえずevalで括る

特に問題なく50回づつ飛ばせた。 1秒でおおよそ200〜250ぐらいは飛んでる 7秒で33回から大躍進だ

沢山回す

しばらく遊んで2000万クリック程度だったので寝る前に4つ回して寝て起きたら条件クリア(4900万クリック)していた f:id:kyuu1999:20151102111536p:plain

ケイブさん申し訳ありませんでした。ゴ魔乙大好きです。

こういうことへの対策

さて
もし自分が企画側だったとして一定期間でユーザーに遊んで貰って、最後のほうで条件クリアなるか!?って盛り上がるのが一番良いのでこういったスクリプト荒らしはあんまり嬉しくないと思う。(とはいえ閑古鳥で条件達成もクソも無いと逆に萎えもするが) となるとどうやってこういうスクリプト荒らしを防御するかって話になる

今回の場合は同一のクライアント(IP&ユーザーエージェント)から5秒以下の間隔で来るリクエストは弾いたほうが良いんじゃないかなと思うが、直近のクライアントをキーにしてKVSに保持しておいて5秒でexpireし、キーの存在チェックを毎回する感じになるのでめちゃくちゃ流行った時にここがボトルネックになるかもしれない。
リバースプロキシで特定のURLに対して前回のリクエストから一定時間経ってないクライアントは弾くみたいなモジュールってあるのかなぁDDOS防御用みたいな感じで。

対策に掛けるコスト考えると全く無視するのも1つの手なんだろうなぁ。
また他にこういうユーザー参加型キャンペーンがあったら解析してみたい。