Konstas.Engineering Notes
Networking · Cybersecurity · Infrastructure
Offensive Security

Memory Safety, Buffer Overflows & Baggy Bounds

Articles · 2026 · Ioannis (John) Konstas

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.

Systems Security  ·  C/C++  ·  x86-64 Linux  ·  Ioannis (John) Alexander Konstas

Buffer Overflow Baggy Bounds Stack Smashing Heap Corruption ASLR Stack Canaries AddressSanitizer GDB / pwndbg glibc / jemalloc Tcache Poisoning
Silicon die micrograph beside C exploitation and factoring code — memory safety reference
💥 Buffer Overflow Fundamentals
§1

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 Frame Layout — Before and After Buffer Overflow NORMAL STATE HIGH ADDR LOW ADDR argc / argv [caller args] Return Address 0x555555555219 Saved RBP 0x7fffffffe2a0 Stack Canary 0xdeadbeef… char buf[64] [safe input] ↑ RSP (stack top) AFTER OVERFLOW (700+ bytes of 0x41) argc / argv [caller args] Return Address ← SMASHED 0x4141414141414141 Saved RBP ← OVERWRITTEN 0x4141414141414141 Stack Canary ← CORRUPTED 0x4141414141414141 char buf[64] AAAA AAAA AAAA AAAA… 0x41414141 × 16 dwords ⬆ overflow grows up the stack SIGABRT triggered when corrupted canary detected on function return
Figure 1 — Stack frame before and after a buffer overflow. 0x41 = ASCII 'A'. Canary corruption triggers *** stack smashing detected *** before the corrupted return address executes.
The OS cannot prevent intra-process memory corruption. OS protections isolate processes from each other — not from their own logic errors. All buffer overflow exploitation happens entirely within the victim process's address space.
🛡 Standard Mitigations & Their Limits
§2

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.

MitigationWhat It ProtectsBypass / Limit
Stack CanariesReturn address overwrite on stackDoes not protect heap metadata. Bypassable with information leak of canary value.
Non-Executable Memory (NX/DEP)Code injection via stack/heap shellcodeDoes not prevent ROP (Return-Oriented Programming) — attacker reuses existing code.
ASLRPredictable addresses for gadgets, libc, heapEntropy is limited. Bypassable via format string leaks, side channels, or heap spraying.
Control Flow Integrity (CFI)Indirect control-flow hijackingOverhead and deployment complexity. Does not protect data-only attacks.
Safe APIs (strncpy, snprintf)Common unsafe function callsRequires disciplined developer adoption across all code paths. Legacy code resists.
Memory-Safe Languages (Rust, Go)Most classes of memory corruptionImpractical for performance-critical, hardware-adjacent, or legacy code paths.
AddressSanitizerHeap/stack out-of-bounds, use-after-free2–3× slowdown. Not suitable for production. Changes memory layout; can mask bugs.
Defense in depth is the only realistic strategy. Layer canaries, NX, ASLR, CFI, sanitizers in development, and bounds checking together. No single mechanism is sufficient.
📏 Approaches to Bounds Checking
§3

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 — Design & Tradeoffs
§4

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.

Baggy Bounds — Slot Table & Bounds Check Memory Slots (each = 16 bytes) malloc(20) → rounded to 32 (2⁵) table[slot] = 5 malloc(100) → rounded to 128 (2⁷), table[slot] = 7 Side Table (1 byte per slot) 5 5 0 0 7 slot 4 base of 128-byte block Bounds Check (constant time — no loop, no metadata traversal): slot = ptr >> SLOT_SHIFT // which slot does this pointer land in? size = 1 << table[slot] // recover block size from log₂ value valid = (ptr ^ base) < size // still inside the power-of-two block?
Figure 2 — Baggy Bounds slot table. Each allocation is rounded to the next power of 2. The side table stores log₂(size) per slot (1 byte). Bounds checking is three bitwise operations — no metadata traversal required.

Key Tradeoffs

PropertyBaggy BoundsFat PointersASan 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
🗺 Process Memory Layout
§5
x86-64 Linux Process Virtual Address Space 0xFFFFFFFF 0x00000000 Kernel Space [inaccessible from userland] vDSO / vsyscall [0xffff…] Stack ↓ grows down 0x7fff… — frames, locals, return addresses, canaries ↕ ASLR gap Memory-Mapped Segments libc.so, ld.so, anonymous mappings, file-backed pages Heap ↑ grows up 0x555…9000 — malloc/free managed; chunk headers adjacent to data BSS — zero-initialized global & static data Data — initialized global & static data Text (Code) Segment [r-xp] 0x555555554000 — executable instructions, read-only NULL page [0x0 — unmapped, traps null dereferences] ← 0x7fff…de000 ← 0x7fff…c00000 ← 0x5555…9000 [heap] ← 0x5555…4000
Figure 3 — x86-64 Linux virtual address space. Addresses from the GDB 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.

🔀 How Memory Is Moved
§6
OperationBehaviorOverlap Safe?Notes
memcpy(dst, src, n)Copies n bytes src→dst❌ UB on overlapFastest; compiler may emit SIMD. Never use on overlapping regions.
memmove(dst, src, n)Copies n bytes, handles overlap✅ YesDetects and handles forward/backward copy direction.
realloc(ptr, size)Resize allocationN/AMay allocate new block, copy, free old. Old pointer becomes invalid — use-after-free risk.
mremap(old, old_sz, new_sz)Kernel: resize VM regionN/AMay avoid copy by remapping page tables. Syscall only.
After realloc(), the original pointer is invalidated — even if the address did not change. Any code retaining the old pointer commits use-after-free.
⚙️ Allocator Internals
§7

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.

glibc chunk layout (64-bit)
/*   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.

☠️ Heap Metadata Corruption Primitives
§8
TechniqueMechanismOutcome
Unlink AttackForge prev/next pointers in a free chunk's fd/bk fieldsArbitrary write during free() coalescing
Off-by-OneWrite one byte past allocation boundary, corrupt PREV_INUSE flag or size of next chunkOverlapping chunks; controlled allocation reuse
Double FreeFree same pointer twice, inserting chunk twice into free listNext malloc returns same address twice; write-after-free
Tcache PoisoningCorrupt tcache next pointer after freeing chunkmalloc() returns arbitrary address; write primitive
Heap Feng ShuiShape heap layout via controlled allocation/free patterns so target chunk ends up adjacent to attacker-controlled dataDeterministic corruption of target object
Use-After-FreeAccess freed chunk after allocator reuses its memoryType confusion, stale pointer reads, arbitrary write if attacker controls reallocated data
🔬 Debugging & Inspection Techniques
§9
AddressSanitizer — build and run
# 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
Valgrind — memory error detection
# 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
GDB — heap inspection workflow
# 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
✅ Heap Corruption Investigation Checklist
§10
Work smallest-to-largest: minimize the test case first, then apply tools in order of overhead. Never modify source data during investigation — always work from a reproducible copy.
  1. Minimize the reproducing test case — smallest input that reliably triggers the crash
  2. Run under AddressSanitizer — catches most heap/stack issues with precise attribution
  3. Run under Valgrind if ASan unavailable or doesn't catch it — slower but more exhaustive
  4. Break on malloc/free in GDB — build a timeline of all allocations and frees
  5. Set watchpoints on chunk headers — catch the exact write that corrupts metadata
  6. Inspect tcache lists for duplicates, impossible pointers, or out-of-range values
  7. Examine free list bins — coalescing bugs show up as corrupted fd/bk pointers
  8. Run a fuzzer (AFL++, libFuzzer) against the component — surface hidden paths
  9. Goal: identify the corrupting instruction, not just the downstream crash site
🩺 Troubleshooting Guide — Common Scenarios
§11

Scenario 1 — Crash Inside free()

Symptom: *** Error in 'a.out': free(): invalid pointer *** or double free or corruption

Workflow
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

Workflow
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

ASan report + GDB workflow
# 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

pwndbg workflow
# 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()

Track old pointer invalidation
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
🖥 Annotated GDB Session — Live Buffer Overflow
§12
This is a real GDB session on Ubuntu 22.04 debugging a compiled buffer_overflow binary. The binary was compiled without debug symbols (stripped). Annotations explain what each output reveals.
1. Load binary and execute
$ 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]
2. Overflow detected — SIGABRT triggered
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.
3. Inspect process memory map
(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.
4. Registers at SIGABRT time
(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
5. Stack memory dump — finding the 0x41 fill
(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.
6. Breakpoint on main and stack frame inspection
(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
📋 Command Reference
§13

GDB Essentials

CommandPurpose
info proc mappingsPrint full virtual memory map — all segments, permissions, file backing
info registersAll general-purpose registers + flags at current execution point
info registers rspSingle register — quick stack pointer check
x/64x $rspHex dump: 64 dwords from RSP — find overflow fill pattern
x/20i $pcDisassemble 20 instructions from current IP — works on stripped binaries
info frameCurrent stack frame: saved RIP, saved RBP, local variable sizes
backtraceCall stack at time of crash
watch *addressHardware watchpoint — halt when that memory address is written
break *0xaddrSet breakpoint at raw address (essential for stripped binaries)
finishRun until current function returns; show return value in RAX
set $var = valueDefine convenience variable (save pointer for later comparison)

ASan Flags

FlagEffect
ASAN_OPTIONS=detect_leaks=1Enable LeakSanitizer integration (default on Linux)
ASAN_OPTIONS=halt_on_error=0Continue after first error — find all bugs in one run
ASAN_OPTIONS=log_path=/tmp/asan.logWrite ASan report to file instead of stderr
-fsanitize=address,undefinedCombine ASan + UBSan — catches integer overflow, null deref, misaligned access
-g -O1Debug symbols + light optimization — best ASan call stack quality

Valgrind Options

OptionEffect
--leak-check=fullFull leak report including indirect/reachable leaks
--show-leak-kinds=allInclude definitely/indirectly/possibly lost
--track-origins=yesShow where uninitialized values were created (high overhead)
--error-limit=noDon't stop after 100 errors
--gen-suppressions=allGenerate suppression file for known false positives
🗺 Buffer & Memory Topology
§14

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.

STACK FRAME MEMORY LAYOUT (x86-64, grows downward ↓) HIGH ADDR LOW ADDR Caller's Stack Frame Saved Return Address (RIP) ← overflow target Saved Base Pointer (RBP) Stack Canary (random 8 bytes) checked on return SIGABRT if corrupted Local Variables (int, pointers…) char buf[256] ← attacker-controlled input buf[0] at RSP+offset buf[255] = last safe byte buf[256+] = OVERFLOW ZONE RSP OVERFLOW DIRECTION 0x41414141 0x41414141 0x41414141… ← 'AAAA' fill pattern cyclic pattern: Aa0Aa1Aa2… ← pwntools cyclic(300) offset found when saved RIP = pattern ← cyclic_find($rip_value)

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.

GLIBC PTMALLOC CHUNK LAYOUT (64-bit) malloc() returns → prev_size (8 bytes — used if prev chunk is free) size | flags (8 bytes) [P|M|A bits] user data → User data (malloc'd bytes) fd / bk pointers live here when chunk is FREE Next chunk: prev_size (8 bytes) Next chunk: size | flags 0x0 if prev in use P=prev_in_use M=mmap'd A=non-main fd → next free chunk bk → prev free chunk off-by-one corruption → x/4gx (ptr - 0x10) ← examine chunk header: prev_size, size, fd, bk x/4gx ptr ← user data start (fd/bk visible when freed) size field = actual_size | flags ← mask: size & ~0x7 = real size

Figure: ptmalloc chunk — user pointer is 16 bytes past chunk start; fd/bk overlay user data when freed

🛠 GDB Debugging Workflows — Complete Reference
§15

Launch & Attach

Starting GDB
# 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

Setting, managing, and scripting breakpoints
# 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

Navigate program flow
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

x/NuF — count, unit, format
# 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

Registers, frames, locals, args
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

Stop on memory access, syscalls, exceptions
# 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

Reverse engineer stripped binaries
# 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

Scripting inside GDB
# 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)

Pattern workflow — from overflow to offset
# 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
Manual offset calculation in GDB
# 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

Defeating ASLR in GDB + live binary
# 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

Verify gadgets execute correctly
# 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

Automate workflows with scripts
# ~/.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

pwndbg — install and key commands
# 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

Split-screen debugging in the terminal
# 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

gdbserver + cross-arch debugging
# 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

CommandWhat it doesWhen to use
break *0xaddrBP at raw addressStripped binaries — no symbol names
x/64xb $rsp64 bytes from RSPSpot overflow fill pattern (0x41)
x/20i $pcDisassemble from RIPSee what instruction is about to execute
info proc mappingsVirtual memory mapFind libc base, stack bounds, heap range
watch *($rbp-8)Watch canary slotCatch the exact write that corrupts canary
catch syscall writeBreak on write()Intercept output — trace exploitation
find /b start, end, bytesSearch memoryLocate shellcode, strings, gadgets
set $rip = 0xaddrRedirect executionSkip code, test gadgets interactively
call malloc(256)Allocate mid-sessionTest heap state without restarting
disas /r mainDisassemble + raw bytesCompare opcodes against known gadgets
backtrace fullStack + localsSee variable values at every frame
record + rcReverse executionStep backward to the corrupting instruction
layout splitTUI asm + srcVisual debugging with source and disasm
rbreak ^__libc_BP on all libc fnsTrace every libc call in sequence
ignore 1 99Skip 99 BP hitsBreak on the 100th malloc — skip early ones

← All articles

Research and direction by Ioannis (John) Konstas; compiled and finalized with AI assistance.

👁· views