- Published on
Cyhub CTF 2025 - Stack Guardian
- Authors

- Name
- Varik Matevosyan
- @D4RK7ET
Stack Guardian
For Cyhub CTF 2025, I created a classic PWN challenge that combines multiple exploitation techniques. Stack Guardian is a stack-based buffer overflow challenge with modern protections enabled, requiring players to chain together a format string vulnerability and ROP (Return-Oriented Programming) to achieve code execution. This challenge tested players' understanding of both modern binary protections and classic exploitation techniques.
Challenge Description
Players are given a service to connect to, and the goal is to exploit vulnerabilities in the binary to gain shell access and read the flag from /flag.txt.
Category: PWN
Difficulty: Medium
Flag Format: cyhub{xxxxx}
Challenge Overview
Stack Guardian implements a seemingly simple program that asks for a username and then accepts additional input. However, it contains two critical vulnerabilities that can be chained together:
- Format String Vulnerability: The program uses
printf(username)directly without format specifiers, allowing attackers to read from (and potentially write to) arbitrary memory locations - Buffer Overflow: A classic buffer overflow where 1024 bytes can be read into a 32-byte buffer
- Modern Protections: Stack canary, PIE, ASLR, and NX are all enabled, requiring a sophisticated multi-stage exploit
The challenge requires understanding how these protections work and how to bypass them systematically.
Vulnerabilities
Format String Bug (main.c:24)
The first vulnerability is straightforward but critical:
printf("Hi: ");
printf(username); // Direct use of user input as format string
By passing format specifiers like %llx as the username, players can read values from the stack. This becomes essential for leaking the stack canary and calculating address offsets.
Buffer Overflow (main.c:32)
The second vulnerability is a classic buffer overflow:
char nonono[32] = {0};
// ...
fgets(nonono, 1024, stdin); // Reading 1024 bytes into 32-byte buffer
This allows us to overwrite the return address, but we must first bypass the stack canary protection.
Protection Mechanisms
The challenge isn't just about finding vulnerabilities—it's about bypassing modern security mechanisms:
- Stack Canary: Enabled via
-fstack-protector, a random value placed before the return address that must remain unchanged - PIE: Position Independent Executable - all addresses are randomized at runtime
- ASLR: Address Space Layout Randomization - libc and stack addresses change on each execution
- NX bit: Stack is non-executable, preventing simple shellcode injection
Solution Walkthrough
Stage 1: Information Leakage
The first step is using the format string vulnerability to leak critical information:
Leak Stack Canary and PIE base:
payload = b'%45$llx|%49$llx' # Canary at 45th offset, main address at 49th
By carefully examining the stack layout (using GDB), we can identify that:
- The stack canary is at the 45th position
- A return address pointing to main is at the 49th position
Calculate PIE base:
elf.address = leaked_main - elf.sym['main']
Since PIE randomizes the base address but maintains relative offsets, we can calculate the actual base address by subtracting the known offset of main.
Stage 2: Libc Leak
With PIE defeated, we need to leak a libc address to defeat ASLR:
ROP chain to leak libc:
pop_rdi = rop.find_gadget(['pop rdi', 'ret']).address
rop_chain = pop_rdi + puts_got + elf.plt.puts + main_addr
This ROP chain:
- Pops the GOT address of
putsinto RDI - Calls
putsto print the actual libc address - Returns to main to give us another chance to exploit
Buffer overflow payload structure:
[padding (296 bytes)] + [canary (8 bytes)] + [saved rbp (8 bytes)] + [ROP chain]
The padding of 296 bytes was found through debugging - this is the distance from our buffer to the saved canary.
Stage 3: Code Execution
With the libc address leaked, we can now calculate the addresses of useful functions:
Calculate libc base:
libc.address = leaked_puts - libc.sym['puts']
Final ROP chain to spawn shell:
rop.call(libc.sym['system'], [bin_sh_address])
This calls system("/bin/sh") to give us a shell.
Stack alignment caveat:
Modern x86_64 requires 16-byte stack alignment before function calls. We need to add a ret gadget before the system call to ensure proper alignment:
ret_gadget = rop.find_gadget(['ret']).address
payload = ret_gadget + system_call
Key Technical Details
- Stack layout: Buffer is 296 bytes from the saved return address
- Format string offsets: Canary at
%45$llx, main return address at%49$llx - ROP gadgets: Uses
pop rdi; retgadget found in the binary'somg()function - Stack alignment: x86_64 calling convention requires 16-byte alignment
- Canary handling: Must preserve the exact canary value when overflowing
Tools Recommended
- pwntools - Essential Python library for exploit development
- GDB with pwndbg - For debugging and finding stack offsets
- ROPgadget - For discovering ROP gadgets
- checksec - To verify which protections are enabled
Solving the Challenge
Once the exploit is executed successfully:
- The format string leaks the canary and PIE base
- The first ROP chain leaks a libc address
- The second ROP chain spawns a shell
- Read the flag:
cat /flag.txt
The full exploit code can be found here