mirror of
https://github.com/mytechnotalent/rea.git
synced 2026-11-07 05:56:22 -10:00
Release v4.0.0: Go pclntab, .NET CIL, Android APK forensics, and OpenCode/Claude Code MCP tools
This commit is contained in:
1 parent
e8d097f148
commit
8652218cd2
17 files changed
+1830
-84
No files matched your search
Generated
+6
-6
@@ -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
@@ -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"
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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};
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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.");
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Executable
BIN
Binary file not shown.
Reference in new issue
Block a user