チーム Harekaze で SECCON 2017 x CEDEC CHALLENGE に参加し、優勝しました。
この記事では感想や発表資料の補足などについて書いていきたいと思います。
まず SECCON 2017 x CEDEC CHALLENGE とはなんぞやということですが、これはゲームのクラッキングやチートを行い、その対策案を考えるという大会でした。
8/1 ~ 8/15 にかけて事前に予選が行われました。
この予選では 3 つのゲームが配布され、2 つは与えられた目標 (通常のプレイでは不可能なもの) を達成すること、1 つはセキュリティ上の問題点を探してその手法・影響度・対策案を調べることを目的に、最大 4 人のチームで調査を行い、その結果をプレゼンテーション資料としてまとめるという競技が行われました。
予選を勝ち抜いたチームは 9/1 の CEDEC 2017 のセッションで調査結果について発表するということで、Harekaze は 30 分ほどプレゼンを行いました。
ちなみに、今回予選で配布された問題は現在 SECCON の公式サイトで公開されているので、興味のある方はぜひ挑戦してみてください。
いろいろ理由はあるのですが、私は特に
といったことから参加を決めました。
発表に用いた資料です。本編が約 90 ページ、おまけが約 30 ページです。
大まかな内容は上記の発表資料に書いているので、この記事では発表資料には書かなかったことを補足として書いてみます。
ゲームサーバとの通信は SSL/TLS が利用されていたことから、mitmproxy を用いて復号を行いました。
ただし、通信は SSL/TLS の上に独自の暗号化が施されているため、このままではゲームサーバと何を通信しているか知ることができません。
まずどのような暗号化方式が使われているか調べるため、初回起動時の名前登録の通信をキャプチャしてみました。すると、/2017/uuid
と以下のような通信を行っているのが確認できました。
(1 回目、名前は hirotasora)
Request: data=EFvo1xD5OLWuQbwCBsebTOolsz8f5AMiwdtTbFGNrv8=
Response: W8KR7sKvcgPdj3ysGPi5G6O8yrZZBOJiv0Cev0+wymIEu7+oPBW/G6GIv0AEwz2/fm5J/Ve3xAj6vj6YcdnsEECbjwGylC132mAr4xwFn54B9KxJrdyI1Q7pQ/QlG0lE
(2 回目、名前は hirotasora)
Request: data=EFvo1xD5OLWuQbwCBsebTOolsz8f5AMiwdtTbFGNrv8=
Response: W8KR7sKvcgPdj3ysGPi5G8Vd/MEzzFQW1uKgjVwWscvzyWJ5ucOE9kGN32A/M/Yf2UxgBJIKjJbWYic0Fq6CNARpUAN/A8gZQSQK+plet1TOtG6LlZ8JsEORBH0Apb4W
(3 回目、名前は hirotasoradayooo)
Request: data=EFvo1xD5OLWuQbwCBsebTKv09nPh5sk+jRboZJ017eM=
Response: W8KR7sKvcgPdj3ysGPi5G7ACRoBimDkOdAuKpNE9kTlA2L/ial1WxxjqRMEypt2yWLT9O1mQqnq8YB2gqvYHdqPGei69/F47rqZN5fCF+2E1QLCu1yicq/GNugxh52cZ
(4 回目、名前は hirotasoraaaaaaaaaaaaaaaaa)
Request: data=EFvo1xD5OLWuQbwCBsebTCI2jGWh/mYv16SbU5SwrFxd3wCrzD9cIlYZBlmEpoDx
Response: W8KR7sKvcgPdj3ysGPi5Gw+OvbVZL/dTliGe9A3Gf59Een0ZmkEvT6/yO77RsjKANcCZ6ZiZYzuwouIOemJowdpKYieBCbC4Nrj0HMlBIrEH6MTt+b4k/e29Ha4pf/xI
リクエストもレスポンスも Base64 エンコードが行われているようです。デコードするとリクエストのデータのサイズはそれぞれ 32, 32, 32, 48 になりました。16 バイト単位でサイズが変化していることから、AES-CBC-128
のようなブロック暗号が用いられているのではないかと考えました。
鍵と IV はどのようにして設定しているのでしょうか。/2017/uuid
に POST するより前にはゲームサーバとは通信を行っていないため、ゲームサーバから得ているのではなくクライアント側で保存されていそうです。
与えられた apk ファイルから何か情報が得られないか assets/bin/Data/Managed/Metadata/global-metadata.dat
を strings にかけてみると以下のような文字列が見つかりました。
$ strings -a global-metadata.dat
...uuidTitleMenudef4ul7KeY1Z3456K33pK3y53cr3TYeaIVisNotSecret123game Key confusingCookieplainTextcipherTextcalcHmac...
IV は IVisNotSecret123
でしょう。鍵はその前にある def4ul7KeY1Z3456
K33pK3y53cr3TYea
の 2 つが怪しそうです。
暗号化方式に AES-CBC-128
、IV に IVisNotSecret123
、鍵に def4ul7KeY1Z3456
と K33pK3y53cr3TYea
を xor した文字列を指定して 1 回目のリクエストとレスポンスを復号してみましょう。
from Crypto.Cipher import AES
def xor(a, b):
res = ''
if len(a) < len(b):
a, b = b, a
for k, c in enumerate(a):
res += chr(ord(c) ^ ord(b[k % len(b)]))
return res
KEY = xor('def4ul7KeY1Z3456', 'K33pK3y53cr3TYea')
IV = 'IVisNotSecret123'
def decrypt(msg):
cipher = AES.new(KEY, AES.MODE_CBC, IV=IV)
return cipher.decrypt(msg)
request = 'EFvo1xD5OLWuQbwCBsebTOolsz8f5AMiwdtTbFGNrv8='
response = 'W8KR7sKvcgPdj3ysGPi5G6O8yrZZBOJiv0Cev0+wymIEu7+oPBW/G6GIv0AEwz2/fm5J/Ve3xAj6vj6YcdnsEECbjwGylC132mAr4xwFn54B9KxJrdyI1Q7pQ/QlG0lE'
print repr(decrypt(request.decode('base64')))
print repr(decrypt(response.decode('base64')))
$ python2 decrypt.py
Request: '{"name":"hirotasora"}\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b'
Response: '{"metadata": {"uuid": "1d372414d86e59ea1935518e8868b62b", "iv": "SCCdoLiO6Q5IuHif"}}\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c'
入力した名前を POST し、UUID が返ってきている様子が確認できました。
mitmproxy には便利なことにスクリプティング機能が存在します。
手作業でいちいち復号するのは面倒なので、この機能を使って自動で通信を復号してくれるスクリプトを書きましょう。以下のスクリプトを mitmproxy_decrypt.py
として保存し、mitmdump -s mitmproxy_decrypt.py
を実行すると復号された通信が出力されるようになります。
import hashlib
import json
import sys
from mitmproxy import ctx
from Crypto.Cipher import AES
def xor(a, b):
res = ''
if len(a) < len(b):
a, b = b, a
for k, c in enumerate(a):
res += chr(ord(c) ^ ord(b[k % len(b)]))
return res
def unpad(msg):
return msg[:-ord(msg[-1])]
def decrypt(key, iv, c):
s = AES.new(key, AES.MODE_CBC, IV=iv).decrypt(c)
return json.loads(unpad(s))
KEY_A = 'def4ul7KeY1Z3456'
KEY_B = 'K33pK3y53cr3TYea'
KEY = xor(KEY_A, KEY_B)
IV = 'IVisNotSecret123'
key, iv = KEY, IV
def request(flow):
global key, iv
if flow.request.path in ('/2017/key', '/2017/uuid'):
key, iv = KEY, IV
if flow.request.urlencoded_form:
data = flow.request.urlencoded_form['data'].decode('base64')
data = decrypt(key, iv, data)
ctx.log.info('>%s: %s' % (flow.request.path, data))
def response(flow):
global key, iv
data = flow.response.get_content()
if data:
data = decrypt(key, iv, data.decode('base64'))
if 'metadata' in data:
metadata = data['metadata']
if 'key' in metadata:
key = metadata['key']
if 'iv' in metadata:
iv = metadata['iv']
ctx.log.info('<%s: %s' % (flow.request.path, data))
$ mitmdump -s mitmproxy_decrypt.py
...
>/2017/key: {u'uuid': u'1d372414d86e59ea1935518e8868b62b'}
</2017/key: {u'metadata': {u'uuid': u'1d372414d86e59ea1935518e8868b62b', u'key': u'QyqxE262qG944kpX', u'iv': u'VEgFY2qx9GsIyJ0J'}}
192.168.11.4:50594: POST https://cedec.seccon.jp/2017/key
<< 200 OK 168b
...
ゲームサーバへのリクエスト時には常に X-Signature
ヘッダが付与されています。名前からしてリクエストボディの内容の検証に使っていそうですが、どのようにして計算しているのでしょうか。
/2017/uuid
との通信を観察してみると、以下のように X-Signature
ヘッダの値が集められました。
(1 回目、名前は hirotasora)
X-Signature: 111d7cf2cd5dac5d0f23abd89ae4dc969c2eb4eb621447e81bfd9d9fb0dfc295
(2 回目、名前は hirotasora)
X-Signature: 111d7cf2cd5dac5d0f23abd89ae4dc969c2eb4eb621447e81bfd9d9fb0dfc295
(3 回目、名前は hirotasoraaaaaaaaaaaaaaaaa)
X-Signature: c1d3bf8c5d5c98c545681f36ec75e015d796fd5558cb1a47493504e9ed9e2eec
サイズは 32 バイトで固定されているようです。このことから、HMAC-SHA256
が用いられているのではと考えました。
HMAC の秘密鍵には何が使われているのでしょうか。assets/bin/Data/Managed/Metadata/global-metadata.dat
を見ると calcHmac
という文字列が含まれているのが分かります。
ハッシュ関数に SHA-256
、秘密鍵に calcHmac
を指定して 1 回目のリクエストボディの HMAC を計算してみましょう。
import hashlib
import hmac
HMAC_KEY = 'calcHmac'
def calc_hmac(msg):
return hmac.new(HMAC_KEY, msg, hashlib.sha256).hexdigest()
request = '{"name":"hirotasora"}'
print repr(calc_hmac(request))
$ python2 calc_hmac.py
'c1d3bf8c5d5c98c545681f36ec75e015d796fd5558cb1a47493504e9ed9e2eec'
正規のリクエストに付与されている X-Signature
ヘッダの値と同じ値になりました。
スクリプトを使いながら通信を観察すると、以下のような解析結果が得られました。
xor('def4ul7KeY1Z3456', 'K33pK3y53cr3TYea')
、IV が IVisNotSecret123
X-Signature
というヘッダでリクエストボディを検証
calcHmac
token
という Cookie が存在
token="!ROem1XSLfsXkWB6Y6Gw2zA==?gAJVBXRva2VucQFVIEVNTFBPNkxvbzJ6dG12enVaMzBaT0NKRUdickNCR1NncQKGcQMu"
/2017/uuid
に対して {"name":"(入力した名前)"}
を POST
{"metadata": {"uuid": "(発行されたUUID)", "iv": "(次のIV)"}}
がレスポンスで返ってくる/2017/key
に対して {"uuid":"(UUID)"}
を POST
{"metadata": {"uuid": "(UUID)", "key": "(次の鍵)", "iv": "(次の IV)"}}
がレスポンスで返ってくる/2017/key
に対して 初期状態の鍵とIVで {"uuid":"(UUID)"}
を POST
{"metadata": {"uuid": "(UUID)", "key": "(次の鍵)", "iv": "(次の IV)"}}
がレスポンスで返ってくる/2017/key
に対して 初期状態の鍵とIVで {"uuid":"(UUID)"}
を POST/2017/skill
に対して GET
{"skills": [], "metadata": {"uuid": "(UUID)", "iv": "(次の IV)"}}
がレスポンスで返ってくる/2017/account
に対して GET
{"userData": {"stone": (ダイヤ石の個数), "coin": (コインの枚数), "uuid": "(UUID)", "exp": (経験値), "maxStamina": (スタミナの最大値), "availableMusic": 1, "rank": (プレイヤーのランク), "name": "(ユーザ名)"}, "metadata": {"uuid": "(UUID)", "key": "(次の鍵)", "iv": "(次の IV)"}}
がレスポンスで返ってくる/2017/key
に対して 初期状態の鍵とIVで {"uuid":"(UUID)"}
を POST/2017/skill
に対して GET/2017/account
に対して GET/2017/skill
に対して GET/2017/gacha
に対して {"gacha":1}
を POST
{"skills": [{"param": (スキルの値), "id": (スキルの ID), "skillType": (スキルのタイプ), "name": "(スキルの名前)"}], "metadata": {"uuid": "(UUID)", "iv": "(次のIV)"}}
がレスポンスで返ってくる/2017/gacha
に対して {"gacha":5}
を POST
{"skills": [({"param": (スキルの値), "id": (スキルの ID), "skillType": (スキルのタイプ), "name": "(スキルの名前)"} が 5 つ)], "metadata": {"uuid": "(UUID)", "iv": "(次の IV)"}}
がレスポンスで返ってくる/2017/useItem
に対して {"item":"stone"}
を POST
{"status": "ok", "metadata": {"uuid": "(UUID)", "iv": "(次の IV)"}}
がレスポンスで返ってくる/2017/useItem
に対して {"item":"coin"}
を POST
{"status": "ok", "metadata": {"uuid": "(UUID)", "iv": "(次の IV)"}}
がレスポンスで返ってくる/2017/score
に対して {"myScore":{"musicId":(楽曲の ID),"difficulty":(難易度),"score":(スコア),"name":"","uuid":"(UUID)"}}
を POST
{"gameScores": [{"score": (スコア), "name": "(ユーザ名)"}], "metadata": {"uuid": "(UUID)", "iv": "(次の IV)"}}
がレスポンスで返ってくる私はゲームの解析も資料の作成も発表もあまり経験がなく、競技中は不安でいっぱいでしたが、優勝という結果を残すことができ大変嬉しい思いです。SECCON 2017 国内決勝大会でも頑張ります💪
チームメンバー、運営の皆様、セッションにお越し頂いた皆様ありがとうございました。
st98.github.io / st98 の日記帳