You found the crash. EIP is 0x41414141, the pattern offset checks out, and the exploit behaves perfectly under the debugger. Then you run it against the real target and get a segfault inside libc instead of a shell. That gap between "crashes the way I want" and "spawns a shell" is where most exploit development time actually goes — and it is almost always caused by the same handful of mistakes.
The classic version looks like this:
for (int i = 0; i <= len; i++) buf[i] = src[i];
One extra iteration. The result is a single 0x00 landing just past the buffer, usually on the least significant byte of a saved frame pointer or a saved return address. It is easy to miss in a hexdump because everything else looks correct, and easy to underestimate because a single byte is rarely enough to build a full ROP chain.
But a single byte is often enough to do real damage. On x86-64, zeroing the low byte of a saved RBP turns leave; ret into a partial stack pivot, which can hand you control of the stack pointer even when you never touched RIP directly. In length-prefixed protocols, the same byte can clear a size field and create a much larger overflow on the next read.
The practical fix is measurement, not intuition. Send a cyclic pattern every time instead of a run of "A"s, and use a pattern generator to get the exact offset. Then inspect the saved frame with a debugger rather than reading the dump by eye. If you only control one byte, ask what one byte actually changes — a frame pointer, a flags field, a length — before deciding the bug is unusable.
Shellcode that works in a test harness frequently dies in transit. The usual suspects:
Do not guess this. Send a buffer containing every byte from 0x01 to 0xff, break after the copy, and compare the memory byte for byte with what you sent. Note every difference. Then re-run the test on the real transport, because a WAF, a proxy, or a JSON encoder may strip bytes that the vulnerable function itself accepts.
The second half of the problem is size. Encoders produce padding and checksums, and an encoded payload can be several times larger than the original. If the overflow is tight, your options are a smaller payload, a staged loader, or an egghunter — not a bigger buffer that does not exist.
The System V ABI on x86-64 expects RSP to be 16-byte aligned at the moment a function is called. Compilers rely on this and emit instructions like movaps that fault immediately on a misaligned address. If your ROP chain leaves the stack off by eight bytes, execution may reach your target function, run a few instructions, and die inside a perfectly innocent libc routine.
The signature is telling: the exploit works when launched one way and crashes another way, with no change to the payload. Common triggers are a different number of environment variables, a longer argv[0], a different working directory, or a wrapper script such as sudo or systemd.
If the crash is inside strlen, printf, or a system call wrapper rather than at your return address, suspect alignment before you suspect your gadgets.
The fix is usually a single ret gadget inserted before the call. It costs nothing, shifts RSP by eight, and realigns the frame. If that is not enough, look for a short stack adjustment gadget, or call a function variant that tolerates a misaligned frame. Also remember that gdb disables ASLR by default — verify addresses outside the debugger before trusting them.
Hardcoded addresses are the most common source of "it worked yesterday." ASLR, the size of the environment block, the length of the program path, and the contents of argv all shift the stack in ways that are invisible until the target changes.
Three approaches, in rough order of reliability:
Do not hardcode a stack address because it worked in one session. Reproduce the target environment as closely as you can — same shell, same user, same startup path — and test repeatedly.
Reliability is a separate engineering problem from control. A payload that succeeds once in twenty attempts is not a working exploit. Build a loop that runs it a hundred times and counts successes, and test it over the real network path rather than on localhost, because packets get split and reads return partial data.
Two failure modes are worth naming. First, shellcode that spawns a shell on a socket without duplicating the file descriptors to 0, 1, and 2 will produce a shell that exits the instant it reads EOF. Second, a payload that depends on timing — a race window, a sleep, a blocking recv — will fail under load even when it passes in isolation. Add retries and generous waits, then tighten them once the stage is stable.
Finally, confirm what the binary actually allows. NX, ASLR, stack canaries, CET, and seccomp change the entire approach, and a classic ret2stack chain simply will not run against a non-executable stack. Check the protections before writing a single gadget.
Before calling an exploit finished, walk through this list. It catches the majority of problems above in a few minutes:
Keep this workflow on a second monitor, and test only against systems you own or are explicitly authorized to assess. Most "unsolvable" bugs turn out to be one of these seven mistakes wearing a disguise.
Photo: Daniil Komov / Pexels