本記事は23卒のエンジニア職志望向けAdvent Calendar Advent Calendar 2021 の10日目の枠が空いていたので、登録された記事です。
11日目の記事は、@yamamoto-yuta さんの SlackをTweetDeckぽく使えるChrome拡張機能を作った話 です。中々イケてるので、ぜひ読んでみてください!
はじめに
今年も、誕生日なので初心者から中級者向けに、taskCTFを開催させていただきました。
開催はこれで3回目です(過去の開催レポート)
正の得点を獲得した参加者は36人で、そのうち全完した参加者は8人(22.2%)でした。
ご参加いただいた方々、ありがとうございました
おかげさまで、今年も楽しい誕生日になりました。
taskCTF21で提供した問題や設定ファイル、writeupはGitHubで公開しています。もし良ければ参考までにどうぞ。
本記事は、前半がCTF開催の振り返り、後半が出題したPolygolfの解説という構成になっています。興味のある部分のみをご覧ください。
もくじ
CTF開催の振り返り
CTFを開催するまで
まず、昨年のCTFのアンケートを見直しました。
悪かったところを見ると、Pwnがないことが残念がられていたため、今回のメインをPwnにすることにしました。
Pwnはそれだけで難しいジャンルなので、ひとまず超初心者でも解けるBuffer Overflow問を出すことにしました。結果として、最も簡単な問題であるsuper_easyの正答率は正の得点を得た参加者の66.7%になり、その次のメモリ構造を意識しないと解けない問題であるsuper_easy2の正答率は33.3%になりました。
そこそこ正解率は出ましたが、今回のアンケートを見ると、super_easyも解けない参加者さんもいたようなので、次回は例題も提示してみるようにしようと思いました。一応、writeupに簡単な解説を含めたので、興味のある方はご覧ください。
また、今回はWeb問題としてSQLi系の問題を3題出題しようと考えていたのですが、SQLi問題はBlind SQLiをやられるとマシンに過剰な負荷がかかると考え出題しませんでした。一昨年と同様にGPUの無料枠で問題サーバとスコアサーバを同じインスタンス内で立てたために、問題サーバが落ちるとスコアデータまで消えてしまうのだけは避けたくて出題しませんでした。楽しみにされていた方は申し訳ありませんでした......
今年は多くのインターンシップに参加させていただき懐が温まってきた気がするので、来年はインスタンスを強化するかもしれないです。
また、今回は環境構築としてTerraformを導入してみました。
数ヶ月前にTerraformでGCP無料枠のインスタンスを構築できるTerraformを書いたのでそれを使いました。
Terraformのおかげで、問題開始と共にポート開放をまとめてできたのが良かったと思います。
resource "google_compute_firewall" "whitelist" {
name = "allow-ssh"
network = google_compute_network.vpc_network.name
allow {
protocol = "tcp"
ports = ["443", "2526", "30002", "30003", "30004", "30005", "30006", "30007", "30008", "30009", "30010"]
}
}
CTFの開催中
基本的に、
- マシンリソースの観察
- Twitter, メールの確認
をメインでやっていました。今回はいくつかの問題で障害が発生しましたが、報告されてから1時間以内に対応できたので良かったと思います。一方で、その問題には参加者からの報告か自分の手動確認でしか気づけない仕組みだったため、後ほども書きますが、死活監視を導入すればよかったと思っています。
また、後述しますが、アンケートに応じて問題の改善も行えたのが良かったポイントだと思います。
CTFの開催後
- 問題とWriteupの公開
- 振り返りの実施
をやりました。リポジトリは先述した通りで、振り返りは本記事が該当します。
出題したPolygolfの解説
Polyglotとは
Polyglotとは、単一のコードで複数のプログラミング言語やファイル形式に対応するものです。Polyglotで重要なのは、いかに他言語のコードをコメントとしてコンパイラ・インタプリタに勘違いさせるかです。
コメントになる条件は幾つかまとめたので、こちらをご参照ください。
例として、CとPythonyでhoge
を出力するPolyglotをみてみます。
#define i /*
i="""*/1
main(){puts("hoge");}/*
""";print('hoge')#*/
Cのコンパイラから見ると、下記の部分がコードとして認識される部分です。
#define i
1
main(){puts("hoge");}
Pythonのインタプリタから見ると、下記の部分がコードとして認識される部分です。
i=
;print('hoge')
このようにして、単一のコードで複数のプログラミング言語に対応させることができます。他にも、PDFファイルの関係ない領域にこのようなPolyglotを仕込むことで、JavaScriptを意図せず発火させるようなPolyglotもあるので注意が必要です。
また、CSPをJPEGのPolyglotでバイパスする例も報告されたことがあります。
Code Golfとは
Code Golfとは、与えられたアルゴリズムを可能な限り短く書く競技のことです。C言語で実施されることが多いと思います。
例えば、2つの数字を入力して、大きい方の数字を出力するC言語のプログラムを考えます。
愚直に書くとこんな感じですかね。
#include <stdio.h>
int main(int argc, char *argv[]) {
int a, b;
scanf("%d %d", &a, &b);
if (a>b) {
printf("%d", a);
} else {
printf("%d", b);
}
return 0;
}
コードゴルフの結果は下記の通りです。1
main(a,b){scanf("%d%d",&a,&b);printf("%d",a>b?a:b);}
- int main()のintは必要ない
- 変数はmain()の中で型定義なしに宣言できる
- 三項演算子を使う
- printf,scanfはstdioをimportしなくてもlibcに含まれているので一応使える
Polygolf
閑話休題、問題の話に戻ります。
今回の問題は、GoとC言語の両方でFlagを表示するようなPolyglotを作るというものでした。
結論から言うと、下記のコードで条件を満たせます。
//\
/*
main(){char c[]="cat flag";system(c);}
#if 0
*/
package main
import(
_ "embed"
"fmt"
)
//go:embed flag
var s string
func main(){fmt.Print(s)}
//\
/*
#endif
//*/
解説
Goでは下記の部分がコメントとしてコンパイラに解釈されます。
-
/*
から*/
までの部分 -
//
から行末までの部分
一方、C言語では下記の部分がコメントとしてコンパイラに解釈されます。
-
/*
と*/
までの部分 -
//
から行末までの部分- 末尾に
\
を付与した場合は次の行も
- 末尾に
-
#if 0
から#endif
までの部分- プリプロセッサの機能
これに従うと、下記のように解釈できます。
//\ <- Goはここまでをコメントだと認識する
/* <- Cはここの行も前の行の続きのコメントだと認識する(Goは/*からコメントが始まると認識する)
<- Cはここから自由にコードを書ける
# if 0 <- Cはここから#endifまでコメントだと認識する
*/ <- Goはここまでをコメントだと認識する
<- Goはここから自由にコードを書ける
//\ <- 1行目と同じ
/*
#endif <- Cのコメント終わり
//*/ <- 同上
ここからコードゴルフをすれば良いです。基本的に言語固有のfile openをせずに、大人しくコマンドを実行すると短くなります。
ひとまずこれで終わり、のはずでした。
アンケートに来たとあるフィードバック
taskCTFでは、全完した人にのみアンケートが見えるような設定になっています。そのアンケートに下記のフィードバックが来ていました。
golf系は良いと思います。
できればFLAGとは別にベストスコアが表示されていると楽しめたと思います。
これを見た時に、「確かに!」と思ったのでその場凌ぎの実装をシュッと追加しました。
今回は短期間の運用かつランキングのリアルタイム性はそこまで高くなさそうだったので、インメモリキャッシュにすることでレイテンシが低くなるように工夫しました2。即席にしてはよく書けた方かと思います。
// 略
// ランキングの管理
var ranking map[string]int = map[string]int{}
var mu sync.Mutex
// インメモリキャッシュ
var cache []Info = []Info{}
var cMu sync.Mutex
// ランキング管理用の情報
type Info struct {
Hash string `json:"hash"`
Length int `json:"length"`
}
// ジャッジ用の関数
func Judge(w http.ResponseWriter, r *http.Request) {
// ジャッジ処理なので略
// コードのsha256ハッシュを取得する
h := fmt.Sprintf("%x", sha256.Sum256([]byte(code)))
// スコアの登録
mu.Lock()
if lastScore, ok := ranking[h]; !ok {
ranking[h] = len(code)
} else {
if len(code) < lastScore {
ranking[h] = len(code)
}
}
mu.Unlock()
}
// ランキング取得用の関数
func Rank(w http.ResponseWriter, r *http.Request) {
_ = json.NewEncoder(w).Encode(cache)
}
func main() {
go func() {
// 1分ごとにランキングのキャッシュを更新する
t := time.NewTicker(time.Minute)
defer t.Stop()
for {
select {
case <-t.C:
// ランキングの更新(O(NlogN))
cMu.Lock()
cache = make([]Info, 0, len(ranking))
for k, v := range ranking {
cache = append(cache, Info{Hash: k, Length: v})
}
sort.SliceStable(cache, func(i int, j int) bool {
return cache[i].Length < cache[j].Length
})
cMu.Unlock()
}
}
}()
http.HandleFunc("/judge", Judge)
http.HandleFunc("/rank", Rank)
http.ListenAndServe("0.0.0.0:30008", nil)
}
その後、Twitterで下記の投稿を行い、コードゴルフが始まりました。
第一の刺客-"Flag"をそのまま出力する-
1時間程度は限界の160~180程度の提出が数件あっただけでしたが、急に130~140台のコードが提出されるようになりました。
気になったのでログを見ると、Flagを直に文字列で出力されてました。
今回のコードで文字列の大半を占めているのは、Goのパッケージインポートやコマンド実行のための処理です。そのため、標準出力でFlagをそのまま出力すれば良いと考えたのでしょう。賢いですね。
writeupを見た限りkusanoさんの所業だったようです。参考までに、最後のkusanoさんの提出を引用させていただきます。
//\
/*
main(){system("cat f*");}/*/package main;import."fmt";func main(){Print(`taskctf{H4ve_y0u_kn0w_p0lygl0t}
`)}//*/
第二の刺客-Flagを空にする-
さて、これ以上は出ないだろと思っていたら友人からこんなメッセージが届きました。
少し様子を見ていると、コード長82が提出されました。
詳しくは本人のwriteupを参照してください。
https://github.com/satoki/ctf_writeups/tree/master/taskctf21/polygolf
ハッカーって感じでとても良いですね(?)
次の開催に向けて
- Web問題の提供
- 死活監視の導入
- 問題数の増量
の3点をメインでやろうと思います。来年の自分、任せた。
おわりに
今回のCTFでは昨年のアンケートに従って、初心者向けのPwn問題を多めに出してみました。しかし、あまり楽しめなかった参加者さんも1割ほどいたようです。アンケートに、何が残念だったのか書かれてなかったので原因がよく分かりませんでしたが、問題のジャンルの少なさが指摘されていたので、来年は問題数をもう少し増やしてみようと思います。
最後になりますが、参加してくださった方はありがとうございました。もし良ければ来年も参加していただければ幸いです。
PS: そういえば、今年は欲しいものリストを公開し忘れていたので、通りがかりの富豪がいたらどれか買ってください。