Use a negative index to change _IO_2_1_STDOUT_ and execute IO_OVERFLOW.
Next puts will leak a libc address.
Repeat 1st step but now change flags field to “/bin/sh\x00” and the vtable to IO_helper_jumps.
Change IO_helper_jumps IO_OVERFLOW pointer to system.
Next puts/printf will execute IO_OVEFLOW (fp, EOF) which is now system(fp=/bin/sh).
Challenge
I feel like I ended up using an unintended solution, this binary had a lot more options but I ended up only using the change_plane_name function. In the end my solution is based in exploiting the IO_FILE_STRUCTURE, by abusing a negative index that allow us to modify STDOUT.
Preparing the binary to LD_PRELOAD
To preload this binary we need to use patchelf to use the ld given by the challenge:
$ git checkout release/2.29/master Updating files: 100% (12744/12744), done. Branch 'release/2.29/master'set up to track remote branch 'release/2.29/master' from 'origin'. Switched to a new branch 'release/2.29/master'
And why ? “puts” internally calls _IO_new_file_xsputn which eventually calls IO_OVERFLOW. Examining IO_OVERFLOW which its function is denoted by _IO_new_file_overflow and located at glibc/libio/fileops.c:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
int _IO_new_file_overflow (FILE *f, int ch) { if (f->_flags & _IO_NO_WRITES) /* SET ERROR */ { f->_flags |= _IO_ERR_SEEN; __set_errno (EBADF); return EOF; } /* If currently reading or no buffer allocated. */ if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0 || f->_IO_write_base == NULL) { ... truncated ... } if (ch == EOF) return _IO_do_write (f, f->_IO_write_base, // We want this f->_IO_write_ptr - f->_IO_write_base);
Eventually _IO_do_write will be called in this function. stdout->_flags & _IO_NO_WRITES is set to zero to avoid running some unnecessary code, we do the same for stdout->_flags & _IO_CURRENTLY_PUTTING.
_IO_new_file_overflow calls _IO_do_write with arguments as stdout, stdout->_IO_write_base and size of the buffer which is calculated via f->_IO_write_ptr - f->_IO_write_base.
From changelogs we know that _IO_do_write is defined as a macro for _IO_new_do_write:
staticsize_t new_do_write (FILE *fp, constchar *data, size_t to_do) { size_t count; if (fp->_flags & _IO_IS_APPENDING) /* On a system without a proper O_APPEND implementation, you would need to sys_seek(0, SEEK_END) here, but is not needed nor desirable for Unix- or Posix-like systems. Instead, just indicate that offset (before and after) is unpredictable. */ fp->_offset = _IO_pos_BAD; elseif (fp->_IO_read_end != fp->_IO_write_base) { off64_t new_pos = _IO_SYSSEEK (fp, fp->_IO_write_base - fp->_IO_read_end, 1); if (new_pos == _IO_pos_BAD) return0; fp->_offset = new_pos; } count = _IO_SYSWRITE (fp, data, to_do); // our aim ... truncated ... return count; }
The intention is to skip the else if block, to achieve this we need to make this true fp->_flags & _IO_IS_APPENDING, so we can set the right flags like this
All that we have to do is to set stdout->_flags to the value we calculated and partial overwrite stdout->_IO_write_base to make it point somewhere to get a leak.
Having libc we just need to find a way to get a shell, we can use IO_FILE structure again, but this time instead of entering IO_OVERFLOW we want to actually change its pointer and how we can do this? Each IO_FILE has a vtable that contains multiple saved pointers to functions like IO_OVERFLOW:
Let’s see the contents of IO_file_jumps vtable:
But IO_file_jumps is to far from the stdout, to actually change that pointer, it would require us to change a lot of things in memory, instead we can change the vtable pointer to IO_helper_jumps.
And yes vtables are writeable again in libc-2.29 for some reason:
Here is the call of IO_OVERFLOW at _IO_new_file_xsputn:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
size_t _IO_new_file_xsputn (FILE *f, constvoid *data, size_t n) { constchar *s = (constchar *) data; size_t to_do = n; int must_flush = 0; size_t count = 0; ... truncated ... if (to_do + must_flush > 0) { size_t block_size, do_write; /* Next flush the (full) buffer. */ if (_IO_OVERFLOW (f, EOF) == EOF) // We want to get control of this /* If nothing else has to be written we must not signal the caller that everything has been written. */ return to_do == 0 ? EOF : n - to_do; ... truncated ... return n - to_do; }