SECCON CTF 2019 予選 write-up

# About
SECCON CTF 2019 公式予選（オンライン予選）に参加して2問（サービス問題は除く）解いたので、そのwrite-up（解法）を記録しておく。
競技概要についてはこちらを参照：[SECCON CTF 2019 公式予選（QUALS）の登録開始しました！10月19日（土）から開催！ - SECCON2019](https://www.seccon.jp/2019/seccon2019/seccon_ctf_2019_quals.html)

結果は、最終スコアが266点で、順位は204位だった。（1点以上点数を獲得したチームは799チーム）

# misc / Beeeeeeeeeer
今回解いた中で一番ポイントの高かった問題。
2019/10/20 15:17時点で110ポイントになっている。
182チームが解いた問題。

問題文は以下の通り。
<img width="497" alt="スクリーンショット 2019-10-20 15.21.38.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/112615/2ccc20c3-16d9-6bb0-9bb3-10203a416c41.png">

Beeeeeeeeeerという名前のファイルと共に、それをデコードしろというシンプルな問題文。
このファイルはテキストファイルで、中身は難読化されたシェルスクリプト。

> ファイルは以下の文字列で始まる33,341文字の1行のみだった。
> 
> ```bash
> echo -e "\033#8";sleep 1;C=$(tput cols);L=$(tput lines);for ID in (以下省略)
> ```
> 所々に`echo`や`sleep`などのLinuxコマンドや`$(...)`などのシェル特有の記法があったので、シェルスクリプトと判断した。

とりあえず実行してみると、コンソール画面を埋め尽くすように沢山の`E`という文字が表示された後に
それがポツポツと消えていくというアニメーションが一瞬表示された後、

```
Let's decording!(≧∀≦*)
```
というメッセージが表示され、それ以降は何を入力しても何も応答がなく止まった状態になる。

なので `bash -x` （スクリプト内で実行するコマンドを表示するデバッグモードみたいなオプション）で再実行すると、
先程と同様のアニメーションのあと、以下の表示になった。

```bash
+ echo TGV0J3MgZGVjb3JkaW5nISjiiafiiIDiiaYqKQo=
+ base64 -d
Let's decording!(≧∀≦*)
+ read

++ echo MjAK
++ base64 -d
+ trap '' 1 2 3 15 18 19 20
+ echo hxB
+ grep x
hxB
+ exit
```

`read`が出た後は入力待ちとなったので、Enterを押すと、
先程とは違ってすぐにスクリプトが終了した。
動作が変わってしまったし、上記の`+ echo hxB`のあたりが謎なので、
地道にこのスクリプトを解読していくことにした。

> ちなみに、macOSでこのスクリプトを実行すると、以下のエラーが出た。
> 
> ```bash
> base64: invalid option -- d
> Usage:	base64 [-hvD] [-b num] [-i in_file] [-o out_file]
>   -h, --help     display this message
>   -D, --decode   decodes input
>   -b, --break    break encoded string into num character lines
>   -i, --input    input file (default: "-" for stdin)
>   -o, --output   output file (default: "-" for stdout)
> 
> ```
> 
> macOSの`base64`は`-d`オプションに対応していないので、
> 以下のようにUbuntuのDokcerコンテナ上で実行した。
> 
> ```bash
> $ docker run -it --rm -v $PWD:/tmp/ ubuntu bash -x /tmp/Beeeeeeeeeer
> ```

スクリプト中に出てくる`$'\164\162\141\160'` のような `$'string'` という形式の文字列は、
スクリプト実行時に文字コード→文字へと変換されて実行されるらしい。（参考：[ANSI-C Quoting (Bash Reference Manual)](http://www.gnu.org/software/bash/manual/html_node/ANSI_002dC-Quoting.html#ANSI_002dC-Quoting)）
変換後の文字列を見るには、以下のように`echo`に渡せばOK。

```bash
$ echo $'\164\162\141\160'
trap
```

このように地道に解読していった結果、
先ほど謎だった`+ echo hxB`の部分にたどり着いた。

```bash
echo $-|grep x && exit;
```

`$-`は[Man page of BASH](https://linuxjm.osdn.jp/html/GNU_bash/man1/bash.1.html)によると、
> オプションはシェルを起動する際の引き数としても指定できます。 現在のオプションの集合は、 $- で知ることができます。

とのこと。
つまり、`bash -x`とすると、`$-`に`x`が含まれるため、`echo $-|grep x`の終了コードが0となって、`exit`してしまった。
なので、この部分をコメントアウトした。

このような感じでスクリプトの実行を阻む罠がいくつも仕掛けられているので、それらを地道に無効化していく。

> 他にも以下のような罠が見つかった。
>
> ```bash:ログインユーザ名がroot,user,adm,nobody,test,スクリプトの第1引数のいずれかを含まないと即exit
> if [ -z "$1" ];then
>   ID=nandoku
> else
>   ID="$1"
> fi
> 
> if whoami | grep -e root -e user -e adm -e nobody -e test -e "$ID" >/dev/null
> then
>   :
> else
>   exit
> fi
> ```
>
> ```bash:(0から9のランダムな数)×(0から299のランダムな数)の分だけsleepする
> for i in $(seq $((RANDOM % 10)))
> do
>   sleep $((RANDOM % 300))
> done
> ```
> 最初の実行時に何も応答がなく止まった状態になったのは、恐らくこれの影響

以下のように謎の変数`S1`をexportしているコードもあった。
（このときはまだこの変数が後々重要な役割を果たすとは思いもしなかった）

```bash
export S1=hogefuga
```

そして、上記の直後に、以下のように27,548文字のBase64エンコードされた文字列をデコードして、
`gunzip`で展開し、Bashスクリプトとして実行しているコードを見つけた。

```bash
echo -n H4sIAMlSRl0AA439Z6（中略）sAAA==|base64 -D|gunzip|bash;
```

この部分を`bash -x`で実行すると、
数回ビープ音が鳴った後に、`How many beeps?`とメッセージが表示されたので、聞こえた回数を入力すると、
再び、数回ビープ音が鳴る→メッセージが表示、となったのでその流れを数回繰り返した。
すると、いろいろなコマンドが実行されたログが表示された後、以下の表示とともにスクリプトが終了した。

```bash
++ echo -n 3
++ md5sum
++ cut -c2,3,5,12
+ openssl aes-256-cbc -d -pass pass:cccc -md md5
```

終了した原因を探るために、先程`gunzip`で展開して実行したスクリプトをファイルに出力して、
中身を確認した。

```bash
echo -n H4sIAMlSRl0AA439Z6（中略）sAAA==|base64 -D|gunzip > hoge
```

この中身も、以下の文字列で始まる27,448文字が1行に収められている難読化されたシェルスクリプトだった。

```bash
for k in $($(echo p2IkPt== |tr A-Za-z N-ZA-Mn-za-m|base64 -d) （省略）
```

この末尾を見ると以下のコマンドで終わっており、内容的に先程終了したスクリプトの最後の部分（`openssl aes-256-cbc -d -pass pass:cccc -md md5`）と一致する。

```bash
openssl aes-256-cbc -d -pass pass:$(echo -n $n|md5sum |cut -c2,3,5,12) -md md5 2>/dev/null |bash;
```

`openssl`コマンドがエラーで終了した可能性も疑い、エラー出力をリダイレクトしている部分も含めて`2>/dev/null |bash;`を削除し、再実行。

```bash
bash: openssl: command not found
```

すると上記の通り、`openssl`コマンドがインストールされていなくてエラーとなっていたことが分かった。
そのため、`apt update && apt install openssl`でインストールしたあと、
`2>/dev/null |bash -x`を追加し、スクリプトを実行した。
すると、以下のようにパスワードを入力しろ、というメッセージが表示され、入力待ちとなった。

```bash
+ echo 'Enter the password'
Enter the password
+ read _____
```

当然、パスワードは不明なため、適当に`hoge`と入力し、Enterを押すと、以下の表示が出てスクリプトが終了した。

```bash
+ : password is bash
++ echo -n hoge
+ grep -q d574d4bb40c84861791a694a999cce69
++ md5sum
++ cut '-d ' -f1
+ echo ea703e7aa1efda0064eaa507d9e8ab7e
+ echo -e '\033[?7h'
```

このメッセージに、`: password is bash`というパスワードが書かれていたので、
再実行して`bash`を入力すると、`Good Job!`というメッセージとともに、`SECCON{3bash}`が表示された!:clap:

と喜んだのも束の間・・・これを出題画面で入力しても、正解にならない・・・
なので、このスクリプトを`bash -x`で実行せずにファイルに出力してみると、
末尾に`echo SECCON{$S1$n$_____}`というコマンドを発見！

`$_____`は、パスワード入力時に実行されていた`read _____`というコマンドから、`bash`という文字列がセットされることがわかる。
`$n`は、ビープ音何回鳴った？クイズで最後に入力した答えがセットされる。（なお、このクイズの最終問題は常に答えが3になるようにプログラムされていた）
`$S1`は、途中で出てきた謎の変数！　なので`hogefuga`が入る。

よって答えは`SECCON{hogefuga3bash}`。これでクリアできた！:v:
この問題を解くのに2時間52分かかった。

# crypto / coffee_break

2019/10/20 17:45時点で56ポイントになっている。
384チームが解いた問題。

問題文は以下の通り。

<img width="497" alt="スクリーンショット 2019-10-20 17.43.20.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/112615/e7a5c20c-69a8-f2fd-126f-e0ce6e4cc8f2.png">

以下の`encrypt.py`で暗号化した文字列`FyRyZNBO2MG6ncd3hEkC/yeYKUseI/CxYoZiIeV2fe/Jmtwx+WbWmU1gtMX9m905`を復号する問題。


```python:encrypt.py
import sys
from Crypto.Cipher import AES
import base64


def encrypt(key, text):
    s = ''
    for i in range(len(text)):
        s += chr((((ord(text[i]) - 0x20) + (ord(key[i % len(key)]) - 0x20)) % (0x7e - 0x20 + 1)) + 0x20)
    return s


key1 = "SECCON"
key2 = "seccon2019"
text = sys.argv[1]

enc1 = encrypt(key1, text)
cipher = AES.new(key2 + chr(0x00) * (16 - (len(key2) % 16)), AES.MODE_ECB)
p = 16 - (len(enc1) % 16)
enc2 = cipher.encrypt(enc1 + chr(p) * p)
print(base64.b64encode(enc2).decode('ascii'))
```

以下のスクリプトで解けた。

```python:decrypt.py
# coding: utf-8
import sys
from Crypto.Cipher import AES
import base64

encoded = b'FyRyZNBO2MG6ncd3hEkC/yeYKUseI/CxYoZiIeV2fe/Jmtwx+WbWmU1gtMX9m905'
key1 = "SECCON"
key2 = "seccon2019"

# encrypt.pyの
#   base64.b64encode(enc2).decode('ascii')
# の実行結果 == "FyRyZNBO2MG6ncd3hEkC/yeYKUseI/CxYoZiIeV2fe/Jmtwx+WbWmU1gtMX9m905"
# なので、その文字列をBase64デコードすると enc2 となる
enc2 = base64.b64decode(encoded)


# cipher は decrypt というメソッドを持っているので、encrypt.pyと同じcipherを使う
cipher = AES.new(key2 + chr(0x00) * (16 - (len(key2) % 16)), AES.MODE_ECB)


# encrypt.pyでは
#   enc2 = cipher.encrypt(enc1 + chr(p) * p)
# となっているので
#   cipher.decrypt(enc2)
# の結果(下記)は enc1 + chr(p) * p と同値。
#   b"'jff~|Ox9'34G9#g52F?489>B%|)173~)%8.'jff~|Q\x05\x05\x05\x05\x05"
# "chr(p) * p" の部分は chr(p) を p 回繰り返した文字列になるので、
# 末尾の繰り返されている5文字 "\x05\x05\x05\x05\x05" がそれに該当する。
# なので、それを除外すると enc1 は以下のようになる。
enc1_plus_alpha = cipher.decrypt(enc2) # -> b"'jff~|Ox9'34G9#g52F?489>B%|)173~)%8.'jff~|Q\x05\x05\x05\x05\x05"
length = len(enc1_plus_alpha)
enc1 = cipher.decrypt(enc2)[0:length-5]


# encrypt.pyのencrypt関数と逆の処理を行うdecrypt関数を定義
import re
def decrypt(key, enc):
    s = ''
    for i in range(len(enc)):
        c = enc[i] - 0x20
        b = ord(key[i % len(key)]) - 0x20
        a = c + 0x5F - b + 0x20
        # a は上記の "0x5F" の加算が必要なパターンと不要なパターンの2通りの可能性があるため、
        # chr(a) の結果が答えの文字列として使われそうな文字に該当するか否かで判断する
        if re.search(r"[a-zA-Z0-9{}_]", chr(a)):
            s += chr(a)
        else:
            s += chr(a - 0x5F)
    return s

answer = decrypt(key1, enc1)
print(answer)
```

答えは`SECCON{Success_Decryption_Yeah_Yeah_SECCON}`となる。
この問題は解くのに3時間かかった。