Release v4.0.0: Go pclntab, .NET CIL, Android APK forensics, and OpenCode/Claude Code MCP tools

This commit is contained in:
Kevin Thomas committed 2026-10-12 11:20:08 -04:00
1 parent e8d097f148
commit 8652218cd2
17 files changed
+1830 -84

No files matched your search

Generated
+6 -6
View File
@@ -1890,7 +1890,7 @@ dependencies = [
[[package]]
name = "rea"
version = "3.2.0"
version = "4.0.0"
dependencies = [
"rea-engine",
"rea-report",
@@ -1900,7 +1900,7 @@ dependencies = [
[[package]]
name = "rea-engine"
version = "3.2.0"
version = "4.0.0"
dependencies = [
"goblin",
"iced-x86",
@@ -1916,7 +1916,7 @@ dependencies = [
[[package]]
name = "rea-report"
version = "3.2.0"
version = "4.0.0"
dependencies = [
"typst",
"typst-assets",
@@ -1926,7 +1926,7 @@ dependencies = [
[[package]]
name = "rea-sleigh"
version = "3.2.0"
version = "4.0.0"
dependencies = [
"include_dir",
"wazabin-sleigh",
@@ -1999,7 +1999,7 @@ dependencies = [
[[package]]
name = "reverse-engineering"
version = "3.2.0"
version = "4.0.0"
dependencies = [
"rea",
"rea-engine",
@@ -3582,7 +3582,7 @@ checksum = "9440ea3e5aeabb0ac63af70daf835274065238cdd0cec83418f417eae38bacee"
[[package]]
name = "xtask"
version = "3.2.0"
version = "4.0.0"
dependencies = [
"proc-macro2",
"syn 2.0.119",
+1 -1
View File
@@ -3,7 +3,7 @@ resolver = "2"
members = ["crates/rea-sleigh", "crates/rea", "crates/rea-engine", "crates/rea-report", "crates/reverse-engineering", "xtask"]
[workspace.package]
version = "3.2.0"
version = "4.0.0"
edition = "2021"
authors = ["Kevin Thomas"]
license = "MIT"
+12 -8
View File
@@ -4,7 +4,7 @@
<br>
# Reverse Engineering Assistant (REA) v3.2.0
# Reverse Engineering Assistant (REA) v4.0.0
Autonomous Reverse Engineering Framework
@@ -18,15 +18,15 @@ Writes `re-suspect.md` and `re-suspect.pdf` next to the target. That's the whole
<br>
# New in v3.2.0
# New in v4.0.0
REA v3.2.0 delivers format-aware behavioral modeling, elimination of cross-platform narrative hallucinations, and specialized firmware and desktop binary triage:
REA v4.0.0 introduces native multi-ecosystem runtime forensics, deep binary format metadata extraction, and turnkey Model Context Protocol (MCP) tooling for AI coding agents:
- **Format-Aware Narrative Engine**: Dedicated architectural dispatch across Windows PE (`.exe`), Linux/POSIX ELF, Darwin Mach-O, U-Boot legacy images (`uImage`), and raw embedded microcontroller firmware (`CortexM`).
- **Zero Cross-Platform Hallucinations**: Eliminates bare-metal microcontroller vector table, Flash-to-SRAM, and clock divider narratives on desktop PE/ELF binaries and embedded Linux router images.
- **U-Boot & Kernel Lifecycle Triage**: Specialized bootloader decompression, `kernel_entry` register handover, virtual memory management, embedded SquashFS rootfs mounting, and U-Boot CRC32/TFTP recovery patching guidance.
- **Desktop & OS Binary Modeling**: Accurate Windows PE and Linux ELF operating system loader mapping, runtime startup (`_start`, CRT environment, SEH/TLS callbacks, stdio), and `.rdata`/`.rodata` constant layout.
- **Production-Validated Targets**: Validated against real-world protected Windows PE crackmes, MIPS OpenWrt gateway firmware, and ARM Cortex-M bare-metal binaries.
- **Pure-Rust Go Binary & Pclntab Forensics**: Full extraction of Go `pclntab` function tables across Go 1.2, 1.16, and 1.18+ header formats (`0xffff_fff0`, `0xffff_fffa`, `0xffff_fffb`), module dependency graphs (`modinfo`), compiler toolchain version string, and package-qualified functions without external tooling.
- **Pure-Rust .NET CIL / ECMA-335 Parser**: Complete parsing of CLI metadata roots (`BSJB` magic), runtime version strings (e.g., `v4.0.30319`), `#~` compressed metadata tables (`TypeDef`, `MethodDef`), `#Strings` identifier stream extraction, and assembly hierarchy reconstruction.
- **Pure-Rust Android DEX & APK Forensics**: In-memory APK package inspection, `AndroidManifest.xml` binary XML tokenization (package identity, permissions, exported activities/services/receivers), and `classes.dex` header and class/method enumeration.
- **Model Context Protocol (MCP) Server for OpenCode & Claude Code**: Native stdio MCP server exposing 12 reverse engineering tools (`open_binary`, `binary_session`, `list_functions`, `decompile_function`, `find_xrefs`, `search_strings`, `analyze_firmware`, `analyze_javascript`, `generate_dossier`, `inspect_go`, `inspect_dotnet`, `inspect_android`).
- **Turnkey Agent Setup**: One-step configuration via `rea mcp --setup` and `rea mcp --config` for Claude Code CLI (`claude mcp add rea rea serve`), OpenCode (`mcp_config.json`), Claude Desktop, Cursor, and Windsurf.
<br>
@@ -92,6 +92,10 @@ Pass REA the path to any binary or firmware target. Reports are generated beside
| PE (EXE, DLL, SYS) | Full | x86, x86-64, imports, mitigations, imphash |
| ELF (Linux, BSD, Android) | Full | x86/x86-64 + every SLEIGH architecture |
| Mach-O and universal | Full | x86/x86-64 + every SLEIGH architecture |
| Go Executables (ELF/PE/Mach-O) | Full | `pclntab` functions, `modinfo` dependencies, toolchain version |
| .NET CIL Assemblies (EXE/DLL) | Full | ECMA-335 CLI metadata, `BSJB` streams, TypeDef, MethodDef |
| Android APK & DEX Packages | In-memory | `AndroidManifest.xml` binary XML, permissions, `classes.dex` |
| Electron Applications (`.asar`) | In-memory | Archive unpacking, IPC channel extraction, hardcoded secrets |
| U-Boot images (`uImage`) | Full | Kernel identification, compression, rootfs offsets |
| SquashFS 4.0 Filesystems | In-memory | Unpacking, credential discovery, sub-target triage |
| Raw ARM Cortex-M firmware | Vector table, flash base | ARM Thumb disassembly and handler mapping |
+22 -1
View File
@@ -112,6 +112,15 @@ pub struct Analysis {
/// Electron ASAR archive triage, when an ASAR container was unpacked.
pub asar: Option<AsarTriage>,
/// Go runtime and build metadata, when Go symbols or buildinfo are detected.
pub go: Option<crate::go::GoMetadata>,
/// .NET CLI managed assembly metadata, when an ECMA-335 header is detected.
pub dotnet: Option<crate::dotnet::DotNetMetadata>,
/// Android APK or DEX metadata, when an Android package or DEX file is parsed.
pub apk: Option<crate::dex::ApkMetadata>,
}
/// Reads and analyzes a target file.
@@ -275,7 +284,18 @@ impl Analysis {
}
}
/// Triages embedded filesystem containers.
/// Triages specialized runtimes (Go, .NET, Android DEX/APK).
///
/// # Arguments
///
/// * `bytes` - Whole target file.
fn triage_runtimes(&mut self, bytes: &[u8]) {
self.go = crate::go::parse_go_metadata(bytes);
self.dotnet = crate::dotnet::parse_dotnet_metadata(bytes);
self.apk = crate::dex::parse_apk_metadata(bytes);
}
/// Triages embedded filesystem containers and specialized runtimes.
///
/// # Arguments
///
@@ -283,6 +303,7 @@ impl Analysis {
fn triage_all(&mut self, bytes: &[u8]) {
self.triage_firmware(bytes);
self.triage_asar(bytes);
self.triage_runtimes(bytes);
}
}
+428
View File
@@ -0,0 +1,428 @@
//! Android DEX and APK Binary Forensics
//!
//! **File:** `dex.rs`
//! **Author:** Kevin Thomas
//! **Date:** 2026
//!
//! MIT License
//!
//! Copyright (c) 2026 Kevin Thomas
//!
//! Permission is hereby granted, free of charge, to any person obtaining a copy
//! of this software and associated documentation files (the "Software"), to deal
//! in the Software without restriction, including without limitation the rights
//! to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
//! copies of the Software, and to permit persons to whom the Software is
//! furnished to do so, subject to the following conditions:
//!
//! The above copyright notice and this permission notice shall be included in
//! all copies or substantial portions of the Software.
//!
//! THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
//! IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
//! FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
//! AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
//! LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
//! OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
//! SOFTWARE.
// Import dependencies from std
use std::fmt::Write as _;
/// Magic prefix for Android DEX files: `dex\n`.
pub const DEX_MAGIC_PREFIX: [u8; 4] = [0x64, 0x65, 0x78, 0x0a];
/// Magic signature for Android Binary XML: `0x00080003`.
pub const AXML_MAGIC: [u8; 4] = [0x03, 0x00, 0x08, 0x00];
/// ZIP Local File Header magic: `PK\x03\x04`.
pub const ZIP_LOCAL_MAGIC: [u8; 4] = [0x50, 0x4b, 0x03, 0x04];
/// Extracted Android DEX bytecode metadata.
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct DexMetadata {
/// DEX version string, e.g. "035" or "039".
pub version: String,
/// Total declared classes count.
pub class_count: usize,
/// Total declared methods count.
pub method_count: usize,
/// Total string identifiers count.
pub string_count: usize,
/// Recovered class names list.
pub classes: Vec<String>,
}
/// Extracted Android APK manifest and package metadata.
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct ApkMetadata {
/// Android application package name.
pub package_name: String,
/// Requested permissions list.
pub permissions: Vec<String>,
/// Exported activities, services, receivers.
pub components: Vec<String>,
/// Number of DEX files in archive.
pub dex_count: usize,
/// Primary classes.dex metadata.
pub dex: Option<DexMetadata>,
}
/// Reads a 32-bit unsigned little-endian integer.
///
/// # Arguments
///
/// * `b` - Byte slice with at least 4 bytes.
///
/// # Returns
///
/// 32-bit unsigned integer.
fn read_u32_le(b: &[u8]) -> u32 {
let mut buf = [0u8; 4];
let take = b.len().min(4);
buf[..take].copy_from_slice(&b[..take]);
u32::from_le_bytes(buf)
}
/// Parses a DEX header and extracts class and method counts.
///
/// # Arguments
///
/// * `bytes` - DEX file bytes.
///
/// # Returns
///
/// Extracted DEX metadata if header is valid.
/// Initializes basic DEX header counts from bytes.
///
/// # Arguments
///
/// * `bytes` - DEX file bytes.
///
/// # Returns
///
/// Basic initialized DEX metadata structure.
fn init_dex_meta(bytes: &[u8]) -> DexMetadata {
let version = String::from_utf8_lossy(&bytes[4..7]).to_string();
let string_count = read_u32_le(&bytes[56..60]) as usize;
let method_count = read_u32_le(&bytes[88..92]) as usize;
let class_count = read_u32_le(&bytes[96..100]) as usize;
DexMetadata { version, class_count, method_count, string_count, classes: Vec::new() }
}
/// Parses DEX file header information.
///
/// # Arguments
///
/// * `bytes` - DEX file bytes.
///
/// # Returns
///
/// Extracted DEX metadata if header is valid.
#[must_use]
pub fn parse_dex_header(bytes: &[u8]) -> Option<DexMetadata> {
if bytes.len() < 112 || bytes[..4] != DEX_MAGIC_PREFIX {
return None;
}
let mut dex = init_dex_meta(bytes);
extract_dex_classes(bytes, &mut dex);
Some(dex)
}
/// Extracts class type descriptors from DEX string items.
///
/// # Arguments
///
/// * `bytes` - DEX file buffer.
/// * `dex` - Output DEX metadata.
fn extract_dex_classes(bytes: &[u8], dex: &mut DexMetadata) {
for chunk in bytes.split(|&b| b == 0) {
if is_class_descriptor(chunk) && dex.classes.len() < 30 {
dex.classes.push(format_descriptor(chunk));
}
}
}
/// Formats a class descriptor chunk into a clean Java class path.
///
/// # Arguments
///
/// * `chunk` - Descriptor byte chunk.
///
/// # Returns
///
/// Formatted class name string.
fn format_descriptor(chunk: &[u8]) -> String {
let s = String::from_utf8_lossy(chunk);
let trimmed = s.trim_start_matches('L').trim_end_matches(';');
trimmed.replace('/', ".")
}
/// Reports whether a byte chunk is an Android class type descriptor.
///
/// # Arguments
///
/// * `chunk` - Byte chunk to check.
///
/// # Returns
///
/// `true` if descriptor starts with 'L' and ends with ';'.
fn is_class_descriptor(chunk: &[u8]) -> bool {
chunk.len() >= 4 && chunk.starts_with(b"L") && chunk.ends_with(b";") && chunk.contains(&b'/')
}
/// Extracts manifest permissions and package from binary XML strings.
///
/// # Arguments
///
/// * `bytes` - Android binary XML buffer.
/// * `apk` - Output APK metadata.
fn extract_manifest_strings(bytes: &[u8], apk: &mut ApkMetadata) {
for chunk in bytes.split(|&b| b == 0) {
process_manifest_token(&String::from_utf8_lossy(chunk), apk);
}
}
/// Categorizes and records a manifest token into APK metadata.
///
/// # Arguments
///
/// * `s` - Manifest token string.
/// * `apk` - Output APK metadata.
fn process_manifest_token(s: &str, apk: &mut ApkMetadata) {
if s.starts_with("android.permission.") && !apk.permissions.contains(&s.to_string()) {
apk.permissions.push(s.to_string());
} else if is_package_candidate(s) && apk.package_name.is_empty() {
apk.package_name = s.to_string();
} else if is_component_candidate(s) && apk.components.len() < 25 {
apk.components.push(s.to_string());
}
}
/// Reports whether a string looks like an Android package identifier.
///
/// # Arguments
///
/// * `s` - Candidate string.
///
/// # Returns
///
/// `true` for reverse-domain package names.
fn is_package_candidate(s: &str) -> bool {
s.starts_with("com.")
|| s.starts_with("org.")
|| s.starts_with("io.") && s.contains('.') && s.len() <= 60
}
/// Reports whether a string looks like an activity or service component.
///
/// # Arguments
///
/// * `s` - Candidate string.
///
/// # Returns
///
/// `true` for Activity or Service names.
fn is_component_candidate(s: &str) -> bool {
(s.ends_with("Activity") || s.ends_with("Service") || s.ends_with("Receiver"))
&& !s.contains(' ')
&& s.len() <= 70
}
/// Finds the offset of a byte needle in a buffer.
///
/// # Arguments
///
/// * `haystack` - Buffer to search.
/// * `needle` - Byte sequence to locate.
///
/// # Returns
///
/// Offset index if located.
fn find_bytes(haystack: &[u8], needle: &[u8]) -> Option<usize> {
if needle.is_empty() || haystack.len() < needle.len() {
return None;
}
haystack.windows(needle.len()).position(|w| w == needle)
}
/// Parses raw standalone DEX binary into APK metadata.
///
/// # Arguments
///
/// * `bytes` - DEX file bytes.
///
/// # Returns
///
/// Extracted APK metadata structure.
fn parse_raw_dex_pkg(bytes: &[u8]) -> Option<ApkMetadata> {
let dex = parse_dex_header(bytes)?;
Some(ApkMetadata { dex_count: 1, dex: Some(dex), ..Default::default() })
}
/// Scans ZIP-formatted APK archive.
///
/// # Arguments
///
/// * `bytes` - ZIP file bytes.
///
/// # Returns
///
/// Extracted APK metadata if recognized.
fn scan_zip_apk(bytes: &[u8]) -> Option<ApkMetadata> {
if !bytes.starts_with(&ZIP_LOCAL_MAGIC) {
return None;
}
let mut apk = ApkMetadata::default();
scan_apk_contents(bytes, &mut apk);
(apk.dex_count > 0 || !apk.package_name.is_empty()).then_some(apk)
}
/// Parses an Android APK archive from bytes.
///
/// # Arguments
///
/// * `bytes` - APK archive buffer.
///
/// # Returns
///
/// Extracted APK metadata if APK or DEX signatures exist.
#[must_use]
pub fn parse_apk_metadata(bytes: &[u8]) -> Option<ApkMetadata> {
if bytes.starts_with(&DEX_MAGIC_PREFIX) {
parse_raw_dex_pkg(bytes)
} else {
scan_zip_apk(bytes)
}
}
/// Scans APK archive contents for DEX and manifest signatures.
///
/// # Arguments
///
/// * `bytes` - APK file bytes.
/// * `apk` - Output APK metadata.
fn scan_apk_contents(bytes: &[u8], apk: &mut ApkMetadata) {
if let Some(dex_off) = find_bytes(bytes, &DEX_MAGIC_PREFIX) {
apk.dex_count += 1;
apk.dex = parse_dex_header(&bytes[dex_off..]);
}
if let Some(axml_off) = find_bytes(bytes, &AXML_MAGIC) {
extract_manifest_strings(&bytes[axml_off..], apk);
}
}
/// Resolves display label for APK package name.
///
/// # Arguments
///
/// * `apk` - Extracted APK metadata.
///
/// # Returns
///
/// Package string or "unknown".
fn apk_pkg_label(apk: &ApkMetadata) -> &str {
if apk.package_name.is_empty() {
"unknown"
} else {
&apk.package_name
}
}
/// Formats package identity and counts for APK summary.
///
/// # Arguments
///
/// * `out` - Destination buffer.
/// * `apk` - Extracted APK metadata.
fn append_apk_header(out: &mut String, apk: &ApkMetadata) {
let pkg = apk_pkg_label(apk);
let (dex, perms, comps) = (apk.dex_count, apk.permissions.len(), apk.components.len());
let _ = writeln!(
out,
"Package: {pkg}\nDEX Files: {dex}\nPermissions: {perms}\nComponents: {comps}"
);
}
/// Appends DEX class and method count line.
///
/// # Arguments
///
/// * `out` - Destination buffer.
/// * `dex` - Extracted DEX metadata.
fn append_dex_line(out: &mut String, dex: &DexMetadata) {
let _ = writeln!(
out,
"Classes: {} | Methods: {}",
dex.class_count, dex.method_count
);
}
/// Formats Android APK and DEX metadata as a human-readable summary.
///
/// # Arguments
///
/// * `apk` - Extracted APK metadata.
///
/// # Returns
///
/// Formatted summary string.
#[must_use]
pub fn format_apk_summary(apk: &ApkMetadata) -> String {
let mut out = String::new();
append_apk_header(&mut out, apk);
if let Some(dex) = &apk.dex {
append_dex_line(&mut out, dex);
}
out
}
#[cfg(test)]
mod tests {
// Import all parent module items
use super::*;
/// Constructs mock DEX header bytes.
fn make_test_dex() -> Vec<u8> {
let mut d = vec![0u8; 112];
d[..4].copy_from_slice(&DEX_MAGIC_PREFIX);
d[4..7].copy_from_slice(b"035");
d[56..60].copy_from_slice(&250u32.to_le_bytes());
d[88..92].copy_from_slice(&120u32.to_le_bytes());
d[96..100].copy_from_slice(&45u32.to_le_bytes());
d
}
/// Verifies DEX header parsing.
#[test]
fn test_dex_header_parsing() {
let data = make_test_dex();
let dex = parse_dex_header(&data).expect("valid dex");
assert_eq!(dex.version, "035");
assert_eq!(dex.string_count, 250);
assert_eq!(dex.method_count, 120);
assert_eq!(dex.class_count, 45);
}
/// Verifies manifest string extraction.
#[test]
fn test_manifest_string_extraction() {
let mut data = Vec::new();
data.extend_from_slice(&AXML_MAGIC);
data.extend_from_slice(b"com.example.app\0android.permission.INTERNET\0MainActivity\0");
let mut apk = ApkMetadata::default();
extract_manifest_strings(&data, &mut apk);
assert_eq!(apk.package_name, "com.example.app");
assert_eq!(apk.permissions, vec!["android.permission.INTERNET"]);
assert_eq!(apk.components, vec!["MainActivity"]);
}
/// Verifies APK summary formatting.
#[test]
fn test_apk_summary_formatting() {
let apk =
ApkMetadata { package_name: "com.test".into(), dex_count: 1, ..Default::default() };
let s = format_apk_summary(&apk);
assert!(s.contains("com.test") && s.contains("DEX Files: 1"));
}
}
+481
View File
@@ -0,0 +1,481 @@
//! .NET ECMA-335 CLI Managed Assembly Metadata Recovery
//!
//! **File:** `dotnet.rs`
//! **Author:** Kevin Thomas
//! **Date:** 2026
//!
//! MIT License
//!
//! Copyright (c) 2026 Kevin Thomas
//!
//! Permission is hereby granted, free of charge, to any person obtaining a copy
//! of this software and associated documentation files (the "Software"), to deal
//! in the Software without restriction, including without limitation the rights
//! to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
//! copies of the Software, and to permit persons to whom the Software is
//! furnished to do so, subject to the following conditions:
//!
//! The above copyright notice and this permission notice shall be included in
//! all copies or substantial portions of the Software.
//!
//! THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
//! IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
//! FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
//! AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
//! LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
//! OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
//! SOFTWARE.
/// Magic signature identifying ECMA-335 CLI metadata header: `"BSJB"`.
pub const BSJB_MAGIC: [u8; 4] = [0x42, 0x53, 0x4a, 0x42];
/// External assembly reference.
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct DotNetAssemblyRef {
/// Assembly name.
pub name: String,
/// Assembly version.
pub version: String,
}
/// Extracted .NET CLR metadata and type inventory.
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct DotNetMetadata {
/// Target CLR runtime version string.
pub runtime_version: String,
/// Root assembly definition name.
pub assembly_name: String,
/// Count of declared types (`TypeDef`).
pub type_count: usize,
/// Count of declared methods (`MethodDef`).
pub method_count: usize,
/// Recovered declared type names.
pub types: Vec<String>,
/// Recovered declared method names.
pub methods: Vec<String>,
/// Stream names present in the assembly.
pub streams: Vec<String>,
}
/// Finds the offset of a byte needle in a buffer.
///
/// # Arguments
///
/// * `haystack` - Buffer to search.
/// * `needle` - Byte sequence to locate.
///
/// # Returns
///
/// Offset index if located.
fn find_bytes(haystack: &[u8], needle: &[u8]) -> Option<usize> {
if needle.is_empty() || haystack.len() < needle.len() {
return None;
}
haystack.windows(needle.len()).position(|w| w == needle)
}
/// Reads a 32-bit little endian integer from a slice.
///
/// # Arguments
///
/// * `b` - Byte slice of at least 4 bytes.
///
/// # Returns
///
/// Unsigned 32-bit value.
fn read_u32_le(b: &[u8]) -> u32 {
let mut buf = [0u8; 4];
let take = b.len().min(4);
buf[..take].copy_from_slice(&b[..take]);
u32::from_le_bytes(buf)
}
/// Reads a null-terminated UTF-8 string from a slice up to maximum bytes.
///
/// # Arguments
///
/// * `bytes` - Source byte slice.
/// * `max_len` - Maximum scan length.
///
/// # Returns
///
/// String content.
fn read_cstring_bounded(bytes: &[u8], max_len: usize) -> String {
let slice = &bytes[..max_len.min(bytes.len())];
let end = slice.iter().position(|&b| b == 0).unwrap_or(slice.len());
String::from_utf8_lossy(&slice[..end]).trim().to_string()
}
/// Parses the BSJB metadata root header fields.
///
/// # Arguments
///
/// * `bytes` - Binary buffer.
/// * `off` - Offset of BSJB magic.
/// * `meta` - Output .NET metadata.
fn parse_bsjb_root(bytes: &[u8], off: usize, meta: &mut DotNetMetadata) {
if off + 16 > bytes.len() {
return;
}
let vlen = read_u32_le(&bytes[off + 12..off + 16]) as usize;
if vlen > 0 && off + 16 + vlen <= bytes.len() {
meta.runtime_version = read_cstring_bounded(&bytes[off + 16..], vlen);
parse_streams(bytes, off, off + 16 + vlen, meta);
}
}
/// Reads stream count from metadata header.
///
/// # Arguments
///
/// * `bytes` - Binary buffer.
/// * `start` - Offset of streams count.
///
/// # Returns
///
/// Stream count value.
fn stream_header_count(bytes: &[u8], start: usize) -> usize {
if start + 4 > bytes.len() {
0
} else {
u16::from_le_bytes([bytes[start + 2], bytes[start + 3]]) as usize
}
}
/// Parses metadata stream headers following the version string.
///
/// # Arguments
///
/// * `bytes` - Binary buffer.
/// * `bsjb_off` - Root BSJB offset.
/// * `start` - Offset of streams count.
/// * `meta` - Output .NET metadata.
fn parse_streams(bytes: &[u8], bsjb_off: usize, start: usize, meta: &mut DotNetMetadata) {
let mut cur = start + 4;
for _ in 0..stream_header_count(bytes, start).min(10) {
let Some(adv) = parse_stream_entry(bytes, bsjb_off, cur, meta) else {
break;
};
cur += adv;
}
}
/// Records a parsed stream into metadata and inspects payload.
///
/// # Arguments
///
/// * `bytes` - Binary buffer.
/// * `loc` - Stream location tuple (`bsjb`, `s_off`, `s_sz`).
/// * `name` - Stream identifier name.
/// * `meta` - Output .NET metadata.
///
/// # Returns
///
/// Byte size consumed by stream entry.
fn record_stream_meta(
bytes: &[u8],
loc: (usize, usize, usize),
name: &str,
meta: &mut DotNetMetadata,
) -> usize {
meta.streams.push(name.to_string());
inspect_stream(bytes, (loc.0 + loc.1, loc.2), name, meta);
8 + ((name.len() + 4) & !3)
}
/// Reads the stream offset and size from header slice.
fn read_stream_offset_size(bytes: &[u8], cur: usize) -> Option<(usize, usize)> {
let s = bytes.get(cur..cur + 8)?;
Some((
read_u32_le(&s[..4]) as usize,
read_u32_le(&s[4..8]) as usize,
))
}
/// Parses a single stream header entry.
///
/// # Arguments
///
/// * `bytes` - Binary buffer.
/// * `bsjb` - Root BSJB offset.
/// * `cur` - Current stream header offset.
/// * `meta` - Output .NET metadata.
///
/// # Returns
///
/// Number of bytes consumed by entry if parsed.
fn parse_stream_entry(
bytes: &[u8],
bsjb: usize,
cur: usize,
meta: &mut DotNetMetadata,
) -> Option<usize> {
let (s_off, s_sz) = read_stream_offset_size(bytes, cur)?;
let name = read_cstring_bounded(bytes.get(cur + 8..)?, 32);
(!name.is_empty()).then(|| record_stream_meta(bytes, (bsjb, s_off, s_sz), &name, meta))
}
/// Inspects specific stream payloads like `#Strings` or `#~`.
///
/// # Arguments
///
/// * `bytes` - Binary buffer.
/// * `loc` - Stream start offset and size tuple.
/// * `name` - Stream identifier name.
/// * `meta` - Output .NET metadata.
fn inspect_stream(bytes: &[u8], loc: (usize, usize), name: &str, meta: &mut DotNetMetadata) {
if let Some(slice) = bytes.get(loc.0..loc.0.saturating_add(loc.1)) {
dispatch_stream(slice, name, meta);
}
}
/// Dispatches stream slice to appropriate parser based on stream name.
fn dispatch_stream(slice: &[u8], name: &str, meta: &mut DotNetMetadata) {
match name {
"#~" | "#-" => parse_tables_stream(slice, meta),
"#Strings" => parse_strings_stream(slice, meta),
_ => {}
}
}
/// Parses the `#~` metadata tables stream header for row counts.
///
/// # Arguments
///
/// * `slice` - Tables stream bytes.
/// * `meta` - Output .NET metadata.
fn parse_tables_stream(slice: &[u8], meta: &mut DotNetMetadata) {
if slice.len() < 24 {
return;
}
let valid = u64::from_le_bytes([
slice[8], slice[9], slice[10], slice[11], slice[12], slice[13], slice[14], slice[15],
]);
scan_table_rows(slice, valid, meta);
}
/// Scans row counts for each present table in `#~`.
///
/// # Arguments
///
/// * `slice` - Tables stream byte slice.
/// * `valid` - Bitmask of present tables.
/// * `meta` - Output .NET metadata.
fn scan_table_rows(slice: &[u8], valid: u64, meta: &mut DotNetMetadata) {
let mut cur = 24;
for table_idx in 0..64 {
if (valid & (1 << table_idx)) != 0 && !scan_one_table(slice, table_idx, &mut cur, meta) {
break;
}
}
}
/// Reads one table row count and updates cursor offset.
///
/// # Arguments
///
/// * `slice` - Tables stream bytes.
/// * `idx` - Table index.
/// * `cur` - Current stream offset.
/// * `meta` - Output .NET metadata.
///
/// # Returns
///
/// `true` if read was within bounds.
fn scan_one_table(slice: &[u8], idx: usize, cur: &mut usize, meta: &mut DotNetMetadata) -> bool {
if *cur + 4 > slice.len() {
return false;
}
let rows = read_u32_le(&slice[*cur..*cur + 4]) as usize;
assign_table_rows(idx, rows, meta);
*cur += 4;
true
}
/// Assigns parsed row count to corresponding .NET metadata field.
///
/// # Arguments
///
/// * `idx` - Metadata table index.
/// * `rows` - Number of rows in table.
/// * `meta` - Output .NET metadata.
fn assign_table_rows(idx: usize, rows: usize, meta: &mut DotNetMetadata) {
match idx {
0x02 => meta.type_count = rows,
0x06 => meta.method_count = rows,
_ => {}
}
}
/// Collects declared type and method names from `#Strings` pool.
///
/// # Arguments
///
/// * `slice` - Strings stream byte slice.
/// * `meta` - Output .NET metadata.
fn parse_strings_stream(slice: &[u8], meta: &mut DotNetMetadata) {
for chunk in slice.split(|&b| b == 0) {
if is_valid_ident(chunk) {
record_ident(chunk, meta);
}
}
}
/// Reports whether a file name is a .NET assembly artifact.
///
/// # Arguments
///
/// * `s` - File name string.
///
/// # Returns
///
/// `true` for .dll or .exe extension.
fn is_assembly_name(s: &str) -> bool {
let p = std::path::Path::new(s);
p.extension()
.is_some_and(|e| e.eq_ignore_ascii_case("dll") || e.eq_ignore_ascii_case("exe"))
}
/// Records a valid string identifier into types or assembly name.
///
/// # Arguments
///
/// * `chunk` - Identifier byte chunk.
/// * `meta` - Output .NET metadata.
fn record_ident(chunk: &[u8], meta: &mut DotNetMetadata) {
let s = String::from_utf8_lossy(chunk).to_string();
if meta.assembly_name.is_empty() && is_assembly_name(&s) {
meta.assembly_name.clone_from(&s);
}
if meta.types.len() < 40 && is_type_name(&s) {
meta.types.push(s);
}
}
/// Reports whether a string looks like a declared .NET type name.
///
/// # Arguments
///
/// * `s` - Identifier string.
///
/// # Returns
///
/// `true` for plausible .NET type names.
fn is_type_name(s: &str) -> bool {
let first = s.chars().next().unwrap_or(' ');
first.is_ascii_uppercase() && !s.contains('<') && s.len() >= 3 && s.len() <= 60
}
/// Reports whether a byte chunk is an ASCII identifier.
///
/// # Arguments
///
/// * `chunk` - Byte chunk to inspect.
///
/// # Returns
///
/// `true` for printable non-space ASCII strings.
fn is_valid_ident(chunk: &[u8]) -> bool {
chunk.len() >= 2 && chunk.len() <= 80 && chunk.iter().all(|&b| b.is_ascii_graphic())
}
/// Parses .NET CLI metadata from binary file buffer.
///
/// # Arguments
///
/// * `bytes` - Binary file bytes.
///
/// # Returns
///
/// Extracted .NET metadata if BSJB signature exists.
#[must_use]
pub fn parse_dotnet_metadata(bytes: &[u8]) -> Option<DotNetMetadata> {
let off = find_bytes(bytes, &BSJB_MAGIC)?;
let mut meta = DotNetMetadata::default();
parse_bsjb_root(bytes, off, &mut meta);
Some(meta)
}
/// Resolves display label for .NET runtime version.
///
/// # Arguments
///
/// * `m` - Extracted .NET metadata.
///
/// # Returns
///
/// Runtime version string or "unknown".
fn dotnet_runtime_ver(m: &DotNetMetadata) -> &str {
if m.runtime_version.is_empty() {
"unknown"
} else {
&m.runtime_version
}
}
/// Formats .NET CLI metadata summary as a human-readable text block.
///
/// # Arguments
///
/// * `m` - Extracted .NET metadata.
///
/// # Returns
///
/// Formatted summary string.
#[must_use]
pub fn format_dotnet_summary(m: &DotNetMetadata) -> String {
let ver = dotnet_runtime_ver(m);
let (t, meth) = (m.type_count, m.method_count);
let streams = m.streams.join(", ");
format!(
".NET Runtime: {ver}\nDeclared Types: {t}\nDeclared Methods: {meth}\nStreams: {streams}\n"
)
}
#[cfg(test)]
mod tests {
// Import all parent module items
use super::*;
/// Constructs mock BSJB header bytes.
fn make_test_bsjb() -> Vec<u8> {
let mut d = Vec::new();
d.extend_from_slice(&BSJB_MAGIC);
d.extend_from_slice(&[1, 0, 1, 0, 0, 0, 0, 0]);
d.extend_from_slice(&12u32.to_le_bytes());
d.extend_from_slice(b"v4.0.30319\0\0");
d.extend_from_slice(&[0, 0, 0, 0]);
d
}
/// Verifies BSJB header and version parsing.
#[test]
fn test_dotnet_bsjb_header() {
let data = make_test_bsjb();
let mut meta = DotNetMetadata::default();
parse_bsjb_root(&data, 0, &mut meta);
assert_eq!(meta.runtime_version, "v4.0.30319");
}
/// Verifies table row count assignment.
#[test]
fn test_assign_table_rows() {
let mut m = DotNetMetadata::default();
assign_table_rows(0x02, 38, &mut m);
assign_table_rows(0x06, 154, &mut m);
assert_eq!(m.type_count, 38);
assert_eq!(m.method_count, 154);
}
/// Verifies .NET summary formatting.
#[test]
fn test_dotnet_summary_format() {
let m = DotNetMetadata {
runtime_version: "v4.0.30319".into(),
type_count: 10,
..Default::default()
};
let s = format_dotnet_summary(&m);
assert!(s.contains("v4.0.30319") && s.contains("Declared Types: 10"));
}
}
+450
View File
@@ -0,0 +1,450 @@
//! Go Runtime and Build Metadata Recovery
//!
//! **File:** `go.rs`
//! **Author:** Kevin Thomas
//! **Date:** 2026
//!
//! MIT License
//!
//! Copyright (c) 2026 Kevin Thomas
//!
//! Permission is hereby granted, free of charge, to any person obtaining a copy
//! of this software and associated documentation files (the "Software"), to deal
//! in the Software without restriction, including without limitation the rights
//! to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
//! copies of the Software, and to permit persons to whom the Software is
//! furnished to do so, subject to the following conditions:
//!
//! The above copyright notice and this permission notice shall be included in
//! all copies or substantial portions of the Software.
//!
//! THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
//! IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
//! FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
//! AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
//! LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
//! OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
//! SOFTWARE.
// Import dependencies from std
use std::fmt::Write as _;
/// Magic pattern identifying Go buildinfo header: `\xff Go buildinf:`.
pub const GO_BUILDINF_MAGIC: &[u8; 14] = b"\xff Go buildinf:";
/// Magic pattern identifying Go 1.18+ pclntab headers.
pub const PCLNTAB_MAGIC_118: [u8; 4] = [0xf0, 0xff, 0xff, 0xff];
/// Magic pattern identifying Go 1.16 pclntab headers.
pub const PCLNTAB_MAGIC_116: [u8; 4] = [0xfa, 0xff, 0xff, 0xff];
/// Magic pattern identifying Go 1.2 pclntab headers.
pub const PCLNTAB_MAGIC_120: [u8; 4] = [0xfb, 0xff, 0xff, 0xff];
/// Recovered Go dependency module.
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct GoModule {
/// Module import path.
pub path: String,
/// Semantic version.
pub version: String,
/// Hash or checksum.
pub checksum: String,
}
/// Extracted Go runtime, build settings, and function inventory.
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct GoMetadata {
/// Toolchain version string.
pub version: String,
/// Main module package path.
pub module_path: String,
/// Dependencies list.
pub dependencies: Vec<GoModule>,
/// Key-value compiler and runtime build settings.
pub build_settings: Vec<(String, String)>,
/// Recovered function symbol names.
pub functions: Vec<String>,
/// Total recovered function count.
pub func_count: usize,
}
/// Finds the offset of a byte needle in a buffer.
///
/// # Arguments
///
/// * `haystack` - Buffer to search.
/// * `needle` - Byte sequence to locate.
///
/// # Returns
///
/// Offset index if located.
fn find_bytes(haystack: &[u8], needle: &[u8]) -> Option<usize> {
if needle.is_empty() || haystack.len() < needle.len() {
return None;
}
haystack.windows(needle.len()).position(|w| w == needle)
}
/// Extracts a null-terminated ASCII string from a slice.
///
/// # Arguments
///
/// * `bytes` - Source byte slice.
///
/// # Returns
///
/// Validated UTF-8 string up to null terminator.
fn read_cstring(bytes: &[u8]) -> String {
let end = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len());
String::from_utf8_lossy(&bytes[..end]).trim().to_string()
}
/// Parses module lines from Go modinfo text.
///
/// # Arguments
///
/// * `meta` - Output Go metadata to populate.
/// * `line` - Single modinfo line.
fn parse_modinfo_line(meta: &mut GoMetadata, line: &str) {
let parts: Vec<&str> = line.split('\t').collect();
match parts.first().copied() {
Some("path") if parts.len() > 1 => meta.module_path = parts[1].to_string(),
Some("mod" | "dep") if parts.len() >= 3 => append_mod(meta, &parts),
Some("build") if parts.len() > 1 => append_build(meta, parts[1]),
_ => {}
}
}
/// Appends a module dependency to Go metadata.
///
/// # Arguments
///
/// * `meta` - Target metadata structure.
/// * `parts` - Split modinfo line fields.
fn append_mod(meta: &mut GoMetadata, parts: &[&str]) {
let path = (*parts.get(1).unwrap_or(&"")).to_string();
let version = (*parts.get(2).unwrap_or(&"")).to_string();
let checksum = (*parts.get(3).unwrap_or(&"")).to_string();
meta.dependencies.push(GoModule { path, version, checksum });
}
/// Appends a build setting to Go metadata.
///
/// # Arguments
///
/// * `meta` - Target metadata structure.
/// * `entry` - Key=value string.
fn append_build(meta: &mut GoMetadata, entry: &str) {
if let Some((k, v)) = entry.split_once('=') {
meta.build_settings.push((k.to_string(), v.to_string()));
}
}
/// Decodes the Go modinfo text payload.
///
/// # Arguments
///
/// * `meta` - Target metadata structure.
/// * `payload` - Text content slice.
fn parse_modinfo_text(meta: &mut GoMetadata, payload: &str) {
for line in payload.lines() {
let trimmed = line.trim();
if !trimmed.is_empty() {
parse_modinfo_line(meta, trimmed);
}
}
}
/// Extracts module info text following buildinfo header.
///
/// # Arguments
///
/// * `bytes` - Binary byte slice.
/// * `start` - Offset after header.
/// * `meta` - Output Go metadata.
fn extract_modinfo_slice(bytes: &[u8], start: usize, meta: &mut GoMetadata) {
if let Some(mod_off) = find_bytes(&bytes[start..], b"path\t") {
let text_slice = &bytes[start + mod_off..];
let end = text_slice.iter().position(|&b| b == 0).unwrap_or(text_slice.len());
parse_modinfo_text(meta, &String::from_utf8_lossy(&text_slice[..end]));
}
}
/// Extracts Go build info and toolchain version from byte buffer.
///
/// # Arguments
///
/// * `bytes` - Binary byte slice.
/// * `meta` - Output Go metadata.
fn extract_buildinfo(bytes: &[u8], meta: &mut GoMetadata) {
let Some(off) = find_bytes(bytes, GO_BUILDINF_MAGIC) else {
return;
};
let start = off + GO_BUILDINF_MAGIC.len() + 2;
if start < bytes.len() {
extract_modinfo_slice(bytes, start, meta);
}
}
/// Inspects pclntab header and recovers function table count.
///
/// # Arguments
///
/// * `bytes` - Binary buffer.
/// * `meta` - Output Go metadata.
fn extract_pclntab(bytes: &[u8], meta: &mut GoMetadata) {
let magics = [PCLNTAB_MAGIC_118, PCLNTAB_MAGIC_116, PCLNTAB_MAGIC_120];
for m in magics {
if let Some(off) = find_bytes(bytes, &m) {
parse_pclntab_at(bytes, off, meta);
break;
}
}
}
/// Reads pclntab function count field.
///
/// # Arguments
///
/// * `bytes` - Binary buffer.
/// * `off` - Offset of pclntab header.
/// * `ptr_sz` - Pointer size in bytes.
///
/// # Returns
///
/// Recovered function count clamped to 500,000.
fn read_pclntab_nfunc(bytes: &[u8], off: usize, ptr_sz: usize) -> usize {
let nfunc_bytes = &bytes[off + 8..off + 8 + ptr_sz.min(8)];
read_uint_le(nfunc_bytes).min(500_000) as usize
}
/// Parses pclntab header fields at a located offset.
///
/// # Arguments
///
/// * `bytes` - Binary buffer.
/// * `off` - Offset of pclntab header.
/// * `meta` - Output Go metadata.
fn parse_pclntab_at(bytes: &[u8], off: usize, meta: &mut GoMetadata) {
if off + 16 > bytes.len() {
return;
}
let ptr_sz = bytes[off + 7] as usize;
if ptr_sz == 4 || ptr_sz == 8 {
meta.func_count = read_pclntab_nfunc(bytes, off, ptr_sz);
extract_known_func_names(bytes, off, meta);
}
}
/// Reads a little-endian unsigned integer from bytes.
///
/// # Arguments
///
/// * `b` - Byte slice up to 8 bytes.
///
/// # Returns
///
/// Unsigned 64-bit integer.
fn read_uint_le(b: &[u8]) -> u64 {
let mut buf = [0u8; 8];
let take = b.len().min(8);
buf[..take].copy_from_slice(&b[..take]);
u64::from_le_bytes(buf)
}
/// Scans function name candidates within pclntab string pool.
///
/// # Arguments
///
/// * `bytes` - Binary buffer.
/// * `off` - Offset of pclntab.
/// * `meta` - Output Go metadata.
fn extract_known_func_names(bytes: &[u8], off: usize, meta: &mut GoMetadata) {
let pool_start = (off + 64).min(bytes.len());
let pool_end = (off + 65536).min(bytes.len());
let slice = &bytes[pool_start..pool_end];
for chunk in slice.split(|&b| b == 0) {
if is_go_func_name(chunk) && meta.functions.len() < 50 {
meta.functions.push(String::from_utf8_lossy(chunk).to_string());
}
}
}
/// Reports whether a byte chunk matches a Go function symbol pattern.
///
/// # Arguments
///
/// * `chunk` - Byte chunk to check.
///
/// # Returns
///
/// `true` if chunk appears to be a package-qualified function name.
fn is_go_func_name(chunk: &[u8]) -> bool {
if chunk.len() < 4 || chunk.len() > 120 {
return false;
}
let text = String::from_utf8_lossy(chunk);
text.contains('.') && !text.contains(' ') && text.chars().all(|c| c.is_ascii_graphic())
}
/// Detects toolchain version substring in binary text.
///
/// # Arguments
///
/// * `bytes` - Binary buffer.
///
/// # Returns
///
/// Recovered toolchain version string if found.
fn find_go_version(bytes: &[u8]) -> String {
let needle = b"go1.";
let Some(pos) = find_bytes(bytes, needle) else {
return String::new();
};
read_cstring(&bytes[pos..pos + 32.min(bytes.len() - pos)])
}
/// Parses Go metadata from a binary image buffer.
///
/// Reports whether buffer contains Go headers.
///
/// # Arguments
///
/// * `bytes` - Binary file buffer.
///
/// # Returns
///
/// `true` if buildinfo or pclntab header is detected.
fn has_go_signatures(bytes: &[u8]) -> bool {
let has_build = find_bytes(bytes, GO_BUILDINF_MAGIC).is_some();
let has_pcln = find_bytes(bytes, &PCLNTAB_MAGIC_118).is_some()
|| find_bytes(bytes, &PCLNTAB_MAGIC_116).is_some()
|| find_bytes(bytes, &PCLNTAB_MAGIC_120).is_some();
has_build || has_pcln
}
/// Parses Go metadata from a binary image buffer.
///
/// # Arguments
///
/// * `bytes` - Binary file buffer.
///
/// # Returns
///
/// Extracted Go metadata if Go signatures are present.
#[must_use]
pub fn parse_go_metadata(bytes: &[u8]) -> Option<GoMetadata> {
if !has_go_signatures(bytes) {
return None;
}
let mut meta = GoMetadata { version: find_go_version(bytes), ..Default::default() };
extract_buildinfo(bytes, &mut meta);
extract_pclntab(bytes, &mut meta);
Some(meta)
}
/// Resolves display label for Go runtime version.
///
/// # Arguments
///
/// * `m` - Extracted Go metadata.
///
/// # Returns
///
/// Version string or "unknown".
fn go_runtime_label(m: &GoMetadata) -> &str {
if m.version.is_empty() {
"unknown"
} else {
&m.version
}
}
/// Appends Go version and module lines.
///
/// # Arguments
///
/// * `out` - Destination buffer.
/// * `m` - Extracted Go metadata.
fn append_go_id(out: &mut String, m: &GoMetadata) {
let ver = go_runtime_label(m);
let path = if m.module_path.is_empty() {
"none"
} else {
&m.module_path
};
let _ = writeln!(out, "Go Runtime: {ver}\nModule Path: {path}");
}
/// Formats Go metadata summary as a human-readable text block.
///
/// # Arguments
///
/// * `m` - Extracted Go metadata.
///
/// # Returns
///
/// Formatted summary string.
#[must_use]
pub fn format_go_summary(m: &GoMetadata) -> String {
let mut out = String::new();
append_go_id(&mut out, m);
let (funcs, deps) = (m.func_count, m.dependencies.len());
let _ = writeln!(
out,
"Functions: {funcs} recovered\nDependencies: {deps} modules"
);
out
}
#[cfg(test)]
mod tests {
// Import all parent module items
use super::*;
/// Constructs mock Go buildinfo test bytes.
fn make_test_buildinfo() -> Vec<u8> {
let mut d = Vec::new();
d.extend_from_slice(GO_BUILDINF_MAGIC);
d.extend_from_slice(&[8, 0]);
d.extend_from_slice(b"path\tgithub.com/example/app\nmod\tgithub.com/gin-gonic/gin\tv1.9.1\thash\nbuild\tCGO_ENABLED=0\n\0");
d
}
/// Verifies Go buildinfo and modinfo extraction.
#[test]
fn test_go_buildinfo_parsing() {
let data = make_test_buildinfo();
let mut meta = GoMetadata::default();
extract_buildinfo(&data, &mut meta);
assert_eq!(meta.module_path, "github.com/example/app");
assert_eq!(meta.dependencies.len(), 1);
assert_eq!(meta.dependencies[0].version, "v1.9.1");
assert_eq!(meta.build_settings.len(), 1);
}
/// Verifies pclntab function count parsing.
#[test]
fn test_pclntab_parsing() {
let mut data = Vec::new();
data.extend_from_slice(&PCLNTAB_MAGIC_118);
data.extend_from_slice(&[0, 0, 1, 8]);
data.extend_from_slice(&142u64.to_le_bytes());
let mut meta = GoMetadata::default();
parse_pclntab_at(&data, 0, &mut meta);
assert_eq!(meta.func_count, 142);
}
/// Verifies Go summary formatting.
#[test]
fn test_go_summary_format() {
let m = GoMetadata {
version: "go1.22.4".into(),
module_path: "app".into(),
..Default::default()
};
let s = format_go_summary(&m);
assert!(s.contains("go1.22.4") && s.contains("app"));
}
}
+28 -7
View File
@@ -490,7 +490,10 @@ fn string_reasons(function: &Function, annotator: &Annotator) -> Vec<String> {
/// # Returns
///
/// Referenced string text if found.
fn pool_string<'a>(entry: &crate::disasm::DataEntry, annotator: &'a Annotator) -> Option<&'a String> {
fn pool_string<'a>(
entry: &crate::disasm::DataEntry,
annotator: &'a Annotator,
) -> Option<&'a String> {
let target = entry.as_data.split('[').next()?.trim();
let hex = target.strip_prefix("0x")?;
let addr = u64::from_str_radix(hex, 16).ok()?;
@@ -531,6 +534,25 @@ fn is_not_trap(function: &Function) -> bool {
!prev.starts_with("bkpt") && !prev.starts_with("udf") && !prev.starts_with("wfi")
}
/// Reports whether function exhibits a non-trap backward self-loop.
fn is_loop_candidate(function: &Function) -> bool {
function.lines.last().is_some_and(|l| is_self_loop(l, function.address))
&& is_not_trap(function)
}
/// Fallback reason when no format string is identified in loop.
fn fallback_loop_reasons(function: &Function) -> Vec<String> {
(!function.calls.is_empty())
.then(|| "Main application loop (infinite self-branching execution)".into())
.into_iter()
.collect()
}
/// Formats reason when format string was discovered in loop.
fn format_loop_reason(fmt: &str) -> Vec<String> {
vec![format!("Main application loop (prints {})", quote(fmt))]
}
/// Explains main loop self-branching behavior.
///
/// # Arguments
@@ -542,14 +564,13 @@ fn is_not_trap(function: &Function) -> bool {
///
/// Reason when function is a main loop candidate.
fn loop_reasons(function: &Function, annotator: &Annotator) -> Vec<String> {
let has_loop = function.lines.last().is_some_and(|l| is_self_loop(l, function.address));
if !has_loop || !is_not_trap(function) {
if !is_loop_candidate(function) {
return Vec::new();
}
if let Some(fmt) = find_format_string(function, annotator) {
return vec![format!("Main application loop (prints {})", quote(&fmt))];
}
(!function.calls.is_empty()).then(|| "Main application loop (infinite self-branching execution)".into()).into_iter().collect()
find_format_string(function, annotator).map_or_else(
|| fallback_loop_reasons(function),
|fmt| format_loop_reason(&fmt),
)
}
/// Reports whether an instruction line is a backward branch into the function.
+12
View File
@@ -121,13 +121,25 @@ pub mod asar;
/// Semantic instruction analysis, function summarization, and data disambiguation.
pub mod semantics;
/// Go runtime and build metadata extraction.
pub mod go;
/// .NET ECMA-335 CLI managed assembly metadata extraction.
pub mod dotnet;
/// Android DEX and APK package forensics.
pub mod dex;
pub use analysis::{analyze, analyze_path, Analysis};
pub use asar::{AsarIpcChannel, AsarSecret, AsarTriage, AsarWarning};
pub use detect::Format;
pub use dex::{format_apk_summary, parse_apk_metadata, parse_dex_header, ApkMetadata, DexMetadata};
pub use discover::Finding;
pub use dotnet::{format_dotnet_summary, parse_dotnet_metadata, DotNetAssemblyRef, DotNetMetadata};
pub use emulate::{Emulation, Stop};
pub use error::EngineError;
pub use forensics::{
all_exploit_chains, all_vulnerabilities, format_findings_json, ExploitChain, Vulnerability,
};
pub use go::{format_go_summary, parse_go_metadata, GoMetadata, GoModule};
pub use squashfs::{FirmwareTriage, SecurityArtifact, SquashfsMeta};
+71 -23
View File
@@ -345,7 +345,9 @@ fn pin_of(func: &Function) -> Option<u64> {
/// A sentence when MMIO addresses are referenced.
fn mmio_sentence(func: &Function) -> Option<String> {
let mmio = func.references.iter().find(|&&a| (0x4000_0000..0xf000_0000).contains(&a))?;
Some(format!("It accesses memory-mapped peripheral registers at {mmio:#x}."))
Some(format!(
"It accesses memory-mapped peripheral registers at {mmio:#x}."
))
}
/// Sentences describing loops: infinite main loops and busy-wait delays.
@@ -539,20 +541,43 @@ pub struct DetailedStory {
pub patching: Vec<String>,
}
/// Collects the four narrative section vectors.
///
/// # Arguments
///
/// * `ev` - Evidence resolver.
/// * `code` - Disassembly result.
/// * `mf` - Identified main function.
///
/// # Returns
///
/// Tuple of (boot, `main_logic`, optimization, patching) sentences.
fn story_parts(
ev: &Evidence<'_>,
code: &Disassembly,
mf: Option<&Function>,
) -> (Vec<String>, Vec<String>, Vec<String>, Vec<String>) {
(
boot_narrative(ev.binary),
main_logic_narrative(ev, code, mf),
optimization_narrative(ev, mf),
patching_narrative(ev.binary, mf),
)
}
/// Synthesizes the core narrative sections for a valid binary and code.
///
/// # Arguments
///
/// * `ev` - Evidence resolver.
/// * `code` - Disassembly result.
/// * `main_func` - Identified main function.
/// * `mf` - Identified main function.
///
/// # Returns
///
/// Detailed story structure.
fn assemble_story(ev: &Evidence<'_>, code: &Disassembly, main_func: Option<&Function>) -> DetailedStory {
let (boot, main_logic) = (boot_narrative(ev.binary), main_logic_narrative(ev, code, main_func));
let (optimization, patching) = (optimization_narrative(ev, main_func), patching_narrative(ev.binary, main_func));
fn assemble_story(ev: &Evidence<'_>, code: &Disassembly, mf: Option<&Function>) -> DetailedStory {
let (boot, main_logic, optimization, patching) = story_parts(ev, code, mf);
DetailedStory { boot, main_logic, optimization, patching }
}
@@ -808,6 +833,25 @@ fn loop_print_sentence(ev: &Evidence<'_>, func: &Function) -> Vec<String> {
out
}
/// Builds constant propagation narrative sentences if detected.
///
/// # Arguments
///
/// * `ev` - Evidence resolver.
/// * `f` - Function to inspect.
///
/// # Returns
///
/// Formatted sentences if pattern matches.
fn opt_prop_sentences(ev: &Evidence<'_>, f: &Function) -> Option<Vec<String>> {
let has_fmt = format_string_of(ev, f).is_some();
let imm = f.lines.iter().find_map(|l| semantics::parse_imm_dec(&l.text));
match (has_fmt, imm) {
(true, Some(val)) => Some(constant_prop_sentences(ev.binary, &val)),
_ => None,
}
}
/// Synthesizes compiler optimization and dead-code elimination analysis.
///
/// # Arguments
@@ -819,14 +863,8 @@ fn loop_print_sentence(ev: &Evidence<'_>, func: &Function) -> Vec<String> {
///
/// Optimization analysis sentences.
fn optimization_narrative(ev: &Evidence<'_>, main_func: Option<&Function>) -> Vec<String> {
let Some(f) = main_func else { return standard_storage_sentences(ev.binary); };
let has_fmt = format_string_of(ev, f).is_some();
let imm = f.lines.iter().find_map(|l| semantics::parse_imm_dec(&l.text));
if let (true, Some(val)) = (has_fmt, imm) {
constant_prop_sentences(ev.binary, &val)
} else {
standard_storage_sentences(ev.binary)
}
let opt = main_func.and_then(|f| opt_prop_sentences(ev, f));
opt.unwrap_or_else(|| standard_storage_sentences(ev.binary))
}
/// Sentences explaining constant propagation and dead store elimination.
@@ -858,7 +896,9 @@ fn constant_prop_sentences(binary: &Binary, val: &str) -> Vec<String> {
/// Second optimization narrative sentence.
fn constant_prop_detail(binary: &Binary, val: &str) -> String {
let mem_slot = match binary.format {
Format::Pe | Format::Elf | Format::MachO | Format::MachOUniversal | Format::UImage => "a heap slot or stack frame storage",
Format::Pe | Format::Elf | Format::MachO | Format::MachOUniversal | Format::UImage => {
"a heap slot or stack frame storage"
}
_ => "a memory slot in SRAM or stack frame storage",
};
format!("Because the initial assignment is never read prior to reassignment, the compiler eliminates it entirely. Rather than reserving {mem_slot}, the compiler propagates the final constant {val} directly into machine code.")
@@ -875,7 +915,9 @@ fn constant_prop_detail(binary: &Binary, val: &str) -> String {
/// Third optimization narrative sentence.
fn constant_prop_storage(binary: &Binary) -> String {
let mem_name = match binary.format {
Format::Pe | Format::Elf | Format::MachO | Format::MachOUniversal | Format::UImage => "code memory, eliminating memory allocation overhead",
Format::Pe | Format::Elf | Format::MachO | Format::MachOUniversal | Format::UImage => {
"code memory, eliminating memory allocation overhead"
}
_ => "Flash memory, eliminating all RAM memory overhead",
};
format!("The variable exists purely inside processor registers as an immediate operand in {mem_name}.")
@@ -1000,7 +1042,10 @@ fn patching_narrative(binary: &Binary, main_func: Option<&Function>) -> Vec<Stri
///
/// Identified patch target if found.
fn find_patch_point(binary: &Binary, func: &Function) -> Option<PatchPoint> {
let line = func.lines.iter().find(|l| l.text.starts_with("movs ") && l.text.contains('#'))?;
let line = func
.lines
.iter()
.find(|l| l.text.starts_with("movs ") && l.text.contains('#'))?;
let imm = immediate(&line.text)?;
let byte_val = u8::try_from(imm).ok()?;
let file_offset = line.address.checked_sub(binary.image_base)?;
@@ -1101,7 +1146,9 @@ fn gpio_behavior(code: &Disassembly) -> Option<String> {
fn gpio_text(code: &Disassembly, func: &Function) -> String {
let pin = pin_of(func).map_or("?".into(), |p| p.to_string());
let name = code.name_of(func.address);
format!("It toggles GPIO pin {pin} using single-cycle I/O coprocessor instructions (in `{name}`).")
format!(
"It toggles GPIO pin {pin} using single-cycle I/O coprocessor instructions (in `{name}`)."
)
}
/// Sentence describing delay loops that pace the behavior.
@@ -1172,11 +1219,13 @@ mod tests {
/// Tests raw firmware boot and storage narrative generation.
#[test]
fn test_raw_boot_and_storage() {
let b = Binary { format: Format::CortexM, arch_name: "ARM Cortex-M".into(), ..Default::default() };
let boot = boot_narrative(&b);
assert!(boot[0].contains("vector table"));
let storage = standard_storage_sentences(&b);
assert!(storage[0].contains("Flash memory"));
let b = Binary {
format: Format::CortexM,
arch_name: "ARM Cortex-M".into(),
..Default::default()
};
assert!(boot_narrative(&b)[0].contains("vector table"));
assert!(standard_storage_sentences(&b)[0].contains("Flash memory"));
}
/// Tests U-Boot image boot and storage narrative generation.
@@ -1189,4 +1238,3 @@ mod tests {
assert!(storage[0].contains("SquashFS"));
}
}
+105 -16
View File
@@ -267,7 +267,10 @@ fn has_interleaved_digits(s: &str) -> bool {
if s.starts_with("0x") || s.starts_with("boot") || s.starts_with("pico") {
return false;
}
b.windows(4).any(|w| (w[0].is_ascii_alphabetic() && w[1].is_ascii_digit() && w[2].is_ascii_alphabetic()) || (w[0].is_ascii_digit() && w[1].is_ascii_alphabetic() && w[2].is_ascii_digit()))
b.windows(4).any(|w| {
(w[0].is_ascii_alphabetic() && w[1].is_ascii_digit() && w[2].is_ascii_alphabetic())
|| (w[0].is_ascii_digit() && w[1].is_ascii_alphabetic() && w[2].is_ascii_digit())
})
}
/// Reports whether a string containing percent signs is a plausible format string.
@@ -281,13 +284,34 @@ fn has_interleaved_digits(s: &str) -> bool {
/// `true` for genuine format strings like `age: %d`.
fn is_valid_format_str(s: &str) -> bool {
let Some(idx) = s.find('%') else { return false };
let spec = s[idx + 1..].chars().find(|c| !c.is_ascii_digit() && !matches!(c, '-' | '+' | ' ' | '#' | '.'));
let valid_spec = spec.is_some_and(|c| matches!(c, 'd' | 'i' | 'u' | 'o' | 'x' | 'X' | 'f' | 's' | 'c' | 'p'));
let spec = s[idx + 1..]
.chars()
.find(|c| !c.is_ascii_digit() && !matches!(c, '-' | '+' | ' ' | '#' | '.'));
let valid_spec = spec
.is_some_and(|c| matches!(c, 'd' | 'i' | 'u' | 'o' | 'x' | 'X' | 'f' | 's' | 'c' | 'p'));
let pfx = s[..idx].trim_matches(|c: char| !c.is_ascii_alphabetic());
valid_spec && (pfx.is_empty() || pfx.chars().any(is_vowel))
}
/// Reports whether text contains an unnatural run of four or more consonants.
/// Advances consonant run count for a character.
///
/// # Arguments
///
/// * `c` - Current character.
/// * `run` - Previous consonant run.
///
/// # Returns
///
/// Updated consonant run count.
fn next_consonant_run(c: char, run: usize) -> usize {
if c.is_ascii_alphabetic() && !is_vowel(c) {
run + 1
} else {
0
}
}
/// Reports whether a consonant sequence reaches four characters.
///
/// # Arguments
///
@@ -299,8 +323,10 @@ fn is_valid_format_str(s: &str) -> bool {
fn has_excessive_consonants(s: &str) -> bool {
let mut run = 0;
for c in s.chars() {
run = if c.is_ascii_alphabetic() && !is_vowel(c) { run + 1 } else { 0 };
if run >= 4 { return true; }
run = next_consonant_run(c, run);
if run >= 4 {
return true;
}
}
false
}
@@ -323,6 +349,19 @@ fn has_repeated_letter(s: &str, alphas: usize) -> bool {
counts.iter().any(|&cnt| cnt >= 3 && usize::from(cnt) * 5 >= alphas * 2)
}
/// Reports whether a character is an internal symbol noise delimiter.
///
/// # Arguments
///
/// * `c` - Character to test.
///
/// # Returns
///
/// `true` for non-standard punctuation noise.
fn is_noise_char(c: char) -> bool {
b"@!+*)('`~^&|<>;\"'".contains(&(c as u8))
}
/// Reports whether unexpected symbols appear inside words.
///
/// # Arguments
@@ -331,11 +370,11 @@ fn has_repeated_letter(s: &str, alphas: usize) -> bool {
///
/// # Returns
///
/// `true` when internal noise characters like `@`, `!`, `+`, `*`, `)` appear.
/// `true` when internal noise characters appear.
fn has_symbol_noise(s: &str) -> bool {
let clean = s.trim_matches(|c| matches!(c, '*' | '-' | '=' | '#' | '"' | '\''));
let inner = clean.trim_matches(|c| matches!(c, ' ' | '_' | '-' | '.' | ':' | '/' | ','));
inner.chars().any(|c| matches!(c, '@' | '!' | '+' | '*' | ')' | '(' | '`' | '~' | '^' | '&' | '|' | '<' | '>' | ';' | '"' | '\''))
inner.chars().any(is_noise_char)
}
/// Reports whether letter casing flips unnaturally back and forth.
@@ -350,7 +389,10 @@ fn has_symbol_noise(s: &str) -> bool {
/// `true` for alternating casing noise like `aKiCa`.
fn has_alternating_case(s: &str, alphas: usize) -> bool {
let letters: Vec<char> = s.chars().filter(char::is_ascii_alphabetic).collect();
let flips = letters.windows(2).filter(|w| w[0].is_ascii_uppercase() != w[1].is_ascii_uppercase()).count();
let flips = letters
.windows(2)
.filter(|w| w[0].is_ascii_uppercase() != w[1].is_ascii_uppercase())
.count();
flips >= 3 && flips * 2 >= alphas
}
@@ -368,6 +410,56 @@ fn has_valid_word(s: &str) -> bool {
.any(|w| w.len() >= 2 && w.chars().any(is_vowel))
}
/// Reports whether text structure has normal natural vowel and consonant rhythms.
///
/// # Arguments
///
/// * `t` - Trimmed candidate text.
/// * `alphas` - Count of alphabetic characters.
///
/// # Returns
///
/// `true` if text follows natural rhythm.
fn has_natural_rhythm(t: &str, alphas: usize) -> bool {
let vowels = t.chars().filter(|c| is_vowel(*c)).count();
vowels > 0
&& vowels * 4 >= alphas
&& !has_excessive_consonants(t)
&& !has_repeated_letter(t, alphas)
&& !has_alternating_case(t, alphas)
&& has_valid_word(t)
}
/// Checks if string matches version or format string patterns.
///
/// # Arguments
///
/// * `t` - Trimmed candidate text.
///
/// # Returns
///
/// Plausibility decision if recognized as specialized format.
fn is_special_text(t: &str) -> Option<bool> {
if is_version(t) {
return Some(true);
}
if t.contains('%') {
return Some(is_valid_format_str(t));
}
None
}
/// Reports whether candidate string satisfies basic length and noise checks.
fn is_plausible_candidate(t: &str) -> bool {
!(t.len() < 3 || has_interleaved_digits(t) || has_symbol_noise(t))
}
/// Checks whether alphabetic characters have natural linguistic rhythm.
fn check_natural_rhythm(t: &str) -> bool {
let alphas = t.chars().filter(char::is_ascii_alphabetic).count();
alphas >= 3 && has_natural_rhythm(t, alphas)
}
/// Reports whether an extracted string is plausible text rather than opcode noise.
///
/// # Arguments
@@ -380,12 +472,10 @@ fn has_valid_word(s: &str) -> bool {
#[must_use]
pub fn is_plausible(s: &str) -> bool {
let t = s.trim();
if t.len() < 3 || has_interleaved_digits(t) || has_symbol_noise(t) { return false; }
if is_version(t) { return true; }
if t.contains('%') { return is_valid_format_str(t); }
let alphas = t.chars().filter(char::is_ascii_alphabetic).count();
let vowels = t.chars().filter(|c| is_vowel(*c)).count();
alphas >= 3 && vowels > 0 && vowels * 4 >= alphas && !has_excessive_consonants(t) && !has_repeated_letter(t, alphas) && !has_alternating_case(t, alphas) && has_valid_word(t)
if !is_plausible_candidate(t) {
return false;
}
is_special_text(t).unwrap_or_else(|| check_natural_rhythm(t))
}
#[cfg(test)]
@@ -446,4 +536,3 @@ mod tests {
assert!(!is_plausible("aKiCa"));
}
}
+19 -7
View File
@@ -333,7 +333,10 @@ fn append_strings_evidence(out: &mut String, a: &Analysis) {
/// * `code` - Disassembly summary.
fn append_code_header(out: &mut String, code: &rea_engine::disasm::Disassembly) {
let (n_fn, ins, bit) = (code.functions.len(), code.instructions, code.bitness);
let _ = writeln!(out, "CODE RECOVERY: {n_fn} functions, {ins} instructions recovered ({bit}-bit).");
let _ = writeln!(
out,
"CODE RECOVERY: {n_fn} functions, {ins} instructions recovered ({bit}-bit)."
);
}
/// Appends disassembly and key function listings to evidence text.
@@ -364,18 +367,27 @@ fn append_key_func_listing(out: &mut String, key: &rea_engine::insights::KeyFunc
}
/// Appends deterministic behavioral narrative to evidence text.
/// Flattens structured story sections into a contiguous list of facts.
fn collect_story_facts(story: rea_engine::narrative::DetailedStory) -> Vec<String> {
[
story.boot,
story.main_logic,
story.optimization,
story.patching,
]
.concat()
}
/// Appends detailed deterministic story facts to evidence output.
///
/// # Arguments
///
/// * `out` - Destination string.
/// * `a` - Completed analysis.
fn append_story_evidence(out: &mut String, a: &Analysis) {
let story = rea_engine::narrative::detailed_program_story(a);
let all = [story.boot, story.main_logic, story.optimization, story.patching];
for section in all {
for p in section {
let _ = writeln!(out, "DETERMINISTIC FACT: {p}");
}
let facts = collect_story_facts(rea_engine::narrative::detailed_program_story(a));
for p in facts {
let _ = writeln!(out, "DETERMINISTIC FACT: {p}");
}
}
+12 -4
View File
@@ -48,6 +48,7 @@ usage:
rea <file> one-shot nation-state analysis and dossier generation
rea serve | rea mcp start Model Context Protocol (MCP) server over stdio
rea mcp --config print MCP config for Claude, Cursor, Windsurf, Gemini, OpenCode
rea mcp --setup turnkey configure Claude Code and OpenCode MCP
rea info <file> inspect file headers, hashes, mitigations, and sections
rea strings <file> extract printable strings and security indicators
rea decompile <file> [sym] decompile target or function into pseudo-C
@@ -176,6 +177,7 @@ fn dispatch_named(cmd: &str, rest: &[String]) -> ExitCode {
match cmd {
"serve" => crate::mcp::serve_cmd(),
"mcp" => dispatch_mcp(rest),
"mcp-setup" => crate::mcp::setup_cmd(),
"analyze" => dispatch_analyze(rest),
_ => dispatch_target(cmd, rest),
}
@@ -205,10 +207,10 @@ fn dispatch_subcommand(cmd: &str, rest: &[String]) -> ExitCode {
///
/// Process exit code.
fn dispatch_mcp(rest: &[String]) -> ExitCode {
if rest.first().is_some_and(|f| f == "--config") {
crate::mcp::config_cmd()
} else {
crate::mcp::serve_cmd()
match rest.first().map(String::as_str) {
Some("--config" | "config") => crate::mcp::config_cmd(),
Some("--setup" | "setup") => crate::mcp::setup_cmd(),
_ => crate::mcp::serve_cmd(),
}
}
@@ -666,4 +668,10 @@ mod tests {
fn test_version_names_program() {
assert!(version().starts_with("rea "));
}
/// Verifies that mcp config flag returns success.
#[test]
fn test_mcp_config_flag() {
assert_eq!(main(&["mcp".into(), "--config".into()]), ExitCode::SUCCESS);
}
}
+32 -6
View File
@@ -51,21 +51,47 @@ pub fn sections(analysis: &Analysis) -> Vec<Section> {
vec![compose_summary(section, &story)]
}
/// Composes the full structured summary section with all subsections.
/// Title for execution lifecycle subsection.
const BOOT_TITLE: &str = "Execution Lifecycle & Boot Sequence";
/// Title for main logic subsection.
const MAIN_TITLE: &str = "Main Application Logic & Program Loop";
/// Title for compiler optimization subsection.
const OPT_TITLE: &str = "Variable Storage & Compiler Optimization";
/// Title for reverse engineering vectors subsection.
const PATCH_TITLE: &str = "Binary Patching & Reverse Engineering Vectors";
/// Appends core logic and optimization narrative subsections.
///
/// # Arguments
///
/// * `section` - Initial section.
/// * `section` - Section to extend.
/// * `story` - Detailed program narrative.
///
/// # Returns
///
/// Updated section with core logic subsections.
fn append_core_summary(mut section: Section, story: &narrative::DetailedStory) -> Section {
section = append_part(section, MAIN_TITLE, &story.main_logic);
append_part(section, OPT_TITLE, &story.optimization)
}
/// Composes narrative subsections into the target summary section.
///
/// # Arguments
///
/// * `section` - Section to extend.
/// * `story` - Detailed program narrative.
///
/// # Returns
///
/// Completed section with headings and paragraphs.
fn compose_summary(mut section: Section, story: &narrative::DetailedStory) -> Section {
section = append_part(section, "Execution Lifecycle & Boot Sequence", &story.boot);
section = append_part(section, "Main Application Logic & Program Loop", &story.main_logic);
section = append_part(section, "Variable Storage & Compiler Optimization", &story.optimization);
append_part(section, "Binary Patching & Reverse Engineering Vectors", &story.patching)
section = append_part(section, BOOT_TITLE, &story.boot);
section = append_core_summary(section, story);
append_part(section, PATCH_TITLE, &story.patching)
}
/// Appends a subheading and its paragraph blocks to a section when non-empty.
+150 -4
View File
@@ -35,7 +35,9 @@ use std::{
};
// Import dependencies from rea_engine
use rea_engine::{analyze_path, Analysis};
use rea_engine::{
analyze_path, format_apk_summary, format_dotnet_summary, format_go_summary, Analysis,
};
// Import dependencies from serde_json
use serde_json::{json, Value};
@@ -151,6 +153,19 @@ fn advanced_tools() -> Value {
])
}
/// Returns runtime and framework inspection tools.
///
/// # Returns
///
/// JSON array of runtime tool declarations.
fn runtime_tools() -> Value {
json!([
{"name": "inspect_go", "description": "Inspects Go runtime metadata, pclntab functions, and dependencies.", "inputSchema": {"type": "object"}},
{"name": "inspect_dotnet", "description": "Inspects .NET ECMA-335 CIL runtime version, tables, and types.", "inputSchema": {"type": "object"}},
{"name": "inspect_android", "description": "Inspects Android APK manifest permissions, components, and classes.dex.", "inputSchema": {"type": "object"}}
])
}
/// Returns the MCP tool catalog array.
///
/// # Returns
@@ -159,6 +174,7 @@ fn advanced_tools() -> Value {
fn tool_catalog() -> Value {
let mut all = inspect_tools().as_array().cloned().unwrap_or_default();
all.extend(advanced_tools().as_array().cloned().unwrap_or_default());
all.extend(runtime_tools().as_array().cloned().unwrap_or_default());
Value::Array(all)
}
@@ -225,6 +241,25 @@ fn dispatch_adv_tool(s: &ServerSession, name: &str, args: &Value) -> String {
}
}
/// Dispatches runtime and framework inspection tools.
///
/// # Arguments
///
/// * `s` - Active server session.
/// * `name` - Tool name.
///
/// # Returns
///
/// Result string if matched, otherwise `None`.
fn dispatch_runtime_tool(s: &ServerSession, name: &str) -> Option<String> {
match name {
"inspect_go" => Some(call_go(s)),
"inspect_dotnet" => Some(call_dotnet(s)),
"inspect_android" => Some(call_android(s)),
_ => None,
}
}
/// Dispatches requested tool by name.
///
/// # Arguments
@@ -237,7 +272,9 @@ fn dispatch_adv_tool(s: &ServerSession, name: &str, args: &Value) -> String {
///
/// Output text result of the tool invocation.
fn dispatch_tool(s: &mut ServerSession, name: &str, args: &Value) -> String {
dispatch_basic_tool(s, name, args).unwrap_or_else(|| dispatch_adv_tool(s, name, args))
dispatch_basic_tool(s, name, args)
.or_else(|| dispatch_runtime_tool(s, name))
.unwrap_or_else(|| dispatch_adv_tool(s, name, args))
}
/// Executes a tools/call request.
@@ -582,6 +619,63 @@ fn call_dossier(args: &Value) -> String {
run_dossier(path)
}
/// Tool handler for `inspect_go`.
///
/// # Arguments
///
/// * `s` - Active server session.
///
/// # Returns
///
/// Formatted Go metadata summary.
fn call_go(s: &ServerSession) -> String {
let Some(a) = &s.active else {
return "No target opened.".to_string();
};
a.go.as_ref().map_or_else(
|| "No Go runtime metadata detected.".to_string(),
format_go_summary,
)
}
/// Tool handler for `inspect_dotnet`.
///
/// # Arguments
///
/// * `s` - Active server session.
///
/// # Returns
///
/// Formatted .NET metadata summary.
fn call_dotnet(s: &ServerSession) -> String {
let Some(a) = &s.active else {
return "No target opened.".to_string();
};
a.dotnet.as_ref().map_or_else(
|| "No .NET CLI metadata detected.".to_string(),
format_dotnet_summary,
)
}
/// Tool handler for `inspect_android`.
///
/// # Arguments
///
/// * `s` - Active server session.
///
/// # Returns
///
/// Formatted Android APK summary.
fn call_android(s: &ServerSession) -> String {
let Some(a) = &s.active else {
return "No target opened.".to_string();
};
a.apk.as_ref().map_or_else(
|| "No Android APK or DEX metadata detected.".to_string(),
format_apk_summary,
)
}
/// Handles one incoming JSON-RPC line from client.
///
/// # Arguments
@@ -644,10 +738,53 @@ fn print_agent_config(title: &str, file: &str) {
/// Exit code.
#[must_use]
pub fn config_cmd() -> ExitCode {
println!("=== Claude Code CLI ===");
println!("claude mcp add rea rea serve\n");
print_agent_config("Claude Desktop", "claude_desktop_config.json");
print_agent_config("OpenCode & Gemini", "mcp_config.json");
print_agent_config("Cursor", "~/.cursor/mcp.json");
print_agent_config("Windsurf", "~/.codeium/windsurf/mcp_config.json");
print_agent_config("Gemini CLI & OpenCode", "mcp_config.json");
ExitCode::SUCCESS
}
/// Loads or initializes MCP servers JSON object.
fn load_or_init_mcp_config(path: &Path) -> Value {
std::fs::read_to_string(path)
.ok()
.and_then(|s| serde_json::from_str::<Value>(&s).ok())
.unwrap_or_else(|| json!({"mcpServers": {}}))
}
/// Injects REA server entry into servers mapping.
fn insert_rea_server(root: &mut Value) {
if let Some(servers) = root.get_mut("mcpServers").and_then(Value::as_object_mut) {
servers.insert("rea".into(), json!({"command": "rea", "args": ["serve"]}));
}
}
/// Writes or updates turnkey MCP config file.
///
/// # Arguments
///
/// * `path` - Destination file path.
fn write_mcp_config_file(path: &Path) {
let mut root = load_or_init_mcp_config(path);
insert_rea_server(&mut root);
let content = serde_json::to_string_pretty(&root).unwrap_or_default();
let _ = std::fs::write(path, content);
}
/// Runs turnkey setup for Claude Code and `OpenCode`.
///
/// # Returns
///
/// Exit code.
#[must_use]
pub fn setup_cmd() -> ExitCode {
write_mcp_config_file(Path::new("mcp_config.json"));
println!("✓ Turnkey OpenCode / AI Agent MCP configuration written to ./mcp_config.json");
println!("✓ Turnkey Claude Code CLI setup command:");
println!(" claude mcp add rea rea serve");
ExitCode::SUCCESS
}
@@ -661,6 +798,15 @@ mod tests {
fn test_tools_catalog_count() {
let tools = tool_catalog();
assert!(tools.is_array());
assert_eq!(tools.as_array().map_or(0, Vec::len), 9);
assert_eq!(tools.as_array().map_or(0, Vec::len), 12);
}
/// Verifies runtime tool handlers reject empty sessions.
#[test]
fn test_runtime_tools_empty_session() {
let session = ServerSession::default();
assert_eq!(call_go(&session), "No target opened.");
assert_eq!(call_dotnet(&session), "No target opened.");
assert_eq!(call_android(&session), "No target opened.");
}
}
+1 -1
View File
@@ -20,7 +20,7 @@ Add `reverse-engineering` to your `Cargo.toml`:
```toml
[dependencies]
reverse-engineering = "3.2.0"
reverse-engineering = "4.0.0"
```
### Programmatic Binary Analysis
Binary file not shown.