use std::{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>>,
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,
..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()),
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;
}
if let Ok(assemblies) = self.assemblies.read() {
if assemblies.iter().any(|a| a.data == data) {
return; }
}
let len = data.len();
let assembly = CapturedAssembly {
data,
source,
load_method,
name,
};
if let Ok(mut assemblies) = self.assemblies.write() {
assemblies.push(assembly);
}
if let Ok(mut stats) = self.stats.write() {
stats.assembly_bytes += 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;
}
let string = CapturedString {
value,
source,
encrypted_data: None,
key: None,
};
if let Ok(mut strings) = self.strings.write() {
strings.push(string);
}
if let Ok(mut stats) = self.stats.write() {
stats.string_count += 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 += 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>,
) {
let len = data.len();
let buffer = CapturedBuffer {
data,
source,
buffer_source,
label: label.into(),
};
if let Ok(mut buffers) = self.buffers.write() {
buffers.push(buffer);
}
if let Ok(mut stats) = self.stats.write() {
stats.buffer_bytes += len;
}
}
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 += 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 += 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 - 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 += 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 - 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 += 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 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);
}
}