docs(shared-skills): batch 43 (4 files)
This commit is contained in:
@@ -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)
|
||||
```
|
||||
Reference in New Issue
Block a user