House of apple1

文章发布时间:

最后更新时间:

文章总字数:
1.5k

预计阅读时间:
6 分钟

前言

2.35很好用的一条链 十分推荐学习 仍然建议是自己看完分析以后 然后动调学着伪造结构体 毕竟ctf的pwn学习主要还是培养思维

利用链分析

通过劫持io_list_all 利用exit函数触发overflow 可以达到任意地址写已知地址的功能

完整的利用链如下

exit -> fcloseall -> _IO_cleanup -> _IO_flush_all_lockp -> _IO_OVERFLOW

而我们只需要利用largebinattack往IO_list_all中写入一个堆地址 随后在该chunk上构造结构体

首先要明白一点 exit函数的执行 会通过IO_list_all来索引所有的iofile 并且通过其vtable来索引到对应的overflow函数 那么来看一下IO_wstrn_jumps的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;
}

在最开始 进行了一个指针类型的转化 但是snf和fp还是指向同一个iofile 也就是我们的chunk

接着如果通过了这个判断 就可以进入if分支

1
fp->_wide_data->_IO_buf_base != snf->overflow_buf

这里的overflow_buf是什么呢 我们来看一下_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的数组变量

大部分情况下 这个判断是可以通过的 目前还没有遇到过不通过的情况 遇到了再做补充

进入if分支以后 可以看到我们对于fp->_wide_data存储的成员变量赋值了很多次

赋的值我们在随后的伪造过程中可以清晰的看到 是fakefile_addr + 0xf0 或者是+0x1f0 这里先记住一下 随后再深入研究

为了实现上述的任意地址写已知值 我们需要控制一下fakefile的_wide_data成员 并且还需要使得vtable为IO_wstrn_jumps 这样就可以往 _wide_data处的指针写入fake_file+0xf0和fake_file+0x1f0

通过下面iofile的成员偏移 可以得知我们需要往fakefile的0xa0写入一个地址 往0xd8写入IO_wstrn_jumps 同时伪造好相关的成员 使得我们能够执行到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
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'

伪造分析

前面利用largebin往IO_list_all写堆地址的部分就不讲了

我们先大致伪造一下fakefile 随后断点打在 _IO_flush_all_lockp 观察如何满足条件 使其执行到IO_wstrn_overflow函数

1
2
3
fake_file = b''
fake_file = fake_file.ljust(0xa0,b'\x00')+p64(heap_addr)
fake_file = fake_file.ljust(0xd8,b'\x00')+p64(IO_wstrn_jumps)

image-20230707233506450

通过简单的观察汇编 我们不难发现我们需要使得程序执行到_IO_flush_all_lockp+223处才可以进入overflow函数 接下来逐步n下去 观察到这里的判断 如果不使其跳转 那么我们就跳过了223处的调用

image-20230707233633281

观察一下此时r15寄存器参数

image-20230707233914959

这里需要使得r15+0x28处小于r15+0x20处 而此时r15寄存器存储的就是我们fake_file

所以我们在对应地址布置好参数 此时再次动调 发现成功进入overflow函数

1
2
3
4
fake_file = b''
fake_file = fake_file.ljust(0x20,b'\x00')+p64(0)+p64(1)
fake_file = fake_file.ljust(0xa0,b'\x00')+p64(heap_addr)
fake_file = fake_file.ljust(0xd8,b'\x00')+p64(IO_wstrn_jumps)

image-20230707234040440

image-20230707234112609

同时你可以看到这个函数 其赋值就是通过那一堆movdpa指令 其参数时xmm0 向上溯源一下会发现就是rdi+0xf0和rdi+0x1f0两种

此时观察我们想要写入已知值的堆基址

image-20230707234301169

可以发现写入成功

注意事项

需要注意的是 在_IO_wstrn_overflow+78处 你会发现其执行了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;
}

image-20230707234637488

这里的rdx值就是我们想要任意写已知值的地址 如果其+0x30处 不为0 那么就不会跳转 从而触发执行free函数 这里有时候需要注意一下 不过如果地址无法满足判断条件的话 这里还有一次更改fakefile参数来避免的办法

rbx寄存器的值就是我们fakefile的地址 使其0x74处的数据赋值给了eax 同时对于al寄存器和8进行test 如果其不为8的话 就可以绕过 (不乱改数据的话 我觉得很少的情况才会遇到吧

image-20230707234823126