SECCON2013 CTFのスロットマシーン
- まず普通に何回か動かしてみる
- デバッガで処理止めたり、XHRのリクエストをcopy as curl(Chormeの機能)でコピーして叩いてみたり。
- この時点でスロットで何の数値が出ているかはリクエストに含まれていないことが分かった。
- betした段階ではコインが減らず、finishのリクエストを送るとコインが減る。
- じゃあ、スロットで当たった時だけfinishのリクエストを送るように改造してやればいいはずだ。
- 力技で何回もスロット回す必要がありそうだな、と、ここでPerlで自動化コードを書こうとするが、何やら再現するのが難しそうだ
- よく考えたらブラウザ上でやればいいかと思ってjsでチートツールを書き始める。
- slot.jsのpackerを解除
こんなかんじになってるやつ。
eval(function(p,a,c,k,e,r){
...
p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c]);
return p
}(...))
- eval直前のreturn pの箇所にブレークポイントを入れて、pの値を取り出す → 生のslot.jsが手に入る
- js-beautifyを使って整形して読む。 https://github.com/einars/js-beautify
- あとはスロットを回して、当たった時だけfinishをリクエストするようにする。
当たる運命のスロットだとJSONレスポンスのreturnプロパティに0以外の文字列が入る。 最初に出来たのはこんなの。
a = 10;
function cheat() {
hash = keccak32(a + ":" + (new Date).getTime());
$.getJSON("slot.cgi", {
action: "spin",
bet: a,
hash: hash
}, function(a) {
var r = a.result;
var ret = parseInt(a.return);
if (ret > 0) {
$.getJSON("slot.cgi", {
action: "finish",
hash: hash
});
}
})
}
setInterval(cheat, 1000)
コインが増えていくのを確認して、タイマーを早くしたところ。hashがグローバル変数になっていたため、一度破産した。
あれーおかしいなと思いつつもゆっくり回せば大丈夫だったので、それよりも掛金を変更する処理を入れる。 この時点で、keccak32というハッシュの生成アルゴリズムは解析する必要が無くなったので無視した。
var base_bet = 10;
function cheat() {
var a = base_bet;
hash = keccak32(a + ":" + (new Date).getTime());
$.getJSON("slot.cgi", {
action: "spin",
bet: a,
hash: hash
}, function(a) {
var r = a.result;
var ret = parseInt(a.return);
if (ret > 0) {
$.getJSON("slot.cgi", {
action: "finish",
hash: hash
}, function(f) {
var amount = f.amount;
base_bet = parseInt(f.amount / 10);
});
}
})
}
finishのレスポンスのamountプロパティに当たった後のコインの額が入る。 最初は掛け金10からはじめて、finishの後に、持ってる金の1/10を突っ込むようにしてみる。全額でも良かったのだけど一度破産してしまったので念のため。
これでどんどんコインが増えていくのが確認できたので、別タブでコイン残高を表示しつつ見守り。$100,000,000だったっけ?に到達するとコインがリセットされてしまった。おっとやばい、と思って clearInterval(3) などとタイマーをキャンセル。
ネットワークのログを遡って、JSONレスポンスを見てみると、目的の額を上回った時だけ特別なメッセージが返ってきていた。本来普通に実行しているとこれがalertで表示されるのだな、ということで、そのメッセージを取り出して解答。