mirror of
https://github.com/encounter/ghidra-cli.git
synced 2026-07-10 03:18:56 -07:00
Refactor ilspy-cli for enhanced .NET decompilation and project support
- Introduced `decompile_project` function in IlSpyBridge to handle project-style decompilation, generating per-type .cs files. - Updated FFI layer to support new decompile_project functionality. - Enhanced CLI to accept output directory for project decompilation, ensuring compatibility with existing type and method flags. - Improved error handling and output formatting for decompilation results. - Added comprehensive command reference for ilspy-cli, detailing usage for detecting .NET vs native binaries, listing types and methods, and searching decompiled source. - Integrated memory reading capabilities in GhidraCliBridge for enhanced analysis workflows. - Updated main.rs to robustly check for existing bridge instances, improving reliability in project mode.
This commit is contained in:
+292
-420
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,261 @@
|
||||
---
|
||||
name: ilspy-cli
|
||||
description: >
|
||||
Use ilspy-cli for .NET assembly decompilation and analysis. Produces clean C# source from .NET IL bytecode.
|
||||
Activate when the user requests:
|
||||
- .NET assembly decompilation
|
||||
- .NET type or method inspection
|
||||
- Detecting whether a binary is .NET or native
|
||||
- Searching decompiled .NET source code
|
||||
- Assembly metadata inspection
|
||||
Prefer ilspy-cli over ghidra-cli for .NET binaries (ghidra produces poor output for .NET IL).
|
||||
---
|
||||
|
||||
# ilspy-cli Agent Reference
|
||||
|
||||
Rust CLI for .NET decompilation using ILSpy (ICSharpCode.Decompiler). Binary name: `ilspy`.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
Rust CLI (clap) ──netcorehost FFI──► C# Bridge DLL (IlSpyBridge.dll) ──► ICSharpCode.Decompiler
|
||||
```
|
||||
|
||||
- **In-process**: .NET runtime hosted inside the Rust binary via `netcorehost` crate
|
||||
- **No server/daemon**: each command loads the assembly, runs, exits
|
||||
- **Data exchange**: JSON strings over FFI function pointers with `[UnmanagedCallersOnly]`
|
||||
- **Key differentiator**: single-method decompilation (`--type T --method M`), which `ilspycmd` cannot do
|
||||
|
||||
## Requirements
|
||||
|
||||
- .NET 8 runtime (loaded by netcorehost at runtime)
|
||||
- .NET 8 SDK (build time only, for `dotnet publish` of the C# bridge)
|
||||
|
||||
## When to Use ilspy-cli vs ghidra-cli
|
||||
|
||||
| Binary type | Tool | Reason |
|
||||
|-------------|------|--------|
|
||||
| .NET (.dll/.exe with CLR header) | `ilspy` | Clean C# decompilation |
|
||||
| Native (C/C++/Delphi/Go) | `ghidra` | Assembly-level analysis |
|
||||
| Unknown | `ilspy detect` first | Classifies .NET vs native |
|
||||
|
||||
Use `ilspy detect` to triage before choosing a tool.
|
||||
|
||||
## Global Flags
|
||||
|
||||
| Flag | Effect |
|
||||
|------|--------|
|
||||
| `--json` | Minified JSON output |
|
||||
| `--pretty` | Pretty-printed JSON |
|
||||
| `--compact` | One line per item |
|
||||
| `-v` / `--verbose` | Verbose output |
|
||||
|
||||
**Format auto-detection**: TTY → table; non-TTY → compact.
|
||||
|
||||
## Command Reference
|
||||
|
||||
### Detect (.NET vs Native)
|
||||
|
||||
Classify binaries without loading .NET runtime (pure Rust PE header parsing).
|
||||
|
||||
```bash
|
||||
ilspy detect FILE # single file
|
||||
ilspy detect DIRECTORY # scan directory (top-level)
|
||||
ilspy detect DIRECTORY --recursive # scan recursively
|
||||
ilspy detect DIRECTORY --dotnet-only # show only .NET assemblies
|
||||
ilspy detect DIRECTORY --native-only # show only native binaries
|
||||
```
|
||||
|
||||
**Output fields**: path, isDotnet, framework, recommendedTool
|
||||
|
||||
Framework detection includes: .NET Core/5+/6+/7+/8+, .NET Framework, .NET Standard, Native (Delphi), Native (Qt/C++), Native (MFC/C++).
|
||||
|
||||
The `recommendedTool` field returns `"ilspy"` for .NET or `"ghidra"` for native.
|
||||
|
||||
### List Types
|
||||
|
||||
```bash
|
||||
ilspy list types ASSEMBLY # all types
|
||||
ilspy list types ASSEMBLY --filter Controller # substring filter (case-insensitive)
|
||||
ilspy list types ASSEMBLY --kind class # filter by kind
|
||||
ilspy list types ASSEMBLY --kind enum
|
||||
ilspy list types ASSEMBLY --filter Crypto --kind class
|
||||
```
|
||||
|
||||
**Kind values**: `class`, `interface`, `struct`, `enum`, `delegate`
|
||||
|
||||
**Output fields**: fullName, ns, name, kind, methodCount, propertyCount, fieldCount, isPublic
|
||||
|
||||
### List Methods
|
||||
|
||||
```bash
|
||||
ilspy list methods ASSEMBLY # all methods
|
||||
ilspy list methods ASSEMBLY --type MyNamespace.MyClass # methods of one type
|
||||
ilspy list methods ASSEMBLY --type MyNamespace.MyClass --filter DoWork # filter by name
|
||||
```
|
||||
|
||||
**Output fields**: typeName, name, returnType, parameters (name + type), accessibility, isStatic, isVirtual, isAbstract
|
||||
|
||||
### Decompile
|
||||
|
||||
```bash
|
||||
ilspy decompile ASSEMBLY # full assembly → C# source
|
||||
ilspy decompile ASSEMBLY --type MyNamespace.MyClass # single type
|
||||
ilspy decompile ASSEMBLY --type MyNamespace.MyClass --method DoWork # single method!
|
||||
```
|
||||
|
||||
**Output**: C# source code (default). With `--json`/`--pretty`: `{ "source": "...", "typeName": "...", "methodName": "...", "returnType": "..." }`
|
||||
|
||||
`--method` requires `--type` (must specify the containing type).
|
||||
|
||||
**Default format override**: decompile always outputs raw source unless `--json` or `--pretty` is specified.
|
||||
|
||||
### Search Decompiled Source
|
||||
|
||||
```bash
|
||||
ilspy search ASSEMBLY "ConnectionString"
|
||||
ilspy search ASSEMBLY "password|secret|key"
|
||||
ilspy search ASSEMBLY "HttpClient\.Post"
|
||||
```
|
||||
|
||||
Pattern is a **regex** (case-insensitive, multiline). The tool decompiles each type and runs the regex against the C# source.
|
||||
|
||||
**Output fields per match**: typeName, matchCount, matches[].line, matches[].matched, matches[].context (surrounding lines)
|
||||
|
||||
### Assembly Info
|
||||
|
||||
```bash
|
||||
ilspy info ASSEMBLY
|
||||
```
|
||||
|
||||
**Output fields**: name, typeCount, targetFramework, references[].name, references[].version
|
||||
|
||||
### Doctor (Health Check)
|
||||
|
||||
```bash
|
||||
ilspy doctor
|
||||
```
|
||||
|
||||
Checks: .NET runtime availability, bridge DLL presence, bridge loading.
|
||||
|
||||
**Environment variable**: `ILSPY_BRIDGE_DIR` overrides bridge DLL search path.
|
||||
|
||||
## Agent Best Practices
|
||||
|
||||
### 1. Triage First
|
||||
|
||||
Always detect before decompiling an unknown binary:
|
||||
|
||||
```bash
|
||||
ilspy detect ./target.dll --json
|
||||
# Check recommendedTool field
|
||||
```
|
||||
|
||||
### 2. Count-First for Large Assemblies
|
||||
|
||||
```bash
|
||||
# Check type count
|
||||
ilspy info MyLib.dll --json
|
||||
# If typeCount is large, filter:
|
||||
ilspy list types MyLib.dll --filter Controller --json
|
||||
```
|
||||
|
||||
### 3. Targeted Decompilation
|
||||
|
||||
Decompile specific types/methods instead of entire assemblies:
|
||||
|
||||
```bash
|
||||
# GOOD: specific type
|
||||
ilspy decompile MyLib.dll --type MyApp.Services.AuthService
|
||||
|
||||
# GOOD: specific method
|
||||
ilspy decompile MyLib.dll --type MyApp.Services.AuthService --method ValidateToken
|
||||
|
||||
# AVOID for large assemblies: full decompile
|
||||
ilspy decompile MyLib.dll
|
||||
```
|
||||
|
||||
### 4. Search Before Decompile
|
||||
|
||||
Find relevant types first, then decompile:
|
||||
|
||||
```bash
|
||||
# Find types containing crypto code
|
||||
ilspy search MyLib.dll "AES|Rijndael|SHA256" --json
|
||||
|
||||
# Decompile the matching type
|
||||
ilspy decompile MyLib.dll --type MyApp.Security.CryptoHelper
|
||||
```
|
||||
|
||||
### 5. Use Compact/JSON for Parsing
|
||||
|
||||
```bash
|
||||
ilspy list types MyLib.dll --json # minified JSON for piping
|
||||
ilspy list types MyLib.dll --compact # one line per type
|
||||
ilspy decompile MyLib.dll --type T --json # source wrapped in JSON
|
||||
```
|
||||
|
||||
## Analysis Workflow
|
||||
|
||||
```bash
|
||||
# 1. Detect binary type
|
||||
ilspy detect ./target.dll
|
||||
|
||||
# 2. Get overview
|
||||
ilspy info ./target.dll
|
||||
ilspy list types ./target.dll --json
|
||||
|
||||
# 3. Find interesting types
|
||||
ilspy list types ./target.dll --filter Service --kind class
|
||||
ilspy list types ./target.dll --filter Crypto
|
||||
|
||||
# 4. Inspect methods
|
||||
ilspy list methods ./target.dll --type MyApp.Services.AuthService
|
||||
|
||||
# 5. Decompile specific method
|
||||
ilspy decompile ./target.dll --type MyApp.Services.AuthService --method ValidateToken
|
||||
|
||||
# 6. Search for patterns
|
||||
ilspy search ./target.dll "password|credential|secret"
|
||||
ilspy search ./target.dll "HttpClient|WebRequest|Socket"
|
||||
ilspy search ./target.dll "Process\.Start|Shell|Exec"
|
||||
```
|
||||
|
||||
## Directory Scanning Workflow
|
||||
|
||||
```bash
|
||||
# Triage a directory of binaries
|
||||
ilspy detect "C:\Program Files\MyApp" --recursive --json
|
||||
|
||||
# Focus on .NET assemblies only
|
||||
ilspy detect "C:\Program Files\MyApp" --recursive --dotnet-only --compact
|
||||
```
|
||||
|
||||
## Integration with ghidra-cli
|
||||
|
||||
ghidra-cli's `decompile` command warns when it encounters .NET IL bytecode:
|
||||
> "This appears to be .NET managed code. Consider using ilspy-cli."
|
||||
|
||||
Workflow for mixed codebases:
|
||||
|
||||
```bash
|
||||
# 1. Classify all binaries
|
||||
ilspy detect ./binaries --recursive --json
|
||||
|
||||
# 2. Use ilspy for .NET
|
||||
ilspy decompile ./binaries/managed.dll --type MyClass
|
||||
|
||||
# 3. Use ghidra for native
|
||||
ghidra import ./binaries/native.exe --project analysis
|
||||
ghidra decompile main --project analysis
|
||||
```
|
||||
|
||||
## Error Recovery
|
||||
|
||||
| Problem | Fix |
|
||||
|---------|-----|
|
||||
| "dotnet not found" | Install .NET 8 SDK: https://dot.net/download |
|
||||
| "Bridge DLL not found" | `cargo build` or set `ILSPY_BRIDGE_DIR` |
|
||||
| "Type not found" | Use `ilspy list types ASSEMBLY --filter NAME` to find exact fullName |
|
||||
| "Method not found" | Use `ilspy list methods ASSEMBLY --type T` to see available methods |
|
||||
| Bridge load fails | Run `ilspy doctor` for diagnostics |
|
||||
@@ -1,13 +1,16 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection.Metadata;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using ICSharpCode.Decompiler;
|
||||
using ICSharpCode.Decompiler.CSharp;
|
||||
using ICSharpCode.Decompiler.CSharp.ProjectDecompiler;
|
||||
using ICSharpCode.Decompiler.Metadata;
|
||||
using ICSharpCode.Decompiler.TypeSystem;
|
||||
|
||||
@@ -398,6 +401,45 @@ public static class IlSpyBridge
|
||||
}
|
||||
}
|
||||
|
||||
[UnmanagedCallersOnly(EntryPoint = "DecompileProject")]
|
||||
public static IntPtr DecompileProject(IntPtr pathPtr, int pathLen,
|
||||
IntPtr targetDirPtr, int targetDirLen,
|
||||
IntPtr resultLenPtr)
|
||||
{
|
||||
int resultLen = 0;
|
||||
try
|
||||
{
|
||||
var path = ReadUtf8(pathPtr, pathLen);
|
||||
var targetDir = ReadUtf8(targetDirPtr, targetDirLen);
|
||||
|
||||
var peFile = new PEFile(path);
|
||||
|
||||
Directory.CreateDirectory(targetDir);
|
||||
var resolver = new UniversalAssemblyResolver(path, false, null);
|
||||
var decompiler = new WholeProjectDecompiler(resolver);
|
||||
|
||||
decompiler.DecompileProject(peFile, targetDir, CancellationToken.None);
|
||||
|
||||
var fileCount = Directory.GetFiles(targetDir, "*.cs", SearchOption.AllDirectories).Length;
|
||||
|
||||
var result = new
|
||||
{
|
||||
files = fileCount,
|
||||
directory = targetDir
|
||||
};
|
||||
|
||||
var ptr = MarshalJson(result, out resultLen);
|
||||
Marshal.WriteInt32(resultLenPtr, resultLen);
|
||||
return ptr;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var ptr = MarshalError(ex.Message, out resultLen);
|
||||
Marshal.WriteInt32(resultLenPtr, resultLen);
|
||||
return ptr;
|
||||
}
|
||||
}
|
||||
|
||||
[UnmanagedCallersOnly(EntryPoint = "FreeMem")]
|
||||
public static void FreeMem(IntPtr ptr)
|
||||
{
|
||||
|
||||
@@ -33,6 +33,8 @@ pub type FnThreeArgs = unsafe extern "system" fn(
|
||||
/// FFI function signature for FreeMem.
|
||||
pub type FnFreeMem = unsafe extern "system" fn(*mut u8);
|
||||
|
||||
/// Call a two-arg bridge function and return the JSON string result.
|
||||
|
||||
/// Call a one-arg bridge function and return the JSON string result.
|
||||
pub unsafe fn call_one_arg(func: FnOneArg, free: FnFreeMem, arg1: &str) -> String {
|
||||
let mut result_len: c_int = 0;
|
||||
|
||||
@@ -17,6 +17,7 @@ pub struct IlSpyBridge {
|
||||
decompile_type_fn: FnTwoArgs,
|
||||
decompile_method_fn: FnThreeArgs,
|
||||
decompile_full_fn: FnOneArg,
|
||||
decompile_project_fn: FnTwoArgs,
|
||||
assembly_info_fn: FnOneArg,
|
||||
search_fn: FnTwoArgs,
|
||||
free_fn: FnFreeMem,
|
||||
@@ -73,6 +74,7 @@ impl IlSpyBridge {
|
||||
decompile_type_fn: load!("DecompileType", FnTwoArgs),
|
||||
decompile_method_fn: load!("DecompileMethod", FnThreeArgs),
|
||||
decompile_full_fn: load!("DecompileFull", FnOneArg),
|
||||
decompile_project_fn: load!("DecompileProject", FnTwoArgs),
|
||||
assembly_info_fn: load!("GetAssemblyInfo", FnOneArg),
|
||||
search_fn: load!("SearchSource", FnTwoArgs),
|
||||
free_fn: load!("FreeMem", FnFreeMem),
|
||||
@@ -137,6 +139,12 @@ impl IlSpyBridge {
|
||||
Self::parse_result(&json)
|
||||
}
|
||||
|
||||
/// Decompile a project into a directory of per-type .cs files.
|
||||
pub fn decompile_project(&self, assembly: &str, target_dir: &str) -> Result<serde_json::Value> {
|
||||
let json = unsafe { call_two_args(self.decompile_project_fn, self.free_fn, assembly, target_dir) };
|
||||
Self::parse_result(&json)
|
||||
}
|
||||
|
||||
// ── Internal helpers ─────────────────────────────────────────────
|
||||
|
||||
fn parse_result<T: serde::de::DeserializeOwned>(json: &str) -> Result<T> {
|
||||
|
||||
@@ -103,6 +103,10 @@ pub struct DecompileArgs {
|
||||
/// Decompile a specific method (requires --type)
|
||||
#[arg(long, short, requires = "type")]
|
||||
pub method: Option<String>,
|
||||
|
||||
/// Output directory for project-style decompilation (one .cs file per type)
|
||||
#[arg(long, short = 'o')]
|
||||
pub output_dir: Option<PathBuf>,
|
||||
}
|
||||
|
||||
#[derive(Args, Debug)]
|
||||
|
||||
@@ -8,6 +8,30 @@ pub fn decompile(bridge: &IlSpyBridge, args: &DecompileArgs, fmt: OutputFormat)
|
||||
.unwrap_or_else(|_| args.assembly.clone());
|
||||
let path_str = assembly.to_string_lossy();
|
||||
|
||||
if let Some(output_dir) = &args.output_dir {
|
||||
if args.r#type.is_some() || args.method.is_some() {
|
||||
return Err(crate::error::IlSpyError::BridgeCallFailed(
|
||||
"--output-dir cannot be used with --type or --method".to_string()
|
||||
));
|
||||
}
|
||||
|
||||
let target_dir = dunce::canonicalize(output_dir)
|
||||
.unwrap_or_else(|_| output_dir.clone());
|
||||
let target_str = target_dir.to_string_lossy();
|
||||
|
||||
let result = bridge.decompile_project(&path_str, &target_str)?;
|
||||
|
||||
return Ok(match fmt {
|
||||
OutputFormat::Json => serde_json::to_string(&result).unwrap_or_default(),
|
||||
OutputFormat::JsonPretty => serde_json::to_string_pretty(&result).unwrap_or_default(),
|
||||
_ => {
|
||||
let files = result.get("files").and_then(|v| v.as_u64()).unwrap_or(0);
|
||||
let dir = result.get("directory").and_then(|v| v.as_str()).unwrap_or("");
|
||||
format!("Decompiled {} files to {}", files, dir)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let result = match (&args.r#type, &args.method) {
|
||||
(Some(type_name), Some(method_name)) => {
|
||||
// Single method decompilation
|
||||
|
||||
@@ -243,6 +243,8 @@ public class GhidraCliBridge extends GhidraScript {
|
||||
case "batch": return handleBatch(args);
|
||||
// Bridge info
|
||||
case "bridge_info": return handleBridgeInfo();
|
||||
// Memory read
|
||||
case "read_memory": return handleReadMemory(args);
|
||||
default: return null;
|
||||
}
|
||||
}
|
||||
@@ -2603,4 +2605,74 @@ public class GhidraCliBridge extends GhidraScript {
|
||||
// Batch operations are handled by the Rust side, not the bridge directly
|
||||
return errorResult("Batch operations are handled by the CLI, not via bridge script");
|
||||
}
|
||||
|
||||
// --- Memory Read Handler ---
|
||||
|
||||
private JsonObject handleReadMemory(JsonObject args) {
|
||||
String addrStr = getArgString(args, "address");
|
||||
if (addrStr == null) return errorResult("Address required");
|
||||
|
||||
int size = 200;
|
||||
if (args != null && args.has("size")) {
|
||||
size = args.get("size").getAsInt();
|
||||
}
|
||||
|
||||
try {
|
||||
ghidra.program.model.mem.Memory mem = currentProgram.getMemory();
|
||||
ghidra.program.model.address.AddressFactory af = currentProgram.getAddressFactory();
|
||||
|
||||
// Parse address
|
||||
long addrLong;
|
||||
if (addrStr.startsWith("0x") || addrStr.startsWith("0X")) {
|
||||
addrLong = Long.parseUnsignedLong(addrStr.substring(2), 16);
|
||||
} else {
|
||||
addrLong = Long.parseUnsignedLong(addrStr, 16);
|
||||
}
|
||||
|
||||
ghidra.program.model.address.Address baseAddr = af.getDefaultAddressSpace().getAddress(addrLong);
|
||||
|
||||
// Read bytes
|
||||
byte[] bytes = new byte[size];
|
||||
int bytesRead = mem.getBytes(baseAddr, bytes);
|
||||
|
||||
// Build hex string
|
||||
StringBuilder hexStr = new StringBuilder();
|
||||
for (int i = 0; i < bytesRead; i++) {
|
||||
hexStr.append(String.format("%02x", bytes[i] & 0xFF));
|
||||
}
|
||||
|
||||
// Also interpret as array of 8-byte pointers
|
||||
JsonArray pointers = new JsonArray();
|
||||
for (int i = 0; i + 7 < bytesRead; i += 8) {
|
||||
long val = 0;
|
||||
for (int j = 0; j < 8; j++) {
|
||||
val |= ((long)(bytes[i+j] & 0xFF)) << (8*j);
|
||||
}
|
||||
JsonObject ptrObj = new JsonObject();
|
||||
ptrObj.addProperty("offset", i);
|
||||
ptrObj.addProperty("address", String.format("0x%08x", addrLong + i));
|
||||
ptrObj.addProperty("value", String.format("0x%016x", val));
|
||||
|
||||
// Check if value looks like a code address
|
||||
if (val >= 0x00401000L && val <= 0x05bb99ffL) {
|
||||
ghidra.program.model.address.Address funcAddr = af.getDefaultAddressSpace().getAddress(val);
|
||||
ghidra.program.model.listing.Function func = currentProgram.getFunctionManager().getFunctionAt(funcAddr);
|
||||
if (func != null) {
|
||||
ptrObj.addProperty("function", func.getName());
|
||||
}
|
||||
}
|
||||
|
||||
pointers.add(ptrObj);
|
||||
}
|
||||
|
||||
JsonObject result = new JsonObject();
|
||||
result.addProperty("address", String.format("0x%08x", addrLong));
|
||||
result.addProperty("size", bytesRead);
|
||||
result.addProperty("hex", hexStr.toString());
|
||||
result.add("pointers", pointers);
|
||||
return result;
|
||||
} catch (Exception e) {
|
||||
return errorResult("Failed to read memory: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+17
-3
@@ -432,9 +432,23 @@ fn run_with_bridge(cli: Cli) -> anyhow::Result<()> {
|
||||
anyhow::bail!("Binary not found: {}", args.binary);
|
||||
}
|
||||
|
||||
// Check if bridge is already running
|
||||
if let Some(port) = bridge::is_bridge_running(&project_path) {
|
||||
// Bridge running - import via bridge command
|
||||
// First, robustly check if a bridge is already running using ensure_bridge_running
|
||||
// with Project mode. This will reuse an existing bridge or start a minimal one.
|
||||
let existing_bridge_port = bridge::read_port_file(&project_path)
|
||||
.ok()
|
||||
.flatten()
|
||||
.and_then(|port| {
|
||||
// Verify this port is actually reachable
|
||||
let client = BridgeClient::new(port);
|
||||
if client.ping().unwrap_or(false) {
|
||||
Some(port)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
if let Some(port) = existing_bridge_port {
|
||||
// Bridge is running - import via TCP command
|
||||
let client = BridgeClient::new(port);
|
||||
verify_bridge(&client)?;
|
||||
let result = client.import_binary(&args.binary, args.program.as_deref())?;
|
||||
|
||||
Reference in New Issue
Block a user