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チームが解いた問題。
Beeeeeeeeeerという名前のファイルと共に、それをデコードしろというシンプルな問題文。
このファイルはテキストファイルで、中身は難読化されたシェルスクリプト。
ファイルは以下の文字列で始まる33,341文字の1行のみだった。
echo -e "\033#8";sleep 1;C=$(tput cols);L=$(tput lines);for ID in (以下省略)
所々に
echo
やsleep
などの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引数のいずれかを含まないと即exitif [ -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}
が表示された!
と喜んだのも束の間・・・これを出題画面で入力しても、正解にならない・・・
なので、このスクリプトをbash -x
で実行せずにファイルに出力してみると、
末尾にecho SECCON{$S1$n$_____}
というコマンドを発見!
$_____
は、パスワード入力時に実行されていたread _____
というコマンドから、bash
という文字列がセットされることがわかる。
$n
は、ビープ音何回鳴った?クイズで最後に入力した答えがセットされる。(なお、このクイズの最終問題は常に答えが3になるようにプログラムされていた)
$S1
は、途中で出てきた謎の変数! なのでhogefuga
が入る。
よって答えはSECCON{hogefuga3bash}
。これでクリアできた!
この問題を解くのに2時間52分かかった。
crypto / coffee_break
2019/10/20 17:45時点で56ポイントになっている。
384チームが解いた問題。
問題文は以下の通り。
以下のencrypt.py
で暗号化した文字列FyRyZNBO2MG6ncd3hEkC/yeYKUseI/CxYoZiIeV2fe/Jmtwx+WbWmU1gtMX9m905
を復号する問題。
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'))
以下のスクリプトで解けた。
# 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時間かかった。