algoatson.github.io ~/posts
segfault@algoatson.github.io
back to index

pwndbg muscle memory

2026-05-014 min readpwn heap notes

This is the subset of pwndbg I use without thinking. It isn't the manual — it's the muscle memory, the commands that survive contact with a real target. Install is one line into a fresh gdb:

console
$ git clone https://github.com/pwndbg/pwndbg && cd pwndbg && ./setup.sh

pre-flight#

Three commands before anything else, every time:

  • checksec — what am I up against (NX, PIE, RELRO, canary)
  • start — break at the entry/main and get a live process + mappings
  • vmmap — where is everything actually mapped right now

the three I type most#

telescope
dereference a chain of pointers and label what each one points at — telescope $rsp 30 is how I read a stack. telescope -r $rax walks backwards. It's x/gx that does the deref work for you.
vmmap
the live memory map with permissions. vmmap libc filters to one module; vmmap $rsp tells you which region an address lives in. This is your leak sanity-check — does that pointer land in libc, the heap, or the stack?
context (alias ctx)
the register / code / stack / backtrace dashboard pwndbg prints on every stop. context reprints it; ctx code, ctx stack show one pane when the full one scrolls off.
command what it does
b *0x401234 break at an absolute address
b main / b file:42 break at a symbol or source line
breakrva 0x1234 break at an offset — the right way under PIE
b *$rebase(0x1234) same idea inline: $rebase() adds the PIE load base
starti stop at the very first instruction (_start)
ni / si step over / into one instruction
nextcall / nextret run until the next call / ret — skip the boilerplate
finish (fin) run until the current function returns
set follow-fork-mode child follow the fork (forking servers)

PIE addresses change, offsets don't

Never hard-code a PIE address from a previous run — ASLR moved it. Set breakpoints with breakrva/$rebase against the file offset you saw in the disassembler, and they survive every restart.

inspecting memory#

command what it does
x/20gx $rsp 20 giant (8-byte) words in hex from $rsp
x/8i $pc next 8 instructions
x/s 0x4040a0 read a C string
hexdump $rax 0x40 pwndbg's annotated hexdump
p/x $rdi / p $eax print a register, hex or decimal
xinfo 0x7fff... "what is this address" — module, section, offset
set {int}0x404060 = 1 poke a value into memory
set $rip = 0x401200 redirect execution
watch *0x404060 break when that memory changes (rwatch on read)

the heap#

The reason pwndbg exists. All of these read glibc's live allocator state:

command what it shows
vis (vis_heap_chunks) the heap drawn chunk-by-chunk, freelist links coloured
heap every chunk in the current arena, top to top chunk
bins all bins at once — tcache, fast, unsorted, small, large
tcachebins just the tcache, per size class, with the next chain
fastbins the fastbin freelist
malloc_chunk 0x.. decode one chunk header (size, prev_size, flags)
top_chunk the wilderness chunk and its size
find_fake_fast 0x.. scan for a size field that lets you allocate onto an address
arena / arenas the malloc_state for this / every arena
chunk
glibc's allocation unit: an 8/16-byte header (prev_size, size with its low flag bits) followed by user data. size & ~0x7 is the real size.
tcache
per-thread, single-linked cache of recently freed chunks; the first stop on free and the first served on malloc. Poisoning its next is the modern "near-arbitrary write" — see tcache poisoning.
safe-linking
glibc ≥ 2.32 stores next as ptr ^ (chunk_addr >> 12), so overwriting a freelist pointer needs a heap leak first. vis shows the demangled value.

search is the swiss-army primitive — find a value, a string, or a pointer:

txt
1
2
3
4
search -t qword 0xdeadbeefcafef00d   # an 8-byte value, anywhere mapped
search -s "/bin/sh"                   # a string (great in libc)
search -x 6a3b58                      # a hex byte pattern
search -w 0x404060                    # writable mappings only
advanced — the one-liners I keep in a snippet file

Find a pop rdi; ret without leaving gdb:

txt
rop --grep "pop rdi"

Dump the GOT and PLT, see what libc calls are reachable:

txt
got
plt

Cyclic pattern in, offset out — no pen-and-paper:

txt
cyclic 200            # paste this as input
cyclic -l 0x6161616c  # the value in $rip after the crash -> the offset

Turn ASLR off for a deterministic session (then turn it back on before you trust an exploit):

txt
aslr off

don't ship an exploit you only tested with aslr off

A chain that works at a fixed base and breaks under ASLR means your leak is wrong, not your gadgets. Test the final version with aslr on and from a fresh run each time.