docs(shared-skills): batch 43 (4 files)

This commit is contained in:
YeonGyu-Kim
2026-05-30 19:12:07 +09:00
parent f7d50701e3
commit e11d24d141
4 changed files with 934 additions and 0 deletions
@@ -0,0 +1,212 @@
# Ghidra — Decompile Binaries Into Readable C
**https://github.com/NationalSecurityAgency/ghidra**
Ghidra is the NSA's open-source reverse-engineering suite. Its defining feature is a **decompiler** that turns machine code back into readable C. For any binary you don't have source for, this is the correct starting point — not `strings`, not hex-staring, not `objdump -d`.
**Use Ghidra when**: third-party closed-source libs, malware analysis, vendored binaries whose behavior contradicts docs, CTF challenges, firmware, any time you need to read compiled code.
---
## Install
```bash
# macOS
brew install --cask ghidra
# OR download the release ZIP from the repo and ./ghidraRun
# Linux
# Download from https://github.com/NationalSecurityAgency/ghidra/releases
# Requires JDK 21+
./ghidraRun
# Dependency
java -version # must be 21+
```
Ghidra is a Java Swing app. Looks dated, works well.
---
## First-time workflow (memorize this — it's not obvious)
1. **Start Ghidra**: `ghidraRun`
2. **Create a project**: File → New Project → Non-Shared → name it `debug-<binary-name>` (journal this path so you can rm it at Phase 9 if disposable).
3. **Import the binary**: File → Import File → pick your target. Accept default format detection.
4. **Double-click the imported binary** in the project listing. Ghidra asks to analyze it — say **yes**, accept defaults for the first pass. This takes anywhere from seconds (small binary) to tens of minutes (large binary).
5. **Once analysis completes**, you're in the CodeBrowser view.
Two panels you'll use 95% of the time:
- **Listing** (middle) — the disassembly with Ghidra's inferred labels/types.
- **Decompiler** (right) — the reconstructed C. The real value.
---
## Finding the right function fast
Don't try to read the whole binary. Use these to narrow:
### Symbol Tree (left panel)
- `Functions` — all detected functions. Stripped binaries show `FUN_00401234` (address-named); unstripped show actual names.
- `Imports` — dynamically-linked functions. Great for "does this binary call `system()`, `strcpy`, `curl_easy_perform`?"
- `Exports` — if it's a library.
Click to jump. The Decompiler updates instantly.
### String search
```
Search → For Strings
```
Produces a list of all strings. Right-click a string → `References``Show References to Address`. Jumps to code that references it. **This is how you find which function handles the error message you saw at runtime.**
### Memory search for bytes
```
Search → Memory
```
Search hex or text. Useful for known magic bytes, file-format signatures, constants.
### Cross-references (XREF)
Right-click any function / address → `References → Find References to`. Shows every place that calls it. Walk the call graph backward from interesting functions.
---
## Making the decompiler's output readable
Ghidra's decompiler is good but needs hints. These three actions dramatically improve its output:
### 1. Rename variables
Click a variable in the Decompiler view → press `L` → type a better name. Ghidra propagates the rename across all uses.
### 2. Set types
A variable that looks like `undefined4` or `void *` is unhelpful. Click it → press `Ctrl+L` → set type (e.g. `int`, `char *`, `struct my_header *`).
For pointers to structs from headers you have, use:
```
File → Parse C Source → paste header file → auto-creates struct types
```
Then assign the struct type to the pointer. Ghidra resolves field accesses immediately.
### 3. Retype function signatures
Click the function name in the Decompiler → press `F` (Edit Function Signature) → set return type + argument types. This propagates through callers.
Do these three actions on the 3-5 most relevant functions and the decompiler output becomes near-source-readable.
---
## Patterns for specific bug types
### Looking for integer overflow / buffer overflow
```
Listing: look for
- LEA → CMP patterns on sizes
- memcpy/strcpy/sprintf with non-constant sizes
Decompiler: look for
- arithmetic on size_t without bounds check
- `+ user_input` in a length calculation
```
### Looking for a missing auth check
Navigate from the handler entry (found via Strings or Imports) and check the control flow:
```
Decompiler: does the function return early / jump to error handler when some flag is not set?
If the check is absent, that's the bug.
```
### Looking for hardcoded URLs / keys / paths
```
Search → For Strings → filter `http:` / `https:` / `/etc/` / `bearer ` / `api_key`
```
### Looking for dispatch / plugin loading
```
Imports → dlopen, LoadLibrary, dlsym, GetProcAddress
```
The strings referenced near those calls are often plugin names.
---
## Scripting (headless Ghidra)
When you need to automate analysis across many binaries, or repeat a workflow:
```bash
# Headless analyzer
$GHIDRA_INSTALL_DIR/support/analyzeHeadless \
<project-dir> <project-name> \
-import <binary> \
-postScript <script.py or script.java>
```
Ghidra supports Python 3 scripts (via Jython-compatible API) and Java. Useful scripts:
- Dump all function signatures to JSON
- Find all calls to `system()` with constant arguments
- Auto-rename FUN_xxx based on heuristics (string refs, call patterns)
The community has a large collection: https://github.com/NationalSecurityAgency/ghidra/tree/master/Ghidra/Features/Base/ghidra_scripts
---
## Bookmarks + Notes
Ghidra has built-in bookmarks and comments. Use them as your journal inside the project:
- Right-click an address → `Set Bookmark` → tag as `Note`. Attach a description.
- Right-click → `Comments → Set EOL Comment` (shows up inline in decompiler).
Treat these as part of the journal. If you end up promoting this Ghidra project (keeping it after the debug session), the comments become durable documentation.
---
## Gotchas
- **Large binaries need more Java heap.** Edit `support/launch.properties` and bump `VMARGS=-Xmx8G` (default is often 2G, too small).
- **Save often.** Ghidra's autosave is not instant; a crash loses uncommitted analysis.
- **Decompiler has timeouts.** For complex functions it may give up and print `/* WARNING: ... */`. Increase timeout in `Edit → Tool Options → Decompiler`.
- **Archs beyond x86/ARM/MIPS** sometimes need Sleigh processor module tweaks. Rare but possible.
- **Signed vs unsigned decompilation** is frequently wrong. Manually retype when integer behavior matters.
---
## When Ghidra is NOT the right tool
- You have the source. Go read it.
- Bug is in your own recently-compiled binary. Rebuild with `-g` and use gdb/pwndbg (see [pwndbg.md](pwndbg.md)).
- You just need to know which libs a binary links. `ldd` / `otool -L` / `readelf -d` are faster.
- You just need strings. `strings -n 8` is faster.
Ghidra is the right tool when you need to **read the logic** of a binary you don't have source for.
---
## Phase 9 cleanup specifics
```bash
# If you created a scratch project just for this debug session, journal path and remove:
# (the journal should have the exact path)
# Example:
ls ~/ghidra-projects/ 2>/dev/null
# rm -rf ~/ghidra-projects/debug-<binary-name>
# If you promoted the project (kept it), it's not cleanup — note it in the final summary to the user
# Kill any running Ghidra headless processes
pkill -f 'analyzeHeadless' || true
```
@@ -0,0 +1,194 @@
# Playwright CLI — Browser QA That Actually Drives a Browser
**https://playwright.dev/ · https://github.com/microsoft/playwright**
For any browser-served web UI bug, this is the correct tool. Not curl. Not imagination. Not a headless HTTP library. A real browser with a real rendering engine, real JS execution, real cookies, real service workers, real viewport.
**In Phase 8 Manual QA for browser products, using Playwright is not optional.** Curl cannot catch: CSS that breaks at specific viewport widths, hydration mismatches, client-side router bugs, cookie/session interactions, service-worker caching, JS-triggered navigations. All of those are common bug classes. Drive a browser.
> Note: `microsoft/playwright-cli` is the legacy repo; the current tooling lives in `@playwright/test` (npm) and `playwright` (pip), which include the `playwright` CLI. Use those — the legacy `playwright-cli` package is deprecated.
---
## When to reach for Playwright
| Bug symptom | Use Playwright? |
|---|---|
| Form submit produces wrong result | ✅ — Playwright drives the form exactly as a user does |
| Page blank in prod, fine locally | ✅ — hydration/env differences need a real browser |
| CSS looks wrong at a specific width | ✅ — use `--viewport-size` |
| Click doesn't fire / wrong handler | ✅ — Playwright fires real DOM events |
| Flash of unstyled content / loading glitch | ✅ — use trace viewer to see frames |
| API returns wrong data | ❌ — use curl, this isn't a browser bug |
| Backend returns wrong status code | ❌ — use curl |
| Client hits a URL that returns 500 | ✅ but also ❌ — Playwright shows the call + response + failure effect on UI |
---
## Install (per-project)
Playwright installs browser binaries separately from the npm package.
```bash
# In the project
npm init playwright@latest # interactive; picks TS/JS + browsers + config
# Or if Playwright is already a dep:
npx playwright install # downloads browsers
npx playwright install chromium # just chromium
npx playwright install --with-deps # also installs OS deps (Linux)
```
Python:
```bash
pip install playwright
playwright install
```
---
## The four things you'll actually use
### 1. `codegen` — record a session, generate the script
The fastest way to create a repro. Opens a real browser; your clicks / typing become a Playwright script you can paste into a test.
```bash
npx playwright codegen https://your-app.local
npx playwright codegen --viewport-size=375,667 https://your-app.local # iPhone SE size
npx playwright codegen --device="iPhone 14" https://your-app.local
```
Click / type / navigate in the browser; watch the script build in the side panel. Copy the generated script into your journal as the repro for Phase 8.
### 2. A one-shot Playwright script — reproduce + capture
Usually the Phase 8 QA artifact. Save to `/tmp/debug-repro.spec.ts` (journal it):
```ts
// /tmp/debug-repro.spec.ts
import { test, expect } from '@playwright/test';
test('refinement chat shows non-empty response when env var set', async ({ page }) => {
await page.goto('http://localhost:3000/chat');
await page.fill('textarea[name="message"]', 'Add a logging step');
await page.click('button[type=submit]');
// Wait for the response to appear (not just the spinner to disappear)
const response = page.locator('[data-testid="assistant-reply"]');
await expect(response).toBeVisible({ timeout: 30_000 });
await expect(response).not.toBeEmpty();
// Capture evidence
await page.screenshot({ path: '/tmp/debug-after-fix.png', fullPage: true });
console.log(await response.textContent());
});
```
Run it with tracing enabled for rich post-mortem:
```bash
npx playwright test /tmp/debug-repro.spec.ts --trace on --headed
```
### 3. `PWDEBUG=1` — step through the script with Playwright Inspector
```bash
PWDEBUG=1 npx playwright test /tmp/debug-repro.spec.ts
```
Opens the Playwright Inspector alongside the browser. You can step through Playwright actions, see the DOM state at each step, and edit selectors on the fly.
Use this when the script doesn't reproduce cleanly and you need to watch it run.
### 4. `show-trace` — post-mortem on a failed run
```bash
npx playwright show-trace trace.zip
# or from the test-results dir:
npx playwright show-trace test-results/<test-name>/trace.zip
```
Scrubs through a recorded session: timeline, DOM snapshot at each action, network, console, source. When a test failed on CI but passed locally, this is the single best artifact.
---
## Headless vs headed during debugging
Always add `--headed` when debugging. Headless browsers sometimes behave subtly differently (font rendering, viewport, media permissions). For QA evidence, run headed and screenshot.
```bash
npx playwright test --headed
npx playwright test --headed --project=chromium # pin the browser
```
---
## Catching the silent-failure patterns Playwright is good at
```ts
// Toast that flashes and disappears
page.on('console', msg => console.log('[browser console]', msg.type(), msg.text()));
// Unhandled page errors (uncaught exceptions in the page JS)
page.on('pageerror', err => console.error('[page error]', err));
// Network failures — e.g., backend returned 500 but UI shows nothing
page.on('response', async resp => {
if (!resp.ok()) {
console.warn(`[network ${resp.status()}] ${resp.url()}${await resp.text()}`);
}
});
// Request that never came back
page.on('requestfailed', req => {
console.error('[request failed]', req.url(), req.failure()?.errorText);
});
```
Add these listeners to the top of the debug script. They surface a lot of the "UI showed nothing" class of bug.
---
## Viewport and device emulation
CSS bugs that only appear at specific sizes, or layout bugs on mobile:
```ts
// At test level
test.use({ viewport: { width: 375, height: 667 } });
// Per-page
await page.setViewportSize({ width: 375, height: 667 });
// Predefined devices
import { devices } from '@playwright/test';
test.use({ ...devices['iPhone 14'] });
```
---
## Gotchas
- **Wait for state, not for time.** `await page.waitForTimeout(2000)` is flaky. Use `await expect(locator).toBeVisible()` or `page.waitForResponse(urlPattern)`.
- **Stale selectors re-resolve.** Playwright's locators re-find the element on each action, unlike Puppeteer's handles. Don't over-think it.
- **Service workers persist across test runs in headed mode.** If you see cached behavior from a previous run, add `await context.clearCookies()` + clear storage before the test.
- **Installing on CI requires `--with-deps`** on Linux images that lack the browser's shared-library deps.
- **Parallel tests share a browser process by default**; if one test polls a debugger port, others may interfere. Use `workers: 1` for debugging.
---
## Phase 9 cleanup specifics
```bash
# Remove trace files from debug runs
rm -rf playwright-report/ test-results/ trace.zip
# Remove debug spec files from /tmp
rm -f /tmp/debug-*.spec.ts
# Remove screenshot captures
rm -f /tmp/debug-*.png
# If you installed browsers just for this session (rare):
# Don't remove them — they're useful for future sessions. They live in ~/Library/Caches/ms-playwright (macOS) or ~/.cache/ms-playwright (Linux).
```
@@ -0,0 +1,263 @@
# pwndbg — GDB With the Useful Views Always On
**https://github.com/pwndbg/pwndbg**
pwndbg is a GDB plugin that turns GDB into something humans can actually use for binary debugging. It's strictly a superset of plain GDB — every vanilla GDB command still works, and pwndbg adds views and commands that make you productive.
**If you'd reach for plain `gdb`, reach for pwndbg instead.** The only reason not to is if pwndbg isn't installed on the machine, and that's a 2-minute fix.
---
## Install
```bash
# macOS
brew install pwndbg
# Or from source:
git clone https://github.com/pwndbg/pwndbg
cd pwndbg && ./setup.sh
# Linux
# Most distros: apt/dnf/pacman install pwndbg (check availability)
# Or the same git + ./setup.sh
# Verify
gdb --version
gdb ./any-binary
# At gdb prompt, you should see pwndbg banner + colorful context view
```
Once installed, pwndbg auto-loads every time you start `gdb`. You don't source anything manually.
---
## The `context` view — the one feature that changes everything
Plain GDB: you run `info registers`, then `bt`, then `x/10xw $rsp`, then `disas`. Four commands to see what's going on.
pwndbg: `context` (or it auto-shows at every break). One command. Everything on screen:
```
──── registers ────
RAX 0x0
RBX 0x7ffffffde158
RCX 0x7fffff7abf10
...
──── disasm ────
► 0x401234 mov rdi, rax
0x401237 call 0x401190
...
──── stack ────
00:0000│ rsp 0x7ffffffde0a0 → 0x7fffff7c4000
01:0008│ 0x7ffffffde0a8 → 0x0
...
──── backtrace ────
► f 0 0x401234 parse_input+0x3c
f 1 0x401180 main+0x120
f 2 0x7fffff7a5083 __libc_start_main+0xf3
```
You always know where you are, what the CPU state is, what's on the stack, and how you got here. This is why pwndbg is the default.
---
## Launch recipes
```bash
# Debug an existing binary
gdb ./target
# With args
gdb --args ./target arg1 arg2
# Attach to a running process
gdb -p $(pgrep target)
# With a core dump
gdb ./target ./core
# Headless / remote (for automation or IDE attach)
gdbserver :2345 ./target # on the target box
gdb ./target # on your box
(gdb) target remote <host>:2345
```
At the pwndbg prompt:
---
## Essential commands (pwndbg additions)
### Layout / view
```
context # reprint the context view (usually auto)
context regs stack # only show registers + stack sections
tel $rsp 20 # telescope — walk pointers at $rsp for 20 slots (KEY COMMAND)
tel $rdi 10 # walk pointers at $rdi (e.g. to dump a struct)
stack 20 # 20 entries of stack
vmmap # virtual memory map of the process
```
**`telescope` is pwndbg's killer command.** Given an address, it walks pointers recursively:
```
00:0000│ 0x7ffd... → 0x601010 (heap) → 0x2a (unknown, i.e. a number 42)
01:0008│ 0x7ffd... → 0x7fff... (stack) → 'hello world'
```
This single view resolves 80% of "what is at this address" questions.
### Heap debugging
```
heap # overview of chunks
bins # tcache / fastbin / unsorted / smallbin / largebin state
malloc_chunk <addr> # inspect a specific chunk
find_fake_fast <addr> # (exploit context) find fake-fast overlap candidates
vis_heap_chunks # visualize heap layout
```
For use-after-free / double-free / heap overflow hypotheses, `heap` + `bins` is usually sufficient to see the corruption.
### Exploitation-adjacent (useful for bug understanding too)
```
checksec # NX, PIE, RELRO, canary status
rop --grep 'pop rdi' # find ROP gadgets
nx # step over (aliased nicely)
ni # step over single instruction
si # step into single instruction
```
### Search
```
search -t byte 0x41 # find byte 0x41 anywhere in memory
search -t string "admin" # find string
search -p <addr> # find pointers to <addr>
```
---
## Standard GDB commands still work
pwndbg doesn't replace GDB; it augments it. Everything you know still works:
```
break main # breakpoint at function
b *0x401234 # breakpoint at address
b file.c:42 # breakpoint at file:line
c # continue
n # next (source-level step over)
s # step (source-level step into)
finish # step out
info breakpoints # list breakpoints
delete <n> # delete breakpoint
watch <var> # break on write to variable
rwatch <var> # break on read
awatch <var> # break on access
p <expr> # print expression
p/x <expr> # print in hex
x/20xw <addr> # examine 20 words as hex
bt # backtrace
frame <n> # switch frame
info registers # registers (but `context` is better)
disassemble <func> # disasm a function
```
---
## Python scripting inside GDB
pwndbg exposes a full Python API. Useful for automating observations across many breakpoints:
```python
(gdb) python
import gdb
def on_break():
frame = gdb.selected_frame()
pc = frame.read_register('pc')
print(f'hit at {hex(int(pc))}')
# Dump args, locals, anything
end
```
Or scripted runs from outside:
```bash
gdb -batch -ex 'source script.gdb' -ex 'run' ./target
```
---
## Common workflows by bug type
### Segfault / crash
```bash
gdb ./target
(gdb) run <args>
# ... crash ...
(gdb) context # see the crash site
(gdb) bt # how did we get here?
(gdb) info registers # what state
(gdb) tel $rsp 20 # what's on the stack
```
### "Function returns wrong value"
```bash
gdb ./target
(gdb) break <function>
(gdb) run <args>
# At breakpoint:
(gdb) finish # let it run to the return
# pwndbg shows RAX (return value) in context
```
### "Variable has unexpected value at point X"
```bash
gdb ./target
(gdb) break <point-X>
(gdb) run
# At breakpoint:
(gdb) p <var> # its value
(gdb) watch <var> # set a watchpoint — break when it changes
(gdb) c # continue; next stop is where it was modified
```
### "Memory corruption / heap bug"
```bash
gdb ./target
(gdb) run
# Crash at free():
(gdb) heap # heap state
(gdb) bins # bin state — often shows corruption here
(gdb) vis_heap_chunks # visualize
(gdb) malloc_chunk <suspicious-addr>
```
---
## Gotchas
- **`bt` looks weird on stripped binaries** — function names become offsets. Use Ghidra's function labels to map back (see [ghidra.md](ghidra.md)).
- **PIE binaries have randomized base addresses.** Addresses you see in Ghidra are unslid; addresses in pwndbg are slid. The `vmmap` command shows the base, and pwndbg's `piebase` command gives you the offset.
- **Optimized builds inline functions.** You'll set a breakpoint on `my_function` and it won't hit because the function was inlined. Either disable optimizations or break on callers.
- **Stack canaries trigger `__stack_chk_fail`.** If you see that in a backtrace, the bug caused a stack-smash; look one frame up.
---
## Phase 9 cleanup specifics
```bash
# Kill gdb / pwndbg sessions
pkill -f 'gdb' || true
pkill -f 'gdbserver' || true
# Remove core dumps generated during session
rm -f ./core ./core.* ~/core.*
# Remove any scripted GDB files
rm -f /tmp/debug-*.gdb
```
@@ -0,0 +1,265 @@
# pwntools — Scripted Binary / Network Interaction
**https://docs.pwntools.com/en/stable/ · https://github.com/Gallopsled/pwntools**
pwntools is a Python framework for building reproducible interactions with binaries and network services. Originally built for CTF exploitation, it's the correct tool for any situation where you need:
- A crafted input sent to a binary or network service, repeatably
- A "failing test" equivalent for a bug that only manifests with specific byte-level input
- A fuzz harness
- An exploit PoC
- Anything where you're tempted to use `echo ... | ./binary` but need more control than shell allows
**Use pwntools for Phase 5 reproduction of binary bugs and Phase 7 tests against binaries.**
---
## Install
```bash
pip install pwntools
# Or in a venv:
python -m venv .venv && source .venv/bin/activate
pip install pwntools
# Verify
python -c 'from pwn import *; print("ok")'
```
On some Linux distros you may need build deps: `apt install python3-dev libssl-dev`.
---
## The core API in five idioms
### 1. Process / Remote — the same interface
```python
from pwn import *
# Local process
p = process('./target')
# Remote service
p = remote('example.com', 1337)
# SSH (tunnel to a remote process)
shell = ssh('user', 'host', password='...')
p = shell.process('./target', cwd='/tmp')
# Same methods on all of the above — this is the value proposition
```
### 2. I/O — the only five methods you need
```python
p.send(b'data') # send bytes
p.sendline(b'data') # send bytes + \n
p.recv(n) # receive up to n bytes
p.recvuntil(b'> ') # receive until pattern (blocks)
p.recvline() # receive until \n
p.interactive() # hand control to your terminal (for manual exploration)
# Combined
p.sendlineafter(b'prompt> ', b'payload')
p.sendafter(b'key:', key)
```
Timeouts:
```python
try:
data = p.recvuntil(b'done', timeout=5)
except pwnlib.exception.EOFError:
print('process died')
except TimeoutError:
print('no response in 5s')
```
### 3. context — set arch/OS once, tools align
```python
context.binary = elf = ELF('./target') # auto-sets arch/os/endianness
# or explicitly:
context.update(arch='amd64', os='linux', endian='little', bits=64)
```
After setting context, helpers like `asm()`, `disasm()`, `cyclic()`, and `ROP()` produce correct output for that target automatically.
### 4. ELF — parse without reverse-engineering by hand
```python
elf = ELF('./target')
elf.symbols['main'] # address of main
elf.plt['printf'] # address in PLT (dynamic linkage)
elf.got['printf'] # GOT entry
elf.address = 0x555555554000 # set base for PIE binaries
elf.search(b'/bin/sh') # find string or bytes in the binary
elf.functions['main'].address # same as elf.symbols['main']
list(elf.functions)[:10] # first 10 function names
```
For the libc that's linked:
```python
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
libc.symbols['system']
```
### 5. cyclic — find offsets without counting
For "where exactly does user input reach this variable" bugs:
```python
p = process('./target')
p.sendline(cyclic(256)) # send a De Bruijn pattern
# Crash occurs; note the crash value (e.g. RIP = 0x6161616c)
offset = cyclic_find(0x6161616c) # returns 12 (or wherever in the pattern)
# Now you know: byte 12 of your input lands at RIP
```
Saves an hour of "pad by N bytes then check" iteration.
---
## Logging during debug
pwntools logs output by default. Configure level in the script:
```python
context.log_level = 'debug' # very verbose — shows sent/received bytes
context.log_level = 'info' # default
context.log_level = 'warning' # quiet
```
For long scripts, log milestones:
```python
log.info('Connected to target')
log.success('Bypassed the check')
log.failure('Canary corrupted')
log.progress('brute-forcing').status('attempt %d' % i)
```
---
## Typical debug-session patterns
### Reproduce a crash with a specific input
```python
# /tmp/debug-repro.py
from pwn import *
context.binary = './target'
p = process('./target')
p.sendlineafter(b'> ', b'<bad input that crashes>')
p.wait()
# If it crashed, p.poll() returns non-zero
assert p.poll() is not None and p.poll() != 0, 'expected crash, got clean exit'
log.success(f'confirmed crash (exit {p.poll()})')
```
Journal this script path. Run it as your "red test":
```bash
python /tmp/debug-repro.py
```
### Fuzz harness for a suspected input class
```python
# /tmp/debug-fuzz.py
from pwn import *
import random
context.binary = './target'
context.log_level = 'warning' # keep quiet in the loop
crashes = []
for i in range(1000):
payload = bytes(random.randint(0, 255) for _ in range(random.randint(1, 100)))
p = process('./target')
p.sendline(payload)
p.wait()
if p.poll() is not None and p.poll() < 0: # crashed by signal
crashes.append((payload, p.poll()))
log.success(f'iter {i}: crash sig={-p.poll()}')
open('/tmp/debug-crashes.txt', 'w').write(repr(crashes))
log.info(f'found {len(crashes)} crashes')
```
### Automated exploit harness (CTF or self-testing a known CVE)
```python
from pwn import *
context.binary = elf = ELF('./target')
libc = elf.libc or ELF('/lib/x86_64-linux-gnu/libc.so.6')
p = process('./target')
# Leak
p.sendline(b'A' * 64 + p64(elf.plt['puts']) + p64(elf.symbols['main']) + p64(elf.got['puts']))
leak = u64(p.recv(6).ljust(8, b'\x00'))
libc.address = leak - libc.symbols['puts']
log.success(f'libc base: {hex(libc.address)}')
# Exploit
rop = ROP(libc)
rop.system(next(libc.search(b'/bin/sh')))
p.sendline(b'A' * 64 + rop.chain())
p.interactive()
```
---
## Integration with gdb / pwndbg
pwntools can launch your process under gdb:
```python
p = gdb.debug('./target', gdbscript='''
break main
continue
''')
```
Or attach to a running pwntools-launched process:
```python
p = process('./target')
gdb.attach(p, gdbscript='break *0x401234')
# continues in a new terminal window with gdb attached
p.sendline(b'trigger input')
```
This is the best way to debug a specific crash repeatably — pwntools drives input, gdb/pwndbg observes runtime state.
---
## Gotchas
- **Python version**: pwntools supports Python 3.8+. Very old distros may not have it.
- **`p.interactive()` blocks.** It's for manual exploration; remove it from automated scripts.
- **ASLR on local runs**: turn off for reproducibility during debugging: `echo 0 | sudo tee /proc/sys/kernel/randomize_va_space` (remember to revert — journal this!).
- **`gdb.debug()` requires `gdb-multiarch`** for cross-arch binaries.
- **Subprocess cleanup**: if your script crashes, orphan `./target` processes may linger. Kill them at Phase 9 or add `atexit` cleanup.
---
## Phase 9 cleanup specifics
```bash
# Remove pwntools debug scripts
rm -f /tmp/debug-*.py
rm -f /tmp/debug-crashes.txt
# Kill orphan target processes from failed runs
pkill -f './target' || true # adjust to actual binary name
# Restore ASLR if disabled
# echo 2 | sudo tee /proc/sys/kernel/randomize_va_space # Linux default
# Revert any binary patches applied for testing (see native-binary.md for details)
```