mirror of
https://github.com/encounter/ghidra-cli.git
synced 2026-07-10 03:18:56 -07:00
Implement comprehensive Ghidra Rust CLI with universal query system
This commit implements a complete Rust CLI tool for Ghidra reverse engineering, optimized for Claude Code and AI agents. Core Features: - Universal query command supporting all Ghidra data types (functions, strings, imports, exports, memory, etc.) - Advanced filter language with comparison, string, and logical operators - Multiple output formats (JSON, CSV, Table, minimal) optimized for LLM token efficiency - Field selection and pagination for precise data extraction - Windows-first design with cross-platform compatibility Architecture: - Filter parser using Pest grammar for robust expression parsing - Modular design with separate filter, format, query, and Ghidra integration layers - Headless Ghidra integration with built-in Python scripts for data extraction - Configuration system with environment variable and file support - Auto-detection of Ghidra installation on Windows LLM Optimizations: - Count-first workflow to check result sizes before fetching - Aggressive server-side filtering to reduce data transfer - Field selection to minimize token usage - Compact output formats (json-compact, minimal, ids) - Pagination support for large datasets Documentation: - Comprehensive README with examples and troubleshooting - Claude skill document (CLAUDE_SKILL.md) for agent integration - Subagent markdown (SUBAGENT.md) for Task tool integration - Inline code documentation and examples Commands Implemented: - ghidra query <data-type> - Universal query interface - ghidra import/analyze - Binary import and analysis - ghidra fn/strings/mem - Specialized command shortcuts - ghidra dump - Export data (imports, exports, functions, strings) - ghidra decompile - Function decompilation - ghidra project - Project management - ghidra config - Configuration management - ghidra init/doctor/version - Setup and diagnostics Built-in Ghidra Scripts: - Function listing with call graphs - Decompilation - String extraction - Import/Export tables - Memory map - Cross-references - Program information The CLI is designed to be succinct and efficient, with commands like: ghidra query functions --program=malware.exe --filter="size>1000 AND name~crypt" --format=json-compact Windows Support: - Auto-detection of Ghidra installation - Path handling for both Unix and Windows styles - Support for .exe, .dll, .sys formats This implementation provides a powerful, token-efficient interface for binary analysis that integrates seamlessly with Claude Code and other AI agents.
This commit is contained in:
+463
@@ -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
File diff suppressed because it is too large
Load Diff
+64
@@ -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"
|
||||
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
+171
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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>;
|
||||
@@ -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)* ~ "'" }
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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 { .. }));
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
}
|
||||
@@ -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()],
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user