diff --git a/.claude/skills/ghidra-cli/SKILL.md b/.claude/skills/ghidra-cli/SKILL.md index fc36b32..512785e 100644 --- a/.claude/skills/ghidra-cli/SKILL.md +++ b/.claude/skills/ghidra-cli/SKILL.md @@ -12,526 +12,398 @@ description: > - Ghidra project management --- -# ghidra-cli +# ghidra-cli Agent Reference -A high-performance Rust CLI for automating Ghidra reverse engineering tasks. Designed for both direct usage and AI agent integration. +Rust CLI for Ghidra reverse engineering. Binary name: `ghidra`. -## Architecture Overview - -ghidra-cli uses a **daemon-only architecture**: +## Architecture ``` -┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐ -│ CLI Command │────▶│ Daemon (IPC) │────▶│ GhidraBridge │ -│ ghidra ... │ │ Unix socket │ │ TCP to Ghidra │ -└─────────────────┘ └──────────────────┘ └─────────────────┘ - │ - ▼ - ┌─────────────────┐ - │ bridge.py │ - │ (Ghidra Script)│ - └─────────────────┘ +CLI (Rust/clap) ──TCP──► GhidraCliBridge.java (GhidraScript in Ghidra JVM) ``` -**Key concepts:** -- **Daemon**: Background process managing IPC and Ghidra bridge -- **Bridge**: Python script running inside Ghidra, executing commands -- **Auto-start**: Daemon starts automatically when needed (import, analyze, quick) -- **One daemon per project**: Each project gets its own daemon instance -- **One program per daemon**: Daemon loads a single program for queries +- **Direct bridge**: no daemon process. The Java bridge IS the persistent server. +- One bridge per project, keyed by `~/.local/share/ghidra-cli/bridge-{md5}.port` +- Import/Analyze/query commands **auto-start** the bridge if not running +- Sequential command processing (Ghidra API is not thread-safe) -## When to Use +## Global Flags -Activate when the user requests: -- Binary analysis or reverse engineering -- Decompilation or disassembly -- Function listing, inspection, or renaming -- Cross-reference or call graph analysis -- String or byte pattern searches -- Binary patching or modification -- Ghidra project management +| Flag | Effect | +|------|--------| +| `--json` | Compact JSON output (single line) | +| `--pretty` | Pretty-printed JSON | +| `-v` / `-vv` / `-vvv` | Log verbosity: warn / info / debug | +| `-q` / `--quiet` | Suppress non-essential stderr | + +**Format auto-detection**: TTY → compact human-readable; pipe → json-compact. Override with `--json`, `--pretty`, or `-o FORMAT`. ## Quick Start -### Fastest Path (Auto-Start) - ```bash -# Import and analyze - daemon starts automatically -ghidra quick ./binary - -# Daemon is now running, queries are fast -ghidra function list -ghidra decompile main -``` - -### Full Project Setup - -```bash -# Create project structure -ghidra project create myproject - -# Import binary (auto-starts daemon) -ghidra import ./binary --project myproject --program mybinary - -# Analyze (uses running daemon) +# Fastest path: import + analyze, bridge starts automatically +ghidra import ./binary --project myproject ghidra analyze --project myproject --program mybinary -# All subsequent queries use daemon -ghidra function list -ghidra decompile main -ghidra find string "password" -``` - -### Manual Daemon Control - -```bash -# Start daemon explicitly -ghidra daemon start --project myproject --program mybinary - -# Check status -ghidra daemon status --project myproject - -# Stop daemon -ghidra daemon stop --project myproject - -# Restart with different program -ghidra daemon restart --project myproject --program other_binary +# All subsequent queries reuse the running bridge +ghidra function list --project myproject +ghidra decompile main --project myproject ``` ## Command Reference +### Bridge Lifecycle + +```bash +ghidra start [--project P] [--program PROG] +ghidra stop [--project P] +ghidra restart [--project P] [--program PROG] +ghidra status [--project P] +ghidra ping [--project P] +``` + ### Project Management -| Command | Description | -|---------|-------------| -| `ghidra project create ` | Create new project | -| `ghidra project list` | List all projects | -| `ghidra project info ` | Show project details | -| `ghidra project delete ` | Delete project and all programs | +```bash +ghidra project create NAME +ghidra project list +ghidra project info [NAME] +ghidra project delete NAME +``` ### Import & Analysis -| Command | Description | -|---------|-------------| -| `ghidra import --project

` | Import binary (auto-starts daemon) | -| `ghidra analyze --project

--program ` | Run Ghidra analysis | -| `ghidra quick ` | Import + analyze in one step | +```bash +ghidra import BINARY [--project P] [--program PROG] [--detach] +ghidra analyze [--project P] [--program PROG] [--detach] +``` + +Both auto-start bridge. `--detach` returns immediately. + +### Program Management + +```bash +ghidra program list [--project P] # alias: prog, programs +ghidra program open --program PROG [--project P] +ghidra program close [--project P] +ghidra program delete --program PROG [--project P] +ghidra program info [--project P] +ghidra program export FORMAT [--project P] [-o OUTPUT] # FORMAT: xml, json, asm, c +``` ### Function Operations ```bash -# List functions -ghidra function list -ghidra function list --limit 50 -ghidra function list --filter "size > 100" -ghidra function list --filter "name contains 'crypt'" - -# Get function details -ghidra function get main -ghidra function get 0x401000 - -# Decompile to C-like pseudocode -ghidra decompile main -ghidra decompile 0x401000 - -# Disassemble -ghidra disasm main -ghidra disasm 0x401000 --count 50 - -# Rename function -ghidra function rename sub_401000 decrypt_key +ghidra function list [QUERY_OPTS] # aliases: fn, func, functions +ghidra function get TARGET [QUERY_OPTS] # TARGET = name or 0xADDRESS +ghidra function decompile TARGET [QUERY_OPTS] +ghidra function disasm TARGET [QUERY_OPTS] +ghidra function calls TARGET [QUERY_OPTS] # outgoing calls +ghidra function xrefs TARGET [QUERY_OPTS] # incoming references +ghidra function rename OLD NEW [--project P] [--program PROG] +ghidra function create ADDRESS [NAME] [--project P] [--program PROG] +ghidra function delete TARGET [QUERY_OPTS] ``` -### Search Operations +### Top-level Shortcuts ```bash -# Find functions by pattern (glob) -ghidra find function "*crypt*" -ghidra find function "str*" +ghidra decompile TARGET [QUERY_OPTS] # aliases: decomp, dec +ghidra disasm ADDRESS [-n COUNT] [QUERY_OPTS] # aliases: disassemble, dis +``` -# Find strings -ghidra find string "password" -ghidra find string "error" --case-insensitive +### String Operations -# Find byte patterns (hex, spaces optional) -ghidra find bytes "4883ec08" -ghidra find bytes "48 83 ec 08" +```bash +ghidra strings list [QUERY_OPTS] # aliases: string, str +ghidra strings refs STRING [QUERY_OPTS] # xrefs to string +``` -# Find function calls -ghidra find calls malloc +### Symbol Operations -# Find crypto constants (AES, DES, RSA, etc.) -ghidra find crypto +```bash +ghidra symbol list [QUERY_OPTS] # aliases: sym, symbols +ghidra symbol get NAME [QUERY_OPTS] +ghidra symbol create ADDRESS NAME [--project P] [--program PROG] +ghidra symbol delete NAME [QUERY_OPTS] +ghidra symbol rename OLD NEW [--project P] [--program PROG] +``` -# Find suspicious patterns (anti-debug, obfuscation, etc.) -ghidra find interesting +### Memory Operations + +```bash +ghidra memory map [QUERY_OPTS] # alias: mem +ghidra memory read ADDRESS SIZE [QUERY_OPTS] +ghidra memory write ADDRESS BYTES [--project P] [--program PROG] +ghidra memory search PATTERN [QUERY_OPTS] ``` ### Cross-References ```bash -# References TO an address (who calls/reads this) -ghidra x-ref to 0x401000 -ghidra x-ref to main - -# References FROM an address (what this calls/reads) -ghidra x-ref from 0x401000 -ghidra x-ref from main +ghidra x-ref to ADDRESS [QUERY_OPTS] # aliases: xref, xrefs, crossref +ghidra x-ref from ADDRESS [QUERY_OPTS] +ghidra x-ref list [QUERY_OPTS] ``` -### Call Graphs +### Type Operations ```bash -# Full call graph from function -ghidra graph calls main - -# Callers only (who calls this function) -ghidra graph callers main --depth 3 - -# Callees only (what this function calls) -ghidra graph callees main --depth 3 - -# Export to DOT format for visualization -ghidra graph export dot --output callgraph.dot +ghidra type list [QUERY_OPTS] # alias: types +ghidra type get NAME [QUERY_OPTS] +ghidra type create DEFINITION [--project P] [--program PROG] +ghidra type apply ADDRESS TYPE_NAME [--project P] [--program PROG] ``` -### Symbols +### Comment Operations ```bash -# List all symbols -ghidra symbol list -ghidra symbol list --limit 100 - -# Get symbol at address -ghidra symbol get 0x401000 - -# Create new symbol -ghidra symbol create my_func 0x401000 - -# Rename symbol -ghidra symbol rename old_name new_name - -# Delete symbol -ghidra symbol delete my_func +ghidra comment list [QUERY_OPTS] # alias: comments +ghidra comment get ADDRESS [QUERY_OPTS] +ghidra comment set ADDRESS TEXT [--comment-type TYPE] [--project P] [--program PROG] +ghidra comment delete ADDRESS [QUERY_OPTS] ``` -### Strings +### Search / Find ```bash -# List strings -ghidra strings list -ghidra strings list --limit 100 -ghidra strings list --filter "length > 20" - -# Find string references -ghidra strings refs "error message" +ghidra find string PATTERN [QUERY_OPTS] # alias: search +ghidra find bytes HEX [QUERY_OPTS] +ghidra find function PATTERN [QUERY_OPTS] # glob patterns +ghidra find calls FUNCTION [QUERY_OPTS] +ghidra find crypto [QUERY_OPTS] # detect AES/SHA/RSA constants +ghidra find interesting [QUERY_OPTS] # suspicious patterns ``` -### Data Types +### Graph / Call Graph ```bash -# List data types -ghidra type list -ghidra type list --filter "name contains 'struct'" - -# Get type details -ghidra type get "MyStruct" - -# Create type -ghidra type create "typedef int HANDLE" - -# Apply type to address -ghidra type apply 0x402000 "char[32]" +ghidra graph calls [QUERY_OPTS] # aliases: callgraph, cg +ghidra graph callers FUNCTION [--depth N] [QUERY_OPTS] +ghidra graph callees FUNCTION [--depth N] [QUERY_OPTS] +ghidra graph export FORMAT [QUERY_OPTS] # FORMAT: dot, json ``` -### Memory +### Diff ```bash -# Show memory map -ghidra memory map - -# Read bytes at address -ghidra memory read 0x401000 64 - -# Dump section -ghidra dump section .text +ghidra diff programs PROG1 PROG2 [--project P] [--format F] +ghidra diff functions FUNC1 FUNC2 [--project P] [--format F] ``` -### Comments +### Dump / Export ```bash -# Get comment at address -ghidra comment get 0x401000 - -# Set comment -ghidra comment set 0x401000 "Entry point for decryption" - -# List all comments -ghidra comment list - -# Delete comment -ghidra comment delete 0x401000 +ghidra dump imports [QUERY_OPTS] # alias: export +ghidra dump exports [QUERY_OPTS] +ghidra dump functions [QUERY_OPTS] +ghidra dump strings [QUERY_OPTS] ``` -### Binary Patching +### Patch ```bash -# Patch bytes at address -ghidra patch bytes 0x401000 "90909090" - -# NOP out instructions -ghidra patch nop 0x401000 --count 5 - -# Export patched binary -ghidra patch export --output patched.bin +ghidra patch bytes ADDRESS HEX [--project P] [--program PROG] +ghidra patch nop ADDRESS [--count N] [--project P] [--program PROG] +ghidra patch export -o OUTPUT [--project P] [--program PROG] ``` -### Scripting +### Script Execution ```bash -# List available scripts +ghidra script run PATH [--project P] [--program PROG] [-- ARGS...] +ghidra script python CODE [--project P] [--program PROG] +ghidra script java CODE [--project P] [--program PROG] ghidra script list - -# Run Python script -ghidra script run analysis.py - -# Run with arguments -ghidra script run myscript.py --args "arg1 arg2" - -# Inline Python (access currentProgram, state, etc.) -ghidra script python "print(currentProgram.getName())" - -# Inline Java -ghidra script java "println(currentProgram.getName());" ``` -### Batch Operations +### Batch ```bash -# Run commands from file -ghidra batch commands.txt - -# Commands file format (one per line): -# function list -# decompile main -# find string "password" +ghidra batch SCRIPT_FILE [--project P] [--program PROG] ``` -### Statistics +Batch file: one subcommand per line (without `ghidra` prefix), `#` comments. + +### Universal Query ```bash -# Program statistics -ghidra stats - -# Program summary -ghidra summary +ghidra query DATA_TYPE [QUERY_OPTS] ``` -### Daemon Management +DATA_TYPE: `functions`, `strings`, `imports`, `exports`, `memory`. + +### Statistics & Info ```bash -# Start daemon for project -ghidra daemon start --project myproject --program mybinary - -# Start in foreground (for debugging) -ghidra daemon start --project myproject --program mybinary --foreground - -# Check if daemon is running -ghidra daemon status --project myproject - -# Ping daemon (health check) -ghidra daemon ping --project myproject - -# Clear result cache -ghidra daemon clear-cache --project myproject - -# Stop daemon -ghidra daemon stop --project myproject - -# Restart with new program -ghidra daemon restart --project myproject --program newbinary +ghidra summary [QUERY_OPTS] # alias: info +ghidra stats [QUERY_OPTS] ``` -## Output Formats +### Configuration ```bash -# Human-readable (default for terminal) -ghidra function list - -# JSON output -ghidra function list --json - -# Pretty JSON -ghidra function list --pretty - -# Select specific fields -ghidra function list --fields "name,address,size" - -# Count only -ghidra function list --format count +ghidra init # create config +ghidra doctor # check installation +ghidra version +ghidra config list +ghidra config get KEY +ghidra config set KEY VALUE # keys: ghidra_install_dir, ghidra_project_dir, default_program, default_project, default_output_format, timeout, default_limit +ghidra config reset +ghidra set-default KIND VALUE # KIND: program, project +ghidra setup [--version V] [--dir D] [--force] ``` -## Filtering +## Common Query Options (QUERY_OPTS) -Use expressions to filter results: - -```bash -# Numeric comparisons -ghidra function list --filter "size > 100" -ghidra function list --filter "size >= 50" -ghidra function list --filter "size < 1000" - -# String matching -ghidra function list --filter "name contains 'crypt'" -ghidra function list --filter "name starts_with 'sub_'" -ghidra function list --filter "name ends_with '_init'" - -# Combine with limit -ghidra function list --filter "size > 100" --limit 20 -``` - -## Common Analysis Patterns - -### Investigate a Suspicious Function - -```bash -# Get overview -ghidra function get suspicious_func - -# See the code -ghidra decompile suspicious_func - -# What does it call? -ghidra graph callees suspicious_func --depth 2 - -# Who calls it? -ghidra graph callers suspicious_func --depth 3 - -# Check cross-references -ghidra x-ref to suspicious_func -``` - -### Find Crypto or Sensitive Code - -```bash -# Find crypto constants -ghidra find crypto - -# Find password-related strings -ghidra find string "password" -ghidra find string "key" -ghidra find string "secret" - -# Find crypto function names -ghidra find function "*crypt*" -ghidra find function "*aes*" -ghidra find function "*sha*" -``` - -### Trace Data Flow - -```bash -# Find where data is written -ghidra x-ref to 0x404000 - -# Find where data is read -ghidra x-ref from 0x404000 - -# Trace through call graph -ghidra graph callees source_func --depth 5 -``` - -### Analyze Anti-Analysis Techniques - -```bash -# Find interesting/suspicious patterns -ghidra find interesting - -# Look for timing checks, debugger detection -ghidra find string "IsDebuggerPresent" -ghidra find function "*debug*" - -# Find self-modifying code indicators -ghidra find bytes "e8 00 00 00 00" # call $+5 pattern -``` - -### Patch and Export - -```bash -# Identify patch location -ghidra disasm 0x401000 --count 10 - -# Apply patch -ghidra patch nop 0x401000 --count 2 - -# Verify -ghidra disasm 0x401000 --count 10 - -# Export -ghidra patch export --output patched.exe -``` - -## Error Recovery - -| Situation | Resolution | -|-----------|------------| -| Daemon not running | Commands auto-start daemon; or `ghidra daemon start --project

--program ` | -| No project exists | `ghidra project create ` or use `ghidra quick ` | -| Function not found | Use `ghidra find function "*pattern*"` to search | -| Address format | Use hex with 0x prefix: `0x401000` | -| Slow queries | Daemon should be running; check with `ghidra daemon status` | -| Wrong program loaded | `ghidra daemon restart --project

--program ` | -| Daemon crashed | `ghidra daemon start --project

--program ` | - -## Global Options - -All commands accept: +All query commands accept these: | Option | Description | |--------|-------------| -| `--project ` | Target project (auto-detected if daemon running) | -| `--program ` | Target program within project | -| `--json` | JSON output | -| `--pretty` | Pretty-printed JSON output | -| `--filter ` | Filter expression | -| `--limit ` | Maximum results to return | -| `--fields ` | Comma-separated fields to include | +| `--project P` | Project name or path (env: `GHIDRA_DEFAULT_PROJECT`) | +| `--program PROG` | Program within project (env: `GHIDRA_DEFAULT_PROGRAM`) | +| `--filter EXPR` | Filter expression | +| `--fields LIST` | Comma-separated fields to return | +| `-o FORMAT` | Output format | +| `--limit N` | Max results | +| `--offset N` | Skip first N | +| `--sort FIELDS` | Sort: comma-separated, prefix `-` for descending | +| `--count` | Return count only | +| `--json` | Shorthand for `--format=json` | + +## Output Formats + +| Value | Use | +|-------|-----| +| `compact` | Default for TTY. One line per item. | +| `full` | Multi-line labeled blocks | +| `json` | Pretty JSON | +| `json-compact` | Default for pipes. Single-line JSON. | +| `json-stream` / `ndjson` | One JSON object per line | +| `csv` / `tsv` | Delimited with header | +| `table` | ASCII box-drawn table | +| `count` | Number only | +| `ids` / `minimal` | Address/name only, one per line | +| `tree` | Indented hierarchy | +| `hex` | Hex dump | +| `asm` | Assembly | +| `c` | C pseudocode | + +## Filter Expressions + +```bash +# Numeric +--filter "size > 100" +--filter "size >= 50" + +# String +--filter "name contains 'crypt'" + +# Combined +--filter "size > 100 and name contains 'main'" +--filter "name != 'main'" +``` + +Operators: `>`, `<`, `>=`, `<=`, `=`, `!=`, `contains`, `in`, `and`, `or`. + +## Agent Best Practices + +### 1. Count-First Pattern + +Always check result volume before fetching: + +```bash +ghidra function list --count --project P +# If manageable: +ghidra function list --limit 50 --fields name,address,size --project P +``` + +### 2. Aggressive Filtering + +Pre-filter server-side, not client-side: + +```bash +# GOOD +ghidra function list --filter "size > 1000" --project P +# BAD +ghidra function list --project P # then filter in agent code +``` + +### 3. Field Selection + +Request only needed fields: + +```bash +ghidra function list --fields name,address --json --project P +``` + +### 4. Set Defaults + +Avoid repeating `--project` and `--program`: + +```bash +ghidra set-default project myproject +ghidra set-default program mybinary +# Now: ghidra function list (no flags needed) +``` + +## .NET Warning + +ghidra decompile emits a warning for .NET IL bytecode: +> "This appears to be .NET managed code. Consider using ilspy-cli." + +Use `ilspy detect` to classify binaries before decompiling. + +## Analysis Workflow + +```bash +# 1. Import and analyze +ghidra import ./target.exe --project analysis +ghidra analyze --project analysis + +# 2. Recon +ghidra summary --project analysis +ghidra function list --count --project analysis +ghidra function list --filter "NOT name contains 'FUN_'" --fields name,address,size --limit 30 --project analysis + +# 3. Investigate +ghidra decompile main --project analysis +ghidra find crypto --project analysis +ghidra find string "password" --project analysis + +# 4. Deep dive +ghidra graph callers suspicious_func --depth 3 --project analysis +ghidra x-ref to 0x401000 --project analysis +ghidra function disasm 0x401000 --project analysis + +# 5. Patch +ghidra patch nop 0x401234 --count 3 --project analysis +ghidra patch export -o patched.exe --project analysis +``` ## Environment Variables -| Variable | Description | -|----------|-------------| -| `GHIDRA_INSTALL_DIR` | Path to Ghidra installation | -| `GHIDRA_PROJECT_DIR` | Default project directory | +| Variable | Purpose | +|----------|---------| +| `GHIDRA_INSTALL_DIR` | Ghidra installation path | +| `GHIDRA_DEFAULT_PROJECT` | Default `--project` value | +| `GHIDRA_DEFAULT_PROGRAM` | Default `--program` value | -## Troubleshooting +## File Locations -### Check Installation +| File | Purpose | +|------|---------| +| `~/.local/share/ghidra-cli/bridge-{md5}.port` | TCP port for running bridge | +| `~/.local/share/ghidra-cli/bridge-{md5}.pid` | Bridge process PID | +| `~/.local/share/ghidra-cli/config.yaml` | Configuration | +| `~/.local/share/ghidra-cli/ghidra-cli.log` | Debug log | -```bash -ghidra doctor -``` +## Error Recovery -### View Daemon Logs - -```bash -# Logs are at ~/.local/share/ghidra-cli/daemon.log -tail -f ~/.local/share/ghidra-cli/daemon.log -``` - -### Debug Mode - -```bash -# Run daemon in foreground to see output -ghidra daemon start --project myproject --program mybinary --foreground -``` - -### Reset State - -```bash -# Stop all daemons -ghidra daemon stop --project myproject - -# Remove lock files if needed -rm ~/.local/share/ghidra-cli/daemon-*.lock -``` +| Problem | Fix | +|---------|-----| +| "No project specified" | Add `--project NAME` or `ghidra set-default project NAME` | +| "Bridge not responding" | `ghidra stop --project P` then retry (auto-starts) | +| "Ghidra installation not configured" | `ghidra setup` or set `GHIDRA_INSTALL_DIR` | +| Function not found | Use `ghidra find function "*pattern*"` | +| Slow first command | Normal: bridge startup + analysis takes seconds | diff --git a/.claude/skills/ilspy-cli/SKILL.md b/.claude/skills/ilspy-cli/SKILL.md new file mode 100644 index 0000000..d6411d0 --- /dev/null +++ b/.claude/skills/ilspy-cli/SKILL.md @@ -0,0 +1,261 @@ +--- +name: ilspy-cli +description: > + Use ilspy-cli for .NET assembly decompilation and analysis. Produces clean C# source from .NET IL bytecode. + Activate when the user requests: + - .NET assembly decompilation + - .NET type or method inspection + - Detecting whether a binary is .NET or native + - Searching decompiled .NET source code + - Assembly metadata inspection + Prefer ilspy-cli over ghidra-cli for .NET binaries (ghidra produces poor output for .NET IL). +--- + +# ilspy-cli Agent Reference + +Rust CLI for .NET decompilation using ILSpy (ICSharpCode.Decompiler). Binary name: `ilspy`. + +## Architecture + +``` +Rust CLI (clap) ──netcorehost FFI──► C# Bridge DLL (IlSpyBridge.dll) ──► ICSharpCode.Decompiler +``` + +- **In-process**: .NET runtime hosted inside the Rust binary via `netcorehost` crate +- **No server/daemon**: each command loads the assembly, runs, exits +- **Data exchange**: JSON strings over FFI function pointers with `[UnmanagedCallersOnly]` +- **Key differentiator**: single-method decompilation (`--type T --method M`), which `ilspycmd` cannot do + +## Requirements + +- .NET 8 runtime (loaded by netcorehost at runtime) +- .NET 8 SDK (build time only, for `dotnet publish` of the C# bridge) + +## When to Use ilspy-cli vs ghidra-cli + +| Binary type | Tool | Reason | +|-------------|------|--------| +| .NET (.dll/.exe with CLR header) | `ilspy` | Clean C# decompilation | +| Native (C/C++/Delphi/Go) | `ghidra` | Assembly-level analysis | +| Unknown | `ilspy detect` first | Classifies .NET vs native | + +Use `ilspy detect` to triage before choosing a tool. + +## Global Flags + +| Flag | Effect | +|------|--------| +| `--json` | Minified JSON output | +| `--pretty` | Pretty-printed JSON | +| `--compact` | One line per item | +| `-v` / `--verbose` | Verbose output | + +**Format auto-detection**: TTY → table; non-TTY → compact. + +## Command Reference + +### Detect (.NET vs Native) + +Classify binaries without loading .NET runtime (pure Rust PE header parsing). + +```bash +ilspy detect FILE # single file +ilspy detect DIRECTORY # scan directory (top-level) +ilspy detect DIRECTORY --recursive # scan recursively +ilspy detect DIRECTORY --dotnet-only # show only .NET assemblies +ilspy detect DIRECTORY --native-only # show only native binaries +``` + +**Output fields**: path, isDotnet, framework, recommendedTool + +Framework detection includes: .NET Core/5+/6+/7+/8+, .NET Framework, .NET Standard, Native (Delphi), Native (Qt/C++), Native (MFC/C++). + +The `recommendedTool` field returns `"ilspy"` for .NET or `"ghidra"` for native. + +### List Types + +```bash +ilspy list types ASSEMBLY # all types +ilspy list types ASSEMBLY --filter Controller # substring filter (case-insensitive) +ilspy list types ASSEMBLY --kind class # filter by kind +ilspy list types ASSEMBLY --kind enum +ilspy list types ASSEMBLY --filter Crypto --kind class +``` + +**Kind values**: `class`, `interface`, `struct`, `enum`, `delegate` + +**Output fields**: fullName, ns, name, kind, methodCount, propertyCount, fieldCount, isPublic + +### List Methods + +```bash +ilspy list methods ASSEMBLY # all methods +ilspy list methods ASSEMBLY --type MyNamespace.MyClass # methods of one type +ilspy list methods ASSEMBLY --type MyNamespace.MyClass --filter DoWork # filter by name +``` + +**Output fields**: typeName, name, returnType, parameters (name + type), accessibility, isStatic, isVirtual, isAbstract + +### Decompile + +```bash +ilspy decompile ASSEMBLY # full assembly → C# source +ilspy decompile ASSEMBLY --type MyNamespace.MyClass # single type +ilspy decompile ASSEMBLY --type MyNamespace.MyClass --method DoWork # single method! +``` + +**Output**: C# source code (default). With `--json`/`--pretty`: `{ "source": "...", "typeName": "...", "methodName": "...", "returnType": "..." }` + +`--method` requires `--type` (must specify the containing type). + +**Default format override**: decompile always outputs raw source unless `--json` or `--pretty` is specified. + +### Search Decompiled Source + +```bash +ilspy search ASSEMBLY "ConnectionString" +ilspy search ASSEMBLY "password|secret|key" +ilspy search ASSEMBLY "HttpClient\.Post" +``` + +Pattern is a **regex** (case-insensitive, multiline). The tool decompiles each type and runs the regex against the C# source. + +**Output fields per match**: typeName, matchCount, matches[].line, matches[].matched, matches[].context (surrounding lines) + +### Assembly Info + +```bash +ilspy info ASSEMBLY +``` + +**Output fields**: name, typeCount, targetFramework, references[].name, references[].version + +### Doctor (Health Check) + +```bash +ilspy doctor +``` + +Checks: .NET runtime availability, bridge DLL presence, bridge loading. + +**Environment variable**: `ILSPY_BRIDGE_DIR` overrides bridge DLL search path. + +## Agent Best Practices + +### 1. Triage First + +Always detect before decompiling an unknown binary: + +```bash +ilspy detect ./target.dll --json +# Check recommendedTool field +``` + +### 2. Count-First for Large Assemblies + +```bash +# Check type count +ilspy info MyLib.dll --json +# If typeCount is large, filter: +ilspy list types MyLib.dll --filter Controller --json +``` + +### 3. Targeted Decompilation + +Decompile specific types/methods instead of entire assemblies: + +```bash +# GOOD: specific type +ilspy decompile MyLib.dll --type MyApp.Services.AuthService + +# GOOD: specific method +ilspy decompile MyLib.dll --type MyApp.Services.AuthService --method ValidateToken + +# AVOID for large assemblies: full decompile +ilspy decompile MyLib.dll +``` + +### 4. Search Before Decompile + +Find relevant types first, then decompile: + +```bash +# Find types containing crypto code +ilspy search MyLib.dll "AES|Rijndael|SHA256" --json + +# Decompile the matching type +ilspy decompile MyLib.dll --type MyApp.Security.CryptoHelper +``` + +### 5. Use Compact/JSON for Parsing + +```bash +ilspy list types MyLib.dll --json # minified JSON for piping +ilspy list types MyLib.dll --compact # one line per type +ilspy decompile MyLib.dll --type T --json # source wrapped in JSON +``` + +## Analysis Workflow + +```bash +# 1. Detect binary type +ilspy detect ./target.dll + +# 2. Get overview +ilspy info ./target.dll +ilspy list types ./target.dll --json + +# 3. Find interesting types +ilspy list types ./target.dll --filter Service --kind class +ilspy list types ./target.dll --filter Crypto + +# 4. Inspect methods +ilspy list methods ./target.dll --type MyApp.Services.AuthService + +# 5. Decompile specific method +ilspy decompile ./target.dll --type MyApp.Services.AuthService --method ValidateToken + +# 6. Search for patterns +ilspy search ./target.dll "password|credential|secret" +ilspy search ./target.dll "HttpClient|WebRequest|Socket" +ilspy search ./target.dll "Process\.Start|Shell|Exec" +``` + +## Directory Scanning Workflow + +```bash +# Triage a directory of binaries +ilspy detect "C:\Program Files\MyApp" --recursive --json + +# Focus on .NET assemblies only +ilspy detect "C:\Program Files\MyApp" --recursive --dotnet-only --compact +``` + +## Integration with ghidra-cli + +ghidra-cli's `decompile` command warns when it encounters .NET IL bytecode: +> "This appears to be .NET managed code. Consider using ilspy-cli." + +Workflow for mixed codebases: + +```bash +# 1. Classify all binaries +ilspy detect ./binaries --recursive --json + +# 2. Use ilspy for .NET +ilspy decompile ./binaries/managed.dll --type MyClass + +# 3. Use ghidra for native +ghidra import ./binaries/native.exe --project analysis +ghidra decompile main --project analysis +``` + +## Error Recovery + +| Problem | Fix | +|---------|-----| +| "dotnet not found" | Install .NET 8 SDK: https://dot.net/download | +| "Bridge DLL not found" | `cargo build` or set `ILSPY_BRIDGE_DIR` | +| "Type not found" | Use `ilspy list types ASSEMBLY --filter NAME` to find exact fullName | +| "Method not found" | Use `ilspy list methods ASSEMBLY --type T` to see available methods | +| Bridge load fails | Run `ilspy doctor` for diagnostics | diff --git a/ilspy-cli/bridge/IlSpyBridge.cs b/ilspy-cli/bridge/IlSpyBridge.cs index c5ffa70..12abc17 100644 --- a/ilspy-cli/bridge/IlSpyBridge.cs +++ b/ilspy-cli/bridge/IlSpyBridge.cs @@ -1,13 +1,16 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Reflection.Metadata; using System.Runtime.InteropServices; using System.Text; using System.Text.Json; using System.Text.RegularExpressions; +using System.Threading; using ICSharpCode.Decompiler; using ICSharpCode.Decompiler.CSharp; +using ICSharpCode.Decompiler.CSharp.ProjectDecompiler; using ICSharpCode.Decompiler.Metadata; using ICSharpCode.Decompiler.TypeSystem; @@ -398,6 +401,45 @@ public static class IlSpyBridge } } + [UnmanagedCallersOnly(EntryPoint = "DecompileProject")] + public static IntPtr DecompileProject(IntPtr pathPtr, int pathLen, + IntPtr targetDirPtr, int targetDirLen, + IntPtr resultLenPtr) + { + int resultLen = 0; + try + { + var path = ReadUtf8(pathPtr, pathLen); + var targetDir = ReadUtf8(targetDirPtr, targetDirLen); + + var peFile = new PEFile(path); + + Directory.CreateDirectory(targetDir); + var resolver = new UniversalAssemblyResolver(path, false, null); + var decompiler = new WholeProjectDecompiler(resolver); + + decompiler.DecompileProject(peFile, targetDir, CancellationToken.None); + + var fileCount = Directory.GetFiles(targetDir, "*.cs", SearchOption.AllDirectories).Length; + + var result = new + { + files = fileCount, + directory = targetDir + }; + + var ptr = MarshalJson(result, out resultLen); + Marshal.WriteInt32(resultLenPtr, resultLen); + return ptr; + } + catch (Exception ex) + { + var ptr = MarshalError(ex.Message, out resultLen); + Marshal.WriteInt32(resultLenPtr, resultLen); + return ptr; + } + } + [UnmanagedCallersOnly(EntryPoint = "FreeMem")] public static void FreeMem(IntPtr ptr) { diff --git a/ilspy-cli/src/bridge/ffi.rs b/ilspy-cli/src/bridge/ffi.rs index 430704f..a657494 100644 --- a/ilspy-cli/src/bridge/ffi.rs +++ b/ilspy-cli/src/bridge/ffi.rs @@ -33,6 +33,8 @@ pub type FnThreeArgs = unsafe extern "system" fn( /// FFI function signature for FreeMem. pub type FnFreeMem = unsafe extern "system" fn(*mut u8); +/// Call a two-arg bridge function and return the JSON string result. + /// Call a one-arg bridge function and return the JSON string result. pub unsafe fn call_one_arg(func: FnOneArg, free: FnFreeMem, arg1: &str) -> String { let mut result_len: c_int = 0; diff --git a/ilspy-cli/src/bridge/mod.rs b/ilspy-cli/src/bridge/mod.rs index 7bbd32b..1d9bfd0 100644 --- a/ilspy-cli/src/bridge/mod.rs +++ b/ilspy-cli/src/bridge/mod.rs @@ -17,6 +17,7 @@ pub struct IlSpyBridge { decompile_type_fn: FnTwoArgs, decompile_method_fn: FnThreeArgs, decompile_full_fn: FnOneArg, + decompile_project_fn: FnTwoArgs, assembly_info_fn: FnOneArg, search_fn: FnTwoArgs, free_fn: FnFreeMem, @@ -73,6 +74,7 @@ impl IlSpyBridge { decompile_type_fn: load!("DecompileType", FnTwoArgs), decompile_method_fn: load!("DecompileMethod", FnThreeArgs), decompile_full_fn: load!("DecompileFull", FnOneArg), + decompile_project_fn: load!("DecompileProject", FnTwoArgs), assembly_info_fn: load!("GetAssemblyInfo", FnOneArg), search_fn: load!("SearchSource", FnTwoArgs), free_fn: load!("FreeMem", FnFreeMem), @@ -137,6 +139,12 @@ impl IlSpyBridge { Self::parse_result(&json) } + /// Decompile a project into a directory of per-type .cs files. + pub fn decompile_project(&self, assembly: &str, target_dir: &str) -> Result { + let json = unsafe { call_two_args(self.decompile_project_fn, self.free_fn, assembly, target_dir) }; + Self::parse_result(&json) + } + // ── Internal helpers ───────────────────────────────────────────── fn parse_result(json: &str) -> Result { diff --git a/ilspy-cli/src/cli.rs b/ilspy-cli/src/cli.rs index 8f14347..db070a4 100644 --- a/ilspy-cli/src/cli.rs +++ b/ilspy-cli/src/cli.rs @@ -103,6 +103,10 @@ pub struct DecompileArgs { /// Decompile a specific method (requires --type) #[arg(long, short, requires = "type")] pub method: Option, + + /// Output directory for project-style decompilation (one .cs file per type) + #[arg(long, short = 'o')] + pub output_dir: Option, } #[derive(Args, Debug)] diff --git a/ilspy-cli/src/commands/decompile.rs b/ilspy-cli/src/commands/decompile.rs index cb8711d..9f9a22f 100644 --- a/ilspy-cli/src/commands/decompile.rs +++ b/ilspy-cli/src/commands/decompile.rs @@ -8,6 +8,30 @@ pub fn decompile(bridge: &IlSpyBridge, args: &DecompileArgs, fmt: OutputFormat) .unwrap_or_else(|_| args.assembly.clone()); let path_str = assembly.to_string_lossy(); + if let Some(output_dir) = &args.output_dir { + if args.r#type.is_some() || args.method.is_some() { + return Err(crate::error::IlSpyError::BridgeCallFailed( + "--output-dir cannot be used with --type or --method".to_string() + )); + } + + let target_dir = dunce::canonicalize(output_dir) + .unwrap_or_else(|_| output_dir.clone()); + let target_str = target_dir.to_string_lossy(); + + let result = bridge.decompile_project(&path_str, &target_str)?; + + return Ok(match fmt { + OutputFormat::Json => serde_json::to_string(&result).unwrap_or_default(), + OutputFormat::JsonPretty => serde_json::to_string_pretty(&result).unwrap_or_default(), + _ => { + let files = result.get("files").and_then(|v| v.as_u64()).unwrap_or(0); + let dir = result.get("directory").and_then(|v| v.as_str()).unwrap_or(""); + format!("Decompiled {} files to {}", files, dir) + } + }); + } + let result = match (&args.r#type, &args.method) { (Some(type_name), Some(method_name)) => { // Single method decompilation diff --git a/src/ghidra/scripts/GhidraCliBridge.java b/src/ghidra/scripts/GhidraCliBridge.java index 46e1d15..a32de24 100644 --- a/src/ghidra/scripts/GhidraCliBridge.java +++ b/src/ghidra/scripts/GhidraCliBridge.java @@ -243,6 +243,8 @@ public class GhidraCliBridge extends GhidraScript { case "batch": return handleBatch(args); // Bridge info case "bridge_info": return handleBridgeInfo(); + // Memory read + case "read_memory": return handleReadMemory(args); default: return null; } } @@ -2603,4 +2605,74 @@ public class GhidraCliBridge extends GhidraScript { // Batch operations are handled by the Rust side, not the bridge directly return errorResult("Batch operations are handled by the CLI, not via bridge script"); } + + // --- Memory Read Handler --- + + private JsonObject handleReadMemory(JsonObject args) { + String addrStr = getArgString(args, "address"); + if (addrStr == null) return errorResult("Address required"); + + int size = 200; + if (args != null && args.has("size")) { + size = args.get("size").getAsInt(); + } + + try { + ghidra.program.model.mem.Memory mem = currentProgram.getMemory(); + ghidra.program.model.address.AddressFactory af = currentProgram.getAddressFactory(); + + // Parse address + long addrLong; + if (addrStr.startsWith("0x") || addrStr.startsWith("0X")) { + addrLong = Long.parseUnsignedLong(addrStr.substring(2), 16); + } else { + addrLong = Long.parseUnsignedLong(addrStr, 16); + } + + ghidra.program.model.address.Address baseAddr = af.getDefaultAddressSpace().getAddress(addrLong); + + // Read bytes + byte[] bytes = new byte[size]; + int bytesRead = mem.getBytes(baseAddr, bytes); + + // Build hex string + StringBuilder hexStr = new StringBuilder(); + for (int i = 0; i < bytesRead; i++) { + hexStr.append(String.format("%02x", bytes[i] & 0xFF)); + } + + // Also interpret as array of 8-byte pointers + JsonArray pointers = new JsonArray(); + for (int i = 0; i + 7 < bytesRead; i += 8) { + long val = 0; + for (int j = 0; j < 8; j++) { + val |= ((long)(bytes[i+j] & 0xFF)) << (8*j); + } + JsonObject ptrObj = new JsonObject(); + ptrObj.addProperty("offset", i); + ptrObj.addProperty("address", String.format("0x%08x", addrLong + i)); + ptrObj.addProperty("value", String.format("0x%016x", val)); + + // Check if value looks like a code address + if (val >= 0x00401000L && val <= 0x05bb99ffL) { + ghidra.program.model.address.Address funcAddr = af.getDefaultAddressSpace().getAddress(val); + ghidra.program.model.listing.Function func = currentProgram.getFunctionManager().getFunctionAt(funcAddr); + if (func != null) { + ptrObj.addProperty("function", func.getName()); + } + } + + pointers.add(ptrObj); + } + + JsonObject result = new JsonObject(); + result.addProperty("address", String.format("0x%08x", addrLong)); + result.addProperty("size", bytesRead); + result.addProperty("hex", hexStr.toString()); + result.add("pointers", pointers); + return result; + } catch (Exception e) { + return errorResult("Failed to read memory: " + e.getMessage()); + } + } } diff --git a/src/main.rs b/src/main.rs index 130d4d4..7b99cea 100644 --- a/src/main.rs +++ b/src/main.rs @@ -432,9 +432,23 @@ fn run_with_bridge(cli: Cli) -> anyhow::Result<()> { anyhow::bail!("Binary not found: {}", args.binary); } - // Check if bridge is already running - if let Some(port) = bridge::is_bridge_running(&project_path) { - // Bridge running - import via bridge command + // First, robustly check if a bridge is already running using ensure_bridge_running + // with Project mode. This will reuse an existing bridge or start a minimal one. + let existing_bridge_port = bridge::read_port_file(&project_path) + .ok() + .flatten() + .and_then(|port| { + // Verify this port is actually reachable + let client = BridgeClient::new(port); + if client.ping().unwrap_or(false) { + Some(port) + } else { + None + } + }); + + if let Some(port) = existing_bridge_port { + // Bridge is running - import via TCP command let client = BridgeClient::new(port); verify_bridge(&client)?; let result = client.import_binary(&args.binary, args.program.as_deref())?;