Challenge: Slot Machine · CTF: DalCTF 2026 · Category: Pwning · Difficulty: Easy
- Connection:
nc instancer.dalctf2026.com 24698 - Description: Yay Dal just installed a slot machine for all students! Maybe I can pay my tuition if I hit big!
TL;DR: A textbook
ret2win. The program usesgets()into a 32-byte stack buffer (classic unbounded read), there’s no stack canary and no PIE, and ajackpot()function literally reads and printsflag.txt. We smash the saved return address to redirect execution intojackpot(). The only twist is that the menu loop never returns on its own — so we close stdin to force an EOF, which breaks the loop and triggers the corrupted return.
Recon
We’re given two files: the binary slot_machine and its source slot_machine.c. Always start by understanding the program’s behavior and its protections.
Checksec
| |
This is a dream scenario for an attacker:
| Protection | Status | Why it matters |
|---|---|---|
| Stack canary | ❌ Disabled | We can overflow the saved return address without tripping a guard value. |
| PIE | ❌ Disabled (base 0x400000) | Function addresses are fixed and known — no leak required. |
| NX | ⚠️ Stack executable | Shellcode would even be possible, but we won’t need it. |
No canary + no PIE means: if we find a stack overflow, we can jump straight to any function we want at a hardcoded address.
Reading the source
The interesting parts of slot_machine.c:
| |
jackpot() is our win function — it reads the flag and prints it. Notice that nothing in the normal game flow ever calls it. The slot machine is rigged: roll() deliberately picks three distinct symbols every time (do { b = rand()... } while (b == a)), so you can never win legitimately. The intended path is to corrupt control flow and call jackpot() ourselves.
Now the vulnerability, in game_loop():
| |
cmd is a 32-byte stack buffer, and the program reads into it with gets(). gets() reads an entire line of input with no length limit — it will happily write past the end of cmd, over the saved RBP, and over the saved return address. This is the most classic stack overflow primitive there is. (Modern compilers even warn about it; gets() was removed from the C11 standard for exactly this reason.)
Building the exploit
Step 1 — Find the win address
The binary isn’t stripped, so we can just look the symbol up:
| |
jackpot() lives at 0x401206, and because PIE is disabled this address is valid at runtime.
Step 2 — Find the offset to the return address
Let’s look at the prologue of game_loop() to see where cmd sits relative to the saved frame:
| |
cmd is at rbp-0x20, i.e. 32 bytes below the saved RBP. The standard x86-64 stack frame looks like this:
| |
So the distance from the start of cmd to the saved return address is:
| |
Our payload is therefore 40 filler bytes, followed by the address we want to return to.
Step 3 — Stack alignment (the ret gadget)
There’s one subtlety that bites a lot of people on modern glibc. jackpot() calls printf(), and printf internally uses SSE instructions like movaps that require the stack to be 16-byte aligned. If the stack is misaligned when we land, the program crashes with a SIGSEGV inside libc instead of printing the flag.
To fix the alignment, we prepend a single bare ret gadget before the jackpot address. The extra ret consumes 8 bytes and shifts the stack alignment back into place before execution reaches jackpot.
Find one:
| |
Our final payload layout:
| |
Step 4 — The twist: forcing the loop to return
Here’s the catch that makes this challenge slightly more than a copy-paste ret2win.
The overflow happens inside game_loop(), but the saved return address is only used when game_loop() actually returns. The function is an infinite while (1) loop — after our overflowing input is processed, it just prints Unknown command and loops back to call gets() again. If we send another line, gets() overwrites cmd again and our corrupted return address… is still there, but we still never leave the loop.
So how do we make it return? Look at the loop condition:
| |
gets() returns NULL on EOF. When that happens, the loop breaks, game_loop() runs its epilogue (leave; ret), and that ret uses our corrupted saved return address.
Critically, when gets() hits EOF on an empty read it returns NULL without modifying the buffer, so our overwritten return address stays intact.
The plan:
- Send the overflow payload as one line. This corrupts the saved return address. The loop prints
Unknown commandand goes around again. - Close our side of the connection (send EOF). The next
gets()call returnsNULL, the loop breaks, andgame_loop()returns straight into ourret→jackpot().
In pwntools that’s p.shutdown('send') after sending the payload.
The exploit
| |
Run it (test locally with a dummy flag.txt first, then fire at the remote):
| |
Output:
| |
The Unknown command line is our overflow input being processed by the loop, and once we close stdin the loop breaks and jumps into jackpot(). Jackpot indeed.
Flag
| |
Takeaways
gets()is never safe. A single unbounded read is the entire vulnerability here. If you seegets()in a pwn challenge, it’s almost always the way in.- No canary + no PIE = ret2win. When function addresses are fixed and there’s no stack guard, overwriting the saved return address with a “win” function is the simplest possible exploit.
- Mind the alignment. Modern glibc’s
printf/putsusemovaps, which needs a 16-byte-aligned stack. A spareretgadget is the standard one-liner fix when your ret2win crashes inside libc. - Control flow ≠ overflow. The overflow only pays off when the vulnerable function actually returns. Recognizing that an EOF breaks the menu loop (and that
gets()leaves the buffer untouched on EOF) is what turns the overwrite into a working exploit.