You're fuzzing a target binary, throwing progressively longer strings at an input it clearly wasn't built to handle, and it segfaults. Instant adrenaline: found it. You copy the exact input that crashed it straight into an exploit script, point it at the target expecting a shell, and run it.
Nothing happens. Or worse, it crashes differently this time. Or it doesn't crash at all.
This is the gap almost everyone hits the first time they try to move from "found a crash" to "got a shell," and it's usually not a hard technical wall, it's a missing step. A crash means you found an input the program can't handle. It says nothing yet about whether you control what happens next.
The first thing a crash actually tells you is close to nothing on its own. What you need is the exact offset: how many bytes into your input does the overflow start overwriting the saved return address specifically, as opposed to overwriting other stack data that happens to also cause a segfault. Guessing at this with trial and error, sending 100 bytes, then 150, then 200, is slow and imprecise. The reliable way is a cyclic pattern, a De Bruijn sequence where every 4 or 8-byte chunk is unique, generated with a tool like pwntools' cyclic(400). Send the whole pattern, let it crash, then read whatever ended up in the instruction pointer and run cyclic_find() on that value. It tells you the exact byte offset where your input starts controlling execution flow, not an estimate, the precise number.
Confirming that offset matters before doing anything else with it: overwrite exactly that many bytes with junk, then put a clean, recognizable marker right after it, and check that the crash now shows your marker sitting in the instruction pointer. If it does, you have real, confirmed control over where execution goes next. If it doesn't, you found the wrong offset and everything built on top of it will fail in confusing ways.
Confirmed control still isn't a working exploit. Next is bad character analysis: some byte values never make it through in one piece. A null byte will silently truncate anything read with something like strcpy. Newline and carriage return bytes get mangled by input handling that treats them as line terminators. Skip this check and your carefully placed shellcode gets corrupted in transit, and the resulting crash looks identical to a bad offset, which is exactly why people get stuck here for hours convinced they've miscounted bytes that were actually counted correctly.
Then there's what protections the binary was actually built with, because this decides your whole strategy from here. Is the stack executable, or is NX/DEP on? If NX is off, once you control the instruction pointer you can point it at your own shellcode sitting on the stack, usually via a jmp esp or call esp gadget address pulled from a non-randomized module. If NX is on, jumping to stack-resident shellcode just segfaults again, and the actual path forward is return-oriented programming or a ret2libc chain, stitching together existing executable code already loaded in the process instead of injecting new code at all. ASLR changes the picture again: with it on, hardcoded addresses from one run are wrong on the next, and the exploit needs either an info leak or a target that disables or doesn't randomize the piece you're relying on.
None of this is one trick. It's a sequence: exact offset, confirmed control, bad character map, protections identified, then the technique that actually fits what the binary allows. Skip any one step and the failure looks identical to every other failure, a silent nothing or a different crash, which is exactly why "I already found the crash, why doesn't this work" is such a common and such a frustrating place to get stuck.
Codelivly's Exploit Development Book walks through that full chain end to end: precise offset-finding, bad character analysis, and building working exploits against both unprotected and protected binaries, the part that turns a crash into a shell instead of just a more detailed crash report.
Top comments (0)