チーム Harekaze で SECCON 2018 x CEDEC CHALLENGE に参加し、優勝しました。この記事では、どういった競技だったかという説明や、15 日間の競技期間中何をしていたかについて紹介したいと思います。
競技の概要
昨年は 15 日間で配布されたゲームの問題点を探して、その手法や対策案をプレゼンテーション資料としてまとめるという競技内容でした。今年は 3 つのフェーズに分割され、オンラインで開催される 5 日間の調査フェーズと 10 日間の対策フェーズ、そしてこれらを突破したチームが参加できるオンサイトの撃墜フェーズで競技が行われました。
調査フェーズは昨年の競技内容とほとんど同じで、各チームに apk ファイルが配布され、これに存在する問題点を探してプレゼンテーション資料に手法のみをまとめるというものでした。
対策フェーズは今年新しく追加されたもので、各チームに調査フェーズで使われたクライアントとサーバのソースコードが配布され、実際にチートやマクロへの対策を現実的な範囲で実装して、実装した対策をプレゼンテーション資料にまとめたものとソースコードを提出するというものでした。このフェーズでは他チームへの攻撃はできず、自チームのクライアントとサーバを堅牢にすることだけに集中できます。
撃墜フェーズは対策フェーズ同様今年新しく追加されたもので、各チームが対策フェーズで変更を加えたソースコードをビルドした apk ファイルが配布され、これをもとに問題点を指摘するというものでした。このフェーズでは自チームのクライアントやサーバに修正を加えられず、とにかく攻撃をすることになります。
発表資料
CEDEC 2018 での発表に用いた資料です。調査フェーズと対策フェーズで提出したものをほとんどそのまま使っています。
調査フェーズ
1 日目
- まとめ: 配布された apk の表層的な解析を行った。
- クライアントとサーバ間の通信は昨年と同じ方法 (詳細は昨年の記事や発表資料を参照下さい) が使われていることを確認した。ただし、暗号化/復号に使うデフォルトの秘密鍵や HMAC の秘密鍵については昨年から変更が加えられていた。
classes.dex
中に含まれている com.totem.keygenerator.KeyGenerator
がデフォルトの鍵を生成しており、ネイティブのライブラリである libY4uSGIkm.so
中の scramble
(Java_com_totem_keygenerator_KeyGenerator_scramble
) という関数を呼んでいることを確認した。
- どうやってデフォルトの秘密鍵を手に入れればいいんだろうと考えながら、チームの wiki に apk 内に含まれるファイルの役割や、使われている API のエンドポイント等をまとめてこの日は終了。
2 日目
- まとめ: 1 日目で得られなかったデフォルトの秘密鍵を取得し、通信が復号できるようになった。
- mitmproxy を使って MITM をテスト、証明書のピン留めが行われていないことを確認できた。
- Perfare/Il2CppDumper を使って
global-metadata.dat
から libil2cpp.so
のメソッドのアドレス等を復元できた。
- デフォルトの秘密鍵の取得について、
KeyGenerator.class
の鍵を生成するメソッドを呼んで表示する Android アプリを作ればいいんじゃねと思いつく。
KeyGenerator.class
と libY4uSGIkm.so
を取り込んで、以下のコードでビルド。アプリを起動すると 45 6e 4a 30 59 43 33 44 33 43 32 30 31 38 21 21
というダイアログが表示され、デフォルトの秘密鍵は EnJ0YC3D3C2018!!
と特定できた。
package com.example.st98.secconxcedec2018test;
import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import com.totem.keygenerator.KeyGenerator;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
KeyGenerator k = new KeyGenerator();
new AlertDialog.Builder(MainActivity.this)
.setTitle("title")
.setMessage(toHex(k.generateKey()))
.setPositiveButton("OK", null)
.show();
}
public String toHex(String s) {
StringBuilder res = new StringBuilder();
for (int i = 0; i < s.length(); i++) {
res.append(Integer.toString(Integer.valueOf(s.charAt(i)), 16));
res.append(" ");
}
return res.toString();
}
}
- 昨年作成した通信関連のスクリプトがほとんどそのまま使えることを確認してこの日は終了。
3 日目
- まとめ: 昨年存在した問題点が今年も存在しているか検証を行った。
- ランキングへの不正なスコアの登録が可能、apk の改変が容易に可能等、昨年 Harekaze が見つけた問題点を検証し、全て今年も存在していることを確認した。
<size=256>LARGE</size>
のようなユーザ名で登録しランキングに入ることで、ランキング画面でそのユーザ以降の他ユーザの表示を妨害できることを発見した。
- この時点で確認できた問題点を提出資料にまとめてこの日は終了。
4 日目
- まとめ: 昨年見つけられなかった問題点を複数見つけた。
- 昨年は詳細な調査を行わなかった
musicgame.db
について調べ、これは自分のハイスコアや (“所持しているスキル” ではなく…) 装備中のスキルを保存しているデータベースであることが分かった。
- これを書き換えることで、自分が所持しているスキルであれば (本来 5 個までしか装備できないが) 6 個以上のスキルが装備できることがわかった。
- ガチャを行う際に同時に複数リクエストを発生させることで、所持しているジュエル (ガチャ時に消費されるもの) で引ける以上の枚数スキルを引けることがわかった (レースコンディション)。
- 昨年実証できなかったメモリの改変について、GameGuardian を使ってコインの枚数やジュエルの個数を増やすことができた。
- 秘密鍵の初期化を行う API に
' and 0 union select 1, 2, 3, 4, 5, 6, 7, '
のような UUID を投げ、アカウントの情報を取得する API を叩くことで SQLi が可能なことがわかった。
' and 0 union select (select group_concat(flag) from flag), 2, 3, 4, 5, 6, 7, '
で FLAG{Well_done!Enj0y_C3D3C!}
というフラグを入手できた
5 日目
調査フェーズの感想
- SQLi やレースコンディション等、昨年見つけられなかったサーバ側の問題点を複数見つけられて嬉しかった。特に SQLi ではフラグが手に入れられて CTF が好きな人間としてテンションが上がった (意味はないけど!)。
- 昨年とほとんど構成の同じゲームが出題されたため、少なくとも調査フェーズでは Harekaze に有利になってしまうのでは…と思ったものの、昨年作成した資料の配布が行われたり、このフェーズはあまり重視されないというアナウンスがあったのでよかった。
対策フェーズ
1 日目
- 参加チームが多かったため選考によって撃墜フェーズでは 6 チームまで絞られるという記述がメールにあり、ビビっていた。
- クライアント側のソースコードのファイル数が結構多かったので、それぞれどのような役割を持っているか確認しているうちにこの日は終了。
2 日目
- スコア登録時、クライアントにスコアに加えてプレイの結果と装備しているスキルを提出させるようにして、サーバ側でスコアや装備しているスキルの数が正しいか、そのユーザのヒットポイントでちゃんとクリアできるようなプレイだったか等の検証を行うようにした。
3 日目
- ガチャを引く回数 (負数でないか、本来引くことができない回数でないか) やユーザ名の検証 (空文字列でないか等) を行うように修正した。
- アカウント情報の取得時にのみ SQL 文にユーザ入力をそのまま展開するようになっていたので、プリペアドステートメントを導入して SQLi を行えないようにした。
4 日目
5 日目
- ガチャ時のレースコンディションの修正を行った。これで持っているジュエルで引ける以上の数のスキルを引くことができなくなった (対策の実装も発表資料の説明もひどくてすみません…すみません…)。
- 楽曲開始時、クライアントにサーバへ通知をさせて楽曲の終了時刻を Redis に保存し、スコア登録時にサーバ側で時刻を検証することで、スピードハック等ができないように修正した。
6 日目
- 完全にクライアント側に任されていたスタミナの管理をサーバ側に移した。これで shared_prefs の書き換えによるスタミナの回復や、スタミナが足りない状況でのスコアの登録ができなくなった。
- あとは
musicgame.db
からの装備スキルのロード時に装備個数をチェックしたり、ジュエルとかコインのタップ時に既にスタミナが最大であれば無駄に消費できないようにしたり、クライアント側に細かい変更を加えた。
7 日目
- ak1t0 さんがクライアント側に証明書のピン留めを実装されたのでバイパスできないか検証。
UnityWebRequest.certificateHandler
が使われており、これによって URL が HTTPS ではなく HTTP であった場合にチェックが行われない (ハンドラが呼ばれない) ことを確認したので、修正を行ってもらった。
- 同じく ak1t0 さんが実装された USB デバッグの検知について、ちゃんと動いていることを確認した。
- それから、ユーザ名のチェック (空文字列でないか、空白文字だけでないか…) とか、ランキング画面のリッチテキストの無効化 (チェックボックスを外すだけ!) とかクライアント側の細かい修正をしてこの日は終わり。
8 日目
- ひたすら寝ていた。
- 流石に提出 2 日前で丸一日何もしないのはひどいなあと思って、
ObfuscatedIntXor
(数値を secret
と value
に分割してメモリ上での検索や改ざんを難しくするクラス) にチェックサムを追加した。気休めではあるけど、これでメモリの改ざんが行われても一応検知ができるようになった。
9 日目
- この時点で特に実装したい対策案として挙がっていたものの未実装だったのは「通信の暗号化をより強固にする」「root 化されているかのチェック」「ファイルの改ざんのチェック」の 3 つ。
- ここまででサーバ側に様々な対策を実装してはいたものの、通信の復号や改ざん等が簡単にできてしまうのはちょっとつらいなあと思ったので最初の対策案を実装してみることに決めた。
- まずデフォルトの鍵とか IV が全ユーザ同じで通信を読むのが簡単すぎではと感じたので、応急措置としてサーバへの接続時にサーバとクライアントでユーザごとにユニークなデフォルトの鍵と IV を生成・共有する方針で実装することにした。
- この日はサーバ側の実装が大体できたところで終わり。
10 日目
- 対策フェーズ最終日ということで、前日に実装し始めた鍵共有の機能をクライアント側でも対応させていた。一応実装はできたがローカルでは動くものの本番サーバではうまく動かないという状況で、原因の特定とバグのチェックがしきれず諦めてしまった。
- ak1t0 さんが実装されたクライアント側でのエミュレータ検知や root 検知を検証、うまく動いていることを確認した。
- 鍵共有の機能の実装中にサーバ側の
crypt_middleware.py
中にある session key 関連のコメントを見て、なにか問題があるのではと推測。実証まではする時間がなかったものの、Cookie の使い回しができたりするのではと思い、古い session は削除するように修正を加えた。
- 最後に、サーバ側に移行したスタミナ関連の処理のバグ (初めて接続したときのスタミナの初期値のミス、スタミナが参照されるタイミングによって正常に時間経過による回復がされないバグ) を修正して対策の実装は終わり。
対策フェーズの感想
- 調査フェーズで解析した内容の答え合わせができて面白かった。サーバ側では Bottle が使われてるんだろうなあとかどうでもいい推測が当たっていたりして嬉しかった。
- Attack & Defense 形式の CTF に苦手意識を持っていて、それっぽいこの競技では大丈夫だろうかと心配していたけれど、10 日間ゆっくり対策だけに集中でき、楽しめてよかった。
- 最終日前日と最終日の 2 日間を費やして実装しようとしていた機能が結局完成しなかったのがつらかった。調査フェーズの時点で実装したいと思っていた機能なんだから、最初にこれを実装していればよかったのではと反省している。チームメンバー各位ごめんなさい。
- Unity にはあまり触ったことがなかったり、クライアント側ではどのような対策をすればいいのかがよく分かっていなかったりしたので、私は主にサーバ側の対策の実装を行っていた。精進したい。
撃墜フェーズ
- 調査フェーズと対策フェーズはオンラインだったが、撃墜フェーズは都内某所でオンサイト。
- 事前に運営側から撃墜フェーズにあたってのヒントがあったように、他チームが実装してきそうな対策を考え、そのバイパス手段を念の為考えてまとめておくようにした。
- 例えば、通信周りの検証が難しくなる証明書のピン留めに対しては、対策フェーズ 7 日目で把握していた仕様等のバイパス手段をまとめていた。
- 最終的に撃墜フェーズに参加したのは 4 チーム (Harekaze 含む)。最大 6 チームが来るということだったので少なめではあるけれど、強そうなチームばかりで戦々恐々としていた。
- 私は通信周りやサーバ側の対策を中心に攻撃するという前日に決めておいた方針に従って、各チームの apk ファイルの解析やサーバへの攻撃を行った。以下、各チームの実装した対策や、競技終了後の撃墜報告タイムで報告したバイパス手段の紹介。
- air0dnocr0tciv
- サーバ側の対策中心。Android 以外のリクエストは処理しない、UUID や token、ガチャ回数などの検証を行うようになっていた。
- 撃墜 1: Android 以外のリクエストは処理しないということだったが、用意していた UUID を生成するスクリプトによるリクエスト (
User-Agent
が python-requests/...
であったり、X-Unity-Version
ヘッダが付与されていなかったり) がそのまま通ってしまった。
- Ninjastars
- root 化検知や各種ステータスの暗号化等、クライアント側の対策中心。共有ライブラリの改ざん検知などの対策も実装されており、クライアント側から攻めるのは正直厳しい印象だったので私は通信周りの対策に対して攻撃を行っていた。
- wabisabi
- shared_prefs でのスタミナの暗号化や証明書のピン留めなどクライアント側の対策中心ではあったが、スコアの検証 (理論値より大きい場合はスコア登録を行わない) や鍵と IV に変更を加えて管理をネイティブの共有ライブラリで行うようにするなど、サーバ側や通信周りでも対策を行っていた。
- 通信のデフォルトの鍵と IV 等の格納場所の変更は突破できず、結局通信は読めずじまい。Golang バイナリなので 4 時間という限られた時間の中では解析が厳しく、この中の関数を呼ぶにしても
libil2cpp.so
側を解析する必要がありつらい感じだった。メモリを調べたりバイパス手段を考えていたのだけれどダメだった。めっちゃ悔しいしリベンジしたい。
- 撃墜 1: shared_prefs にスタミナを暗号化して保存しているということだったが、この暗号化に使う鍵と IV は (たぶん) 変わらないので、楽曲開始前に
adb pull
→ 終了後に adb push
で回復できる。
- 撃墜 2: 証明書のピン留めが実装されているということだったが、恐らく実装にミスがあり mitmproxy が間に入っているような状態でも検知されない。また、(未実証だが) HTTPS であるかどうか恐らく確認がされておらず、
global-metadata.dat
中の URL を HTTP のものに書き換えると勝手サーバであってもゲームが続行できてしまうのでは?
撃墜フェーズの感想
- 他チームの実装した対策をバイパスしたり、実際に機能しているか試したりするのが楽しかった。
- ファイルのハッシュ値の検証等、Harekaze では対策フェーズで実装が間に合わなかったり案として挙がらなかった対策についても、どのように実装しているかどのチームも資料にわかりやすくまとめられていたので、大変勉強になった。クライアント側で対策に対策を重ねてメモリの改変やバイナリの改変にも多大な時間をかけて対策を解除する必要があるようになっていたり、あるいはソースコードの難読化や root 化等の検知によってクライアントの解析自体を困難にしたり、チームによって対策の方針も三者三様で面白く感じた。
- 各チームが行った対策のプレゼンテーション資料の配布や発表は行われたが、競技時間 (というか解析できる時間) は 4 時間と短いものだったので、十分に調査がしきれず個人的には不完全燃焼に終わった感じ。
- 私が実装したサーバ側の対策は (たしか) 破られず嬉しかった。
まとめ
- 攻撃 → 防御 → 攻撃という流れが面白かった。昨年は攻撃と対策案の提案のみの競技だったが、今年は実際にその対策案を実装することになり、言うだけなら簡単だけど実装するのは面倒だなあとか、意外と実効性がなかったり実装でミスりやすい対策もあるなあとか、そもそもどうやって対策すればいいのか分からない問題も多いなあとか、いろいろ感じることがあった。
- 15 日間という競技期間は長いようで短く感じた。より短い競技期間の A&D 形式の CTF でも対応できるよう精進したい。
- なにより、SECCON x CEDEC CHALLENGE を 2 連覇することができ嬉しかった。
st98.github.io / st98 の日記帳