Merge pull request #1 from akiselev/claude/rust-binary-cli-Lu99H

Build Rust CLI for binary reverse engineering
This commit is contained in:
Alexander Kiselev
2026-01-12 13:21:00 -08:00
committed by GitHub
19 changed files with 6622 additions and 2 deletions
+463
View File
@@ -0,0 +1,463 @@
# Ghidra CLI - Claude Code Skill
This skill enables Claude Code and other AI agents to efficiently reverse engineer binaries using Ghidra through a powerful CLI interface.
## Quick Reference
### Most Common Commands
```bash
# Count-first workflow (ALWAYS use this pattern)
ghidra query functions --program=<binary> --count
ghidra query functions --program=<binary> --filter="<expr>" --count
ghidra query functions --program=<binary> --filter="<expr>" --fields=name,address --format=json-compact
# Query data types
ghidra query functions|strings|imports|exports|memory --program=<binary> [options]
# Decompile
ghidra decompile <addr|name> --program=<binary>
# Dump data
ghidra dump imports|exports|functions|strings --program=<binary> [options]
```
## Setup & Initialization
```bash
# First time setup
ghidra init
# Check installation
ghidra doctor
# Import a binary
ghidra import <binary-path> --project=<project-name>
# Quick analysis (import + analyze + summary)
ghidra quick <binary-path>
```
## Universal Query Command
The `query` command is your primary tool. It supports filtering, field selection, and multiple output formats.
### Syntax
```bash
ghidra query <data-type> --program=<binary> [options]
```
### Data Types
- `functions` - All functions
- `strings` - String literals
- `imports` - Imported functions
- `exports` - Exported functions
- `memory` - Memory regions
- `symbols` - Symbol table
- `xrefs` - Cross-references
### Options
```bash
--filter="<expression>" # Filter results
--fields=<list> # Select specific fields (comma-separated)
--format=<format> # Output format (json, json-compact, table, minimal, count)
--limit=<n> # Max results
--offset=<n> # Skip first n results
--sort=<field> # Sort by field (prefix with - for descending)
--count # Just return count
```
## Filter Language
### Comparison Operators
```bash
field=value # Exact match
field!=value # Not equal
field>value # Greater than
field>=value # Greater or equal
field<value # Less than
field<=value # Less or equal
```
### String Operators
```bash
field~pattern # Contains (case-insensitive)
field^pattern # Starts with
field$pattern # Ends with
field=~regex # Regex match
```
### Logical Operators
```bash
expr AND expr # Both conditions
expr OR expr # Either condition
NOT expr # Negation
(expr) # Grouping
```
### Special Operators
```bash
field EXISTS # Field is present
field IN [val1,val2] # One of values
```
## Essential Workflows
### 1. Initial Binary Analysis
```bash
# Get summary
ghidra summary --program=<binary>
# Count functions
ghidra query functions --program=<binary> --count
# Count named functions
ghidra query functions --program=<binary> --filter="NOT name^FUN_" --count
# List named functions (minimal output)
ghidra query functions --program=<binary> \
--filter="NOT name^FUN_" \
--fields=name,address,size \
--format=json-compact \
--limit=50
```
### 2. Finding Interesting Functions
```bash
# Large functions
ghidra query functions --program=<binary> \
--filter="size>1000" \
--fields=name,address,size \
--sort=-size \
--limit=20
# Functions with specific keywords
ghidra query functions --program=<binary> \
--filter="name~crypt OR name~encrypt OR name~password" \
--fields=name,address \
--format=json-compact
# Functions that call specific APIs
ghidra query functions --program=<binary> \
--filter="calls~WinExec OR calls~CreateProcess" \
--format=json-compact
```
### 3. String Analysis
```bash
# Count strings
ghidra query strings --program=<binary> --count
# Find URLs
ghidra query strings --program=<binary> \
--filter="value~http" \
--fields=value,address \
--format=minimal
# Find long strings (potential paths/URLs)
ghidra query strings --program=<binary> \
--filter="length>50" \
--format=json-compact \
--limit=20
# Find specific keywords
ghidra query strings --program=<binary> \
--filter="value~password OR value~key OR value~token" \
--format=json-compact
```
### 4. Import Analysis
```bash
# List all imports
ghidra dump imports --program=<binary> --format=json-compact
# Find suspicious imports
ghidra query imports --program=<binary> \
--filter="name IN [CreateProcess,WinExec,ShellExecute,WriteFile,CreateRemoteThread]" \
--format=json-compact
# Find crypto imports
ghidra query imports --program=<binary> \
--filter="name~Crypt" \
--format=json-compact
```
### 5. Decompilation
```bash
# Decompile by address
ghidra decompile 0x401000 --program=<binary>
# Decompile by name
ghidra decompile main --program=<binary>
# Decompile with minimal output
ghidra fn decompile 0x401000 --program=<binary> --format=compact
```
### 6. Cross-Reference Analysis
```bash
# Find what calls a function
ghidra query xrefs --program=<binary> \
--filter="to~WinExec" \
--fields=from,from_function \
--format=json-compact
# Find all callers to an address
ghidra xref to 0x401000 --program=<binary> --format=json-compact
```
## Output Formats
Choose the right format for your use case:
- `count` - Just the number (best for checking result size)
- `json-compact` - Minimal JSON (best for LLMs)
- `minimal` - Just addresses/names (good for piping)
- `ids` - Just IDs (good for further queries)
- `table` - Human-readable table (good for display)
- `json` - Full JSON (when you need all data)
## Best Practices for LLMs
### 1. Always Count First
```bash
# BAD: Fetching all data without knowing size
ghidra query functions --program=<binary>
# GOOD: Count first, then filter
ghidra query functions --program=<binary> --count
ghidra query functions --program=<binary> --filter="size>1000" --count
ghidra query functions --program=<binary> --filter="size>1000" --format=json-compact
```
### 2. Use Aggressive Filtering
```bash
# BAD: Fetching then filtering in code
ghidra query functions --program=<binary> --format=json
# GOOD: Filter on Ghidra side
ghidra query functions --program=<binary> \
--filter="size>1000 AND name~crypt" \
--format=json-compact
```
### 3. Select Only Needed Fields
```bash
# BAD: Getting all fields
ghidra query functions --program=<binary>
# GOOD: Select only what you need
ghidra query functions --program=<binary> \
--fields=name,address,size \
--format=json-compact
```
### 4. Paginate Large Results
```bash
# Get first page
ghidra query functions --program=<binary> --limit=50
# Get next page
ghidra query functions --program=<binary> --limit=50 --offset=50
```
### 5. Use Appropriate Output Format
```bash
# For analysis: json-compact
ghidra query functions --program=<binary> --format=json-compact
# For display: table
ghidra query functions --program=<binary> --format=table
# For piping: minimal or ids
ghidra query functions --program=<binary> --format=ids
```
## Common Analysis Patterns
### Pattern 1: Find Entry Points
```bash
# Find main or WinMain
ghidra query functions --program=<binary> \
--filter="name~main OR name~WinMain OR name~DllMain" \
--format=json-compact
```
### Pattern 2: Find Crypto Functions
```bash
# By name
ghidra query functions --program=<binary> \
--filter="name~crypt OR name~cipher OR name~hash OR name~aes OR name~rsa" \
--format=json-compact
# By imports
ghidra query imports --program=<binary> \
--filter="name~Crypt" \
--format=json-compact
```
### Pattern 3: Find Network Functions
```bash
# By imports
ghidra query imports --program=<binary> \
--filter="name~socket OR name~connect OR name~send OR name~recv OR name~http" \
--format=json-compact
```
### Pattern 4: Find File Operations
```bash
ghidra query imports --program=<binary> \
--filter="name~File OR name~Read OR name~Write OR name~Create" \
--format=json-compact
```
### Pattern 5: Find Suspicious Strings
```bash
# Registry keys
ghidra query strings --program=<binary> \
--filter="value~HKEY OR value~Software" \
--format=json-compact
# URLs
ghidra query strings --program=<binary> \
--filter="value~http" \
--format=json-compact
# Credentials
ghidra query strings --program=<binary> \
--filter="value~password OR value~username OR value~token" \
--format=json-compact
```
## Error Handling
### Common Errors
1. **Program not specified**: Use `--program=<binary>` or set default with `ghidra set-default program <binary>`
2. **Ghidra not found**: Run `ghidra init` or set `GHIDRA_INSTALL_DIR`
3. **Analysis timeout**: Increase with `set GHIDRA_TIMEOUT=600`
4. **Project not found**: Create with `ghidra project create <name>`
### Troubleshooting
```bash
# Check installation
ghidra doctor
# List available projects
ghidra project list
# Show current configuration
ghidra config list
```
## Configuration
### Set Defaults
```bash
# Set default program (so you don't have to pass --program each time)
ghidra set-default program <binary>
# Set default project
ghidra set-default project <project-name>
```
### Environment Variables
```bash
# Windows
set GHIDRA_INSTALL_DIR=C:\ghidra\ghidra_11.0
set GHIDRA_DEFAULT_PROGRAM=malware.exe
# Unix
export GHIDRA_INSTALL_DIR=/opt/ghidra
export GHIDRA_DEFAULT_PROGRAM=malware.elf
```
## Example Workflow
Here's a complete analysis workflow:
```bash
# 1. Import and analyze
ghidra import suspicious.exe --project=analysis
# 2. Get overview
ghidra summary --program=suspicious.exe
# 3. Count functions
ghidra query functions --program=suspicious.exe --count
# Output: 1247
# 4. Count named functions
ghidra query functions --program=suspicious.exe --filter="NOT name^FUN_" --count
# Output: 89
# 5. Get named functions
ghidra query functions --program=suspicious.exe \
--filter="NOT name^FUN_" \
--fields=name,address,size \
--format=json-compact
# 6. Find suspicious imports
ghidra dump imports --program=suspicious.exe \
--filter="name~Exec OR name~Create OR name~Write" \
--format=json-compact
# 7. Find interesting strings
ghidra query strings --program=suspicious.exe \
--filter="value~http OR value~password" \
--format=json-compact
# 8. Decompile interesting functions
ghidra decompile 0x401000 --program=suspicious.exe
```
## Tips
1. **Always count before fetching** - Prevents overwhelming your context
2. **Use filters aggressively** - Pre-filter on Ghidra side
3. **Select minimal fields** - Reduces token usage
4. **Use json-compact format** - Most efficient for LLMs
5. **Set defaults** - Avoids repeating `--program` and `--project`
6. **Paginate large results** - Use `--limit` and `--offset`
7. **Cache results** - Store commonly-used queries in variables
## Advanced: Chaining Commands
```bash
# Get list of function addresses, then decompile each
FUNCS=$(ghidra query functions --program=<binary> \
--filter="name~suspicious" \
--format=ids)
for addr in $FUNCS; do
ghidra decompile $addr --program=<binary> --format=compact
done
```
This skill gives you powerful, token-efficient access to Ghidra for binary analysis!
Generated
+1348
View File
File diff suppressed because it is too large Load Diff
+64
View File
@@ -0,0 +1,64 @@
[package]
name = "ghidra-cli"
version = "0.1.0"
edition = "2021"
authors = ["Alexander Kiselev"]
description = "Rust CLI to run Ghidra headless for reverse engineering with Claude Code and other agents"
license = "GPL-3.0"
repository = "http://127.0.0.1:62915/git/akiselev/ghidra-cli"
[dependencies]
# CLI framework
clap = { version = "4.5", features = ["derive", "env", "cargo"] }
# Serialization
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
serde_yaml = "0.9"
# Parsing
pest = "2.7"
pest_derive = "2.7"
# Output formatting
tabled = "0.15"
comfy-table = "7.1"
# Error handling
anyhow = "1.0"
thiserror = "1.0"
# Logging
env_logger = "0.11"
log = "0.4"
# File system & paths
dirs = "5.0"
tempfile = "3.8"
walkdir = "2.4"
# Process management
which = "6.0"
# Regex
regex = "1.10"
lazy_static = "1.4"
# CSV/TSV
csv = "1.3"
# String similarity (for fuzzy matching)
strsim = "0.11"
# Cross-platform support
dunce = "1.0" # Windows path handling
atty = "0.2" # TTY detection
[dev-dependencies]
assert_cmd = "2.0"
predicates = "3.0"
tempfile = "3.8"
[[bin]]
name = "ghidra"
path = "src/main.rs"
+425 -2
View File
@@ -1,2 +1,425 @@
# ghidra-cli
Rust cli to run ghidra headless on files so that Claude Code and other agents can reverse engineer stuff
# Ghidra CLI
A powerful Rust CLI tool for Ghidra reverse engineering, designed for Claude Code and other AI agents to efficiently analyze binaries.
## Features
- 🚀 **Universal Query System** - Query any Ghidra data type with a single command
- 🔍 **Advanced Filtering** - Powerful filter language for precise data extraction
- 📊 **Multiple Output Formats** - JSON, CSV, TSV, Table, and more
- 🤖 **LLM-Optimized** - Designed for minimal token usage and maximum efficiency
- 🪟 **Windows-First** - Native Windows support with cross-platform compatibility
- 📦 **Zero Configuration** - Auto-detection of Ghidra installation
-**Fast** - Direct headless Ghidra integration
## Installation
### Prerequisites
- [Ghidra](https://ghidra-sre.org/) 10.0 or later
- Rust 1.70+ (for building from source)
### From Source
```bash
git clone https://github.com/yourusername/ghidra-cli
cd ghidra-cli
cargo build --release
```
The binary will be at `target/release/ghidra.exe` (Windows) or `target/release/ghidra` (Unix).
### Setup
1. Run the initialization wizard:
```bash
ghidra init
```
2. Set your Ghidra installation (if not auto-detected):
```bash
set GHIDRA_INSTALL_DIR=C:\ghidra\ghidra_11.0
```
3. Verify installation:
```bash
ghidra doctor
```
## Quick Start
```bash
# Quick analysis of a binary
ghidra quick malware.exe
# Import a binary
ghidra import suspicious.exe --project=analysis
# Query functions
ghidra query functions --program=suspicious.exe --filter="size>1000"
# Decompile a function
ghidra decompile 0x401000 --program=suspicious.exe
# List suspicious imports
ghidra dump imports --program=suspicious.exe --filter="name~Crypt OR name~Process"
```
## Universal Query Command
The `query` command is the primary interface for data extraction:
```bash
ghidra query <data-type> [options]
```
### Supported Data Types
- `functions` - All functions in the program
- `strings` - String data
- `imports` - Import table
- `exports` - Export table
- `memory` - Memory regions
- `symbols` - Symbol table
- `xrefs` - Cross-references
- `comments` - All comments
- `types` - Data types
### Query Options
```bash
--filter="<expression>" # Filter results
--fields=<list> # Select specific fields
--format=<format> # Output format
--limit=<n> # Max results
--offset=<n> # Skip first n results
--sort=<field> # Sort order
--count # Just return count
```
### Filter Language
```bash
# Comparison operators
name=malloc # Exact match
size>1000 # Greater than
address>=0x401000 # Greater or equal
# String operators
name~crypt # Contains (case-insensitive)
name^sub_ # Starts with
name$_exit # Ends with
name=~"regex" # Regex match
# Logical operators
name~crypt AND size>500 # AND
name~main OR name~start # OR
NOT name^FUN_ # NOT
# Special operators
calls EXISTS # Field exists
name IN [malloc,free] # One of values
size>=100 AND size<=1000 # Range
```
## Examples
### Function Analysis
```bash
# List all functions
ghidra query functions --program=malware.exe
# Find large functions with crypto in the name
ghidra query functions --program=malware.exe \
--filter="size>1000 AND name~crypt" \
--format=json-compact
# Count unnamed functions
ghidra query functions --program=malware.exe \
--filter="name^FUN_" \
--count
# Get specific fields only
ghidra query functions --program=malware.exe \
--fields=name,address,size \
--limit=10
```
### String Analysis
```bash
# Find HTTP/HTTPS URLs
ghidra query strings --program=malware.exe \
--filter="value~http"
# Find long strings (potential paths, URLs)
ghidra query strings --program=malware.exe \
--filter="length>50" \
--format=minimal
```
### Import Analysis
```bash
# Find suspicious imports
ghidra query imports --program=malware.exe \
--filter="name IN [CreateProcess,WinExec,ShellExecute]"
# Find crypto imports
ghidra query imports --program=malware.exe \
--filter="name~Crypt" \
--format=table
```
### Memory Analysis
```bash
# List executable memory regions
ghidra query memory --program=malware.exe \
--filter="permissions~x" \
--format=table
```
### Decompilation
```bash
# Decompile a specific function
ghidra decompile 0x401000 --program=malware.exe
# Decompile by name
ghidra decompile main --program=malware.exe
# Get compact output
ghidra fn decompile suspicious_func --program=malware.exe \
--format=compact
```
## Specialized Commands
### Function Commands
```bash
ghidra fn list [options] # List functions
ghidra fn get <addr|name> [options] # Get function details
ghidra fn decompile <addr|name> [options] # Decompile
ghidra fn calls <addr|name> [options] # What it calls
ghidra fn xrefs <addr|name> [options] # What calls it
```
### String Commands
```bash
ghidra strings [options] # List all strings
ghidra strings refs <string> [options] # Get references
```
### Memory Commands
```bash
ghidra mem map [options] # Memory map
ghidra mem read <addr> <size> [options] # Read memory
ghidra mem search <pattern> [options] # Search for pattern
```
### Dump Commands
```bash
ghidra dump imports [options] # All imports
ghidra dump exports [options] # All exports
ghidra dump functions [options] # All functions
ghidra dump strings [options] # All strings
```
## Output Formats
```
full - Full human-readable (default for TTY)
compact - One-line summaries
minimal - Just addresses/names
json - Full JSON
json-compact - Minimal JSON
json-stream - NDJSON (one per line)
csv - CSV format
tsv - TSV format
table - Pretty table
ids - Just addresses/IDs
count - Just count
```
## Configuration
### Environment Variables
```bash
GHIDRA_INSTALL_DIR # Path to Ghidra installation
GHIDRA_PROJECT_DIR # Project directory
GHIDRA_DEFAULT_PROGRAM # Default program to analyze
GHIDRA_DEFAULT_PROJECT # Default project name
GHIDRA_TIMEOUT # Command timeout (seconds)
```
### Configuration File
Located at:
- Windows: `%APPDATA%\ghidra-cli\config.yaml`
- Linux/Mac: `~/.config/ghidra-cli/config.yaml`
```yaml
ghidra_install_dir: C:\ghidra\ghidra_11.0
ghidra_project_dir: C:\Users\username\.ghidra-projects
default_program: malware.exe
default_project: analysis
default_output_format: json-compact
default_limit: 1000
timeout: 300
```
### Set Defaults
```bash
# Set default program
ghidra set-default program malware.exe
# Set default project
ghidra set-default project analysis
```
## LLM-Optimized Workflow
For Claude Code and other agents:
```bash
# 1. Count first (check result size)
ghidra query functions --program=malware.exe --count
# → 1,247 functions
# 2. Refine filter and count
ghidra query functions --program=malware.exe \
--filter="NOT name^FUN_" \
--count
# → 89 named functions
# 3. Get minimal data
ghidra query functions --program=malware.exe \
--filter="NOT name^FUN_" \
--fields=name,address \
--format=json-compact
# 4. Deep dive on specific items
ghidra fn decompile <address> --program=malware.exe \
--format=compact
```
## Project Management
```bash
# Create project
ghidra project create myproject
# List projects
ghidra project list
# Delete project
ghidra project delete myproject
```
## Scripting
### Run Custom Scripts
```bash
# Run a Python script
ghidra script run my_analysis.py --program=malware.exe -- arg1 arg2
# Execute inline Python
ghidra script python "print(currentProgram.getName())" --program=malware.exe
# Execute inline Java
ghidra script java "println(currentProgram.getName());" --program=malware.exe
```
### Built-in Scripts
The CLI includes built-in scripts for:
- Function listing
- Decompilation
- String extraction
- Import/Export tables
- Memory map
- Cross-references
- Program information
## Windows-Specific Notes
### Path Handling
The CLI handles both Unix-style (`/`) and Windows-style (`\`) paths automatically.
### Ghidra Installation Detection
Auto-detection checks these locations:
- `C:\Program Files\Ghidra`
- `C:\Program Files (x86)\Ghidra`
- `C:\ghidra`
- Registry entries (if available)
### Executable Detection
Supports common Windows formats:
- `.exe` - Executables
- `.dll` - Dynamic libraries
- `.sys` - System drivers
## Performance Tips
1. **Use `--count` first** - Check result size before fetching data
2. **Filter aggressively** - Pre-filter on Ghidra side, not in your code
3. **Select minimal fields** - Use `--fields` to reduce data transfer
4. **Use compact formats** - `json-compact` or `minimal` for LLMs
5. **Paginate large results** - Use `--limit` and `--offset`
## Troubleshooting
### Ghidra Not Found
```bash
# Check doctor
ghidra doctor
# Set manually
set GHIDRA_INSTALL_DIR=C:\path\to\ghidra
# Or in config
ghidra config set ghidra_install_dir C:\path\to\ghidra
```
### Analysis Timeout
```bash
# Increase timeout
set GHIDRA_TIMEOUT=600
# Or in config
ghidra config set timeout 600
```
### Project Issues
```bash
# List projects
ghidra project list
# Delete and recreate
ghidra project delete myproject
ghidra project create myproject
```
## License
GNU General Public License v3.0 - see [LICENSE](LICENSE) for details.
## Credits
- [Ghidra](https://ghidra-sre.org/) - NSA's reverse engineering framework
- Built with ❤️ for Claude Code and the AI agent community
+483
View File
@@ -0,0 +1,483 @@
# Ghidra Binary Analysis Subagent
## Overview
This subagent specializes in reverse engineering binaries using Ghidra CLI. It provides efficient, token-optimized access to binary analysis capabilities for Claude Code and other AI agents.
## When to Use This Subagent
Use this subagent when you need to:
- Analyze binary executables (PE, ELF, Mach-O)
- Reverse engineer malware or suspicious binaries
- Extract functions, strings, imports/exports from binaries
- Decompile functions to understand behavior
- Find specific patterns in binary code
- Analyze memory layout and structure
- Identify crypto functions, network operations, or file I/O
- Generate reports on binary capabilities
## Capabilities
### Data Extraction
- **Functions**: List, filter, and decompile functions
- **Strings**: Extract and search string literals
- **Imports/Exports**: Analyze external dependencies
- **Memory Layout**: View memory regions and permissions
- **Symbols**: Access symbol table
- **Cross-References**: Find call relationships
### Analysis Features
- **Universal Query System**: Query any data type with powerful filters
- **Advanced Filtering**: Complex boolean expressions with field-level filtering
- **Multiple Output Formats**: JSON, CSV, Table, minimal (token-efficient)
- **Decompilation**: Convert assembly to C-like pseudocode
- **Pattern Matching**: Find specific code patterns and strings
### LLM Optimizations
- **Count-First Workflow**: Check result sizes before fetching data
- **Field Selection**: Request only needed fields
- **Aggressive Filtering**: Pre-filter on Ghidra side
- **Compact Formats**: Minimal token usage with `json-compact`
- **Pagination**: Handle large datasets efficiently
## Command Reference
### Quick Start
```bash
# Import and analyze a binary
ghidra import <binary-path> --project=<project>
# Quick analysis (all-in-one)
ghidra quick <binary-path>
# Get program summary
ghidra summary --program=<binary>
```
### Universal Query
```bash
# Query any data type
ghidra query <data-type> --program=<binary> [options]
# Data types: functions, strings, imports, exports, memory, symbols, xrefs
# Essential options:
--filter="<expression>" # Filter results
--fields=<list> # Select specific fields
--format=<format> # Output format (json, json-compact, table, count)
--limit=<n> # Max results
--count # Just return count
```
### Common Queries
```bash
# List functions with filtering
ghidra query functions --program=<binary> \
--filter="size>1000 AND name~crypt" \
--fields=name,address,size \
--format=json-compact
# Find strings
ghidra query strings --program=<binary> \
--filter="value~http" \
--format=minimal
# List imports
ghidra dump imports --program=<binary> \
--filter="name~Crypt" \
--format=json-compact
# Get memory map
ghidra query memory --program=<binary> --format=table
```
### Decompilation
```bash
# Decompile function by address
ghidra decompile 0x401000 --program=<binary>
# Decompile by name
ghidra decompile main --program=<binary>
# Compact output
ghidra fn decompile <addr> --program=<binary> --format=compact
```
## Filter Language
### Operators
```
Comparison: =, !=, >, >=, <, <=
String: ~ (contains), ^ (starts), $ (ends), =~ (regex)
Logical: AND, OR, NOT, ()
Special: EXISTS, IN [val1,val2]
```
### Examples
```bash
# Exact match
name=malloc
# Numeric comparison
size>1000
# String matching (case-insensitive)
name~crypt
# Boolean logic
name~crypt AND size>500
(name~main OR name~start) AND NOT name^FUN_
# IN operator
name IN [malloc,free,realloc]
# Field existence
calls EXISTS
# Complex expression
size>=100 AND size<=1000 AND (name~crypt OR calls~Crypt)
```
## Output Formats
- `count` - Just the number (check result size)
- `json-compact` - Minimal JSON (best for LLMs)
- `minimal` - Addresses/names only (piping)
- `ids` - Just IDs (for further queries)
- `table` - Human-readable (display)
- `json` - Full JSON (complete data)
## Best Practices
### 1. Count-First Pattern
Always check the result size before fetching data:
```bash
# Step 1: Count
ghidra query functions --program=<binary> --count
# Step 2: Refine filter if needed
ghidra query functions --program=<binary> \
--filter="NOT name^FUN_" \
--count
# Step 3: Fetch minimal data
ghidra query functions --program=<binary> \
--filter="NOT name^FUN_" \
--fields=name,address \
--format=json-compact \
--limit=50
```
### 2. Aggressive Filtering
Filter on Ghidra side, not in your code:
```bash
# GOOD: Pre-filter
ghidra query functions --program=<binary> \
--filter="size>1000 AND name~crypt"
# BAD: Fetch all, then filter
ghidra query functions --program=<binary> # Then filter in code
```
### 3. Field Selection
Request only what you need:
```bash
# Only name and address
ghidra query functions --program=<binary> \
--fields=name,address \
--format=json-compact
```
### 4. Use Compact Formats
Minimize token usage:
```bash
# For analysis: json-compact
--format=json-compact
# For display: table
--format=table
# For piping: minimal or ids
--format=ids
```
## Analysis Workflows
### Initial Reconnaissance
```bash
# 1. Get summary
ghidra summary --program=<binary>
# 2. Count functions
ghidra query functions --program=<binary> --count
# 3. Count named functions
ghidra query functions --program=<binary> \
--filter="NOT name^FUN_" --count
# 4. List key functions
ghidra query functions --program=<binary> \
--filter="NOT name^FUN_" \
--fields=name,address,size \
--format=json-compact \
--limit=20
```
### Finding Suspicious Behavior
```bash
# Network operations
ghidra query imports --program=<binary> \
--filter="name~socket OR name~http OR name~inet" \
--format=json-compact
# File operations
ghidra query imports --program=<binary> \
--filter="name~File OR name~Read OR name~Write" \
--format=json-compact
# Process operations
ghidra query imports --program=<binary> \
--filter="name~Process OR name~Thread OR name~Exec" \
--format=json-compact
# Crypto operations
ghidra query imports --program=<binary> \
--filter="name~Crypt" \
--format=json-compact
```
### String Analysis
```bash
# URLs
ghidra query strings --program=<binary> \
--filter="value~http" \
--format=json-compact
# Registry keys
ghidra query strings --program=<binary> \
--filter="value~HKEY OR value~Software" \
--format=json-compact
# Credentials
ghidra query strings --program=<binary> \
--filter="value~password OR value~username OR value~key" \
--format=json-compact
# Long strings (paths, URLs)
ghidra query strings --program=<binary> \
--filter="length>50" \
--format=json-compact
```
### Deep Dive Analysis
```bash
# 1. Find interesting functions
ghidra query functions --program=<binary> \
--filter="name~crypt OR calls~Crypt" \
--fields=name,address \
--format=json-compact
# 2. Decompile each
ghidra decompile <address> --program=<binary>
# 3. Find cross-references
ghidra xref to <address> --program=<binary> \
--format=json-compact
# 4. Analyze callers
ghidra fn calls <address> --program=<binary> \
--format=json-compact
```
## Common Patterns
### Pattern: Find Entry Point
```bash
ghidra query functions --program=<binary> \
--filter="name~main OR name~WinMain OR name~DllMain" \
--format=json-compact
```
### Pattern: Find Crypto Functions
```bash
# By name
ghidra query functions --program=<binary> \
--filter="name~crypt OR name~cipher OR name~aes OR name~rsa" \
--format=json-compact
# By imports
ghidra query imports --program=<binary> \
--filter="name~Crypt" \
--format=json-compact
```
### Pattern: Find Large/Complex Functions
```bash
ghidra query functions --program=<binary> \
--filter="size>2000" \
--fields=name,address,size \
--sort=-size \
--limit=10 \
--format=json-compact
```
### Pattern: Trace Function Calls
```bash
# What does function call?
ghidra fn calls <function> --program=<binary> \
--format=json-compact
# What calls this function?
ghidra fn xrefs <function> --program=<binary> \
--format=json-compact
```
## Configuration
### Set Defaults (avoid repeating --program)
```bash
# Set default program
ghidra set-default program <binary>
# Now you can omit --program
ghidra query functions --count
```
### Environment Variables
```bash
# Windows
set GHIDRA_INSTALL_DIR=C:\ghidra\ghidra_11.0
set GHIDRA_DEFAULT_PROGRAM=malware.exe
# Unix
export GHIDRA_INSTALL_DIR=/opt/ghidra
export GHIDRA_DEFAULT_PROGRAM=malware.elf
```
## Error Handling
### Common Issues
1. **Ghidra not found**: Run `ghidra init` or set `GHIDRA_INSTALL_DIR`
2. **Program not specified**: Use `--program=<binary>` or set default
3. **Analysis timeout**: Increase with `set GHIDRA_TIMEOUT=600`
4. **Large result set**: Use `--count` first, then filter more aggressively
### Troubleshooting
```bash
# Check installation
ghidra doctor
# Show configuration
ghidra config list
# List projects
ghidra project list
```
## Performance Considerations
1. **Always count first** - Prevents context overflow
2. **Filter aggressively** - Reduce data before transfer
3. **Select minimal fields** - Less data = fewer tokens
4. **Use compact formats** - `json-compact` is most efficient
5. **Paginate results** - Use `--limit` for large datasets
6. **Cache results** - Store commonly-used data in variables
## Example: Complete Analysis
```bash
# Import binary
ghidra import suspicious.exe --project=analysis
# Set as default
ghidra set-default program suspicious.exe
# Overview
ghidra summary
# Count functions
ghidra query functions --count
# → 1247
# Named functions only
ghidra query functions --filter="NOT name^FUN_" --count
# → 89
# Get named functions
ghidra query functions \
--filter="NOT name^FUN_" \
--fields=name,address,size \
--format=json-compact
# Find suspicious imports
ghidra dump imports \
--filter="name~Exec OR name~Process OR name~Write" \
--format=json-compact
# Find URLs/IPs
ghidra query strings \
--filter="value~http OR value=~\"[0-9]{1,3}\\.[0-9]{1,3}\"" \
--format=json-compact
# Decompile interesting functions
ghidra decompile 0x401000
# Find what calls it
ghidra xref to 0x401000 --format=json-compact
```
## Integration Tips
This subagent works best when:
- You have a binary file that needs analysis
- You need to understand malware behavior
- You're investigating suspicious executables
- You need to extract specific information (strings, functions, imports)
- You want to generate a report on binary capabilities
The subagent is optimized for:
- Token efficiency (minimal output)
- Fast queries (count-first pattern)
- Precise filtering (server-side pre-filtering)
- Flexible output (multiple formats)
- Automation-friendly (scriptable)
## Limitations
- Requires Ghidra to be installed
- Windows path handling is primary (but cross-platform)
- Initial analysis can be slow for large binaries
- Decompilation quality depends on Ghidra's capabilities
- Complex analysis may require multiple queries
## Support
- Run `ghidra doctor` to check installation
- See `README.md` for full documentation
- Check `CLAUDE_SKILL.md` for detailed examples
+721
View File
File diff suppressed because it is too large Load Diff
+171
View File
@@ -0,0 +1,171 @@
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use std::fs;
use crate::error::{GhidraError, Result};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Config {
pub ghidra_install_dir: Option<PathBuf>,
pub ghidra_project_dir: Option<PathBuf>,
pub default_program: Option<String>,
pub default_project: Option<String>,
pub default_output_format: Option<String>,
pub default_limit: Option<usize>,
pub timeout: Option<u64>,
pub aliases: std::collections::HashMap<String, String>,
}
impl Default for Config {
fn default() -> Self {
Self {
ghidra_install_dir: None,
ghidra_project_dir: None,
default_program: None,
default_project: None,
default_output_format: Some("auto".to_string()),
default_limit: Some(1000),
timeout: Some(300),
aliases: std::collections::HashMap::new(),
}
}
}
impl Config {
pub fn load() -> Result<Self> {
let config_path = Self::config_path()?;
if !config_path.exists() {
return Ok(Self::default());
}
let content = fs::read_to_string(&config_path)?;
let config: Config = serde_yaml::from_str(&content)?;
Ok(config)
}
pub fn save(&self) -> Result<()> {
let config_path = Self::config_path()?;
if let Some(parent) = config_path.parent() {
fs::create_dir_all(parent)?;
}
let content = serde_yaml::to_string(self)?;
fs::write(config_path, content)?;
Ok(())
}
pub fn config_path() -> Result<PathBuf> {
let config_dir = dirs::config_dir()
.ok_or_else(|| GhidraError::ConfigError("Could not determine config directory".to_string()))?;
Ok(config_dir.join("ghidra-cli").join("config.yaml"))
}
pub fn get_ghidra_install_dir(&self) -> Result<PathBuf> {
// Check environment variable first
if let Ok(dir) = std::env::var("GHIDRA_INSTALL_DIR") {
return Ok(PathBuf::from(dir));
}
// Check config
if let Some(dir) = &self.ghidra_install_dir {
return Ok(dir.clone());
}
// Try to auto-detect on Windows
#[cfg(target_os = "windows")]
{
if let Some(dir) = Self::detect_ghidra_windows() {
return Ok(dir);
}
}
Err(GhidraError::GhidraNotFound)
}
pub fn get_project_dir(&self) -> Result<PathBuf> {
// Check environment variable first
if let Ok(dir) = std::env::var("GHIDRA_PROJECT_DIR") {
return Ok(PathBuf::from(dir));
}
// Check config
if let Some(dir) = &self.ghidra_project_dir {
return Ok(dir.clone());
}
// Default to ~/.ghidra-projects
let home = dirs::home_dir()
.ok_or_else(|| GhidraError::ConfigError("Could not determine home directory".to_string()))?;
Ok(home.join(".ghidra-projects"))
}
#[cfg(target_os = "windows")]
fn detect_ghidra_windows() -> Option<PathBuf> {
// Check common installation paths
let common_paths = vec![
PathBuf::from("C:\\Program Files\\Ghidra"),
PathBuf::from("C:\\Program Files (x86)\\Ghidra"),
PathBuf::from("C:\\ghidra"),
];
for path in common_paths {
if path.exists() {
// Look for ghidra_* directories
if let Ok(entries) = fs::read_dir(&path) {
for entry in entries.flatten() {
let entry_path = entry.path();
if entry_path.is_dir() {
let name = entry_path.file_name()?.to_str()?;
if name.starts_with("ghidra_") {
// Check if analyzeHeadless.bat exists
let headless = entry_path.join("support").join("analyzeHeadless.bat");
if headless.exists() {
return Some(entry_path);
}
}
}
}
}
}
}
None
}
pub fn get_timeout(&self) -> u64 {
std::env::var("GHIDRA_TIMEOUT")
.ok()
.and_then(|s| s.parse().ok())
.or(self.timeout)
.unwrap_or(300)
}
pub fn get_default_program(&self) -> Option<String> {
std::env::var("GHIDRA_DEFAULT_PROGRAM")
.ok()
.or_else(|| self.default_program.clone())
}
pub fn get_default_project(&self) -> Option<String> {
std::env::var("GHIDRA_DEFAULT_PROJECT")
.ok()
.or_else(|| self.default_project.clone())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_config() {
let config = Config::default();
assert_eq!(config.timeout, Some(300));
assert_eq!(config.default_limit, Some(1000));
}
}
+57
View File
@@ -0,0 +1,57 @@
use thiserror::Error;
#[derive(Error, Debug)]
pub enum GhidraError {
#[error("Ghidra installation not found. Set GHIDRA_INSTALL_DIR or run 'ghidra init'")]
GhidraNotFound,
#[error("Ghidra project not found: {0}")]
ProjectNotFound(String),
#[error("Program not found: {0}")]
ProgramNotFound(String),
#[error("Failed to execute Ghidra: {0}")]
ExecutionFailed(String),
#[error("Failed to parse filter: {0}")]
FilterParseError(String),
#[error("Invalid filter expression: {0}")]
InvalidFilter(String),
#[error("Field not found: {0}")]
FieldNotFound(String),
#[error("Invalid format: {0}")]
InvalidFormat(String),
#[error("Invalid data type: {0}")]
InvalidDataType(String),
#[error("Configuration error: {0}")]
ConfigError(String),
#[error("IO error: {0}")]
IoError(#[from] std::io::Error),
#[error("JSON error: {0}")]
JsonError(#[from] serde_json::Error),
#[error("YAML error: {0}")]
YamlError(#[from] serde_yaml::Error),
#[error("Command failed: {0}")]
CommandFailed(String),
#[error("Invalid address: {0}")]
InvalidAddress(String),
#[error("Analysis timeout after {0} seconds")]
Timeout(u64),
#[error("{0}")]
Other(String),
}
pub type Result<T> = std::result::Result<T, GhidraError>;
+36
View File
@@ -0,0 +1,36 @@
WHITESPACE = _{ " " | "\t" | "\n" | "\r" }
// Main expression
expr = { logical_expr }
// Logical expressions
logical_expr = { logical_term ~ (logical_op ~ logical_term)* }
logical_term = { logical_not | "(" ~ logical_expr ~ ")" | comparison }
logical_not = { ("NOT" | "!") ~ logical_term }
logical_op = { "AND" | "OR" | "&&" | "||" }
// Comparison expressions
comparison = { field ~ compare_op ~ value | field ~ string_op ~ string_value | existence_check | in_check }
// Operators
compare_op = { ">=" | "<=" | "!=" | "=" | ">" | "<" }
string_op = { "=~" | "~" | "^" | "$" }
// Existence checks
existence_check = { field ~ ("EXISTS" | "EMPTY" | "NULL") }
// IN operator
in_check = { field ~ "IN" ~ "[" ~ value_list ~ "]" }
value_list = { value ~ ("," ~ value)* }
// Field access (including nested fields and arrays)
field = @{ identifier ~ ("." ~ identifier | "[" ~ number ~ "]")* }
identifier = @{ (ASCII_ALPHA | "_") ~ (ASCII_ALPHANUMERIC | "_")* }
// Values
value = { number | hex_number | boolean | quoted_string | identifier }
string_value = { quoted_string | identifier }
number = @{ "-"? ~ ASCII_DIGIT+ ~ ("." ~ ASCII_DIGIT+)? }
hex_number = @{ "0x" ~ ASCII_HEX_DIGIT+ }
boolean = { "true" | "false" | "TRUE" | "FALSE" }
quoted_string = @{ "\"" ~ (!"\"" ~ ANY)* ~ "\"" | "'" ~ (!"'" ~ ANY)* ~ "'" }
+256
View File
@@ -0,0 +1,256 @@
use regex::Regex;
use serde_json::Value as JsonValue;
use crate::error::{GhidraError, Result};
use super::{FilterExpr, CompareOp, StringOp, LogicalOp, ExistenceCheck, Value};
pub fn evaluate(expr: &FilterExpr, data: &JsonValue) -> Result<bool> {
match expr {
FilterExpr::Compare { field, op, value } => {
evaluate_compare(field, *op, value, data)
}
FilterExpr::StringOp { field, op, value } => {
evaluate_string_op(field, *op, value, data)
}
FilterExpr::Logical { op, exprs } => {
evaluate_logical(*op, exprs, data)
}
FilterExpr::Not(inner) => {
Ok(!evaluate(inner, data)?)
}
FilterExpr::Exists { field, check } => {
evaluate_exists(field, *check, data)
}
FilterExpr::In { field, values } => {
evaluate_in(field, values, data)
}
}
}
fn get_field_value<'a>(field: &str, data: &'a JsonValue) -> Option<&'a JsonValue> {
let parts: Vec<&str> = field.split('.').collect();
let mut current = data;
for part in parts {
// Check for array index like "field[0]"
if let Some(bracket_pos) = part.find('[') {
let field_name = &part[..bracket_pos];
let index_str = &part[bracket_pos + 1..part.len() - 1];
current = current.get(field_name)?;
if let Ok(index) = index_str.parse::<usize>() {
current = current.get(index)?;
} else {
return None;
}
} else {
current = current.get(part)?;
}
}
Some(current)
}
fn evaluate_compare(field: &str, op: CompareOp, value: &Value, data: &JsonValue) -> Result<bool> {
let field_value = get_field_value(field, data);
if field_value.is_none() {
return Ok(false);
}
let field_value = field_value.unwrap();
match (field_value, value) {
(JsonValue::Number(n), val) => {
let field_num = n.as_f64().unwrap();
let compare_num = val.as_f64()
.ok_or_else(|| GhidraError::InvalidFilter(format!("Cannot compare number with {:?}", val)))?;
Ok(match op {
CompareOp::Equal => (field_num - compare_num).abs() < f64::EPSILON,
CompareOp::NotEqual => (field_num - compare_num).abs() >= f64::EPSILON,
CompareOp::Greater => field_num > compare_num,
CompareOp::GreaterEqual => field_num >= compare_num,
CompareOp::Less => field_num < compare_num,
CompareOp::LessEqual => field_num <= compare_num,
})
}
(JsonValue::String(s), Value::String(val)) => {
Ok(match op {
CompareOp::Equal => s == val,
CompareOp::NotEqual => s != val,
_ => return Err(GhidraError::InvalidFilter("Cannot use numeric comparison on strings".to_string())),
})
}
(JsonValue::Bool(b), Value::Boolean(val)) => {
Ok(match op {
CompareOp::Equal => *b == *val,
CompareOp::NotEqual => *b != *val,
_ => return Err(GhidraError::InvalidFilter("Cannot use numeric comparison on booleans".to_string())),
})
}
_ => Ok(false),
}
}
fn evaluate_string_op(field: &str, op: StringOp, value: &str, data: &JsonValue) -> Result<bool> {
let field_value = get_field_value(field, data);
if field_value.is_none() {
return Ok(false);
}
let field_str = match field_value.unwrap() {
JsonValue::String(s) => s.to_lowercase(),
JsonValue::Number(n) => n.to_string(),
JsonValue::Bool(b) => b.to_string(),
_ => return Ok(false),
};
let value_lower = value.to_lowercase();
Ok(match op {
StringOp::Contains => field_str.contains(&value_lower),
StringOp::StartsWith => field_str.starts_with(&value_lower),
StringOp::EndsWith => field_str.ends_with(&value_lower),
StringOp::Regex => {
let re = Regex::new(value)
.map_err(|e| GhidraError::InvalidFilter(format!("Invalid regex: {}", e)))?;
re.is_match(&field_str)
}
})
}
fn evaluate_logical(op: LogicalOp, exprs: &[FilterExpr], data: &JsonValue) -> Result<bool> {
match op {
LogicalOp::And => {
for expr in exprs {
if !evaluate(expr, data)? {
return Ok(false);
}
}
Ok(true)
}
LogicalOp::Or => {
for expr in exprs {
if evaluate(expr, data)? {
return Ok(true);
}
}
Ok(false)
}
}
}
fn evaluate_exists(field: &str, check: ExistenceCheck, data: &JsonValue) -> Result<bool> {
let field_value = get_field_value(field, data);
Ok(match check {
ExistenceCheck::Exists => field_value.is_some(),
ExistenceCheck::Empty => {
match field_value {
None => true,
Some(JsonValue::Null) => true,
Some(JsonValue::String(s)) => s.is_empty(),
Some(JsonValue::Array(a)) => a.is_empty(),
Some(JsonValue::Object(o)) => o.is_empty(),
_ => false,
}
}
ExistenceCheck::Null => {
matches!(field_value, None | Some(JsonValue::Null))
}
})
}
fn evaluate_in(field: &str, values: &[Value], data: &JsonValue) -> Result<bool> {
let field_value = get_field_value(field, data);
if field_value.is_none() {
return Ok(false);
}
let field_value = field_value.unwrap();
for val in values {
match (field_value, val) {
(JsonValue::String(s), Value::String(v)) => {
if s.eq_ignore_ascii_case(v) {
return Ok(true);
}
}
(JsonValue::Number(n), v) => {
if let Some(compare_num) = v.as_f64() {
if (n.as_f64().unwrap() - compare_num).abs() < f64::EPSILON {
return Ok(true);
}
}
}
(JsonValue::Bool(b), Value::Boolean(v)) => {
if *b == *v {
return Ok(true);
}
}
_ => {}
}
}
Ok(false)
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_evaluate_compare() {
let data = json!({
"name": "test",
"size": 100
});
let expr = FilterExpr::Compare {
field: "size".to_string(),
op: CompareOp::Greater,
value: Value::Integer(50),
};
assert!(evaluate(&expr, &data).unwrap());
}
#[test]
fn test_evaluate_string_op() {
let data = json!({
"name": "test_function"
});
let expr = FilterExpr::StringOp {
field: "name".to_string(),
op: StringOp::Contains,
value: "func".to_string(),
};
assert!(evaluate(&expr, &data).unwrap());
}
#[test]
fn test_evaluate_nested_field() {
let data = json!({
"function": {
"name": "test",
"xrefs": {
"count": 10
}
}
});
let expr = FilterExpr::Compare {
field: "function.xrefs.count".to_string(),
op: CompareOp::Greater,
value: Value::Integer(5),
};
assert!(evaluate(&expr, &data).unwrap());
}
}
+137
View File
@@ -0,0 +1,137 @@
pub mod parser;
pub mod evaluator;
use serde::{Deserialize, Serialize};
use crate::error::Result;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum FilterExpr {
Compare {
field: String,
op: CompareOp,
value: Value,
},
StringOp {
field: String,
op: StringOp,
value: String,
},
Logical {
op: LogicalOp,
exprs: Vec<FilterExpr>,
},
Not(Box<FilterExpr>),
Exists {
field: String,
check: ExistenceCheck,
},
In {
field: String,
values: Vec<Value>,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum CompareOp {
Equal,
NotEqual,
Greater,
GreaterEqual,
Less,
LessEqual,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum StringOp {
Contains, // ~
StartsWith, // ^
EndsWith, // $
Regex, // =~
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum LogicalOp {
And,
Or,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ExistenceCheck {
Exists,
Empty,
Null,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum Value {
String(String),
Number(f64),
Integer(i64),
Boolean(bool),
Hex(u64),
}
impl Value {
pub fn as_str(&self) -> Option<&str> {
match self {
Value::String(s) => Some(s),
_ => None,
}
}
pub fn as_f64(&self) -> Option<f64> {
match self {
Value::Number(n) => Some(*n),
Value::Integer(i) => Some(*i as f64),
Value::Hex(h) => Some(*h as f64),
_ => None,
}
}
pub fn as_i64(&self) -> Option<i64> {
match self {
Value::Integer(i) => Some(*i),
Value::Number(n) => Some(*n as i64),
Value::Hex(h) => Some(*h as i64),
_ => None,
}
}
pub fn as_bool(&self) -> Option<bool> {
match self {
Value::Boolean(b) => Some(*b),
_ => None,
}
}
}
pub struct Filter {
pub expr: FilterExpr,
}
impl Filter {
pub fn parse(input: &str) -> Result<Self> {
parser::parse_filter(input)
}
pub fn evaluate(&self, data: &serde_json::Value) -> Result<bool> {
evaluator::evaluate(&self.expr, data)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_simple_filter() {
let filter = Filter::parse("name=test").unwrap();
assert!(matches!(filter.expr, FilterExpr::Compare { .. }));
}
#[test]
fn test_parse_logical_filter() {
let filter = Filter::parse("name=test AND size>100").unwrap();
assert!(matches!(filter.expr, FilterExpr::Logical { .. }));
}
}
+270
View File
@@ -0,0 +1,270 @@
use pest::Parser;
use pest_derive::Parser;
use crate::error::{GhidraError, Result};
use super::{Filter, FilterExpr, CompareOp, StringOp, LogicalOp, ExistenceCheck, Value};
#[derive(Parser)]
#[grammar = "filter.pest"]
struct FilterParser;
pub fn parse_filter(input: &str) -> Result<Filter> {
let pairs = FilterParser::parse(Rule::expr, input)
.map_err(|e| GhidraError::FilterParseError(format!("{}", e)))?;
let mut expr = None;
for pair in pairs {
if pair.as_rule() == Rule::expr {
for inner in pair.into_inner() {
if inner.as_rule() == Rule::logical_expr {
expr = Some(parse_logical_expr(inner)?);
}
}
}
}
expr.map(|e| Filter { expr: e })
.ok_or_else(|| GhidraError::FilterParseError("Empty expression".to_string()))
}
fn parse_logical_expr(pair: pest::iterators::Pair<Rule>) -> Result<FilterExpr> {
let mut terms = Vec::new();
let mut ops = Vec::new();
for inner in pair.into_inner() {
match inner.as_rule() {
Rule::logical_term => {
terms.push(parse_logical_term(inner)?);
}
Rule::logical_op => {
let op_str = inner.as_str();
let op = match op_str {
"AND" | "&&" => LogicalOp::And,
"OR" | "||" => LogicalOp::Or,
_ => return Err(GhidraError::FilterParseError(format!("Unknown operator: {}", op_str))),
};
ops.push(op);
}
_ => {}
}
}
if terms.is_empty() {
return Err(GhidraError::FilterParseError("No terms in logical expression".to_string()));
}
if terms.len() == 1 {
return Ok(terms.into_iter().next().unwrap());
}
// Build expression tree respecting precedence (AND before OR)
// For simplicity, we'll evaluate left-to-right for now
// TODO: Proper precedence handling
let mut result = terms[0].clone();
for (i, op) in ops.iter().enumerate() {
result = FilterExpr::Logical {
op: *op,
exprs: vec![result, terms[i + 1].clone()],
};
}
Ok(result)
}
fn parse_logical_term(pair: pest::iterators::Pair<Rule>) -> Result<FilterExpr> {
for inner in pair.into_inner() {
match inner.as_rule() {
Rule::logical_not => {
return parse_logical_not(inner);
}
Rule::logical_expr => {
return parse_logical_expr(inner);
}
Rule::comparison => {
return parse_comparison(inner);
}
_ => {}
}
}
Err(GhidraError::FilterParseError("Invalid logical term".to_string()))
}
fn parse_logical_not(pair: pest::iterators::Pair<Rule>) -> Result<FilterExpr> {
for inner in pair.into_inner() {
if inner.as_rule() == Rule::logical_term {
let term = parse_logical_term(inner)?;
return Ok(FilterExpr::Not(Box::new(term)));
}
}
Err(GhidraError::FilterParseError("Invalid NOT expression".to_string()))
}
fn parse_comparison(pair: pest::iterators::Pair<Rule>) -> Result<FilterExpr> {
let mut field = None;
let mut op = None;
let mut value = None;
let mut string_op = None;
let mut existence = None;
let mut in_values = None;
for inner in pair.into_inner() {
match inner.as_rule() {
Rule::field => {
field = Some(inner.as_str().to_string());
}
Rule::compare_op => {
let op_str = inner.as_str();
op = Some(match op_str {
"=" => CompareOp::Equal,
"!=" => CompareOp::NotEqual,
">" => CompareOp::Greater,
">=" => CompareOp::GreaterEqual,
"<" => CompareOp::Less,
"<=" => CompareOp::LessEqual,
_ => return Err(GhidraError::FilterParseError(format!("Unknown compare op: {}", op_str))),
});
}
Rule::string_op => {
let op_str = inner.as_str();
string_op = Some(match op_str {
"~" => StringOp::Contains,
"^" => StringOp::StartsWith,
"$" => StringOp::EndsWith,
"=~" => StringOp::Regex,
_ => return Err(GhidraError::FilterParseError(format!("Unknown string op: {}", op_str))),
});
}
Rule::value => {
value = Some(parse_value(inner)?);
}
Rule::string_value => {
value = Some(parse_value(inner)?);
}
Rule::existence_check => {
let check_str = inner.as_str();
existence = Some(match check_str {
"EXISTS" => ExistenceCheck::Exists,
"EMPTY" => ExistenceCheck::Empty,
"NULL" => ExistenceCheck::Null,
_ => return Err(GhidraError::FilterParseError(format!("Unknown existence check: {}", check_str))),
});
}
Rule::in_check => {
// Already handled field
continue;
}
Rule::value_list => {
let mut values = Vec::new();
for val_pair in inner.into_inner() {
if val_pair.as_rule() == Rule::value {
values.push(parse_value(val_pair)?);
}
}
in_values = Some(values);
}
_ => {}
}
}
let field = field.ok_or_else(|| GhidraError::FilterParseError("Missing field".to_string()))?;
if let Some(existence_check) = existence {
return Ok(FilterExpr::Exists {
field,
check: existence_check,
});
}
if let Some(values) = in_values {
return Ok(FilterExpr::In { field, values });
}
if let Some(str_op) = string_op {
let val = value.ok_or_else(|| GhidraError::FilterParseError("Missing value".to_string()))?;
let val_str = match val {
Value::String(s) => s,
_ => return Err(GhidraError::FilterParseError("String operation requires string value".to_string())),
};
return Ok(FilterExpr::StringOp {
field,
op: str_op,
value: val_str,
});
}
if let Some(cmp_op) = op {
let val = value.ok_or_else(|| GhidraError::FilterParseError("Missing value".to_string()))?;
return Ok(FilterExpr::Compare {
field,
op: cmp_op,
value: val,
});
}
Err(GhidraError::FilterParseError("Invalid comparison".to_string()))
}
fn parse_value(pair: pest::iterators::Pair<Rule>) -> Result<Value> {
for inner in pair.into_inner() {
match inner.as_rule() {
Rule::number => {
let num_str = inner.as_str();
if num_str.contains('.') {
let num = num_str.parse::<f64>()
.map_err(|_| GhidraError::FilterParseError(format!("Invalid number: {}", num_str)))?;
return Ok(Value::Number(num));
} else {
let num = num_str.parse::<i64>()
.map_err(|_| GhidraError::FilterParseError(format!("Invalid integer: {}", num_str)))?;
return Ok(Value::Integer(num));
}
}
Rule::hex_number => {
let hex_str = inner.as_str().trim_start_matches("0x");
let num = u64::from_str_radix(hex_str, 16)
.map_err(|_| GhidraError::FilterParseError(format!("Invalid hex number: {}", inner.as_str())))?;
return Ok(Value::Hex(num));
}
Rule::boolean => {
let bool_str = inner.as_str().to_lowercase();
return Ok(Value::Boolean(bool_str == "true"));
}
Rule::quoted_string => {
let s = inner.as_str();
let s = s.trim_matches(|c| c == '"' || c == '\'');
return Ok(Value::String(s.to_string()));
}
Rule::identifier => {
return Ok(Value::String(inner.as_str().to_string()));
}
_ => {}
}
}
Err(GhidraError::FilterParseError("Invalid value".to_string()))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_simple() {
let filter = parse_filter("name=test").unwrap();
assert!(matches!(filter.expr, FilterExpr::Compare { .. }));
}
#[test]
fn test_parse_and() {
let filter = parse_filter("name=test AND size>100").unwrap();
assert!(matches!(filter.expr, FilterExpr::Logical { .. }));
}
#[test]
fn test_parse_hex() {
let filter = parse_filter("address=0x401000").unwrap();
if let FilterExpr::Compare { value, .. } = filter.expr {
assert!(matches!(value, Value::Hex(0x401000)));
} else {
panic!("Expected Compare expression");
}
}
}
+264
View File
@@ -0,0 +1,264 @@
use serde::Serialize;
use serde_json::Value as JsonValue;
use crate::error::{GhidraError, Result};
use comfy_table::{Table, presets::UTF8_FULL};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OutputFormat {
Full,
Compact,
Minimal,
Json,
JsonCompact,
JsonStream,
Csv,
Tsv,
Table,
Ids,
Count,
Tree,
Hex,
Asm,
C,
}
impl OutputFormat {
pub fn from_str(s: &str) -> Result<Self> {
match s.to_lowercase().as_str() {
"full" => Ok(Self::Full),
"compact" => Ok(Self::Compact),
"minimal" => Ok(Self::Minimal),
"json" => Ok(Self::Json),
"json-compact" => Ok(Self::JsonCompact),
"json-stream" | "ndjson" => Ok(Self::JsonStream),
"csv" => Ok(Self::Csv),
"tsv" => Ok(Self::Tsv),
"table" => Ok(Self::Table),
"ids" => Ok(Self::Ids),
"count" => Ok(Self::Count),
"tree" => Ok(Self::Tree),
"hex" => Ok(Self::Hex),
"asm" => Ok(Self::Asm),
"c" => Ok(Self::C),
_ => Err(GhidraError::InvalidFormat(format!("Unknown format: {}", s))),
}
}
pub fn is_human_friendly(&self) -> bool {
matches!(self, Self::Full | Self::Compact | Self::Table | Self::Tree)
}
pub fn is_machine_friendly(&self) -> bool {
matches!(self, Self::Json | Self::JsonCompact | Self::JsonStream | Self::Csv | Self::Tsv)
}
}
pub trait Formatter {
fn format<T: Serialize>(&self, data: &[T], format: OutputFormat) -> Result<String>;
}
pub struct DefaultFormatter;
impl Formatter for DefaultFormatter {
fn format<T: Serialize>(&self, data: &[T], format: OutputFormat) -> Result<String> {
match format {
OutputFormat::Json => {
serde_json::to_string_pretty(data).map_err(|e| e.into())
}
OutputFormat::JsonCompact => {
serde_json::to_string(data).map_err(|e| e.into())
}
OutputFormat::JsonStream => {
let mut result = String::new();
for item in data {
let json = serde_json::to_string(item)?;
result.push_str(&json);
result.push('\n');
}
Ok(result)
}
OutputFormat::Count => {
Ok(format!("{}", data.len()))
}
OutputFormat::Table => {
format_table(data)
}
OutputFormat::Csv => {
format_csv(data, ',')
}
OutputFormat::Tsv => {
format_csv(data, '\t')
}
OutputFormat::Minimal | OutputFormat::Ids => {
format_minimal(data)
}
_ => {
// For other formats, default to JSON
serde_json::to_string_pretty(data).map_err(|e| e.into())
}
}
}
}
fn format_table<T: Serialize>(data: &[T]) -> Result<String> {
if data.is_empty() {
return Ok("No results".to_string());
}
// Convert to JSON values to inspect structure
let json_data: Vec<JsonValue> = data.iter()
.map(|item| serde_json::to_value(item))
.collect::<std::result::Result<Vec<_>, _>>()?;
if json_data.is_empty() {
return Ok("No results".to_string());
}
// Get all keys from first object
let keys = if let Some(JsonValue::Object(map)) = json_data.first() {
map.keys().cloned().collect::<Vec<_>>()
} else {
return Ok(format!("{} results", data.len()));
};
let mut table = Table::new();
table.load_preset(UTF8_FULL);
// Add header
table.set_header(&keys);
// Add rows
for item in &json_data {
if let JsonValue::Object(map) = item {
let row: Vec<String> = keys.iter()
.map(|k| {
map.get(k)
.map(|v| format_json_value(v))
.unwrap_or_else(|| "".to_string())
})
.collect();
table.add_row(row);
}
}
Ok(table.to_string())
}
fn format_csv<T: Serialize>(data: &[T], delimiter: char) -> Result<String> {
if data.is_empty() {
return Ok(String::new());
}
let json_data: Vec<JsonValue> = data.iter()
.map(|item| serde_json::to_value(item))
.collect::<std::result::Result<Vec<_>, _>>()?;
if json_data.is_empty() {
return Ok(String::new());
}
let keys = if let Some(JsonValue::Object(map)) = json_data.first() {
map.keys().cloned().collect::<Vec<_>>()
} else {
return Ok(String::new());
};
let mut result = String::new();
// Header
result.push_str(&keys.join(&delimiter.to_string()));
result.push('\n');
// Rows
for item in &json_data {
if let JsonValue::Object(map) = item {
let row: Vec<String> = keys.iter()
.map(|k| {
map.get(k)
.map(|v| format_json_value(v))
.unwrap_or_else(|| "".to_string())
})
.collect();
result.push_str(&row.join(&delimiter.to_string()));
result.push('\n');
}
}
Ok(result)
}
fn format_minimal<T: Serialize>(data: &[T]) -> Result<String> {
let json_data: Vec<JsonValue> = data.iter()
.map(|item| serde_json::to_value(item))
.collect::<std::result::Result<Vec<_>, _>>()?;
let mut result = String::new();
for item in &json_data {
if let JsonValue::Object(map) = item {
// Try to get address or name or first field
let value = map.get("address")
.or_else(|| map.get("name"))
.or_else(|| map.get("id"))
.or_else(|| map.values().next())
.map(|v| format_json_value(v))
.unwrap_or_else(|| "".to_string());
result.push_str(&value);
result.push('\n');
} else {
result.push_str(&format_json_value(item));
result.push('\n');
}
}
Ok(result)
}
fn format_json_value(value: &JsonValue) -> String {
match value {
JsonValue::Null => "null".to_string(),
JsonValue::Bool(b) => b.to_string(),
JsonValue::Number(n) => n.to_string(),
JsonValue::String(s) => s.clone(),
JsonValue::Array(arr) => {
format!("[{}]", arr.iter().map(format_json_value).collect::<Vec<_>>().join(", "))
}
JsonValue::Object(_) => serde_json::to_string(value).unwrap_or_else(|_| "{}".to_string()),
}
}
pub fn auto_detect_format(is_tty: bool) -> OutputFormat {
if is_tty {
OutputFormat::Table
} else {
OutputFormat::JsonCompact
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_format_json() {
let data = vec![
json!({"name": "test", "value": 123}),
];
let formatter = DefaultFormatter;
let result = formatter.format(&data, OutputFormat::Json).unwrap();
assert!(result.contains("test"));
}
#[test]
fn test_format_count() {
let data = vec![
json!({"name": "test1"}),
json!({"name": "test2"}),
];
let formatter = DefaultFormatter;
let result = formatter.format(&data, OutputFormat::Count).unwrap();
assert_eq!(result, "2");
}
}
+168
View File
@@ -0,0 +1,168 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Function {
pub name: String,
pub address: String,
pub size: u64,
pub signature: Option<String>,
pub entry_point: String,
pub calling_convention: Option<String>,
#[serde(default)]
pub parameters: Vec<Parameter>,
#[serde(default)]
pub local_variables: Vec<LocalVariable>,
#[serde(default)]
pub calls: Vec<String>,
#[serde(default)]
pub called_by: Vec<String>,
pub decompiled: Option<String>,
pub comment: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Parameter {
pub name: String,
pub data_type: String,
pub ordinal: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LocalVariable {
pub name: String,
pub data_type: String,
pub stack_offset: Option<i32>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StringData {
pub address: String,
pub value: String,
pub length: usize,
pub encoding: String,
#[serde(default)]
pub references: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Symbol {
pub name: String,
pub address: String,
pub symbol_type: String,
pub namespace: Option<String>,
pub source: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Import {
pub name: String,
pub address: String,
pub library: String,
pub ordinal: Option<u32>,
pub is_external: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Export {
pub name: String,
pub address: String,
pub ordinal: Option<u32>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct XRef {
pub from: String,
pub to: String,
pub ref_type: String,
pub from_function: Option<String>,
pub to_function: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MemoryBlock {
pub name: String,
pub start: String,
pub end: String,
pub size: u64,
pub permissions: String,
pub is_initialized: bool,
pub is_loaded: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Section {
pub name: String,
pub address: String,
pub size: u64,
pub virtual_address: String,
pub file_offset: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Comment {
pub address: String,
pub comment_type: String,
pub text: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DataType {
pub name: String,
pub category: String,
pub size: Option<u64>,
pub description: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Instruction {
pub address: String,
pub mnemonic: String,
pub operands: String,
pub bytes: String,
pub length: u32,
pub flow_type: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BasicBlock {
pub start: String,
pub end: String,
pub size: u64,
pub instruction_count: u32,
#[serde(default)]
pub successors: Vec<String>,
#[serde(default)]
pub predecessors: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProgramInfo {
pub name: String,
pub executable_path: String,
pub executable_format: String,
pub compiler: Option<String>,
pub language: String,
pub creation_date: Option<String>,
pub image_base: String,
pub min_address: String,
pub max_address: String,
pub function_count: usize,
pub instruction_count: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum GhidraData {
Function(Function),
String(StringData),
Symbol(Symbol),
Import(Import),
Export(Export),
XRef(XRef),
MemoryBlock(MemoryBlock),
Section(Section),
Comment(Comment),
DataType(DataType),
Instruction(Instruction),
BasicBlock(BasicBlock),
}
+211
View File
@@ -0,0 +1,211 @@
use std::path::Path;
use std::process::Command;
use serde_json::Value as JsonValue;
use crate::error::{GhidraError, Result};
use super::GhidraClient;
use super::scripts;
pub struct HeadlessExecutor<'a> {
client: &'a GhidraClient,
}
impl<'a> HeadlessExecutor<'a> {
pub fn new(client: &'a GhidraClient) -> Self {
Self { client }
}
pub fn execute_script(
&self,
project_name: &str,
program_name: &str,
script_content: &str,
script_name: &str,
args: &[String],
) -> Result<JsonValue> {
// Save script to disk
let scripts_dir = self.get_scripts_dir()?;
let script_path = scripts::save_script(script_name, script_content, &scripts_dir)?;
// Execute script
let output = self.run_ghidra_script(project_name, program_name, &script_path, args)?;
// Parse JSON output
let json: JsonValue = serde_json::from_str(&output)
.map_err(|e| GhidraError::ExecutionFailed(format!("Failed to parse script output: {}", e)))?;
Ok(json)
}
fn run_ghidra_script(
&self,
project_name: &str,
program_name: &str,
script_path: &Path,
args: &[String],
) -> Result<String> {
let project_path = self.client.get_project_path(project_name);
let headless = self.client.get_headless_script();
let mut cmd = Command::new(&headless);
cmd.arg(project_path.to_str().unwrap())
.arg(project_name)
.arg("-process")
.arg(program_name)
.arg("-noanalysis")
.arg("-scriptPath")
.arg(script_path.parent().unwrap().to_str().unwrap())
.arg("-postScript")
.arg(script_path.file_name().unwrap().to_str().unwrap());
for arg in args {
cmd.arg(arg);
}
// Capture output
let output = cmd.output()?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(GhidraError::ExecutionFailed(format!("Script execution failed: {}", stderr)));
}
// Extract JSON from output (Ghidra adds some logging we need to skip)
let stdout = String::from_utf8_lossy(&output.stdout);
let json_output = self.extract_json_from_output(&stdout)?;
Ok(json_output)
}
fn extract_json_from_output(&self, output: &str) -> Result<String> {
// Find the JSON output in the Ghidra output
// Look for lines starting with { or [
let lines: Vec<&str> = output.lines().collect();
let mut json_start = None;
let mut json_end = None;
let mut brace_count = 0;
for (i, line) in lines.iter().enumerate() {
let trimmed = line.trim();
if json_start.is_none() && (trimmed.starts_with('{') || trimmed.starts_with('[')) {
json_start = Some(i);
brace_count = trimmed.chars().filter(|&c| c == '{' || c == '[').count() as i32;
brace_count -= trimmed.chars().filter(|&c| c == '}' || c == ']').count() as i32;
if brace_count == 0 {
json_end = Some(i);
break;
}
} else if json_start.is_some() {
brace_count += trimmed.chars().filter(|&c| c == '{' || c == '[').count() as i32;
brace_count -= trimmed.chars().filter(|&c| c == '}' || c == ']').count() as i32;
if brace_count == 0 {
json_end = Some(i);
break;
}
}
}
if let (Some(start), Some(end)) = (json_start, json_end) {
let json_lines = &lines[start..=end];
Ok(json_lines.join("\n"))
} else {
Err(GhidraError::ExecutionFailed("Could not find JSON in script output".to_string()))
}
}
fn get_scripts_dir(&self) -> Result<std::path::PathBuf> {
let config_dir = dirs::config_dir()
.ok_or_else(|| GhidraError::ConfigError("Could not determine config directory".to_string()))?;
let scripts_dir = config_dir.join("ghidra-cli").join("scripts");
if !scripts_dir.exists() {
std::fs::create_dir_all(&scripts_dir)?;
}
Ok(scripts_dir)
}
pub fn list_functions(&self, project_name: &str, program_name: &str) -> Result<JsonValue> {
self.execute_script(
project_name,
program_name,
scripts::get_list_functions_script(),
"list_functions",
&[],
)
}
pub fn decompile_function(&self, project_name: &str, program_name: &str, address: &str) -> Result<JsonValue> {
self.execute_script(
project_name,
program_name,
scripts::get_decompile_function_script(),
"decompile_function",
&[address.to_string()],
)
}
pub fn list_strings(&self, project_name: &str, program_name: &str) -> Result<JsonValue> {
self.execute_script(
project_name,
program_name,
scripts::get_list_strings_script(),
"list_strings",
&[],
)
}
pub fn list_imports(&self, project_name: &str, program_name: &str) -> Result<JsonValue> {
self.execute_script(
project_name,
program_name,
scripts::get_list_imports_script(),
"list_imports",
&[],
)
}
pub fn list_exports(&self, project_name: &str, program_name: &str) -> Result<JsonValue> {
self.execute_script(
project_name,
program_name,
scripts::get_list_exports_script(),
"list_exports",
&[],
)
}
pub fn get_memory_map(&self, project_name: &str, program_name: &str) -> Result<JsonValue> {
self.execute_script(
project_name,
program_name,
scripts::get_memory_map_script(),
"memory_map",
&[],
)
}
pub fn get_program_info(&self, project_name: &str, program_name: &str) -> Result<JsonValue> {
self.execute_script(
project_name,
program_name,
scripts::get_program_info_script(),
"program_info",
&[],
)
}
pub fn get_xrefs_to(&self, project_name: &str, program_name: &str, address: &str) -> Result<JsonValue> {
self.execute_script(
project_name,
program_name,
scripts::get_xrefs_to_script(),
"xrefs_to",
&[address.to_string()],
)
}
}
+212
View File
@@ -0,0 +1,212 @@
pub mod headless;
pub mod data;
pub mod scripts;
use std::path::{Path, PathBuf};
use std::process::Command;
use crate::config::Config;
use crate::error::{GhidraError, Result};
pub struct GhidraClient {
config: Config,
install_dir: PathBuf,
project_dir: PathBuf,
}
impl GhidraClient {
pub fn new(config: Config) -> Result<Self> {
let install_dir = config.get_ghidra_install_dir()?;
let project_dir = config.get_project_dir()?;
// Create project directory if it doesn't exist
if !project_dir.exists() {
std::fs::create_dir_all(&project_dir)?;
}
Ok(Self {
config,
install_dir,
project_dir,
})
}
pub fn get_headless_script(&self) -> PathBuf {
let support_dir = self.install_dir.join("support");
#[cfg(target_os = "windows")]
{
support_dir.join("analyzeHeadless.bat")
}
#[cfg(not(target_os = "windows"))]
{
support_dir.join("analyzeHeadless")
}
}
pub fn verify_installation(&self) -> Result<()> {
let headless = self.get_headless_script();
if !headless.exists() {
return Err(GhidraError::GhidraNotFound);
}
Ok(())
}
pub fn get_project_path(&self, project_name: &str) -> PathBuf {
self.project_dir.join(project_name)
}
pub fn project_exists(&self, project_name: &str) -> bool {
let project_path = self.get_project_path(project_name);
project_path.exists() && project_path.join(format!("{}.rep", project_name)).exists()
}
pub fn create_project(&self, project_name: &str) -> Result<()> {
let project_path = self.get_project_path(project_name);
if self.project_exists(project_name) {
return Ok(());
}
std::fs::create_dir_all(&project_path)?;
// Run headless to create project
let headless = self.get_headless_script();
let output = Command::new(&headless)
.arg(project_path.to_str().unwrap())
.arg(project_name)
.output()?;
if !output.status.success() {
return Err(GhidraError::ExecutionFailed(
String::from_utf8_lossy(&output.stderr).to_string()
));
}
Ok(())
}
pub fn import_binary(&self, project_name: &str, binary_path: &Path, program_name: Option<&str>) -> Result<String> {
if !self.project_exists(project_name) {
self.create_project(project_name)?;
}
let program_name = program_name.unwrap_or_else(|| {
binary_path.file_name()
.and_then(|n| n.to_str())
.unwrap_or("program")
});
let project_path = self.get_project_path(project_name);
let headless = self.get_headless_script();
let output = Command::new(&headless)
.arg(project_path.to_str().unwrap())
.arg(project_name)
.arg("-import")
.arg(binary_path.to_str().unwrap())
.arg("-overwrite")
.output()?;
if !output.status.success() {
return Err(GhidraError::ExecutionFailed(
String::from_utf8_lossy(&output.stderr).to_string()
));
}
Ok(program_name.to_string())
}
pub fn analyze_program(&self, project_name: &str, program_name: &str) -> Result<()> {
let project_path = self.get_project_path(project_name);
let headless = self.get_headless_script();
let output = Command::new(&headless)
.arg(project_path.to_str().unwrap())
.arg(project_name)
.arg("-process")
.arg(program_name)
.arg("-noanalysis") // Don't auto-analyze
.arg("-scriptPath")
.arg(self.get_scripts_dir()?.to_str().unwrap())
.output()?;
if !output.status.success() {
return Err(GhidraError::ExecutionFailed(
String::from_utf8_lossy(&output.stderr).to_string()
));
}
Ok(())
}
pub fn run_script(&self, project_name: &str, program_name: &str, script_path: &Path, args: &[String]) -> Result<String> {
let project_path = self.get_project_path(project_name);
let headless = self.get_headless_script();
let mut cmd = Command::new(&headless);
cmd.arg(project_path.to_str().unwrap())
.arg(project_name)
.arg("-process")
.arg(program_name)
.arg("-scriptPath")
.arg(script_path.parent().unwrap().to_str().unwrap())
.arg("-postScript")
.arg(script_path.file_name().unwrap().to_str().unwrap());
for arg in args {
cmd.arg(arg);
}
let output = cmd.output()?;
if !output.status.success() {
return Err(GhidraError::ExecutionFailed(
String::from_utf8_lossy(&output.stderr).to_string()
));
}
Ok(String::from_utf8_lossy(&output.stdout).to_string())
}
fn get_scripts_dir(&self) -> Result<PathBuf> {
let config_dir = dirs::config_dir()
.ok_or_else(|| GhidraError::ConfigError("Could not determine config directory".to_string()))?;
let scripts_dir = config_dir.join("ghidra-cli").join("scripts");
if !scripts_dir.exists() {
std::fs::create_dir_all(&scripts_dir)?;
}
Ok(scripts_dir)
}
pub fn get_install_dir(&self) -> &Path {
&self.install_dir
}
pub fn get_project_dir(&self) -> &Path {
&self.project_dir
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ghidra_client_creation() {
// This will fail if GHIDRA_INSTALL_DIR is not set, which is expected
let config = Config::default();
let result = GhidraClient::new(config);
// We can't test this properly without a Ghidra installation
// Just verify the error is what we expect
if result.is_err() {
assert!(matches!(result.unwrap_err(), GhidraError::GhidraNotFound));
}
}
}
+310
View File
@@ -0,0 +1,310 @@
/// Built-in Ghidra scripts for data extraction
/// These are Python scripts that will be written to disk and executed by Ghidra headless
pub fn get_list_functions_script() -> &'static str {
r#"
# List all functions in the program
# @category Analysis
import json
functions = []
function_manager = currentProgram.getFunctionManager()
for func in function_manager.getFunctions(True):
entry = func.getEntryPoint()
body = func.getBody()
func_data = {
"name": func.getName(),
"address": entry.toString(),
"size": body.getNumAddresses(),
"entry_point": entry.toString(),
"signature": func.getPrototypeString(False, False) if func.getSignature() else None,
"calling_convention": func.getCallingConventionName(),
"comment": func.getComment()
}
# Get called functions
called = []
refs = func.getBody().getAddresses(True)
for addr in refs:
for ref in currentProgram.getReferenceManager().getReferencesFrom(addr):
if ref.getReferenceType().isCall():
to_addr = ref.getToAddress()
to_func = function_manager.getFunctionAt(to_addr)
if to_func:
called.append(to_func.getName())
func_data["calls"] = list(set(called))
# Get callers
callers = []
refs_to = currentProgram.getReferenceManager().getReferencesTo(entry)
for ref in refs_to:
if ref.getReferenceType().isCall():
from_addr = ref.getFromAddress()
from_func = function_manager.getFunctionContaining(from_addr)
if from_func:
callers.append(from_func.getName())
func_data["called_by"] = list(set(callers))
functions.append(func_data)
print(json.dumps(functions, indent=2))
"#
}
pub fn get_decompile_function_script() -> &'static str {
r#"
# Decompile a specific function
# @category Analysis
import json
from ghidra.app.decompiler import DecompInterface
from ghidra.util.task import ConsoleTaskMonitor
# Get function address from args
if len(args) < 1:
print(json.dumps({"error": "No address provided"}))
exit(1)
addr_str = args[0]
addr = currentProgram.getAddressFactory().getAddress(addr_str)
function_manager = currentProgram.getFunctionManager()
func = function_manager.getFunctionContaining(addr)
if not func:
print(json.dumps({"error": "No function at address " + addr_str}))
exit(1)
# Decompile
decompiler = DecompInterface()
decompiler.openProgram(currentProgram)
monitor = ConsoleTaskMonitor()
results = decompiler.decompileFunction(func, 30, monitor)
if results.decompileCompleted():
code = results.getDecompiledFunction().getC()
result = {
"name": func.getName(),
"address": func.getEntryPoint().toString(),
"signature": func.getPrototypeString(False, False),
"code": code
}
print(json.dumps(result, indent=2))
else:
print(json.dumps({"error": "Decompilation failed"}))
"#
}
pub fn get_list_strings_script() -> &'static str {
r#"
# List all strings in the program
# @category Analysis
import json
strings = []
string_table = currentProgram.getListing().getDefinedData(True)
for data in string_table:
if data.hasStringValue():
string_data = {
"address": data.getAddress().toString(),
"value": data.getValue().toString(),
"length": len(data.getValue().toString()),
"encoding": "ascii"
}
# Get references to this string
refs = []
refs_to = currentProgram.getReferenceManager().getReferencesTo(data.getAddress())
for ref in refs_to:
refs.append(ref.getFromAddress().toString())
string_data["references"] = refs
strings.append(string_data)
print(json.dumps(strings, indent=2))
"#
}
pub fn get_list_imports_script() -> &'static str {
r#"
# List all imports in the program
# @category Analysis
import json
imports = []
symbol_table = currentProgram.getSymbolTable()
external_manager = currentProgram.getExternalManager()
for symbol in symbol_table.getExternalSymbols():
external_location = external_manager.getExternalLocation(symbol)
if external_location:
import_data = {
"name": symbol.getName(),
"address": symbol.getAddress().toString(),
"library": external_location.getLibraryName(),
"is_external": True
}
imports.append(import_data)
print(json.dumps(imports, indent=2))
"#
}
pub fn get_list_exports_script() -> &'static str {
r#"
# List all exports in the program
# @category Analysis
import json
exports = []
symbol_table = currentProgram.getSymbolTable()
for symbol in symbol_table.getSymbolIterator():
if symbol.isExternalEntryPoint():
export_data = {
"name": symbol.getName(),
"address": symbol.getAddress().toString()
}
exports.append(export_data)
print(json.dumps(exports, indent=2))
"#
}
pub fn get_memory_map_script() -> &'static str {
r#"
# Get memory map
# @category Analysis
import json
blocks = []
memory = currentProgram.getMemory()
for block in memory.getBlocks():
block_data = {
"name": block.getName(),
"start": block.getStart().toString(),
"end": block.getEnd().toString(),
"size": block.getSize(),
"permissions": "",
"is_initialized": block.isInitialized(),
"is_loaded": block.isLoaded()
}
# Build permissions string
perms = ""
if block.isRead():
perms += "r"
if block.isWrite():
perms += "w"
if block.isExecute():
perms += "x"
block_data["permissions"] = perms
blocks.append(block_data)
print(json.dumps(blocks, indent=2))
"#
}
pub fn get_program_info_script() -> &'static str {
r#"
# Get program information
# @category Analysis
import json
info = {
"name": currentProgram.getName(),
"executable_path": currentProgram.getExecutablePath(),
"executable_format": currentProgram.getExecutableFormat(),
"compiler": currentProgram.getCompiler() if currentProgram.getCompiler() else None,
"language": currentProgram.getLanguage().toString(),
"image_base": currentProgram.getImageBase().toString(),
"min_address": currentProgram.getMinAddress().toString(),
"max_address": currentProgram.getMaxAddress().toString()
}
# Count functions and instructions
function_manager = currentProgram.getFunctionManager()
info["function_count"] = function_manager.getFunctionCount()
instruction_count = 0
listing = currentProgram.getListing()
for instruction in listing.getInstructions(True):
instruction_count += 1
info["instruction_count"] = instruction_count
print(json.dumps(info, indent=2))
"#
}
pub fn get_xrefs_to_script() -> &'static str {
r#"
# Get cross-references to an address
# @category Analysis
import json
if len(args) < 1:
print(json.dumps({"error": "No address provided"}))
exit(1)
addr_str = args[0]
addr = currentProgram.getAddressFactory().getAddress(addr_str)
xrefs = []
refs = currentProgram.getReferenceManager().getReferencesTo(addr)
function_manager = currentProgram.getFunctionManager()
for ref in refs:
from_addr = ref.getFromAddress()
from_func = function_manager.getFunctionContaining(from_addr)
to_func = function_manager.getFunctionContaining(addr)
xref_data = {
"from": from_addr.toString(),
"to": addr.toString(),
"ref_type": ref.getReferenceType().toString(),
"from_function": from_func.getName() if from_func else None,
"to_function": to_func.getName() if to_func else None
}
xrefs.append(xref_data)
print(json.dumps(xrefs, indent=2))
"#
}
/// Save a script to disk
pub fn save_script(name: &str, content: &str, scripts_dir: &std::path::Path) -> crate::error::Result<std::path::PathBuf> {
let script_path = scripts_dir.join(format!("{}.py", name));
std::fs::write(&script_path, content)?;
Ok(script_path)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_scripts_not_empty() {
assert!(!get_list_functions_script().is_empty());
assert!(!get_decompile_function_script().is_empty());
assert!(!get_list_strings_script().is_empty());
}
}
+670
View File
File diff suppressed because it is too large Load Diff
+356
View File
@@ -0,0 +1,356 @@
use serde_json::Value as JsonValue;
use crate::error::{GhidraError, Result};
use crate::filter::{Filter, FilterExpr};
use crate::format::{OutputFormat, Formatter, DefaultFormatter};
use crate::ghidra::GhidraClient;
use crate::ghidra::headless::HeadlessExecutor;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DataType {
Functions,
Strings,
Symbols,
Imports,
Exports,
XRefs,
Memory,
Sections,
Comments,
Types,
Instructions,
BasicBlocks,
CallGraph,
Data,
References,
}
impl DataType {
pub fn from_str(s: &str) -> Result<Self> {
match s.to_lowercase().as_str() {
"functions" | "function" | "fn" => Ok(Self::Functions),
"strings" | "string" | "str" => Ok(Self::Strings),
"symbols" | "symbol" | "sym" => Ok(Self::Symbols),
"imports" | "import" => Ok(Self::Imports),
"exports" | "export" => Ok(Self::Exports),
"xrefs" | "xref" | "crossrefs" => Ok(Self::XRefs),
"memory" | "mem" => Ok(Self::Memory),
"sections" | "section" => Ok(Self::Sections),
"comments" | "comment" => Ok(Self::Comments),
"types" | "type" => Ok(Self::Types),
"instructions" | "instruction" | "insn" => Ok(Self::Instructions),
"basicblocks" | "basic-blocks" | "blocks" => Ok(Self::BasicBlocks),
"callgraph" | "call-graph" => Ok(Self::CallGraph),
"data" => Ok(Self::Data),
"references" | "refs" => Ok(Self::References),
_ => Err(GhidraError::InvalidDataType(format!("Unknown data type: {}", s))),
}
}
}
pub struct Query {
pub data_type: DataType,
pub filter: Option<Filter>,
pub fields: Option<FieldSelector>,
pub format: OutputFormat,
pub limit: Option<usize>,
pub offset: Option<usize>,
pub sort: Option<Vec<SortKey>>,
pub count_only: bool,
}
impl Query {
pub fn new(data_type: DataType) -> Self {
Self {
data_type,
filter: None,
fields: None,
format: OutputFormat::Json,
limit: None,
offset: None,
sort: None,
count_only: false,
}
}
pub fn with_filter(mut self, filter: Filter) -> Self {
self.filter = Some(filter);
self
}
pub fn with_format(mut self, format: OutputFormat) -> Self {
self.format = format;
self
}
pub fn with_limit(mut self, limit: usize) -> Self {
self.limit = Some(limit);
self
}
pub fn with_offset(mut self, offset: usize) -> Self {
self.offset = Some(offset);
self
}
pub fn count_only(mut self) -> Self {
self.count_only = true;
self
}
pub fn execute(&self, client: &GhidraClient, project: &str, program: &str) -> Result<String> {
let executor = HeadlessExecutor::new(client);
// Fetch data based on type
let data = match self.data_type {
DataType::Functions => executor.list_functions(project, program)?,
DataType::Strings => executor.list_strings(project, program)?,
DataType::Imports => executor.list_imports(project, program)?,
DataType::Exports => executor.list_exports(project, program)?,
DataType::Memory => executor.get_memory_map(project, program)?,
_ => {
return Err(GhidraError::Other(format!(
"Data type {:?} not yet implemented",
self.data_type
)));
}
};
// Data should be an array
let data_array = match data {
JsonValue::Array(arr) => arr,
_ => {
return Err(GhidraError::ExecutionFailed(
"Expected array from Ghidra script".to_string()
));
}
};
// Apply filter
let filtered = if let Some(filter) = &self.filter {
self.apply_filter(&data_array, filter)?
} else {
data_array
};
// Apply field selection
let selected = if let Some(fields) = &self.fields {
self.select_fields(&filtered, fields)?
} else {
filtered
};
// Apply sorting
let sorted = if let Some(sort) = &self.sort {
self.apply_sort(&selected, sort)?
} else {
selected
};
// Apply pagination
let paginated = self.apply_pagination(&sorted);
// Return count if requested
if self.count_only {
return Ok(paginated.len().to_string());
}
// Format output
let formatter = DefaultFormatter;
formatter.format(&paginated, self.format)
}
fn apply_filter(&self, data: &[JsonValue], filter: &Filter) -> Result<Vec<JsonValue>> {
let mut result = Vec::new();
for item in data {
if filter.evaluate(item)? {
result.push(item.clone());
}
}
Ok(result)
}
fn select_fields(&self, data: &[JsonValue], selector: &FieldSelector) -> Result<Vec<JsonValue>> {
let mut result = Vec::new();
for item in data {
if let JsonValue::Object(map) = item {
let mut new_map = serde_json::Map::new();
if let Some(include) = &selector.include {
for field in include {
if let Some(value) = map.get(field) {
new_map.insert(field.clone(), value.clone());
}
}
} else if let Some(exclude) = &selector.exclude {
for (key, value) in map {
if !exclude.contains(key) {
new_map.insert(key.clone(), value.clone());
}
}
} else {
new_map = map.clone();
}
result.push(JsonValue::Object(new_map));
} else {
result.push(item.clone());
}
}
Ok(result)
}
fn apply_sort(&self, data: &[JsonValue], sort_keys: &[SortKey]) -> Result<Vec<JsonValue>> {
let mut result = data.to_vec();
result.sort_by(|a, b| {
for sort_key in sort_keys {
let a_val = self.get_field_for_sort(a, &sort_key.field);
let b_val = self.get_field_for_sort(b, &sort_key.field);
let cmp = match (&a_val, &b_val) {
(Some(JsonValue::Number(a)), Some(JsonValue::Number(b))) => {
a.as_f64().partial_cmp(&b.as_f64()).unwrap_or(std::cmp::Ordering::Equal)
}
(Some(JsonValue::String(a)), Some(JsonValue::String(b))) => a.cmp(b),
_ => std::cmp::Ordering::Equal,
};
let final_cmp = if sort_key.descending {
cmp.reverse()
} else {
cmp
};
if final_cmp != std::cmp::Ordering::Equal {
return final_cmp;
}
}
std::cmp::Ordering::Equal
});
Ok(result)
}
fn get_field_for_sort(&self, value: &JsonValue, field: &str) -> Option<JsonValue> {
if let JsonValue::Object(map) = value {
map.get(field).cloned()
} else {
None
}
}
fn apply_pagination(&self, data: &[JsonValue]) -> Vec<JsonValue> {
let offset = self.offset.unwrap_or(0);
let limit = self.limit.unwrap_or(usize::MAX);
data.iter()
.skip(offset)
.take(limit)
.cloned()
.collect()
}
}
pub struct FieldSelector {
pub include: Option<Vec<String>>,
pub exclude: Option<Vec<String>>,
}
impl FieldSelector {
pub fn include(fields: Vec<String>) -> Self {
Self {
include: Some(fields),
exclude: None,
}
}
pub fn exclude(fields: Vec<String>) -> Self {
Self {
include: None,
exclude: Some(fields),
}
}
pub fn parse(input: &str) -> Result<Self> {
if input.starts_with('-') {
// Exclude fields
let fields: Vec<String> = input
.trim_start_matches('-')
.split(',')
.map(|s| s.trim().to_string())
.collect();
Ok(Self::exclude(fields))
} else {
// Include fields
let fields: Vec<String> = input
.split(',')
.map(|s| s.trim().to_string())
.collect();
Ok(Self::include(fields))
}
}
}
pub struct SortKey {
pub field: String,
pub descending: bool,
}
impl SortKey {
pub fn parse(input: &str) -> Vec<Self> {
input
.split(',')
.map(|s| {
let s = s.trim();
if s.starts_with('-') {
SortKey {
field: s.trim_start_matches('-').to_string(),
descending: true,
}
} else {
SortKey {
field: s.to_string(),
descending: false,
}
}
})
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_data_type_parsing() {
assert_eq!(DataType::from_str("functions").unwrap(), DataType::Functions);
assert_eq!(DataType::from_str("fn").unwrap(), DataType::Functions);
assert_eq!(DataType::from_str("strings").unwrap(), DataType::Strings);
}
#[test]
fn test_field_selector_parse() {
let selector = FieldSelector::parse("name,address,size").unwrap();
assert!(selector.include.is_some());
assert_eq!(selector.include.unwrap().len(), 3);
let selector = FieldSelector::parse("-metadata,internal").unwrap();
assert!(selector.exclude.is_some());
}
#[test]
fn test_sort_key_parse() {
let keys = SortKey::parse("name,-size");
assert_eq!(keys.len(), 2);
assert_eq!(keys[0].field, "name");
assert!(!keys[0].descending);
assert_eq!(keys[1].field, "size");
assert!(keys[1].descending);
}
}