Memory Safety, Buffer Overflows & Baggy Bounds
A unified technical reference for system programmers, security engineers, and reverse engineers — from overflow fundamentals through allocator internals, heap exploitation, and live GDB debugging workflows.
A buffer overflow occurs when a program writes more data into a memory region than it was allocated to hold. Languages such as C and C++ expose raw memory through pointer arithmetic and provide no inherent bounds enforcement — making unsafe writes invisible at runtime unless explicit instrumentation is present.
Classic stack-based exploitation begins with a fixed-size buffer allocated on the stack inside a called function. When unchecked input arrives via APIs such as gets() or strcpy(), excess bytes overflow the buffer and overwrite adjacent stack metadata — including the saved frame pointer and the return address. By controlling the return address, an attacker redirects execution to injected shellcode or reusable code sequences already present in the binary.
*** stack smashing detected *** before the corrupted return address executes.No single mitigation eliminates memory corruption. Each defense addresses a specific attack vector while leaving others exposed. Understanding the limits of each control is as important as deploying it.
| Mitigation | What It Protects | Bypass / Limit |
|---|---|---|
| Stack Canaries | Return address overwrite on stack | Does not protect heap metadata. Bypassable with information leak of canary value. |
| Non-Executable Memory (NX/DEP) | Code injection via stack/heap shellcode | Does not prevent ROP (Return-Oriented Programming) — attacker reuses existing code. |
| ASLR | Predictable addresses for gadgets, libc, heap | Entropy is limited. Bypassable via format string leaks, side channels, or heap spraying. |
| Control Flow Integrity (CFI) | Indirect control-flow hijacking | Overhead and deployment complexity. Does not protect data-only attacks. |
| Safe APIs (strncpy, snprintf) | Common unsafe function calls | Requires disciplined developer adoption across all code paths. Legacy code resists. |
| Memory-Safe Languages (Rust, Go) | Most classes of memory corruption | Impractical for performance-critical, hardware-adjacent, or legacy code paths. |
| AddressSanitizer | Heap/stack out-of-bounds, use-after-free | 2–3× slowdown. Not suitable for production. Changes memory layout; can mask bugs. |
Guard Pages (Electric Fence)
Unmapped pages placed adjacent to allocations cause an immediate segfault on overflow. Highly effective for debugging — every overflow is caught at the exact offending instruction. Too memory-intensive for production: each allocation requires a full page of guard space.
Fat Pointers
Bounds metadata (base, size) is stored inside the pointer itself, enabling exact per-pointer bounds checking at every dereference. Requires ABI changes — fat pointers are wider than regular pointers, breaking interoperability with unmodified libraries and system interfaces. Performance overhead on every pointer operation.
Shadow Metadata (Baggy Bounds, ASan)
Bounds information is stored in a separate, compact side table indexed by memory address or slot number. Avoids ABI changes — existing pointer widths are preserved. Checking uses constant-time table lookups. Granularity is coarser than fat pointers (slot-sized, not byte-exact), introducing some internal fragmentation.
Baggy Bounds (Akritidis et al., USENIX Security 2009) rounds every allocation up to the nearest power of two and aligns the block accordingly. A compact side table stores the logarithm of the block size for each slot — one byte per slot, enabling O(1) lookup via bit shifts and masks.
Key Tradeoffs
| Property | Baggy Bounds | Fat Pointers | ASan Shadow |
|---|---|---|---|
| ABI compatibility | ✅ Unchanged | ❌ Incompatible | ✅ Unchanged |
| Check cost | ✅ O(1) bitwise | ⚠️ Per-deref | ⚠️ Shadow lookup |
| Granularity | ⚠️ Power-of-2 slot | ✅ Exact byte | ✅ 8-byte aligned |
| Memory overhead | ⚠️ Fragmentation | ❌ 2–3× pointer size | ❌ 1:8 shadow ratio |
| Production-safe | ✅ Yes | ❌ Overhead too high | ❌ 2–3× slowdown |
info proc mappings output in §12. ASLR randomizes the gaps between segments at each exec().When malloc() is called, the allocator selects a strategy based on size. Small allocations come from thread-local caches (tcache in glibc) or size-class bins. Large allocations use mmap(). Each allocated chunk stores metadata in the bytes immediately preceding the returned user pointer — making adjacent overflows directly dangerous to allocator state.
| Operation | Behavior | Overlap Safe? | Notes |
|---|---|---|---|
memcpy(dst, src, n) | Copies n bytes src→dst | ❌ UB on overlap | Fastest; compiler may emit SIMD. Never use on overlapping regions. |
memmove(dst, src, n) | Copies n bytes, handles overlap | ✅ Yes | Detects and handles forward/backward copy direction. |
realloc(ptr, size) | Resize allocation | N/A | May allocate new block, copy, free old. Old pointer becomes invalid — use-after-free risk. |
mremap(old, old_sz, new_sz) | Kernel: resize VM region | N/A | May avoid copy by remapping page tables. Syscall only. |
realloc(), the original pointer is invalidated — even if the address did not change. Any code retaining the old pointer commits use-after-free.
glibc — ptmalloc2
Each chunk has a header storing size and state flags in the 8–16 bytes preceding the user data pointer. Free chunks are stored in doubly-linked bins sorted by size. Historically, corrupted list pointers in free chunks enabled "unlink attacks" — arbitrary writes during the coalescing operation. Modern glibc adds pointer integrity checks, but metadata corruption remains a powerful primitive when those checks can be bypassed.
/* 8 bytes */ prev_size // size of previous chunk (if free) /* 8 bytes */ size // size of THIS chunk | flags (P=prev_in_use, M=mmap, A=arena) /* user data */ <returned pointer starts here> /* (if free) */ fd, bk // forward/backward links in free bin /* (if free) */ fd_nextsize, bk_nextsize // for large bin skip lists
jemalloc
Separates metadata from user data via extents and runs. Uses multiple arenas for concurrency, fine-grained size classes, and thread caches. Profile-friendly: built-in allocation profiling via MALLOC_CONF=prof:true. Less vulnerable to traditional heap overflows because metadata is not adjacent to user data.
Hardened Allocators (Scudo, dlmalloc-hardened)
Add integrity-protected chunk headers (checksummed), randomized allocation ordering, and stack canary-like values inside chunk metadata. Dramatically reduce exploitability of heap corruption — attackers cannot reliably predict or forge the protected header values without a separate leak.
| Technique | Mechanism | Outcome |
|---|---|---|
| Unlink Attack | Forge prev/next pointers in a free chunk's fd/bk fields | Arbitrary write during free() coalescing |
| Off-by-One | Write one byte past allocation boundary, corrupt PREV_INUSE flag or size of next chunk | Overlapping chunks; controlled allocation reuse |
| Double Free | Free same pointer twice, inserting chunk twice into free list | Next malloc returns same address twice; write-after-free |
| Tcache Poisoning | Corrupt tcache next pointer after freeing chunk | malloc() returns arbitrary address; write primitive |
| Heap Feng Shui | Shape heap layout via controlled allocation/free patterns so target chunk ends up adjacent to attacker-controlled data | Deterministic corruption of target object |
| Use-After-Free | Access freed chunk after allocator reuses its memory | Type confusion, stale pointer reads, arbitrary write if attacker controls reallocated data |
# Compile with ASan instrumentation gcc -fsanitize=address -fsanitize=undefined -g -O1 -o target target.c # Run — ASan intercepts the process and reports: # ==PID==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x… # READ of size 4 at 0x… thread T0 # #0 0x… in vulnerable_function target.c:42 ./target
# Full error detection with call stacks valgrind --leak-check=full --show-leak-kinds=all \ --track-origins=yes --verbose ./target # Track file descriptors too valgrind --track-fds=yes ./target
# Break on malloc/free to trace allocations break malloc break free commands 1 silent printf "malloc(%ld) = %p\n", $rdi, $rax continue end # Watchpoint — halt when specific memory is written watch *0x555555559010 # chunk header address # pwndbg — heap state visualization heap # list all chunks bins # show tcache/fastbins/smallbins vis_heap_chunks # color-coded heap map arena # main_arena struct # Inspect chunk header manually x/4gx 0x555555559000 # prev_size, size, fd, bk
- Minimize the reproducing test case — smallest input that reliably triggers the crash
- Run under AddressSanitizer — catches most heap/stack issues with precise attribution
- Run under Valgrind if ASan unavailable or doesn't catch it — slower but more exhaustive
- Break on malloc/free in GDB — build a timeline of all allocations and frees
- Set watchpoints on chunk headers — catch the exact write that corrupts metadata
- Inspect tcache lists for duplicates, impossible pointers, or out-of-range values
- Examine free list bins — coalescing bugs show up as corrupted fd/bk pointers
- Run a fuzzer (AFL++, libFuzzer) against the component — surface hidden paths
- Goal: identify the corrupting instruction, not just the downstream crash site
Scenario 1 — Crash Inside free()
Symptom: *** Error in 'a.out': free(): invalid pointer *** or double free or corruption
break free run # When free() is hit, inspect the pointer in $rdi: info registers rdi x/4gx ($rdi - 16) # examine chunk header: prev_size, size flags # If size field has garbage: a previous write corrupted this chunk's header # Set watchpoint on the header to find who wrote it: watch *($rdi - 8) run # re-run; GDB will stop at the corrupting instruction
Scenario 2 — Stack Buffer Overflow (our GDB session case)
Symptom: *** stack smashing detected ***: terminated + SIGABRT
break vulnerable_function # break before overflow run info frame # show saved RIP, saved RBP x/64x $rsp # dump stack — look for 0x41414141 fill pattern # Note where 0x41 bytes stop: offset from RSP = required padding to reach return address watch *(char*)($rbp + 8) # watchpoint on return address slot continue
Scenario 3 — Use-After-Free
# Step 1: ASan identifies UAF immediately with allocation/free/access stacks gcc -fsanitize=address -g -O0 -o target target.c && ./target # ==PID==ERROR: AddressSanitizer: heap-use-after-free # READ of size 8 at 0x... — this is the stale pointer access # freed by thread T0 here: — this is the free() # previously allocated by thread T0 here: — this is the malloc() # Step 2: In GDB without ASan, poison freed memory to catch stale pointers break free commands memset($rdi, 0xde, <size>) # fill freed region with 0xde = dead sentinel continue end # Crash will now occur much closer to the stale access
Scenario 4 — Tcache Poisoning / Double Free
# After suspected double free: tcache_entries # pwndbg: list all tcache bins and their entries # Look for: same address appearing twice in the same bin # Look for: next pointer pointing outside heap range # Look for: count field mismatching actual number of entries break __libc_malloc commands printf "malloc(%ld) returning %p\n", $rdi, $rax continue end # If malloc returns 0x0 or an invalid address: tcache is poisoned
Scenario 5 — Crash After realloc()
break realloc run # Save the old pointer before call: set $old_ptr = $rdi # After realloc returns ($rax = new pointer): finish set $new_ptr = $rax # Set watchpoint on old pointer to catch any post-realloc access: watch -l *$old_ptr continue # Any access to old_ptr after this point is a use-after-realloc bug
buffer_overflow binary. The binary was compiled without debug symbols (stripped). Annotations explain what each output reveals.
$ gdb ./buffer_overflow Reading symbols from ./buffer_overflow... (No debugging symbols found in ./buffer_overflow) ↑ Binary is stripped — no function names or line numbers available. This limits GDB visibility but we can still inspect registers, stack, and memory. (gdb) run Starting program: /root/buffer_overflow [Thread debugging using libthread_db enabled]
AAAAAAAAAA…AAAAAAAAA ← 700+ bytes of 'A' (0x41) fed to stdin *** stack smashing detected ***: terminated ↑ Stack canary was corrupted. glibc's __stack_chk_fail() raised SIGABRT before the corrupted return address could execute. This is the canary protection working as designed. Program received signal SIGABRT, Aborted. __pthread_kill_implementation (...) at ./nptl/pthread_kill.c:44 ↑ We are paused inside the SIGABRT handler — deep in pthread/libc internals. The actual overflow site is much earlier in the call stack.
(gdb) info proc mappings 0x555555554000–0x555555555000 r--p ← ELF headers (read-only) 0x555555555000–0x555555556000 r-xp ← .text: executable code 0x555555557000–0x555555558000 r--p ← .rodata: read-only data 0x555555558000–0x555555559000 rw-p ← .data / .bss: writable globals 0x555555559000–0x55555557a000 rw-p ← [heap] ~132KB, grows upward 0x7ffff7c00000–0x7ffff7dbd000 r-xp ← libc.so.6 text (no write, no exec for data pages) 0x7ffff7fc1000–0x7ffff7fc3000 r-xp ← [vdso]: kernel-provided fast syscalls 0x7ffffffde000–0x7ffffffff000 rw-p ← [stack]: ~132KB, grows downward 0xffffffffff600000 --xp ← [vsyscall]: legacy compat page Note: NX is active — no segment is both writable AND executable. This prevents direct shellcode injection into the stack or heap.
(gdb) info registers rax 0x0 ← syscall return value (0 = pending) rdi 0x1740f ← thread ID (pthread_kill arg) rsi 0x1740f ← also thread ID rdx 0x6 ← signal number 6 = SIGABRT rbp 0x1740f ← CORRUPTED: RBP should be a stack address (0x7fff…), not 0x1740f ↑ We are deep in libc signal handling — this is not the overflow frame's RBP rsp 0x7fffffffdf60 ← current stack pointer (in signal handler frame) rip 0x7ffff7c969fc ← executing inside __GI___pthread_kill+300 (libc) r12 0x6 ← signal number preserved
(gdb) x/64x $rsp ↑ Dump 64 dwords starting at current stack pointer 0x7fffffffe2f0 → 0x7fffffffe560: ← region filled with 'A' bytes 0x41414141 0x41414141 0x41414141 0x41414141 ← AAAA AAAA AAAA AAAA 0x41414141 0x41414141 ... ← 700+ bytes of attacker input ... 0x7fffffffe570: 0x00414141 0x00007fff ← fill ends here (null terminator from input) ↑ 0x00007fff = environment pointer — not part of overflow The overflow started at 0x7fffffffe2f0 (buf[]) and grew upward. Offset from RSP to the 0x41 region = return address + saved RBP were overwritten before the canary check fired. Beyond the fill: ASCII-encoded environment variables (PATH, SHELL, TERM…) visible as readable strings at higher stack addresses — useful for ROP gadget leaks.
(gdb) break main Breakpoint 3 at 0x555555555219 ← main+8 (entry point after prologue) (gdb) info breakpoints Num Type Enb Address What 1 breakpoint y <PENDING> point ← function "point" not found (stripped binary) 3 breakpoint y 0x555555555219 <main+8> With stripped binaries: - Use: break *0x<address> instead of break function_name - Use: disassemble $pc, +100 to read instructions around crash - Use: x/20i 0x555555555219 to examine main's disassembly
GDB Essentials
| Command | Purpose |
|---|---|
info proc mappings | Print full virtual memory map — all segments, permissions, file backing |
info registers | All general-purpose registers + flags at current execution point |
info registers rsp | Single register — quick stack pointer check |
x/64x $rsp | Hex dump: 64 dwords from RSP — find overflow fill pattern |
x/20i $pc | Disassemble 20 instructions from current IP — works on stripped binaries |
info frame | Current stack frame: saved RIP, saved RBP, local variable sizes |
backtrace | Call stack at time of crash |
watch *address | Hardware watchpoint — halt when that memory address is written |
break *0xaddr | Set breakpoint at raw address (essential for stripped binaries) |
finish | Run until current function returns; show return value in RAX |
set $var = value | Define convenience variable (save pointer for later comparison) |
ASan Flags
| Flag | Effect |
|---|---|
ASAN_OPTIONS=detect_leaks=1 | Enable LeakSanitizer integration (default on Linux) |
ASAN_OPTIONS=halt_on_error=0 | Continue after first error — find all bugs in one run |
ASAN_OPTIONS=log_path=/tmp/asan.log | Write ASan report to file instead of stderr |
-fsanitize=address,undefined | Combine ASan + UBSan — catches integer overflow, null deref, misaligned access |
-g -O1 | Debug symbols + light optimization — best ASan call stack quality |
Valgrind Options
| Option | Effect |
|---|---|
--leak-check=full | Full leak report including indirect/reachable leaks |
--show-leak-kinds=all | Include definitely/indirectly/possibly lost |
--track-origins=yes | Show where uninitialized values were created (high overhead) |
--error-limit=no | Don't stop after 100 errors |
--gen-suppressions=all | Generate suppression file for known false positives |
Stack Frame Buffer Layout
When a function is called, the compiler allocates a stack frame. Buffers live at the lowest addresses (highest numeric distance from RSP in typical layouts). An overflow grows toward the canary, saved RBP, and finally the return address.
Figure: Stack frame layout — overflow grows from buf[] upward through canary → RBP → return address
Heap Chunk Topology (glibc ptmalloc)
Every malloc'd region is wrapped in a chunk header. Corrupting the size field or fd/bk pointers gives an allocator primitive — read or write anywhere in the process.
Figure: ptmalloc chunk — user pointer is 16 bytes past chunk start; fd/bk overlay user data when freed
Launch & Attach
# Load binary gdb ./target # Load with arguments gdb --args ./target arg1 arg2 # Attach to running process gdb -p <PID> # Open core dump gdb ./target core # Batch mode — run script and exit (no interactive prompt) gdb -batch -ex "run" -ex "bt" ./target # With init file override gdb -x /path/to/script.gdb ./target # Quiet startup (suppress banner) gdb -q ./target # Pass stdin payload gdb -q ./target <<< $(python3 -c "print('A'*300)")
Breakpoints — Full Syntax
# By function name break main break vulnerable_function # By raw address (required on stripped binaries) break *0x555555555219 # By file and line break target.c:42 # Temporary breakpoint (auto-deletes after first hit) tbreak main # Conditional breakpoint break malloc if $rdi > 1024 break target.c:42 if i == 0 # Regex breakpoint — set on every function matching pattern rbreak ^str.* # breaks on strcat, strcpy, strlen, etc. # Break on every function in a file rbreak target.c:. # Disable / enable / delete info breakpoints # list all — shows Num column disable 2 # disable breakpoint #2 enable 2 delete 2 # permanently remove delete # delete ALL breakpoints (confirms) # Skip N hits before stopping ignore 1 10 # ignore breakpoint #1 for next 10 hits # Run commands automatically when breakpoint fires break malloc commands 1 # enter command sequence for BP #1 silent printf "malloc(%ld) → %p\n", $rdi, $rax continue end # Add condition to existing BP condition 1 $rdi == 512
Execution Control
run # start/restart from beginning run arg1 arg2 # with arguments run < /tmp/payload.bin # redirect stdin from file continue (or c) # resume until next BP/signal step (or s) # source line step — enters functions next (or n) # source line step — steps OVER function calls stepi (or si) # single machine instruction — enters calls nexti (or ni) # single machine instruction — steps over calls finish # run until function returns; prints return value until addr # run until reaching address (useful for loops) jump *0xaddr # move RIP to address without executing (skip code) signal SIGINT # inject signal into process kill # kill inferior (stop without exiting GDB) quit # exit GDB # Reverse execution (requires: target record) record # begin recording execution for replay reverse-continue (rc) # run backward to previous BP reverse-step (rs) # one source line backward reverse-stepi (rsi)# one instruction backward reverse-next (rn) # one source line backward, step over
Memory Examination — x Command Deep Dive
# Syntax: x/NuF where N=count u=unit F=format # Units: b=byte h=halfword(2) w=word(4) g=giant(8) # Format: x=hex d=decimal u=unsigned o=octal t=binary s=string i=instruction a=address c=char x/64xb $rsp # 64 bytes in hex — byte-by-byte x/32xw $rsp # 32 dwords (4 bytes each) in hex x/16xg $rsp # 16 qwords (8 bytes each) in hex x/4gx $rbp-0x20 # 4 qwords starting at rbp-32 x/s 0x555555557000 # print as C string x/20i $pc # disassemble 20 instructions from RIP x/20i 0x555555555219 # disassemble from absolute address x/i $pc # just the current instruction x/4gx ($rdi-16) # chunk header (prev_size, size, fd, bk) — malloc pointer x/40xb $rsp # find null terminator after overflow fill x/64xg 0x555555559000 # raw heap region scan # Examine after last x command — "." repeats with next address x/16gx 0x7ffff7c00000 # start of libc — scan for ROP gadgets # Search memory for a byte pattern (requires pwndbg or GDB 7.5+) find /b 0x555555554000, 0x555555558000, 0x41, 0x41, 0x41, 0x41 # search for a string find /s 0x7ffffffde000, 0x7ffffffff000, "AAAA"
Registers & Stack Inspection
info registers # all general-purpose + eflags info registers rsp rip rbp # just these three info registers eflags # flags register (ZF CF OF SF) info all-registers # includes SSE/AVX/FP registers info frame # current frame: saved RIP, RBP, size info args # function arguments (requires symbols) info locals # local variables (requires symbols) backtrace (bt) # call stack backtrace full # call stack + locals at each frame backtrace 5 # top 5 frames only frame 3 # switch to frame #3 up # move to caller frame down # move to callee frame # Modify a register set $rip = 0x555555555219 # redirect execution set $rsp = $rsp - 8 # adjust stack pointer set $rax = 0 # override return value # Print register value as address symbol info symbol $rip # resolve address to nearest symbol
Watchpoints & Catchpoints
# Watchpoints — stop when memory is accessed watch *0x555555559010 # stop on WRITE to this address rwatch *0x555555559010 # stop on READ awatch *0x555555559010 # stop on READ or WRITE watch -l *$rdi # watch location (follows pointer if it moves) watch buf[0] # watch named variable (requires symbols) watch *($rbp - 0x8) # watch canary slot # Catchpoints — stop on events catch syscall # stop on EVERY syscall catch syscall write # stop only on write(2) catch syscall mmap brk # memory allocation syscalls catch throw # C++ exceptions thrown catch catch # C++ exceptions caught catch exec # stop when execve() is called catch fork # stop when fork() is called catch signal SIGSEGV # stop on segfault before handler catch signal all # stop on any signal # Signal handling config handle SIGSEGV stop print # stop and print on segfault (default) handle SIGPIPE nostop pass # ignore SIGPIPE, pass to program info signals # show all signal handling config
Disassembly & Symbols
# Disassemble disassemble main # named function (needs symbols) disassemble $pc, +64 # 64 bytes from current IP disassemble 0x555555555219, 0x555555555280 disassemble /r main # include raw hex bytes disassemble /m main # interleave source lines x/20i $pc # works on stripped — no symbol needed # Set disassembly flavor set disassembly-flavor intel # Intel syntax (default AT&T is confusing) set disassembly-flavor att # Symbol resolution info functions # list all known functions info functions malloc # filter by name info variables # global/static variables info symbol 0x7ffff7c4b6d0 # resolve address → symbol name info address malloc # resolve symbol → address # PLT / GOT inspection (pwndbg) plt # print PLT entries got # print GOT entries + resolved addresses # Examine PLT manually x/3i 0x555555555090 # PLT stub: jmp [GOT], push index, jmp resolver x/gx 0x555555558020 # GOT entry — address after first resolution
Convenience Variables & Expressions
# Define variables set $buf = $rsp + 0x10 set $canary = *((long*)($rbp - 0x8)) set $retaddr = *((long*)($rbp + 0x8)) # Print expressions print $retaddr # print value print /x $retaddr # in hex print (int)$rax # cast to int print (char*)$rdi # interpret as string pointer print sizeof(long) # 8 on x86-64 # Arithmetic print $rbp - $rsp # frame size in bytes print 0x41414141 << 32 | 0x41414141 # Modify memory directly set *((int*)0x555555559010) = 0xdeadbeef set {char[8]}$rsp = "BBBBBBBB" # Call functions mid-session call (int) printf("rsp=%p\n", $rsp) call malloc(256) # allocate and see address returned call system("id") # inject command if NX bypassed
Finding Offset to RIP (Exploit Development)
# Step 1: generate cyclic pattern (pwntools) python3 -c "from pwn import *; print(cyclic(400))" > /tmp/pattern.txt # Step 2: run in GDB with pattern as input gdb -q ./target (gdb) run < /tmp/pattern.txt # Step 3: crash — check what landed in RIP / RSP (gdb) x/gx $rsp # value of [RSP] at crash = overwritten ret addr (gdb) info registers rip # or check RIP directly # Step 4: find offset python3 -c "from pwn import *; print(cyclic_find(0x6161616c))" # → offset # Alternative: msf pattern (Metasploit not required — script it) python3 -c " import string chars = string.ascii_letters + string.digits pat = ''.join(chars[i%len(chars)] for i in range(400)) print(pat) " > /tmp/msf_pattern.txt
# After crash on stack smash — we are in SIGABRT handler # Go to frame where overflow happened (gdb) backtrace # find the vulnerable function frame # (gdb) frame 5 # switch to it (gdb) info frame # shows: saved rip = 0x4141414141414141 # Compute: offset = &saved_rip - buf_start (gdb) x/gx $rbp+8 # saved RIP slot address (gdb) print $rbp + 8 - <buf_addr> # = number of bytes to reach RIP # Verify offset python3 -c " from pwn import * p = process('./target') payload = b'A' * <offset> + p64(0xdeadbeefcafebabe) p.sendline(payload) p.wait() "
ASLR & Library Base Leaks
# Disable ASLR for GDB session only (easier testing) (gdb) set disable-randomization on # default in GDB (gdb) set disable-randomization off # test with ASLR on # Find libc base in running process (gdb) info proc mappings # look for libc-2.35.so — the r-xp segment start address = libc base # Compute offset to target gadget # libc_base = 0x7ffff7c00000 (from mappings) # system@libc = base + offset_from_readelf python3 -c " import subprocess, re out = subprocess.check_output('readelf -s /lib/x86_64-linux-gnu/libc.so.6', shell=True).decode() for line in out.splitlines(): if ' system@@' in line or ' system@' in line: print(line) " # system offset → add to libc_base → call address # Find /bin/sh string in libc (gdb) find 0x7ffff7c00000, 0x7ffff7dbd000, "/bin/sh" # → 0x7ffff7d8c678 (use as RDI in ret2libc)
ROP Chain Validation
# Find gadget addresses with ROPgadget (external tool) ROPgadget --binary ./target --rop | grep "pop rdi" # → 0x000000000040118b : pop rdi ; ret # Confirm gadget in GDB (gdb) x/3i 0x40118b # 0x40118b: pop %rdi # 0x40118c: ret # Set RIP to gadget manually and step through (gdb) break *0x40118b (gdb) set $rip = 0x40118b (gdb) set $rsp = <stack_containing_rop_chain> (gdb) stepi # pop rdi — watch $rdi change (gdb) stepi # ret — watch $rip = next gadget # Inspect full ROP chain on stack (gdb) x/20gx $rsp # each 8-byte entry = one gadget/value
GDB Scripting & .gdbinit Automation
# ~/.gdbinit — runs on every GDB start set disassembly-flavor intel set print pretty on set print array on set pagination off # no "--More--" prompts set confirm off # no "delete all breakpoints?" prompt set follow-fork-mode child # follow child process on fork # Define a custom command define hexdump x/64xb $arg0 end # Usage: hexdump $rsp define stackdump x/32gx $rsp info frame end # Hook — run automatically before every step/next define hook-stop x/i $pc end # Python scripting inside GDB python import gdb def on_stop(event): frame = gdb.selected_frame() pc = frame.pc() print(f"[stop] RIP=0x{pc:016x}") gdb.events.stop.connect(on_stop) end
pwndbg / GEF Extensions
# Install pwndbg git clone https://github.com/pwndbg/pwndbg && cd pwndbg && ./setup.sh # Heap visualization heap # all malloc chunks in main arena heap --all # including all arenas bins # tcache, fast, small, large, unsorted bins tcache # tcache bins only with counts vis_heap_chunks # color-coded ASCII heap map vis_heap_chunks --resolve # resolve pointers to symbols arena # main_arena internal struct malloc_chunk 0x555555559010 # parse one chunk's header # Memory & mappings vmmap # virtual memory map (prettier than info proc) vmmap libc # filter to libc mappings only search -x 41414141 # search all writable regions for 0x41414141 search -s "/bin/sh" # find string in memory # Binary info checksec # NX, PIE, RELRO, canary, FORTIFY state got # GOT entries + current resolved addresses plt # PLT stubs rop # ROP gadgets in binary # Exploit helpers cyclic 200 # generate cyclic pattern cyclic -l 0x6161616c # find offset for that 4-byte value distance $rsp $rbp # compute distance between two addresses
TUI Mode — Terminal UI
# Enable TUI tui enable # or press Ctrl-X Ctrl-A layout asm # top pane: disassembly layout reg # top pane: registers layout src # top pane: source (needs debug symbols) layout split # source + asm both visible layout next # cycle through layouts # Focus between panes focus cmd # focus on command pane (for input) focus asm # focus on assembly pane (for scrolling) focus reg # focus on register pane # Refresh if display corrupts refresh # redraw TUI tui disable # back to text mode # Keyboard shortcuts in TUI # Ctrl-X 1 — single window mode # Ctrl-X 2 — two window mode # Ctrl-X A — toggle TUI on/off # PageUp/Down — scroll active pane
Remote Debugging & QEMU
# On the TARGET machine (ARM, MIPS, embedded, etc.) gdbserver :1234 ./target # listen on port 1234 gdbserver :1234 --attach <PID> # attach to running process # On the ANALYSIS machine gdb-multiarch ./target (gdb) target remote <target_ip>:1234 (gdb) continue # QEMU user-mode (single binary, no VM) qemu-x86_64 -g 1234 ./target # start with GDB stub on port 1234 gdb ./target (gdb) target remote localhost:1234 # QEMU system-mode (full VM) qemu-system-x86_64 -s -S -kernel vmlinuz … # -s = :1234 stub, -S = pause at start gdb (gdb) target remote localhost:1234 (gdb) set architecture i386:x86-64 (gdb) continue
Quick Reference Card
| Command | What it does | When to use |
|---|---|---|
break *0xaddr | BP at raw address | Stripped binaries — no symbol names |
x/64xb $rsp | 64 bytes from RSP | Spot overflow fill pattern (0x41) |
x/20i $pc | Disassemble from RIP | See what instruction is about to execute |
info proc mappings | Virtual memory map | Find libc base, stack bounds, heap range |
watch *($rbp-8) | Watch canary slot | Catch the exact write that corrupts canary |
catch syscall write | Break on write() | Intercept output — trace exploitation |
find /b start, end, bytes | Search memory | Locate shellcode, strings, gadgets |
set $rip = 0xaddr | Redirect execution | Skip code, test gadgets interactively |
call malloc(256) | Allocate mid-session | Test heap state without restarting |
disas /r main | Disassemble + raw bytes | Compare opcodes against known gadgets |
backtrace full | Stack + locals | See variable values at every frame |
record + rc | Reverse execution | Step backward to the corrupting instruction |
layout split | TUI asm + src | Visual debugging with source and disasm |
rbreak ^__libc_ | BP on all libc fns | Trace every libc call in sequence |
ignore 1 99 | Skip 99 BP hits | Break on the 100th malloc — skip early ones |