1#![allow(clippy::manual_c_str_literals, clippy::missing_safety_doc)]
4
5use std::ffi::c_void;
6use std::fmt;
7use std::path::Path;
8
9pub mod guest;
10mod pe;
11
12#[derive(Clone, Copy, PartialEq, Eq, Debug)]
13pub enum Portability {
14 PublicExportsOnly,
15 LoaderInternals,
16 PrologueBytes,
17}
18
19impl Portability {
20 pub fn label(self) -> &'static str {
21 match self {
22 Portability::PublicExportsOnly => "public-exports-only",
23 Portability::LoaderInternals => "loader-internals",
24 Portability::PrologueBytes => "prologue-bytes",
25 }
26 }
27
28 pub fn upholds_export_guarantee(self) -> bool {
29 matches!(self, Portability::PublicExportsOnly)
30 }
31}
32
33#[derive(Clone, Copy)]
34pub struct ProcessHandle(pub *mut c_void);
35
36#[derive(Clone, Copy)]
37pub struct ThreadHandle(pub *mut c_void);
38
39#[derive(Clone)]
43pub struct ReadyToken {
44 pub name: String,
45}
46
47impl ReadyToken {
48 pub fn new(name: impl Into<String>) -> Self {
49 Self { name: name.into() }
50 }
51
52 pub fn none() -> Self {
53 Self {
54 name: String::new(),
55 }
56 }
57}
58
59pub struct InjectionRequest<'a> {
64 pub target: ProcessHandle,
65 pub main_thread: ThreadHandle,
66 pub target_pid: u32,
67 pub carafe_path: &'a Path,
68 pub carafe_image: &'a [u8],
69 pub ready: ReadyToken,
70}
71
72pub struct LoadInfo {
73 pub method: String,
74 pub remote_base: Option<usize>,
75 pub notes: Vec<String>,
76}
77
78#[derive(Debug)]
79pub enum InjectError {
80 RemoteAlloc(u32),
81 RemoteRead(u32),
82 RemoteWrite(u32),
83 RemoteProtect(u32),
84 ResolveLoadLibrary,
85 RemoteThread(u32),
86 Timeout,
87 Unsupported(String),
88 ManualMap(String),
89 ThreadHijack(String),
90 Plugin(String),
91 External(String),
92 Config(String),
93}
94
95impl fmt::Display for InjectError {
96 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
97 match self {
98 InjectError::RemoteAlloc(e) => write!(f, "remote allocation failed (err={e})"),
99 InjectError::RemoteRead(e) => write!(f, "remote read failed (err={e})"),
100 InjectError::RemoteWrite(e) => write!(f, "remote write failed (err={e})"),
101 InjectError::RemoteProtect(e) => write!(f, "remote protection change failed (err={e})"),
102 InjectError::ResolveLoadLibrary => write!(f, "could not resolve kernel32!LoadLibraryA"),
103 InjectError::RemoteThread(e) => write!(f, "remote thread creation failed (err={e})"),
104 InjectError::Timeout => write!(f, "carafe did not signal ready before the timeout"),
105 InjectError::Unsupported(m) => write!(f, "unsupported: {m}"),
106 InjectError::ManualMap(m) => write!(f, "manual-map: {m}"),
107 InjectError::ThreadHijack(m) => write!(f, "thread-hijack: {m}"),
108 InjectError::Plugin(m) => write!(f, "plugin: {m}"),
109 InjectError::External(m) => write!(f, "external: {m}"),
110 InjectError::Config(m) => write!(f, "config: {m}"),
111 }
112 }
113}
114
115impl std::error::Error for InjectError {}
116
117pub trait Injector {
118 fn name(&self) -> &str;
119 fn portability(&self) -> Portability;
120 fn inject(&self, req: &InjectionRequest) -> Result<LoadInfo, InjectError>;
121}
122
123pub mod external {
128 use std::io::{self, Cursor, Read, Write};
129
130 pub struct ExternalPayload {
131 pub pid: u32,
132 pub carafe_path: String,
133 pub carafe_image: Vec<u8>,
134 pub ready_token: String,
135 }
136
137 fn put(buf: &mut Vec<u8>, b: &[u8]) {
138 buf.extend_from_slice(&(b.len() as u32).to_le_bytes());
139 buf.extend_from_slice(b);
140 }
141
142 pub fn write_request(
143 w: &mut impl Write,
144 pid: u32,
145 carafe_path: &str,
146 carafe_image: &[u8],
147 ready_token: &str,
148 ) -> io::Result<()> {
149 let mut buf = pid.to_le_bytes().to_vec();
150 put(&mut buf, carafe_path.as_bytes());
151 put(&mut buf, carafe_image);
152 put(&mut buf, ready_token.as_bytes());
153 w.write_all(&buf)
154 }
155
156 fn read_u32(r: &mut impl Read) -> io::Result<u32> {
157 let mut b = [0u8; 4];
158 r.read_exact(&mut b)?;
159 Ok(u32::from_le_bytes(b))
160 }
161
162 fn read_vec(r: &mut impl Read) -> io::Result<Vec<u8>> {
163 let n = read_u32(r)? as usize;
164 let mut v = vec![0u8; n];
165 r.read_exact(&mut v)?;
166 Ok(v)
167 }
168
169 pub fn read_request(r: &mut impl Read) -> io::Result<ExternalPayload> {
170 let mut all = Vec::new();
171 r.read_to_end(&mut all)?;
172 let mut c = Cursor::new(all);
173 let pid = read_u32(&mut c)?;
174 let carafe_path = String::from_utf8_lossy(&read_vec(&mut c)?).into_owned();
175 let carafe_image = read_vec(&mut c)?;
176 let ready_token = String::from_utf8_lossy(&read_vec(&mut c)?).into_owned();
177 Ok(ExternalPayload {
178 pid,
179 carafe_path,
180 carafe_image,
181 ready_token,
182 })
183 }
184}
185
186#[derive(Clone, Copy, PartialEq, Eq, Debug, Default, serde::Deserialize)]
187#[serde(rename_all = "kebab-case")]
188pub enum Method {
189 #[default]
190 Standard,
191 ManualMap,
192 ThreadHijack,
193 Plugin,
194 External,
195}
196
197impl Method {
198 pub fn label(self) -> &'static str {
199 match self {
200 Method::Standard => "standard",
201 Method::ManualMap => "manual-map",
202 Method::ThreadHijack => "thread-hijack",
203 Method::Plugin => "plugin",
204 Method::External => "external",
205 }
206 }
207}
208
209#[derive(Clone, Copy, PartialEq, Eq, Debug, Default, serde::Deserialize)]
210#[serde(rename_all = "kebab-case")]
211pub enum InjectionDomain {
212 #[default]
213 Tool,
214 Guest,
215}
216
217fn default_timeout_ms() -> u32 {
218 5000
219}
220
221#[derive(Clone, Debug, serde::Deserialize)]
222#[serde(deny_unknown_fields)]
223pub struct InjectionConfig {
224 #[serde(default)]
225 pub domain: InjectionDomain,
226 #[serde(default)]
227 pub method: Method,
228 #[serde(default = "default_timeout_ms")]
229 pub timeout_ms: u32,
230 #[serde(default)]
231 pub plugin_path: Option<std::path::PathBuf>,
232 #[serde(default)]
233 pub external_command: Option<Vec<String>>,
234}
235
236impl Default for InjectionConfig {
237 fn default() -> Self {
238 Self {
239 domain: InjectionDomain::Tool,
240 method: Method::Standard,
241 timeout_ms: default_timeout_ms(),
242 plugin_path: None,
243 external_command: None,
244 }
245 }
246}
247
248#[derive(Clone, Debug, Default, serde::Deserialize)]
249pub struct DecantConfig {
250 #[serde(default)]
251 pub injection: InjectionConfig,
252 #[serde(default)]
253 pub guest: guest::GuestInjectionConfig,
254}
255
256impl DecantConfig {
257 pub fn from_toml_str(s: &str) -> Result<Self, InjectError> {
258 toml::from_str::<DecantConfig>(s).map_err(|e| InjectError::Config(e.message().to_string()))
259 }
260
261 pub fn load(path: &Path) -> Result<Self, InjectError> {
262 match std::fs::read_to_string(path) {
263 Ok(s) => Self::from_toml_str(&s),
264 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Self::default()),
265 Err(e) => Err(InjectError::Config(e.to_string())),
266 }
267 }
268}
269
270impl InjectionConfig {
271 pub fn from_toml_str(s: &str) -> Result<Self, InjectError> {
272 DecantConfig::from_toml_str(s).map(|c| c.injection)
273 }
274
275 pub fn load(path: &Path) -> Result<Self, InjectError> {
277 DecantConfig::load(path).map(|c| c.injection)
278 }
279}
280
281#[cfg(windows)]
282pub mod sdk;
283
284#[cfg(windows)]
285mod manual_map;
286
287#[cfg(windows)]
288mod thread_hijack;
289
290#[cfg(windows)]
291pub use manual_map::ManualMapInjector;
292#[cfg(windows)]
293pub use thread_hijack::ThreadHijackInjector;
294
295pub fn thread_hijack_release_event_name(ready_name: &str) -> String {
296 format!("{ready_name}_release")
297}
298
299#[cfg(windows)]
302pub struct StandardInjector;
303
304#[cfg(windows)]
305impl Injector for StandardInjector {
306 fn name(&self) -> &str {
307 "standard"
308 }
309
310 fn portability(&self) -> Portability {
311 Portability::PublicExportsOnly
312 }
313
314 fn inject(&self, req: &InjectionRequest) -> Result<LoadInfo, InjectError> {
315 let mut path_bytes = req.carafe_path.to_string_lossy().into_owned().into_bytes();
316 path_bytes.push(0);
317
318 unsafe {
319 let proc = req.target.0;
320 let remote = sdk::alloc(proc, path_bytes.len(), sdk::PAGE_READWRITE)?;
321 sdk::write(proc, remote, &path_bytes)?;
322
323 let kernel32 = sdk::raw::GetModuleHandleA(b"kernel32.dll\0".as_ptr());
324 let load_library = sdk::raw::GetProcAddress(kernel32, b"LoadLibraryA\0".as_ptr());
325 if load_library.is_null() {
326 return Err(InjectError::ResolveLoadLibrary);
327 }
328
329 sdk::create_remote_thread_and_wait(proc, load_library, remote)?;
330
331 Ok(LoadInfo {
332 method: self.name().to_string(),
333 remote_base: Some(remote as usize),
334 notes: Vec::new(),
335 })
336 }
337 }
338}
339
340pub const DECANT_INJECT_ABI: u32 = 1;
345
346#[repr(C)]
347pub struct DecantInjectRequest {
348 pub abi_version: u32,
349 pub target_process: *mut c_void,
350 pub main_thread: *mut c_void,
351 pub carafe_path: *const u16,
352 pub carafe_image: *const u8,
353 pub carafe_image_len: usize,
354 pub ready_token_name: *const u16,
355 pub out_remote_base: u64,
356}
357
358#[cfg(windows)]
359fn wide_z(s: &str) -> Vec<u16> {
360 s.encode_utf16().chain(std::iter::once(0)).collect()
361}
362
363#[cfg(windows)]
367pub struct PluginInjector {
368 pub path: std::path::PathBuf,
369}
370
371#[cfg(windows)]
372impl Injector for PluginInjector {
373 fn name(&self) -> &str {
374 "plugin"
375 }
376
377 fn portability(&self) -> Portability {
378 Portability::LoaderInternals
379 }
380
381 fn inject(&self, req: &InjectionRequest) -> Result<LoadInfo, InjectError> {
382 type AbiFn = unsafe extern "system" fn() -> u32;
383 type InjectFn = unsafe extern "system" fn(*mut DecantInjectRequest) -> i32;
384
385 let mut lib_path = self.path.to_string_lossy().into_owned().into_bytes();
386 lib_path.push(0);
387
388 let path_w = wide_z(&req.carafe_path.to_string_lossy());
389 let token_w = wide_z(&req.ready.name);
390
391 unsafe {
392 let lib = sdk::raw::LoadLibraryA(lib_path.as_ptr());
393 if lib.is_null() {
394 return Err(InjectError::Plugin(format!(
395 "loading plugin {} failed (err={}); a plugin must be a PE cdylib in this Wine prefix",
396 self.path.display(),
397 sdk::raw::GetLastError()
398 )));
399 }
400
401 let abi_sym = sdk::raw::GetProcAddress(lib, b"decant_inject_abi\0".as_ptr());
402 if abi_sym.is_null() {
403 return Err(InjectError::Plugin(
404 "plugin missing export 'decant_inject_abi'".into(),
405 ));
406 }
407 let abi = std::mem::transmute::<*mut c_void, AbiFn>(abi_sym)();
408 if abi != DECANT_INJECT_ABI {
409 return Err(InjectError::Plugin(format!(
410 "ABI mismatch: plugin reports {abi}, harness expects {DECANT_INJECT_ABI}"
411 )));
412 }
413
414 let inject_sym = sdk::raw::GetProcAddress(lib, b"decant_inject\0".as_ptr());
415 if inject_sym.is_null() {
416 return Err(InjectError::Plugin(
417 "plugin missing export 'decant_inject'".into(),
418 ));
419 }
420 let inject = std::mem::transmute::<*mut c_void, InjectFn>(inject_sym);
421
422 let mut c_req = DecantInjectRequest {
423 abi_version: DECANT_INJECT_ABI,
424 target_process: req.target.0,
425 main_thread: req.main_thread.0,
426 carafe_path: path_w.as_ptr(),
427 carafe_image: req.carafe_image.as_ptr(),
428 carafe_image_len: req.carafe_image.len(),
429 ready_token_name: token_w.as_ptr(),
430 out_remote_base: 0,
431 };
432 let rc = inject(&mut c_req);
433 if rc != 0 {
434 return Err(InjectError::Plugin(format!("plugin returned error {rc}")));
435 }
436
437 Ok(LoadInfo {
438 method: self.name().to_string(),
439 remote_base: (c_req.out_remote_base != 0).then_some(c_req.out_remote_base as usize),
440 notes: Vec::new(),
441 })
442 }
443 }
444}
445
446#[cfg(windows)]
454pub struct ExternalInjector {
455 pub command: Vec<String>,
456}
457
458#[cfg(windows)]
459impl Injector for ExternalInjector {
460 fn name(&self) -> &str {
461 "external"
462 }
463
464 fn portability(&self) -> Portability {
465 Portability::PrologueBytes
466 }
467
468 fn inject(&self, req: &InjectionRequest) -> Result<LoadInfo, InjectError> {
469 use std::process::{Command, Stdio};
470
471 let (prog, rest) = self
472 .command
473 .split_first()
474 .ok_or_else(|| InjectError::Config("external_command is empty".into()))?;
475
476 let mut child = Command::new(prog)
477 .args(rest)
478 .stdin(Stdio::piped())
479 .spawn()
480 .map_err(|e| {
481 InjectError::External(format!(
482 "spawning {prog}: {e}; the command must be a PE exe in this Wine prefix"
483 ))
484 })?;
485
486 let mut stdin = child
487 .stdin
488 .take()
489 .ok_or_else(|| InjectError::External("command stdin unavailable".into()))?;
490 external::write_request(
491 &mut stdin,
492 req.target_pid,
493 &req.carafe_path.to_string_lossy(),
494 req.carafe_image,
495 &req.ready.name,
496 )
497 .map_err(|e| InjectError::External(format!("writing protocol frame: {e}")))?;
498 drop(stdin);
499
500 let status = child
501 .wait()
502 .map_err(|e| InjectError::External(format!("waiting on command: {e}")))?;
503 match status.success() {
504 true => Ok(LoadInfo {
505 method: self.name().to_string(),
506 remote_base: None,
507 notes: vec![format!("delegated to: {}", self.command.join(" "))],
508 }),
509 false => Err(InjectError::External(format!(
510 "command {prog} exited with {status}"
511 ))),
512 }
513 }
514}
515
516#[cfg(windows)]
517impl InjectionConfig {
518 pub fn injector(&self) -> Result<Box<dyn Injector>, InjectError> {
519 match self.method {
520 Method::Standard => Ok(Box::new(StandardInjector)),
521 Method::ManualMap => Ok(Box::new(ManualMapInjector)),
522 Method::ThreadHijack => Ok(Box::new(ThreadHijackInjector)),
523 Method::Plugin => match &self.plugin_path {
524 Some(p) => Ok(Box::new(PluginInjector { path: p.clone() })),
525 None => Err(InjectError::Config(
526 "method = \"plugin\" requires plugin_path".into(),
527 )),
528 },
529 Method::External => match self.external_command.as_deref() {
530 Some(cmd) if !cmd.is_empty() => Ok(Box::new(ExternalInjector {
531 command: cmd.to_vec(),
532 })),
533 _ => Err(InjectError::Config(
534 "method = \"external\" requires a non-empty external_command".into(),
535 )),
536 },
537 }
538 }
539}
540
541#[cfg(test)]
542mod tests {
543 use super::*;
544
545 #[test]
546 fn empty_config_is_standard_defaults() {
547 let c = InjectionConfig::from_toml_str("").unwrap();
548 assert_eq!(c.domain, InjectionDomain::Tool);
549 assert_eq!(c.method, Method::Standard);
550 assert_eq!(c.timeout_ms, 5000);
551 assert!(c.plugin_path.is_none());
552 }
553
554 #[test]
555 fn injection_table_parses() {
556 let c = InjectionConfig::from_toml_str(
557 "[injection]\ndomain = \"guest\"\nmethod = \"manual-map\"\ntimeout_ms = 250\n",
558 )
559 .unwrap();
560 assert_eq!(c.domain, InjectionDomain::Guest);
561 assert_eq!(c.method, Method::ManualMap);
562 assert_eq!(c.timeout_ms, 250);
563 }
564
565 #[test]
566 fn unknown_method_errors() {
567 assert!(matches!(
568 InjectionConfig::from_toml_str("[injection]\nmethod = \"bogus\"\n"),
569 Err(InjectError::Config(_))
570 ));
571 }
572
573 #[test]
574 fn external_command_parses() {
575 let c = InjectionConfig::from_toml_str(
576 "[injection]\nmethod = \"external\"\nexternal_command = [\"inject.exe\", \"--flag\"]\n",
577 )
578 .unwrap();
579 assert_eq!(c.method, Method::External);
580 assert_eq!(
581 c.external_command.as_deref(),
582 Some(&["inject.exe".to_string(), "--flag".to_string()][..])
583 );
584 }
585
586 #[test]
587 fn external_protocol_round_trips() {
588 let mut buf = Vec::new();
589 let image = [0xDEu8, 0xAD, 0xBE, 0xEF];
590 external::write_request(
591 &mut buf,
592 4321,
593 "c:/decant_interpose.dll",
594 &image,
595 "decant_ready_7",
596 )
597 .unwrap();
598 let got = external::read_request(&mut &buf[..]).unwrap();
599 assert_eq!(got.pid, 4321);
600 assert_eq!(got.carafe_path, "c:/decant_interpose.dll");
601 assert_eq!(got.carafe_image, image);
602 assert_eq!(got.ready_token, "decant_ready_7");
603 }
604
605 #[test]
606 fn decant_config_parses_guest_table() {
607 let c = DecantConfig::from_toml_str(
608 "[injection]\ndomain = \"guest\"\nmethod = \"manual-map\"\n\
609 [guest]\nprocess = \"notepad.exe\"\npayload_path = \"payload.dll\"\n",
610 )
611 .unwrap();
612 let plan = guest::GuestInjectionPlan::from_config(&c).unwrap();
613 assert_eq!(plan.method, Method::ManualMap);
614 assert_eq!(plan.target.name.as_deref(), Some("notepad.exe"));
615 assert_eq!(plan.payload_path, std::path::PathBuf::from("payload.dll"));
616 }
617}