use std::path::Path;
use std::sync::Arc;
use log::debug;
use crate::{
emulation::{
capture::CaptureContext,
engine::TraceWriter,
fakeobjects::SharedFakeObjects,
loader::{DataLoader, PeLoader, PeLoaderConfig},
memory::{AddressSpace, MemoryProtection, SharedHeap},
process::{CaptureConfig, EmulationConfig, EmulationProcess, TracingConfig},
runtime::{Hook, RuntimeState},
EmValue,
},
metadata::{tables::FieldRvaRaw, token::Token, typesystem::PointerSize},
CilObject, Result,
};
fn populate_fieldrva_statics(assembly: &CilObject, address_space: &AddressSpace) {
let Some(tables) = assembly.tables() else {
return;
};
let Some(fieldrva_table) = tables.table::<FieldRvaRaw>() else {
return;
};
let types = assembly.types();
let file = assembly.file();
let pe_data = file.data();
let ptr_size = PointerSize::from_pe(file.pe().is_64bit);
for row in fieldrva_table {
if row.rva == 0 {
continue;
}
let field_token = Token::new(row.field | 0x0400_0000);
let Some(field_type_size) = types.get_field_byte_size(&field_token, ptr_size) else {
continue;
};
let Ok(file_offset) = file.rva_to_offset(row.rva as usize) else {
continue;
};
if file_offset + field_type_size > pe_data.len() {
continue;
}
let data = &pe_data[file_offset..file_offset + field_type_size];
let value = match field_type_size {
1 => EmValue::I32(i32::from(data[0].cast_signed())),
2 => {
let bytes = [data[0], data[1]];
EmValue::I32(i32::from(i16::from_le_bytes(bytes)))
}
4 => {
let bytes = [data[0], data[1], data[2], data[3]];
EmValue::I32(i32::from_le_bytes(bytes))
}
8 => {
let bytes = [
data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7],
];
EmValue::I64(i64::from_le_bytes(bytes))
}
_ => continue,
};
address_space.statics().set(field_token, value);
}
}
#[derive(Clone)]
enum MappingOperation {
PeImage {
data: Vec<u8>,
name: String,
config: PeLoaderConfig,
},
PeFile {
path: std::path::PathBuf,
config: PeLoaderConfig,
},
DataAt {
address: u64,
data: Vec<u8>,
label: String,
protection: MemoryProtection,
},
Data {
data: Vec<u8>,
label: String,
protection: MemoryProtection,
},
File {
path: std::path::PathBuf,
address: u64,
protection: MemoryProtection,
},
Zeroed {
address: u64,
size: usize,
label: String,
protection: MemoryProtection,
},
}
pub struct ProcessBuilder {
assembly: Option<Arc<CilObject>>,
config: EmulationConfig,
capture_config: CaptureConfig,
hooks: Vec<Hook>,
mappings: Vec<MappingOperation>,
register_defaults: bool,
name: Option<String>,
}
impl ProcessBuilder {
#[must_use]
pub fn new() -> Self {
Self {
assembly: None,
config: EmulationConfig::default(),
capture_config: CaptureConfig::default(),
hooks: Vec::new(),
mappings: Vec::new(),
register_defaults: true,
name: None,
}
}
#[must_use]
pub fn assembly(mut self, assembly: CilObject) -> Self {
self.assembly = Some(Arc::new(assembly));
self
}
#[must_use]
pub fn assembly_arc(mut self, assembly: Arc<CilObject>) -> Self {
self.assembly = Some(assembly);
self
}
#[must_use]
pub fn config(mut self, config: EmulationConfig) -> Self {
self.config = config;
self
}
#[must_use]
pub fn pointer_size(mut self, ptr_size: PointerSize) -> Self {
self.config.pointer_size = ptr_size;
self
}
#[must_use]
pub fn capture(mut self, config: CaptureConfig) -> Self {
self.capture_config = config;
self
}
#[must_use]
pub fn name(mut self, name: impl Into<String>) -> Self {
self.name = Some(name.into());
self
}
#[must_use]
pub fn for_extraction(mut self) -> Self {
self.config = EmulationConfig::extraction();
self.capture_config.assemblies = true;
self
}
#[must_use]
pub fn for_analysis(mut self) -> Self {
self.config = EmulationConfig::analysis();
self
}
#[must_use]
pub fn for_full_emulation(mut self) -> Self {
self.config = EmulationConfig::full();
self
}
#[must_use]
pub fn for_minimal(mut self) -> Self {
self.config = EmulationConfig::minimal();
self
}
#[must_use]
pub fn with_max_instructions(mut self, max: u64) -> Self {
self.config.limits.max_instructions = max;
self
}
#[must_use]
pub fn with_max_call_depth(mut self, max: usize) -> Self {
self.config.limits.max_call_depth = max;
self
}
#[must_use]
pub fn with_max_heap_bytes(mut self, max: usize) -> Self {
self.config.limits.max_heap_bytes = max;
self
}
#[must_use]
pub fn with_timeout_ms(mut self, ms: u64) -> Self {
self.config.limits.timeout_ms = ms;
self
}
#[must_use]
pub fn with_tracing(mut self, tracing: TracingConfig) -> Self {
self.config.tracing = tracing;
self
}
#[must_use]
pub fn map_pe_image(mut self, pe_bytes: &[u8], name: impl Into<String>) -> Self {
self.mappings.push(MappingOperation::PeImage {
data: pe_bytes.to_vec(),
name: name.into(),
config: PeLoaderConfig::default(),
});
self
}
#[must_use]
pub fn map_pe_image_with_config(
mut self,
pe_bytes: &[u8],
name: impl Into<String>,
config: PeLoaderConfig,
) -> Self {
self.mappings.push(MappingOperation::PeImage {
data: pe_bytes.to_vec(),
name: name.into(),
config,
});
self
}
#[must_use]
pub fn map_pe_file(mut self, path: impl AsRef<Path>) -> Self {
self.mappings.push(MappingOperation::PeFile {
path: path.as_ref().to_path_buf(),
config: PeLoaderConfig::default(),
});
self
}
#[must_use]
pub fn map_data(mut self, address: u64, data: Vec<u8>, label: impl Into<String>) -> Self {
self.mappings.push(MappingOperation::DataAt {
address,
data,
label: label.into(),
protection: MemoryProtection::READ | MemoryProtection::WRITE,
});
self
}
#[must_use]
pub fn map_data_with_protection(
mut self,
address: u64,
data: Vec<u8>,
label: impl Into<String>,
protection: MemoryProtection,
) -> Self {
self.mappings.push(MappingOperation::DataAt {
address,
data,
label: label.into(),
protection,
});
self
}
#[must_use]
pub fn map_file(mut self, address: u64, path: impl AsRef<Path>) -> Self {
self.mappings.push(MappingOperation::File {
path: path.as_ref().to_path_buf(),
address,
protection: MemoryProtection::READ,
});
self
}
#[must_use]
pub fn map_zeroed(mut self, address: u64, size: usize, label: impl Into<String>) -> Self {
self.mappings.push(MappingOperation::Zeroed {
address,
size,
label: label.into(),
protection: MemoryProtection::READ | MemoryProtection::WRITE,
});
self
}
#[must_use]
pub fn capture_assemblies(mut self) -> Self {
self.capture_config.assemblies = true;
self
}
#[must_use]
pub fn capture_strings(mut self) -> Self {
self.capture_config.strings = true;
self
}
#[must_use]
pub fn capture_file_operations(mut self) -> Self {
self.capture_config.file_operations = true;
self
}
#[must_use]
pub fn capture_network_operations(mut self) -> Self {
self.capture_config.network_operations = true;
self
}
#[must_use]
pub fn capture_memory_region(mut self, start: u64, end: u64) -> Self {
self.capture_config.memory_regions.push(start..end);
self
}
#[must_use]
pub fn hook(mut self, hook: Hook) -> Self {
self.hooks.push(hook);
self
}
#[must_use]
pub fn no_default_stubs(mut self) -> Self {
self.register_defaults = false;
self
}
pub fn build(self) -> Result<EmulationProcess> {
debug!(
"Creating emulation process: instruction_limit={}, call_depth={}",
self.config.limits.max_instructions, self.config.limits.max_call_depth
);
let heap_size = self.config.memory.max_heap_size;
let heap = SharedHeap::new(heap_size);
let fake_objects = SharedFakeObjects::new(heap.heap());
let address_space = Arc::new(AddressSpace::with_heap(heap));
let mut config = self.config.clone();
if !self.register_defaults {
config.stubs.bcl_stubs = false;
config.stubs.pinvoke_stubs = false;
}
if let Some(ref assembly) = self.assembly {
config.pointer_size = if assembly.file().pe().is_64bit {
PointerSize::Bit64
} else {
PointerSize::Bit32
};
}
let config_arc = Arc::new(config);
let mut runtime = RuntimeState::with_config(config_arc.clone());
for hook in self.hooks {
runtime.register_hook(hook);
}
let capture = Arc::new(CaptureContext::with_config(self.capture_config));
let mut loaded_images = Vec::new();
let mut mapped_regions = Vec::new();
if let Some(ref assembly) = self.assembly {
let loader = PeLoader::default();
let pe_bytes = assembly.file().data();
let name = assembly
.assembly()
.map_or_else(|| "primary".to_string(), |a| a.name.clone());
if let Ok(image) = loader.load(pe_bytes, &address_space, name) {
loaded_images.push(image);
}
populate_fieldrva_statics(assembly, &address_space);
}
for mapping in self.mappings {
match mapping {
MappingOperation::PeImage { data, name, config } => {
let loader = PeLoader::with_config(config);
let image = loader.load(&data, &address_space, name)?;
loaded_images.push(image);
}
MappingOperation::PeFile { path, config } => {
let loader = PeLoader::with_config(config);
let image = loader.load_file(&path, &address_space)?;
loaded_images.push(image);
}
MappingOperation::DataAt {
address,
data,
label,
protection,
} => {
let info =
DataLoader::map_at(&address_space, address, &data, label, protection)?;
mapped_regions.push(info);
}
MappingOperation::Data {
data,
label,
protection,
} => {
let info = DataLoader::map(&address_space, &data, label, protection)?;
mapped_regions.push(info);
}
MappingOperation::File {
path,
address,
protection,
} => {
let info = DataLoader::map_file(&address_space, &path, address, protection)?;
mapped_regions.push(info);
}
MappingOperation::Zeroed {
address,
size,
label,
protection,
} => {
let info =
DataLoader::map_zeroed(&address_space, address, size, label, protection)?;
mapped_regions.push(info);
}
}
}
let name = self.name.unwrap_or_else(|| {
self.assembly
.as_ref()
.and_then(|a| a.assembly())
.map_or_else(|| "emulation".to_string(), |asm| asm.name.clone())
});
let trace_writer = if config_arc.tracing.is_enabled() {
let context = config_arc.tracing.context_prefix.clone();
if let Some(ref path) = config_arc.tracing.output_path {
let writer = TraceWriter::new_file(path, context).map_err(|e| {
crate::Error::TracingError(format!(
"Failed to create trace file {}: {e}",
path.display()
))
})?;
Some(Arc::new(writer))
} else {
Some(Arc::new(TraceWriter::new_memory(
config_arc.tracing.max_trace_entries,
context,
)))
}
} else {
None
};
let hook_count = runtime.hooks().len();
let process = EmulationProcess {
name,
assembly: self.assembly,
config: config_arc,
address_space,
runtime: Arc::new(std::sync::RwLock::new(runtime)),
capture,
loaded_images,
mapped_regions,
instruction_count: std::sync::atomic::AtomicU64::new(0),
fake_objects,
trace_writer,
};
debug!("Emulation process ready: {} hooks registered", hook_count);
Ok(process)
}
}
impl Default for ProcessBuilder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_builder_defaults() {
let builder = ProcessBuilder::new();
let process = builder.build().unwrap();
assert!(!process.has_assembly());
assert_eq!(process.loaded_image_count(), 0);
}
#[test]
fn test_builder_config_presets() {
let process = ProcessBuilder::new().for_extraction().build().unwrap();
assert!(process.capture().config().assemblies);
}
#[test]
fn test_builder_capture_config() {
let process = ProcessBuilder::new()
.capture_assemblies()
.capture_strings()
.capture_memory_region(0x400000, 0x410000)
.build()
.unwrap();
let config = process.capture().config();
assert!(config.assemblies);
assert!(config.strings);
assert_eq!(config.memory_regions.len(), 1);
}
#[test]
fn test_builder_map_data() {
let process = ProcessBuilder::new()
.map_data(0x10000, vec![0x01, 0x02, 0x03, 0x04], "test_data")
.build()
.unwrap();
assert_eq!(process.mapped_region_count(), 1);
let data = process.address_space().read(0x10000, 4).unwrap();
assert_eq!(data, vec![0x01, 0x02, 0x03, 0x04]);
}
#[test]
fn test_builder_name() {
let process = ProcessBuilder::new().name("test_process").build().unwrap();
assert_eq!(process.name(), "test_process");
}
#[test]
fn test_builder_no_default_hooks() {
let process_with_hooks = ProcessBuilder::new().build().unwrap();
let runtime_with = process_with_hooks.runtime.read().unwrap();
let count_with = runtime_with.hooks().len();
let process_no_hooks = ProcessBuilder::new().no_default_stubs().build().unwrap();
let runtime_no = process_no_hooks.runtime.read().unwrap();
let count_no = runtime_no.hooks().len();
assert!(count_with > 0, "Expected default hooks to be registered");
assert_eq!(
count_no, 0,
"Expected no hooks when no_default_stubs() is called"
);
}
}