search
LoginSignup
1

More than 3 years have passed since last update.

posted at

updated at

SECCON CTF 2019 予選 write-up

About

SECCON CTF 2019 公式予選(オンライン予選)に参加して2問(サービス問題は除く)解いたので、そのwrite-up(解法)を記録しておく。
競技概要についてはこちらを参照:SECCON CTF 2019 公式予選(QUALS)の登録開始しました!10月19日(土)から開催! - SECCON2019

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

misc / Beeeeeeeeeer

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

問題文は以下の通り。
スクリーンショット 2019-10-20 15.21.38.png

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

ファイルは以下の文字列で始まる33,341文字の1行のみだった。

echo -e "\033#8";sleep 1;C=$(tput cols);L=$(tput lines);for ID in (以下省略)

所々にechosleepなどのLinuxコマンドや$(...)などのシェル特有の記法があったので、シェルスクリプトと判断した。

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

Let's decording!(≧∀≦*)

というメッセージが表示され、それ以降は何を入力しても何も応答がなく止まった状態になる。

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

+ 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でこのスクリプトを実行すると、以下のエラーが出た。

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コンテナ上で実行した。

$ docker run -it --rm -v $PWD:/tmp/ ubuntu bash -x /tmp/Beeeeeeeeeer

スクリプト中に出てくる$'\164\162\141\160' のような $'string' という形式の文字列は、
スクリプト実行時に文字コード→文字へと変換されて実行されるらしい。(参考:ANSI-C Quoting (Bash Reference Manual)
変換後の文字列を見るには、以下のようにechoに渡せばOK。

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

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

echo $-|grep x && exit;

$-Man page of BASHによると、

オプションはシェルを起動する際の引き数としても指定できます。 現在のオプションの集合は、 $- で知ることができます。

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

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

他にも以下のような罠が見つかった。

ログインユーザ名が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
(0から9のランダムな数)×(0から299のランダムな数)の分だけsleepする
for i in $(seq $((RANDOM % 10)))
do
  sleep $((RANDOM % 300))
done

最初の実行時に何も応答がなく止まった状態になったのは、恐らくこれの影響

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

export S1=hogefuga

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

echo -n H4sIAMlSRl0AA439Z6(中略)sAAA==|base64 -D|gunzip|bash;

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

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

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

echo -n H4sIAMlSRl0AA439Z6(中略)sAAA==|base64 -D|gunzip > hoge

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

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)と一致する。

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: openssl: command not found

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

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

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

+ : 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チームが解いた問題。

問題文は以下の通り。

スクリーンショット 2019-10-20 17.43.20.png

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

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

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

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時間かかった。

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
What you can do with signing up
1