use std::sync::{
atomic::{AtomicU64, Ordering},
Arc, RwLock,
};
use log::{debug, warn};
use crate::{
emulation::{
capture::{
CaptureContext, CapturedAssembly, CapturedBuffer, CapturedString, FileOperation,
},
engine::{EmulationController, EmulationError, EmulationOutcome, TraceWriter},
fakeobjects::SharedFakeObjects,
loader::{LoadedImage, MappedRegionInfo},
memory::AddressSpace,
process::EmulationConfig,
runtime::RuntimeState,
EmValue, EmulationThread, StepResult, UnknownMethodBehavior,
},
metadata::token::Token,
CilObject, Error, Result,
};
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum LimitKind {
Instructions(u64),
CallDepth(usize),
HeapObjects(usize),
HeapBytes(usize),
Timeout,
}
#[derive(Clone, Debug)]
pub struct StackTraceEntry {
pub method: Token,
pub method_name: Option<String>,
pub offset: u32,
}
pub struct EmulationProcess {
pub(super) name: String,
pub(super) assembly: Option<Arc<CilObject>>,
pub(super) config: Arc<EmulationConfig>,
pub(super) address_space: Arc<AddressSpace>,
pub(super) runtime: Arc<RwLock<RuntimeState>>,
pub(super) capture: Arc<CaptureContext>,
pub(super) loaded_images: Vec<LoadedImage>,
pub(super) mapped_regions: Vec<MappedRegionInfo>,
pub(super) instruction_count: AtomicU64,
pub(super) fake_objects: SharedFakeObjects,
pub(super) trace_writer: Option<Arc<TraceWriter>>,
}
impl EmulationProcess {
pub fn name(&self) -> &str {
&self.name
}
pub fn has_assembly(&self) -> bool {
self.assembly.is_some()
}
pub fn assembly(&self) -> Option<&Arc<CilObject>> {
self.assembly.as_ref()
}
pub fn config(&self) -> &EmulationConfig {
&self.config
}
pub fn address_space(&self) -> &Arc<AddressSpace> {
&self.address_space
}
pub fn runtime(&self) -> &Arc<RwLock<RuntimeState>> {
&self.runtime
}
pub fn capture(&self) -> &Arc<CaptureContext> {
&self.capture
}
pub fn fake_objects(&self) -> &SharedFakeObjects {
&self.fake_objects
}
pub fn loaded_images(&self) -> &[LoadedImage] {
&self.loaded_images
}
pub fn loaded_image_count(&self) -> usize {
self.loaded_images.len()
}
pub fn mapped_regions(&self) -> &[MappedRegionInfo] {
&self.mapped_regions
}
pub fn mapped_region_count(&self) -> usize {
self.mapped_regions.len()
}
pub fn primary_image(&self) -> Option<&LoadedImage> {
self.loaded_images.first()
}
pub fn image_by_name(&self, name: &str) -> Option<&LoadedImage> {
self.loaded_images.iter().find(|i| i.name == name)
}
pub fn instruction_count(&self) -> u64 {
self.instruction_count.load(Ordering::Relaxed)
}
pub fn increment_instructions(&self, count: u64) -> Result<()> {
let new_count = self.instruction_count.fetch_add(count, Ordering::Relaxed) + count;
if self.config.limits.max_instructions > 0
&& new_count > self.config.limits.max_instructions
{
warn!(
"Emulation limit reached: {} instructions (limit: {})",
new_count, self.config.limits.max_instructions
);
return Err(EmulationError::InstructionLimitExceeded {
executed: new_count,
limit: self.config.limits.max_instructions,
}
.into());
}
Ok(())
}
pub fn reset_instruction_count(&self) {
self.instruction_count.store(0, Ordering::Relaxed);
}
pub fn read_memory(&self, address: u64, len: usize) -> Result<Vec<u8>> {
self.address_space.read(address, len)
}
pub fn write_memory(&self, address: u64, data: &[u8]) -> Result<()> {
self.address_space.write(address, data)
}
pub fn is_valid_address(&self, address: u64) -> bool {
self.address_space.is_valid(address)
}
pub fn get_static(&self, field_token: Token) -> Option<EmValue> {
self.address_space.get_static(field_token)
}
pub fn set_static(&self, field_token: Token, value: EmValue) {
self.address_space.set_static(field_token, value);
}
pub fn has_captures(&self) -> bool {
self.capture.has_captures()
}
pub fn captured_assemblies(&self) -> Vec<CapturedAssembly> {
self.capture.assemblies()
}
pub fn captured_strings(&self) -> Vec<CapturedString> {
self.capture.strings()
}
pub fn captured_buffers(&self) -> Vec<CapturedBuffer> {
self.capture.buffers()
}
pub fn captured_file_operations(&self) -> Vec<FileOperation> {
self.capture.file_operations()
}
pub fn clear_captures(&self) {
self.capture.clear();
}
pub fn rva_to_va(&self, rva: u32) -> Option<u64> {
self.primary_image().map(|img| img.rva_to_va(rva))
}
pub fn entry_point(&self) -> Option<u64> {
self.primary_image().and_then(LoadedImage::entry_point_va)
}
pub fn image_base(&self) -> Option<u64> {
self.primary_image().map(|img| img.base_address)
}
pub fn find_method(&self, type_name: &str, method_name: &str) -> Option<Token> {
let assembly = self.assembly.as_ref()?;
assembly
.query_types()
.filter(move |t| {
let fqn = t.fullname();
fqn.ends_with(type_name)
|| fqn == type_name
|| fqn.split('.').next_back() == Some(type_name)
})
.methods()
.name(method_name)
.find_first()
.map(|m| m.token)
}
pub fn find_cctor(&self, type_name: &str) -> Option<Token> {
self.find_method(type_name, ".cctor")
}
pub fn find_entry_point(&self) -> Option<Token> {
let assembly = self.assembly.as_ref()?;
let entry_token_value = assembly.cor20header().entry_point_token;
if entry_token_value != 0 {
Some(Token::new(entry_token_value))
} else {
None
}
}
pub fn execute_method(&self, method: Token, args: Vec<EmValue>) -> Result<EmulationOutcome> {
debug!("Executing method {}", method);
let assembly = self
.assembly
.as_ref()
.ok_or_else(|| Error::Other("No assembly loaded".into()))?;
let mut controller = EmulationController::new(
Arc::clone(&self.address_space),
Arc::clone(&self.runtime),
Arc::clone(&self.capture),
Arc::clone(&self.config),
Some(Arc::clone(assembly)),
self.fake_objects.clone(),
self.trace_writer.clone(),
);
let outcome = controller.emulate_method(method, args, Arc::clone(assembly));
if let Ok(ref result) = outcome {
let count = self.instruction_count.load(Ordering::Relaxed);
debug!(
"Execution completed: {} instructions, outcome={}",
count, result
);
}
outcome
}
pub fn emulate_until<F>(
&self,
method: Token,
args: Vec<EmValue>,
condition: F,
) -> Result<EmulationOutcome>
where
F: Fn(&StepResult, &EmulationThread) -> bool,
{
let assembly = self
.assembly
.as_ref()
.ok_or_else(|| Error::Other("No assembly loaded".into()))?;
let mut controller = EmulationController::new(
Arc::clone(&self.address_space),
Arc::clone(&self.runtime),
Arc::clone(&self.capture),
Arc::clone(&self.config),
Some(Arc::clone(assembly)),
self.fake_objects.clone(),
self.trace_writer.clone(),
);
controller.emulate_until(method, args, Arc::clone(assembly), condition)
}
pub fn set_default_behavior(&self, behavior: UnknownMethodBehavior) -> Result<()> {
self.runtime
.write()
.map_err(|e| Error::LockError(format!("runtime write lock: {e}")))?
.set_unknown_method_behavior(behavior);
Ok(())
}
#[must_use]
pub fn fork(&self) -> Self {
Self {
name: self.name.clone(),
assembly: self.assembly.clone(),
config: Arc::clone(&self.config),
address_space: Arc::new(self.address_space.fork()),
runtime: Arc::clone(&self.runtime),
capture: Arc::new(CaptureContext::with_config(self.capture.config().clone())),
loaded_images: self.loaded_images.clone(),
mapped_regions: self.mapped_regions.clone(),
instruction_count: AtomicU64::new(0),
fake_objects: self.fake_objects.clone(),
trace_writer: self.trace_writer.clone(),
}
}
#[must_use]
pub fn fork_with_captures(&self) -> Self {
let capture = CaptureContext::with_config(self.capture.config().clone());
for asm in self.capture.assemblies() {
capture.capture_assembly(asm.data, asm.source.clone(), asm.load_method, asm.name);
}
for s in self.capture.strings() {
capture.capture_string_with_details(s.value, s.source.clone(), s.encrypted_data, s.key);
}
for b in self.capture.buffers() {
capture.capture_buffer(b.data, b.source.clone(), b.buffer_source.clone(), &b.label);
}
for op in self.capture.file_operations() {
capture.capture_file_operation(
op.operation,
op.path,
op.destination,
op.data,
op.source.clone(),
op.success,
);
}
for op in self.capture.network_operations() {
capture.capture_network_operation(op);
}
Self {
name: self.name.clone(),
assembly: self.assembly.clone(),
config: Arc::clone(&self.config),
address_space: Arc::new(self.address_space.fork()),
runtime: Arc::clone(&self.runtime),
capture: Arc::new(capture),
loaded_images: self.loaded_images.clone(),
mapped_regions: self.mapped_regions.clone(),
instruction_count: AtomicU64::new(self.instruction_count.load(Ordering::Relaxed)),
fake_objects: self.fake_objects.clone(),
trace_writer: self.trace_writer.clone(),
}
}
pub fn summary(&self) -> ProcessSummary {
ProcessSummary {
name: self.name.clone(),
has_assembly: self.assembly.is_some(),
loaded_images: self.loaded_images.len(),
mapped_regions: self.mapped_regions.len(),
instruction_count: self.instruction_count(),
captured_assemblies: self.capture.assembly_count(),
captured_strings: self.capture.string_count(),
captured_buffers: self.capture.buffer_count(),
captured_file_ops: self.capture.file_operation_count(),
captured_network_ops: self.capture.network_operation_count(),
}
}
}
impl std::fmt::Debug for EmulationProcess {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("EmulationProcess")
.field("name", &self.name)
.field("has_assembly", &self.assembly.is_some())
.field("loaded_images", &self.loaded_images.len())
.field("mapped_regions", &self.mapped_regions.len())
.field("instruction_count", &self.instruction_count())
.finish_non_exhaustive()
}
}
#[derive(Clone, Debug)]
pub struct ProcessSummary {
pub name: String,
pub has_assembly: bool,
pub loaded_images: usize,
pub mapped_regions: usize,
pub instruction_count: u64,
pub captured_assemblies: usize,
pub captured_strings: usize,
pub captured_buffers: usize,
pub captured_file_ops: usize,
pub captured_network_ops: usize,
}
#[cfg(test)]
mod tests {
use crate::{
emulation::{
capture::CaptureSource,
engine::{EmulationOutcome, LimitExceeded},
process::{CaptureConfig, ProcessBuilder},
EmValue, ThreadId,
},
metadata::token::Token,
};
#[test]
fn test_process_creation() {
let process = ProcessBuilder::new().name("test").build().unwrap();
assert_eq!(process.name(), "test");
assert!(!process.has_assembly());
assert_eq!(process.instruction_count(), 0);
}
#[test]
fn test_instruction_counting() {
let process = ProcessBuilder::new().build().unwrap();
process.increment_instructions(100).unwrap();
assert_eq!(process.instruction_count(), 100);
process.increment_instructions(50).unwrap();
assert_eq!(process.instruction_count(), 150);
process.reset_instruction_count();
assert_eq!(process.instruction_count(), 0);
}
#[test]
fn test_memory_operations() {
let process = ProcessBuilder::new()
.map_data(0x10000, vec![0xDE, 0xAD, 0xBE, 0xEF], "test")
.build()
.unwrap();
let data = process.read_memory(0x10000, 4).unwrap();
assert_eq!(data, vec![0xDE, 0xAD, 0xBE, 0xEF]);
process.write_memory(0x10000, &[0x01, 0x02]).unwrap();
let data = process.read_memory(0x10000, 4).unwrap();
assert_eq!(data, vec![0x01, 0x02, 0xBE, 0xEF]);
}
#[test]
fn test_static_fields() {
let process = ProcessBuilder::new().build().unwrap();
let token = Token::new(0x04000001);
assert!(process.get_static(token).is_none());
process.set_static(token, EmValue::I32(42));
assert_eq!(process.get_static(token), Some(EmValue::I32(42)));
}
#[test]
fn test_process_summary() {
let process = ProcessBuilder::new()
.name("summary_test")
.map_data(0x10000, vec![0x01, 0x02], "region1")
.build()
.unwrap();
let summary = process.summary();
assert_eq!(summary.name, "summary_test");
assert!(!summary.has_assembly);
assert_eq!(summary.mapped_regions, 1);
assert_eq!(summary.instruction_count, 0);
}
#[test]
fn test_emulation_outcome() {
let completed = EmulationOutcome::Completed {
return_value: Some(EmValue::I32(42)),
instructions: 100,
};
match completed {
EmulationOutcome::Completed {
return_value: Some(EmValue::I32(v)),
..
} => assert_eq!(v, 42),
_ => panic!("Expected Completed"),
}
let limit = EmulationOutcome::LimitReached {
limit: LimitExceeded::Instructions {
executed: 1000000,
limit: 500000,
},
partial_state: None,
};
match limit {
EmulationOutcome::LimitReached { limit, .. } => {
assert!(matches!(limit, LimitExceeded::Instructions { .. }));
}
_ => panic!("Expected LimitReached"),
}
}
#[test]
fn test_fork_memory_isolation() {
let process = ProcessBuilder::new()
.name("original")
.map_data(0x10000, vec![1, 2, 3, 4], "data")
.build()
.unwrap();
let forked = process.fork();
assert_eq!(process.read_memory(0x10000, 4).unwrap(), vec![1, 2, 3, 4]);
assert_eq!(forked.read_memory(0x10000, 4).unwrap(), vec![1, 2, 3, 4]);
forked.write_memory(0x10000, &[0xFF, 0xFE]).unwrap();
assert_eq!(process.read_memory(0x10000, 4).unwrap(), vec![1, 2, 3, 4]);
assert_eq!(
forked.read_memory(0x10000, 4).unwrap(),
vec![0xFF, 0xFE, 3, 4]
);
}
#[test]
fn test_fork_statics_isolation() {
let process = ProcessBuilder::new().build().unwrap();
let field = Token::new(0x04000001);
process.set_static(field, EmValue::I32(42));
let forked = process.fork();
assert_eq!(process.get_static(field), Some(EmValue::I32(42)));
assert_eq!(forked.get_static(field), Some(EmValue::I32(42)));
forked.set_static(field, EmValue::I32(100));
assert_eq!(process.get_static(field), Some(EmValue::I32(42)));
assert_eq!(forked.get_static(field), Some(EmValue::I32(100)));
}
#[test]
fn test_fork_captures_isolation() {
let capture_config = CaptureConfig {
assemblies: true,
..Default::default()
};
let process = ProcessBuilder::new()
.capture(capture_config)
.build()
.unwrap();
let forked = process.fork();
forked.capture.capture_assembly_load_bytes(
vec![0x4D, 0x5A],
Token::new(0x06000001),
ThreadId::MAIN,
0,
0,
);
assert!(!process.has_captures());
assert!(forked.has_captures());
}
#[test]
fn test_fork_instruction_count_fresh() {
let process = ProcessBuilder::new().build().unwrap();
process.increment_instructions(100).unwrap();
assert_eq!(process.instruction_count(), 100);
let forked = process.fork();
assert_eq!(forked.instruction_count(), 0);
forked.increment_instructions(50).unwrap();
assert_eq!(process.instruction_count(), 100);
assert_eq!(forked.instruction_count(), 50);
}
#[test]
fn test_fork_with_captures() {
let capture_config = CaptureConfig {
assemblies: true,
strings: true,
..Default::default()
};
let process = ProcessBuilder::new()
.capture(capture_config)
.build()
.unwrap();
process.capture.capture_assembly_load_bytes(
vec![0x4D, 0x5A, 0x90, 0x00],
Token::new(0x06000001),
ThreadId::MAIN,
0,
0,
);
let source = CaptureSource::new(Token::new(0x06000001), ThreadId::MAIN, 0, 0);
process
.capture
.capture_string("test string".to_string(), source);
assert_eq!(process.captured_assemblies().len(), 1);
assert_eq!(process.captured_strings().len(), 1);
let forked = process.fork_with_captures();
assert_eq!(forked.captured_assemblies().len(), 1);
assert_eq!(forked.captured_strings().len(), 1);
forked.capture.capture_assembly_load_bytes(
vec![0x4D, 0x5A, 0x90, 0x00, 0x03],
Token::new(0x06000002),
ThreadId::MAIN,
0,
0,
);
assert_eq!(process.captured_assemblies().len(), 1);
assert_eq!(forked.captured_assemblies().len(), 2);
}
#[test]
fn test_multiple_forks() {
let process = ProcessBuilder::new()
.map_data(0x10000, vec![0u8; 16], "data")
.build()
.unwrap();
let field = Token::new(0x04000001);
process.set_static(field, EmValue::I32(1));
let fork1 = process.fork();
let fork2 = process.fork();
fork1.set_static(field, EmValue::I32(10));
fork1.write_memory(0x10000, &[0x11]).unwrap();
fork2.set_static(field, EmValue::I32(20));
fork2.write_memory(0x10000, &[0x22]).unwrap();
assert_eq!(process.get_static(field), Some(EmValue::I32(1)));
assert_eq!(fork1.get_static(field), Some(EmValue::I32(10)));
assert_eq!(fork2.get_static(field), Some(EmValue::I32(20)));
assert_eq!(process.read_memory(0x10000, 1).unwrap(), vec![0]);
assert_eq!(fork1.read_memory(0x10000, 1).unwrap(), vec![0x11]);
assert_eq!(fork2.read_memory(0x10000, 1).unwrap(), vec![0x22]);
}
}