Stack overflow: how data overwrites return address
The stack in x86-64 grows from senior addresses to juniors. When you call the function, the processor creates a stack frame - a stack is placed on the stack return address (the address of the instruction where the management will return after the completion of the function), then the prologue saves RBP Instructions push rbp, puts a new frame (mov rbp, rsp) and allocates a place for local variables (sub rsp, N). This is true for assemblies without optimization (-O0), which is typical of CTF tasks. At -fomit-frame-pointer (default in GCC/Clang with -O1 and higher on x86-64) RBP may not be saved, and offset to return address will have to look otherwise. More information about our material about binary analysis of vulnerabilities.
And now the essence of exploitation. The local buffer is at the junior addresses, the return address is for the senior. The recording comes from the younger to the elders. The data is larger than the buffer holds – they consistently rub the saved RBP and return address. This is stack-based buffer overflow, CWE-121 according to the MITRE classification. What will be on the place of return address will determine the fate of the process: garbage - segfault; the address of the desired function is a controlled transition.
Instructions ret takes the value from the top of the stack and loads it into the RIP. Re-write the return address address win() - the processor will go there. In CTF tasks win() prints the flag from the file. Hence the name - ret2win.
According to MITRE ATT&CK, buffer overflow is at the heart of the Exploitation for Client Execution (T1203) and Exploitation for Privilege Escalation (T1068) techniques. At CTF, we learn mechanics; in real-world attacks, the same mechanics work for initial access or privilege enhancement. (Note: Atomic Red Team public tests for T1068 at the time of writing are only available for Windows, although the technique itself is platform independent.)
Environment for pwn-tasks: pwntools, GDB, checksec
We need Linux — Ubuntu, Debian, Kali — and three tools.
pwntools Python library for exploit automation. pip install pwntools (I recommend version ≥ 4.12.0, in earlier detected SSTI – GHSA-7xc5-ggpp-g249). Forming payload, packing addresses in a little-endian, interacting with a local process or a remote server is all through it. Without pwntools, you can write exploits, but each payload will have to be collected with your hands through struct.pack. On the second task it is already tired.
pwndbg – plugin for GDB. Cloned with GitHub, put by script setup.sh. Turns GDB from the text interface of the 1970s into a working tool: the context of the registers, the disassembled code around the current instruction, the contents of the stack are all on the same screen. Alternatives – GEF or PEDA, functions are similar, choice is a matter of taste. I use pwndbg because it has the most readable context output.
Checksec – utility from the pwntools package. checksec ./binary show what protections are included. The whole strategy depends on the result: there is a canary - you need a leak, there is a PIE - you need a leak address, there is nothing - direct ret2win.
It's still useful ROPgadget (pip install ROPgadget) to search for gadgets. For reverses, it is more difficult to get Ghidra (free) or IDA Pro. For the first ret2win is enough objdump -d and GDB.
Analysis of binary for operation buffer overflow
Downloaded the binary vuln. The first step is checksec ./vuln. We are interested in four lines:
Stack: No canary found – there is no canary, the stack is rewritten directly.
NX: NX enabled – stack is unperformable, shellcode on the stack does not start. For ret2win it doesn't matter.
PIE: No PIE – the addresses of the functions are fixed, win() will be at one address at each launch.
RELRO: Partial RELRO – partial protection of GOT, for a simple ret2win it does not matter.
The combination “No canary + No PIE” is the ideal conditions for ret2win. NX does not interfere: we do not put the shellcode on the stack, but we redirect the execution to the existing function.
Looking for a goal. objdump -t ./vuln | grep win show the address. In GDB - info functions for the full list. If the binary stripped (symbols removed - on CTF happens), these commands will not help. Then objdump -d with manual signature or Ghidra search. Let's say, win() sitting at the address 0x401156.
Typical vulnerable ret2win task code:
#include <stdio.h>
void win() { system("cat flag.txt"); }
void vuln() {
char buf[64];
gets(buf);
}
int main() { vuln(); return 0; }
gets() reads input without limitation of length – a classic of buffer overflow. The buffer buf occupies 64 bytes, behind it in the stack - saved RBP (8 bytes) and return address (8 bytes). We serve a line longer than 72 bytes - return address overwritten.
The stack in x86-64 grows from senior addresses to juniors. When you call the function, the processor creates a stack frame - a stack is placed on the stack return address (the address of the instruction where the management will return after the completion of the function), then the prologue saves RBP Instructions push rbp, puts a new frame (mov rbp, rsp) and allocates a place for local variables (sub rsp, N). This is true for assemblies without optimization (-O0), which is typical of CTF tasks. At -fomit-frame-pointer (default in GCC/Clang with -O1 and higher on x86-64) RBP may not be saved, and offset to return address will have to look otherwise. More information about our material about binary analysis of vulnerabilities.
And now the essence of exploitation. The local buffer is at the junior addresses, the return address is for the senior. The recording comes from the younger to the elders. The data is larger than the buffer holds – they consistently rub the saved RBP and return address. This is stack-based buffer overflow, CWE-121 according to the MITRE classification. What will be on the place of return address will determine the fate of the process: garbage - segfault; the address of the desired function is a controlled transition.
Instructions ret takes the value from the top of the stack and loads it into the RIP. Re-write the return address address win() - the processor will go there. In CTF tasks win() prints the flag from the file. Hence the name - ret2win.
According to MITRE ATT&CK, buffer overflow is at the heart of the Exploitation for Client Execution (T1203) and Exploitation for Privilege Escalation (T1068) techniques. At CTF, we learn mechanics; in real-world attacks, the same mechanics work for initial access or privilege enhancement. (Note: Atomic Red Team public tests for T1068 at the time of writing are only available for Windows, although the technique itself is platform independent.)
Environment for pwn-tasks: pwntools, GDB, checksec
We need Linux — Ubuntu, Debian, Kali — and three tools.
pwntools Python library for exploit automation. pip install pwntools (I recommend version ≥ 4.12.0, in earlier detected SSTI – GHSA-7xc5-ggpp-g249). Forming payload, packing addresses in a little-endian, interacting with a local process or a remote server is all through it. Without pwntools, you can write exploits, but each payload will have to be collected with your hands through struct.pack. On the second task it is already tired.
pwndbg – plugin for GDB. Cloned with GitHub, put by script setup.sh. Turns GDB from the text interface of the 1970s into a working tool: the context of the registers, the disassembled code around the current instruction, the contents of the stack are all on the same screen. Alternatives – GEF or PEDA, functions are similar, choice is a matter of taste. I use pwndbg because it has the most readable context output.
Checksec – utility from the pwntools package. checksec ./binary show what protections are included. The whole strategy depends on the result: there is a canary - you need a leak, there is a PIE - you need a leak address, there is nothing - direct ret2win.
It's still useful ROPgadget (pip install ROPgadget) to search for gadgets. For reverses, it is more difficult to get Ghidra (free) or IDA Pro. For the first ret2win is enough objdump -d and GDB.
Analysis of binary for operation buffer overflow
Downloaded the binary vuln. The first step is checksec ./vuln. We are interested in four lines:
Stack: No canary found – there is no canary, the stack is rewritten directly.
NX: NX enabled – stack is unperformable, shellcode on the stack does not start. For ret2win it doesn't matter.
PIE: No PIE – the addresses of the functions are fixed, win() will be at one address at each launch.
RELRO: Partial RELRO – partial protection of GOT, for a simple ret2win it does not matter.
The combination “No canary + No PIE” is the ideal conditions for ret2win. NX does not interfere: we do not put the shellcode on the stack, but we redirect the execution to the existing function.
Looking for a goal. objdump -t ./vuln | grep win show the address. In GDB - info functions for the full list. If the binary stripped (symbols removed - on CTF happens), these commands will not help. Then objdump -d with manual signature or Ghidra search. Let's say, win() sitting at the address 0x401156.
Typical vulnerable ret2win task code:
#include <stdio.h>
void win() { system("cat flag.txt"); }
void vuln() {
char buf[64];
gets(buf);
}
int main() { vuln(); return 0; }
gets() reads input without limitation of length – a classic of buffer overflow. The buffer buf occupies 64 bytes, behind it in the stack - saved RBP (8 bytes) and return address (8 bytes). We serve a line longer than 72 bytes - return address overwritten.