fr33f0r4ll

自分用雑記

TokyoWesterns CTF4th 2018 Writeup

TokyoWesterns CTF 4th 2018 Writeup

TWCTF4th 2018のWriteupです。今回は始めてのチーム戦でした。

pwn解けなかった...

warmupだけです。

SimpleAuth

Web問、warmup

問題文にはURLが貼ってある。 最初にアクセスするとPHPソースコードが表示され、MD5ハッシュの比較をしていることが分かる。 MD5ハッシュはhash('md5', $user.$pass);と、ユーザ名とパスの組み合わせになっている。 Webに詳しくないのでMD5のハッシュをクラックする問題かと思ってしばらく解けなかった...

PHPではURLで渡されたクエリがそのまま変数として扱えるので、hashed_passwordというパラメータを送信すると、$hashed_passwordの値はパラメータの値になる。 ソースコードを読むとuserとpassを設定していなくてもhashed_passwordのチェックは行われるため、ハッシュを送信するだけでいい。 つまりpass the hashが成立する。 具体的には、http://simpleauth.chal.ctf.westerns.tokyo/?action=auth&hashed_password=c019f6e5cd8aa0bbbcc6e994a54c757eというクエリを投げればいい。 これでフラグが表示される。

PHPとWebにもう少し詳しければすぐに気付けたはず、複雑に考え過ぎた。 これはWeb問であってCrypto問じゃない。繰り返す、Web問であってCrypto問じゃない

dec dec dec

Rev問

バイナリが降ってくる。 チームの人が解析したのを見て閃いた感じ。

静的解析で見てみると、文字列処理っぽいのを3回繰り返してからバイナリ中の文字列と比較している。 入力がフラグらしいので、この文字列を復元するのが目的になる。

動的解析してみると、3回の文字列処理のうち一回目は単なるBase64だと分かる。適当にやってれば末尾に=が付いた文字列が出てくるのでなんとなく察せると思う。

次の処理が若干分かりにくいが、=のままアルファベットだけ変化してることと単なる符号化であるという前提で考えるとrot13ではないかと閃いた。試してみると正解だった。

最後の文字列処理が分かりにくかったが、色々試してみてuuencodeだと分かった。

まとめると、入力文字列 -> base64 -> rot13 -> uuencode -> バイナリ中の文字列と比較、という流れになる。 つまり、バイナリ中の文字列 ->uudecode -> rot13 -> base64 decodeとすればフラグが手に入るはずである。

しかし、何故かバイナリ中の文字列を復号してもTWCTF{base64までしか復元できなかった。謎。多分uudecodeのやり方が悪いんだとは思う。 でもここまで分かったら十分である。base64, rot13, uuencodeだったんだから符号化方式の名前だと当たりを付けてみたらビンゴだった。

とりあえず復号スクリプト

python3 -c "from codecs import decode;print(decode(b'begin 660 <data>\n425-Q44E233=,>E-M34=,,\'-$/3T \nend\n', 'uu').decode())" | nkf -r | base64 -d

破損しているので注意。 Ubuntuからuuencodeなくなってるから凄い不便。

scs7

Crypto問

問題文にはサーバの情報しかない。 とりあえずリモートサーバに接続すると暗号化されたフラグが表示され、メッセージの入力と暗号文の取得ができるようになっている。 適当に入力して試してみたところ、以前に出てきた文字の情報を暗号化に使用することが分かった。 例えば、適当にAAAAとAAABを入力すると最後の1文字だけ違う暗号文になっていて、ABAAとAAAAを入力すると先頭の数文字以降はまったく違う暗号文になっている。

この特徴とフラグがTWCTF{から始まっていることを利用すると、フラグを一文字ずつ特定することが可能になる。 まず、TWCTF{を一文字ずつ増やしていって先頭の数文字が一致する長さを探す。これでフラグの文字数を特定する。 この前提は少し怪しいけどとりあえずうまくいった、47文字だと判明。

次にありえそうな文字を順番に1文字付加していって、暗号化されたフラグと一致する文字数が増加する文字を採用するようにする。 例えば、TWCTF{A + 40文字, TWCTF{B + 40文字...という感じで試行し、返ってきた暗号文と暗号化フラグを比較して、一致する文字数が増えたものを採用といった感じである。 やってて気付いたけど複数の候補が存在するので探査するときには注意が必要になる、その場合は2文字目でマッチする長さが増加しなくなるので弾く。 これを40文字分繰り返せばいい。途中までやると16進数値になっていることが分かるので探索範囲は狭まる。

スクリプトを書いたがほぼ手動だった。失敗するとサーバへのブルートフォースになるので自動化がためらわれる...

from pwn import *
import string
import time

flag_head = 'TWCTF{'
flag_tail = '}'
search_range = 'abcdef0123456789' # 複数の文字種が候補になりうるので、順番にブルートフォースしてた
flag_size = 47  # get by analyze_flag_len


# 暗号化されたフラグを抽出する
def fetch_first_msg(io):
    first_msg = io.recvuntil('message:')
    encrypted_flag = first_msg.split('\n')[0]
    encrypted_flag = encrypted_flag.replace('encrypted flag: ', '').strip()

    return encrypted_flag


# メッセージを投げて得られた暗号文を返す
def try_encrypt(io, msg):
    io.clean()
    io.sendline(msg)

    encrypt_msg = io.recvuntil('message:')
    encrypt_msg = encrypt_msg.split('\n')[0].replace('ciphertext: ', '')

    return encrypt_msg


# フラグの長さ調査用、最初の5文字くらいを比較して一致してればその長さを返す
def analyze_flag_len(io, encrypted_flag):
    for i in range(1, 101):
        test_flag = flag_head + 'A' * i  # padding

        enc_msg = try_encrypt(io, test_flag)

        if enc_msg[0:5] == encrypted_flag[0:5]:
            log.info("Challenge message: {}".format(enc_msg))
            log.info("Encrypted flag: {}".format(encrypted_flag))

            return len(test_flag)

    return None


# 文字列の先頭から何文字一致するか返す、他に関数あるかも
def match_len(str1, str2):
    count = 0

    for ch1, ch2 in zip(str1, str2):
        if ch1 == ch2:
            count += 1
        else:
            break

    return count


# フラグ解析、一文字だけブルートフォースする感じ
def crack_encryption(io, encrypted_flag, flag_size, flag_head, flag_tail):
    pad_ch = '*'

    # ハッシュだと分かる前は文字全部試してた
    # candidates = string.ascii_letters
    # candidates += string.digits
    # candidates += string.punctuation
    candidates = 'abcdef0123456789'
    hit_chs = []

    get_flag = lambda head, tail: head + pad_ch * (flag_size - len(head) - len(tail)) + tail
    
    test_flag = get_flag(flag_head, flag_tail)  
    io.info("Flag is: " + test_flag)    
    enc_msg = try_encrypt(io, test_flag)

    # 初期の一致文字数、これより長い文字を発見する
    match_length = match_len(enc_msg, encrypted_flag)
    log.info('Initial match length: ' + str(match_length))

    # ブルートフォース
    for ch in candidates:
        test_flag = get_flag(flag_head + ch, flag_tail)
        enc_msg = try_encrypt(io, test_flag)
        tmp_len = match_len(enc_msg, encrypted_flag)

        if tmp_len > match_length:
            log.info("Flag: " + test_flag)
            log.info(tmp_len)
            hit_chs += ch # 複数の文字が候補になりうる

    return hit_chs


for ch in search_range:
    temp_flag_head = flag_head + ch

    io = remote('crypto.chal.ctf.westerns.tokyo', 14791)

    encrypted_flag = fetch_first_msg(io)
    log.info("Encrypted flag is : ")
    log.info(encrypted_flag)

    hit_chs = crack_encryption(io, encrypted_flag, flag_size, temp_flag_head, flag_tail)
    hit_chs = ''.join(hit_chs)
    log.info("Result:" + ch + ", " + hit_chs)

    # ヒットする文字があれば探索を中止。2文字の組み合わせでマッチする長さが伸びるパターンはないと判断した
    if len(hit_chs) != 0:
        io.close()
        exit(0)

    io.close()

    # サーバへのお情け
    time.sleep(3)

# フラグの長さ解析、長さによって暗号文全体が変わるので最初に長さを合わせる必要がある
# flag_len = analyze_flag_len(io, encrypted_flag)
# log.info("Check flag size: {}".format(flag_len))

mondai.zip

misc問

複雑に考え過ぎてやらかしたシリーズその2。

問題文からzipファイルがダウンロードできる。 解凍するとy0k0s0.zipというzipファイルが出てくる。 johnでクラック。

zip2john y0k0s0.zip > hash.txt
john hash.txt

ファイル名がパスなのですぐ終わる。

y0k0s0.zipを解凍するとmondai.zipというzipファイルとcapture.pcapngが出てくる。 pcapファイルを開くとpingのパケットだけがある。 データを見るとすぐに分かるけど、abcdef...とアルファベット順にデータが並んでいることが分かる。 いくつかのpingを見比べるとそれぞれ微妙に長さが違ったので、とりあえず長さをasciiにしてみたらビンゴだった。 いくつかダミーのpingがあるのだけ気を付けてasciiに直すと、それがパスになる。

mondai.zipを解凍すると、さらにmondai.zipとlist.txtが出てくる。 list.txtを開くと10文字くらいの文字列が1000行並んでいる。 ここで複雑に考え過ぎてしまい、list.txtを符号化された文章だと思ってしまった。 実際にはただのパスワードリストなので、これで辞書型攻撃をするだけでいい。

zip2john mondai.zip > hash.txt
john hash.txt --wordlist=list.txt

これで解ける。

解凍すると今度はハッシュ値みたいな名前のファイルが出てくる。 fileコマンドで調べるとこれもzipだった。 ハッシュの長さからMD5かと思って、ファイルのMD5ハッシュを取ったが一致しなかった。 今度こそレインボーテーブルで攻撃するとヒットした。

解凍すると、README.txtとmondai.zipが出てきた。 REDAME.txtによると、パスワードは短いらしいのでブルートフォース

zip2john mondai.zip > hash.txt
john hash.txt

2文字なのですぐに出てくる。 そして出てきた文章によるとこれまでのパスの組み合わせがフラグになる。

これでフラグゲット。

load

Pwn問

できなかったpwn... ファイルを読み込んで、読み込みの成否を表示するだけのプログラムになっている。 リモートで試してみるとflag.txtがあるようなので、おそらくこの中身を見ればいいはず。 /proc/self/fd/0を読み込み先に指定できて、ファイルを読み込むバッファがBOFする。 NX, Full RELRO, FORTIFYとなっている。CanaryはないのでやはりBOF。 しかし、肝心の標準入出力がmain関数の最後で閉じられてしまうためどう頑張ってもflag.txtを表示できない。 使える関数からしても、ファイルを開いたりはできるが新しく外部へと接続するようなことはできないっぽい。 色々考えたけど思い付かなかった。 最初に入力するファイル名に複数のファイルを入れて、/proc/self/fd/1を開いてみたり/dev/ttyとか探してみたりflag.txtを開いてみたりしたけど、どうしても外部に出力できなかった。 BOF自体はfdのクローズ前に可能なので、どうにかして誤魔化すんだろうか。 整数値の読み込み周りで何かあるのかもしれないが思い付かない。 くそう。