use std::{
collections::{hash_map::DefaultHasher, HashSet as StdHashSet},
hash::{Hash, Hasher},
ops::Range,
sync::RwLock,
};
use crate::{
emulation::{
capture::types::{
AssemblyLoadMethod, BufferSource, CaptureSource, CapturedAssembly, CapturedBuffer,
CapturedMethodReturn, CapturedString, FileOpKind, FileOperation, MemoryRegionSnapshot,
MemorySnapshot, NetworkOperation,
},
memory::AddressSpace,
process::CaptureConfig,
ThreadId,
},
metadata::token::Token,
};
#[derive(Debug)]
pub struct CaptureContext {
config: CaptureConfig,
assemblies: RwLock<Vec<CapturedAssembly>>,
strings: RwLock<Vec<CapturedString>>,
buffers: RwLock<Vec<CapturedBuffer>>,
retained_bytes: RwLock<usize>,
assembly_hashes: RwLock<StdHashSet<u64>>,
method_returns: RwLock<Vec<CapturedMethodReturn>>,
file_operations: RwLock<Vec<FileOperation>>,
network_operations: RwLock<Vec<NetworkOperation>>,
snapshots: RwLock<Vec<MemorySnapshot>>,
stats: RwLock<CaptureStats>,
}
#[derive(Debug, Default, Clone)]
pub struct CaptureStats {
pub assembly_bytes: usize,
pub string_count: usize,
pub buffer_bytes: usize,
pub file_op_count: usize,
pub network_op_count: usize,
pub snapshot_count: usize,
}
impl CaptureContext {
#[must_use]
pub fn new() -> Self {
Self::with_config(CaptureConfig {
assemblies: true,
strings: true,
file_operations: true,
network_operations: true,
buffers: true,
..Default::default()
})
}
#[must_use]
pub fn with_config(config: CaptureConfig) -> Self {
Self {
config,
assemblies: RwLock::new(Vec::new()),
strings: RwLock::new(Vec::new()),
buffers: RwLock::new(Vec::new()),
retained_bytes: RwLock::new(0),
assembly_hashes: RwLock::new(StdHashSet::new()),
method_returns: RwLock::new(Vec::new()),
file_operations: RwLock::new(Vec::new()),
network_operations: RwLock::new(Vec::new()),
snapshots: RwLock::new(Vec::new()),
stats: RwLock::new(CaptureStats::default()),
}
}
#[must_use]
pub fn assemblies_only() -> Self {
Self::with_config(CaptureConfig {
assemblies: true,
..Default::default()
})
}
#[must_use]
pub fn strings_only() -> Self {
Self::with_config(CaptureConfig {
strings: true,
..Default::default()
})
}
pub fn config(&self) -> &CaptureConfig {
&self.config
}
pub fn capture_assembly(
&self,
data: Vec<u8>,
source: CaptureSource,
load_method: AssemblyLoadMethod,
name: Option<String>,
) {
if !self.config.assemblies {
return;
}
let mut hasher = DefaultHasher::new();
data.hash(&mut hasher);
let digest = hasher.finish();
if let Ok(mut hashes) = self.assembly_hashes.write() {
if !hashes.insert(digest) {
return; }
}
let len = data.len();
if !self.admit(len) {
return;
}
let assembly = CapturedAssembly {
data,
source,
load_method,
name,
};
if let Ok(mut assemblies) = self.assemblies.write() {
if assemblies.len() >= self.config.max_items {
return;
}
assemblies.push(assembly);
}
if let Ok(mut stats) = self.stats.write() {
stats.assembly_bytes = stats.assembly_bytes.saturating_add(len);
}
}
pub fn capture_assembly_load_bytes(
&self,
data: Vec<u8>,
method: Token,
thread_id: ThreadId,
offset: u32,
instruction_count: u64,
) {
let source = CaptureSource::new(method, thread_id, offset, instruction_count);
self.capture_assembly(data, source, AssemblyLoadMethod::LoadBytes, None);
}
pub fn assemblies(&self) -> Vec<CapturedAssembly> {
self.assemblies
.read()
.map(|a| a.clone())
.unwrap_or_default()
}
pub fn assembly_count(&self) -> usize {
self.assemblies.read().map_or(0, |a| a.len())
}
pub fn capture_string(&self, value: String, source: CaptureSource) {
if !self.config.strings {
return;
}
if !self.admit(value.len()) {
return;
}
let string = CapturedString {
value,
source,
encrypted_data: None,
key: None,
};
if let Ok(mut strings) = self.strings.write() {
if strings.len() >= self.config.max_items {
return;
}
strings.push(string);
}
if let Ok(mut stats) = self.stats.write() {
stats.string_count = stats.string_count.saturating_add(1);
}
}
pub fn capture_string_with_details(
&self,
value: String,
source: CaptureSource,
encrypted_data: Option<Vec<u8>>,
key: Option<Vec<u8>>,
) {
if !self.config.strings {
return;
}
let string = CapturedString {
value,
source,
encrypted_data,
key,
};
if let Ok(mut strings) = self.strings.write() {
strings.push(string);
}
if let Ok(mut stats) = self.stats.write() {
stats.string_count = stats.string_count.saturating_add(1);
}
}
pub fn strings(&self) -> Vec<CapturedString> {
self.strings
.read()
.map_or_else(|_| Vec::new(), |s| s.clone())
}
pub fn string_count(&self) -> usize {
self.strings.read().map_or(0, |s| s.len())
}
pub fn capture_buffer(
&self,
data: Vec<u8>,
source: CaptureSource,
buffer_source: BufferSource,
label: impl Into<String>,
) {
if !self.config.buffers {
return;
}
let len = data.len();
if !self.admit(len) {
return;
}
let buffer = CapturedBuffer {
data,
source,
buffer_source,
label: label.into(),
};
if let Ok(mut buffers) = self.buffers.write() {
if buffers.len() >= self.config.max_items {
return;
}
buffers.push(buffer);
}
if let Ok(mut stats) = self.stats.write() {
stats.buffer_bytes = stats.buffer_bytes.saturating_add(len);
}
}
fn admit(&self, len: usize) -> bool {
let Ok(mut retained) = self.retained_bytes.write() else {
return false;
};
let next = retained.saturating_add(len);
if next > self.config.max_total_bytes {
return false;
}
*retained = next;
true
}
pub fn buffers(&self) -> Vec<CapturedBuffer> {
self.buffers
.read()
.map_or_else(|_| Vec::new(), |b| b.clone())
}
pub fn buffer_count(&self) -> usize {
self.buffers.read().map_or(0, |b| b.len())
}
pub fn capture_method_return(&self, capture: CapturedMethodReturn) {
if let Ok(mut returns) = self.method_returns.write() {
returns.push(capture);
}
}
pub fn method_returns(&self) -> Vec<CapturedMethodReturn> {
self.method_returns
.read()
.map(|r| r.clone())
.unwrap_or_default()
}
pub fn capture_file_operation(
&self,
operation: FileOpKind,
path: String,
destination: Option<String>,
data: Option<Vec<u8>>,
source: CaptureSource,
success: bool,
) {
if !self.config.file_operations {
return;
}
let op = FileOperation {
operation,
path,
destination,
data,
source,
success,
};
if let Ok(mut ops) = self.file_operations.write() {
ops.push(op);
}
if let Ok(mut stats) = self.stats.write() {
stats.file_op_count = stats.file_op_count.saturating_add(1);
}
}
pub fn file_operations(&self) -> Vec<FileOperation> {
self.file_operations
.read()
.map(|o| o.clone())
.unwrap_or_default()
}
pub fn file_operation_count(&self) -> usize {
self.file_operations.read().map_or(0, |o| o.len())
}
pub fn capture_network_operation(&self, operation: NetworkOperation) {
if !self.config.network_operations {
return;
}
if let Ok(mut ops) = self.network_operations.write() {
ops.push(operation);
}
if let Ok(mut stats) = self.stats.write() {
stats.network_op_count = stats.network_op_count.saturating_add(1);
}
}
pub fn network_operations(&self) -> Vec<NetworkOperation> {
self.network_operations
.read()
.map_or_else(|_| Vec::new(), |o| o.clone())
}
pub fn network_operation_count(&self) -> usize {
self.network_operations.read().map_or(0, |o| o.len())
}
pub fn snapshot_memory(
&self,
address_space: &AddressSpace,
label: impl Into<String>,
thread_id: ThreadId,
instruction_count: u64,
) {
let regions: Vec<MemoryRegionSnapshot> = self
.config
.memory_regions
.iter()
.filter_map(|range| {
let base = range.start;
#[allow(clippy::cast_possible_truncation)]
let size = range.end.saturating_sub(range.start) as usize;
address_space
.read(base, size)
.ok()
.map(|data| MemoryRegionSnapshot {
base,
data,
label: format!("0x{:X}-0x{:X}", range.start, range.end),
})
})
.collect();
if regions.is_empty() {
return;
}
let snapshot = MemorySnapshot {
label: label.into(),
regions,
instruction_count,
thread_id,
};
if let Ok(mut snapshots) = self.snapshots.write() {
snapshots.push(snapshot);
}
if let Ok(mut stats) = self.stats.write() {
stats.snapshot_count = stats.snapshot_count.saturating_add(1);
}
}
pub fn snapshot_regions(
&self,
address_space: &AddressSpace,
regions: &[Range<u64>],
label: impl Into<String>,
thread_id: ThreadId,
instruction_count: u64,
) {
let region_snapshots: Vec<MemoryRegionSnapshot> = regions
.iter()
.filter_map(|range| {
let base = range.start;
#[allow(clippy::cast_possible_truncation)]
let size = range.end.saturating_sub(range.start) as usize;
address_space
.read(base, size)
.ok()
.map(|data| MemoryRegionSnapshot {
base,
data,
label: format!("0x{:X}-0x{:X}", range.start, range.end),
})
})
.collect();
if region_snapshots.is_empty() {
return;
}
let snapshot = MemorySnapshot {
label: label.into(),
regions: region_snapshots,
instruction_count,
thread_id,
};
if let Ok(mut snapshots) = self.snapshots.write() {
snapshots.push(snapshot);
}
if let Ok(mut stats) = self.stats.write() {
stats.snapshot_count = stats.snapshot_count.saturating_add(1);
}
}
pub fn snapshots(&self) -> Vec<MemorySnapshot> {
self.snapshots
.read()
.map_or_else(|_| Vec::new(), |s| s.clone())
}
pub fn snapshot_count(&self) -> usize {
self.snapshots.read().map_or(0, |s| s.len())
}
pub fn stats(&self) -> CaptureStats {
self.stats
.read()
.ok()
.map(|g| g.clone())
.unwrap_or_default()
}
pub fn has_captures(&self) -> bool {
self.assembly_count() > 0
|| self.string_count() > 0
|| self.buffer_count() > 0
|| self.file_operation_count() > 0
|| self.network_operation_count() > 0
}
pub fn clear(&self) {
if let Ok(mut assemblies) = self.assemblies.write() {
assemblies.clear();
}
if let Ok(mut strings) = self.strings.write() {
strings.clear();
}
if let Ok(mut buffers) = self.buffers.write() {
buffers.clear();
}
if let Ok(mut returns) = self.method_returns.write() {
returns.clear();
}
if let Ok(mut ops) = self.file_operations.write() {
ops.clear();
}
if let Ok(mut ops) = self.network_operations.write() {
ops.clear();
}
if let Ok(mut snapshots) = self.snapshots.write() {
snapshots.clear();
}
if let Ok(mut stats) = self.stats.write() {
*stats = CaptureStats::default();
}
}
}
impl Default for CaptureContext {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_config_captures_no_buffers() {
let ctx = CaptureContext::with_config(CaptureConfig::default());
ctx.capture_buffer(
vec![0u8; 64],
CaptureSource::new(Token::new(0x0600_0001), ThreadId::new(1), 0, 0),
BufferSource::MarshalCopy { address: 0x1000 },
"test",
);
assert_eq!(ctx.buffer_count(), 0, "capture was not enabled");
}
#[test]
fn buffer_capture_stops_at_the_item_ceiling() {
let ctx = CaptureContext::with_config(CaptureConfig {
buffers: true,
max_items: 10,
..Default::default()
});
for _ in 0..1000 {
ctx.capture_buffer(
vec![0u8; 8],
CaptureSource::new(Token::new(0x0600_0001), ThreadId::new(1), 0, 0),
BufferSource::MarshalCopy { address: 0x1000 },
"test",
);
}
assert_eq!(ctx.buffer_count(), 10);
}
#[test]
fn buffer_capture_stops_at_the_byte_ceiling() {
let ctx = CaptureContext::with_config(CaptureConfig {
buffers: true,
max_items: usize::MAX,
max_total_bytes: 4096,
..Default::default()
});
for _ in 0..100 {
ctx.capture_buffer(
vec![0u8; 1024],
CaptureSource::new(Token::new(0x0600_0001), ThreadId::new(1), 0, 0),
BufferSource::MarshalCopy { address: 0x1000 },
"test",
);
}
assert_eq!(
ctx.buffer_count(),
4,
"only what fits in the byte budget is retained"
);
}
#[test]
fn test_capture_context_creation() {
let ctx = CaptureContext::new();
assert!(!ctx.has_captures());
assert_eq!(ctx.assembly_count(), 0);
assert_eq!(ctx.string_count(), 0);
}
#[test]
fn test_capture_assembly() {
let ctx = CaptureContext::new();
ctx.capture_assembly_load_bytes(
vec![0x4D, 0x5A, 0x90, 0x00],
Token::new(0x06000001),
ThreadId::MAIN,
0x10,
100,
);
assert_eq!(ctx.assembly_count(), 1);
assert!(ctx.has_captures());
let assemblies = ctx.assemblies();
assert_eq!(assemblies.len(), 1);
assert_eq!(assemblies[0].data, vec![0x4D, 0x5A, 0x90, 0x00]);
assert_eq!(assemblies[0].load_method, AssemblyLoadMethod::LoadBytes);
}
#[test]
fn test_capture_string() {
let ctx = CaptureContext::new();
let source = CaptureSource::new(Token::new(0x06000001), ThreadId::MAIN, 0x20, 200);
ctx.capture_string("decrypted secret".to_string(), source);
assert_eq!(ctx.string_count(), 1);
let strings = ctx.strings();
assert_eq!(strings[0].value, "decrypted secret");
}
#[test]
fn test_capture_disabled_by_config() {
let ctx = CaptureContext::with_config(CaptureConfig {
assemblies: false,
strings: false,
..Default::default()
});
ctx.capture_assembly_load_bytes(
vec![0x4D, 0x5A],
Token::new(0x06000001),
ThreadId::MAIN,
0,
0,
);
let source = CaptureSource::new(Token::new(0x06000001), ThreadId::MAIN, 0, 0);
ctx.capture_string("test".to_string(), source);
assert_eq!(ctx.assembly_count(), 0);
assert_eq!(ctx.string_count(), 0);
}
#[test]
fn test_capture_buffer() {
let ctx = CaptureContext::new();
let source = CaptureSource::new(Token::new(0x06000001), ThreadId::MAIN, 0x30, 300);
ctx.capture_buffer(
vec![0x01, 0x02, 0x03, 0x04],
source,
BufferSource::MarshalCopy { address: 0x10000 },
"decrypted data",
);
assert_eq!(ctx.buffer_count(), 1);
let buffers = ctx.buffers();
assert_eq!(buffers[0].data, vec![0x01, 0x02, 0x03, 0x04]);
assert_eq!(buffers[0].label, "decrypted data");
}
#[test]
fn test_capture_file_operation() {
let ctx = CaptureContext::new();
let source = CaptureSource::new(Token::new(0x06000001), ThreadId::MAIN, 0, 0);
ctx.capture_file_operation(
FileOpKind::Write,
"C:\\temp\\malware.exe".to_string(),
None,
Some(vec![0x4D, 0x5A]),
source,
true,
);
assert_eq!(ctx.file_operation_count(), 1);
let ops = ctx.file_operations();
assert_eq!(ops[0].operation, FileOpKind::Write);
assert_eq!(ops[0].path, "C:\\temp\\malware.exe");
}
#[test]
fn test_capture_stats() {
let ctx = CaptureContext::new();
ctx.capture_assembly_load_bytes(
vec![0x4D, 0x5A, 0x90, 0x00, 0x03, 0x00],
Token::new(0x06000001),
ThreadId::MAIN,
0,
0,
);
let source = CaptureSource::new(Token::new(0x06000001), ThreadId::MAIN, 0, 0);
ctx.capture_string("test".to_string(), source.clone());
ctx.capture_string("test2".to_string(), source);
let stats = ctx.stats();
assert_eq!(stats.assembly_bytes, 6);
assert_eq!(stats.string_count, 2);
}
#[test]
fn test_clear() {
let ctx = CaptureContext::new();
ctx.capture_assembly_load_bytes(
vec![0x4D, 0x5A],
Token::new(0x06000001),
ThreadId::MAIN,
0,
0,
);
assert!(ctx.has_captures());
ctx.clear();
assert!(!ctx.has_captures());
assert_eq!(ctx.assembly_count(), 0);
}
#[test]
fn test_assemblies_only_preset() {
let ctx = CaptureContext::assemblies_only();
ctx.capture_assembly_load_bytes(
vec![0x4D, 0x5A],
Token::new(0x06000001),
ThreadId::MAIN,
0,
0,
);
let source = CaptureSource::new(Token::new(0x06000001), ThreadId::MAIN, 0, 0);
ctx.capture_string("test".to_string(), source);
assert_eq!(ctx.assembly_count(), 1);
assert_eq!(ctx.string_count(), 0);
}
}