1577 lines
52 KiB
Rust
1577 lines
52 KiB
Rust
// ============================================================================
|
|
// WARLOCK'S STAVE: PRODUCTION CORE IMPLEMENTATION (v7.0)
|
|
// ============================================================================
|
|
//
|
|
// This is the monolithic core library for Warlock's Stave.
|
|
// Submodules are split into separate files and declared below.
|
|
//
|
|
// Architecture: stateless FFI/JNI boundary -> Arc<Mutex<HexHeatmapEngine>>
|
|
// Design: Unix Philosophy — engine is permanent, UI is disposable.
|
|
|
|
use std::collections::{HashMap, HashSet};
|
|
use std::path::Path;
|
|
use std::os::raw::c_char;
|
|
use std::ffi::{CString, CStr};
|
|
use std::net::TcpListener;
|
|
use std::io::Read;
|
|
use std::sync::{Arc, Mutex};
|
|
|
|
// Submodule declarations — each file is an independent module.
|
|
pub mod ebpf;
|
|
pub mod windows_etw;
|
|
pub mod polymorphic_ipc;
|
|
pub mod firmware;
|
|
pub mod stave_ui;
|
|
|
|
use iced_x86::{Decoder, DecoderOptions, Instruction, Mnemonic};
|
|
use jni::JNIEnv;
|
|
use jni::objects::{JClass, JObject};
|
|
use jni::sys::{jlong, jint, jstring};
|
|
use async_trait::async_trait;
|
|
|
|
// Re-export types needed by other modules and the UI layer.
|
|
pub use ebpf::{KernelTraceEvent, LinuxEbpfEngine};
|
|
pub use windows_etw::WindowsEtwEngine;
|
|
pub use polymorphic_ipc::*;
|
|
pub use firmware::HardwareFirmwareAnalyzer;
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Global Constants
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// Square root of 3, pre-computed to f64 precision. Used by the hexagonal
|
|
/// pixel-raycaster to avoid calling .sqrt() on every frame tick.
|
|
pub const SQRT_3: f64 = 1.7320508075688772;
|
|
|
|
/// Static FFI error string allocated in the data segment. Returned on
|
|
/// error paths to prevent host memory leaks when C++ or Java callers
|
|
/// forget to invoke the cleanup handler on error branches.
|
|
const STATIC_FFI_ERROR: &str =
|
|
"{\"status\":\"ERROR\",\"message\":\"Invalid null parameters passed to core\"}\0";
|
|
|
|
|
|
// ============================================================================
|
|
// VECTOR HEX TIMELINE & PIXEL-RAYCASTER
|
|
// ============================================================================
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
pub struct HexCoordinate {
|
|
pub q: i32,
|
|
pub r: i32,
|
|
}
|
|
|
|
pub struct FractionalHex {
|
|
pub q: f64,
|
|
pub r: f64,
|
|
pub s: f64,
|
|
}
|
|
|
|
/// Semantic markers attached to hex cells to classify execution activity.
|
|
/// Used for timeline filtering via Alt+L/B/D/N.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
pub enum TimelineSemanticMarker {
|
|
InsideLoop,
|
|
BreakpointHit,
|
|
ModuleLoaded,
|
|
BulkDiskRead,
|
|
BulkNetworkStream,
|
|
}
|
|
|
|
/// Comprehensive cell state for a single hexagonal grid cell.
|
|
pub struct HexCellState {
|
|
pub coordinate: HexCoordinate,
|
|
pub associated_ticks: Vec<u64>,
|
|
pub total_disk_bytes: u64,
|
|
pub total_mem_bytes: u64,
|
|
pub total_net_bytes: u64,
|
|
pub has_breakpoint: bool,
|
|
pub markers: HashSet<TimelineSemanticMarker>,
|
|
}
|
|
|
|
/// Central analysis engine. Thread-safe via Arc<Mutex<>> wrapping.
|
|
/// Owns the heatmap grid, dictionary scanner, and signature database.
|
|
pub struct HexHeatmapEngine {
|
|
pub cell_matrix: HashMap<HexCoordinate, HexCellState>,
|
|
pub max_intensity_found: u64,
|
|
pub hunting_dictionary: Vec<DictionaryRule>,
|
|
pub signature_db: SignatureDatabaseEngine,
|
|
pub active_filter_mask: u8,
|
|
}
|
|
|
|
impl HexHeatmapEngine {
|
|
pub fn new() -> Self {
|
|
let engine = Self {
|
|
cell_matrix: HashMap::new(),
|
|
max_intensity_found: 0,
|
|
hunting_dictionary: DictionaryRule::get_default_hunting_library(),
|
|
signature_db: {
|
|
let mut db = SignatureDatabaseEngine::new();
|
|
db.load_default_heuristics();
|
|
db
|
|
},
|
|
active_filter_mask: 0x00,
|
|
};
|
|
engine
|
|
}
|
|
|
|
/// Converts absolute mouse screen pixels to axial hex coordinates.
|
|
/// Uses f64 for perfect floating-point coordinate stability.
|
|
pub fn pixel_to_axial(
|
|
&self,
|
|
px: f32,
|
|
py: f32,
|
|
radius: f32,
|
|
origin_x: f32,
|
|
origin_y: f32,
|
|
) -> HexCoordinate {
|
|
let lx = (px - origin_x) as f64;
|
|
let ly = (py - origin_y) as f64;
|
|
let r_val = radius as f64;
|
|
|
|
let frac_q = (2.0 / 3.0 * lx) / r_val;
|
|
let frac_r = (-1.0 / 3.0 * lx + SQRT_3 / 3.0 * ly) / r_val;
|
|
let frac_s = -frac_q - frac_r;
|
|
|
|
self.round_to_nearest_hex(FractionalHex {
|
|
q: frac_q,
|
|
r: frac_r,
|
|
s: frac_s,
|
|
})
|
|
}
|
|
|
|
fn round_to_nearest_hex(&self, frac: FractionalHex) -> HexCoordinate {
|
|
let mut q = frac.q.round() as i32;
|
|
let mut r = frac.r.round() as i32;
|
|
let s = frac.s.round() as i32;
|
|
|
|
let q_diff = (q as f64 - frac.q).abs();
|
|
let r_diff = (r as f64 - frac.r).abs();
|
|
let s_diff = (s as f64 - frac.s).abs();
|
|
|
|
if q_diff > r_diff && q_diff > s_diff {
|
|
q = -r - s;
|
|
} else if r_diff > s_diff {
|
|
r = -q - s;
|
|
}
|
|
|
|
HexCoordinate { q, r }
|
|
}
|
|
|
|
/// Determines if an execution cell should paint based on dashboard
|
|
/// filtering toggles (Alt+L / Alt+B / Alt+D / Alt+N).
|
|
pub fn is_cell_visible(&self, cell: &HexCellState) -> bool {
|
|
if (self.active_filter_mask & 0b0000_0001) != 0
|
|
&& !cell.markers.contains(&TimelineSemanticMarker::InsideLoop)
|
|
{
|
|
return false;
|
|
}
|
|
if (self.active_filter_mask & 0b0000_0010) != 0
|
|
&& !cell.markers.contains(&TimelineSemanticMarker::BreakpointHit)
|
|
{
|
|
return false;
|
|
}
|
|
if (self.active_filter_mask & 0b0000_0100) != 0
|
|
&& !cell.markers.contains(&TimelineSemanticMarker::BulkDiskRead)
|
|
{
|
|
return false;
|
|
}
|
|
if (self.active_filter_mask & 0b0000_1000) != 0
|
|
&& !cell.markers.contains(&TimelineSemanticMarker::BulkNetworkStream)
|
|
{
|
|
return false;
|
|
}
|
|
true
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================================
|
|
// PREDICTIVE BRANCH EVALUATOR ENGINE
|
|
// ============================================================================
|
|
|
|
/// Standalone, zero-allocation processing pipeline driven by iced-x86.
|
|
/// Decodes the current opcode segment and checks processor conditional
|
|
/// registers to predict instruction outcomes before hardware commits.
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum BranchOutcome {
|
|
ConditionMet { target_address: u64 },
|
|
ConditionNotMet,
|
|
Unconditional { target_address: u64 },
|
|
NonControlFlow,
|
|
}
|
|
|
|
pub struct BranchEvaluator;
|
|
|
|
impl BranchEvaluator {
|
|
/// Evaluates execution flow for the instruction at `rip` with
|
|
/// the given `eflags`. Enforces strict 15-byte decoder bounds
|
|
/// to prevent reading unmapped memory pages.
|
|
pub fn evaluate_execution_flow(
|
|
bytes: &[u8],
|
|
rip: u64,
|
|
eflags: u32,
|
|
) -> BranchOutcome {
|
|
let max_safe_length = std::cmp::min(bytes.len(), 15);
|
|
if max_safe_length == 0 {
|
|
return BranchOutcome::NonControlFlow;
|
|
}
|
|
let safe_decoding_slice = &bytes[0..max_safe_length];
|
|
|
|
let mut decoder =
|
|
Decoder::with_ip(64, safe_decoding_slice, rip, DecoderOptions::NONE);
|
|
let mut instruction = Instruction::default();
|
|
decoder.decode_out(&mut instruction);
|
|
|
|
if instruction.is_invalid() {
|
|
return BranchOutcome::NonControlFlow;
|
|
}
|
|
|
|
let zf = (eflags & (1 << 6)) != 0;
|
|
let sf = (eflags & (1 << 7)) != 0;
|
|
let of = (eflags & (1 << 11)) != 0;
|
|
|
|
match instruction.mnemonic() {
|
|
Mnemonic::Jmp | Mnemonic::Call => BranchOutcome::Unconditional {
|
|
target_address: instruction.near_branch_target(),
|
|
},
|
|
Mnemonic::Je => {
|
|
if zf {
|
|
BranchOutcome::ConditionMet {
|
|
target_address: instruction.near_branch_target(),
|
|
}
|
|
} else {
|
|
BranchOutcome::ConditionNotMet
|
|
}
|
|
}
|
|
Mnemonic::Jne => {
|
|
if !zf {
|
|
BranchOutcome::ConditionMet {
|
|
target_address: instruction.near_branch_target(),
|
|
}
|
|
} else {
|
|
BranchOutcome::ConditionNotMet
|
|
}
|
|
}
|
|
Mnemonic::Jg => {
|
|
if !zf && (sf == of) {
|
|
BranchOutcome::ConditionMet {
|
|
target_address: instruction.near_branch_target(),
|
|
}
|
|
} else {
|
|
BranchOutcome::ConditionNotMet
|
|
}
|
|
}
|
|
Mnemonic::Jl => {
|
|
if sf != of {
|
|
BranchOutcome::ConditionMet {
|
|
target_address: instruction.near_branch_target(),
|
|
}
|
|
} else {
|
|
BranchOutcome::ConditionNotMet
|
|
}
|
|
}
|
|
_ => BranchOutcome::NonControlFlow,
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================================
|
|
// DICTIONARY SCANNER & SIGNATURE DATABASE
|
|
// ============================================================================
|
|
|
|
/// A hunting rule for real-time binary pattern matching.
|
|
/// NOTE: Does not derive Copy — contains heap-allocated Strings.
|
|
pub struct DictionaryRule {
|
|
pub label: String,
|
|
pub pattern: Vec<u8>,
|
|
pub description: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct DictionaryMatch {
|
|
pub label: String,
|
|
pub start_offset: usize,
|
|
pub length: usize,
|
|
}
|
|
|
|
impl DictionaryRule {
|
|
/// Returns the default threat-hunting library containing common
|
|
/// indicators: outbound URLs, anti-debugging signatures, encoding
|
|
/// alphabets, and embedded executable headers.
|
|
pub fn get_default_hunting_library() -> Vec<Self> {
|
|
vec![
|
|
DictionaryRule {
|
|
label: "NET_URI_HTTP".to_string(),
|
|
pattern: b"http://".to_vec(),
|
|
description: "Outbound HTTP Connection String".to_string(),
|
|
},
|
|
DictionaryRule {
|
|
label: "NET_URI_HTTPS".to_string(),
|
|
pattern: b"https://".to_vec(),
|
|
description: "Encrypted HTTPS Connection String".to_string(),
|
|
},
|
|
DictionaryRule {
|
|
label: "ANTI_DBG_PRESENT".to_string(),
|
|
pattern: b"IsDebuggerPresent".to_vec(),
|
|
description: "Anti-Debug Presence Check API".to_string(),
|
|
},
|
|
DictionaryRule {
|
|
label: "ANTI_DBG_PTRACE".to_string(),
|
|
pattern: b"ptrace".to_vec(),
|
|
description: "Process Trace Request (Linux)".to_string(),
|
|
},
|
|
DictionaryRule {
|
|
label: "CRYPTO_BASE64".to_string(),
|
|
pattern: b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".to_vec(),
|
|
description: "Base64 Encoding Alphabet Matrix".to_string(),
|
|
},
|
|
DictionaryRule {
|
|
label: "PE_MAGIC_HEADER".to_string(),
|
|
pattern: b"MZ".to_vec(),
|
|
description: "Embedded Portable Executable Header".to_string(),
|
|
},
|
|
]
|
|
}
|
|
}
|
|
|
|
/// Linear scanner that searches target memory buffers against
|
|
/// the active dictionary rule set.
|
|
pub struct MemoryScanner;
|
|
|
|
impl MemoryScanner {
|
|
pub fn scan_buffer(
|
|
buffer: &[u8],
|
|
dictionary: &[DictionaryRule],
|
|
) -> Vec<DictionaryMatch> {
|
|
let mut matches = Vec::new();
|
|
for rule in dictionary {
|
|
let p_len = rule.pattern.len();
|
|
if p_len == 0 || p_len > buffer.len() {
|
|
continue;
|
|
}
|
|
for i in 0..=(buffer.len() - p_len) {
|
|
if &buffer[i..(i + p_len)] == rule.pattern.as_slice() {
|
|
matches.push(DictionaryMatch {
|
|
label: rule.label.clone(),
|
|
start_offset: i,
|
|
length: p_len,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
matches
|
|
}
|
|
}
|
|
|
|
/// External signature rule for loading from YARA-compatible rule files,
|
|
/// ClamAV-compatible signature databases, or custom byte-sequence definitions.
|
|
#[derive(Debug, Clone)]
|
|
pub struct SignatureRule {
|
|
pub rule_id: String,
|
|
pub engine_source: String,
|
|
pub pattern: Vec<u8>,
|
|
pub description: String,
|
|
pub severity: u8, // 0=info, 1=low, 2=medium, 3=high, 4=critical
|
|
}
|
|
|
|
/// Pluggable signature database engine supporting multiple backends.
|
|
/// Performs multi-pass buffer evaluation against all registered rules.
|
|
pub struct SignatureDatabaseEngine {
|
|
pub active_rules_count: u32,
|
|
rules: Vec<SignatureRule>,
|
|
}
|
|
|
|
impl SignatureDatabaseEngine {
|
|
pub fn new() -> Self {
|
|
Self {
|
|
active_rules_count: 0,
|
|
rules: Vec::new(),
|
|
}
|
|
}
|
|
|
|
pub fn register_rule(&mut self, rule: SignatureRule) {
|
|
self.active_rules_count += 1;
|
|
self.rules.push(rule);
|
|
}
|
|
|
|
/// Loads default heuristics covering common packing and
|
|
/// anti-analysis indicators.
|
|
pub fn load_default_heuristics(&mut self) {
|
|
let defaults = vec![
|
|
SignatureRule {
|
|
rule_id: "SIG_UPX_PACKED".to_string(),
|
|
engine_source: "custom".to_string(),
|
|
pattern: b"UPX0".to_vec(),
|
|
description: "UPX-compressed binary section marker detected".to_string(),
|
|
severity: 1,
|
|
},
|
|
SignatureRule {
|
|
rule_id: "SIG_PETITE_PACKED".to_string(),
|
|
engine_source: "custom".to_string(),
|
|
pattern: b"petite".to_vec(),
|
|
description: "Petite packer section header detected".to_string(),
|
|
severity: 1,
|
|
},
|
|
SignatureRule {
|
|
rule_id: "SIG_VM_PROTECT".to_string(),
|
|
engine_source: "custom".to_string(),
|
|
pattern: b".vmp0".to_vec(),
|
|
description: "VMProtect virtualization section marker".to_string(),
|
|
severity: 3,
|
|
},
|
|
SignatureRule {
|
|
rule_id: "SIG_IMPORT_DLL_INJECT".to_string(),
|
|
engine_source: "custom".to_string(),
|
|
pattern: b"LoadLibraryA".to_vec(),
|
|
description: "Dynamic library injection API import".to_string(),
|
|
severity: 2,
|
|
},
|
|
];
|
|
for rule in defaults {
|
|
self.register_rule(rule);
|
|
}
|
|
}
|
|
|
|
/// Evaluates a buffer against all registered signature rules.
|
|
/// Primary hook point for integrating external scanner backends
|
|
/// (YARA via yara-rust, ClamAV via libclamav FFI).
|
|
pub fn evaluate_buffer(&self, buffer: &[u8]) -> Vec<String> {
|
|
let mut hits = Vec::new();
|
|
for rule in &self.rules {
|
|
let p_len = rule.pattern.len();
|
|
if p_len == 0 || p_len > buffer.len() {
|
|
continue;
|
|
}
|
|
for i in 0..=(buffer.len() - p_len) {
|
|
if &buffer[i..(i + p_len)] == rule.pattern.as_slice() {
|
|
hits.push(format!(
|
|
"[{}] {} (severity: {})",
|
|
rule.engine_source, rule.rule_id, rule.severity
|
|
));
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
hits
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================================
|
|
// MIRROR ILLUSION STRUCTURAL DIFF ENGINE
|
|
// ============================================================================
|
|
|
|
/// Status of a single 16-byte grid line in the dual-pane diff viewport.
|
|
#[derive(Debug, Clone, PartialEq)]
|
|
pub enum DiffStatus {
|
|
Identical,
|
|
Modified,
|
|
Inserted,
|
|
Deleted,
|
|
}
|
|
|
|
/// A single row in the diff grid. Represents 16 bytes of aligned
|
|
/// comparison data with per-byte status flags and section context.
|
|
#[derive(Debug, Clone)]
|
|
pub struct DiffGridLine {
|
|
pub file_a_offset: u64,
|
|
pub file_a_bytes: Vec<u8>,
|
|
pub file_b_offset: u64,
|
|
pub file_b_bytes: Vec<u8>,
|
|
pub per_byte_status: Vec<DiffStatus>,
|
|
pub section_name: String,
|
|
}
|
|
|
|
/// Describes a binary section boundary (ELF/PE segment or section header).
|
|
/// Used to align diffs by logical structure rather than raw file offsets.
|
|
#[derive(Debug, Clone)]
|
|
pub struct BinarySectionBoundary {
|
|
pub name: String,
|
|
pub virtual_address: u64,
|
|
pub file_offset: u64,
|
|
pub size: u64,
|
|
}
|
|
|
|
/// Quick structural diff comparing two instruction streams by
|
|
/// (address, length) pairs for the hex heatmap.
|
|
pub fn calculate_structural_diff(
|
|
baseline: &[(u64, u8)],
|
|
current: &[(u64, u8)],
|
|
) -> Vec<DiffEntry> {
|
|
let base_set: HashSet<(u64, u8)> = baseline.iter().cloned().collect();
|
|
let curr_set: HashSet<(u64, u8)> = current.iter().cloned().collect();
|
|
let mut diffs = Vec::new();
|
|
|
|
for &(addr, len) in baseline.iter() {
|
|
if curr_set.contains(&(addr, len)) {
|
|
continue;
|
|
}
|
|
let is_deleted = !current.iter().any(|(a, _)| *a == addr);
|
|
diffs.push(DiffEntry {
|
|
address: addr,
|
|
length: len,
|
|
status: if is_deleted {
|
|
DiffStatus::Deleted
|
|
} else {
|
|
DiffStatus::Modified
|
|
},
|
|
});
|
|
}
|
|
|
|
for &(addr, len) in current.iter() {
|
|
if !base_set.contains(&(addr, len)) {
|
|
if !baseline.iter().any(|(a, _)| *a == addr) {
|
|
diffs.push(DiffEntry {
|
|
address: addr,
|
|
length: len,
|
|
status: DiffStatus::Inserted,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
diffs.sort_by_key(|d| d.address);
|
|
diffs
|
|
}
|
|
|
|
/// Lightweight diff entry for heatmap integration.
|
|
#[derive(Debug, Clone)]
|
|
pub struct DiffEntry {
|
|
pub address: u64,
|
|
pub length: u8,
|
|
pub status: DiffStatus,
|
|
}
|
|
|
|
/// Full section-aware binary diff that aligns two files by their
|
|
/// ELF/PE section boundaries rather than raw byte offsets.
|
|
pub fn calculate_section_aware_diff(
|
|
data_a: &[u8],
|
|
data_b: &[u8],
|
|
sections_a: &[BinarySectionBoundary],
|
|
sections_b: &[BinarySectionBoundary],
|
|
) -> Vec<DiffGridLine> {
|
|
let mut grid_lines = Vec::new();
|
|
|
|
for sec_a in sections_a {
|
|
let sec_b = match sections_b.iter().find(|s| s.name == sec_a.name) {
|
|
Some(s) => s,
|
|
None => continue,
|
|
};
|
|
|
|
let compare_length = sec_a.size.min(sec_b.size) as usize;
|
|
let start_a = sec_a.file_offset as usize;
|
|
let start_b = sec_b.file_offset as usize;
|
|
|
|
let safe_len = compare_length
|
|
.min(data_a.len().saturating_sub(start_a))
|
|
.min(data_b.len().saturating_sub(start_b));
|
|
|
|
let mut offset = 0;
|
|
while offset + 16 <= safe_len {
|
|
let chunk_a = &data_a[start_a + offset..start_a + offset + 16];
|
|
let chunk_b = &data_b[start_b + offset..start_b + offset + 16];
|
|
|
|
let mut per_byte = Vec::with_capacity(16);
|
|
let mut has_any_diff = false;
|
|
for i in 0..16 {
|
|
if chunk_a[i] != chunk_b[i] {
|
|
per_byte.push(DiffStatus::Modified);
|
|
has_any_diff = true;
|
|
} else {
|
|
per_byte.push(DiffStatus::Identical);
|
|
}
|
|
}
|
|
|
|
if has_any_diff {
|
|
grid_lines.push(DiffGridLine {
|
|
file_a_offset: sec_a.file_offset + offset as u64,
|
|
file_a_bytes: chunk_a.to_vec(),
|
|
file_b_offset: sec_b.file_offset + offset as u64,
|
|
file_b_bytes: chunk_b.to_vec(),
|
|
per_byte_status: per_byte,
|
|
section_name: sec_a.name.clone(),
|
|
});
|
|
}
|
|
|
|
offset += 16;
|
|
}
|
|
}
|
|
|
|
grid_lines
|
|
}
|
|
|
|
|
|
// ============================================================================
|
|
// TRI-TIER SCOPE ENGINE
|
|
// ============================================================================
|
|
|
|
/// Three concurrent analytical layers that classify the context of each
|
|
/// execution frame: Global (cross-references), Local (function block),
|
|
/// and Micro (individual instruction side-effects).
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub enum ScopeTier {
|
|
/// Global: maps cross-references, export footprints, and entropy strips.
|
|
Global,
|
|
/// Local: isolates analysis to the active function block.
|
|
/// NOTE: Contains a String, so this enum cannot be Copy.
|
|
Local { function_name: String, start_rip: u64 },
|
|
/// Micro: monitors individual instruction side-effects.
|
|
Micro,
|
|
}
|
|
|
|
/// Predicted implicit register and memory modifications for an instruction.
|
|
#[derive(Debug, Clone)]
|
|
pub struct MicroSideEffect {
|
|
pub implicit_write_registers: Vec<String>,
|
|
pub implicit_memory_writes: Vec<u64>,
|
|
pub flags_affected: bool,
|
|
}
|
|
|
|
/// The scope engine that determines the current analytical context
|
|
/// based on the instruction pointer and control flow history.
|
|
pub struct ScopeEngine {
|
|
pub current_tier: ScopeTier,
|
|
pub function_boundaries: HashMap<String, (u64, u64)>,
|
|
pub global_xref_count: usize,
|
|
pub region_entropy: f64,
|
|
}
|
|
|
|
impl ScopeEngine {
|
|
pub fn new() -> Self {
|
|
Self {
|
|
current_tier: ScopeTier::Global,
|
|
function_boundaries: HashMap::new(),
|
|
global_xref_count: 0,
|
|
region_entropy: 0.0,
|
|
}
|
|
}
|
|
|
|
/// Determines the active scope tier based on the current RIP.
|
|
pub fn update_scope_for_rip(&mut self, rip: u64) {
|
|
for (name, (start, end)) in &self.function_boundaries {
|
|
if rip >= *start && rip < *end {
|
|
self.current_tier = ScopeTier::Local {
|
|
function_name: name.clone(),
|
|
start_rip: *start,
|
|
};
|
|
return;
|
|
}
|
|
}
|
|
self.current_tier = ScopeTier::Global;
|
|
}
|
|
|
|
/// Registers a function boundary for local scope tracking.
|
|
pub fn register_function(&mut self, name: String, start: u64, end: u64) {
|
|
self.function_boundaries.insert(name, (start, end));
|
|
}
|
|
|
|
/// Analyzes the decoded instruction to predict which registers
|
|
/// and memory locations will be implicitly modified.
|
|
pub fn predict_side_effects(
|
|
&self,
|
|
bytes: &[u8],
|
|
rip: u64,
|
|
) -> MicroSideEffect {
|
|
let max_safe = std::cmp::min(bytes.len(), 15);
|
|
if max_safe == 0 {
|
|
return MicroSideEffect {
|
|
implicit_write_registers: Vec::new(),
|
|
implicit_memory_writes: Vec::new(),
|
|
flags_affected: false,
|
|
};
|
|
}
|
|
|
|
let mut decoder =
|
|
Decoder::with_ip(64, &bytes[0..max_safe], rip, DecoderOptions::NONE);
|
|
let mut instruction = Instruction::default();
|
|
decoder.decode_out(&mut instruction);
|
|
|
|
if instruction.is_invalid() {
|
|
return MicroSideEffect {
|
|
implicit_write_registers: Vec::new(),
|
|
implicit_memory_writes: Vec::new(),
|
|
flags_affected: false,
|
|
};
|
|
}
|
|
|
|
let mut written_regs = Vec::new();
|
|
let mut mem_writes = Vec::new();
|
|
let mut flags = false;
|
|
|
|
let mut info_factory = iced_x86::InstructionInfoFactory::new();
|
|
let info = info_factory.info(&instruction);
|
|
|
|
for i in 0..instruction.op_count() {
|
|
let access = info.op_access(i);
|
|
match instruction.op_kind(i) {
|
|
iced_x86::OpKind::Register => {
|
|
let reg = instruction.op_register(i);
|
|
if reg != iced_x86::Register::None {
|
|
let name = format!("{:?}", reg);
|
|
if instruction.mnemonic() != Mnemonic::Push
|
|
&& instruction.mnemonic() != Mnemonic::Pop
|
|
{
|
|
match access {
|
|
iced_x86::OpAccess::Write
|
|
| iced_x86::OpAccess::ReadWrite => {
|
|
written_regs.push(name);
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
iced_x86::OpKind::Memory => {
|
|
match access {
|
|
iced_x86::OpAccess::Write
|
|
| iced_x86::OpAccess::ReadWrite => {
|
|
let disp = instruction.memory_displacement64();
|
|
if disp != 0 {
|
|
mem_writes.push(disp as u64);
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
use iced_x86::FlowControl;
|
|
match instruction.flow_control() {
|
|
FlowControl::Next | FlowControl::ConditionalBranch => {
|
|
flags = true;
|
|
}
|
|
_ => {}
|
|
}
|
|
|
|
MicroSideEffect {
|
|
implicit_write_registers: written_regs,
|
|
implicit_memory_writes: mem_writes,
|
|
flags_affected: flags,
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================================
|
|
// MULTIVERSE SECURITY INFRASTRUCTURE PROVIDERS
|
|
// ============================================================================
|
|
|
|
/// Orchestrates independent local scanners and cloud intelligence
|
|
/// infrastructure using decoupled background pipelines.
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct GlobalSpreadMetrics {
|
|
pub detection_count: u32,
|
|
pub total_scanners: u32,
|
|
pub velocity_flag: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub enum ScanEngineStatus {
|
|
Clean,
|
|
Infected { threat_name: String },
|
|
GlobalAwarenessUpdate(GlobalSpreadMetrics),
|
|
Pending,
|
|
Error(String),
|
|
}
|
|
|
|
/// Trait for external scanner backends. Implementations can wrap
|
|
/// YARA-compatible engines, ClamAV-compatible engines, or cloud
|
|
/// hash-lookup services.
|
|
#[async_trait]
|
|
pub trait ExternalScannerProvider: Send + Sync {
|
|
fn name(&self) -> &'static str;
|
|
async fn scan_file_path(&self, path: &std::path::Path) -> ScanEngineStatus;
|
|
async fn scan_hash(&self, sha256: &str) -> ScanEngineStatus;
|
|
}
|
|
|
|
|
|
// ============================================================================
|
|
// SELF-ASSEMBLING IPC TRANSMISSION FRAMEWORK
|
|
// ============================================================================
|
|
|
|
pub enum IpcTransport {
|
|
UnixSocket(String),
|
|
TcpNetwork(u16),
|
|
}
|
|
|
|
/// Wire-format payload for the Wine/Windows named-pipe translation bridge.
|
|
/// Packed to match the C++ struct layout on the other side of the FFI.
|
|
#[repr(C, packed)]
|
|
#[derive(Clone, Copy)]
|
|
pub struct WineIpcPayload {
|
|
pub rip: u64,
|
|
pub eflags: u32,
|
|
pub buffer_len: u32,
|
|
pub bytes: [u8; 15],
|
|
}
|
|
|
|
/// Dynamically assembles the most efficient IPC transport for the
|
|
/// current host environment at startup.
|
|
pub struct HybridIpcOrchestrator;
|
|
|
|
impl HybridIpcOrchestrator {
|
|
pub fn is_containerized() -> bool {
|
|
Path::new("/.dockerenv").exists()
|
|
|| Path::new("/var/run/secrets/kubernetes.io").exists()
|
|
|| std::env::var("STAVE_CONTAINER_MODE").is_ok()
|
|
}
|
|
|
|
pub fn is_waydroid_environment() -> bool {
|
|
Path::new("/dev/waydroid-binder").exists()
|
|
|| std::fs::read_to_string("/proc/self/cgroup")
|
|
.map(|c| c.contains("waydroid"))
|
|
.unwrap_or(false)
|
|
}
|
|
|
|
pub fn auto_assemble_transport() -> IpcTransport {
|
|
if Self::is_containerized() {
|
|
IpcTransport::TcpNetwork(21860)
|
|
} else {
|
|
IpcTransport::UnixSocket("/tmp/stave_bridge_ipc.sock".to_string())
|
|
}
|
|
}
|
|
|
|
/// Launches the IPC listener on a background thread. Uses Arc<Mutex<>>
|
|
/// for thread-safe access to the core engine from multiple IPC clients.
|
|
pub fn launch(
|
|
core_context: Arc<Mutex<HexHeatmapEngine>>,
|
|
) -> std::io::Result<()> {
|
|
let transport = Self::auto_assemble_transport();
|
|
std::thread::spawn(move || {
|
|
let mut struct_buffer =
|
|
vec![0u8; std::mem::size_of::<WineIpcPayload>()];
|
|
match transport {
|
|
IpcTransport::UnixSocket(path) => {
|
|
if Path::new(&path).exists() {
|
|
let _ = std::fs::remove_file(&path);
|
|
}
|
|
let listener =
|
|
std::os::unix::net::UnixListener::bind(path).unwrap();
|
|
for mut stream in listener.incoming().flatten() {
|
|
while stream.read_exact(&mut struct_buffer).is_ok() {
|
|
Self::process_buffer(&struct_buffer, &core_context);
|
|
}
|
|
}
|
|
}
|
|
IpcTransport::TcpNetwork(port) => {
|
|
let listener =
|
|
TcpListener::bind(format!("0.0.0.0:{}", port)).unwrap();
|
|
for stream in listener.incoming().flatten() {
|
|
let _ = stream.set_nodelay(true);
|
|
let mut s = stream;
|
|
while s.read_exact(&mut struct_buffer).is_ok() {
|
|
Self::process_buffer(&struct_buffer, &core_context);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
Ok(())
|
|
}
|
|
|
|
fn process_buffer(
|
|
raw_buffer: &[u8],
|
|
context: &Arc<Mutex<HexHeatmapEngine>>,
|
|
) {
|
|
let payload =
|
|
unsafe { &*(raw_buffer.as_ptr() as *const WineIpcPayload) };
|
|
let mut core = context.lock().unwrap();
|
|
|
|
let coord = HexCoordinate {
|
|
q: (payload.rip & 0xFF) as i32,
|
|
r: ((payload.rip >> 8) & 0xFF) as i32,
|
|
};
|
|
{
|
|
let entry = core.cell_matrix.entry(coord).or_insert(HexCellState {
|
|
coordinate: coord,
|
|
associated_ticks: Vec::new(),
|
|
total_disk_bytes: 0,
|
|
total_mem_bytes: 0,
|
|
total_net_bytes: 0,
|
|
has_breakpoint: false,
|
|
markers: HashSet::new(),
|
|
});
|
|
entry.associated_ticks.push(payload.rip);
|
|
|
|
let hit_count = entry.associated_ticks.len() as u64;
|
|
if hit_count > core.max_intensity_found {
|
|
core.max_intensity_found = hit_count;
|
|
}
|
|
}
|
|
// Borrow on `entry` is now dropped; safe to access other fields.
|
|
|
|
let usable_len = std::cmp::min(payload.buffer_len as usize, 15);
|
|
let instr_bytes = &payload.bytes[0..usable_len];
|
|
|
|
let _branch = BranchEvaluator::evaluate_execution_flow(
|
|
instr_bytes, payload.rip, payload.eflags,
|
|
);
|
|
|
|
let _dict_hits =
|
|
MemoryScanner::scan_buffer(instr_bytes, &core.hunting_dictionary);
|
|
|
|
let _sig_hits = core.signature_db.evaluate_buffer(instr_bytes);
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================================
|
|
// C-COMPATIBLE FFI BOUNDARY LAYER
|
|
// ============================================================================
|
|
|
|
/// Opaque context handle exposed to C/C++ host environments.
|
|
/// Internally wraps the thread-safe Arc<Mutex<>> core engine.
|
|
pub struct StaveCoreContext;
|
|
|
|
/// Initializes the Warlock's Stave engine and returns an opaque handle.
|
|
///
|
|
/// # Safety
|
|
/// The returned pointer must be passed to `destroy_warlock_stave()` to
|
|
/// free resources. Do not double-free.
|
|
///
|
|
/// # Example (C)
|
|
/// ```c
|
|
/// void* ctx = initialize_warlock_stave();
|
|
/// const char* result = stave_ingest_frame(ctx, buf, len, rip, eflags);
|
|
/// free_stave_string(result);
|
|
/// destroy_warlock_stave(ctx);
|
|
/// ```
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn initialize_warlock_stave() -> *mut StaveCoreContext {
|
|
let core = Arc::new(Mutex::new(HexHeatmapEngine::new()));
|
|
let _ = HybridIpcOrchestrator::launch(core.clone());
|
|
Box::into_raw(Box::new(core)) as *mut StaveCoreContext
|
|
}
|
|
|
|
/// Ingests a single execution frame from the host debugger.
|
|
/// Returns a JSON-Lines string describing branch evaluation and threat hits.
|
|
///
|
|
/// # Safety
|
|
/// - `context` must be a valid pointer from `initialize_warlock_stave()`.
|
|
/// - `raw_buffer_ptr` must be a valid pointer to at least `buffer_length` bytes.
|
|
/// - The returned pointer must be freed with `free_stave_string()`.
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn stave_ingest_frame(
|
|
context: *mut StaveCoreContext,
|
|
raw_buffer_ptr: *const u8,
|
|
buffer_length: usize,
|
|
current_rip: u64,
|
|
eflags: u32,
|
|
) -> *mut c_char {
|
|
if context.is_null() || raw_buffer_ptr.is_null() || buffer_length == 0 {
|
|
return STATIC_FFI_ERROR.as_ptr() as *mut c_char;
|
|
}
|
|
|
|
let core_arc = &*(context as *mut Arc<Mutex<HexHeatmapEngine>>);
|
|
let mut core = core_arc.lock().unwrap();
|
|
let memory_slice = std::slice::from_raw_parts(raw_buffer_ptr, buffer_length);
|
|
|
|
let hex_coord = HexCoordinate {
|
|
q: (current_rip & 0xFF) as i32,
|
|
r: ((current_rip >> 8) & 0xFF) as i32,
|
|
};
|
|
|
|
{
|
|
let entry = core.cell_matrix.entry(hex_coord).or_insert(HexCellState {
|
|
coordinate: hex_coord,
|
|
associated_ticks: Vec::new(),
|
|
total_disk_bytes: 0,
|
|
total_mem_bytes: 0,
|
|
total_net_bytes: 0,
|
|
has_breakpoint: false,
|
|
markers: HashSet::new(),
|
|
});
|
|
entry.associated_ticks.push(current_rip);
|
|
let hits = entry.associated_ticks.len() as u64;
|
|
if hits > core.max_intensity_found {
|
|
core.max_intensity_found = hits;
|
|
}
|
|
}
|
|
|
|
let branch_outcome =
|
|
BranchEvaluator::evaluate_execution_flow(memory_slice, current_rip, eflags);
|
|
let local_dict_matches =
|
|
MemoryScanner::scan_buffer(memory_slice, &core.hunting_dictionary);
|
|
let signature_hits = core.signature_db.evaluate_buffer(memory_slice);
|
|
|
|
let dict_labels: HashSet<&str> = local_dict_matches
|
|
.iter()
|
|
.map(|m| m.label.as_str())
|
|
.collect();
|
|
let has_network_io = dict_labels.contains("NET_URI_HTTP")
|
|
|| dict_labels.contains("NET_URI_HTTPS");
|
|
let has_disk_io = dict_labels.contains("PE_MAGIC_HEADER");
|
|
|
|
let payload = serde_json::json!({
|
|
"status": "OK",
|
|
"tick": core.max_intensity_found,
|
|
"event": "STATE_SYNCHRONIZATION",
|
|
"execution_pointer": format!("{:#018X}", current_rip),
|
|
"hex_coords": {
|
|
"q": hex_coord.q,
|
|
"r": hex_coord.r
|
|
},
|
|
"branch_evaluation": format!("{:?}", branch_outcome),
|
|
"threat_signatures": {
|
|
"dictionary_hits": local_dict_matches.len(),
|
|
"dictionary_labels": local_dict_matches.iter().map(|m| &m.label).collect::<Vec<_>>(),
|
|
"signature_engine_hits": signature_hits
|
|
},
|
|
"io_activity": {
|
|
"disk_bytes": if has_disk_io { buffer_length as u64 } else { 0 },
|
|
"memory_bytes": buffer_length as u64,
|
|
"network_bytes": if has_network_io { buffer_length as u64 } else { 0 }
|
|
}
|
|
});
|
|
|
|
let json_string = CString::new(payload.to_string()).unwrap();
|
|
json_string.into_raw()
|
|
}
|
|
|
|
/// Frees a JSON string previously returned by `stave_ingest_frame`.
|
|
/// Guards against double-freeing the static error string.
|
|
///
|
|
/// # Safety
|
|
/// `ptr` must be either null or a pointer returned by `stave_ingest_frame`.
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn free_stave_string(ptr: *mut c_char) {
|
|
if !ptr.is_null() && ptr != STATIC_FFI_ERROR.as_ptr() as *mut c_char {
|
|
let _ = CString::from_raw(ptr);
|
|
}
|
|
}
|
|
|
|
/// Destroys the core engine context and releases all resources.
|
|
///
|
|
/// # Safety
|
|
/// `context` must be a valid pointer from `initialize_warlock_stave()`.
|
|
/// Do not use the pointer after calling this function.
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn destroy_warlock_stave(context: *mut StaveCoreContext) {
|
|
if !context.is_null() {
|
|
let _ = Box::from_raw(
|
|
context as *mut Arc<Mutex<HexHeatmapEngine>>,
|
|
);
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================================
|
|
// JAVA NATIVE INTERFACE (JNI) EXTENSION
|
|
// ============================================================================
|
|
|
|
/// Host integration entry point. Receives a DirectByteBuffer from the
|
|
/// JVM containing raw instruction bytes, passes them through the core
|
|
/// analysis pipeline, and returns a JSON telemetry string.
|
|
///
|
|
/// # Safety
|
|
/// - `context_ptr` must be a valid pointer from `initializeWarlockStave()`.
|
|
/// - `byte_buffer` must be a valid direct NIO ByteBuffer.
|
|
#[no_mangle]
|
|
pub unsafe extern "system" fn Java_org_stave_WarlockStaveBridge_staveIngestGhidraFrame(
|
|
env: JNIEnv,
|
|
_class: JClass,
|
|
context_ptr: jlong,
|
|
byte_buffer: JObject,
|
|
rip: jlong,
|
|
eflags: jint,
|
|
) -> jstring {
|
|
if context_ptr == 0 || byte_buffer.is_null() {
|
|
return env
|
|
.new_string("{\"status\":\"ERROR\",\"message\":\"Null parameter in JNI link\"}")
|
|
.unwrap()
|
|
.into_raw();
|
|
}
|
|
|
|
let jbb: jni::objects::JByteBuffer = byte_buffer.into();
|
|
let buffer_address = env.get_direct_buffer_address(&jbb).unwrap();
|
|
let buffer_capacity = env.get_direct_buffer_capacity(&jbb).unwrap();
|
|
let raw_memory_slice =
|
|
std::slice::from_raw_parts(buffer_address, buffer_capacity);
|
|
|
|
let res_ptr = stave_ingest_frame(
|
|
context_ptr as *mut StaveCoreContext,
|
|
raw_memory_slice.as_ptr(),
|
|
raw_memory_slice.len(),
|
|
rip as u64,
|
|
eflags as u32,
|
|
);
|
|
let cstr_result = CStr::from_ptr(res_ptr);
|
|
let output_string = env.new_string(cstr_result.to_str().unwrap()).unwrap();
|
|
|
|
free_stave_string(res_ptr);
|
|
output_string.into_raw()
|
|
}
|
|
|
|
|
|
// ============================================================================
|
|
// CROSS-PLATFORM SYSTEM INTELLIGENCE INTERFACES
|
|
// ============================================================================
|
|
|
|
/// Dispatches platform-specific telemetry hooks at engine startup.
|
|
pub struct MultiplatformTelemetryHub;
|
|
|
|
impl MultiplatformTelemetryHub {
|
|
pub fn start_native_hooks(&self) {
|
|
#[cfg(target_os = "linux")]
|
|
{
|
|
println!(
|
|
"[PLATFORM] Linux kernel environment. \
|
|
Mounting raw eBPF context tracking frames."
|
|
);
|
|
// NOTE: LinuxEbpfEngine::spawn_kernel_hooks() requires
|
|
// elevated permissions. Callers who need kernel tracing
|
|
// should invoke it explicitly after confirming permissions.
|
|
}
|
|
#[cfg(target_os = "windows")]
|
|
{
|
|
println!(
|
|
"[PLATFORM] Windows environment. \
|
|
Attaching Event Tracing for Windows (ETW) hooks."
|
|
);
|
|
unsafe { WindowsEtwEngine::spawn_windows_tap() };
|
|
}
|
|
#[cfg(target_os = "macos")]
|
|
{
|
|
println!(
|
|
"[PLATFORM] macOS environment. \
|
|
Endpoint Security Framework consumers reserved."
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================================
|
|
// STATE DECAY SYSTEM
|
|
// ============================================================================
|
|
|
|
/// Default number of frames a highlight persists before fading.
|
|
/// At 60 FPS this gives roughly 2 seconds of visible highlight.
|
|
const DEFAULT_HIGHLIGHT_TTL: u8 = 120;
|
|
|
|
/// Tracks a single register's mutation state and its decay counter.
|
|
#[derive(Debug, Clone)]
|
|
pub struct RegisterHighlight {
|
|
pub register_name: String,
|
|
pub old_value: u64,
|
|
pub new_value: u64,
|
|
pub frames_remaining: u8,
|
|
}
|
|
|
|
/// Tracks a single memory byte's mutation state and its decay counter.
|
|
#[derive(Debug, Clone)]
|
|
pub struct MemoryHighlight {
|
|
pub address: u64,
|
|
pub old_byte: u8,
|
|
pub new_byte: u8,
|
|
pub frames_remaining: u8,
|
|
}
|
|
|
|
/// Manages all active highlights and drives the per-frame decay cycle.
|
|
pub struct StateDecayEngine {
|
|
pub register_highlights: HashMap<String, RegisterHighlight>,
|
|
pub memory_highlights: HashMap<u64, MemoryHighlight>,
|
|
pub highlight_ttl: u8,
|
|
}
|
|
|
|
impl StateDecayEngine {
|
|
pub fn new() -> Self {
|
|
Self {
|
|
register_highlights: HashMap::new(),
|
|
memory_highlights: HashMap::new(),
|
|
highlight_ttl: DEFAULT_HIGHLIGHT_TTL,
|
|
}
|
|
}
|
|
|
|
pub fn mark_register_mutated(
|
|
&mut self,
|
|
name: &str,
|
|
old_val: u64,
|
|
new_val: u64,
|
|
) {
|
|
self.register_highlights.insert(
|
|
name.to_uppercase(),
|
|
RegisterHighlight {
|
|
register_name: name.to_uppercase(),
|
|
old_value: old_val,
|
|
new_value: new_val,
|
|
frames_remaining: self.highlight_ttl,
|
|
},
|
|
);
|
|
}
|
|
|
|
pub fn mark_memory_mutated(
|
|
&mut self,
|
|
address: u64,
|
|
old_byte: u8,
|
|
new_byte: u8,
|
|
) {
|
|
self.memory_highlights.insert(
|
|
address,
|
|
MemoryHighlight {
|
|
address,
|
|
old_byte,
|
|
new_byte,
|
|
frames_remaining: self.highlight_ttl,
|
|
},
|
|
);
|
|
}
|
|
|
|
/// Advances the decay clock by one frame. Returns the set of
|
|
/// register names and memory addresses that expired this tick.
|
|
pub fn tick(&mut self) -> (Vec<String>, Vec<u64>) {
|
|
let mut expired_registers = Vec::new();
|
|
let mut expired_memory = Vec::new();
|
|
|
|
self.register_highlights.retain(|name, highlight| {
|
|
highlight.frames_remaining = highlight.frames_remaining.saturating_sub(1);
|
|
if highlight.frames_remaining == 0 {
|
|
expired_registers.push(name.clone());
|
|
false
|
|
} else {
|
|
true
|
|
}
|
|
});
|
|
|
|
self.memory_highlights.retain(|addr, highlight| {
|
|
highlight.frames_remaining = highlight.frames_remaining.saturating_sub(1);
|
|
if highlight.frames_remaining == 0 {
|
|
expired_memory.push(*addr);
|
|
false
|
|
} else {
|
|
true
|
|
}
|
|
});
|
|
|
|
(expired_registers, expired_memory)
|
|
}
|
|
|
|
pub fn is_register_highlighted(&self, name: &str) -> bool {
|
|
self.register_highlights.contains_key(&name.to_uppercase())
|
|
}
|
|
|
|
pub fn is_memory_highlighted(&self, addr: u64) -> bool {
|
|
self.memory_highlights.contains_key(&addr)
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================================
|
|
// SHELLCODE SENTINEL (HEAP PERMISSION TRANSITION MONITOR)
|
|
// ============================================================================
|
|
|
|
/// Watches runtime memory segment permissions for transitions that
|
|
/// indicate shellcode injection or JIT-spray attacks. The canonical
|
|
/// indicator is a page transitioning from writable (RW-) to executable
|
|
/// (R-X or RWX) — legitimate code does not write to executable memory
|
|
/// at runtime.
|
|
|
|
/// Describes a tracked memory region and its observed permission state.
|
|
#[derive(Debug, Clone)]
|
|
pub struct TrackedMemoryRegion {
|
|
pub start_address: u64,
|
|
pub end_address: u64,
|
|
pub permissions: String,
|
|
pub pathname: Option<String>,
|
|
pub transition_count: u32,
|
|
pub last_transition_kind: Option<PermissionTransition>,
|
|
}
|
|
|
|
/// The kind of permission transition that was detected.
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub enum PermissionTransition {
|
|
WriteAddedToExecutable,
|
|
ExecuteAddedToWritable,
|
|
ExecuteRemoved,
|
|
}
|
|
|
|
/// A shellcode sentinel alert event.
|
|
#[derive(Debug, Clone)]
|
|
pub struct ShellcodeAlert {
|
|
pub pid: u32,
|
|
pub region: TrackedMemoryRegion,
|
|
pub transition: PermissionTransition,
|
|
pub timestamp: std::time::Instant,
|
|
}
|
|
|
|
/// The sentinel engine that monitors memory permission transitions.
|
|
pub struct ShellcodeSentinel {
|
|
previous_snapshots: HashMap<u32, Vec<TrackedMemoryRegion>>,
|
|
pub active_alerts: Vec<ShellcodeAlert>,
|
|
}
|
|
|
|
impl ShellcodeSentinel {
|
|
pub fn new() -> Self {
|
|
Self {
|
|
previous_snapshots: HashMap::new(),
|
|
active_alerts: Vec::new(),
|
|
}
|
|
}
|
|
|
|
/// Parses /proc/<pid>/maps into tracked memory regions.
|
|
fn parse_proc_maps(pid: u32) -> Vec<TrackedMemoryRegion> {
|
|
let mut regions = Vec::new();
|
|
let maps_path = format!("/proc/{}/maps", pid);
|
|
if let Ok(content) = std::fs::read_to_string(&maps_path) {
|
|
for line in content.lines() {
|
|
let parts: Vec<&str> = line.split_whitespace().collect();
|
|
if parts.len() < 2 {
|
|
continue;
|
|
}
|
|
let addrs: Vec<&str> = parts[0].split('-').collect();
|
|
if addrs.len() != 2 {
|
|
continue;
|
|
}
|
|
let start = u64::from_str_radix(addrs[0], 16).unwrap_or(0);
|
|
let end = u64::from_str_radix(addrs[1], 16).unwrap_or(0);
|
|
let pathname = if parts.len() >= 6 {
|
|
Some(parts[5..].join(" "))
|
|
} else {
|
|
None
|
|
};
|
|
regions.push(TrackedMemoryRegion {
|
|
start_address: start,
|
|
end_address: end,
|
|
permissions: parts[1].to_string(),
|
|
pathname,
|
|
transition_count: 0,
|
|
last_transition_kind: None,
|
|
});
|
|
}
|
|
}
|
|
regions
|
|
}
|
|
|
|
/// Compares two permission strings and returns the transition
|
|
/// kind if a suspicious change is detected.
|
|
fn detect_suspicious_transition(
|
|
old_perms: &str,
|
|
new_perms: &str,
|
|
) -> Option<PermissionTransition> {
|
|
let old_w = old_perms.chars().nth(1).unwrap_or('-') == 'w';
|
|
let old_x = old_perms.chars().nth(2).unwrap_or('-') == 'x';
|
|
let new_w = new_perms.chars().nth(1).unwrap_or('-') == 'w';
|
|
let new_x = new_perms.chars().nth(2).unwrap_or('-') == 'x';
|
|
|
|
if !old_x && new_x && (new_w || old_w) {
|
|
return Some(PermissionTransition::ExecuteAddedToWritable);
|
|
}
|
|
if !old_w && new_w && (new_x || old_x) {
|
|
return Some(PermissionTransition::WriteAddedToExecutable);
|
|
}
|
|
if old_x && !new_x {
|
|
return Some(PermissionTransition::ExecuteRemoved);
|
|
}
|
|
None
|
|
}
|
|
|
|
/// Takes a fresh snapshot of the target process's memory map,
|
|
/// compares against the previous snapshot, and fires alerts.
|
|
pub fn scan_process(&mut self, pid: u32) {
|
|
let current_regions = Self::parse_proc_maps(pid);
|
|
let previous = self
|
|
.previous_snapshots
|
|
.entry(pid)
|
|
.or_insert_with(Vec::new);
|
|
|
|
for curr in ¤t_regions {
|
|
for prev in previous.iter_mut() {
|
|
if curr.start_address == prev.start_address
|
|
&& curr.end_address == prev.end_address
|
|
{
|
|
if let Some(transition) =
|
|
Self::detect_suspicious_transition(
|
|
&prev.permissions,
|
|
&curr.permissions,
|
|
)
|
|
{
|
|
prev.transition_count += 1;
|
|
prev.last_transition_kind = Some(transition.clone());
|
|
prev.permissions = curr.permissions.clone();
|
|
|
|
self.active_alerts.push(ShellcodeAlert {
|
|
pid,
|
|
region: prev.clone(),
|
|
transition,
|
|
timestamp: std::time::Instant::now(),
|
|
});
|
|
} else {
|
|
prev.permissions = curr.permissions.clone();
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
*previous = current_regions;
|
|
}
|
|
|
|
/// Clears alerts older than the given duration.
|
|
pub fn expire_old_alerts(&mut self, max_age: std::time::Duration) {
|
|
let now = std::time::Instant::now();
|
|
self.active_alerts
|
|
.retain(|a| now.duration_since(a.timestamp) < max_age);
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================================
|
|
// UNIT TESTS
|
|
// ============================================================================
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_branch_evaluator_unconditional_jmp() {
|
|
// EB 05 = jmp rel8 +5
|
|
let bytes = [0xEB, 0x05];
|
|
let result = BranchEvaluator::evaluate_execution_flow(&bytes, 0x1000, 0);
|
|
assert_eq!(
|
|
result,
|
|
BranchOutcome::Unconditional { target_address: 0x1007 }
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_branch_evaluator_conditional_je_taken() {
|
|
// 74 05 = je +5, with ZF=1
|
|
let bytes = [0x74, 0x05];
|
|
let result = BranchEvaluator::evaluate_execution_flow(&bytes, 0x2000, 1 << 6);
|
|
assert_eq!(
|
|
result,
|
|
BranchOutcome::ConditionMet { target_address: 0x2007 }
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_branch_evaluator_conditional_je_not_taken() {
|
|
// 74 05 = je +5, with ZF=0
|
|
let bytes = [0x74, 0x05];
|
|
let result = BranchEvaluator::evaluate_execution_flow(&bytes, 0x2000, 0);
|
|
assert_eq!(result, BranchOutcome::ConditionNotMet);
|
|
}
|
|
|
|
#[test]
|
|
fn test_branch_evaluator_non_control_flow() {
|
|
// 48 31 C0 = xor rax, rax
|
|
let bytes = [0x48, 0x31, 0xC0];
|
|
let result = BranchEvaluator::evaluate_execution_flow(&bytes, 0x3000, 0);
|
|
assert_eq!(result, BranchOutcome::NonControlFlow);
|
|
}
|
|
|
|
#[test]
|
|
fn test_branch_evaluator_empty_buffer() {
|
|
let result = BranchEvaluator::evaluate_execution_flow(&[], 0x4000, 0);
|
|
assert_eq!(result, BranchOutcome::NonControlFlow);
|
|
}
|
|
|
|
#[test]
|
|
fn test_branch_evaluator_15_byte_clamp() {
|
|
// Should not panic even with a 20-byte buffer
|
|
let bytes = [0u8; 20];
|
|
let _ = BranchEvaluator::evaluate_execution_flow(&bytes, 0x5000, 0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_dictionary_scanner_finds_http() {
|
|
let rules = DictionaryRule::get_default_hunting_library();
|
|
let buffer = b"GET http://example.com HTTP/1.1";
|
|
let matches = MemoryScanner::scan_buffer(buffer, &rules);
|
|
assert!(matches.iter().any(|m| m.label == "NET_URI_HTTP"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_dictionary_scanner_empty_buffer() {
|
|
let rules = DictionaryRule::get_default_hunting_library();
|
|
let matches = MemoryScanner::scan_buffer(&[], &rules);
|
|
assert!(matches.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn test_signature_db_load_and_evaluate() {
|
|
let mut db = SignatureDatabaseEngine::new();
|
|
db.load_default_heuristics();
|
|
assert!(db.active_rules_count > 0);
|
|
|
|
let buffer = b"UPX0";
|
|
let hits = db.evaluate_buffer(buffer);
|
|
assert!(hits.iter().any(|h| h.contains("SIG_UPX_PACKED")));
|
|
}
|
|
|
|
#[test]
|
|
fn test_structural_diff_insertion() {
|
|
let baseline = [(0x1000u64, 5u8), (0x1005u64, 3u8)];
|
|
let current = [(0x1000u64, 5u8), (0x1005u64, 3u8), (0x2000u64, 7u8)];
|
|
let diffs = calculate_structural_diff(&baseline, ¤t);
|
|
assert!(diffs.iter().any(|d| d.status == DiffStatus::Inserted));
|
|
}
|
|
|
|
#[test]
|
|
fn test_structural_diff_modification() {
|
|
let baseline = [(0x1000u64, 5u8)];
|
|
let current = [(0x1000u64, 7u8)];
|
|
let diffs = calculate_structural_diff(&baseline, ¤t);
|
|
assert_eq!(diffs.len(), 1);
|
|
assert_eq!(diffs[0].status, DiffStatus::Modified);
|
|
}
|
|
|
|
#[test]
|
|
fn test_shannon_entropy_uniform() {
|
|
let buffer = [0u8; 256];
|
|
let entropy = crate::polymorphic_ipc::calculate_shannon_entropy(&buffer);
|
|
assert_eq!(entropy, 0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_shannon_entropy_empty() {
|
|
let entropy = crate::polymorphic_ipc::calculate_shannon_entropy(&[]);
|
|
assert_eq!(entropy, 0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_state_decay_tick() {
|
|
let mut engine = StateDecayEngine::new();
|
|
engine.mark_register_mutated("RAX", 0, 1);
|
|
assert!(engine.is_register_highlighted("rax"));
|
|
|
|
// Decay until expired
|
|
for _ in 0..DEFAULT_HIGHLIGHT_TTL {
|
|
engine.tick();
|
|
}
|
|
assert!(!engine.is_register_highlighted("RAX"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_scope_engine_function_boundary() {
|
|
let mut scope = ScopeEngine::new();
|
|
scope.register_function("main".to_string(), 0x1000, 0x1100);
|
|
|
|
scope.update_scope_for_rip(0x1050);
|
|
match scope.current_tier {
|
|
ScopeTier::Local { ref function_name, .. } => {
|
|
assert_eq!(function_name, "main");
|
|
}
|
|
_ => panic!("Expected Local scope"),
|
|
}
|
|
|
|
scope.update_scope_for_rip(0x2000);
|
|
assert_eq!(scope.current_tier, ScopeTier::Global);
|
|
}
|
|
|
|
#[test]
|
|
fn test_pixel_to_axial_roundtrip() {
|
|
let engine = HexHeatmapEngine::new();
|
|
// Center pixel should map to (0, 0)
|
|
let coord = engine.pixel_to_axial(0.0, 0.0, 18.0, 0.0, 0.0);
|
|
assert_eq!(coord.q, 0);
|
|
assert_eq!(coord.r, 0);
|
|
}
|
|
} |