Androidゲームをチートしてみた(SECCON 2015 × CEDEC CHALLENGEにて)

こんにちは、佐藤です。SECCON 2015 × CEDEC CHALLENGEは8月10日まで募集されていたゲームのクラッキングとチートのチャレンジです。

対象アプリは今回はAndroidアプリでした。私がしたチートを書きたいと思います。
[注意]今から書くことを一般のアプリに対して行った場合、犯罪になることがあります。今回は、許可されているアプリであるため検査を行いました。悪用厳禁でお願いします。

対象アプリ1つ目 「sandback」

このアプリは、下のようなサンドバッグをタップするとポイントが加算されていくゲームです。そして、右のようにランキングがでます。使っているゲームエンジンはUnity。

f:id:eyesjapan:20150815192620p:plainf:id:eyesjapan:20150815193057p:plain

このゲームはぼろぼろでした。

まず、通信をのぞき見てみましたら。

f:id:eyesjapan:20150815193916p:plain

httpリクエストは以下の通り。

POST /score/ranking/ HTTP/1.1
X-Unity-Version: 5.0.2f1
Content-Type: application/json; charset=UTF-8
User-Agent: Dalvik/1.6.0 (Linux; U; Android 4.4.4; SO-02G Build/23.0.B.1.59)
Host: api.sandbag2015.net
Connection: Keep-Alive
Accept-Encoding: gzip
Content-Length: 76

{"uuid":"a36cfae0-1eb8-4a55-83ef-e4ac8a2fc809","name":"hooters","point":12345678}

これを送りつけるスクリプトにして、

#!/usr/bin/env python

import urllib2
import sys
argvs = sys.argv
names = '"' + argvs[1] + '"'
points = argvs[2]
target_url = "http://api.sandbag2015.net/score/ranking/"
data = '{"uuid":"a36cfae0-1eb8-4a55-83ef-e4ac8a2fc809","name":' + names + ',"point":' + points + '}'
cl = len(data)
heders = {'X-Unity-Version':'5.0.2f1',
'Content-Type':'application/json; charset=UTF-8',
'User-Agent':'Dalvik/1.6.0 (Linux; U; Android 4.4.4; SO-02G Build/23.0.B.1.59)',
'Host':'api.sandbag2015.net',
'Connection':'Keep-Alive',
'Accept-Encoding':'gzip',
'Content-Length': cl,
}

print data
getpoint = urllib2.Request(target_url,data,heders)
resp = urllib2.urlopen(getpoint)
logs = resp.read()

print logs

res_head = resp.info()
print res_head.getheaders("Date")
print res_head.getheaders("Server")
print res_head.getheaders("X-Powered-By")
print res_head.getheaders("Set-Cookie")
print res_head.getheaders("Expires")
print res_head.getheaders("Cache-Control")
print res_head.getheaders("Pragma")
print res_head.getheaders("Content-Length")
print res_head.getheaders("Connection")
print res_head.getheaders("Content-Type")

f:id:eyesjapan:20150815195951p:plain

もはやゲームしなくてもスコア好きな値にできるし他の人のクッキーも予想できそうにないので次のアプリへ・・・

対象アプリ2つ目「SUNIDRA」

f:id:eyesjapan:20150815200809j:plain

このステージの奥にいるドラゴンを倒したら勝ち。残っているタイムが自分のスコア。
通信見たが暗号化されていてめんどくさいので他のことを考える。
クライアントを改竄する。
Unityアプリ内にゲームをルールなど司るバイナリをリーバースエンジニアリングする。
このバイナリの正体は他のライブラリから直接参照できる中間言語。故に、逆コンパイル、逆アセンブラが簡単。f:id:eyesjapan:20150815204247p:plain
DotNetResolverという逆コンパイラツールをつかってます。上は、ゲームオーバー時の処理です。
とりあえず、nop(何もしない)で埋めてみました。
f:id:eyesjapan:20150815204455p:plain
そして、Apk Editerというアプリを使って再ビルドします。apkを再構築するのには作者を守るため署名が必要でそれなしには偽装アプリをインストールすらできないのですがうまく偽装されています。
f:id:eyesjapan:20150815205112p:plain
ゲームオーバーの処理が行われずスコアがマイナスになり(笑
これでは勝てないので、f:id:eyesjapan:20150815205237p:plain
コンパイラのコードを読むとGameRuleCtrlモジュールのUpdate()に時間をカウントしているところがありました。
Update関数はUnityでワンフレームごとに実行する関数です。
これを+にします。
f:id:eyesjapan:20150815205514p:plain
背景がピンクなのは運営側も知っているバグ出そうです。
sub というオペコードをaddに変更します。
ドラゴンの攻撃力が尋常じゃないので受けたダメージ分自分のHPに加える用に同じ容量で変更しました。
f:id:eyesjapan:20150815210129p:plain
そしてクリア!したと思ったんですが(笑
f:id:eyesjapan:20150815210222p:plain
対策されてた(笑
おそらくチェックサムによるクライアント認識。
こうなったら、プロセスメモリをいじるしかない。(諦め早いかも)
AndroidプロセスメモリエディタのGameGuardianを使って
f:id:eyesjapan:20150815211103p:plain
Androidのasrlを無効(0)にして
f:id:eyesjapan:20150815211236p:plain
メモリを素早く見つけるには少し慣れが必要。アドレス0007DD0に格納される000008DEがスコアに相当。
これを変更すれば反映されると思ったんだのですが。 うまくいかなかった。。。
指摘するところと言えばコピー作成が可能など。
例のぼろぼろアプリはこの方法でも改竄できました。
f:id:eyesjapan:20150815211901p:plain

肝心レポートに手を抜いてしまった。ゲームをチートから守る方法が大きな点数らしいのでまずいです。
とりあえずrootの状態でゲームをさせてしまうと危険です。開発者の皆さんお気をつけください。rootだと判明したら(アプリでrootが必要なコマンドをわざと実行しErrorを得るか得ないかで判定とか)ゲームはさせない設計などもいいと思います。

以上です。