1use std::collections::HashSet;
2use std::fmt;
3use std::path::{Path, PathBuf};
4use std::sync::{Mutex, OnceLock};
5use std::thread;
6use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
7
8use crate::pe::{DLL_PROCESS_ATTACH, ImportSymbol, Pe, Section, u16_at, u32_at};
9use crate::{DecantConfig, InjectError, InjectionDomain, Method};
10
11const DEFAULT_HOOK_MODULE: &str = "kernel32.dll";
12const DEFAULT_HOOK_FUNCTION: &str = "Sleep";
13const STAGE_CAVE_SIZE: usize = 0x400;
14const STAGE_RESULT_OFFSET: u64 = 0x20;
15const STAGE_STUB_OFFSET: u64 = 0x100;
16const STAGE_SCRATCH_OFFSET: u64 = 0x300;
17const STAGE_UNWIND_OFFSET: u64 = 0x3E0;
18const STAGE_SCRATCH_SIZE: usize = (STAGE_UNWIND_OFFSET - STAGE_SCRATCH_OFFSET) as usize;
19const GUEST_PAGE_SIZE: usize = 0x1000;
20const SCAN_CHUNK: u64 = 0x10000;
21const SPOOFED_RETURN_SCAN_LIMIT: u64 = 0x40000;
22const SPOOFED_CALL_LANDING_DELTA: u64 = 46;
23const MEM_COMMIT_RESERVE: u64 = 0x0000_1000 | 0x0000_2000;
24const PAGE_NOACCESS: u64 = 0x01;
25const PAGE_READONLY: u64 = 0x02;
26const PAGE_READWRITE: u64 = 0x04;
27const PAGE_EXECUTE: u64 = 0x10;
28const PAGE_EXECUTE_READ: u64 = 0x20;
29const PAGE_EXECUTE_READWRITE: u64 = 0x40;
30const CFG_CALL_TARGET_VALID: u64 = 0x1;
31const IMAGE_SCN_MEM_EXECUTE: u32 = 0x2000_0000;
32const IMAGE_SCN_MEM_READ: u32 = 0x4000_0000;
33const IMAGE_SCN_MEM_WRITE: u32 = 0x8000_0000;
34const RESULT_RUNNING: u64 = 1;
35const RESULT_STATE: u64 = 2;
36const RESULT_BLOCK_SIZE: usize = 16;
37const OLD_PROTECT_RESULT_OFFSET: u64 = 12;
38const MAX_EXPORT_FORWARD_DEPTH: usize = 8;
39const EXPORT_HEADER_RETRIES: usize = 20;
40const EXPORT_HEADER_RETRY_DELAY: Duration = Duration::from_millis(25);
41const FRAMED_STUB_STACK_ALLOC: u8 = 0x68;
42
43static ACTIVE_GUEST_INJECTIONS: OnceLock<Mutex<HashSet<u32>>> = OnceLock::new();
44
45fn guest_timeout_ms() -> u32 {
46 5000
47}
48
49fn default_hook_module() -> String {
50 DEFAULT_HOOK_MODULE.into()
51}
52
53fn default_hook_function() -> String {
54 DEFAULT_HOOK_FUNCTION.into()
55}
56
57#[derive(Clone, Copy, PartialEq, Eq, Debug)]
58pub enum GuestPortability {
59 GuestPublicExports,
60 GuestLoaderInternals,
61 GuestThreadContext,
62 ExternalAgent,
63}
64
65impl GuestPortability {
66 pub fn label(self) -> &'static str {
67 match self {
68 GuestPortability::GuestPublicExports => "guest-public-exports",
69 GuestPortability::GuestLoaderInternals => "guest-loader-internals",
70 GuestPortability::GuestThreadContext => "guest-thread-context",
71 GuestPortability::ExternalAgent => "external-agent",
72 }
73 }
74}
75
76#[derive(Clone, Copy, PartialEq, Eq, Debug, Default, serde::Deserialize)]
77#[serde(rename_all = "kebab-case")]
78pub enum GuestAllocationMethod {
79 #[default]
80 VirtualAlloc,
81 ExistingRegion,
82 PageTableMap,
83}
84
85impl GuestAllocationMethod {
86 pub fn label(self) -> &'static str {
87 match self {
88 GuestAllocationMethod::VirtualAlloc => "virtual-alloc",
89 GuestAllocationMethod::ExistingRegion => "existing-region",
90 GuestAllocationMethod::PageTableMap => "page-table-map",
91 }
92 }
93}
94
95#[derive(Clone, Copy, PartialEq, Eq, Debug, Default, serde::Deserialize)]
96#[serde(rename_all = "kebab-case")]
97pub enum GuestExecutionMethod {
98 #[default]
99 IatHook,
100 RemoteThread,
101 ThreadHijack,
102 Apc,
103 ExternalAgent,
104 None,
105}
106
107impl GuestExecutionMethod {
108 pub fn label(self) -> &'static str {
109 match self {
110 GuestExecutionMethod::IatHook => "iat-hook",
111 GuestExecutionMethod::RemoteThread => "remote-thread",
112 GuestExecutionMethod::ThreadHijack => "thread-hijack",
113 GuestExecutionMethod::Apc => "apc",
114 GuestExecutionMethod::ExternalAgent => "external-agent",
115 GuestExecutionMethod::None => "none",
116 }
117 }
118}
119
120#[derive(Clone, Copy, PartialEq, Eq, Debug, Default, serde::Deserialize)]
121#[serde(rename_all = "kebab-case")]
122pub enum GuestDependencyPolicy {
123 #[default]
124 RequireLoaded,
125 LoadWithGuestLoader,
126}
127
128impl GuestDependencyPolicy {
129 pub fn label(self) -> &'static str {
130 match self {
131 GuestDependencyPolicy::RequireLoaded => "require-loaded",
132 GuestDependencyPolicy::LoadWithGuestLoader => "load-with-guest-loader",
133 }
134 }
135}
136
137#[derive(Clone, Copy, PartialEq, Eq, Debug, Default, serde::Deserialize)]
138#[serde(rename_all = "kebab-case")]
139pub enum GuestTlsMode {
140 #[default]
141 CallbacksOnly,
142 Skip,
143 RequireStatic,
144}
145
146impl GuestTlsMode {
147 pub fn label(self) -> &'static str {
148 match self {
149 GuestTlsMode::CallbacksOnly => "callbacks-only",
150 GuestTlsMode::Skip => "skip",
151 GuestTlsMode::RequireStatic => "require-static",
152 }
153 }
154}
155
156#[derive(Clone, Copy, PartialEq, Eq, Debug, Default, serde::Deserialize)]
157#[serde(rename_all = "kebab-case")]
158pub enum GuestFinalProtections {
159 Rwx,
160 #[default]
161 Section,
162}
163
164impl GuestFinalProtections {
165 pub fn label(self) -> &'static str {
166 match self {
167 GuestFinalProtections::Rwx => "rwx",
168 GuestFinalProtections::Section => "section",
169 }
170 }
171}
172
173#[derive(Clone, Copy, PartialEq, Eq, Debug, Default, serde::Deserialize)]
174#[serde(rename_all = "kebab-case")]
175pub enum GuestLoaderMetadataPolicy {
176 #[default]
177 RejectUnsupported,
178 BestEffort,
179 AllowUnsupported,
180}
181
182impl GuestLoaderMetadataPolicy {
183 pub fn label(self) -> &'static str {
184 match self {
185 GuestLoaderMetadataPolicy::RejectUnsupported => "reject-unsupported",
186 GuestLoaderMetadataPolicy::BestEffort => "best-effort",
187 GuestLoaderMetadataPolicy::AllowUnsupported => "allow-unsupported",
188 }
189 }
190
191 fn allows_unregistered_metadata(self) -> bool {
192 matches!(
193 self,
194 GuestLoaderMetadataPolicy::BestEffort | GuestLoaderMetadataPolicy::AllowUnsupported
195 )
196 }
197
198 fn registers_public_metadata(self) -> bool {
199 matches!(self, GuestLoaderMetadataPolicy::BestEffort)
200 }
201}
202
203#[derive(Clone, Copy, PartialEq, Eq, Debug, Default, serde::Deserialize)]
204#[serde(rename_all = "kebab-case")]
205pub enum GuestCallStackPolicy {
206 #[default]
207 Native,
208 RegisteredUnwind,
209}
210
211impl GuestCallStackPolicy {
212 pub fn label(self) -> &'static str {
213 match self {
214 GuestCallStackPolicy::Native => "native",
215 GuestCallStackPolicy::RegisteredUnwind => "registered-unwind",
216 }
217 }
218}
219
220#[derive(Clone, Copy, PartialEq, Eq, Debug, Default, serde::Deserialize)]
221#[serde(rename_all = "kebab-case")]
222pub enum GuestPermissionTransitions {
223 #[default]
224 Standard,
225 WriteThroughFinal,
226}
227
228impl GuestPermissionTransitions {
229 pub fn label(self) -> &'static str {
230 match self {
231 GuestPermissionTransitions::Standard => "standard",
232 GuestPermissionTransitions::WriteThroughFinal => "write-through-final",
233 }
234 }
235}
236
237#[derive(Clone, Copy, PartialEq, Eq, Debug, Default, serde::Deserialize)]
238#[serde(rename_all = "kebab-case")]
239pub enum GuestThreadStartPolicy {
240 #[default]
241 ExistingThread,
242 RequireModuleBacked,
243}
244
245impl GuestThreadStartPolicy {
246 pub fn label(self) -> &'static str {
247 match self {
248 GuestThreadStartPolicy::ExistingThread => "existing-thread",
249 GuestThreadStartPolicy::RequireModuleBacked => "require-module-backed",
250 }
251 }
252}
253
254#[derive(Clone, Copy, PartialEq, Eq, Debug, Default, serde::Deserialize)]
255#[serde(rename_all = "kebab-case")]
256pub enum GuestImageBacking {
257 #[default]
258 Private,
259 SecImage,
260}
261
262impl GuestImageBacking {
263 pub fn label(self) -> &'static str {
264 match self {
265 GuestImageBacking::Private => "private",
266 GuestImageBacking::SecImage => "sec-image",
267 }
268 }
269}
270
271#[derive(Clone, Copy, PartialEq, Eq, Debug, Default, serde::Deserialize)]
272#[serde(rename_all = "kebab-case")]
273pub enum GuestBaseAddress {
274 #[default]
275 Preferred,
276 Randomized,
277}
278
279impl GuestBaseAddress {
280 pub fn label(self) -> &'static str {
281 match self {
282 GuestBaseAddress::Preferred => "preferred",
283 GuestBaseAddress::Randomized => "randomized",
284 }
285 }
286}
287
288#[derive(Clone, Copy, PartialEq, Eq, Debug, Default, serde::Deserialize)]
289#[serde(rename_all = "kebab-case")]
290pub enum GuestHeaderWipe {
291 #[default]
292 None,
293 AfterLoad,
294}
295
296impl GuestHeaderWipe {
297 pub fn label(self) -> &'static str {
298 match self {
299 GuestHeaderWipe::None => "none",
300 GuestHeaderWipe::AfterLoad => "after-load",
301 }
302 }
303}
304
305#[derive(Clone, Copy, PartialEq, Eq, Debug, Default, serde::Deserialize)]
306#[serde(rename_all = "kebab-case")]
307pub enum GuestLoaderEntries {
308 #[default]
309 Absent,
310 Synthesized,
311}
312
313impl GuestLoaderEntries {
314 pub fn label(self) -> &'static str {
315 match self {
316 GuestLoaderEntries::Absent => "absent",
317 GuestLoaderEntries::Synthesized => "synthesized",
318 }
319 }
320}
321
322#[derive(Clone, Copy, PartialEq, Eq, Debug, Default, serde::Deserialize)]
323#[serde(rename_all = "kebab-case")]
324pub enum GuestStackShaping {
325 #[default]
326 Native,
327 Spoofed,
328}
329
330impl GuestStackShaping {
331 pub fn label(self) -> &'static str {
332 match self {
333 GuestStackShaping::Native => "native",
334 GuestStackShaping::Spoofed => "spoofed",
335 }
336 }
337}
338
339#[derive(Clone, Copy, PartialEq, Eq, Debug, Default, serde::Deserialize)]
340#[serde(rename_all = "kebab-case")]
341pub enum GuestCleanup {
342 #[default]
343 Resident,
344 Tracked,
345}
346
347impl GuestCleanup {
348 pub fn label(self) -> &'static str {
349 match self {
350 GuestCleanup::Resident => "resident",
351 GuestCleanup::Tracked => "tracked",
352 }
353 }
354}
355
356#[derive(Clone, Copy, PartialEq, Eq, Debug, Default, serde::Deserialize)]
357#[serde(rename_all = "kebab-case")]
358pub enum GuestVadSpoof {
359 #[default]
360 Off,
361 VadImageMap,
362}
363
364impl GuestVadSpoof {
365 pub fn label(self) -> &'static str {
366 match self {
367 GuestVadSpoof::Off => "off",
368 GuestVadSpoof::VadImageMap => "vad-image-map",
369 }
370 }
371}
372
373#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize)]
374#[serde(deny_unknown_fields)]
375pub struct GuestExecutionConfig {
376 #[serde(default)]
377 pub method: GuestExecutionMethod,
378 #[serde(default = "guest_timeout_ms")]
379 pub timeout_ms: u32,
380}
381
382impl Default for GuestExecutionConfig {
383 fn default() -> Self {
384 Self {
385 method: GuestExecutionMethod::IatHook,
386 timeout_ms: guest_timeout_ms(),
387 }
388 }
389}
390
391#[derive(Clone, Debug, Default, PartialEq, Eq, serde::Deserialize)]
392#[serde(deny_unknown_fields)]
393pub struct GuestInjectionConfig {
394 #[serde(default)]
395 pub pid: Option<u32>,
396 #[serde(default)]
397 pub process: Option<String>,
398 #[serde(default)]
399 pub image_path: Option<String>,
400 #[serde(default)]
401 pub session_id: Option<u32>,
402 #[serde(default)]
403 pub package_family_name: Option<String>,
404 #[serde(default)]
405 pub appcontainer_sid: Option<String>,
406 #[serde(default)]
407 pub process_pattern: Option<String>,
408 #[serde(default)]
409 pub payload_path: Option<PathBuf>,
410 #[serde(default)]
411 pub allocation: GuestAllocationMethod,
412 #[serde(default)]
413 pub target_module: Option<String>,
414 #[serde(default)]
415 pub stage_base: Option<u64>,
416 #[serde(default)]
417 pub stage_pattern: Option<String>,
418 #[serde(default)]
419 pub result_base: Option<u64>,
420 #[serde(default)]
421 pub result_pattern: Option<String>,
422 #[serde(default = "default_hook_module")]
423 pub hook_module: String,
424 #[serde(default = "default_hook_function")]
425 pub hook_function: String,
426 #[serde(default)]
427 pub execution: GuestExecutionConfig,
428 #[serde(default)]
429 pub dependency_policy: GuestDependencyPolicy,
430 #[serde(default)]
431 pub tls: GuestTlsMode,
432 #[serde(default)]
433 pub final_protections: GuestFinalProtections,
434 #[serde(default)]
435 pub loader_metadata: GuestLoaderMetadataPolicy,
436 #[serde(default)]
437 pub call_stack: GuestCallStackPolicy,
438 #[serde(default)]
439 pub permission_transitions: GuestPermissionTransitions,
440 #[serde(default)]
441 pub thread_starts: GuestThreadStartPolicy,
442 #[serde(default)]
443 pub image_backing: GuestImageBacking,
444 #[serde(default)]
445 pub base_address: GuestBaseAddress,
446 #[serde(default)]
447 pub header_wipe: GuestHeaderWipe,
448 #[serde(default)]
449 pub loader_entries: GuestLoaderEntries,
450 #[serde(default)]
451 pub stack_shaping: GuestStackShaping,
452 #[serde(default)]
453 pub cleanup: GuestCleanup,
454 #[serde(default)]
455 pub vad_spoof: GuestVadSpoof,
456}
457
458#[derive(Clone, Debug, PartialEq, Eq)]
459pub struct GuestProcessSelector {
460 pub pid: Option<u32>,
461 pub name: Option<String>,
462 pub pattern: Option<GuestBytePattern>,
463}
464
465impl GuestProcessSelector {
466 pub fn validate(&self) -> Result<(), GuestInjectError> {
467 let name = self
468 .name
469 .as_deref()
470 .map(str::trim)
471 .filter(|s| !s.is_empty());
472 match (self.pid, name) {
473 (Some(_), None) => Ok(()),
474 (None, Some(_)) => Ok(()),
475 (Some(_), Some(_)) => Err(GuestInjectError::Config(
476 "set either guest.pid or guest.process, not both".into(),
477 )),
478 (None, None) => Err(GuestInjectError::Config(
479 "set guest.pid or guest.process for guest injection".into(),
480 )),
481 }
482 }
483
484 pub fn label(&self) -> String {
485 let base = match (self.pid, self.name.as_deref()) {
486 (Some(pid), None) => pid.to_string(),
487 (None, Some(name)) => name.to_string(),
488 (Some(pid), Some(name)) => format!("{name} ({pid})"),
489 (None, None) => "<unset>".into(),
490 };
491 match self.pattern {
492 Some(_) => format!("{base} matching process_pattern"),
493 None => base,
494 }
495 }
496}
497
498#[derive(Clone, Debug, PartialEq, Eq)]
499pub struct GuestBytePattern {
500 bytes: Vec<Option<u8>>,
501}
502
503impl GuestBytePattern {
504 fn find_in(&self, haystack: &[u8]) -> Option<usize> {
505 if self.bytes.is_empty() {
506 return Some(0);
507 }
508 haystack.windows(self.bytes.len()).position(|window| {
509 window
510 .iter()
511 .zip(&self.bytes)
512 .all(|(got, expected)| expected.is_none_or(|want| *got == want))
513 })
514 }
515
516 fn len(&self) -> usize {
517 self.bytes.len()
518 }
519}
520
521#[derive(Clone, Debug, PartialEq, Eq)]
522pub struct GuestInjectionPlan {
523 pub method: Method,
524 pub target: GuestProcessSelector,
525 pub payload_path: PathBuf,
526 pub allocation: GuestAllocationMethod,
527 pub dependency_policy: GuestDependencyPolicy,
528 pub tls: GuestTlsMode,
529 pub final_protections: GuestFinalProtections,
530 pub loader_metadata: GuestLoaderMetadataPolicy,
531 pub call_stack: GuestCallStackPolicy,
532 pub permission_transitions: GuestPermissionTransitions,
533 pub thread_starts: GuestThreadStartPolicy,
534 pub image_backing: GuestImageBacking,
535 pub base_address: GuestBaseAddress,
536 pub header_wipe: GuestHeaderWipe,
537 pub loader_entries: GuestLoaderEntries,
538 pub stack_shaping: GuestStackShaping,
539 pub cleanup: GuestCleanup,
540 pub vad_spoof: GuestVadSpoof,
541 pub target_module: Option<String>,
542 pub stage_base: Option<u64>,
543 pub stage_pattern: Option<GuestBytePattern>,
544 pub result_base: Option<u64>,
545 pub result_pattern: Option<GuestBytePattern>,
546 pub hook_module: String,
547 pub hook_function: String,
548 pub execution: GuestExecutionConfig,
549 pub timeout_ms: u32,
550}
551
552impl GuestInjectionPlan {
553 pub fn from_config(config: &DecantConfig) -> Result<Self, GuestInjectError> {
554 match config.injection.domain {
555 InjectionDomain::Guest => {}
556 InjectionDomain::Tool => {
557 return Err(GuestInjectError::Config(
558 "set injection.domain = \"guest\" for guest injection".into(),
559 ));
560 }
561 }
562 match config.injection.method {
563 Method::ManualMap => {}
564 Method::ThreadHijack => {
565 return Err(GuestInjectError::Config(
566 "guest domain uses method = \"manual-map\"; guest.execution.method selects the execution path".into(),
567 ));
568 }
569 Method::Standard | Method::Plugin | Method::External => {
570 return Err(GuestInjectError::Config(
571 "guest domain uses method = \"manual-map\"".into(),
572 ));
573 }
574 }
575 let target = GuestProcessSelector {
576 pid: config.guest.pid,
577 name: config.guest.process.clone(),
578 pattern: match config.guest.process_pattern.as_deref() {
579 Some(pattern) => Some(parse_hex_pattern(pattern, "guest.process_pattern")?),
580 None => None,
581 },
582 };
583 if config.guest.image_path.is_some()
584 || config.guest.session_id.is_some()
585 || config.guest.package_family_name.is_some()
586 || config.guest.appcontainer_sid.is_some()
587 {
588 return Err(GuestInjectError::Config(
589 "guest image_path/session_id/package_family_name/appcontainer_sid selectors are parsed but not implemented by this backend".into(),
590 ));
591 }
592 target.validate()?;
593 let payload_path = match &config.guest.payload_path {
594 Some(path) => path.clone(),
595 None => {
596 return Err(GuestInjectError::Config(
597 "set guest.payload_path to the DLL image".into(),
598 ));
599 }
600 };
601 if config.guest.image_backing == GuestImageBacking::SecImage
602 && config.guest.final_protections == GuestFinalProtections::Rwx
603 {
604 return Err(GuestInjectError::Config(
605 "guest.image_backing = \"sec-image\" requires final_protections = \"section\"; an image-file-backed mapping uses PE-derived page protections, not a single RWX region".into(),
606 ));
607 }
608 if config.guest.image_backing == GuestImageBacking::SecImage
609 && config.guest.allocation != GuestAllocationMethod::VirtualAlloc
610 {
611 return Err(GuestInjectError::Config(
612 "guest.image_backing = \"sec-image\" requires allocation = \"virtual-alloc\"; VirtualAlloc is used for helper buffers while the payload image is mapped through SEC_IMAGE".into(),
613 ));
614 }
615 if config.guest.loader_entries == GuestLoaderEntries::Synthesized
616 && config.guest.image_backing == GuestImageBacking::SecImage
617 {
618 return Err(GuestInjectError::Config(
619 "guest.loader_entries = \"synthesized\" is incompatible with image_backing = \"sec-image\"; SEC_IMAGE mappings already create real section state".into(),
620 ));
621 }
622 if config.guest.vad_spoof == GuestVadSpoof::VadImageMap
623 && config.guest.image_backing != GuestImageBacking::Private
624 {
625 return Err(GuestInjectError::Config(
626 "guest.vad_spoof = \"vad-image-map\" applies only to image_backing = \"private\""
627 .into(),
628 ));
629 }
630 Ok(Self {
631 method: config.injection.method,
632 target,
633 payload_path,
634 allocation: config.guest.allocation,
635 dependency_policy: config.guest.dependency_policy,
636 tls: config.guest.tls,
637 final_protections: config.guest.final_protections,
638 loader_metadata: config.guest.loader_metadata,
639 call_stack: config.guest.call_stack,
640 permission_transitions: config.guest.permission_transitions,
641 thread_starts: config.guest.thread_starts,
642 image_backing: config.guest.image_backing,
643 base_address: config.guest.base_address,
644 header_wipe: config.guest.header_wipe,
645 loader_entries: config.guest.loader_entries,
646 stack_shaping: config.guest.stack_shaping,
647 cleanup: config.guest.cleanup,
648 vad_spoof: config.guest.vad_spoof,
649 target_module: config.guest.target_module.clone(),
650 stage_base: config.guest.stage_base,
651 stage_pattern: match config.guest.stage_pattern.as_deref() {
652 Some(pattern) => Some(parse_hex_pattern(pattern, "guest.stage_pattern")?),
653 None => None,
654 },
655 result_base: config.guest.result_base,
656 result_pattern: match config.guest.result_pattern.as_deref() {
657 Some(pattern) => Some(parse_hex_pattern(pattern, "guest.result_pattern")?),
658 None => None,
659 },
660 hook_module: config.guest.hook_module.clone(),
661 hook_function: config.guest.hook_function.clone(),
662 execution: config.guest.execution.clone(),
663 timeout_ms: config.injection.timeout_ms,
664 })
665 }
666}
667
668#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
669pub struct GuestCapabilities {
670 pub list_processes: bool,
671 pub module_list: bool,
672 pub module_exports: bool,
673 pub read_memory: bool,
674 pub write_memory: bool,
675 pub write_verify: bool,
676 pub memory_map: bool,
677 pub virtual_alloc: bool,
678 pub virtual_free: bool,
679 pub protect_memory: bool,
680 pub iat_hook_execution: bool,
681 pub deterministic_execution: bool,
682 pub thread_context: bool,
683 pub queue_apc: bool,
684 pub create_thread: bool,
685 pub wait_for_result: bool,
686 pub forwarded_exports: bool,
687 pub ordinal_imports: bool,
688 pub delay_imports: bool,
689 pub static_tls: bool,
690 pub exception_registration: bool,
691 pub loader_reference: bool,
692 pub vad_spoof: bool,
693}
694
695impl GuestCapabilities {
696 pub fn memflow_guest_injection() -> Self {
697 Self {
698 list_processes: true,
699 module_list: true,
700 module_exports: true,
701 read_memory: true,
702 write_memory: true,
703 write_verify: true,
704 memory_map: true,
705 virtual_alloc: true,
706 virtual_free: false,
707 protect_memory: false,
708 iat_hook_execution: true,
709 deterministic_execution: false,
710 thread_context: false,
711 queue_apc: false,
712 create_thread: false,
713 wait_for_result: true,
714 forwarded_exports: true,
715 ordinal_imports: true,
716 delay_imports: true,
717 static_tls: false,
718 exception_registration: true,
719 loader_reference: false,
720 vad_spoof: false,
721 }
722 }
723
724 pub fn missing_manual_map(self) -> Vec<&'static str> {
725 let checks = [
726 (self.list_processes, "list-processes"),
727 (self.module_list, "module-list"),
728 (self.read_memory, "read-memory"),
729 (self.write_memory, "write-memory"),
730 (self.write_verify, "write-verify"),
731 (self.memory_map, "memory-map"),
732 (self.virtual_alloc, "virtual-alloc"),
733 (self.iat_hook_execution, "iat-hook-execution"),
734 (self.wait_for_result, "wait-for-result"),
735 (self.forwarded_exports, "forwarded-exports"),
736 (self.ordinal_imports, "ordinal-imports"),
737 (self.delay_imports, "delay-imports"),
738 ];
739 checks
740 .into_iter()
741 .filter_map(|(ok, name)| match ok {
742 true => None,
743 false => Some(name),
744 })
745 .collect()
746 }
747}
748
749#[derive(Clone, Copy, Debug, PartialEq, Eq)]
750pub struct GuestMemoryProtection {
751 pub read: bool,
752 pub write: bool,
753 pub execute: bool,
754}
755
756impl GuestMemoryProtection {
757 pub const READ_WRITE_EXECUTE: Self = Self {
758 read: true,
759 write: true,
760 execute: true,
761 };
762}
763
764#[derive(Clone, Debug, PartialEq, Eq)]
765pub struct GuestProcessInfo {
766 pub pid: u32,
767 pub name: String,
768}
769
770#[derive(Clone, Debug, PartialEq, Eq)]
771pub struct GuestModuleInfo {
772 pub name: String,
773 pub base: u64,
774 pub size: u64,
775}
776
777#[derive(Clone, Copy, Debug, PartialEq, Eq)]
778pub struct GuestMemoryRegion {
779 pub base: u64,
780 pub size: u64,
781 pub readable: bool,
782 pub writable: bool,
783 pub executable: bool,
784}
785
786pub struct GuestInjectionRequest<'a> {
787 pub plan: &'a GuestInjectionPlan,
788 pub payload_path: &'a Path,
789 pub payload_image: &'a [u8],
790}
791
792pub struct GuestLoadInfo {
793 pub method: String,
794 pub pid: u32,
795 pub remote_base: Option<u64>,
796 pub notes: Vec<String>,
797}
798
799#[derive(Debug)]
800pub enum GuestInjectError {
801 Config(String),
802 Backend(String),
803 Process(String),
804 Image(String),
805 Unsupported {
806 operation: &'static str,
807 reason: String,
808 },
809}
810
811impl fmt::Display for GuestInjectError {
812 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
813 match self {
814 GuestInjectError::Config(m) => write!(f, "guest config: {m}"),
815 GuestInjectError::Backend(m) => write!(f, "guest backend: {m}"),
816 GuestInjectError::Process(m) => write!(f, "guest process: {m}"),
817 GuestInjectError::Image(m) => write!(f, "guest image: {m}"),
818 GuestInjectError::Unsupported { operation, reason } => {
819 write!(f, "{operation} unsupported: {reason}")
820 }
821 }
822 }
823}
824
825impl std::error::Error for GuestInjectError {}
826
827impl From<InjectError> for GuestInjectError {
828 fn from(value: InjectError) -> Self {
829 GuestInjectError::Image(value.to_string())
830 }
831}
832
833pub trait GuestMemoryBackend {
834 fn capabilities(&self) -> GuestCapabilities;
835 fn list_processes(&self) -> Result<Vec<GuestProcessInfo>, GuestInjectError>;
836 fn module_list(&self, pid: u32) -> Result<Vec<GuestModuleInfo>, GuestInjectError>;
837 fn module_exports(
838 &self,
839 pid: u32,
840 module: &str,
841 ) -> Result<Vec<(String, u64)>, GuestInjectError>;
842 fn memory_map(&self, pid: u32) -> Result<Vec<GuestMemoryRegion>, GuestInjectError>;
843 fn read(&self, pid: u32, addr: u64, len: usize) -> Result<Vec<u8>, GuestInjectError>;
844 fn write(&self, pid: u32, addr: u64, data: &[u8]) -> Result<(), GuestInjectError>;
845
846 fn call_iat_hook(
847 &self,
848 pid: u32,
849 hook: &GuestIatHook,
850 function: u64,
851 args: [u64; 4],
852 timeout_ms: u32,
853 ) -> Result<u64, GuestInjectError> {
854 memory_iat_call(self, pid, hook, function, args, timeout_ms)
855 }
856
857 fn touch_iat_hook(
858 &self,
859 pid: u32,
860 hook: &GuestIatHook,
861 addr: u64,
862 len: usize,
863 timeout_ms: u32,
864 ) -> Result<(), GuestInjectError> {
865 memory_iat_touch(self, pid, hook, addr, len, timeout_ms)
866 }
867
868 fn read_touch_iat_hook(
869 &self,
870 pid: u32,
871 hook: &GuestIatHook,
872 addr: u64,
873 len: usize,
874 timeout_ms: u32,
875 ) -> Result<(), GuestInjectError> {
876 memory_iat_read_touch(self, pid, hook, addr, len, timeout_ms)
877 }
878
879 fn preserve_touch_iat_hook(
880 &self,
881 pid: u32,
882 hook: &GuestIatHook,
883 addr: u64,
884 len: usize,
885 timeout_ms: u32,
886 ) -> Result<(), GuestInjectError> {
887 memory_iat_preserve_touch(self, pid, hook, addr, len, timeout_ms)
888 }
889
890 fn spoof_vad_type(&self, _pid: u32, _base: u64, _size: u64) -> Result<(), GuestInjectError> {
891 Err(GuestInjectError::Unsupported {
892 operation: "VAD type spoofing",
893 reason: "this backend does not expose kernel memory access".into(),
894 })
895 }
896
897 fn resolve_process(
898 &self,
899 target: &GuestProcessSelector,
900 ) -> Result<GuestProcessInfo, GuestInjectError> {
901 target.validate()?;
902 let processes = self.list_processes()?;
903 tracing::debug!(
904 selector = %target.label(),
905 process_count = processes.len(),
906 "resolving guest process selector"
907 );
908 match (target.pid, target.name.as_deref()) {
909 (Some(pid), _) => processes
910 .into_iter()
911 .find(|p| p.pid == pid)
912 .ok_or_else(|| GuestInjectError::Process(format!("pid {pid} not found")))
913 .and_then(|process| match &target.pattern {
914 Some(pattern) => match find_process_pattern(self, process.pid, pattern)? {
915 Some(addr) => {
916 tracing::info!(
917 pid = process.pid,
918 process = %process.name,
919 pattern_addr = format_args!("{addr:#x}"),
920 "guest process selector matched pid and pattern"
921 );
922 Ok(process)
923 }
924 None => Err(GuestInjectError::Process(format!(
925 "pid {pid} does not contain guest.process_pattern"
926 ))),
927 },
928 None => {
929 tracing::info!(pid = process.pid, process = %process.name, "guest process selector matched pid");
930 Ok(process)
931 }
932 }),
933 (None, Some(name)) => {
934 let candidates = processes
935 .into_iter()
936 .filter(|p| p.name.eq_ignore_ascii_case(name))
937 .collect::<Vec<_>>();
938 tracing::debug!(
939 name,
940 candidates = candidates.len(),
941 pattern = target.pattern.is_some(),
942 "guest process name candidates collected"
943 );
944 if candidates.is_empty() {
945 return Err(GuestInjectError::Process(format!(
946 "process {name:?} not found"
947 )));
948 }
949 match &target.pattern {
950 Some(pattern) => {
951 let mut hits = Vec::new();
952 for process in candidates {
953 if let Some(addr) = find_process_pattern(self, process.pid, pattern)? {
954 tracing::debug!(
955 pid = process.pid,
956 process = %process.name,
957 pattern_addr = format_args!("{addr:#x}"),
958 "guest process selector candidate matched pattern"
959 );
960 hits.push((process, addr));
961 }
962 }
963 match hits.len() {
964 0 => Err(GuestInjectError::Process(format!(
965 "process {name:?} found, but none contained guest.process_pattern"
966 ))),
967 1 => {
968 let (process, addr) = hits.remove(0);
969 tracing::info!(
970 pid = process.pid,
971 process = %process.name,
972 pattern_addr = format_args!("{addr:#x}"),
973 "guest process selector matched name and pattern"
974 );
975 Ok(process)
976 }
977 _ => Err(GuestInjectError::Process(format!(
978 "multiple processes named {name:?} contained guest.process_pattern; set guest.pid"
979 ))),
980 }
981 }
982 None => {
983 let process = candidates.into_iter().next().unwrap();
984 tracing::info!(pid = process.pid, process = %process.name, "guest process selector matched name");
985 Ok(process)
986 }
987 }
988 }
989 (None, None) => Err(GuestInjectError::Config(
990 "set guest.pid or guest.process for guest injection".into(),
991 )),
992 }
993 }
994}
995
996#[derive(Clone, Copy, Debug)]
997pub struct GuestIatHook {
998 pub iat_slot: u64,
999 pub original_target: u64,
1000 pub stub_addr: u64,
1001 pub result_addr: u64,
1002 pub call_stack: GuestCallStackPolicy,
1003 pub spoofed_return: Option<GuestSpoofedReturn>,
1004}
1005
1006#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1007pub struct GuestSpoofedReturn {
1008 pub gadget_addr: u64,
1009 pub stack_adjust: u8,
1010}
1011
1012struct GuestInjectionLock {
1013 pid: u32,
1014}
1015
1016impl GuestInjectionLock {
1017 fn acquire(pid: u32) -> Result<Self, GuestInjectError> {
1018 let active = ACTIVE_GUEST_INJECTIONS.get_or_init(|| Mutex::new(HashSet::new()));
1019 let mut active = active.lock().unwrap();
1020 if !active.insert(pid) {
1021 return Err(GuestInjectError::Unsupported {
1022 operation: "guest injection",
1023 reason: format!("another guest injection is already active for pid {pid}"),
1024 });
1025 }
1026 Ok(Self { pid })
1027 }
1028}
1029
1030impl Drop for GuestInjectionLock {
1031 fn drop(&mut self) {
1032 if let Some(active) = ACTIVE_GUEST_INJECTIONS.get() {
1033 active.lock().unwrap().remove(&self.pid);
1034 }
1035 }
1036}
1037
1038pub trait GuestInjector {
1039 fn name(&self) -> &str;
1040 fn portability(&self) -> GuestPortability;
1041 fn inject(
1042 &self,
1043 backend: &dyn GuestMemoryBackend,
1044 req: &GuestInjectionRequest<'_>,
1045 ) -> Result<GuestLoadInfo, GuestInjectError>;
1046}
1047
1048pub struct GuestManualMapInjector;
1049
1050impl GuestInjector for GuestManualMapInjector {
1051 fn name(&self) -> &str {
1052 "manual-map"
1053 }
1054
1055 fn portability(&self) -> GuestPortability {
1056 GuestPortability::GuestLoaderInternals
1057 }
1058
1059 fn inject(
1060 &self,
1061 backend: &dyn GuestMemoryBackend,
1062 req: &GuestInjectionRequest<'_>,
1063 ) -> Result<GuestLoadInfo, GuestInjectError> {
1064 tracing::info!(
1065 method = self.name(),
1066 target = %req.plan.target.label(),
1067 payload = %req.payload_path.display(),
1068 payload_bytes = req.payload_image.len(),
1069 allocation = req.plan.allocation.label(),
1070 dependency_policy = req.plan.dependency_policy.label(),
1071 execution = req.plan.execution.method.label(),
1072 tls = req.plan.tls.label(),
1073 final_protections = req.plan.final_protections.label(),
1074 loader_metadata = req.plan.loader_metadata.label(),
1075 call_stack = req.plan.call_stack.label(),
1076 permission_transitions = req.plan.permission_transitions.label(),
1077 thread_starts = req.plan.thread_starts.label(),
1078 image_backing = req.plan.image_backing.label(),
1079 vad_spoof = req.plan.vad_spoof.label(),
1080 hook_module = %req.plan.hook_module,
1081 hook_function = %req.plan.hook_function,
1082 timeout_ms = req.plan.execution.timeout_ms,
1083 "guest injection started"
1084 );
1085
1086 let result = (|| {
1087 if req.payload_image.is_empty() {
1088 return Err(GuestInjectError::Image(
1089 "payload_image is empty; guest injection requires DLL bytes".into(),
1090 ));
1091 }
1092 let capabilities = backend.capabilities();
1093 let missing = capabilities.missing_manual_map();
1094 if !missing.is_empty() {
1095 return Err(GuestInjectError::Unsupported {
1096 operation: "guest injection",
1097 reason: format!("manual-map method backend missing {}", missing.join(", ")),
1098 });
1099 }
1100 if req.plan.vad_spoof == GuestVadSpoof::VadImageMap && !capabilities.vad_spoof {
1101 return Err(GuestInjectError::Unsupported {
1102 operation: "VAD type spoofing",
1103 reason: "this backend does not expose validated VAD mutation support".into(),
1104 });
1105 }
1106 tracing::debug!("guest injection backend requirements satisfied");
1107
1108 let process = backend.resolve_process(&req.plan.target)?;
1109 tracing::info!(pid = process.pid, process = %process.name, "guest target resolved");
1110 let _injection_lock = GuestInjectionLock::acquire(process.pid)?;
1111
1112 match req.plan.execution.method {
1113 GuestExecutionMethod::IatHook | GuestExecutionMethod::RemoteThread => {}
1114 other => {
1115 return Err(GuestInjectError::Unsupported {
1116 operation: "guest injection",
1117 reason: format!(
1118 "guest execution method {} is not implemented",
1119 other.label()
1120 ),
1121 });
1122 }
1123 }
1124
1125 let stage = match req.plan.stage_base {
1126 Some(base) => {
1127 validate_stub_region(backend, process.pid, base, STAGE_CAVE_SIZE)?;
1128 tracing::info!(
1129 pid = process.pid,
1130 stage = format_args!("{base:#x}"),
1131 "using configured guest stage"
1132 );
1133 base
1134 }
1135 None => {
1136 tracing::debug!(pid = process.pid, "searching for guest execution stage");
1137 let found = find_stage(backend, process.pid, req.plan.stage_pattern.as_ref())?;
1138 validate_stub_region(backend, process.pid, found, STAGE_CAVE_SIZE)?;
1139 tracing::info!(
1140 pid = process.pid,
1141 stage = format_args!("{found:#x}"),
1142 "guest execution stage selected"
1143 );
1144 found
1145 }
1146 };
1147 let result_addr = match req.plan.result_base {
1148 Some(base) => {
1149 validate_result_region(backend, process.pid, base)?;
1150 tracing::info!(
1151 pid = process.pid,
1152 result = format_args!("{base:#x}"),
1153 "using configured guest result block"
1154 );
1155 base
1156 }
1157 None => {
1158 let found = find_result_block(
1159 backend,
1160 process.pid,
1161 req.plan.result_pattern.as_ref(),
1162 stage + STAGE_RESULT_OFFSET,
1163 )?;
1164 validate_result_region(backend, process.pid, found)?;
1165 tracing::info!(
1166 pid = process.pid,
1167 result = format_args!("{found:#x}"),
1168 "guest result block selected"
1169 );
1170 found
1171 }
1172 };
1173
1174 let hook = find_iat_hook(backend, process.pid, req.plan)?;
1175 let hook = GuestIatHook {
1176 iat_slot: hook.iat_slot,
1177 original_target: hook.original_target,
1178 stub_addr: stage + STAGE_STUB_OFFSET,
1179 result_addr,
1180 call_stack: req.plan.call_stack,
1181 spoofed_return: if req.plan.stack_shaping == GuestStackShaping::Spoofed {
1182 Some(find_spoofed_return(backend, process.pid)?)
1183 } else {
1184 None
1185 },
1186 };
1187 tracing::info!(
1188 pid = process.pid,
1189 iat_slot = format_args!("{:#x}", hook.iat_slot),
1190 original_target = format_args!("{:#x}", hook.original_target),
1191 stub_addr = format_args!("{:#x}", hook.stub_addr),
1192 result_addr = format_args!("{:#x}", hook.result_addr),
1193 hook_module = %req.plan.hook_module,
1194 hook_function = %req.plan.hook_function,
1195 "guest IAT hook selected"
1196 );
1197 let mut thread_start_notes =
1198 validate_guest_thread_start_policy(backend, process.pid, req.plan, stage, &hook)?;
1199 let mut call_stack_notes = Vec::new();
1200 if req.plan.call_stack == GuestCallStackPolicy::RegisteredUnwind {
1201 register_guest_stub_unwind(
1202 backend,
1203 process.pid,
1204 &hook,
1205 stage,
1206 req.plan.execution.timeout_ms,
1207 )?;
1208 call_stack_notes
1209 .push("call stack: registered unwind metadata for IAT-hook stub".to_string());
1210 }
1211
1212 let virtual_alloc =
1213 resolve_import_symbol(backend, process.pid, "kernel32.dll", "VirtualAlloc")?;
1214 tracing::info!(
1215 pid = process.pid,
1216 virtual_alloc = format_args!("{virtual_alloc:#x}"),
1217 "guest allocator resolved"
1218 );
1219 let virtual_protect = match req.plan.final_protections {
1220 GuestFinalProtections::Rwx => None,
1221 GuestFinalProtections::Section => {
1222 let addr = resolve_import_symbol(
1223 backend,
1224 process.pid,
1225 "kernel32.dll",
1226 "VirtualProtect",
1227 )?;
1228 tracing::info!(
1229 pid = process.pid,
1230 virtual_protect = format_args!("{addr:#x}"),
1231 "guest final section protector resolved"
1232 );
1233 Some(addr)
1234 }
1235 };
1236 let loader_apis = match req.plan.dependency_policy {
1237 GuestDependencyPolicy::RequireLoaded => None,
1238 GuestDependencyPolicy::LoadWithGuestLoader => {
1239 let load_library = resolve_import_symbol(
1240 backend,
1241 process.pid,
1242 "kernel32.dll",
1243 "LoadLibraryA",
1244 )?;
1245 let get_proc_address = resolve_import_symbol(
1246 backend,
1247 process.pid,
1248 "kernel32.dll",
1249 "GetProcAddress",
1250 )?;
1251 tracing::info!(
1252 pid = process.pid,
1253 load_library = format_args!("{load_library:#x}"),
1254 get_proc_address = format_args!("{get_proc_address:#x}"),
1255 "guest dependency loader APIs resolved"
1256 );
1257 Some(GuestLoaderApis {
1258 load_library,
1259 get_proc_address,
1260 })
1261 }
1262 };
1263
1264 let pe = Pe::parse(req.payload_image)?;
1265 tracing::info!(
1266 image_base = format_args!("{:#x}", pe.image_base),
1267 entry_rva = format_args!("{:#x}", pe.entry_rva),
1268 size_of_image = pe.size_of_image,
1269 sections = pe.sections.len(),
1270 "payload PE parsed"
1271 );
1272 let allocation_protection = initial_allocation_protection(req.plan, &pe);
1273 let read_touch_materialization = req.plan.permission_transitions
1274 == GuestPermissionTransitions::WriteThroughFinal
1275 && !protection_is_writable(allocation_protection);
1276
1277 let (remote_base, materialize_allocated_pages) =
1278 if req.plan.image_backing == GuestImageBacking::SecImage {
1279 tracing::info!(
1280 pid = process.pid,
1281 "mapping payload through a guest SEC_IMAGE file-backed section"
1282 );
1283 let base = allocate_sec_image(
1284 backend,
1285 process.pid,
1286 &hook,
1287 virtual_alloc,
1288 stage,
1289 req.payload_image,
1290 req.plan.execution.timeout_ms,
1291 )?;
1292 (base, false)
1293 } else {
1294 match req.plan.allocation {
1295 GuestAllocationMethod::VirtualAlloc => {
1296 let remote = allocate_virtual(
1297 backend,
1298 process.pid,
1299 &hook,
1300 virtual_alloc,
1301 &pe,
1302 allocation_protection,
1303 req.plan.base_address,
1304 req.plan.execution.timeout_ms,
1305 )?;
1306 (remote, true)
1307 }
1308 GuestAllocationMethod::ExistingRegion => match req.plan.stage_base {
1309 Some(base) => {
1310 let remote = base + 0x1000;
1311 tracing::info!(
1312 pid = process.pid,
1313 remote_base = format_args!("{remote:#x}"),
1314 "using configured guest existing region"
1315 );
1316 (remote, false)
1317 }
1318 None => {
1319 return Err(GuestInjectError::Config(
1320 "guest allocation = \"existing-region\" requires guest.stage_base"
1321 .into(),
1322 ));
1323 }
1324 },
1325 GuestAllocationMethod::PageTableMap => {
1326 return Err(GuestInjectError::Unsupported {
1327 operation: "guest page-table-map",
1328 reason:
1329 "use allocation = \"virtual-alloc\" for the in-process allocator"
1330 .into(),
1331 });
1332 }
1333 }
1334 };
1335 if remote_base == 0 {
1336 return Err(GuestInjectError::Backend(
1337 "guest image allocation returned NULL".into(),
1338 ));
1339 }
1340 tracing::info!(
1341 pid = process.pid,
1342 remote_base = format_args!("{remote_base:#x}"),
1343 "guest payload memory allocated"
1344 );
1345 if materialize_allocated_pages {
1346 match read_touch_materialization {
1347 true => backend.read_touch_iat_hook(
1348 process.pid,
1349 &hook,
1350 remote_base,
1351 pe.size_of_image,
1352 req.plan.execution.timeout_ms,
1353 )?,
1354 false => backend.touch_iat_hook(
1355 process.pid,
1356 &hook,
1357 remote_base,
1358 pe.size_of_image,
1359 req.plan.execution.timeout_ms,
1360 )?,
1361 }
1362 tracing::info!(
1363 pid = process.pid,
1364 remote_base = format_args!("{remote_base:#x}"),
1365 bytes = pe.size_of_image,
1366 read_touch = read_touch_materialization,
1367 "guest payload pages materialized"
1368 );
1369 }
1370
1371 let (mut image, sec_image_snapshot) =
1372 if req.plan.image_backing == GuestImageBacking::SecImage {
1373 let snap = backend.read(process.pid, remote_base, pe.size_of_image)?;
1374 let local_layout = pe.mapped_image(req.payload_image)?;
1375 tracing::debug!(
1376 pid = process.pid,
1377 remote_base = format_args!("{remote_base:#x}"),
1378 mapped_bytes = snap.len(),
1379 "SEC_IMAGE view read back from guest for patch comparison"
1380 );
1381 tracing::debug!(
1382 mapped_bytes = local_layout.len(),
1383 "payload image laid out in local buffer"
1384 );
1385 (local_layout, Some(snap))
1386 } else {
1387 let view = pe.mapped_image(req.payload_image)?;
1388 tracing::debug!(
1389 mapped_bytes = view.len(),
1390 "payload image laid out in local buffer"
1391 );
1392 (view, None)
1393 };
1394 pe.apply_relocs(&mut image, remote_base)?;
1395 tracing::debug!(
1396 remote_base = format_args!("{remote_base:#x}"),
1397 "payload relocations applied"
1398 );
1399 let mut loader_metadata_notes = Vec::new();
1400 if req.plan.loader_metadata.registers_public_metadata() {
1401 let image_len = image.len();
1402 match pe.seed_security_cookie(
1403 &mut image,
1404 remote_base,
1405 loader_security_cookie(process.pid, remote_base, image_len),
1406 )? {
1407 Some(cookie) => {
1408 tracing::info!(
1409 pid = process.pid,
1410 remote_base = format_args!("{remote_base:#x}"),
1411 cookie = format_args!("{cookie:#x}"),
1412 "payload load-config security cookie seeded"
1413 );
1414 loader_metadata_notes.push(
1415 "loader metadata: seeded load-config security cookie".to_string(),
1416 );
1417 }
1418 None if pe.has_load_config() => {
1419 tracing::debug!(
1420 pid = process.pid,
1421 "payload load-config has no default security cookie to seed"
1422 );
1423 }
1424 None => {}
1425 }
1426 }
1427
1428 let dependency_scratch = stage + STAGE_SCRATCH_OFFSET;
1429 let mut resolve_payload_import = |module: &[u8],
1430 symbol: ImportSymbol<'_>|
1431 -> Result<usize, GuestInjectError> {
1432 let module = std::str::from_utf8(module)
1433 .map_err(|e| GuestInjectError::Image(format!("import module name: {e}")))?;
1434 match symbol {
1435 ImportSymbol::Name(name) => {
1436 let name = std::str::from_utf8(name)
1437 .map_err(|e| GuestInjectError::Image(format!("import name: {e}")))?;
1438 let addr = resolve_import_symbol_with_dependency_policy(
1439 backend,
1440 process.pid,
1441 &hook,
1442 loader_apis,
1443 dependency_scratch,
1444 STAGE_SCRATCH_SIZE,
1445 req.plan.execution.timeout_ms,
1446 req.plan.dependency_policy,
1447 module,
1448 ImportSymbol::Name(name.as_bytes()),
1449 )?;
1450 tracing::debug!(
1451 pid = process.pid,
1452 module,
1453 symbol = name,
1454 address = format_args!("{addr:#x}"),
1455 "payload import resolved"
1456 );
1457 Ok(addr as usize)
1458 }
1459 ImportSymbol::Ordinal(ordinal) => {
1460 let addr = resolve_import_symbol_with_dependency_policy(
1461 backend,
1462 process.pid,
1463 &hook,
1464 loader_apis,
1465 dependency_scratch,
1466 STAGE_SCRATCH_SIZE,
1467 req.plan.execution.timeout_ms,
1468 req.plan.dependency_policy,
1469 module,
1470 ImportSymbol::Ordinal(ordinal),
1471 )?;
1472 tracing::debug!(
1473 pid = process.pid,
1474 module,
1475 ordinal,
1476 address = format_args!("{addr:#x}"),
1477 "payload ordinal import resolved"
1478 );
1479 Ok(addr as usize)
1480 }
1481 }
1482 };
1483 pe.resolve_imports(&mut image, &mut resolve_payload_import)?;
1484 tracing::info!("payload imports resolved");
1485 pe.resolve_delay_imports(&mut image, &mut resolve_payload_import)?;
1486 tracing::info!("payload delay imports resolved");
1487 let has_static_tls = pe.has_static_tls(&image)?;
1488 let mut tls_slot_index: Option<u32> = None;
1489 match (req.plan.tls, has_static_tls) {
1490 (GuestTlsMode::RequireStatic, _) => {
1491 return Err(GuestInjectError::Unsupported {
1492 operation: "guest static TLS",
1493 reason: "static TLS registration requires loader-managed TLS slots; use tls = \"callbacks-only\" or tls = \"skip\" with the current backend".into(),
1494 });
1495 }
1496 (_, true)
1497 if !req.plan.loader_metadata.allows_unregistered_metadata()
1498 && req.plan.loader_entries != GuestLoaderEntries::Synthesized =>
1499 {
1500 return Err(GuestInjectError::Unsupported {
1501 operation: "guest static TLS",
1502 reason: "payload uses static TLS; guest injection can call TLS callbacks but cannot register loader-managed TLS slots. Rebuild without static TLS or set guest.loader_metadata = \"best-effort\"/\"allow-unsupported\" only when the payload does not read static TLS".into(),
1503 });
1504 }
1505 (GuestTlsMode::CallbacksOnly, true)
1506 if req.plan.loader_entries == GuestLoaderEntries::Synthesized =>
1507 {
1508 let tls_alloc =
1509 resolve_import_symbol(backend, process.pid, "kernel32.dll", "TlsAlloc")?;
1510 let tls_set_value =
1511 resolve_import_symbol(backend, process.pid, "kernel32.dll", "TlsSetValue")?;
1512 let raw_slot = backend.call_iat_hook(
1513 process.pid,
1514 &hook,
1515 tls_alloc,
1516 [0, 0, 0, 0],
1517 req.plan.execution.timeout_ms,
1518 )?;
1519 let slot = raw_slot as u32;
1520 if slot == u32::MAX {
1521 return Err(GuestInjectError::Backend(
1522 "guest TlsAlloc returned TLS_OUT_OF_INDEXES".into(),
1523 ));
1524 }
1525 if let Some(index_offset) = pe.tls_index_offset(&image, remote_base as usize)? {
1526 image[index_offset..index_offset + 4].copy_from_slice(&slot.to_le_bytes());
1527 tracing::info!(
1528 pid = process.pid,
1529 tls_slot = slot,
1530 index_offset = format_args!("{index_offset:#x}"),
1531 "static TLS slot allocated; index patched into local image buffer"
1532 );
1533 tls_slot_index = Some(slot);
1534 if let Some(template) = pe.tls_template(&image, remote_base as usize)? {
1535 let template_buf = backend.call_iat_hook(
1536 process.pid,
1537 &hook,
1538 virtual_alloc,
1539 [0, template.len() as u64, MEM_COMMIT_RESERVE, PAGE_READWRITE],
1540 req.plan.execution.timeout_ms,
1541 )?;
1542 if template_buf != 0 {
1543 backend.touch_iat_hook(
1544 process.pid,
1545 &hook,
1546 template_buf,
1547 template.len(),
1548 req.plan.execution.timeout_ms,
1549 )?;
1550 write_verified(
1551 backend,
1552 process.pid,
1553 template_buf,
1554 &template,
1555 "TLS template copy",
1556 )?;
1557 let set_ok = backend.call_iat_hook(
1558 process.pid,
1559 &hook,
1560 tls_set_value,
1561 [slot as u64, template_buf, 0, 0],
1562 req.plan.execution.timeout_ms,
1563 )?;
1564 if set_ok == 0 {
1565 tracing::warn!(
1566 pid = process.pid,
1567 slot,
1568 "guest TlsSetValue returned FALSE; static TLS template not installed for the current helper/target thread"
1569 );
1570 } else {
1571 tracing::info!(
1572 pid = process.pid,
1573 slot,
1574 template_buf = format_args!("{template_buf:#x}"),
1575 template_len = template.len(),
1576 "static TLS template copied and TlsSetValue called for the current helper thread"
1577 );
1578 if req.plan.execution.method
1579 == GuestExecutionMethod::RemoteThread
1580 {
1581 tracing::warn!(
1582 pid = process.pid,
1583 slot,
1584 "remote-thread DllMain runs on a new thread; static TLS template is not propagated to that thread"
1585 );
1586 }
1587 }
1588 }
1589 }
1590 }
1591 }
1592 (GuestTlsMode::CallbacksOnly, true) => {
1593 tracing::warn!(
1594 pid = process.pid,
1595 "payload uses static TLS; guest injection will call TLS callbacks without registering loader-managed TLS slots"
1596 );
1597 }
1598 (GuestTlsMode::Skip, _) | (GuestTlsMode::CallbacksOnly, false) => {}
1599 }
1600
1601 match &sec_image_snapshot {
1602 Some(snap) => {
1603 let vp = virtual_protect.expect(
1604 "sec-image requires final_protections = section so VirtualProtect is resolved",
1605 );
1606 let mut patched_pages = 0u64;
1607 let patched_ranges = sec_image_patched_ranges(&image, snap);
1608 for (range_start, range_end) in &patched_ranges {
1609 guest_virtual_protect(
1610 backend,
1611 process.pid,
1612 &hook,
1613 vp,
1614 remote_base + *range_start as u64,
1615 (range_end - range_start) as u64,
1616 PAGE_READWRITE,
1617 hook.result_addr + OLD_PROTECT_RESULT_OFFSET,
1618 req.plan.execution.timeout_ms,
1619 "SEC_IMAGE patched range",
1620 )?;
1621 backend.preserve_touch_iat_hook(
1622 process.pid,
1623 &hook,
1624 remote_base + *range_start as u64,
1625 range_end - range_start,
1626 req.plan.execution.timeout_ms,
1627 )?;
1628 write_verified(
1629 backend,
1630 process.pid,
1631 remote_base + *range_start as u64,
1632 &image[*range_start..*range_end],
1633 "SEC_IMAGE patched range",
1634 )?;
1635 patched_pages +=
1636 ((range_end - range_start).div_ceil(GUEST_PAGE_SIZE)) as u64;
1637 }
1638 tracing::info!(
1639 pid = process.pid,
1640 remote_base = format_args!("{remote_base:#x}"),
1641 patched_pages,
1642 patched_ranges = patched_ranges.len(),
1643 total_pages = image.len() as u64 / GUEST_PAGE_SIZE as u64,
1644 "SEC_IMAGE patched pages written; unpatched pages remain file-backed"
1645 );
1646 }
1647 None => {
1648 write_verified(
1649 backend,
1650 process.pid,
1651 remote_base,
1652 &image,
1653 "guest mapped payload image",
1654 )?;
1655 tracing::info!(
1656 pid = process.pid,
1657 remote_base = format_args!("{remote_base:#x}"),
1658 bytes = image.len(),
1659 "payload image written to guest"
1660 );
1661 }
1662 }
1663
1664 if pe.has_exception_directory() {
1665 match req.plan.loader_metadata {
1666 GuestLoaderMetadataPolicy::RejectUnsupported => {
1667 return Err(GuestInjectError::Unsupported {
1668 operation: "guest exception metadata",
1669 reason: "payload has an exception directory; set guest.loader_metadata = \"best-effort\" to register the x64 runtime function table or \"allow-unsupported\" only when the payload does not unwind across the mapped image".into(),
1670 });
1671 }
1672 GuestLoaderMetadataPolicy::BestEffort => {
1673 register_guest_exception_table(
1674 backend,
1675 process.pid,
1676 &hook,
1677 remote_base,
1678 &pe,
1679 req.plan.execution.timeout_ms,
1680 )?;
1681 loader_metadata_notes.push(
1682 "loader metadata: registered x64 runtime function table".to_string(),
1683 );
1684 }
1685 GuestLoaderMetadataPolicy::AllowUnsupported => {
1686 tracing::warn!(
1687 pid = process.pid,
1688 "payload has an exception directory; guest injection did not register unwind data"
1689 );
1690 }
1691 }
1692 }
1693 if pe.has_load_config() {
1694 match req.plan.loader_metadata {
1695 GuestLoaderMetadataPolicy::RejectUnsupported => {
1696 return Err(GuestInjectError::Unsupported {
1697 operation: "guest load-config metadata",
1698 reason: "payload has a load-config directory; set guest.loader_metadata = \"best-effort\" to seed the security cookie when present or \"allow-unsupported\" only when load-config entries are inert for the payload".into(),
1699 });
1700 }
1701 GuestLoaderMetadataPolicy::BestEffort => {
1702 tracing::debug!(
1703 pid = process.pid,
1704 "payload load-config was processed with available public metadata hooks; broader loader-private entries are not synthesized"
1705 );
1706 }
1707 GuestLoaderMetadataPolicy::AllowUnsupported => {
1708 tracing::debug!(
1709 pid = process.pid,
1710 "payload has a load-config directory; security-cookie/CFG metadata is left as mapped"
1711 );
1712 }
1713 }
1714 }
1715
1716 if let Some(virtual_protect) = virtual_protect {
1717 protect_guest_sections(
1718 backend,
1719 process.pid,
1720 &hook,
1721 virtual_protect,
1722 remote_base,
1723 &pe,
1724 protect_skip_initial(req.plan, allocation_protection),
1725 req.plan.execution.timeout_ms,
1726 )?;
1727 }
1728
1729 if req.plan.tls == GuestTlsMode::Skip {
1730 tracing::info!("payload TLS callbacks skipped by config");
1731 } else {
1732 let callbacks = pe.tls_callbacks(&image, remote_base as usize)?;
1733 tracing::info!(
1734 count = callbacks.len(),
1735 "payload TLS callback list prepared"
1736 );
1737 for callback in callbacks {
1738 tracing::info!(
1739 pid = process.pid,
1740 callback = format_args!("{callback:#x}"),
1741 "calling payload TLS callback"
1742 );
1743 let _ = backend.call_iat_hook(
1744 process.pid,
1745 &hook,
1746 callback as u64,
1747 [remote_base, DLL_PROCESS_ATTACH as u64, 0, 0],
1748 req.plan.execution.timeout_ms,
1749 )?;
1750 }
1751 }
1752
1753 let mut peb_entry_addr: Option<u64> = None;
1754 let mut peb_entry_unlinked = false;
1755 let mut cfg_marked = false;
1756 let mut cfg_target_count = 0u32;
1757 if req.plan.loader_entries == GuestLoaderEntries::Synthesized {
1758 if should_request_cfg_call_target(req.plan, &pe)
1759 && let Ok(set_valid_call_targets) = resolve_import_symbol(
1760 backend,
1761 process.pid,
1762 "kernel32.dll",
1763 "SetProcessValidCallTargets",
1764 )
1765 {
1766 let targets = pe.cfg_call_targets(&image)?;
1767 cfg_target_count = targets.len() as u32;
1768 if !targets.is_empty() {
1769 let info_bytes = targets.len() * 16;
1770 let cfg_info = backend.call_iat_hook(
1771 process.pid,
1772 &hook,
1773 virtual_alloc,
1774 [0, info_bytes as u64, MEM_COMMIT_RESERVE, PAGE_READWRITE],
1775 req.plan.execution.timeout_ms,
1776 )?;
1777 if cfg_info != 0 {
1778 backend.touch_iat_hook(
1779 process.pid,
1780 &hook,
1781 cfg_info,
1782 info_bytes,
1783 req.plan.execution.timeout_ms,
1784 )?;
1785 let mut info = vec![0u8; info_bytes];
1786 for (i, &rva) in targets.iter().enumerate() {
1787 let off = i * 16;
1788 info[off..off + 8].copy_from_slice(&(rva as u64).to_le_bytes());
1789 info[off + 8..off + 16]
1790 .copy_from_slice(&CFG_CALL_TARGET_VALID.to_le_bytes());
1791 }
1792 write_verified(
1793 backend,
1794 process.pid,
1795 cfg_info,
1796 &info,
1797 "CFG_CALL_TARGET_INFO array",
1798 )?;
1799 let ok = call_guest_proc(
1800 backend,
1801 process.pid,
1802 &hook,
1803 stage + STAGE_TRAMPOLINE_OFFSET,
1804 stage + STAGE_PARAM_OFFSET,
1805 set_valid_call_targets,
1806 &cfg_call_target_registration_args(
1807 remote_base,
1808 pe.size_of_image as u64,
1809 targets.len() as u64,
1810 cfg_info,
1811 ),
1812 req.plan.execution.timeout_ms,
1813 "SetProcessValidCallTargets",
1814 )?;
1815 if let Ok(virtual_free) = resolve_import_symbol(
1816 backend,
1817 process.pid,
1818 "kernel32.dll",
1819 "VirtualFree",
1820 ) {
1821 let _ = backend.call_iat_hook(
1822 process.pid,
1823 &hook,
1824 virtual_free,
1825 [cfg_info, 0, MEM_RELEASE, 0],
1826 req.plan.execution.timeout_ms,
1827 );
1828 }
1829 if ok != 0 {
1830 cfg_marked = true;
1831 tracing::info!(
1832 pid = process.pid,
1833 remote_base = format_args!("{remote_base:#x}"),
1834 target_count = targets.len(),
1835 "CFG: marked all export/entry targets as valid indirect call targets"
1836 );
1837 } else {
1838 tracing::debug!(
1839 pid = process.pid,
1840 "CFG: SetProcessValidCallTargets returned FALSE; process may not have CFG enabled"
1841 );
1842 }
1843 }
1844 }
1845 }
1846 let dll_name = req
1847 .payload_path
1848 .file_name()
1849 .and_then(|n| n.to_str())
1850 .unwrap_or("payload.dll");
1851 let entry_point = pe
1852 .entry(remote_base as usize)?
1853 .map(|e| e as u64)
1854 .unwrap_or(0);
1855 peb_entry_addr = Some(synthesize_peb_loader_entry(
1856 backend,
1857 process.pid,
1858 &hook,
1859 virtual_alloc,
1860 stage,
1861 remote_base,
1862 entry_point,
1863 pe.size_of_image as u32,
1864 dll_name,
1865 req.plan.execution.timeout_ms,
1866 )?);
1867 if req.plan.cleanup == GuestCleanup::Tracked
1868 && let Some(peb_entry) = peb_entry_addr
1869 {
1870 unlink_synthesized_peb_loader_entry(backend, process.pid, peb_entry)?;
1871 peb_entry_unlinked = true;
1872 }
1873 }
1874
1875 if req.plan.vad_spoof == GuestVadSpoof::VadImageMap {
1876 if req.plan.image_backing != GuestImageBacking::Private {
1877 return Err(GuestInjectError::Unsupported {
1878 operation: "VAD type spoofing",
1879 reason: "vad-image-map applies only to private image backing".into(),
1880 });
1881 }
1882 backend.spoof_vad_type(process.pid, remote_base, pe.size_of_image as u64)?;
1883 tracing::info!(
1884 pid = process.pid,
1885 remote_base = format_args!("{remote_base:#x}"),
1886 "VAD type spoofed to VadImageMap for private mapping"
1887 );
1888 }
1889
1890 if let Some(entry) = pe.entry(remote_base as usize)? {
1891 tracing::info!(
1892 pid = process.pid,
1893 entry = format_args!("{entry:#x}"),
1894 execution = req.plan.execution.method.label(),
1895 "calling payload DllMain"
1896 );
1897 let ok = match req.plan.execution.method {
1898 GuestExecutionMethod::RemoteThread => invoke_dllmain_remote_thread(
1899 backend,
1900 process.pid,
1901 &hook,
1902 virtual_alloc,
1903 stage,
1904 entry as u64,
1905 remote_base,
1906 &image,
1907 &pe,
1908 req.plan.thread_starts == GuestThreadStartPolicy::RequireModuleBacked,
1909 req.plan.execution.timeout_ms,
1910 )?,
1911 _ => backend.call_iat_hook(
1912 process.pid,
1913 &hook,
1914 entry as u64,
1915 [remote_base, DLL_PROCESS_ATTACH as u64, 0, 0],
1916 req.plan.execution.timeout_ms,
1917 )?,
1918 };
1919 tracing::info!(
1920 pid = process.pid,
1921 entry_result = ok,
1922 "payload DllMain returned"
1923 );
1924 if ok == 0 {
1925 return Err(GuestInjectError::Image("DllMain returned FALSE".into()));
1926 }
1927 } else {
1928 tracing::info!("payload has no entry point");
1929 }
1930
1931 if req.plan.header_wipe == GuestHeaderWipe::AfterLoad {
1932 let wipe_len = pe.size_of_headers.min(0x1000);
1933 let zeros = vec![0u8; wipe_len];
1934 if req.plan.image_backing == GuestImageBacking::SecImage {
1935 let vp = virtual_protect.expect(
1936 "header wipe requires final_protections = section so VirtualProtect is resolved",
1937 );
1938 guest_virtual_protect(
1939 backend,
1940 process.pid,
1941 &hook,
1942 vp,
1943 remote_base,
1944 wipe_len as u64,
1945 PAGE_READWRITE,
1946 hook.result_addr + OLD_PROTECT_RESULT_OFFSET,
1947 req.plan.execution.timeout_ms,
1948 "header wipe",
1949 )?;
1950 }
1951 write_verified(backend, process.pid, remote_base, &zeros, "PE header wipe")?;
1952 tracing::info!(
1953 pid = process.pid,
1954 remote_base = format_args!("{remote_base:#x}"),
1955 bytes = wipe_len,
1956 "PE headers wiped"
1957 );
1958 }
1959
1960 let mut notes = vec![
1961 format!(
1962 "manual-mapped {} bytes from {}",
1963 req.payload_image.len(),
1964 req.payload_path.display()
1965 ),
1966 format!(
1967 "execution via {}!{} IAT slot {:#x}",
1968 req.plan.hook_module, req.plan.hook_function, hook.iat_slot
1969 ),
1970 format!(
1971 "dependencies={}, tls={}, final_protections={}",
1972 req.plan.dependency_policy.label(),
1973 req.plan.tls.label(),
1974 req.plan.final_protections.label()
1975 ),
1976 format!("loader_metadata={}", req.plan.loader_metadata.label()),
1977 format!("call_stack={}", req.plan.call_stack.label()),
1978 format!(
1979 "permission_transitions={}",
1980 req.plan.permission_transitions.label()
1981 ),
1982 format!("thread_starts={}", req.plan.thread_starts.label()),
1983 format!("image_backing={}", req.plan.image_backing.label()),
1984 format!("base_address={}", req.plan.base_address.label()),
1985 format!("header_wipe={}", req.plan.header_wipe.label()),
1986 format!("loader_entries={}", req.plan.loader_entries.label()),
1987 format!("stack_shaping={}", req.plan.stack_shaping.label()),
1988 format!("cleanup={}", req.plan.cleanup.label()),
1989 format!("vad_spoof={}", req.plan.vad_spoof.label()),
1990 ];
1991 notes.append(&mut loader_metadata_notes);
1992 notes.append(&mut call_stack_notes);
1993 notes.append(&mut thread_start_notes);
1994 if let Some(peb_entry) = peb_entry_addr {
1995 match peb_entry_unlinked {
1996 true => notes.push(format!(
1997 "loader entries: synthesized transient LDR_DATA_TABLE_ENTRY at {peb_entry:#x}, then unlinked from PEB InLoadOrder/InMemoryOrder lists by cleanup=tracked"
1998 )),
1999 false => notes.push(format!(
2000 "loader entries: synthesized LDR_DATA_TABLE_ENTRY at {peb_entry:#x} linked into PEB InLoadOrder/InMemoryOrder lists"
2001 )),
2002 }
2003 }
2004 if let Some(slot) = tls_slot_index {
2005 let thread_scope = match req.plan.execution.method {
2006 GuestExecutionMethod::RemoteThread => {
2007 "current helper thread only; the remote DllMain thread and other existing threads are not covered"
2008 }
2009 _ => "current target thread only; other threads are not covered",
2010 };
2011 notes.push(format!(
2012 "static TLS: allocated slot {slot} via TlsAlloc, patched index into image, copied TLS template, and called TlsSetValue for {thread_scope}"
2013 ));
2014 }
2015 if cfg_marked {
2016 notes.push(format!(
2017 "CFG: marked {cfg_target_count} export/entry targets as valid indirect call targets via SetProcessValidCallTargets before DllMain"
2018 ));
2019 }
2020 notes.extend(guest_artifact_notes(req, &pe, remote_base, has_static_tls));
2021
2022 Ok(GuestLoadInfo {
2023 method: self.name().into(),
2024 pid: process.pid,
2025 remote_base: Some(remote_base),
2026 notes,
2027 })
2028 })();
2029
2030 match &result {
2031 Ok(info) => tracing::info!(
2032 pid = info.pid,
2033 remote_base = info.remote_base.map(|base| format!("{base:#x}")).as_deref(),
2034 "guest injection completed"
2035 ),
2036 Err(err) => tracing::error!(error = %err, "guest injection failed"),
2037 }
2038 result
2039 }
2040}
2041
2042struct RemoteIatHook {
2043 iat_slot: u64,
2044 original_target: u64,
2045}
2046
2047struct IatHookTransaction<'a, B: GuestMemoryBackend + ?Sized> {
2048 backend: &'a B,
2049 pid: u32,
2050 hook: GuestIatHook,
2051 original_iat: [u8; 8],
2052 original_stub: Vec<u8>,
2053 original_result: Vec<u8>,
2054 restored: bool,
2055}
2056
2057impl<'a, B: GuestMemoryBackend + ?Sized> IatHookTransaction<'a, B> {
2058 fn prepare(
2059 backend: &'a B,
2060 pid: u32,
2061 hook: &GuestIatHook,
2062 stub_len: usize,
2063 ) -> Result<Self, GuestInjectError> {
2064 let current_iat = backend.read(pid, hook.iat_slot, 8)?;
2065 let expected_iat = hook.original_target.to_le_bytes();
2066 if current_iat != expected_iat {
2067 return Err(GuestInjectError::Unsupported {
2068 operation: "guest iat-hook execution",
2069 reason: format!(
2070 "IAT slot {:#x} changed before arming; refusing nested or stale hook",
2071 hook.iat_slot
2072 ),
2073 });
2074 }
2075 let original_stub = backend.read(pid, hook.stub_addr, stub_len)?;
2076 let original_result = backend.read(pid, hook.result_addr, RESULT_BLOCK_SIZE)?;
2077 Ok(Self {
2078 backend,
2079 pid,
2080 hook: *hook,
2081 original_iat: expected_iat,
2082 original_stub,
2083 original_result,
2084 restored: false,
2085 })
2086 }
2087
2088 fn restore(&mut self) -> Result<(), GuestInjectError> {
2089 if self.restored {
2090 return Ok(());
2091 }
2092 let mut errors = Vec::new();
2093 for (addr, bytes, label) in [
2094 (self.hook.iat_slot, self.original_iat.as_slice(), "IAT slot"),
2095 (
2096 self.hook.stub_addr,
2097 self.original_stub.as_slice(),
2098 "stub bytes",
2099 ),
2100 (
2101 self.hook.result_addr,
2102 self.original_result.as_slice(),
2103 "result bytes",
2104 ),
2105 ] {
2106 if let Err(err) = self.backend.write(self.pid, addr, bytes) {
2107 errors.push(format!("{label} at {addr:#x}: {err}"));
2108 }
2109 }
2110 self.restored = true;
2111 match errors.is_empty() {
2112 true => Ok(()),
2113 false => Err(GuestInjectError::Backend(format!(
2114 "failed restoring guest IAT hook transaction: {}",
2115 errors.join("; ")
2116 ))),
2117 }
2118 }
2119}
2120
2121impl<B: GuestMemoryBackend + ?Sized> Drop for IatHookTransaction<'_, B> {
2122 fn drop(&mut self) {
2123 if !self.restored
2124 && let Err(err) = self.restore()
2125 {
2126 tracing::warn!(pid = self.pid, error = %err, "guest IAT-hook transaction restore failed");
2127 }
2128 }
2129}
2130
2131fn memory_iat_call<B: GuestMemoryBackend + ?Sized>(
2132 backend: &B,
2133 pid: u32,
2134 hook: &GuestIatHook,
2135 function: u64,
2136 args: [u64; 4],
2137 timeout_ms: u32,
2138) -> Result<u64, GuestInjectError> {
2139 let zero_result = vec![0u8; RESULT_BLOCK_SIZE];
2140 let stub = call_stub(hook, function, args);
2141 let mut transaction = IatHookTransaction::prepare(backend, pid, hook, stub.len())?;
2142 tracing::debug!(
2143 pid,
2144 function = format_args!("{function:#x}"),
2145 iat_slot = format_args!("{:#x}", hook.iat_slot),
2146 original_target = format_args!("{:#x}", hook.original_target),
2147 stub_addr = format_args!("{:#x}", hook.stub_addr),
2148 result_addr = format_args!("{:#x}", hook.result_addr),
2149 arg0 = format_args!("{:#x}", args[0]),
2150 arg1 = format_args!("{:#x}", args[1]),
2151 arg2 = format_args!("{:#x}", args[2]),
2152 arg3 = format_args!("{:#x}", args[3]),
2153 timeout_ms,
2154 "guest IAT-hook call installing stub"
2155 );
2156 write_verified(
2157 backend,
2158 pid,
2159 hook.result_addr,
2160 &zero_result,
2161 "guest IAT-hook result block",
2162 )?;
2163 write_verified(
2164 backend,
2165 pid,
2166 hook.stub_addr,
2167 &stub,
2168 "guest IAT-hook call stub",
2169 )?;
2170 write_verified(
2171 backend,
2172 pid,
2173 hook.iat_slot,
2174 &hook.stub_addr.to_le_bytes(),
2175 "guest IAT-hook thunk",
2176 )?;
2177 tracing::debug!(
2178 pid,
2179 iat_slot = format_args!("{:#x}", hook.iat_slot),
2180 stub_bytes = stub.len(),
2181 "guest IAT-hook call armed"
2182 );
2183 let deadline = Instant::now() + Duration::from_millis(timeout_ms as u64);
2184 loop {
2185 let result = backend.read(pid, hook.result_addr, RESULT_BLOCK_SIZE)?;
2186 let state = u64::from_le_bytes(result[0..8].try_into().unwrap());
2187 let value = u64::from_le_bytes(result[8..16].try_into().unwrap());
2188 if state == RESULT_STATE {
2189 tracing::debug!(
2190 pid,
2191 function = format_args!("{function:#x}"),
2192 return_value = format_args!("{value:#x}"),
2193 "guest IAT-hook call completed"
2194 );
2195 transaction.restore()?;
2196 return Ok(value);
2197 }
2198 if Instant::now() >= deadline {
2199 let restore_result = transaction.restore();
2200 tracing::warn!(
2201 pid,
2202 function = format_args!("{function:#x}"),
2203 iat_slot = format_args!("{:#x}", hook.iat_slot),
2204 result_state = format_args!("{state:#x}"),
2205 result_value = format_args!("{value:#x}"),
2206 timeout_ms,
2207 "guest IAT-hook call timed out; restored original IAT target"
2208 );
2209 if let Err(err) = restore_result {
2210 tracing::warn!(pid, error = %err, "guest IAT-hook timeout restore failed");
2211 }
2212 return Err(GuestInjectError::Unsupported {
2213 operation: "guest iat-hook execution",
2214 reason: format!(
2215 "target did not call the configured import before {} ms",
2216 timeout_ms
2217 ),
2218 });
2219 }
2220 thread::sleep(Duration::from_millis(10));
2221 }
2222}
2223
2224fn memory_iat_touch<B: GuestMemoryBackend + ?Sized>(
2225 backend: &B,
2226 pid: u32,
2227 hook: &GuestIatHook,
2228 addr: u64,
2229 len: usize,
2230 timeout_ms: u32,
2231) -> Result<(), GuestInjectError> {
2232 let zero_result = vec![0u8; RESULT_BLOCK_SIZE];
2233 let stub = touch_stub(hook, addr, len);
2234 let mut transaction = IatHookTransaction::prepare(backend, pid, hook, stub.len())?;
2235 tracing::debug!(
2236 pid,
2237 iat_slot = format_args!("{:#x}", hook.iat_slot),
2238 original_target = format_args!("{:#x}", hook.original_target),
2239 stub_addr = format_args!("{:#x}", hook.stub_addr),
2240 result_addr = format_args!("{:#x}", hook.result_addr),
2241 addr = format_args!("{addr:#x}"),
2242 len,
2243 timeout_ms,
2244 "guest IAT-hook page touch installing stub"
2245 );
2246 write_verified(
2247 backend,
2248 pid,
2249 hook.result_addr,
2250 &zero_result,
2251 "guest IAT-hook result block",
2252 )?;
2253 write_verified(
2254 backend,
2255 pid,
2256 hook.stub_addr,
2257 &stub,
2258 "guest IAT-hook page-touch stub",
2259 )?;
2260 write_verified(
2261 backend,
2262 pid,
2263 hook.iat_slot,
2264 &hook.stub_addr.to_le_bytes(),
2265 "guest IAT-hook thunk",
2266 )?;
2267 tracing::debug!(
2268 pid,
2269 iat_slot = format_args!("{:#x}", hook.iat_slot),
2270 stub_bytes = stub.len(),
2271 "guest IAT-hook page touch armed"
2272 );
2273 let deadline = Instant::now() + Duration::from_millis(timeout_ms as u64);
2274 loop {
2275 let result = backend.read(pid, hook.result_addr, RESULT_BLOCK_SIZE)?;
2276 let state = u64::from_le_bytes(result[0..8].try_into().unwrap());
2277 let value = u64::from_le_bytes(result[8..16].try_into().unwrap());
2278 if state == RESULT_STATE {
2279 tracing::debug!(
2280 pid,
2281 addr = format_args!("{addr:#x}"),
2282 len,
2283 return_value = format_args!("{value:#x}"),
2284 "guest IAT-hook page touch completed"
2285 );
2286 transaction.restore()?;
2287 return Ok(());
2288 }
2289 if Instant::now() >= deadline {
2290 let restore_result = transaction.restore();
2291 tracing::warn!(
2292 pid,
2293 iat_slot = format_args!("{:#x}", hook.iat_slot),
2294 result_state = format_args!("{state:#x}"),
2295 result_value = format_args!("{value:#x}"),
2296 timeout_ms,
2297 "guest IAT-hook page touch timed out; restored original IAT target"
2298 );
2299 if let Err(err) = restore_result {
2300 tracing::warn!(pid, error = %err, "guest IAT-hook page-touch timeout restore failed");
2301 }
2302 return Err(GuestInjectError::Unsupported {
2303 operation: "guest iat-hook page touch",
2304 reason: format!(
2305 "target did not call the configured import before {} ms",
2306 timeout_ms
2307 ),
2308 });
2309 }
2310 thread::sleep(Duration::from_millis(10));
2311 }
2312}
2313
2314fn memory_iat_read_touch<B: GuestMemoryBackend + ?Sized>(
2315 backend: &B,
2316 pid: u32,
2317 hook: &GuestIatHook,
2318 addr: u64,
2319 len: usize,
2320 timeout_ms: u32,
2321) -> Result<(), GuestInjectError> {
2322 let zero_result = vec![0u8; RESULT_BLOCK_SIZE];
2323 let stub = read_touch_stub(hook, addr, len);
2324 let mut transaction = IatHookTransaction::prepare(backend, pid, hook, stub.len())?;
2325 tracing::debug!(
2326 pid,
2327 iat_slot = format_args!("{:#x}", hook.iat_slot),
2328 original_target = format_args!("{:#x}", hook.original_target),
2329 stub_addr = format_args!("{:#x}", hook.stub_addr),
2330 result_addr = format_args!("{:#x}", hook.result_addr),
2331 addr = format_args!("{addr:#x}"),
2332 len,
2333 timeout_ms,
2334 "guest IAT-hook read-touch installing stub"
2335 );
2336 write_verified(
2337 backend,
2338 pid,
2339 hook.result_addr,
2340 &zero_result,
2341 "guest IAT-hook result block",
2342 )?;
2343 write_verified(
2344 backend,
2345 pid,
2346 hook.stub_addr,
2347 &stub,
2348 "guest IAT-hook read-touch stub",
2349 )?;
2350 write_verified(
2351 backend,
2352 pid,
2353 hook.iat_slot,
2354 &hook.stub_addr.to_le_bytes(),
2355 "guest IAT-hook thunk",
2356 )?;
2357 tracing::debug!(
2358 pid,
2359 iat_slot = format_args!("{:#x}", hook.iat_slot),
2360 stub_bytes = stub.len(),
2361 "guest IAT-hook read touch armed"
2362 );
2363 let deadline = Instant::now() + Duration::from_millis(timeout_ms as u64);
2364 loop {
2365 let result = backend.read(pid, hook.result_addr, RESULT_BLOCK_SIZE)?;
2366 let state = u64::from_le_bytes(result[0..8].try_into().unwrap());
2367 let value = u64::from_le_bytes(result[8..16].try_into().unwrap());
2368 if state == RESULT_STATE {
2369 tracing::debug!(
2370 pid,
2371 addr = format_args!("{addr:#x}"),
2372 len,
2373 return_value = format_args!("{value:#x}"),
2374 "guest IAT-hook read touch completed"
2375 );
2376 transaction.restore()?;
2377 return Ok(());
2378 }
2379 if Instant::now() >= deadline {
2380 let restore_result = transaction.restore();
2381 tracing::warn!(
2382 pid,
2383 iat_slot = format_args!("{:#x}", hook.iat_slot),
2384 result_state = format_args!("{state:#x}"),
2385 result_value = format_args!("{value:#x}"),
2386 timeout_ms,
2387 "guest IAT-hook read touch timed out; restored original IAT target"
2388 );
2389 if let Err(err) = restore_result {
2390 tracing::warn!(pid, error = %err, "guest IAT-hook read-touch timeout restore failed");
2391 }
2392 return Err(GuestInjectError::Unsupported {
2393 operation: "guest iat-hook read touch",
2394 reason: format!(
2395 "target did not call the configured import before {} ms",
2396 timeout_ms
2397 ),
2398 });
2399 }
2400 thread::sleep(Duration::from_millis(10));
2401 }
2402}
2403
2404fn memory_iat_preserve_touch<B: GuestMemoryBackend + ?Sized>(
2405 backend: &B,
2406 pid: u32,
2407 hook: &GuestIatHook,
2408 addr: u64,
2409 len: usize,
2410 timeout_ms: u32,
2411) -> Result<(), GuestInjectError> {
2412 let zero_result = vec![0u8; RESULT_BLOCK_SIZE];
2413 let stub = preserve_touch_stub(hook, addr, len);
2414 let mut transaction = IatHookTransaction::prepare(backend, pid, hook, stub.len())?;
2415 tracing::debug!(
2416 pid,
2417 iat_slot = format_args!("{:#x}", hook.iat_slot),
2418 original_target = format_args!("{:#x}", hook.original_target),
2419 stub_addr = format_args!("{:#x}", hook.stub_addr),
2420 result_addr = format_args!("{:#x}", hook.result_addr),
2421 addr = format_args!("{addr:#x}"),
2422 len,
2423 timeout_ms,
2424 "guest IAT-hook preserve-touch installing stub"
2425 );
2426 write_verified(
2427 backend,
2428 pid,
2429 hook.result_addr,
2430 &zero_result,
2431 "guest IAT-hook result block",
2432 )?;
2433 write_verified(
2434 backend,
2435 pid,
2436 hook.stub_addr,
2437 &stub,
2438 "guest IAT-hook preserve-touch stub",
2439 )?;
2440 write_verified(
2441 backend,
2442 pid,
2443 hook.iat_slot,
2444 &hook.stub_addr.to_le_bytes(),
2445 "guest IAT-hook thunk",
2446 )?;
2447 tracing::debug!(
2448 pid,
2449 iat_slot = format_args!("{:#x}", hook.iat_slot),
2450 stub_bytes = stub.len(),
2451 "guest IAT-hook preserve touch armed"
2452 );
2453 let deadline = Instant::now() + Duration::from_millis(timeout_ms as u64);
2454 loop {
2455 let result = backend.read(pid, hook.result_addr, RESULT_BLOCK_SIZE)?;
2456 let state = u64::from_le_bytes(result[0..8].try_into().unwrap());
2457 let value = u64::from_le_bytes(result[8..16].try_into().unwrap());
2458 if state == RESULT_STATE {
2459 tracing::debug!(
2460 pid,
2461 addr = format_args!("{addr:#x}"),
2462 len,
2463 return_value = format_args!("{value:#x}"),
2464 "guest IAT-hook preserve touch completed"
2465 );
2466 transaction.restore()?;
2467 return Ok(());
2468 }
2469 if Instant::now() >= deadline {
2470 let restore_result = transaction.restore();
2471 tracing::warn!(
2472 pid,
2473 iat_slot = format_args!("{:#x}", hook.iat_slot),
2474 result_state = format_args!("{state:#x}"),
2475 result_value = format_args!("{value:#x}"),
2476 timeout_ms,
2477 "guest IAT-hook preserve touch timed out; restored original IAT target"
2478 );
2479 if let Err(err) = restore_result {
2480 tracing::warn!(
2481 pid,
2482 error = %err,
2483 "guest IAT-hook preserve-touch timeout restore failed"
2484 );
2485 }
2486 return Err(GuestInjectError::Unsupported {
2487 operation: "guest iat-hook preserve touch",
2488 reason: format!(
2489 "target did not call the configured import before {} ms",
2490 timeout_ms
2491 ),
2492 });
2493 }
2494 thread::sleep(Duration::from_millis(10));
2495 }
2496}
2497
2498fn guest_artifact_notes(
2499 req: &GuestInjectionRequest<'_>,
2500 pe: &Pe,
2501 remote_base: u64,
2502 has_static_tls: bool,
2503) -> Vec<String> {
2504 let mut notes = vec![
2505 match req.plan.image_backing {
2506 GuestImageBacking::Private => format!(
2507 "artifact audit: payload image is a private committed mapping at {remote_base:#x}, not a loader-created SEC_IMAGE section"
2508 ),
2509 GuestImageBacking::SecImage => format!(
2510 "artifact audit: payload image at {remote_base:#x} is backed by a real SEC_IMAGE section over a staged guest copy of the payload file; the section object and image-file VAD backing are kernel-created, not forged; only patched pages (imports, security cookie) become copy-on-write private, unpatched pages remain file-backed"
2511 ),
2512 },
2513 match req.plan.loader_entries {
2514 GuestLoaderEntries::Absent => {
2515 "artifact audit: PEB loader lists, normal module enumeration, and loader VAD/section-object metadata are not synthesized by guest manual-map".into()
2516 }
2517 GuestLoaderEntries::Synthesized if req.plan.cleanup == GuestCleanup::Tracked => {
2518 "artifact audit: synthesized LDR_DATA_TABLE_ENTRY is linked transiently into PEB InLoadOrder/InMemoryOrder lists, then unlinked by cleanup=tracked to avoid leaving loader-owned shutdown state behind".into()
2519 }
2520 GuestLoaderEntries::Synthesized => {
2521 "artifact audit: synthesized LDR_DATA_TABLE_ENTRY linked into PEB InLoadOrder/InMemoryOrder lists; initialization-order links are left private to avoid loader shutdown ownership of the manual map".into()
2522 }
2523 },
2524 match req.plan.image_backing {
2525 GuestImageBacking::Private => {
2526 "artifact audit: PE headers, sections, imports, relocations, delay imports, TLS callbacks, and entrypoint were processed by decant rather than the Windows loader".into()
2527 }
2528 GuestImageBacking::SecImage => {
2529 "artifact audit: SEC_IMAGE provided the image section layout; relocations, imports, delay imports, TLS callbacks, and entrypoint were processed by decant rather than the Windows loader".into()
2530 }
2531 },
2532 format!(
2533 "artifact audit: execution method {} runs DllMain{}; {}",
2534 req.plan.execution.method.label(),
2535 match req.plan.execution.method {
2536 GuestExecutionMethod::RemoteThread => " on a kernel-created remote thread through a ThreadProc thunk",
2537 _ => " on the target thread that calls the hooked import",
2538 },
2539 match req.plan.execution.method {
2540 GuestExecutionMethod::RemoteThread => "the kernel-recorded thread start is the helper thunk, not the payload entrypoint",
2541 _ => "no remote thread or APC is created by this path",
2542 }
2543 ),
2544 ];
2545
2546 match req.plan.final_protections {
2547 GuestFinalProtections::Rwx => notes.push(
2548 "artifact audit: final_protections=rwx leaves the mapped image writable and executable"
2549 .into(),
2550 ),
2551 GuestFinalProtections::Section => match req.plan.permission_transitions {
2552 GuestPermissionTransitions::Standard => notes.push(
2553 "artifact audit: final_protections=section allocates RW memory, writes the image, then applies PE-derived page protections before TLS callbacks and DllMain"
2554 .into(),
2555 ),
2556 GuestPermissionTransitions::WriteThroughFinal => notes.push(
2557 "artifact audit: permission_transitions=write-through-final allocates with final-ish image permissions, materializes pages by read touch when possible, writes through memflow, and skips section protects that already match the initial protection"
2558 .into(),
2559 ),
2560 },
2561 }
2562 match req.plan.call_stack {
2563 GuestCallStackPolicy::Native => notes.push(
2564 "artifact audit: call_stack=native leaves the IAT-hook stub without dynamic unwind registration"
2565 .into(),
2566 ),
2567 GuestCallStackPolicy::RegisteredUnwind => notes.push(
2568 "artifact audit: call_stack=registered-unwind registers x64 unwind metadata for the IAT-hook stub; caller frames are not spoofed"
2569 .into(),
2570 ),
2571 }
2572 match req.plan.thread_starts {
2573 GuestThreadStartPolicy::ExistingThread => {
2574 if req.plan.execution.method == GuestExecutionMethod::RemoteThread {
2575 notes.push(
2576 "artifact audit: thread_starts=existing-thread is not applicable to remote-thread execution; a guest thread is created with a helper thunk as its recorded start address, using a temporary thunk when no payload-image cave is available"
2577 .into(),
2578 );
2579 } else {
2580 notes.push(
2581 "artifact audit: thread_starts=existing-thread creates no guest thread, so no new thread start address is recorded by this path"
2582 .into(),
2583 );
2584 }
2585 }
2586 GuestThreadStartPolicy::RequireModuleBacked => {
2587 if req.plan.execution.method == GuestExecutionMethod::RemoteThread {
2588 notes.push(
2589 "artifact audit: thread_starts=require-module-backed requires the remote-thread ThreadProc thunk to be placed in a payload-image executable code cave; no temporary thread-start fallback is allowed"
2590 .into(),
2591 );
2592 } else {
2593 notes.push(
2594 "artifact audit: thread_starts=require-module-backed verified the IAT-hook plumbing is inside loaded module ranges; payload entrypoints and helper calls are not thread-start metadata"
2595 .into(),
2596 );
2597 }
2598 }
2599 }
2600
2601 if req.plan.dependency_policy == GuestDependencyPolicy::LoadWithGuestLoader {
2602 notes.push(
2603 "artifact audit: missing dependencies are loaded through guest LoadLibraryA/GetProcAddress; the payload image itself remains manual-mapped"
2604 .into(),
2605 );
2606 }
2607 if req.plan.loader_metadata == GuestLoaderMetadataPolicy::BestEffort {
2608 notes.push(
2609 "artifact audit: loader_metadata=best-effort registers public runtime metadata that can be expressed through guest exports"
2610 .into(),
2611 );
2612 }
2613 if has_static_tls {
2614 match req.plan.loader_entries {
2615 GuestLoaderEntries::Synthesized => {
2616 let thread_scope = match req.plan.execution.method {
2617 GuestExecutionMethod::RemoteThread => {
2618 "the current helper thread only; the remote DllMain thread and other existing threads are not covered"
2619 }
2620 _ => "the current target thread only; other threads are not covered",
2621 };
2622 notes.push(format!(
2623 "artifact audit: static TLS slot allocated via TlsAlloc, index patched into image buffer, template copied and TlsSetValue called for {thread_scope}"
2624 ));
2625 }
2626 GuestLoaderEntries::Absent => notes.push(
2627 "artifact audit: static TLS directory is present, but loader-managed TLS slots are not registered".into(),
2628 ),
2629 }
2630 }
2631 if pe.has_exception_directory() {
2632 match req.plan.loader_metadata {
2633 GuestLoaderMetadataPolicy::BestEffort => notes.push(
2634 "artifact audit: exception directory is present and RtlAddFunctionTable was called for the mapped image"
2635 .into(),
2636 ),
2637 _ => notes.push(
2638 "artifact audit: exception directory is present, but unwind function tables are not registered"
2639 .into(),
2640 ),
2641 }
2642 }
2643 if pe.has_load_config() {
2644 match req.plan.loader_metadata {
2645 GuestLoaderMetadataPolicy::BestEffort => notes.push(
2646 "artifact audit: load-config directory is present; default security-cookie state is seeded when exposed, and a CFG valid-call-target mark is requested when loader_entries=synthesized; broader loader-private entries are not synthesized"
2647 .into(),
2648 ),
2649 _ => notes.push(
2650 "artifact audit: load-config directory is present, but CFG/security-cookie loader metadata is not processed"
2651 .into(),
2652 ),
2653 }
2654 }
2655 match req.plan.header_wipe {
2656 GuestHeaderWipe::None => {}
2657 GuestHeaderWipe::AfterLoad => notes.push(
2658 "artifact audit: PE headers (DOS header, NT headers, section headers) were zeroed after DllMain returned".into(),
2659 ),
2660 }
2661 match req.plan.stack_shaping {
2662 GuestStackShaping::Native => {}
2663 GuestStackShaping::Spoofed => {
2664 if req.plan.execution.method == GuestExecutionMethod::RemoteThread {
2665 notes.push(
2666 "artifact audit: stack_shaping=spoofed is disabled for remote-thread launch helpers; remote-thread DllMain runs through a ThreadProc thunk and is not stack-shaped"
2667 .into(),
2668 );
2669 } else {
2670 notes.push(
2671 "artifact audit: stack_shaping=spoofed writes a synthetic return address from a loaded module onto the stack before payload calls so stack walks attribute the call to a legitimate module"
2672 .into(),
2673 );
2674 }
2675 }
2676 }
2677 match req.plan.cleanup {
2678 GuestCleanup::Resident => {}
2679 GuestCleanup::Tracked => notes.push(
2680 "artifact audit: cleanup=tracked removes transient loader-list links after load; payload memory remains resident for the running fixture".into(),
2681 ),
2682 }
2683 match req.plan.vad_spoof {
2684 GuestVadSpoof::Off => {}
2685 GuestVadSpoof::VadImageMap => notes.push(
2686 "artifact audit: vad_spoof=vad-image-map completed before DllMain; this mutates only VAD metadata for a private mapping and does not create a real SEC_IMAGE section object, so deep kernel inspection may still distinguish it from loader-created image backing".into(),
2687 ),
2688 }
2689 notes
2690}
2691
2692#[allow(clippy::too_many_arguments)]
2693fn protect_guest_sections(
2694 backend: &dyn GuestMemoryBackend,
2695 pid: u32,
2696 hook: &GuestIatHook,
2697 virtual_protect: u64,
2698 remote_base: u64,
2699 pe: &Pe,
2700 skip_initial_protect: Option<u64>,
2701 timeout_ms: u32,
2702) -> Result<(), GuestInjectError> {
2703 let old_protect = hook.result_addr + OLD_PROTECT_RESULT_OFFSET;
2704 if pe.size_of_headers != 0 {
2705 match skip_initial_protect == Some(PAGE_READONLY) {
2706 true => tracing::debug!(
2707 pid,
2708 remote_base = format_args!("{remote_base:#x}"),
2709 "guest final header protection already matches initial allocation"
2710 ),
2711 false => guest_virtual_protect(
2712 backend,
2713 pid,
2714 hook,
2715 virtual_protect,
2716 remote_base,
2717 pe.size_of_headers as u64,
2718 PAGE_READONLY,
2719 old_protect,
2720 timeout_ms,
2721 "headers",
2722 )?,
2723 }
2724 }
2725
2726 let mut applied = 0usize;
2727 let mut skipped = 0usize;
2728 for section in &pe.sections {
2729 let size = guest_section_size(section);
2730 if size == 0 {
2731 continue;
2732 }
2733 let addr = remote_base
2734 .checked_add(u64::from(section.virtual_address))
2735 .ok_or_else(|| GuestInjectError::Image("section address overflows".into()))?;
2736 let protect = guest_section_protect(section.characteristics);
2737 if skip_initial_protect == Some(protect) {
2738 skipped += 1;
2739 tracing::debug!(
2740 pid,
2741 addr = format_args!("{addr:#x}"),
2742 protect = format_args!("{protect:#x}"),
2743 "guest final section protection already matches initial allocation"
2744 );
2745 continue;
2746 }
2747 guest_virtual_protect(
2748 backend,
2749 pid,
2750 hook,
2751 virtual_protect,
2752 addr,
2753 u64::from(size),
2754 protect,
2755 old_protect,
2756 timeout_ms,
2757 "section",
2758 )?;
2759 applied += 1;
2760 }
2761 tracing::info!(
2762 pid,
2763 remote_base = format_args!("{remote_base:#x}"),
2764 sections = applied,
2765 skipped,
2766 "guest final section protections applied"
2767 );
2768 Ok(())
2769}
2770
2771fn find_spoofed_return(
2772 backend: &dyn GuestMemoryBackend,
2773 pid: u32,
2774) -> Result<GuestSpoofedReturn, GuestInjectError> {
2775 let modules = backend.module_list(pid)?;
2776 let regions = backend.memory_map(pid).unwrap_or_else(|err| {
2777 tracing::debug!(
2778 pid,
2779 error = %err,
2780 "stack spoofing: guest memory map unavailable; falling back to bounded module scan"
2781 );
2782 Vec::new()
2783 });
2784
2785 for preferred in ["ntdll.dll", "kernel32.dll", "kernelbase.dll"] {
2786 for module in modules
2787 .iter()
2788 .filter(|module| module.name.eq_ignore_ascii_case(preferred))
2789 {
2790 for (start, end) in spoofed_return_scan_ranges(module, ®ions) {
2791 if let Some(gadget) = find_spoofed_return_in_guest_range(backend, pid, start, end)?
2792 {
2793 tracing::info!(
2794 pid,
2795 addr = format_args!("{:#x}", gadget.gadget_addr),
2796 module = %module.name,
2797 stack_adjust = format_args!("{:#x}", gadget.stack_adjust),
2798 range_start = format_args!("{start:#x}"),
2799 range_end = format_args!("{end:#x}"),
2800 "stack spoofing: found shadow-space-safe return gadget"
2801 );
2802 return Ok(gadget);
2803 }
2804 }
2805 }
2806 }
2807
2808 Err(GuestInjectError::Backend(
2809 "stack spoofing: no shadow-space-safe return gadget found in readable executable ntdll/kernel32/kernelbase ranges".into(),
2810 ))
2811}
2812
2813fn spoofed_return_scan_ranges(
2814 module: &GuestModuleInfo,
2815 regions: &[GuestMemoryRegion],
2816) -> Vec<(u64, u64)> {
2817 let module_end = module.base.saturating_add(module.size);
2818 let mut ranges = Vec::new();
2819 let mut remaining = module.size.min(SPOOFED_RETURN_SCAN_LIMIT);
2820
2821 for region in regions {
2822 if remaining == 0 {
2823 break;
2824 }
2825 if !region.readable || !region.executable {
2826 continue;
2827 }
2828 let region_end = region.base.saturating_add(region.size);
2829 let start = region.base.max(module.base);
2830 let end = region_end.min(module_end);
2831 if start >= end {
2832 continue;
2833 }
2834 let capped_end = start.saturating_add((end - start).min(remaining));
2835 ranges.push((start, capped_end));
2836 remaining = remaining.saturating_sub(capped_end - start);
2837 }
2838
2839 if ranges.is_empty() && module.size != 0 {
2840 ranges.push((
2841 module.base,
2842 module
2843 .base
2844 .saturating_add(module.size.min(SPOOFED_RETURN_SCAN_LIMIT)),
2845 ));
2846 }
2847
2848 ranges
2849}
2850
2851fn find_spoofed_return_in_guest_range(
2852 backend: &dyn GuestMemoryBackend,
2853 pid: u32,
2854 start: u64,
2855 end: u64,
2856) -> Result<Option<GuestSpoofedReturn>, GuestInjectError> {
2857 let mut addr = start;
2858 while addr < end {
2859 let len = (end - addr).min(GUEST_PAGE_SIZE as u64) as usize;
2860 match backend.read(pid, addr, len) {
2861 Ok(bytes) => {
2862 if let Some((off, stack_adjust)) = find_spoofed_return_gadget(&bytes) {
2863 return Ok(Some(GuestSpoofedReturn {
2864 gadget_addr: addr + off as u64,
2865 stack_adjust,
2866 }));
2867 }
2868 }
2869 Err(err) if is_process_gone_error(&err) => return Err(err),
2870 Err(err) => {
2871 tracing::debug!(
2872 pid,
2873 addr = format_args!("{addr:#x}"),
2874 len,
2875 error = %err,
2876 "stack spoofing: skipping unreadable scan chunk"
2877 );
2878 }
2879 }
2880 addr = addr.saturating_add(len as u64);
2881 }
2882 Ok(None)
2883}
2884
2885fn find_spoofed_return_gadget(bytes: &[u8]) -> Option<(usize, u8)> {
2886 for (stack_adjust, pattern) in [
2887 (0x20, [0x48, 0x83, 0xC4, 0x20, 0xC3]),
2888 (0x28, [0x48, 0x83, 0xC4, 0x28, 0xC3]),
2889 ] {
2890 if let Some(off) = bytes.windows(pattern.len()).position(|w| w == pattern) {
2891 return Some((off, stack_adjust));
2892 }
2893 }
2894 None
2895}
2896
2897fn random_base_address() -> u64 {
2898 let time = SystemTime::now()
2899 .duration_since(UNIX_EPOCH)
2900 .map(|d| d.as_nanos() as u64)
2901 .unwrap_or(0);
2902 let pid = std::process::id() as u64;
2903 let mut state = time ^ pid.rotate_left(17) ^ 0x9E37_79B9_7F4A_7C15;
2904 state ^= state >> 33;
2905 state = state.wrapping_mul(0xff51_afd7_ed55_8ccd);
2906 state ^= state >> 33;
2907 let range = 0x7FF0_0000_0000u64 - 0x1000_0000u64;
2908 let offset = state % range;
2909 let base = 0x1000_0000u64 + offset;
2910 base & !0xFFFF
2911}
2912
2913fn remote_thread_param_block(
2914 entry_point: u64,
2915 remote_base: u64,
2916 exit_code: u64,
2917 status: u64,
2918) -> [u8; REMOTE_THREAD_PARAM_SIZE] {
2919 let values = [
2920 entry_point,
2921 remote_base,
2922 DLL_PROCESS_ATTACH as u64,
2923 0,
2924 exit_code,
2925 status,
2926 ];
2927 let mut block = [0u8; REMOTE_THREAD_PARAM_SIZE];
2928 for (i, value) in values.into_iter().enumerate() {
2929 let off = i * 8;
2930 block[off..off + 8].copy_from_slice(&value.to_le_bytes());
2931 }
2932 block
2933}
2934
2935fn poll_remote_thread_exit(
2936 backend: &dyn GuestMemoryBackend,
2937 pid: u32,
2938 status_addr: u64,
2939 exit_code_addr: u64,
2940 timeout_ms: u32,
2941) -> Result<u64, GuestInjectError> {
2942 let deadline = Instant::now() + Duration::from_millis(timeout_ms as u64);
2943 loop {
2944 let status = read_remote_u32(backend, pid, status_addr)?;
2945 if status != 0 {
2946 return read_remote_u32(backend, pid, exit_code_addr).map(u64::from);
2947 }
2948 if Instant::now() >= deadline {
2949 return Err(GuestInjectError::Backend(format!(
2950 "remote thread did not complete within {timeout_ms} ms"
2951 )));
2952 }
2953 thread::sleep(Duration::from_millis(10));
2954 }
2955}
2956
2957fn best_effort_virtual_free(
2958 backend: &dyn GuestMemoryBackend,
2959 pid: u32,
2960 hook: &GuestIatHook,
2961 virtual_free: u64,
2962 addr: u64,
2963 timeout_ms: u32,
2964) {
2965 if addr != 0 {
2966 let _ = backend.call_iat_hook(
2967 pid,
2968 hook,
2969 virtual_free,
2970 [addr, 0, MEM_RELEASE, 0],
2971 timeout_ms,
2972 );
2973 }
2974}
2975
2976#[allow(clippy::too_many_arguments)]
2977fn invoke_dllmain_remote_thread(
2978 backend: &dyn GuestMemoryBackend,
2979 pid: u32,
2980 hook: &GuestIatHook,
2981 virtual_alloc: u64,
2982 stage: u64,
2983 entry_point: u64,
2984 remote_base: u64,
2985 image: &[u8],
2986 pe: &Pe,
2987 require_module_backed_start: bool,
2988 timeout_ms: u32,
2989) -> Result<u64, GuestInjectError> {
2990 let helper_hook = GuestIatHook {
2991 spoofed_return: None,
2992 ..*hook
2993 };
2994 let hook = &helper_hook;
2995 let trampoline = stage + STAGE_TRAMPOLINE_OFFSET;
2996 let param_block = stage + STAGE_PARAM_OFFSET;
2997 let create_thread = resolve_import_symbol(backend, pid, "kernel32.dll", "CreateThread")?;
2998 let virtual_free = resolve_import_symbol(backend, pid, "kernel32.dll", "VirtualFree")?;
2999 let close_handle = resolve_import_symbol(backend, pid, "kernel32.dll", "CloseHandle")?;
3000 let virtual_protect = resolve_import_symbol(backend, pid, "kernel32.dll", "VirtualProtect")?;
3001
3002 let thunk_code_size = REMOTE_THREAD_DLLMAIN_THUNK.len();
3003 let scratch = backend.call_iat_hook(
3004 pid,
3005 hook,
3006 virtual_alloc,
3007 [
3008 0,
3009 REMOTE_THREAD_THUNK_ALLOCATION_SIZE,
3010 MEM_COMMIT_RESERVE,
3011 PAGE_EXECUTE_READWRITE,
3012 ],
3013 timeout_ms,
3014 )?;
3015 if scratch == 0 {
3016 return Err(GuestInjectError::Backend(
3017 "VirtualAlloc returned NULL for remote-thread scratch".into(),
3018 ));
3019 }
3020 if let Err(err) = backend.touch_iat_hook(
3021 pid,
3022 hook,
3023 scratch,
3024 REMOTE_THREAD_THUNK_ALLOCATION_SIZE as usize,
3025 timeout_ms,
3026 ) {
3027 best_effort_virtual_free(backend, pid, hook, virtual_free, scratch, timeout_ms);
3028 return Err(err);
3029 }
3030
3031 let thunk_param = scratch + REMOTE_THREAD_PARAM_OFFSET;
3032 let exit_code_slot = scratch + REMOTE_THREAD_EXIT_CODE_OFFSET;
3033 let status_slot = scratch + REMOTE_THREAD_STATUS_OFFSET;
3034 let tid_slot = scratch + REMOTE_THREAD_THREAD_ID_OFFSET;
3035 let mut scratch_region = vec![0u8; REMOTE_THREAD_THREAD_ID_OFFSET as usize + 8];
3036 scratch_region[REMOTE_THREAD_PARAM_OFFSET as usize
3037 ..REMOTE_THREAD_PARAM_OFFSET as usize + REMOTE_THREAD_PARAM_SIZE]
3038 .copy_from_slice(&remote_thread_param_block(
3039 entry_point,
3040 remote_base,
3041 exit_code_slot,
3042 status_slot,
3043 ));
3044
3045 let (thunk_addr, thread_start_location) = match find_in_image_code_cave(
3046 pe,
3047 image,
3048 thunk_code_size,
3049 ) {
3050 Some(thunk_offset) => {
3051 let thunk_addr = remote_base + thunk_offset as u64;
3052 if let Err(err) = guest_virtual_protect(
3053 backend,
3054 pid,
3055 hook,
3056 virtual_protect,
3057 thunk_addr,
3058 thunk_code_size as u64,
3059 PAGE_EXECUTE_READWRITE,
3060 hook.result_addr + OLD_PROTECT_RESULT_OFFSET,
3061 timeout_ms,
3062 "remote-thread in-image thunk",
3063 ) {
3064 best_effort_virtual_free(backend, pid, hook, virtual_free, scratch, timeout_ms);
3065 return Err(err);
3066 }
3067 let old_protect =
3068 match backend.read(pid, hook.result_addr + OLD_PROTECT_RESULT_OFFSET, 4) {
3069 Ok(old_protect) => old_protect,
3070 Err(err) => {
3071 best_effort_virtual_free(
3072 backend,
3073 pid,
3074 hook,
3075 virtual_free,
3076 scratch,
3077 timeout_ms,
3078 );
3079 return Err(err);
3080 }
3081 };
3082 if let Err(err) = write_verified(
3083 backend,
3084 pid,
3085 thunk_addr,
3086 REMOTE_THREAD_DLLMAIN_THUNK,
3087 "remote-thread in-image thunk",
3088 ) {
3089 best_effort_virtual_free(backend, pid, hook, virtual_free, scratch, timeout_ms);
3090 return Err(err);
3091 }
3092 let old_protect = u32::from_le_bytes(old_protect[0..4].try_into().unwrap()) as u64;
3093 if old_protect != 0 {
3094 let _ = guest_virtual_protect(
3095 backend,
3096 pid,
3097 hook,
3098 virtual_protect,
3099 thunk_addr,
3100 thunk_code_size as u64,
3101 old_protect,
3102 hook.result_addr + OLD_PROTECT_RESULT_OFFSET,
3103 timeout_ms,
3104 "remote-thread in-image thunk restore",
3105 );
3106 }
3107 tracing::info!(
3108 pid,
3109 thunk_addr = format_args!("{thunk_addr:#x}"),
3110 entry_point = format_args!("{entry_point:#x}"),
3111 "DllMain thunk written into payload image code cave; thread start will be module-backed"
3112 );
3113 (thunk_addr, "payload-image code cave")
3114 }
3115 None => {
3116 if require_module_backed_start {
3117 best_effort_virtual_free(backend, pid, hook, virtual_free, scratch, timeout_ms);
3118 return Err(GuestInjectError::Backend(
3119 "remote-thread module-backed start requires an executable payload-image code cave for the DllMain thunk"
3120 .into(),
3121 ));
3122 }
3123 scratch_region[..thunk_code_size].copy_from_slice(REMOTE_THREAD_DLLMAIN_THUNK);
3124 tracing::info!(
3125 pid,
3126 thunk_addr = format_args!("{scratch:#x}"),
3127 entry_point = format_args!("{entry_point:#x}"),
3128 "remote-thread: no payload-image code cave available; using temporary ThreadProc thunk"
3129 );
3130 (scratch, "temporary helper allocation")
3131 }
3132 };
3133 if let Err(err) = write_verified(
3134 backend,
3135 pid,
3136 scratch,
3137 &scratch_region,
3138 "remote-thread scratch",
3139 ) {
3140 best_effort_virtual_free(backend, pid, hook, virtual_free, scratch, timeout_ms);
3141 return Err(err);
3142 }
3143
3144 let saved = match backend.read(pid, trampoline, GUEST_PROC_TRAMPOLINE.len()) {
3145 Ok(saved) => saved,
3146 Err(err) => {
3147 best_effort_virtual_free(backend, pid, hook, virtual_free, scratch, timeout_ms);
3148 return Err(err);
3149 }
3150 };
3151 if let Err(err) = write_verified(
3152 backend,
3153 pid,
3154 trampoline,
3155 GUEST_PROC_TRAMPOLINE,
3156 "remote-thread trampoline",
3157 ) {
3158 best_effort_virtual_free(backend, pid, hook, virtual_free, scratch, timeout_ms);
3159 return Err(err);
3160 }
3161
3162 let thread_handle = call_guest_proc(
3163 backend,
3164 pid,
3165 hook,
3166 trampoline,
3167 param_block,
3168 create_thread,
3169 &[0, 0, thunk_addr, thunk_param, 0, tid_slot],
3170 timeout_ms,
3171 "CreateThread",
3172 );
3173 if let Err(err) = backend.write(pid, trampoline, &saved) {
3174 tracing::warn!(
3175 pid,
3176 error = %err,
3177 "failed to restore guest proc trampoline after remote-thread create"
3178 );
3179 }
3180 let thread_handle = match thread_handle {
3181 Ok(handle) => handle,
3182 Err(err) => {
3183 best_effort_virtual_free(backend, pid, hook, virtual_free, scratch, timeout_ms);
3184 return Err(err);
3185 }
3186 };
3187 if thread_handle == 0 {
3188 best_effort_virtual_free(backend, pid, hook, virtual_free, scratch, timeout_ms);
3189 return Err(GuestInjectError::Backend(
3190 "CreateThread returned NULL".into(),
3191 ));
3192 }
3193 tracing::info!(
3194 pid,
3195 thread_handle = format_args!("{thread_handle:#x}"),
3196 thunk_addr = format_args!("{thunk_addr:#x}"),
3197 thread_start = thread_start_location,
3198 "guest thread created; kernel-recorded start address selected"
3199 );
3200
3201 let exit_code =
3202 match poll_remote_thread_exit(backend, pid, status_slot, exit_code_slot, timeout_ms) {
3203 Ok(exit_code) => exit_code,
3204 Err(err) => {
3205 let _ = backend.call_iat_hook(
3206 pid,
3207 hook,
3208 close_handle,
3209 [thread_handle, 0, 0, 0],
3210 timeout_ms.min(250),
3211 );
3212 best_effort_virtual_free(
3213 backend,
3214 pid,
3215 hook,
3216 virtual_free,
3217 scratch,
3218 timeout_ms.min(250),
3219 );
3220 return Err(err);
3221 }
3222 };
3223 let _ = backend.call_iat_hook(
3224 pid,
3225 hook,
3226 close_handle,
3227 [thread_handle, 0, 0, 0],
3228 timeout_ms.min(250),
3229 );
3230 best_effort_virtual_free(
3231 backend,
3232 pid,
3233 hook,
3234 virtual_free,
3235 scratch,
3236 timeout_ms.min(250),
3237 );
3238 tracing::info!(
3239 pid,
3240 thread_handle = format_args!("{thread_handle:#x}"),
3241 exit_code,
3242 "remote thread completed; DllMain return value retrieved"
3243 );
3244 Ok(exit_code)
3245}
3246
3247fn find_in_image_code_cave(pe: &Pe, image: &[u8], size: usize) -> Option<usize> {
3248 for section in &pe.sections {
3249 if section.characteristics & IMAGE_SCN_MEM_EXECUTE == 0 {
3250 continue;
3251 }
3252 let start = section.virtual_address as usize;
3253 let sec_size = guest_section_size(section) as usize;
3254 if start + sec_size > image.len() {
3255 continue;
3256 }
3257 if let Some(off) = find_code_cave(&image[start..start + sec_size], size) {
3258 return Some(start + off);
3259 }
3260 }
3261 None
3262}
3263
3264#[allow(clippy::too_many_arguments)]
3265fn allocate_virtual(
3266 backend: &dyn GuestMemoryBackend,
3267 pid: u32,
3268 hook: &GuestIatHook,
3269 virtual_alloc: u64,
3270 pe: &Pe,
3271 allocation_protection: u64,
3272 base_policy: GuestBaseAddress,
3273 timeout_ms: u32,
3274) -> Result<u64, GuestInjectError> {
3275 let size = pe.size_of_image as u64;
3276 let try_alloc = |addr: u64| {
3277 backend.call_iat_hook(
3278 pid,
3279 hook,
3280 virtual_alloc,
3281 [addr, size, MEM_COMMIT_RESERVE, allocation_protection],
3282 timeout_ms,
3283 )
3284 };
3285 match base_policy {
3286 GuestBaseAddress::Preferred => {
3287 tracing::info!(
3288 pid,
3289 preferred_base = format_args!("{:#x}", pe.image_base),
3290 size,
3291 protection = format_args!("{allocation_protection:#x}"),
3292 "calling guest VirtualAlloc at preferred image base through IAT hook"
3293 );
3294 let preferred = try_alloc(pe.image_base)?;
3295 if preferred != 0 || !pe.has_relocation_directory() {
3296 return Ok(preferred);
3297 }
3298 tracing::info!(
3299 pid,
3300 preferred_base = format_args!("{:#x}", pe.image_base),
3301 "preferred image base unavailable; retrying guest VirtualAlloc at any base"
3302 );
3303 try_alloc(0)
3304 }
3305 GuestBaseAddress::Randomized => {
3306 if !pe.has_relocation_directory() {
3307 tracing::info!(
3308 pid,
3309 "base_address=randomized but payload has no relocation directory; using preferred base"
3310 );
3311 return try_alloc(pe.image_base);
3312 }
3313 for attempt in 0..3u32 {
3314 let candidate = random_base_address();
3315 tracing::info!(
3316 pid,
3317 attempt,
3318 candidate = format_args!("{candidate:#x}"),
3319 size,
3320 "calling guest VirtualAlloc at randomized base"
3321 );
3322 let result = try_alloc(candidate)?;
3323 if result != 0 {
3324 return Ok(result);
3325 }
3326 }
3327 tracing::info!(
3328 pid,
3329 "randomized base attempts exhausted; falling back to any base"
3330 );
3331 try_alloc(0)
3332 }
3333 }
3334}
3335
3336fn initial_allocation_protection(plan: &GuestInjectionPlan, pe: &Pe) -> u64 {
3337 match (plan.final_protections, plan.permission_transitions) {
3338 (GuestFinalProtections::Rwx, _) => PAGE_EXECUTE_READWRITE,
3339 (GuestFinalProtections::Section, GuestPermissionTransitions::Standard) => PAGE_READWRITE,
3340 (GuestFinalProtections::Section, GuestPermissionTransitions::WriteThroughFinal) => {
3341 final_image_allocation_protection(pe)
3342 }
3343 }
3344}
3345
3346fn final_image_allocation_protection(pe: &Pe) -> u64 {
3347 let mut has_execute = false;
3348 for section in &pe.sections {
3349 if guest_section_size(section) == 0 {
3350 continue;
3351 }
3352 let executable = section.characteristics & IMAGE_SCN_MEM_EXECUTE != 0;
3353 let writable = section.characteristics & IMAGE_SCN_MEM_WRITE != 0;
3354 if executable && writable {
3355 return PAGE_EXECUTE_READWRITE;
3356 }
3357 has_execute |= executable;
3358 }
3359 match has_execute {
3360 true => PAGE_EXECUTE_READ,
3361 false => PAGE_READONLY,
3362 }
3363}
3364
3365fn protection_is_writable(protect: u64) -> bool {
3366 matches!(protect, PAGE_READWRITE | PAGE_EXECUTE_READWRITE)
3367}
3368
3369fn protect_skip_initial(plan: &GuestInjectionPlan, initial_protect: u64) -> Option<u64> {
3370 match (plan.final_protections, plan.permission_transitions) {
3371 (GuestFinalProtections::Section, GuestPermissionTransitions::WriteThroughFinal) => {
3372 Some(initial_protect)
3373 }
3374 _ => None,
3375 }
3376}
3377
3378fn validate_guest_thread_start_policy(
3379 backend: &dyn GuestMemoryBackend,
3380 pid: u32,
3381 plan: &GuestInjectionPlan,
3382 stage: u64,
3383 hook: &GuestIatHook,
3384) -> Result<Vec<String>, GuestInjectError> {
3385 match plan.thread_starts {
3386 GuestThreadStartPolicy::ExistingThread => {
3387 if plan.execution.method == GuestExecutionMethod::RemoteThread {
3388 Ok(vec![
3389 "thread starts: remote-thread execution creates a guest thread; thread_starts=existing-thread is not applicable to this execution method"
3390 .into(),
3391 ])
3392 } else {
3393 Ok(vec![
3394 "thread starts: existing target thread is used; decant does not create guest thread-start metadata"
3395 .into(),
3396 ])
3397 }
3398 }
3399 GuestThreadStartPolicy::RequireModuleBacked => {
3400 if plan.execution.method == GuestExecutionMethod::RemoteThread {
3401 return Ok(vec![
3402 "thread starts: remote-thread execution requires a payload-image code cave for a module-backed ThreadProc thunk"
3403 .into(),
3404 "thread starts: payloads without a large enough executable cave fail rather than falling back to a temporary thread start"
3405 .into(),
3406 ]);
3407 }
3408 if plan.execution.method != GuestExecutionMethod::IatHook {
3409 return Err(GuestInjectError::Unsupported {
3410 operation: "guest thread start policy",
3411 reason: format!(
3412 "thread_starts = \"require-module-backed\" is implemented for iat-hook and remote-thread execution, not {}",
3413 plan.execution.method.label()
3414 ),
3415 });
3416 }
3417 let modules = backend.module_list(pid)?;
3418 let stage_module =
3419 module_covering_range(&modules, stage, STAGE_CAVE_SIZE as u64, "staging cave")?;
3420 let iat_module = module_covering_range(&modules, hook.iat_slot, 8, "IAT slot")?;
3421 let target_module =
3422 module_covering_range(&modules, hook.original_target, 1, "original import target")?;
3423 tracing::info!(
3424 pid,
3425 stage = format_args!("{stage:#x}"),
3426 stage_module = %stage_module.name,
3427 iat_slot = format_args!("{:#x}", hook.iat_slot),
3428 iat_module = %iat_module.name,
3429 original_target = format_args!("{:#x}", hook.original_target),
3430 target_module = %target_module.name,
3431 "guest thread-start policy verified module-backed IAT-hook path"
3432 );
3433 Ok(vec![
3434 "thread starts: no guest thread is created; existing target thread-start metadata remains target-owned".into(),
3435 format!(
3436 "thread starts: IAT-hook staging cave is module-backed by {}",
3437 stage_module.name
3438 ),
3439 format!(
3440 "thread starts: IAT-hook slot is module-backed by {}; original import target is module-backed by {}",
3441 iat_module.name, target_module.name
3442 ),
3443 ])
3444 }
3445 }
3446}
3447
3448fn module_covering_range<'a>(
3449 modules: &'a [GuestModuleInfo],
3450 addr: u64,
3451 len: u64,
3452 label: &str,
3453) -> Result<&'a GuestModuleInfo, GuestInjectError> {
3454 modules
3455 .iter()
3456 .find(|module| module_contains_range(module, addr, len))
3457 .ok_or_else(|| GuestInjectError::Unsupported {
3458 operation: "guest module-backed thread starts",
3459 reason: format!("{label} range {addr:#x}+{len:#x} is not inside a loaded module"),
3460 })
3461}
3462
3463fn module_contains_range(module: &GuestModuleInfo, addr: u64, len: u64) -> bool {
3464 let Some(end) = addr.checked_add(len) else {
3465 return false;
3466 };
3467 let Some(module_end) = module.base.checked_add(module.size) else {
3468 return false;
3469 };
3470 addr >= module.base && end <= module_end
3471}
3472
3473fn register_guest_stub_unwind(
3474 backend: &dyn GuestMemoryBackend,
3475 pid: u32,
3476 hook: &GuestIatHook,
3477 stage: u64,
3478 timeout_ms: u32,
3479) -> Result<(), GuestInjectError> {
3480 let metadata_addr = stage
3481 .checked_add(STAGE_UNWIND_OFFSET)
3482 .ok_or_else(|| GuestInjectError::Image("stub unwind metadata address overflows".into()))?;
3483 let metadata = stub_unwind_metadata()?;
3484 write_verified(
3485 backend,
3486 pid,
3487 metadata_addr,
3488 &metadata,
3489 "guest IAT-hook unwind metadata",
3490 )?;
3491 let rtl_add_function_table =
3492 resolve_import_symbol(backend, pid, "kernel32.dll", "RtlAddFunctionTable")
3493 .or_else(|_| resolve_import_symbol(backend, pid, "ntdll.dll", "RtlAddFunctionTable"))?;
3494 tracing::info!(
3495 pid,
3496 stage = format_args!("{stage:#x}"),
3497 function_table = format_args!("{metadata_addr:#x}"),
3498 "registering guest IAT-hook runtime function table"
3499 );
3500 let ok = backend.call_iat_hook(
3501 pid,
3502 hook,
3503 rtl_add_function_table,
3504 [metadata_addr, 1, stage, 0],
3505 timeout_ms,
3506 )?;
3507 if ok == 0 {
3508 return Err(GuestInjectError::Backend(format!(
3509 "guest RtlAddFunctionTable({metadata_addr:#x}, 1, {stage:#x}) returned FALSE"
3510 )));
3511 }
3512 Ok(())
3513}
3514
3515fn stub_unwind_metadata() -> Result<Vec<u8>, GuestInjectError> {
3516 let begin = u32::try_from(STAGE_STUB_OFFSET)
3517 .map_err(|_| GuestInjectError::Image("stub begin RVA exceeds u32".into()))?;
3518 let end = u32::try_from(STAGE_SCRATCH_OFFSET)
3519 .map_err(|_| GuestInjectError::Image("stub end RVA exceeds u32".into()))?;
3520 let unwind = u32::try_from(STAGE_UNWIND_OFFSET + 12)
3521 .map_err(|_| GuestInjectError::Image("stub unwind RVA exceeds u32".into()))?;
3522 let alloc_info = (FRAMED_STUB_STACK_ALLOC / 8)
3523 .checked_sub(1)
3524 .ok_or_else(|| GuestInjectError::Image("invalid framed stub allocation".into()))?;
3525 let mut out = Vec::with_capacity(20);
3526 out.extend_from_slice(&begin.to_le_bytes());
3527 out.extend_from_slice(&end.to_le_bytes());
3528 out.extend_from_slice(&unwind.to_le_bytes());
3529 out.push(1);
3530 out.push(4);
3531 out.push(1);
3532 out.push(0);
3533 out.push(4);
3534 out.push((alloc_info << 4) | 2);
3535 out.extend_from_slice(&[0, 0]);
3536 Ok(out)
3537}
3538
3539fn register_guest_exception_table(
3540 backend: &dyn GuestMemoryBackend,
3541 pid: u32,
3542 hook: &GuestIatHook,
3543 remote_base: u64,
3544 pe: &Pe,
3545 timeout_ms: u32,
3546) -> Result<(), GuestInjectError> {
3547 if pe.exception.rva == 0 || pe.exception.size == 0 {
3548 return Ok(());
3549 }
3550 let function_table = remote_base
3551 .checked_add(u64::from(pe.exception.rva))
3552 .ok_or_else(|| GuestInjectError::Image("exception table address overflows".into()))?;
3553 let entry_count = pe.exception.size / 12;
3554 if entry_count == 0 {
3555 return Ok(());
3556 }
3557 let rtl_add_function_table =
3558 resolve_import_symbol(backend, pid, "kernel32.dll", "RtlAddFunctionTable")
3559 .or_else(|_| resolve_import_symbol(backend, pid, "ntdll.dll", "RtlAddFunctionTable"))?;
3560 tracing::info!(
3561 pid,
3562 function_table = format_args!("{function_table:#x}"),
3563 entry_count,
3564 base = format_args!("{remote_base:#x}"),
3565 "registering guest runtime function table"
3566 );
3567 let ok = backend.call_iat_hook(
3568 pid,
3569 hook,
3570 rtl_add_function_table,
3571 [function_table, u64::from(entry_count), remote_base, 0],
3572 timeout_ms,
3573 )?;
3574 if ok == 0 {
3575 return Err(GuestInjectError::Backend(format!(
3576 "guest RtlAddFunctionTable({function_table:#x}, {entry_count}, {remote_base:#x}) returned FALSE"
3577 )));
3578 }
3579 Ok(())
3580}
3581
3582fn loader_security_cookie(pid: u32, remote_base: u64, image_len: usize) -> u64 {
3583 let time = SystemTime::now()
3584 .duration_since(UNIX_EPOCH)
3585 .map(|duration| duration.as_nanos() as u64)
3586 .unwrap_or(0);
3587 let mut value = time
3588 ^ remote_base.rotate_left(17)
3589 ^ ((pid as u64) << 32)
3590 ^ (image_len as u64).rotate_left(7);
3591 value ^= value >> 33;
3592 value = value.wrapping_mul(0xff51_afd7_ed55_8ccd);
3593 value ^= value >> 33;
3594 value & 0x0000_FFFF_FFFF_FFFF
3595}
3596
3597const GENERIC_READ: u64 = 0x8000_0000;
3598const GENERIC_WRITE: u64 = 0x4000_0000;
3599const CREATE_ALWAYS: u64 = 2;
3600const FILE_ATTRIBUTE_NORMAL: u64 = 0x80;
3601const FILE_FLAG_DELETE_ON_CLOSE: u64 = 0x0400_0000;
3602const SEC_IMAGE: u64 = 0x0100_0000;
3603const FILE_MAP_COPY: u64 = 1;
3604const MEM_RELEASE: u64 = 0x8000;
3605const STAGE_TRAMPOLINE_OFFSET: u64 = STAGE_SCRATCH_OFFSET;
3606const STAGE_PARAM_OFFSET: u64 = STAGE_SCRATCH_OFFSET + 0x60;
3607const GUEST_PARAM_BLOCK_SIZE: usize = 0x58;
3608
3609const REMOTE_THREAD_THUNK_ALLOCATION_SIZE: u64 = 0x1000;
3610const REMOTE_THREAD_PARAM_OFFSET: u64 = 0x80;
3611const REMOTE_THREAD_PARAM_SIZE: usize = 0x30;
3612const REMOTE_THREAD_EXIT_CODE_OFFSET: u64 = 0x100;
3613const REMOTE_THREAD_STATUS_OFFSET: u64 = 0x108;
3614const REMOTE_THREAD_THREAD_ID_OFFSET: u64 = 0x110;
3615
3616const _: () = {
3617 assert!(
3618 REMOTE_THREAD_PARAM_OFFSET + REMOTE_THREAD_PARAM_SIZE as u64
3619 <= REMOTE_THREAD_EXIT_CODE_OFFSET
3620 );
3621 assert!(REMOTE_THREAD_EXIT_CODE_OFFSET + 4 <= REMOTE_THREAD_STATUS_OFFSET);
3622 assert!(REMOTE_THREAD_STATUS_OFFSET + 4 <= REMOTE_THREAD_THREAD_ID_OFFSET);
3623 assert!(REMOTE_THREAD_THREAD_ID_OFFSET + 8 <= REMOTE_THREAD_THUNK_ALLOCATION_SIZE);
3624};
3625
3626const SEC_IMAGE_PATH_CHARS: u64 = 0x400;
3627const SEC_IMAGE_PATH_BYTES: u64 = SEC_IMAGE_PATH_CHARS * 2;
3628
3629const GUEST_PROC_TRAMPOLINE: &[u8] = &[
3630 0x55, 0x48, 0x89, 0xE5, 0x48, 0x83, 0xE4, 0xF0, 0x48, 0x83, 0xEC, 0x40, 0x4C, 0x8B, 0x11, 0x48,
3631 0x8B, 0x41, 0x30, 0x48, 0x89, 0x44, 0x24, 0x20, 0x48, 0x8B, 0x41, 0x38, 0x48, 0x89, 0x44, 0x24,
3632 0x28, 0x48, 0x8B, 0x41, 0x40, 0x48, 0x89, 0x44, 0x24, 0x30, 0x48, 0x8B, 0x41, 0x48, 0x48, 0x89,
3633 0x44, 0x24, 0x38, 0x4C, 0x8B, 0x49, 0x28, 0x4C, 0x8B, 0x41, 0x20, 0x48, 0x8B, 0x51, 0x18, 0x48,
3634 0x8B, 0x49, 0x10, 0x4C, 0x89, 0xD0, 0xFF, 0xD0, 0x48, 0x89, 0xEC, 0x5D, 0xC3,
3635];
3636const GUEST_GET_PEB_TRAMPOLINE: &[u8] =
3637 &[0x65, 0x48, 0x8B, 0x04, 0x25, 0x60, 0x00, 0x00, 0x00, 0xC3];
3638const REMOTE_THREAD_DLLMAIN_THUNK: &[u8] = &[
3639 0x53, 0x48, 0x83, 0xEC, 0x20, 0x48, 0x89, 0xCB, 0x48, 0x8B, 0x03, 0x48, 0x8B, 0x4B, 0x08, 0x48, 0x8B, 0x53, 0x10, 0x4C, 0x8B, 0x43, 0x18, 0xFF, 0xD0, 0x4C, 0x8B, 0x53, 0x20, 0x41, 0x89, 0x02, 0x4C, 0x8B, 0x53, 0x28, 0x41, 0xC7, 0x02, 0x01, 0x00, 0x00, 0x00, 0x48, 0x83, 0xC4, 0x20, 0x5B, 0xC3, ];
3655
3656struct SecImageCleanup<'a> {
3657 backend: &'a dyn GuestMemoryBackend,
3658 pid: u32,
3659 hook: &'a GuestIatHook,
3660 unmap_view: u64,
3661 close_handle: u64,
3662 virtual_free: u64,
3663 trampoline: u64,
3664 saved_trampoline: Vec<u8>,
3665 timeout_ms: u32,
3666 path_buf: u64,
3667 payload_buf: u64,
3668 file_handle: u64,
3669 mapping_handle: u64,
3670 view_base: u64,
3671}
3672
3673impl Drop for SecImageCleanup<'_> {
3674 fn drop(&mut self) {
3675 if self.view_base != 0 {
3676 let _ = self.backend.call_iat_hook(
3677 self.pid,
3678 self.hook,
3679 self.unmap_view,
3680 [self.view_base, 0, 0, 0],
3681 self.timeout_ms,
3682 );
3683 }
3684 if self.mapping_handle != 0 {
3685 let _ = self.backend.call_iat_hook(
3686 self.pid,
3687 self.hook,
3688 self.close_handle,
3689 [self.mapping_handle, 0, 0, 0],
3690 self.timeout_ms,
3691 );
3692 }
3693 if self.file_handle != 0 {
3694 let _ = self.backend.call_iat_hook(
3695 self.pid,
3696 self.hook,
3697 self.close_handle,
3698 [self.file_handle, 0, 0, 0],
3699 self.timeout_ms,
3700 );
3701 }
3702 if self.payload_buf != 0 {
3703 let _ = self.backend.call_iat_hook(
3704 self.pid,
3705 self.hook,
3706 self.virtual_free,
3707 [self.payload_buf, 0, MEM_RELEASE, 0],
3708 self.timeout_ms,
3709 );
3710 }
3711 if self.path_buf != 0 {
3712 let _ = self.backend.call_iat_hook(
3713 self.pid,
3714 self.hook,
3715 self.virtual_free,
3716 [self.path_buf, 0, MEM_RELEASE, 0],
3717 self.timeout_ms,
3718 );
3719 }
3720 let _ = self
3721 .backend
3722 .write(self.pid, self.trampoline, &self.saved_trampoline);
3723 }
3724}
3725
3726fn sec_image_error(label: &str, err: impl std::fmt::Display) -> GuestInjectError {
3727 GuestInjectError::Backend(format!("guest SEC_IMAGE {label}: {err}"))
3728}
3729
3730fn encode_wide(s: &str) -> Vec<u8> {
3731 let mut out: Vec<u16> = s.encode_utf16().collect();
3732 out.push(0);
3733 let mut bytes = Vec::with_capacity(out.len() * 2);
3734 for word in out {
3735 bytes.extend_from_slice(&word.to_le_bytes());
3736 }
3737 bytes
3738}
3739
3740fn decode_wide(bytes: &[u8]) -> String {
3741 let pairs: Vec<u16> = bytes
3742 .chunks_exact(2)
3743 .map(|c| u16::from_le_bytes([c[0], c[1]]))
3744 .collect();
3745 let len = pairs.iter().position(|&w| w == 0).unwrap_or(pairs.len());
3746 String::from_utf16_lossy(&pairs[..len])
3747}
3748
3749fn should_request_cfg_call_target(plan: &GuestInjectionPlan, pe: &Pe) -> bool {
3750 plan.loader_entries == GuestLoaderEntries::Synthesized
3751 && plan.loader_metadata == GuestLoaderMetadataPolicy::BestEffort
3752 && pe.has_load_config()
3753}
3754
3755fn cfg_call_target_registration_args(
3756 remote_base: u64,
3757 region_size: u64,
3758 target_count: u64,
3759 cfg_info: u64,
3760) -> [u64; 5] {
3761 [u64::MAX, remote_base, region_size, target_count, cfg_info]
3762}
3763
3764#[allow(clippy::too_many_arguments)]
3765fn call_guest_proc(
3766 backend: &dyn GuestMemoryBackend,
3767 pid: u32,
3768 hook: &GuestIatHook,
3769 trampoline: u64,
3770 param_block: u64,
3771 target: u64,
3772 args: &[u64],
3773 timeout_ms: u32,
3774 label: &str,
3775) -> Result<u64, GuestInjectError> {
3776 let mut block = [0u8; GUEST_PARAM_BLOCK_SIZE];
3777 block[0..8].copy_from_slice(&target.to_le_bytes());
3778 for (i, &value) in args.iter().take(8).enumerate() {
3779 let off = 0x10 + i * 8;
3780 block[off..off + 8].copy_from_slice(&value.to_le_bytes());
3781 }
3782 write_verified(backend, pid, param_block, &block, "guest proc param block")?;
3783 tracing::debug!(
3784 pid,
3785 label,
3786 target = format_args!("{target:#x}"),
3787 arg_count = args.len(),
3788 "guest proc call armed"
3789 );
3790 backend.call_iat_hook(pid, hook, trampoline, [param_block, 0, 0, 0], timeout_ms)
3791}
3792
3793#[allow(clippy::too_many_arguments)]
3794fn allocate_sec_image(
3795 backend: &dyn GuestMemoryBackend,
3796 pid: u32,
3797 hook: &GuestIatHook,
3798 virtual_alloc: u64,
3799 stage: u64,
3800 payload_image: &[u8],
3801 timeout_ms: u32,
3802) -> Result<u64, GuestInjectError> {
3803 let trampoline = stage + STAGE_TRAMPOLINE_OFFSET;
3804 let param_block = stage + STAGE_PARAM_OFFSET;
3805 let saved_trampoline = backend.read(pid, trampoline, GUEST_PROC_TRAMPOLINE.len())?;
3806 let get_temp_path = resolve_import_symbol(backend, pid, "kernel32.dll", "GetTempPathW")?;
3807 let create_file = resolve_import_symbol(backend, pid, "kernel32.dll", "CreateFileW")?;
3808 let write_file = resolve_import_symbol(backend, pid, "kernel32.dll", "WriteFile")?;
3809 let close_handle = resolve_import_symbol(backend, pid, "kernel32.dll", "CloseHandle")?;
3810 let virtual_free = resolve_import_symbol(backend, pid, "kernel32.dll", "VirtualFree")?;
3811
3812 let create_file_mapping =
3813 resolve_import_symbol(backend, pid, "kernel32.dll", "CreateFileMappingW")?;
3814 let map_view = resolve_import_symbol(backend, pid, "kernel32.dll", "MapViewOfFile")?;
3815 let unmap_view = resolve_import_symbol(backend, pid, "kernel32.dll", "UnmapViewOfFile")?;
3816
3817 let mut cleanup = SecImageCleanup {
3818 backend,
3819 pid,
3820 hook,
3821 unmap_view,
3822 close_handle,
3823 virtual_free,
3824 trampoline,
3825 saved_trampoline,
3826 timeout_ms,
3827 path_buf: 0,
3828 payload_buf: 0,
3829 file_handle: 0,
3830 mapping_handle: 0,
3831 view_base: 0,
3832 };
3833 write_verified(
3834 backend,
3835 pid,
3836 trampoline,
3837 GUEST_PROC_TRAMPOLINE,
3838 "guest proc trampoline",
3839 )?;
3840
3841 cleanup.path_buf = backend.call_iat_hook(
3842 pid,
3843 hook,
3844 virtual_alloc,
3845 [0, SEC_IMAGE_PATH_BYTES, MEM_COMMIT_RESERVE, PAGE_READWRITE],
3846 timeout_ms,
3847 )?;
3848 if cleanup.path_buf == 0 {
3849 return Err(sec_image_error("path buffer", "VirtualAlloc returned NULL"));
3850 }
3851 cleanup.payload_buf = backend.call_iat_hook(
3852 pid,
3853 hook,
3854 virtual_alloc,
3855 [
3856 0,
3857 payload_image.len() as u64,
3858 MEM_COMMIT_RESERVE,
3859 PAGE_READWRITE,
3860 ],
3861 timeout_ms,
3862 )?;
3863 if cleanup.payload_buf == 0 {
3864 return Err(sec_image_error(
3865 "payload buffer",
3866 "VirtualAlloc returned NULL",
3867 ));
3868 }
3869 backend.touch_iat_hook(
3870 pid,
3871 hook,
3872 cleanup.payload_buf,
3873 payload_image.len(),
3874 timeout_ms,
3875 )?;
3876 write_verified(
3877 backend,
3878 pid,
3879 cleanup.payload_buf,
3880 payload_image,
3881 "guest SEC_IMAGE payload buffer",
3882 )?;
3883
3884 let temp_len = backend.call_iat_hook(
3885 pid,
3886 hook,
3887 get_temp_path,
3888 [SEC_IMAGE_PATH_CHARS, cleanup.path_buf, 0, 0],
3889 timeout_ms,
3890 )?;
3891 if temp_len == 0 || temp_len >= SEC_IMAGE_PATH_CHARS {
3892 return Err(sec_image_error(
3893 "GetTempPathW",
3894 format!("returned unusable length {temp_len}"),
3895 ));
3896 }
3897 let temp_bytes = backend.read(pid, cleanup.path_buf, (temp_len as usize) * 2)?;
3898 let temp_dir = decode_wide(&temp_bytes);
3899 let stamp = SystemTime::now()
3900 .duration_since(UNIX_EPOCH)
3901 .map(|d| d.as_nanos() as u64)
3902 .unwrap_or(0);
3903 let full_path = format!("{temp_dir}decant_{stamp:x}.dll");
3904 let wide_path = encode_wide(&full_path);
3905 if wide_path.len() > SEC_IMAGE_PATH_BYTES as usize {
3906 return Err(sec_image_error(
3907 "temp path",
3908 format!("path buffer too small for {full_path}"),
3909 ));
3910 }
3911 write_verified(
3912 backend,
3913 pid,
3914 cleanup.path_buf,
3915 &wide_path,
3916 "guest SEC_IMAGE wide path",
3917 )?;
3918
3919 let write_len = u32::try_from(payload_image.len())
3920 .map_err(|_| sec_image_error("WriteFile", "payload exceeds DWORD byte count"))?;
3921 let written_slot = cleanup.path_buf;
3922 cleanup.file_handle = call_guest_proc(
3923 backend,
3924 pid,
3925 hook,
3926 trampoline,
3927 param_block,
3928 create_file,
3929 &[
3930 cleanup.path_buf,
3931 GENERIC_READ | GENERIC_WRITE,
3932 0,
3933 0,
3934 CREATE_ALWAYS,
3935 FILE_ATTRIBUTE_NORMAL | FILE_FLAG_DELETE_ON_CLOSE,
3936 0,
3937 ],
3938 timeout_ms,
3939 "CreateFileW",
3940 )?;
3941 let invalid_handle = u64::MAX;
3942 if cleanup.file_handle == 0 || cleanup.file_handle == invalid_handle {
3943 return Err(sec_image_error(
3944 "CreateFileW",
3945 format!("returned invalid handle for {full_path}"),
3946 ));
3947 }
3948 write_verified(
3949 backend,
3950 pid,
3951 written_slot,
3952 &[0; 8],
3953 "guest SEC_IMAGE WriteFile byte-count slot",
3954 )?;
3955 let write_ok = call_guest_proc(
3956 backend,
3957 pid,
3958 hook,
3959 trampoline,
3960 param_block,
3961 write_file,
3962 &[
3963 cleanup.file_handle,
3964 cleanup.payload_buf,
3965 u64::from(write_len),
3966 written_slot,
3967 0,
3968 ],
3969 timeout_ms,
3970 "WriteFile",
3971 )?;
3972 if write_ok == 0 {
3973 return Err(sec_image_error(
3974 "WriteFile",
3975 format!("returned FALSE for {full_path}"),
3976 ));
3977 }
3978 let written = read_remote_u32(backend, pid, written_slot)?;
3979 if written != write_len {
3980 return Err(sec_image_error(
3981 "WriteFile",
3982 format!(
3983 "wrote {written} of {} bytes for {full_path}",
3984 payload_image.len()
3985 ),
3986 ));
3987 }
3988 let _ = backend.call_iat_hook(
3989 pid,
3990 hook,
3991 virtual_free,
3992 [cleanup.payload_buf, 0, MEM_RELEASE, 0],
3993 timeout_ms,
3994 );
3995 cleanup.payload_buf = 0;
3996
3997 cleanup.mapping_handle = call_guest_proc(
3998 backend,
3999 pid,
4000 hook,
4001 trampoline,
4002 param_block,
4003 create_file_mapping,
4004 &[cleanup.file_handle, 0, PAGE_READONLY | SEC_IMAGE, 0, 0, 0],
4005 timeout_ms,
4006 "CreateFileMappingW",
4007 )?;
4008 if cleanup.mapping_handle == 0 {
4009 return Err(sec_image_error(
4010 "CreateFileMappingW",
4011 format!("returned NULL for {full_path}"),
4012 ));
4013 }
4014 cleanup.view_base = call_guest_proc(
4015 backend,
4016 pid,
4017 hook,
4018 trampoline,
4019 param_block,
4020 map_view,
4021 &[cleanup.mapping_handle, FILE_MAP_COPY, 0, 0, 0],
4022 timeout_ms,
4023 "MapViewOfFile",
4024 )?;
4025 let _ = backend.call_iat_hook(
4026 pid,
4027 hook,
4028 close_handle,
4029 [cleanup.mapping_handle, 0, 0, 0],
4030 timeout_ms,
4031 );
4032 cleanup.mapping_handle = 0;
4033 let _ = backend.call_iat_hook(
4034 pid,
4035 hook,
4036 close_handle,
4037 [cleanup.file_handle, 0, 0, 0],
4038 timeout_ms,
4039 );
4040 cleanup.file_handle = 0;
4041 let _ = backend.call_iat_hook(
4042 pid,
4043 hook,
4044 virtual_free,
4045 [cleanup.path_buf, 0, MEM_RELEASE, 0],
4046 timeout_ms,
4047 );
4048 cleanup.path_buf = 0;
4049 if cleanup.view_base == 0 {
4050 return Err(GuestInjectError::Backend(format!(
4051 "guest MapViewOfFile(SEC_IMAGE, {full_path}) returned NULL"
4052 )));
4053 }
4054 let view_base = cleanup.view_base;
4055 cleanup.view_base = 0;
4056 tracing::info!(
4057 pid,
4058 remote_base = format_args!("{view_base:#x}"),
4059 path = %full_path,
4060 bytes = payload_image.len(),
4061 "guest SEC_IMAGE mapping installed; file and mapping handles released, trampoline restored, view retained"
4062 );
4063 Ok(view_base)
4064}
4065
4066#[allow(clippy::too_many_arguments)]
4067fn synthesize_peb_loader_entry(
4068 backend: &dyn GuestMemoryBackend,
4069 pid: u32,
4070 hook: &GuestIatHook,
4071 virtual_alloc: u64,
4072 stage: u64,
4073 remote_base: u64,
4074 entry_point: u64,
4075 size_of_image: u32,
4076 dll_name: &str,
4077 timeout_ms: u32,
4078) -> Result<u64, GuestInjectError> {
4079 let trampoline = stage + STAGE_TRAMPOLINE_OFFSET;
4080 let saved = backend.read(pid, trampoline, GUEST_GET_PEB_TRAMPOLINE.len())?;
4081 write_verified(
4082 backend,
4083 pid,
4084 trampoline,
4085 GUEST_GET_PEB_TRAMPOLINE,
4086 "PEB lookup trampoline",
4087 )?;
4088
4089 let peb = backend.call_iat_hook(pid, hook, trampoline, [0, 0, 0, 0], timeout_ms)?;
4090 let _ = backend.write(pid, trampoline, &saved);
4091 if peb == 0 {
4092 return Err(GuestInjectError::Backend(
4093 "PEB synthesis: guest PEB lookup returned null".into(),
4094 ));
4095 }
4096 let ldr_bytes = backend.read(pid, peb + 0x18, 8)?;
4097 let ldr = u64::from_le_bytes(ldr_bytes[0..8].try_into().unwrap());
4098 if ldr == 0 {
4099 return Err(GuestInjectError::Backend(format!(
4100 "PEB Ldr is null for guest PEB {peb:#x}"
4101 )));
4102 }
4103
4104 let entry_size = 0x830u64;
4105 let entry = backend.call_iat_hook(
4106 pid,
4107 hook,
4108 virtual_alloc,
4109 [0, entry_size, MEM_COMMIT_RESERVE, PAGE_READWRITE],
4110 timeout_ms,
4111 )?;
4112 if entry == 0 {
4113 let _ = backend.write(pid, trampoline, &saved);
4114 return Err(GuestInjectError::Backend(
4115 "PEB synthesis: VirtualAlloc for LDR entry returned NULL".into(),
4116 ));
4117 }
4118 backend.touch_iat_hook(pid, hook, entry, entry_size as usize, timeout_ms)?;
4119
4120 let mut entry_data = vec![0u8; entry_size as usize];
4121 entry_data[0x30..0x38].copy_from_slice(&remote_base.to_le_bytes());
4122 entry_data[0x38..0x40].copy_from_slice(&entry_point.to_le_bytes());
4123 entry_data[0x40..0x44].copy_from_slice(&size_of_image.to_le_bytes());
4124 let init_links = entry + 0x20;
4125 entry_data[0x20..0x28].copy_from_slice(&init_links.to_le_bytes());
4126 entry_data[0x28..0x30].copy_from_slice(&init_links.to_le_bytes());
4127
4128 let base_name_wide = encode_wide(dll_name);
4129 let full_name = format!("C:\\Windows\\System32\\{dll_name}");
4130 let full_name_wide = encode_wide(&full_name);
4131 let base_name_offset = 0x200usize;
4132 let full_name_offset = 0x410usize;
4133 entry_data[base_name_offset..base_name_offset + base_name_wide.len()]
4134 .copy_from_slice(&base_name_wide);
4135 entry_data[full_name_offset..full_name_offset + full_name_wide.len()]
4136 .copy_from_slice(&full_name_wide);
4137
4138 let base_name_len = (base_name_wide.len() - 2) as u16;
4139 let full_name_len = (full_name_wide.len() - 2) as u16;
4140 entry_data[0x48..0x4A].copy_from_slice(&full_name_len.to_le_bytes());
4141 entry_data[0x4A..0x4C].copy_from_slice(&(full_name_wide.len() as u16).to_le_bytes());
4142 entry_data[0x50..0x58].copy_from_slice(&(entry + full_name_offset as u64).to_le_bytes());
4143 entry_data[0x58..0x5A].copy_from_slice(&base_name_len.to_le_bytes());
4144 entry_data[0x5A..0x5C].copy_from_slice(&(base_name_wide.len() as u16).to_le_bytes());
4145 entry_data[0x60..0x68].copy_from_slice(&(entry + base_name_offset as u64).to_le_bytes());
4146
4147 write_verified(backend, pid, entry, &entry_data, "LDR_DATA_TABLE_ENTRY")?;
4148
4149 let list_insert = |list_head: u64, links_offset: u64| -> Result<(), GuestInjectError> {
4150 let links = entry + links_offset;
4151 let head_blink_bytes = backend.read(pid, list_head + 8, 8)?;
4152 let head_blink = u64::from_le_bytes(head_blink_bytes[0..8].try_into().unwrap());
4153 let flink = list_head.to_le_bytes();
4154 let blink = head_blink.to_le_bytes();
4155 backend.write(pid, links, &flink)?;
4156 backend.write(pid, links + 8, &blink)?;
4157 backend.write(pid, head_blink, &links.to_le_bytes())?;
4158 backend.write(pid, list_head + 8, &links.to_le_bytes())?;
4159 Ok(())
4160 };
4161 list_insert(ldr + 0x10, 0x00)?;
4162 list_insert(ldr + 0x20, 0x10)?;
4163
4164 let _ = backend.write(pid, trampoline, &saved);
4165 tracing::info!(
4166 pid,
4167 peb = format_args!("{peb:#x}"),
4168 ldr = format_args!("{ldr:#x}"),
4169 entry = format_args!("{entry:#x}"),
4170 remote_base = format_args!("{remote_base:#x}"),
4171 "synthesized PEB loader entry linked into InLoadOrder and InMemoryOrder lists"
4172 );
4173 Ok(entry)
4174}
4175
4176fn unlink_synthesized_peb_loader_entry(
4177 backend: &dyn GuestMemoryBackend,
4178 pid: u32,
4179 entry: u64,
4180) -> Result<(), GuestInjectError> {
4181 unlink_guest_list_entry(backend, pid, entry)?;
4182 unlink_guest_list_entry(backend, pid, entry + 0x10)?;
4183 tracing::info!(
4184 pid,
4185 entry = format_args!("{entry:#x}"),
4186 "unlinked synthesized PEB loader entry from load and memory lists"
4187 );
4188 Ok(())
4189}
4190
4191fn unlink_guest_list_entry(
4192 backend: &dyn GuestMemoryBackend,
4193 pid: u32,
4194 links: u64,
4195) -> Result<(), GuestInjectError> {
4196 let bytes = backend.read(pid, links, 16)?;
4197 let flink = u64::from_le_bytes(bytes[0..8].try_into().unwrap());
4198 let blink = u64::from_le_bytes(bytes[8..16].try_into().unwrap());
4199 if flink == 0 || blink == 0 {
4200 return Err(GuestInjectError::Backend(format!(
4201 "PEB synthesis cleanup: list entry {links:#x} has null links"
4202 )));
4203 }
4204 backend.write(pid, flink + 8, &blink.to_le_bytes())?;
4205 backend.write(pid, blink, &flink.to_le_bytes())?;
4206 backend.write(pid, links, &links.to_le_bytes())?;
4207 backend.write(pid, links + 8, &links.to_le_bytes())?;
4208 Ok(())
4209}
4210
4211#[allow(clippy::too_many_arguments)]
4212fn guest_virtual_protect(
4213 backend: &dyn GuestMemoryBackend,
4214 pid: u32,
4215 hook: &GuestIatHook,
4216 virtual_protect: u64,
4217 addr: u64,
4218 size: u64,
4219 protect: u64,
4220 old_protect: u64,
4221 timeout_ms: u32,
4222 label: &str,
4223) -> Result<(), GuestInjectError> {
4224 tracing::debug!(
4225 pid,
4226 label,
4227 addr = format_args!("{addr:#x}"),
4228 size,
4229 protect = format_args!("{protect:#x}"),
4230 "calling guest VirtualProtect"
4231 );
4232 let ok = backend.call_iat_hook(
4233 pid,
4234 hook,
4235 virtual_protect,
4236 [addr, size, protect, old_protect],
4237 timeout_ms,
4238 )?;
4239 if ok == 0 {
4240 return Err(GuestInjectError::Backend(format!(
4241 "guest VirtualProtect({label} at {addr:#x}, size {size:#x}, protect {protect:#x}) returned FALSE"
4242 )));
4243 }
4244 Ok(())
4245}
4246
4247fn guest_section_size(section: &Section) -> u32 {
4248 match section.virtual_size {
4249 0 => section.raw_size,
4250 v => v,
4251 }
4252}
4253
4254fn guest_section_protect(characteristics: u32) -> u64 {
4255 let executable = characteristics & IMAGE_SCN_MEM_EXECUTE != 0;
4256 let readable = characteristics & IMAGE_SCN_MEM_READ != 0;
4257 let writable = characteristics & IMAGE_SCN_MEM_WRITE != 0;
4258 match (executable, readable, writable) {
4259 (true, _, true) => PAGE_EXECUTE_READWRITE,
4260 (true, true, false) => PAGE_EXECUTE_READ,
4261 (true, false, false) => PAGE_EXECUTE,
4262 (false, _, true) => PAGE_READWRITE,
4263 (false, true, false) => PAGE_READONLY,
4264 (false, false, false) => PAGE_NOACCESS,
4265 }
4266}
4267
4268fn write_verified<B: GuestMemoryBackend + ?Sized>(
4269 backend: &B,
4270 pid: u32,
4271 addr: u64,
4272 data: &[u8],
4273 label: &str,
4274) -> Result<(), GuestInjectError> {
4275 backend.write(pid, addr, data)?;
4276 let got = backend.read(pid, addr, data.len())?;
4277 if got != data {
4278 return Err(GuestInjectError::Backend(format!(
4279 "guest write verification failed for {label} at {addr:#x}: wrote {} bytes",
4280 data.len()
4281 )));
4282 }
4283 Ok(())
4284}
4285
4286fn sec_image_patched_ranges(image: &[u8], snapshot: &[u8]) -> Vec<(usize, usize)> {
4287 debug_assert_eq!(image.len(), snapshot.len());
4288 let len = image.len().min(snapshot.len());
4289 let mut ranges = Vec::new();
4290 let mut i = 0usize;
4291 while i < len {
4292 if image[i] == snapshot[i] {
4293 i += 1;
4294 continue;
4295 }
4296
4297 let range_start = i & !(GUEST_PAGE_SIZE - 1);
4298 let mut range_end = (range_start + GUEST_PAGE_SIZE).min(len);
4299 let mut scan = range_end;
4300 while scan < len {
4301 let page_end = (scan + GUEST_PAGE_SIZE).min(len);
4302 if image[scan..page_end]
4303 .iter()
4304 .zip(&snapshot[scan..page_end])
4305 .any(|(image, snapshot)| image != snapshot)
4306 {
4307 range_end = page_end;
4308 scan = page_end;
4309 } else {
4310 break;
4311 }
4312 }
4313 ranges.push((range_start, range_end));
4314 i = range_end;
4315 }
4316 ranges
4317}
4318
4319fn call_stub(hook: &GuestIatHook, function: u64, args: [u64; 4]) -> Vec<u8> {
4320 if hook.call_stack == GuestCallStackPolicy::RegisteredUnwind {
4321 return framed_call_stub(hook, function, args);
4322 }
4323 let mut code = X64Stub::new();
4324 preserve_import_args(&mut code);
4325 code.mov_abs(Reg64::R10, hook.result_addr);
4326 code.xor_eax_eax();
4327 code.mov_abs(Reg64::R11, RESULT_RUNNING);
4328 code.lock_cmpxchg_rax_at_r10_with_r11();
4329 let skip_call = code.jne_rel32_placeholder();
4330
4331 code.mov_abs(Reg64::Rcx, args[0]);
4332 code.mov_abs(Reg64::Rdx, args[1]);
4333 code.mov_abs(Reg64::R8, args[2]);
4334 code.mov_abs(Reg64::R9, args[3]);
4335 match hook.spoofed_return {
4336 Some(gadget) => emit_spoofed_call(&mut code, hook, function, gadget, 0),
4337 None => {
4338 code.mov_abs(Reg64::Rax, function);
4339 code.call_rax_windows_x64();
4340 }
4341 }
4342 code.mov_abs(Reg64::R10, hook.result_addr + 8);
4343 code.store_rax_at_r10();
4344 code.mov_abs(Reg64::R10, hook.result_addr);
4345 code.mov_abs(Reg64::Rax, RESULT_STATE);
4346 code.store_rax_at_r10();
4347
4348 let tail_original = code.len();
4349 code.patch_rel32(skip_call, tail_original);
4350 tail_jump_original_import(&mut code, hook.original_target);
4351 code.finish()
4352}
4353
4354fn framed_call_stub(hook: &GuestIatHook, function: u64, args: [u64; 4]) -> Vec<u8> {
4355 let mut code = X64Stub::new();
4356 framed_prologue(&mut code);
4357 code.mov_abs(Reg64::R10, hook.result_addr);
4358 code.xor_eax_eax();
4359 code.mov_abs(Reg64::R11, RESULT_RUNNING);
4360 code.lock_cmpxchg_rax_at_r10_with_r11();
4361 let skip_call = code.jne_rel32_placeholder();
4362
4363 code.mov_abs(Reg64::Rcx, args[0]);
4364 code.mov_abs(Reg64::Rdx, args[1]);
4365 code.mov_abs(Reg64::R8, args[2]);
4366 code.mov_abs(Reg64::R9, args[3]);
4367 match hook.spoofed_return {
4368 Some(gadget) => emit_spoofed_call(&mut code, hook, function, gadget, 8),
4369 None => {
4370 code.mov_abs(Reg64::Rax, function);
4371 code.call_rax_with_current_frame();
4372 }
4373 }
4374 code.mov_abs(Reg64::R10, hook.result_addr + 8);
4375 code.store_rax_at_r10();
4376 code.mov_abs(Reg64::R10, hook.result_addr);
4377 code.mov_abs(Reg64::Rax, RESULT_STATE);
4378 code.store_rax_at_r10();
4379
4380 let tail_original = code.len();
4381 code.patch_rel32(skip_call, tail_original);
4382 framed_tail_jump_original_import(&mut code, hook.original_target);
4383 code.finish()
4384}
4385
4386fn touch_stub(hook: &GuestIatHook, addr: u64, len: usize) -> Vec<u8> {
4387 if hook.call_stack == GuestCallStackPolicy::RegisteredUnwind {
4388 return framed_touch_stub(hook, addr, len, TouchMode::WriteZero);
4389 }
4390 let mut code = X64Stub::new();
4391 preserve_import_args(&mut code);
4392 code.mov_abs(Reg64::R10, hook.result_addr);
4393 code.xor_eax_eax();
4394 code.mov_abs(Reg64::R11, RESULT_RUNNING);
4395 code.lock_cmpxchg_rax_at_r10_with_r11();
4396 let skip_touch = code.jne_rel32_placeholder();
4397
4398 emit_touch_loop(&mut code, addr, len, TouchMode::WriteZero);
4399
4400 code.mov_abs(Reg64::R10, hook.result_addr + 8);
4401 code.mov_abs(Reg64::Rax, addr);
4402 code.store_rax_at_r10();
4403 code.mov_abs(Reg64::R10, hook.result_addr);
4404 code.mov_abs(Reg64::Rax, RESULT_STATE);
4405 code.store_rax_at_r10();
4406
4407 let tail_original = code.len();
4408 code.patch_rel32(skip_touch, tail_original);
4409 tail_jump_original_import(&mut code, hook.original_target);
4410 code.finish()
4411}
4412
4413fn read_touch_stub(hook: &GuestIatHook, addr: u64, len: usize) -> Vec<u8> {
4414 if hook.call_stack == GuestCallStackPolicy::RegisteredUnwind {
4415 return framed_touch_stub(hook, addr, len, TouchMode::ReadOnly);
4416 }
4417 touch_mode_stub(hook, addr, len, TouchMode::ReadOnly)
4418}
4419
4420fn preserve_touch_stub(hook: &GuestIatHook, addr: u64, len: usize) -> Vec<u8> {
4421 if hook.call_stack == GuestCallStackPolicy::RegisteredUnwind {
4422 return framed_touch_stub(hook, addr, len, TouchMode::WriteSame);
4423 }
4424 touch_mode_stub(hook, addr, len, TouchMode::WriteSame)
4425}
4426
4427fn touch_mode_stub(hook: &GuestIatHook, addr: u64, len: usize, mode: TouchMode) -> Vec<u8> {
4428 let mut code = X64Stub::new();
4429 preserve_import_args(&mut code);
4430 code.mov_abs(Reg64::R10, hook.result_addr);
4431 code.xor_eax_eax();
4432 code.mov_abs(Reg64::R11, RESULT_RUNNING);
4433 code.lock_cmpxchg_rax_at_r10_with_r11();
4434 let skip_touch = code.jne_rel32_placeholder();
4435
4436 emit_touch_loop(&mut code, addr, len, mode);
4437
4438 code.mov_abs(Reg64::R10, hook.result_addr + 8);
4439 code.mov_abs(Reg64::Rax, addr);
4440 code.store_rax_at_r10();
4441 code.mov_abs(Reg64::R10, hook.result_addr);
4442 code.mov_abs(Reg64::Rax, RESULT_STATE);
4443 code.store_rax_at_r10();
4444
4445 let tail_original = code.len();
4446 code.patch_rel32(skip_touch, tail_original);
4447 tail_jump_original_import(&mut code, hook.original_target);
4448 code.finish()
4449}
4450
4451#[derive(Clone, Copy)]
4452enum TouchMode {
4453 WriteZero,
4454 ReadOnly,
4455 WriteSame,
4456}
4457
4458fn framed_touch_stub(hook: &GuestIatHook, addr: u64, len: usize, mode: TouchMode) -> Vec<u8> {
4459 let mut code = X64Stub::new();
4460 framed_prologue(&mut code);
4461 code.mov_abs(Reg64::R10, hook.result_addr);
4462 code.xor_eax_eax();
4463 code.mov_abs(Reg64::R11, RESULT_RUNNING);
4464 code.lock_cmpxchg_rax_at_r10_with_r11();
4465 let skip_touch = code.jne_rel32_placeholder();
4466
4467 emit_touch_loop(&mut code, addr, len, mode);
4468
4469 code.mov_abs(Reg64::R10, hook.result_addr + 8);
4470 code.mov_abs(Reg64::Rax, addr);
4471 code.store_rax_at_r10();
4472 code.mov_abs(Reg64::R10, hook.result_addr);
4473 code.mov_abs(Reg64::Rax, RESULT_STATE);
4474 code.store_rax_at_r10();
4475
4476 let tail_original = code.len();
4477 code.patch_rel32(skip_touch, tail_original);
4478 framed_tail_jump_original_import(&mut code, hook.original_target);
4479 code.finish()
4480}
4481
4482fn emit_touch_loop(code: &mut X64Stub, addr: u64, len: usize, mode: TouchMode) {
4483 let page_count = len.div_ceil(GUEST_PAGE_SIZE) as u64;
4484 if page_count == 0 {
4485 return;
4486 }
4487 code.mov_abs(Reg64::Rdx, addr);
4488 code.mov_abs(Reg64::Rcx, page_count);
4489 let touch_loop = code.len();
4490 match mode {
4491 TouchMode::WriteZero => code.mov_byte_zero_at_rdx(),
4492 TouchMode::ReadOnly => code.movzx_eax_byte_at_rdx(),
4493 TouchMode::WriteSame => {
4494 code.movzx_eax_byte_at_rdx();
4495 code.mov_al_at_rdx();
4496 }
4497 }
4498 code.add_rdx_imm32(GUEST_PAGE_SIZE as u32);
4499 code.dec_rcx();
4500 let repeat = code.jne_rel32_placeholder();
4501 code.patch_rel32(repeat, touch_loop);
4502}
4503
4504fn preserve_import_args(code: &mut X64Stub) {
4505 code.push(Reg64::Rcx);
4506 code.push(Reg64::Rdx);
4507 code.push(Reg64::R8);
4508 code.push(Reg64::R9);
4509}
4510
4511fn emit_spoofed_call(
4512 code: &mut X64Stub,
4513 hook: &GuestIatHook,
4514 function: u64,
4515 gadget: GuestSpoofedReturn,
4516 desired_frame_mod: u8,
4517) {
4518 let (frame_size, post_add) = spoofed_stack_frame(gadget.stack_adjust, desired_frame_mod);
4519 let landing = code.len();
4520 let landing_addr = hook.stub_addr + landing as u64 + SPOOFED_CALL_LANDING_DELTA;
4521 code.sub_rsp(frame_size);
4522 code.mov_abs(Reg64::R11, gadget.gadget_addr);
4523 code.store_reg_at_rsp_disp(Reg64::R11, 0);
4524 code.mov_abs(Reg64::Rax, landing_addr);
4525 code.store_reg_at_rsp_disp(Reg64::Rax, 8 + gadget.stack_adjust);
4526 code.mov_abs(Reg64::Rax, function);
4527 code.jmp_rax();
4528 if post_add != 0 {
4529 code.add_rsp(post_add);
4530 }
4531}
4532
4533fn spoofed_stack_frame(stack_adjust: u8, desired_frame_mod: u8) -> (u8, u8) {
4534 let base = 16u16 + u16::from(stack_adjust);
4535 let desired = u16::from(desired_frame_mod);
4536 let post_add = (16 + desired - (base % 16)) % 16;
4537 let frame_size = base + post_add;
4538 (
4539 u8::try_from(frame_size).expect("spoofed stack frame fits in imm8"),
4540 u8::try_from(post_add).expect("spoofed stack fixup fits in imm8"),
4541 )
4542}
4543
4544fn tail_jump_original_import(code: &mut X64Stub, original_target: u64) {
4545 code.pop(Reg64::R9);
4546 code.pop(Reg64::R8);
4547 code.pop(Reg64::Rdx);
4548 code.pop(Reg64::Rcx);
4549 code.mov_abs(Reg64::Rax, original_target);
4550 code.jmp_rax();
4551}
4552
4553fn framed_prologue(code: &mut X64Stub) {
4554 code.sub_rsp(FRAMED_STUB_STACK_ALLOC);
4555 code.store_reg_at_rsp_disp(Reg64::Rcx, 0x28);
4556 code.store_reg_at_rsp_disp(Reg64::Rdx, 0x30);
4557 code.store_reg_at_rsp_disp(Reg64::R8, 0x38);
4558 code.store_reg_at_rsp_disp(Reg64::R9, 0x40);
4559}
4560
4561fn framed_tail_jump_original_import(code: &mut X64Stub, original_target: u64) {
4562 code.load_reg_from_rsp_disp(Reg64::Rcx, 0x28);
4563 code.load_reg_from_rsp_disp(Reg64::Rdx, 0x30);
4564 code.load_reg_from_rsp_disp(Reg64::R8, 0x38);
4565 code.load_reg_from_rsp_disp(Reg64::R9, 0x40);
4566 code.add_rsp(FRAMED_STUB_STACK_ALLOC);
4567 code.mov_abs(Reg64::Rax, original_target);
4568 code.jmp_rax();
4569}
4570
4571#[derive(Clone, Copy)]
4572enum Reg64 {
4573 Rax,
4574 Rcx,
4575 Rdx,
4576 R8,
4577 R9,
4578 R10,
4579 R11,
4580}
4581
4582struct X64Stub {
4583 bytes: Vec<u8>,
4584}
4585
4586impl X64Stub {
4587 fn new() -> Self {
4588 Self {
4589 bytes: Vec::with_capacity(192),
4590 }
4591 }
4592
4593 fn finish(self) -> Vec<u8> {
4594 self.bytes
4595 }
4596
4597 fn len(&self) -> usize {
4598 self.bytes.len()
4599 }
4600
4601 fn mov_abs(&mut self, reg: Reg64, value: u64) {
4602 match reg {
4603 Reg64::Rax => self.bytes.extend_from_slice(&[0x48, 0xB8]),
4604 Reg64::Rcx => self.bytes.extend_from_slice(&[0x48, 0xB9]),
4605 Reg64::Rdx => self.bytes.extend_from_slice(&[0x48, 0xBA]),
4606 Reg64::R8 => self.bytes.extend_from_slice(&[0x49, 0xB8]),
4607 Reg64::R9 => self.bytes.extend_from_slice(&[0x49, 0xB9]),
4608 Reg64::R10 => self.bytes.extend_from_slice(&[0x49, 0xBA]),
4609 Reg64::R11 => self.bytes.extend_from_slice(&[0x49, 0xBB]),
4610 }
4611 self.bytes.extend_from_slice(&value.to_le_bytes());
4612 }
4613
4614 fn store_rax_at_r10(&mut self) {
4615 self.bytes.extend_from_slice(&[0x49, 0x89, 0x02]);
4616 }
4617
4618 fn push(&mut self, reg: Reg64) {
4619 match reg {
4620 Reg64::Rax => self.bytes.push(0x50),
4621 Reg64::Rcx => self.bytes.push(0x51),
4622 Reg64::Rdx => self.bytes.push(0x52),
4623 Reg64::R8 => self.bytes.extend_from_slice(&[0x41, 0x50]),
4624 Reg64::R9 => self.bytes.extend_from_slice(&[0x41, 0x51]),
4625 Reg64::R10 => self.bytes.extend_from_slice(&[0x41, 0x52]),
4626 Reg64::R11 => self.bytes.extend_from_slice(&[0x41, 0x53]),
4627 }
4628 }
4629
4630 fn pop(&mut self, reg: Reg64) {
4631 match reg {
4632 Reg64::Rax => self.bytes.push(0x58),
4633 Reg64::Rcx => self.bytes.push(0x59),
4634 Reg64::Rdx => self.bytes.push(0x5A),
4635 Reg64::R8 => self.bytes.extend_from_slice(&[0x41, 0x58]),
4636 Reg64::R9 => self.bytes.extend_from_slice(&[0x41, 0x59]),
4637 Reg64::R10 => self.bytes.extend_from_slice(&[0x41, 0x5A]),
4638 Reg64::R11 => self.bytes.extend_from_slice(&[0x41, 0x5B]),
4639 }
4640 }
4641
4642 fn mov_byte_zero_at_rdx(&mut self) {
4643 self.bytes.extend_from_slice(&[0xC6, 0x02, 0x00]);
4644 }
4645
4646 fn movzx_eax_byte_at_rdx(&mut self) {
4647 self.bytes.extend_from_slice(&[0x0F, 0xB6, 0x02]);
4648 }
4649
4650 fn mov_al_at_rdx(&mut self) {
4651 self.bytes.extend_from_slice(&[0x88, 0x02]);
4652 }
4653
4654 fn xor_eax_eax(&mut self) {
4655 self.bytes.extend_from_slice(&[0x31, 0xC0]);
4656 }
4657
4658 fn lock_cmpxchg_rax_at_r10_with_r11(&mut self) {
4659 self.bytes
4660 .extend_from_slice(&[0xF0, 0x4D, 0x0F, 0xB1, 0x1A]);
4661 }
4662
4663 fn jne_rel32_placeholder(&mut self) -> usize {
4664 self.bytes.extend_from_slice(&[0x0F, 0x85, 0, 0, 0, 0]);
4665 self.bytes.len() - 4
4666 }
4667
4668 fn patch_rel32(&mut self, imm_offset: usize, target: usize) {
4669 let next = imm_offset + 4;
4670 let rel = (target as isize)
4671 .checked_sub(next as isize)
4672 .expect("stub branch target fits in isize");
4673 let rel = i32::try_from(rel).expect("stub branch target fits in rel32");
4674 self.bytes[imm_offset..imm_offset + 4].copy_from_slice(&rel.to_le_bytes());
4675 }
4676
4677 fn call_rax_windows_x64(&mut self) {
4678 self.sub_rsp(0x28);
4679 self.bytes.extend_from_slice(&[0xFF, 0xD0]);
4680 self.add_rsp(0x28);
4681 }
4682
4683 fn call_rax_with_current_frame(&mut self) {
4684 self.bytes.extend_from_slice(&[0xFF, 0xD0]);
4685 }
4686
4687 fn sub_rsp(&mut self, amount: u8) {
4688 self.bytes.extend_from_slice(&[0x48, 0x83, 0xEC, amount]);
4689 }
4690
4691 fn add_rsp(&mut self, amount: u8) {
4692 self.bytes.extend_from_slice(&[0x48, 0x83, 0xC4, amount]);
4693 }
4694
4695 fn add_rdx_imm32(&mut self, amount: u32) {
4696 self.bytes.extend_from_slice(&[0x48, 0x81, 0xC2]);
4697 self.bytes.extend_from_slice(&amount.to_le_bytes());
4698 }
4699
4700 fn jmp_rax(&mut self) {
4701 self.bytes.extend_from_slice(&[0xFF, 0xE0]);
4702 }
4703
4704 fn dec_rcx(&mut self) {
4705 self.bytes.extend_from_slice(&[0x48, 0xFF, 0xC9]);
4706 }
4707
4708 fn store_reg_at_rsp_disp(&mut self, reg: Reg64, disp: u8) {
4709 match reg {
4710 Reg64::Rax => self
4711 .bytes
4712 .extend_from_slice(&[0x48, 0x89, 0x44, 0x24, disp]),
4713 Reg64::Rcx => self
4714 .bytes
4715 .extend_from_slice(&[0x48, 0x89, 0x4C, 0x24, disp]),
4716 Reg64::Rdx => self
4717 .bytes
4718 .extend_from_slice(&[0x48, 0x89, 0x54, 0x24, disp]),
4719 Reg64::R8 => self
4720 .bytes
4721 .extend_from_slice(&[0x4C, 0x89, 0x44, 0x24, disp]),
4722 Reg64::R9 => self
4723 .bytes
4724 .extend_from_slice(&[0x4C, 0x89, 0x4C, 0x24, disp]),
4725 Reg64::R11 => self
4726 .bytes
4727 .extend_from_slice(&[0x4C, 0x89, 0x5C, 0x24, disp]),
4728 _ => unreachable!("unsupported stack spill register"),
4729 }
4730 }
4731
4732 fn load_reg_from_rsp_disp(&mut self, reg: Reg64, disp: u8) {
4733 match reg {
4734 Reg64::Rcx => self
4735 .bytes
4736 .extend_from_slice(&[0x48, 0x8B, 0x4C, 0x24, disp]),
4737 Reg64::Rdx => self
4738 .bytes
4739 .extend_from_slice(&[0x48, 0x8B, 0x54, 0x24, disp]),
4740 Reg64::R8 => self
4741 .bytes
4742 .extend_from_slice(&[0x4C, 0x8B, 0x44, 0x24, disp]),
4743 Reg64::R9 => self
4744 .bytes
4745 .extend_from_slice(&[0x4C, 0x8B, 0x4C, 0x24, disp]),
4746 _ => unreachable!("unsupported stack reload register"),
4747 }
4748 }
4749}
4750
4751#[cfg(test)]
4752fn windows_x64_call_sequence_offset(bytes: &[u8]) -> Option<usize> {
4753 bytes
4754 .windows(10)
4755 .position(|w| w == [0x48, 0x83, 0xEC, 0x28, 0xFF, 0xD0, 0x48, 0x83, 0xC4, 0x28])
4756}
4757
4758fn find_stage(
4759 backend: &dyn GuestMemoryBackend,
4760 pid: u32,
4761 pattern: Option<&GuestBytePattern>,
4762) -> Result<u64, GuestInjectError> {
4763 if let Some(pattern) = pattern
4764 && let Some(addr) = find_stage_pattern(backend, pid, pattern)?
4765 {
4766 return Ok(addr);
4767 }
4768 find_writable_executable_code_cave(backend, pid, STAGE_CAVE_SIZE)
4769}
4770
4771fn find_stage_pattern(
4772 backend: &dyn GuestMemoryBackend,
4773 pid: u32,
4774 pattern: &GuestBytePattern,
4775) -> Result<Option<u64>, GuestInjectError> {
4776 for region in backend.memory_map(pid)? {
4777 if !region.readable || !region.executable || region.size < pattern.len() as u64 {
4778 continue;
4779 }
4780 let mut pos = 0u64;
4781 while pos < region.size {
4782 let len = (region.size - pos).min(SCAN_CHUNK + pattern.len() as u64) as usize;
4783 let addr = region.base + pos;
4784 if let Ok(bytes) = backend.read(pid, addr, len)
4785 && let Some(off) = pattern.find_in(&bytes)
4786 {
4787 return Ok(Some(addr + off as u64));
4788 }
4789 pos += SCAN_CHUNK;
4790 }
4791 }
4792 Ok(None)
4793}
4794
4795fn find_result_block(
4796 backend: &dyn GuestMemoryBackend,
4797 pid: u32,
4798 pattern: Option<&GuestBytePattern>,
4799 fallback: u64,
4800) -> Result<u64, GuestInjectError> {
4801 if let Some(pattern) = pattern
4802 && let Some(addr) = find_writable_pattern(backend, pid, pattern)?
4803 {
4804 return Ok(addr);
4805 }
4806 validate_result_region(backend, pid, fallback)?;
4807 Ok(fallback)
4808}
4809
4810fn find_writable_pattern(
4811 backend: &dyn GuestMemoryBackend,
4812 pid: u32,
4813 pattern: &GuestBytePattern,
4814) -> Result<Option<u64>, GuestInjectError> {
4815 for region in backend.memory_map(pid)? {
4816 if !region.readable || !region.writable || region.size < pattern.len() as u64 {
4817 continue;
4818 }
4819 let mut pos = 0u64;
4820 while pos < region.size {
4821 let len = (region.size - pos).min(SCAN_CHUNK + pattern.len() as u64) as usize;
4822 let addr = region.base + pos;
4823 if let Ok(bytes) = backend.read(pid, addr, len)
4824 && let Some(off) = pattern.find_in(&bytes)
4825 {
4826 return Ok(Some(addr + off as u64));
4827 }
4828 pos += SCAN_CHUNK;
4829 }
4830 }
4831 Ok(None)
4832}
4833
4834fn validate_stub_region(
4835 backend: &dyn GuestMemoryBackend,
4836 pid: u32,
4837 stage: u64,
4838 size: usize,
4839) -> Result<(), GuestInjectError> {
4840 let end = stage
4841 .checked_add(size as u64)
4842 .ok_or_else(|| GuestInjectError::Config("guest stage range overflows".into()))?;
4843 for region in backend.memory_map(pid)? {
4844 let region_end = region.base.saturating_add(region.size);
4845 if stage >= region.base && end <= region_end {
4846 if region.readable && region.executable {
4847 return Ok(());
4848 }
4849 return Err(GuestInjectError::Config(format!(
4850 "guest execution stub at {stage:#x} must be readable+executable; region permissions are {}{}{}",
4851 if region.readable { 'r' } else { '-' },
4852 if region.writable { 'w' } else { '-' },
4853 if region.executable { 'x' } else { '-' },
4854 )));
4855 }
4856 }
4857 Err(GuestInjectError::Config(format!(
4858 "guest execution stub at {stage:#x} does not fit in one mapped region"
4859 )))
4860}
4861
4862fn validate_result_region(
4863 backend: &dyn GuestMemoryBackend,
4864 pid: u32,
4865 result: u64,
4866) -> Result<(), GuestInjectError> {
4867 let end = result
4868 .checked_add(RESULT_BLOCK_SIZE as u64)
4869 .ok_or_else(|| GuestInjectError::Config("guest result block range overflows".into()))?;
4870 for region in backend.memory_map(pid)? {
4871 let region_end = region.base.saturating_add(region.size);
4872 if result >= region.base && end <= region_end {
4873 if region.readable && region.writable {
4874 return Ok(());
4875 }
4876 return Err(GuestInjectError::Config(format!(
4877 "guest result block at {result:#x} must be readable+writable; region permissions are {}{}{}",
4878 if region.readable { 'r' } else { '-' },
4879 if region.writable { 'w' } else { '-' },
4880 if region.executable { 'x' } else { '-' },
4881 )));
4882 }
4883 }
4884 Err(GuestInjectError::Config(format!(
4885 "guest result block at {result:#x} does not fit in one mapped region"
4886 )))
4887}
4888
4889fn find_writable_executable_code_cave(
4890 backend: &dyn GuestMemoryBackend,
4891 pid: u32,
4892 size: usize,
4893) -> Result<u64, GuestInjectError> {
4894 for region in backend.memory_map(pid)? {
4895 if !region.readable || !region.writable || !region.executable || region.size < size as u64 {
4896 continue;
4897 }
4898 let mut pos = 0u64;
4899 while pos < region.size {
4900 let len = (region.size - pos).min(SCAN_CHUNK + size as u64) as usize;
4901 let addr = region.base + pos;
4902 if let Ok(bytes) = backend.read(pid, addr, len)
4903 && let Some(off) = find_code_cave(&bytes, size)
4904 {
4905 return Ok(addr + off as u64);
4906 }
4907 pos += SCAN_CHUNK;
4908 }
4909 }
4910 Err(GuestInjectError::Config(
4911 "guest IAT-hook execution needs guest.stage_base, guest.stage_pattern, or a writable+executable staging region; refusing to patch arbitrary RX code".into(),
4912 ))
4913}
4914
4915fn find_process_pattern<B: GuestMemoryBackend + ?Sized>(
4916 backend: &B,
4917 pid: u32,
4918 pattern: &GuestBytePattern,
4919) -> Result<Option<u64>, GuestInjectError> {
4920 let regions = match backend.memory_map(pid) {
4921 Ok(regions) => regions,
4922 Err(err) if is_process_gone_error(&err) => return Ok(None),
4923 Err(err) => return Err(err),
4924 };
4925 for region in regions {
4926 if !region.readable || region.size < pattern.len() as u64 {
4927 continue;
4928 }
4929 let mut pos = 0u64;
4930 while pos < region.size {
4931 let len = (region.size - pos).min(SCAN_CHUNK + pattern.len() as u64) as usize;
4932 let addr = region.base + pos;
4933 if let Ok(bytes) = backend.read(pid, addr, len)
4934 && let Some(off) = pattern.find_in(&bytes)
4935 {
4936 return Ok(Some(addr + off as u64));
4937 }
4938 pos += SCAN_CHUNK;
4939 }
4940 }
4941 Ok(None)
4942}
4943
4944fn is_process_gone_error(err: &GuestInjectError) -> bool {
4945 match err {
4946 GuestInjectError::Process(_) => true,
4947 GuestInjectError::Backend(message) => message.contains("no such process"),
4948 _ => false,
4949 }
4950}
4951
4952fn find_code_cave(bytes: &[u8], size: usize) -> Option<usize> {
4953 let mut start = None;
4954 for (idx, byte) in bytes.iter().enumerate() {
4955 if matches!(*byte, 0x00 | 0x90 | 0xCC) {
4956 let aligned = match start {
4957 Some(pos) => pos,
4958 None => align_up(idx, 16),
4959 };
4960 start = Some(aligned);
4961 if idx + 1 >= aligned + size {
4962 return Some(aligned);
4963 }
4964 } else {
4965 start = None;
4966 }
4967 }
4968 None
4969}
4970
4971fn align_up(value: usize, align: usize) -> usize {
4972 (value + align - 1) & !(align - 1)
4973}
4974
4975fn find_iat_hook(
4976 backend: &dyn GuestMemoryBackend,
4977 pid: u32,
4978 plan: &GuestInjectionPlan,
4979) -> Result<RemoteIatHook, GuestInjectError> {
4980 let module = target_module(backend, pid, plan)?;
4981 let mut errors = Vec::new();
4982 for hook_module in import_module_candidates(&plan.hook_module) {
4983 match find_import_iat(backend, pid, module.base, &hook_module, &plan.hook_function) {
4984 Ok(import) => return Ok(import),
4985 Err(err) => errors.push(format!("{hook_module}: {err}")),
4986 }
4987 }
4988 Err(GuestInjectError::Image(format!(
4989 "target import {}!{} not found via {}",
4990 plan.hook_module,
4991 plan.hook_function,
4992 errors.join("; ")
4993 )))
4994}
4995
4996fn target_module(
4997 backend: &dyn GuestMemoryBackend,
4998 pid: u32,
4999 plan: &GuestInjectionPlan,
5000) -> Result<GuestModuleInfo, GuestInjectError> {
5001 let modules = backend.module_list(pid)?;
5002 match plan.target_module.as_deref() {
5003 Some(name) => modules
5004 .into_iter()
5005 .find(|m| m.name.eq_ignore_ascii_case(name))
5006 .ok_or_else(|| GuestInjectError::Process(format!("module {name:?} not found"))),
5007 None => modules
5008 .into_iter()
5009 .find(|m| m.name.to_ascii_lowercase().ends_with(".exe"))
5010 .ok_or_else(|| GuestInjectError::Process("target exe module not found".into())),
5011 }
5012}
5013
5014fn find_import_iat(
5015 backend: &dyn GuestMemoryBackend,
5016 pid: u32,
5017 base: u64,
5018 module_name: &str,
5019 function_name: &str,
5020) -> Result<RemoteIatHook, GuestInjectError> {
5021 let hdr = backend.read(pid, base, 0x1000)?;
5022 if u16_at(&hdr, 0).map_err(GuestInjectError::from)? != 0x5A4D {
5023 return Err(GuestInjectError::Image(
5024 "target module missing MZ header".into(),
5025 ));
5026 }
5027 let nt = u32_at(&hdr, 0x3C).map_err(GuestInjectError::from)? as usize;
5028 if u32_at(&hdr, nt).map_err(GuestInjectError::from)? != 0x0000_4550 {
5029 return Err(GuestInjectError::Image(
5030 "target module missing PE header".into(),
5031 ));
5032 }
5033 let opt = nt + 24;
5034 let import_rva = u32_at(&hdr, opt + 112 + 8).map_err(GuestInjectError::from)? as u64;
5035 if import_rva == 0 {
5036 return Err(GuestInjectError::Image(
5037 "target module has no import table".into(),
5038 ));
5039 }
5040 let mut desc_addr = base + import_rva;
5041 loop {
5042 let desc = backend.read(pid, desc_addr, 20)?;
5043 let original_first_thunk = u32_at(&desc, 0).map_err(GuestInjectError::from)? as u64;
5044 let name_rva = u32_at(&desc, 12).map_err(GuestInjectError::from)? as u64;
5045 let first_thunk = u32_at(&desc, 16).map_err(GuestInjectError::from)? as u64;
5046 if original_first_thunk == 0 && name_rva == 0 && first_thunk == 0 {
5047 break;
5048 }
5049 let remote_name = read_remote_cstr(backend, pid, base + name_rva, 256)?;
5050 if remote_name.eq_ignore_ascii_case(module_name) {
5051 let lookup = match original_first_thunk {
5052 0 => first_thunk,
5053 rva => rva,
5054 };
5055 let mut index = 0u64;
5056 loop {
5057 let thunk = read_remote_u64(backend, pid, base + lookup + index * 8)?;
5058 if thunk == 0 {
5059 break;
5060 }
5061 if thunk & 0x8000_0000_0000_0000 == 0 {
5062 let name = read_remote_cstr(backend, pid, base + thunk + 2, 256)?;
5063 if name == function_name {
5064 let iat_slot = base + first_thunk + index * 8;
5065 return Ok(RemoteIatHook {
5066 iat_slot,
5067 original_target: read_remote_u64(backend, pid, iat_slot)?,
5068 });
5069 }
5070 }
5071 index += 1;
5072 }
5073 }
5074 desc_addr += 20;
5075 }
5076 Err(GuestInjectError::Image(format!(
5077 "target import {module_name}!{function_name} not found"
5078 )))
5079}
5080
5081fn resolve_import_symbol(
5082 backend: &dyn GuestMemoryBackend,
5083 pid: u32,
5084 module: &str,
5085 name: &str,
5086) -> Result<u64, GuestInjectError> {
5087 let candidates = import_module_candidates(module);
5088 let mut errors = Vec::new();
5089 tracing::debug!(
5090 pid,
5091 requested_module = module,
5092 symbol = name,
5093 candidates = %candidates.join(","),
5094 "resolving guest import symbol"
5095 );
5096 for candidate in &candidates {
5097 match resolve_export_name(backend, pid, candidate, name, 0) {
5098 Ok(addr) => {
5099 tracing::debug!(
5100 pid,
5101 requested_module = module,
5102 resolved_module = candidate,
5103 symbol = name,
5104 address = format_args!("{addr:#x}"),
5105 "guest import symbol resolved"
5106 );
5107 return Ok(addr);
5108 }
5109 Err(err) => {
5110 tracing::debug!(
5111 pid,
5112 requested_module = module,
5113 candidate,
5114 symbol = name,
5115 error = %err,
5116 "guest import candidate failed"
5117 );
5118 errors.push(format!("{candidate}: {err}"));
5119 }
5120 }
5121 }
5122 Err(GuestInjectError::Image(format!(
5123 "import {module}!{name} not found via {}",
5124 errors.join("; ")
5125 )))
5126}
5127
5128fn resolve_import_symbol_ordinal(
5129 backend: &dyn GuestMemoryBackend,
5130 pid: u32,
5131 module: &str,
5132 ordinal: u16,
5133) -> Result<u64, GuestInjectError> {
5134 let candidates = import_module_candidates(module);
5135 let mut errors = Vec::new();
5136 tracing::debug!(
5137 pid,
5138 requested_module = module,
5139 ordinal,
5140 candidates = %candidates.join(","),
5141 "resolving guest ordinal import"
5142 );
5143 for candidate in &candidates {
5144 match resolve_export_ordinal(backend, pid, candidate, ordinal, 0) {
5145 Ok(addr) => {
5146 tracing::debug!(
5147 pid,
5148 requested_module = module,
5149 resolved_module = candidate,
5150 ordinal,
5151 address = format_args!("{addr:#x}"),
5152 "guest ordinal import resolved"
5153 );
5154 return Ok(addr);
5155 }
5156 Err(err) => {
5157 tracing::debug!(
5158 pid,
5159 requested_module = module,
5160 candidate,
5161 ordinal,
5162 error = %err,
5163 "guest ordinal import candidate failed"
5164 );
5165 errors.push(format!("{candidate}: {err}"));
5166 }
5167 }
5168 }
5169 Err(GuestInjectError::Image(format!(
5170 "import {module}!#{ordinal} not found via {}",
5171 errors.join("; ")
5172 )))
5173}
5174
5175#[allow(clippy::too_many_arguments)]
5176fn resolve_import_symbol_with_dependency_policy(
5177 backend: &dyn GuestMemoryBackend,
5178 pid: u32,
5179 hook: &GuestIatHook,
5180 loader_apis: Option<GuestLoaderApis>,
5181 scratch_addr: u64,
5182 scratch_len: usize,
5183 timeout_ms: u32,
5184 policy: GuestDependencyPolicy,
5185 module: &str,
5186 symbol: ImportSymbol<'_>,
5187) -> Result<u64, GuestInjectError> {
5188 let resolve = |backend: &dyn GuestMemoryBackend| match symbol {
5189 ImportSymbol::Name(name) => {
5190 let name = std::str::from_utf8(name)
5191 .map_err(|e| GuestInjectError::Image(format!("import name: {e}")))?;
5192 resolve_import_symbol(backend, pid, module, name)
5193 }
5194 ImportSymbol::Ordinal(ordinal) => {
5195 resolve_import_symbol_ordinal(backend, pid, module, ordinal)
5196 }
5197 };
5198
5199 match resolve(backend) {
5200 Ok(addr) => return Ok(addr),
5201 Err(err) if policy == GuestDependencyPolicy::LoadWithGuestLoader => {
5202 tracing::info!(
5203 pid,
5204 module,
5205 error = %err,
5206 "guest import dependency missing; loading through guest loader"
5207 );
5208 }
5209 Err(err) => return Err(err),
5210 }
5211
5212 let loader_apis = loader_apis.ok_or_else(|| GuestInjectError::Unsupported {
5213 operation: "guest dependency loading",
5214 reason: "LoadLibraryA/GetProcAddress were not resolved".into(),
5215 })?;
5216 let load_name = dependency_load_name(module);
5217 let loaded_base = load_guest_dependency(
5218 backend,
5219 pid,
5220 hook,
5221 loader_apis.load_library,
5222 scratch_addr,
5223 scratch_len,
5224 timeout_ms,
5225 &load_name,
5226 )?;
5227 match resolve(backend) {
5228 Ok(addr) => Ok(addr),
5229 Err(err) => {
5230 tracing::debug!(
5231 pid,
5232 module,
5233 loaded_module = load_name,
5234 loaded_base = format_args!("{loaded_base:#x}"),
5235 error = %err,
5236 "guest module-list import retry failed; resolving export through guest GetProcAddress"
5237 );
5238 resolve_loaded_import_symbol(
5239 backend,
5240 pid,
5241 hook,
5242 loader_apis.get_proc_address,
5243 scratch_addr,
5244 scratch_len,
5245 timeout_ms,
5246 &load_name,
5247 loaded_base,
5248 symbol,
5249 )
5250 }
5251 }
5252}
5253
5254#[derive(Clone, Copy)]
5255struct GuestLoaderApis {
5256 load_library: u64,
5257 get_proc_address: u64,
5258}
5259
5260fn dependency_load_name(module: &str) -> String {
5261 let lower = module.to_ascii_lowercase();
5262 if lower.starts_with("api-ms-win-crt-") {
5263 "ucrtbase.dll".into()
5264 } else if lower.starts_with("api-ms-win-core-") {
5265 "kernelbase.dll".into()
5266 } else {
5267 module.into()
5268 }
5269}
5270
5271#[allow(clippy::too_many_arguments)]
5272fn load_guest_dependency(
5273 backend: &dyn GuestMemoryBackend,
5274 pid: u32,
5275 hook: &GuestIatHook,
5276 load_library: u64,
5277 scratch_addr: u64,
5278 scratch_len: usize,
5279 timeout_ms: u32,
5280 module: &str,
5281) -> Result<u64, GuestInjectError> {
5282 let mut bytes = module.as_bytes().to_vec();
5283 bytes.push(0);
5284 if bytes.len() > scratch_len {
5285 return Err(GuestInjectError::Unsupported {
5286 operation: "guest dependency loading",
5287 reason: format!(
5288 "module name {module:?} needs {} scratch bytes, only {scratch_len} available",
5289 bytes.len()
5290 ),
5291 });
5292 }
5293
5294 let original = backend.read(pid, scratch_addr, bytes.len())?;
5295 write_verified(
5296 backend,
5297 pid,
5298 scratch_addr,
5299 &bytes,
5300 "guest dependency module name",
5301 )?;
5302 let call_result =
5303 backend.call_iat_hook(pid, hook, load_library, [scratch_addr, 0, 0, 0], timeout_ms);
5304 let restore_result = backend.write(pid, scratch_addr, &original);
5305 if let Err(err) = restore_result {
5306 tracing::warn!(
5307 pid,
5308 scratch_addr = format_args!("{scratch_addr:#x}"),
5309 error = %err,
5310 "failed to restore guest dependency scratch bytes"
5311 );
5312 }
5313
5314 let module_base = call_result?;
5315 if module_base == 0 {
5316 return Err(GuestInjectError::Backend(format!(
5317 "guest LoadLibraryA({module:?}) returned NULL"
5318 )));
5319 }
5320 tracing::info!(
5321 pid,
5322 module,
5323 module_base = format_args!("{module_base:#x}"),
5324 "guest dependency loaded"
5325 );
5326 Ok(module_base)
5327}
5328
5329#[allow(clippy::too_many_arguments)]
5330fn resolve_loaded_import_symbol(
5331 backend: &dyn GuestMemoryBackend,
5332 pid: u32,
5333 hook: &GuestIatHook,
5334 get_proc_address: u64,
5335 scratch_addr: u64,
5336 scratch_len: usize,
5337 timeout_ms: u32,
5338 module: &str,
5339 module_base: u64,
5340 symbol: ImportSymbol<'_>,
5341) -> Result<u64, GuestInjectError> {
5342 let proc = match symbol {
5343 ImportSymbol::Name(name) => {
5344 let name = std::str::from_utf8(name)
5345 .map_err(|e| GuestInjectError::Image(format!("import name: {e}")))?;
5346 guest_get_proc_address(
5347 backend,
5348 pid,
5349 hook,
5350 get_proc_address,
5351 scratch_addr,
5352 scratch_len,
5353 timeout_ms,
5354 module_base,
5355 name,
5356 )?
5357 }
5358 ImportSymbol::Ordinal(ordinal) => backend.call_iat_hook(
5359 pid,
5360 hook,
5361 get_proc_address,
5362 [module_base, u64::from(ordinal), 0, 0],
5363 timeout_ms,
5364 )?,
5365 };
5366 if proc == 0 {
5367 return Err(GuestInjectError::Image(format!(
5368 "export {module}!{} not found",
5369 symbol.label()
5370 )));
5371 }
5372 tracing::debug!(
5373 pid,
5374 module,
5375 module_base = format_args!("{module_base:#x}"),
5376 symbol = %symbol.label(),
5377 address = format_args!("{proc:#x}"),
5378 "guest loaded dependency export resolved through GetProcAddress"
5379 );
5380 Ok(proc)
5381}
5382
5383#[allow(clippy::too_many_arguments)]
5384fn guest_get_proc_address(
5385 backend: &dyn GuestMemoryBackend,
5386 pid: u32,
5387 hook: &GuestIatHook,
5388 get_proc_address: u64,
5389 scratch_addr: u64,
5390 scratch_len: usize,
5391 timeout_ms: u32,
5392 module_base: u64,
5393 name: &str,
5394) -> Result<u64, GuestInjectError> {
5395 let mut bytes = name.as_bytes().to_vec();
5396 bytes.push(0);
5397 if bytes.len() > scratch_len {
5398 return Err(GuestInjectError::Unsupported {
5399 operation: "guest dependency export lookup",
5400 reason: format!(
5401 "symbol name {name:?} needs {} scratch bytes, only {scratch_len} available",
5402 bytes.len()
5403 ),
5404 });
5405 }
5406
5407 let original = backend.read(pid, scratch_addr, bytes.len())?;
5408 write_verified(
5409 backend,
5410 pid,
5411 scratch_addr,
5412 &bytes,
5413 "guest dependency symbol name",
5414 )?;
5415 let call_result = backend.call_iat_hook(
5416 pid,
5417 hook,
5418 get_proc_address,
5419 [module_base, scratch_addr, 0, 0],
5420 timeout_ms,
5421 );
5422 let restore_result = backend.write(pid, scratch_addr, &original);
5423 if let Err(err) = restore_result {
5424 tracing::warn!(
5425 pid,
5426 scratch_addr = format_args!("{scratch_addr:#x}"),
5427 error = %err,
5428 "failed to restore guest dependency symbol scratch bytes"
5429 );
5430 }
5431 call_result
5432}
5433
5434impl ImportSymbol<'_> {
5435 fn label(&self) -> String {
5436 match self {
5437 ImportSymbol::Name(name) => String::from_utf8_lossy(name).into_owned(),
5438 ImportSymbol::Ordinal(ordinal) => format!("#{ordinal}"),
5439 }
5440 }
5441}
5442
5443#[derive(Clone, Copy)]
5444enum ExportLookup<'a> {
5445 Name(&'a str),
5446 Ordinal(u16),
5447}
5448
5449fn resolve_export_name(
5450 backend: &dyn GuestMemoryBackend,
5451 pid: u32,
5452 module: &str,
5453 name: &str,
5454 depth: usize,
5455) -> Result<u64, GuestInjectError> {
5456 resolve_export(backend, pid, module, ExportLookup::Name(name), depth)
5457}
5458
5459fn resolve_export_ordinal(
5460 backend: &dyn GuestMemoryBackend,
5461 pid: u32,
5462 module: &str,
5463 ordinal: u16,
5464 depth: usize,
5465) -> Result<u64, GuestInjectError> {
5466 resolve_export(backend, pid, module, ExportLookup::Ordinal(ordinal), depth)
5467}
5468
5469fn resolve_export(
5470 backend: &dyn GuestMemoryBackend,
5471 pid: u32,
5472 module: &str,
5473 lookup: ExportLookup<'_>,
5474 depth: usize,
5475) -> Result<u64, GuestInjectError> {
5476 if depth > MAX_EXPORT_FORWARD_DEPTH {
5477 return Err(GuestInjectError::Image(format!(
5478 "forwarded export chain exceeded {MAX_EXPORT_FORWARD_DEPTH} hops"
5479 )));
5480 }
5481 let module = find_module_ci(backend, pid, module)?;
5482 let export_rva = find_export_rva(backend, pid, &module, lookup)?;
5483 let export_dir = export_directory(backend, pid, module.base)?;
5484 if export_rva >= export_dir.rva as u64
5485 && export_rva < export_dir.rva as u64 + export_dir.size as u64
5486 {
5487 let forwarder = read_remote_cstr(backend, pid, module.base + export_rva, 256)?;
5488 tracing::debug!(
5489 pid,
5490 module = %module.name,
5491 forwarder,
5492 "guest export is forwarded"
5493 );
5494 return resolve_forwarded_export(backend, pid, &forwarder, depth + 1);
5495 }
5496 Ok(module.base + export_rva)
5497}
5498
5499fn resolve_forwarded_export(
5500 backend: &dyn GuestMemoryBackend,
5501 pid: u32,
5502 forwarder: &str,
5503 depth: usize,
5504) -> Result<u64, GuestInjectError> {
5505 let (module, symbol) = forwarder.rsplit_once('.').ok_or_else(|| {
5506 GuestInjectError::Image(format!("bad forwarded export string {forwarder:?}"))
5507 })?;
5508 let module = normalize_forwarder_module(module);
5509 match symbol.strip_prefix('#') {
5510 Some(ordinal) => {
5511 let ordinal = ordinal.parse::<u16>().map_err(|e| {
5512 GuestInjectError::Image(format!("bad forwarded export ordinal {forwarder:?}: {e}"))
5513 })?;
5514 resolve_export_ordinal(backend, pid, &module, ordinal, depth)
5515 }
5516 None => resolve_export_name(backend, pid, &module, symbol, depth),
5517 }
5518}
5519
5520fn normalize_forwarder_module(module: &str) -> String {
5521 let lower = module.to_ascii_lowercase();
5522 if lower.ends_with(".dll") || lower.ends_with(".exe") {
5523 module.to_string()
5524 } else {
5525 format!("{module}.dll")
5526 }
5527}
5528
5529fn find_module_ci(
5530 backend: &dyn GuestMemoryBackend,
5531 pid: u32,
5532 module: &str,
5533) -> Result<GuestModuleInfo, GuestInjectError> {
5534 let modules = backend.module_list(pid)?;
5535 modules
5536 .into_iter()
5537 .find(|m| {
5538 m.name.eq_ignore_ascii_case(module)
5539 || m.name
5540 .rsplit(['\\', '/'])
5541 .next()
5542 .is_some_and(|base| base.eq_ignore_ascii_case(module))
5543 })
5544 .ok_or_else(|| GuestInjectError::Process(format!("module {module:?} not found")))
5545}
5546
5547fn find_export_rva(
5548 backend: &dyn GuestMemoryBackend,
5549 pid: u32,
5550 module: &GuestModuleInfo,
5551 lookup: ExportLookup<'_>,
5552) -> Result<u64, GuestInjectError> {
5553 let export = export_directory(backend, pid, module.base)?;
5554 let dir = backend.read(pid, module.base + export.rva as u64, 40)?;
5555 let ordinal_base = u32_at(&dir, 16).map_err(GuestInjectError::from)?;
5556 let function_count = u32_at(&dir, 20).map_err(GuestInjectError::from)?;
5557 let name_count = u32_at(&dir, 24).map_err(GuestInjectError::from)?;
5558 let functions_rva = u32_at(&dir, 28).map_err(GuestInjectError::from)? as u64;
5559 let names_rva = u32_at(&dir, 32).map_err(GuestInjectError::from)? as u64;
5560 let ordinals_rva = u32_at(&dir, 36).map_err(GuestInjectError::from)? as u64;
5561
5562 let index = match lookup {
5563 ExportLookup::Name(want) => {
5564 let mut found = None;
5565 for i in 0..name_count.min(65_536) {
5566 let name_rva =
5567 read_remote_u32(backend, pid, module.base + names_rva + u64::from(i) * 4)?;
5568 let name = read_remote_cstr(backend, pid, module.base + u64::from(name_rva), 512)?;
5569 if name == want {
5570 found = Some(u32::from(read_remote_u16(
5571 backend,
5572 pid,
5573 module.base + ordinals_rva + u64::from(i) * 2,
5574 )?));
5575 break;
5576 }
5577 }
5578 found.ok_or_else(|| {
5579 GuestInjectError::Image(format!("export {}!{want} not found", module.name))
5580 })?
5581 }
5582 ExportLookup::Ordinal(ordinal) => {
5583 let ordinal = u32::from(ordinal);
5584 if ordinal < ordinal_base {
5585 return Err(GuestInjectError::Image(format!(
5586 "export {}!#{ordinal} precedes ordinal base {ordinal_base}",
5587 module.name
5588 )));
5589 }
5590 ordinal - ordinal_base
5591 }
5592 };
5593
5594 if index >= function_count {
5595 return Err(GuestInjectError::Image(format!(
5596 "export {} index {index} exceeds function count {function_count}",
5597 module.name
5598 )));
5599 }
5600 let rva = read_remote_u32(
5601 backend,
5602 pid,
5603 module.base + functions_rva + u64::from(index) * 4,
5604 )?;
5605 if rva == 0 {
5606 return Err(GuestInjectError::Image(format!(
5607 "export {} index {index} is null",
5608 module.name
5609 )));
5610 }
5611 Ok(u64::from(rva))
5612}
5613
5614fn export_directory(
5615 backend: &dyn GuestMemoryBackend,
5616 pid: u32,
5617 base: u64,
5618) -> Result<crate::pe::Dir, GuestInjectError> {
5619 let mut last = None;
5620 for attempt in 0..=EXPORT_HEADER_RETRIES {
5621 match export_directory_once(backend, pid, base) {
5622 Ok(dir) => return Ok(dir),
5623 Err(err)
5624 if is_transient_export_header_error(&err) && attempt < EXPORT_HEADER_RETRIES =>
5625 {
5626 tracing::debug!(
5627 pid,
5628 base = format_args!("{base:#x}"),
5629 attempt = attempt + 1,
5630 error = %err,
5631 "guest export header not ready; retrying"
5632 );
5633 last = Some(err);
5634 thread::sleep(EXPORT_HEADER_RETRY_DELAY);
5635 }
5636 Err(err) => return Err(err),
5637 }
5638 }
5639 Err(last.unwrap_or_else(|| GuestInjectError::Image("export module header not ready".into())))
5640}
5641
5642fn export_directory_once(
5643 backend: &dyn GuestMemoryBackend,
5644 pid: u32,
5645 base: u64,
5646) -> Result<crate::pe::Dir, GuestInjectError> {
5647 let hdr = backend.read(pid, base, 0x1000)?;
5648 if u16_at(&hdr, 0).map_err(GuestInjectError::from)? != 0x5A4D {
5649 return Err(GuestInjectError::Image(format!(
5650 "export module at {base:#x} missing MZ header"
5651 )));
5652 }
5653 let nt = u32_at(&hdr, 0x3C).map_err(GuestInjectError::from)? as usize;
5654 if u32_at(&hdr, nt).map_err(GuestInjectError::from)? != 0x0000_4550 {
5655 return Err(GuestInjectError::Image(format!(
5656 "export module at {base:#x} missing PE header"
5657 )));
5658 }
5659 let opt = nt + 24;
5660 let rva = u32_at(&hdr, opt + 112).map_err(GuestInjectError::from)?;
5661 let size = u32_at(&hdr, opt + 116).map_err(GuestInjectError::from)?;
5662 if rva == 0 || size == 0 {
5663 return Err(GuestInjectError::Image(format!(
5664 "export module at {base:#x} has no export directory"
5665 )));
5666 }
5667 Ok(crate::pe::Dir { rva, size })
5668}
5669
5670fn is_transient_export_header_error(err: &GuestInjectError) -> bool {
5671 match err {
5672 GuestInjectError::Image(message) => {
5673 message.contains("missing MZ header") || message.contains("missing PE header")
5674 }
5675 GuestInjectError::Backend(message) => message.contains("read"),
5676 _ => false,
5677 }
5678}
5679
5680fn import_module_candidates(module: &str) -> Vec<String> {
5681 let mut out = vec![module.to_string()];
5682 let lower = module.to_ascii_lowercase();
5683 if lower == "kernel32.dll" {
5684 out.push("kernelbase.dll".into());
5685 } else if lower.starts_with("api-ms-win-crt-") {
5686 out.push("ucrtbase.dll".into());
5687 } else if lower.starts_with("api-ms-win-core-") {
5688 out.push("kernelbase.dll".into());
5689 out.push("kernel32.dll".into());
5690 out.push("ntdll.dll".into());
5691 }
5692 out.dedup_by(|a, b| a.eq_ignore_ascii_case(b));
5693 out
5694}
5695
5696fn read_remote_u64(
5697 backend: &dyn GuestMemoryBackend,
5698 pid: u32,
5699 addr: u64,
5700) -> Result<u64, GuestInjectError> {
5701 let bytes = backend.read(pid, addr, 8)?;
5702 Ok(u64::from_le_bytes(bytes[0..8].try_into().unwrap()))
5703}
5704
5705fn read_remote_u32(
5706 backend: &dyn GuestMemoryBackend,
5707 pid: u32,
5708 addr: u64,
5709) -> Result<u32, GuestInjectError> {
5710 let bytes = backend.read(pid, addr, 4)?;
5711 Ok(u32::from_le_bytes(bytes[0..4].try_into().unwrap()))
5712}
5713
5714fn read_remote_u16(
5715 backend: &dyn GuestMemoryBackend,
5716 pid: u32,
5717 addr: u64,
5718) -> Result<u16, GuestInjectError> {
5719 let bytes = backend.read(pid, addr, 2)?;
5720 Ok(u16::from_le_bytes(bytes[0..2].try_into().unwrap()))
5721}
5722
5723fn read_remote_cstr(
5724 backend: &dyn GuestMemoryBackend,
5725 pid: u32,
5726 addr: u64,
5727 max: usize,
5728) -> Result<String, GuestInjectError> {
5729 let bytes = backend.read(pid, addr, max)?;
5730 let len = bytes
5731 .iter()
5732 .position(|b| *b == 0)
5733 .ok_or_else(|| GuestInjectError::Image(format!("unterminated string at {addr:#x}")))?;
5734 Ok(String::from_utf8_lossy(&bytes[..len]).into_owned())
5735}
5736
5737fn parse_hex_pattern(pattern: &str, field: &str) -> Result<GuestBytePattern, GuestInjectError> {
5738 let mut out = Vec::new();
5739 for token in pattern.split_whitespace() {
5740 if token == "?" || token == "??" {
5741 out.push(None);
5742 } else {
5743 let hex = token.trim_start_matches("0x");
5744 let byte = u8::from_str_radix(hex, 16).map_err(|e| {
5745 GuestInjectError::Config(format!("bad {field} byte {token:?}: {e}"))
5746 })?;
5747 out.push(Some(byte));
5748 }
5749 }
5750 match out.is_empty() {
5751 true => Err(GuestInjectError::Config(format!("{field} cannot be empty"))),
5752 false => Ok(GuestBytePattern { bytes: out }),
5753 }
5754}
5755
5756#[cfg(test)]
5757mod tests {
5758 use super::*;
5759
5760 #[test]
5761 fn target_requires_pid_or_name() {
5762 let target = GuestProcessSelector {
5763 pid: None,
5764 name: None,
5765 pattern: None,
5766 };
5767 assert!(matches!(
5768 target.validate(),
5769 Err(GuestInjectError::Config(_))
5770 ));
5771 }
5772
5773 #[test]
5774 fn memflow_capabilities_cover_iat_hook_manual_map() {
5775 let capabilities = GuestCapabilities::memflow_guest_injection();
5776 let missing = capabilities.missing_manual_map();
5777 assert!(missing.is_empty(), "{missing:?}");
5778 assert!(capabilities.exception_registration);
5779 assert!(!capabilities.vad_spoof);
5780 }
5781
5782 struct VadRejectBackend;
5783
5784 impl GuestMemoryBackend for VadRejectBackend {
5785 fn capabilities(&self) -> GuestCapabilities {
5786 GuestCapabilities::memflow_guest_injection()
5787 }
5788
5789 fn list_processes(&self) -> Result<Vec<GuestProcessInfo>, GuestInjectError> {
5790 panic!("vad spoof rejection should happen before process access")
5791 }
5792
5793 fn module_list(&self, _pid: u32) -> Result<Vec<GuestModuleInfo>, GuestInjectError> {
5794 panic!("vad spoof rejection should happen before module access")
5795 }
5796
5797 fn module_exports(
5798 &self,
5799 _pid: u32,
5800 _module: &str,
5801 ) -> Result<Vec<(String, u64)>, GuestInjectError> {
5802 panic!("vad spoof rejection should happen before export access")
5803 }
5804
5805 fn memory_map(&self, _pid: u32) -> Result<Vec<GuestMemoryRegion>, GuestInjectError> {
5806 panic!("vad spoof rejection should happen before memory-map access")
5807 }
5808
5809 fn read(&self, _pid: u32, _addr: u64, _len: usize) -> Result<Vec<u8>, GuestInjectError> {
5810 panic!("vad spoof rejection should happen before memory reads")
5811 }
5812
5813 fn write(&self, _pid: u32, _addr: u64, _data: &[u8]) -> Result<(), GuestInjectError> {
5814 panic!("vad spoof rejection should happen before memory writes")
5815 }
5816 }
5817
5818 #[test]
5819 fn vad_spoof_is_rejected_before_guest_access_when_backend_cannot_do_it() {
5820 let config = DecantConfig::from_toml_str(
5821 "[injection]\ndomain = \"guest\"\nmethod = \"manual-map\"\n\
5822 [guest]\npid = 1\npayload_path = \"payload.dll\"\n\
5823 vad_spoof = \"vad-image-map\"\n",
5824 )
5825 .unwrap();
5826 let plan = GuestInjectionPlan::from_config(&config).unwrap();
5827 let injector = GuestManualMapInjector;
5828 let req = GuestInjectionRequest {
5829 plan: &plan,
5830 payload_path: std::path::Path::new("payload.dll"),
5831 payload_image: &[0x4d],
5832 };
5833
5834 match injector.inject(&VadRejectBackend, &req) {
5835 Err(GuestInjectError::Unsupported { operation, reason }) => {
5836 assert_eq!(operation, "VAD type spoofing");
5837 assert!(reason.contains("VAD mutation support"), "{reason}");
5838 }
5839 Ok(_) => panic!("expected VAD spoof rejection"),
5840 Err(err) => panic!("expected VAD spoof unsupported error, got {err:?}"),
5841 }
5842 }
5843
5844 #[test]
5845 fn thread_hijack_is_an_execution_policy() {
5846 let config = DecantConfig::from_toml_str(
5847 "[injection]\ndomain = \"guest\"\nmethod = \"thread-hijack\"\n\
5848 [guest]\npid = 1\npayload_path = \"payload.dll\"\n",
5849 )
5850 .unwrap();
5851 let err = GuestInjectionPlan::from_config(&config).unwrap_err();
5852 assert!(matches!(err, GuestInjectError::Config(_)));
5853 }
5854
5855 #[test]
5856 fn import_candidates_cover_forwarded_and_api_set_imports() {
5857 assert_eq!(
5858 import_module_candidates("KERNEL32.dll"),
5859 vec!["KERNEL32.dll", "kernelbase.dll"]
5860 );
5861 assert_eq!(
5862 import_module_candidates("api-ms-win-core-synch-l1-2-0.dll"),
5863 vec![
5864 "api-ms-win-core-synch-l1-2-0.dll",
5865 "kernelbase.dll",
5866 "kernel32.dll",
5867 "ntdll.dll"
5868 ]
5869 );
5870 assert_eq!(
5871 import_module_candidates("api-ms-win-crt-runtime-l1-1-0.dll"),
5872 vec!["api-ms-win-crt-runtime-l1-1-0.dll", "ucrtbase.dll"]
5873 );
5874 assert_eq!(normalize_forwarder_module("KERNELBASE"), "KERNELBASE.dll");
5875 }
5876
5877 #[test]
5878 fn iat_hook_stub_uses_windows_x64_shadow_space() {
5879 let hook = GuestIatHook {
5880 iat_slot: 0x1000,
5881 original_target: 0x2000,
5882 stub_addr: 0x3000,
5883 result_addr: 0x4000,
5884 call_stack: GuestCallStackPolicy::Native,
5885 spoofed_return: None,
5886 };
5887 let stub = call_stub(&hook, 0x5000, [1, 2, 3, 4]);
5888 assert!(
5889 windows_x64_call_sequence_offset(&stub).is_some(),
5890 "stub must reserve 32 bytes of Windows x64 shadow space and keep stack alignment"
5891 );
5892 assert!(
5893 stub.windows(5).any(|w| w == [0xF0, 0x4D, 0x0F, 0xB1, 0x1A]),
5894 "stub must atomically claim the result block so only one thread runs the injection call"
5895 );
5896 assert!(
5897 !stub.windows(8).any(|w| w == hook.iat_slot.to_le_bytes()),
5898 "stub must not write the guest IAT; the host transaction restores it"
5899 );
5900 assert!(
5901 stub.starts_with(&[0x51, 0x52, 0x41, 0x50, 0x41, 0x51]),
5902 "stub must save the import caller's register arguments before the injected call"
5903 );
5904 assert!(
5905 !stub
5906 .windows(10)
5907 .any(|w| w == [0x48, 0x83, 0xEC, 0x20, 0xFF, 0xD0, 0x48, 0x83, 0xC4, 0x20]),
5908 "0x20 would misalign the nested Windows x64 call from an IAT tail jump"
5909 );
5910 assert!(
5911 stub.windows(8)
5912 .any(|w| w == hook.original_target.to_le_bytes()),
5913 "stub must tail-jump to the original imported function after publishing the result"
5914 );
5915 assert_eq!(
5916 &stub[stub.len() - 2..],
5917 &[0xFF, 0xE0],
5918 "stub should tail-jump through RAX to the original import target"
5919 );
5920 }
5921
5922 #[test]
5923 fn iat_hook_spoofed_stub_keeps_landing_after_shadow_space() {
5924 let hook = GuestIatHook {
5925 iat_slot: 0x1000,
5926 original_target: 0x2000,
5927 stub_addr: 0x3000,
5928 result_addr: 0x4000,
5929 call_stack: GuestCallStackPolicy::Native,
5930 spoofed_return: Some(GuestSpoofedReturn {
5931 gadget_addr: 0x7000,
5932 stack_adjust: 0x20,
5933 }),
5934 };
5935 let stub = call_stub(&hook, 0x5000, [1, 2, 3, 4]);
5936 assert!(
5937 stub.windows(4).any(|w| w == [0x48, 0x83, 0xEC, 0x30]),
5938 "native spoofed call must reserve return, shadow space, and continuation"
5939 );
5940 assert!(
5941 stub.windows(5).any(|w| w == [0x4C, 0x89, 0x5C, 0x24, 0x00]),
5942 "spoofed gadget must be placed as the callee return address"
5943 );
5944 assert!(
5945 stub.windows(5).any(|w| w == [0x48, 0x89, 0x44, 0x24, 0x28]),
5946 "real continuation must be stored after the 32-byte Windows x64 shadow space"
5947 );
5948 assert!(
5949 !stub.windows(3).any(|w| w == [0x50, 0x41, 0x53]),
5950 "spoofed call must not put the continuation in the callee shadow space with pushes"
5951 );
5952 }
5953
5954 #[test]
5955 fn spoofed_stack_frame_restores_native_and_framed_callers() {
5956 assert_eq!(spoofed_stack_frame(0x20, 0), (0x30, 0));
5957 assert_eq!(spoofed_stack_frame(0x28, 0), (0x40, 0x08));
5958 assert_eq!(spoofed_stack_frame(0x20, 8), (0x38, 0x08));
5959 assert_eq!(spoofed_stack_frame(0x28, 8), (0x38, 0));
5960 }
5961
5962 #[test]
5963 fn iat_hook_touch_stub_materializes_pages_without_iat_write() {
5964 let hook = GuestIatHook {
5965 iat_slot: 0x1000,
5966 original_target: 0x2000,
5967 stub_addr: 0x3000,
5968 result_addr: 0x4000,
5969 call_stack: GuestCallStackPolicy::Native,
5970 spoofed_return: None,
5971 };
5972 let stub = touch_stub(&hook, 0x5000, GUEST_PAGE_SIZE + 1);
5973 assert!(
5974 stub.windows(3).any(|w| w == [0xC6, 0x02, 0x00]),
5975 "touch stub must fault in pages by writing through RDX"
5976 );
5977 assert!(
5978 stub.windows(7)
5979 .any(|w| w == [0x48, 0x81, 0xC2, 0x00, 0x10, 0x00, 0x00]),
5980 "touch stub must advance by guest page size"
5981 );
5982 assert!(
5983 !stub.windows(8).any(|w| w == hook.iat_slot.to_le_bytes()),
5984 "touch stub must not write the guest IAT; the host transaction restores it"
5985 );
5986 assert!(
5987 stub.windows(8)
5988 .any(|w| w == hook.original_target.to_le_bytes()),
5989 "touch stub must tail-jump to the original imported function"
5990 );
5991 assert_eq!(
5992 &stub[stub.len() - 2..],
5993 &[0xFF, 0xE0],
5994 "touch stub should tail-jump through RAX to the original import target"
5995 );
5996 }
5997
5998 #[test]
5999 fn iat_hook_read_touch_stub_materializes_without_writes() {
6000 let hook = GuestIatHook {
6001 iat_slot: 0x1000,
6002 original_target: 0x2000,
6003 stub_addr: 0x3000,
6004 result_addr: 0x4000,
6005 call_stack: GuestCallStackPolicy::Native,
6006 spoofed_return: None,
6007 };
6008 let stub = read_touch_stub(&hook, 0x5000, GUEST_PAGE_SIZE + 1);
6009 assert!(
6010 stub.windows(3).any(|w| w == [0x0F, 0xB6, 0x02]),
6011 "read-touch stub must fault in pages by reading through RDX"
6012 );
6013 assert!(
6014 !stub.windows(3).any(|w| w == [0xC6, 0x02, 0x00]),
6015 "read-touch stub must not write to materialized pages"
6016 );
6017 assert!(
6018 !stub.windows(8).any(|w| w == hook.iat_slot.to_le_bytes()),
6019 "read-touch stub must not write the guest IAT; the host transaction restores it"
6020 );
6021 }
6022
6023 #[test]
6024 fn iat_hook_preserve_touch_stub_writes_same_byte() {
6025 let hook = GuestIatHook {
6026 iat_slot: 0x1000,
6027 original_target: 0x2000,
6028 stub_addr: 0x3000,
6029 result_addr: 0x4000,
6030 call_stack: GuestCallStackPolicy::Native,
6031 spoofed_return: None,
6032 };
6033 let stub = preserve_touch_stub(&hook, 0x5000, GUEST_PAGE_SIZE + 1);
6034 assert!(
6035 stub.windows(5).any(|w| w == [0x0F, 0xB6, 0x02, 0x88, 0x02]),
6036 "preserve-touch stub must fault in pages by writing the original byte back"
6037 );
6038 assert!(
6039 !stub.windows(3).any(|w| w == [0xC6, 0x02, 0x00]),
6040 "preserve-touch stub must not zero image-backed page contents"
6041 );
6042 assert!(
6043 !stub.windows(8).any(|w| w == hook.iat_slot.to_le_bytes()),
6044 "preserve-touch stub must not write the guest IAT; the host transaction restores it"
6045 );
6046 }
6047
6048 #[test]
6049 fn sec_image_patch_ranges_batch_contiguous_changed_pages() {
6050 let mut image = vec![0u8; GUEST_PAGE_SIZE * 5];
6051 let snapshot = image.clone();
6052 image[17] = 1;
6053 image[GUEST_PAGE_SIZE + 2] = 2;
6054 image[(GUEST_PAGE_SIZE * 3) + 4] = 3;
6055
6056 assert_eq!(
6057 sec_image_patched_ranges(&image, &snapshot),
6058 vec![
6059 (0, GUEST_PAGE_SIZE * 2),
6060 (GUEST_PAGE_SIZE * 3, GUEST_PAGE_SIZE * 4),
6061 ]
6062 );
6063 }
6064
6065 #[test]
6066 fn spoofed_return_scan_uses_executable_module_regions() {
6067 let module = GuestModuleInfo {
6068 name: "ntdll.dll".into(),
6069 base: 0x1000,
6070 size: 0x8000,
6071 };
6072 let regions = vec![
6073 GuestMemoryRegion {
6074 base: 0x1000,
6075 size: 0x1000,
6076 readable: true,
6077 writable: false,
6078 executable: false,
6079 },
6080 GuestMemoryRegion {
6081 base: 0x2000,
6082 size: 0x2000,
6083 readable: true,
6084 writable: false,
6085 executable: true,
6086 },
6087 GuestMemoryRegion {
6088 base: 0x9000,
6089 size: 0x1000,
6090 readable: true,
6091 writable: false,
6092 executable: true,
6093 },
6094 ];
6095
6096 assert_eq!(
6097 spoofed_return_scan_ranges(&module, ®ions),
6098 vec![(0x2000, 0x4000)]
6099 );
6100 }
6101
6102 #[test]
6103 fn spoofed_return_scan_falls_back_to_bounded_module_range() {
6104 let module = GuestModuleInfo {
6105 name: "kernel32.dll".into(),
6106 base: 0x7fff_0000,
6107 size: SPOOFED_RETURN_SCAN_LIMIT + 0x1000,
6108 };
6109
6110 assert_eq!(
6111 spoofed_return_scan_ranges(&module, &[]),
6112 vec![(0x7fff_0000, 0x7fff_0000 + SPOOFED_RETURN_SCAN_LIMIT)]
6113 );
6114 }
6115
6116 #[test]
6117 fn spoofed_return_gadget_scan_requires_shadow_space_adjustment() {
6118 assert_eq!(
6119 find_spoofed_return_gadget(&[0x90, 0x48, 0x83, 0xC4, 0x20, 0xC3]),
6120 Some((1, 0x20))
6121 );
6122 assert_eq!(find_spoofed_return_gadget(&[0x90, 0xC3]), None);
6123 }
6124
6125 #[test]
6126 fn registered_unwind_stub_uses_single_frame_allocation() {
6127 let hook = GuestIatHook {
6128 iat_slot: 0x1000,
6129 original_target: 0x2000,
6130 stub_addr: 0x3000,
6131 result_addr: 0x4000,
6132 call_stack: GuestCallStackPolicy::RegisteredUnwind,
6133 spoofed_return: None,
6134 };
6135 let stub = call_stub(&hook, 0x5000, [1, 2, 3, 4]);
6136 assert!(
6137 stub.starts_with(&[0x48, 0x83, 0xEC, FRAMED_STUB_STACK_ALLOC]),
6138 "registered-unwind stub must use one unwindable stack allocation"
6139 );
6140 assert!(
6141 !stub.starts_with(&[0x51, 0x52, 0x41, 0x50, 0x41, 0x51]),
6142 "registered-unwind stub must not use volatile pushes that cannot be represented in x64 unwind metadata"
6143 );
6144 assert!(
6145 !stub
6146 .windows(10)
6147 .any(|w| w == [0x48, 0x83, 0xEC, 0x28, 0xFF, 0xD0, 0x48, 0x83, 0xC4, 0x28]),
6148 "registered-unwind stub keeps its shadow space inside the fixed frame"
6149 );
6150 assert_eq!(
6151 &stub[stub.len() - 2..],
6152 &[0xFF, 0xE0],
6153 "registered-unwind stub should tail-jump through RAX to the original import target"
6154 );
6155 }
6156
6157 #[test]
6158 fn stub_unwind_metadata_describes_registered_frame() {
6159 let metadata = stub_unwind_metadata().unwrap();
6160 assert_eq!(&metadata[0..4], &(STAGE_STUB_OFFSET as u32).to_le_bytes());
6161 assert_eq!(
6162 &metadata[4..8],
6163 &(STAGE_SCRATCH_OFFSET as u32).to_le_bytes()
6164 );
6165 assert_eq!(
6166 &metadata[8..12],
6167 &((STAGE_UNWIND_OFFSET + 12) as u32).to_le_bytes()
6168 );
6169 assert_eq!(metadata[12], 1);
6170 assert_eq!(metadata[13], 4);
6171 assert_eq!(metadata[14], 1);
6172 assert_eq!(metadata[17], ((FRAMED_STUB_STACK_ALLOC / 8 - 1) << 4) | 2);
6173 }
6174
6175 #[test]
6176 fn guest_section_protection_matches_pe_section_flags() {
6177 assert_eq!(
6178 guest_section_protect(IMAGE_SCN_MEM_EXECUTE | IMAGE_SCN_MEM_READ),
6179 PAGE_EXECUTE_READ
6180 );
6181 assert_eq!(
6182 guest_section_protect(IMAGE_SCN_MEM_EXECUTE | IMAGE_SCN_MEM_WRITE),
6183 PAGE_EXECUTE_READWRITE
6184 );
6185 assert_eq!(
6186 guest_section_protect(IMAGE_SCN_MEM_READ | IMAGE_SCN_MEM_WRITE),
6187 PAGE_READWRITE
6188 );
6189 assert_eq!(guest_section_protect(IMAGE_SCN_MEM_READ), PAGE_READONLY);
6190 assert_eq!(guest_section_protect(0), PAGE_NOACCESS);
6191 }
6192
6193 #[test]
6194 fn write_through_final_selects_non_writable_initial_protection() {
6195 let pe = Pe {
6196 image_base: 0,
6197 entry_rva: 0,
6198 size_of_image: 0,
6199 size_of_headers: 0,
6200 import: crate::pe::Dir { rva: 0, size: 0 },
6201 exception: crate::pe::Dir { rva: 0, size: 0 },
6202 reloc: crate::pe::Dir { rva: 0, size: 0 },
6203 tls: crate::pe::Dir { rva: 0, size: 0 },
6204 load_config: crate::pe::Dir { rva: 0, size: 0 },
6205 delay_import: crate::pe::Dir { rva: 0, size: 0 },
6206 export_dir: crate::pe::Dir { rva: 0, size: 0 },
6207 sections: vec![
6208 Section {
6209 virtual_size: 0x1000,
6210 virtual_address: 0x1000,
6211 raw_size: 0x1000,
6212 raw_ptr: 0,
6213 characteristics: IMAGE_SCN_MEM_EXECUTE | IMAGE_SCN_MEM_READ,
6214 },
6215 Section {
6216 virtual_size: 0x1000,
6217 virtual_address: 0x2000,
6218 raw_size: 0x1000,
6219 raw_ptr: 0,
6220 characteristics: IMAGE_SCN_MEM_READ | IMAGE_SCN_MEM_WRITE,
6221 },
6222 ],
6223 };
6224 assert_eq!(final_image_allocation_protection(&pe), PAGE_EXECUTE_READ);
6225 }
6226
6227 #[test]
6228 fn module_backed_range_requires_complete_containment() {
6229 let module = GuestModuleInfo {
6230 name: "target.exe".into(),
6231 base: 0x1400_0000,
6232 size: 0x2000,
6233 };
6234 assert!(module_contains_range(&module, 0x1400_0100, 0x100));
6235 assert!(module_contains_range(&module, 0x1400_1FF0, 0x10));
6236 assert!(!module_contains_range(&module, 0x1400_1FF0, 0x11));
6237 assert!(!module_contains_range(&module, 0x13FF_FFF0, 0x20));
6238 }
6239
6240 #[test]
6241 fn code_cave_finder_uses_generic_padding() {
6242 let mut bytes = vec![0x41; 128];
6243 bytes[23..23 + 64].fill(0xCC);
6244 assert_eq!(find_code_cave(&bytes, 32), Some(32));
6245
6246 bytes[23..23 + 64].fill(0x90);
6247 assert_eq!(find_code_cave(&bytes, 32), Some(32));
6248
6249 bytes[23..23 + 64].fill(0x00);
6250 assert_eq!(find_code_cave(&bytes, 32), Some(32));
6251 }
6252
6253 #[test]
6254 fn guest_pattern_supports_wildcards() {
6255 let pattern = parse_hex_pattern("44 45 ?? 41", "guest.process_pattern").unwrap();
6256 assert_eq!(pattern.find_in(b"DECANT"), Some(0));
6257 assert_eq!(pattern.find_in(b"DEZANT"), Some(0));
6258 assert_eq!(pattern.find_in(b"DE"), None);
6259 }
6260
6261 #[test]
6262 fn guest_mapper_policies_parse() {
6263 let config = DecantConfig::from_toml_str(
6264 "[injection]\ndomain = \"guest\"\nmethod = \"manual-map\"\n\
6265 [guest]\npid = 1\npayload_path = \"payload.dll\"\n\
6266 dependency_policy = \"require-loaded\"\ntls = \"skip\"\nfinal_protections = \"rwx\"\nloader_metadata = \"reject-unsupported\"\ncall_stack = \"registered-unwind\"\npermission_transitions = \"write-through-final\"\nthread_starts = \"require-module-backed\"\nvad_spoof = \"vad-image-map\"\nresult_base = 8192\nresult_pattern = \"44 45\"\n",
6267 )
6268 .unwrap();
6269 let plan = GuestInjectionPlan::from_config(&config).unwrap();
6270 assert_eq!(plan.dependency_policy, GuestDependencyPolicy::RequireLoaded);
6271 assert_eq!(plan.tls, GuestTlsMode::Skip);
6272 assert_eq!(plan.final_protections, GuestFinalProtections::Rwx);
6273 assert_eq!(
6274 plan.loader_metadata,
6275 GuestLoaderMetadataPolicy::RejectUnsupported
6276 );
6277 assert_eq!(plan.call_stack, GuestCallStackPolicy::RegisteredUnwind);
6278 assert_eq!(
6279 plan.permission_transitions,
6280 GuestPermissionTransitions::WriteThroughFinal
6281 );
6282 assert_eq!(
6283 plan.thread_starts,
6284 GuestThreadStartPolicy::RequireModuleBacked
6285 );
6286 assert_eq!(plan.vad_spoof, GuestVadSpoof::VadImageMap);
6287 assert_eq!(plan.result_base, Some(8192));
6288 assert!(plan.result_pattern.is_some());
6289 }
6290
6291 #[test]
6292 fn guest_final_protections_default_to_section() {
6293 let config = DecantConfig::from_toml_str(
6294 "[injection]\ndomain = \"guest\"\nmethod = \"manual-map\"\n\
6295 [guest]\npid = 1\npayload_path = \"payload.dll\"\n",
6296 )
6297 .unwrap();
6298 let plan = GuestInjectionPlan::from_config(&config).unwrap();
6299 assert_eq!(plan.final_protections, GuestFinalProtections::Section);
6300 }
6301
6302 #[test]
6303 fn guest_loader_metadata_best_effort_parses() {
6304 let config = DecantConfig::from_toml_str(
6305 "[injection]\ndomain = \"guest\"\nmethod = \"manual-map\"\n\
6306 [guest]\npid = 1\npayload_path = \"payload.dll\"\n\
6307 loader_metadata = \"best-effort\"\n",
6308 )
6309 .unwrap();
6310 let plan = GuestInjectionPlan::from_config(&config).unwrap();
6311 assert_eq!(plan.loader_metadata, GuestLoaderMetadataPolicy::BestEffort);
6312 assert!(plan.loader_metadata.allows_unregistered_metadata());
6313 assert!(plan.loader_metadata.registers_public_metadata());
6314 }
6315
6316 #[test]
6317 fn guest_package_selectors_are_parsed_but_not_implemented() {
6318 let config = DecantConfig::from_toml_str(
6319 "[injection]\ndomain = \"guest\"\nmethod = \"manual-map\"\n\
6320 [guest]\nprocess = \"target.exe\"\npackage_family_name = \"pkg\"\npayload_path = \"payload.dll\"\n",
6321 )
6322 .unwrap();
6323 let err = GuestInjectionPlan::from_config(&config).unwrap_err();
6324 assert!(matches!(err, GuestInjectError::Config(_)));
6325 }
6326
6327 #[test]
6328 fn guest_image_backing_defaults_to_private() {
6329 let config = DecantConfig::from_toml_str(
6330 "[injection]\ndomain = \"guest\"\nmethod = \"manual-map\"\n\
6331 [guest]\npid = 1\npayload_path = \"payload.dll\"\n",
6332 )
6333 .unwrap();
6334 let plan = GuestInjectionPlan::from_config(&config).unwrap();
6335 assert_eq!(plan.image_backing, GuestImageBacking::Private);
6336 }
6337
6338 #[test]
6339 fn guest_sec_image_parses_and_requires_section_protections() {
6340 let ok = DecantConfig::from_toml_str(
6341 "[injection]\ndomain = \"guest\"\nmethod = \"manual-map\"\n\
6342 [guest]\npid = 1\npayload_path = \"payload.dll\"\n\
6343 image_backing = \"sec-image\"\nfinal_protections = \"section\"\n",
6344 )
6345 .unwrap();
6346 let plan = GuestInjectionPlan::from_config(&ok).unwrap();
6347 assert_eq!(plan.image_backing, GuestImageBacking::SecImage);
6348
6349 let err = DecantConfig::from_toml_str(
6350 "[injection]\ndomain = \"guest\"\nmethod = \"manual-map\"\n\
6351 [guest]\npid = 1\npayload_path = \"payload.dll\"\n\
6352 image_backing = \"sec-image\"\nfinal_protections = \"rwx\"\n",
6353 )
6354 .unwrap();
6355 match GuestInjectionPlan::from_config(&err).unwrap_err() {
6356 GuestInjectError::Config(msg) => assert!(
6357 msg.contains("sec-image") && msg.contains("section"),
6358 "unexpected config error: {msg}"
6359 ),
6360 other => panic!("expected config error, got {other:?}"),
6361 }
6362
6363 let err = DecantConfig::from_toml_str(
6364 "[injection]\ndomain = \"guest\"\nmethod = \"manual-map\"\n\
6365 [guest]\npid = 1\npayload_path = \"payload.dll\"\n\
6366 image_backing = \"sec-image\"\nfinal_protections = \"section\"\nallocation = \"existing-region\"\n",
6367 )
6368 .unwrap();
6369 match GuestInjectionPlan::from_config(&err).unwrap_err() {
6370 GuestInjectError::Config(msg) => assert!(
6371 msg.contains("sec-image") && msg.contains("virtual-alloc"),
6372 "unexpected config error: {msg}"
6373 ),
6374 other => panic!("expected config error, got {other:?}"),
6375 }
6376
6377 let err = DecantConfig::from_toml_str(
6378 "[injection]\ndomain = \"guest\"\nmethod = \"manual-map\"\n\
6379 [guest]\npid = 1\npayload_path = \"payload.dll\"\n\
6380 image_backing = \"sec-image\"\nfinal_protections = \"section\"\nvad_spoof = \"vad-image-map\"\n",
6381 )
6382 .unwrap();
6383 match GuestInjectionPlan::from_config(&err).unwrap_err() {
6384 GuestInjectError::Config(msg) => assert!(
6385 msg.contains("vad-image-map") && msg.contains("private"),
6386 "unexpected config error: {msg}"
6387 ),
6388 other => panic!("expected config error, got {other:?}"),
6389 }
6390 }
6391
6392 #[test]
6393 fn guest_proc_trampoline_loads_args_from_param_block() {
6394 assert_eq!(GUEST_PROC_TRAMPOLINE.len(), 77);
6395 assert_eq!(GUEST_PROC_TRAMPOLINE[0], 0x55);
6396 assert_eq!(*GUEST_PROC_TRAMPOLINE.last().unwrap(), 0xC3);
6397 assert_eq!(
6398 GUEST_GET_PEB_TRAMPOLINE,
6399 &[0x65, 0x48, 0x8B, 0x04, 0x25, 0x60, 0, 0, 0, 0xC3]
6400 );
6401 assert_eq!(GUEST_PARAM_BLOCK_SIZE, 0x58);
6402 assert!(STAGE_PARAM_OFFSET + GUEST_PARAM_BLOCK_SIZE as u64 <= STAGE_UNWIND_OFFSET);
6403 }
6404
6405 #[test]
6406 fn remote_thread_thunk_calls_dllmain_with_three_args() {
6407 assert_eq!(&REMOTE_THREAD_DLLMAIN_THUNK[..4], &[0x53, 0x48, 0x83, 0xEC]);
6408 assert!(
6409 REMOTE_THREAD_DLLMAIN_THUNK
6410 .windows(2)
6411 .any(|w| w == [0xFF, 0xD0])
6412 );
6413 assert!(
6414 REMOTE_THREAD_DLLMAIN_THUNK
6415 .windows(3)
6416 .any(|w| w == [0x41, 0x89, 0x02])
6417 );
6418 assert!(
6419 REMOTE_THREAD_DLLMAIN_THUNK
6420 .windows(7)
6421 .any(|w| w == [0x41, 0xC7, 0x02, 0x01, 0, 0, 0])
6422 );
6423 assert_eq!(*REMOTE_THREAD_DLLMAIN_THUNK.last().unwrap(), 0xC3);
6424 assert!(REMOTE_THREAD_DLLMAIN_THUNK.len() < REMOTE_THREAD_PARAM_OFFSET as usize);
6425 }
6426
6427 #[test]
6428 fn cfg_call_target_registration_requires_best_effort_load_config() {
6429 let config = DecantConfig::from_toml_str(
6430 "[injection]\ndomain = \"guest\"\nmethod = \"manual-map\"\n\
6431 [guest]\npid = 1\npayload_path = \"payload.dll\"\n\
6432 loader_entries = \"synthesized\"\nloader_metadata = \"best-effort\"\n",
6433 )
6434 .unwrap();
6435 let plan = GuestInjectionPlan::from_config(&config).unwrap();
6436 let mut pe = Pe {
6437 image_base: 0,
6438 entry_rva: 0,
6439 size_of_image: 0,
6440 size_of_headers: 0,
6441 import: crate::pe::Dir { rva: 0, size: 0 },
6442 exception: crate::pe::Dir { rva: 0, size: 0 },
6443 reloc: crate::pe::Dir { rva: 0, size: 0 },
6444 tls: crate::pe::Dir { rva: 0, size: 0 },
6445 load_config: crate::pe::Dir { rva: 0, size: 0 },
6446 delay_import: crate::pe::Dir { rva: 0, size: 0 },
6447 export_dir: crate::pe::Dir { rva: 0, size: 0 },
6448 sections: Vec::new(),
6449 };
6450
6451 assert!(!should_request_cfg_call_target(&plan, &pe));
6452 pe.load_config = crate::pe::Dir {
6453 rva: 0x40,
6454 size: 0x60,
6455 };
6456 assert!(should_request_cfg_call_target(&plan, &pe));
6457
6458 let reject_config = DecantConfig::from_toml_str(
6459 "[injection]\ndomain = \"guest\"\nmethod = \"manual-map\"\n\
6460 [guest]\npid = 1\npayload_path = \"payload.dll\"\n\
6461 loader_entries = \"synthesized\"\nloader_metadata = \"reject-unsupported\"\n",
6462 )
6463 .unwrap();
6464 let reject_plan = GuestInjectionPlan::from_config(&reject_config).unwrap();
6465 assert!(!should_request_cfg_call_target(&reject_plan, &pe));
6466 }
6467
6468 #[test]
6469 fn wide_roundtrip_preserves_ascii_paths() {
6470 let path = "C:\\Users\\lobby\\Temp\\decant_payload.dll";
6471 assert_eq!(decode_wide(&encode_wide(path)), path);
6472 let bytes = encode_wide(path);
6473 assert_eq!(bytes.len(), (path.len() + 1) * 2);
6474 assert_eq!(bytes[bytes.len() - 2], 0);
6475 assert_eq!(bytes[bytes.len() - 1], 0);
6476 }
6477
6478 #[test]
6479 fn code_cave_finder_rejects_short_padding() {
6480 let mut bytes = vec![0x41; 128];
6481 bytes[16..31].fill(0xCC);
6482 assert_eq!(find_code_cave(&bytes, 16), None);
6483 }
6484}