演示调试代码
int main(){
puts("beef");
puts("dead");
return 0
}
几个重要的结构体
struct _IO_FILE {
int _flags;
char *_IO_read_ptr; /读取时 当前指针
char *_IO_read_end; /结束
char *_IO_read_base;
char *_IO_write_base;
char *_IO_write_ptr;
char *_IO_write_end;
char *_IO_buf_base;
char *_IO_buf_end;
char *_IO_save_base;
char *_IO_backup_base;
char *_IO_save_end;
struct _IO_marker *_markers;
struct _IO_FILE *_chain;
int _fileno;
int _flags2;
__off_t _old_offset;
unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[1];
_IO_lock_t *_lock;
__off64_t _offset;
struct _IO_codecvt *_codecvt;
struct _IO_wide_data *_wide_data;
struct _IO_FILE *_freeres_list;
void *_freeres_buf;
size_t __pad5;
int _mode;
char _unused2[20];
} *
_IO_file处理文件时使用的一个核心结构体,同时linux预先创建了 _fileno为0,1,2用来表示stdin,stdout ,stderr。而puts之类输出函数通常使用stdout。
本文主要在结合gdb与对应源码的基础上重新讲述程序首次运行时对应结构体的初始化以及该结构体在何时,如何发挥作用
在进入puts函数之前,stdout内容如下
简单解释一下对应的字段含义
_flag字段保存了魔术头和一些标志位,实际上该字段表示,在利用该时通常需要修改_flag 绕过一些检测
_flag=0xfbad2084--> _IO_IS_FILEBUF|_IO_LINKED|_IO_NO_READS
0xFBAD 为magic head 0x2084对应为标志位。对应一些标志的定义如下
经过延时绑定的跳转进入到puts函数主体
针对puts,进行调用了如图的函数,处理传入puts的字符串的长度问题
单步调试 发现第一个函数调用 _IO_file_xsputn
在源代码中对应的函数为 _IO_file_xsputn
在调试时出现函数名不同的原因是因为下面这个宏
如下
count = f->_IO_write_end - f->_IO_write_ptr; /* Space available. */
if ((f->_flags & _IO_LINE_BUF) && (f->_flags & _IO_CURRENTLY_PUTTING)
此时count为0,且无法满足对标志位的判定
if (to_do + must_flush > 0)
{
_IO_size_t block_size, do_write;
/* Next flush the (full) buffer. */
if (_IO_OVERFLOW (f, EOF) == EOF)
_IO_OVERFLOW
对应到运行时的函数跳转为
即 _IO_new_file_overflow ,
if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0 || f->_IO_write_base == 0)
{
/* Allocate a buffer if needed. */
if (f->_IO_write_base == 0)
{
INTUSE(_IO_doallocbuf) (f);
_IO_setg (f, f->_IO_buf_base, f->_IO_buf_base, f->_IO_buf_base);
}
此时 调用_IO_doallocbuf 这个函数进行了对缓冲区的申请,以及填充对应字段。
实际调用 _IO_file_doallocate函数
在glibc-2.3.1/libio/filedoalloc.c
进入到该函数,进行对file_no的检查,应该是防止错误出现
if (fp->_fileno >= 0 && __builtin_expect (_IO_SYSSTAT (fp, &st), 0) >= 0)
这段代码实际执行 _IO_file_stat,对应调用系统调用syscall 5 获取对应文件信息 储存在stat中,而st刚好是stat64类型的数据
ps:
关于stat的系统调用,在下面的调试过程中可以体现出作用
示例:
int main(){
struct stat m_stat;
► stat("./a.txt",&m_stat);
printf("%ldn",m_stat.st_size);
return 0;
10 }
调用初始化之前 对应结构体
调用stat后
将m_stat 填充
具体字段含义与本文关联性不大,辅助理解用
psend:
通过stat获取到stdout的文件信息
if (fp->_fileno >= 0 && __builtin_expect (_IO_SYSSTAT (fp, &st), 0) >= 0)
{
if (S_ISCHR (st.st_mode))
{
/* Possibly a tty. */
if (
DEV_TTY_P (&st) ||
isatty (fp->_fileno))
fp->_flags |= _IO_LINE_BUF;
}
if (st.st_blksize > 0)
size = st.st_blksize;
}
st_mode = 0x2190
(S_ISCHR (st.st_mode))j宏定义展开为
在通过file_no的检查后 设置stdout 的_flag
fp->_flags |= _IO_LINE_BUF;
执行后_flag改变
然后根据获取的文件信息st 申请buf的空间 通过调用malloc实现
size=0x400从 size = st.st_blksize;中取出
(合理)
do {
(_B) = (char*)malloc(_S);
if ((_B) == NULL)
return (_R);
} while (0)
最后在_IO_setb中实现对buf_base与buf_end的填充 参数为malloc申请的chunk初始地址与结束地址
前:
后:
合理)
随后返回到 _IO_file_overflow 进行剩余指针的填充
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;
#define _IO_CURRENTLY_PUTTING 0x800
结果如下
标志位再变
if (ch == EOF)
return _IO_new_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_new_do_write(f, f->_IO_write_base,
f->_IO_write_ptr - f->_IO_write_base) == EOF)
return EOF;
return (unsigned char) ch;
接下来 进入 _IO_new_do_write,
_IO_new_do_write (fp, data, to_do)
_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;
}
由于满足to_do == 0 返回到 _IO_file_xsputn中,
继续运行到调用该函数,参数为stdout 以及传入字符 字符长度 buf区size
在该函数中as通过多次调用_IO_file_overflow 将字符写入到缓冲区中,经过第一次的执行
write_ptr + 1 指向输入的末尾 ,
填充最后的结果
完成后返回到puts函数主体 , puts调用_overflow , 之后调用 _IO_file_overflow , 调用 _IO_do_write ,
_IO_new_do_write (fp, data, to_do)
_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;
}
这次的调用进入到 new_do_write中 ,
实际调用到write的syscall,输出,
write系统调用后,在_IO_file_write中将 _IO_write_ptr 复位.
在这一过程中,write并没有使用file结构体
验证代码如下:
int main(){
write(1,"asasd",3);
}
调用之前
调用之后
前后没有变化,说明write并没有使用该结构体,直接输出。
对puts函数整体的分析到此为止
- 结尾 - 精彩推荐 【技术分享】Largebin Attack for Glibc 2.31 【技术分享】shiro550漏洞复现与研究 【技术分享】pocassist—全新的开源在线poc测试框架 戳“阅读原文”查看更多内容 原文始发于微信公众号(安全客):【技术分享】从puts函数执行角度看_IO_FILE的使用
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论