wraith/manipulation/spoof/
stack.rs

1//! Stack frame synthesis and spoofing
2//!
3//! Creates fake stack frames that look like legitimate call chains,
4//! making syscall invocations appear to come from expected call paths.
5
6#[cfg(all(not(feature = "std"), feature = "alloc"))]
7use alloc::{format, string::String, vec, vec::Vec};
8
9#[cfg(feature = "std")]
10use std::{format, string::String, vec, vec::Vec};
11
12use crate::error::{Result, WraithError};
13use crate::navigation::ModuleQuery;
14use crate::structures::Peb;
15
16/// a single fake stack frame
17#[derive(Debug, Clone)]
18pub struct FakeFrame {
19    /// return address (points into a legitimate module)
20    pub return_address: usize,
21    /// saved RBP value (points to previous frame or 0)
22    pub saved_rbp: usize,
23    /// any additional values to push for this frame (local variables/saved regs)
24    pub extra_values: Vec<usize>,
25    /// description of what this frame represents
26    pub description: &'static str,
27}
28
29impl FakeFrame {
30    /// create a simple frame with just return address and saved rbp
31    pub const fn simple(return_address: usize, saved_rbp: usize, description: &'static str) -> Self {
32        Self {
33            return_address,
34            saved_rbp,
35            extra_values: Vec::new(),
36            description,
37        }
38    }
39
40    /// create a frame with extra values
41    pub fn with_extra(
42        return_address: usize,
43        saved_rbp: usize,
44        extra: Vec<usize>,
45        description: &'static str,
46    ) -> Self {
47        Self {
48            return_address,
49            saved_rbp,
50            extra_values: extra,
51            description,
52        }
53    }
54
55    /// total size of this frame in bytes
56    pub fn size(&self) -> usize {
57        // return address + saved rbp + extra values
58        (2 + self.extra_values.len()) * core::mem::size_of::<usize>()
59    }
60}
61
62/// template for generating fake frames based on known call patterns
63#[derive(Debug, Clone)]
64pub struct FrameTemplate {
65    /// name of this template (e.g., "CreateFileW -> NtCreateFile")
66    pub name: &'static str,
67    /// module containing the return address
68    pub module: &'static str,
69    /// function name for the return address
70    pub function: &'static str,
71    /// offset from function start (to look like mid-function return)
72    pub offset: usize,
73    /// number of extra stack slots this frame uses
74    pub extra_slots: usize,
75    /// description
76    pub description: &'static str,
77}
78
79impl FrameTemplate {
80    /// resolve this template to an actual fake frame
81    pub fn resolve(&self) -> Result<FakeFrame> {
82        let peb = Peb::current()?;
83        let query = ModuleQuery::new(&peb);
84        let module = query.find_by_name(self.module)?;
85        let func_addr = module.get_export(self.function)?;
86        let return_addr = func_addr + self.offset;
87
88        Ok(FakeFrame {
89            return_address: return_addr,
90            saved_rbp: 0, // will be set by StackSpoofer
91            extra_values: vec![0; self.extra_slots],
92            description: self.description,
93        })
94    }
95}
96
97/// common frame templates for Windows API call chains
98pub static COMMON_FRAME_TEMPLATES: &[FrameTemplate] = &[
99    // kernel32!CreateFileW calling ntdll!NtCreateFile
100    FrameTemplate {
101        name: "CreateFileW",
102        module: "kernel32.dll",
103        function: "CreateFileW",
104        offset: 0x50, // typical offset after internal call
105        extra_slots: 4,
106        description: "kernel32!CreateFileW frame",
107    },
108    // kernel32!ReadFile calling ntdll!NtReadFile
109    FrameTemplate {
110        name: "ReadFile",
111        module: "kernel32.dll",
112        function: "ReadFile",
113        offset: 0x40,
114        extra_slots: 4,
115        description: "kernel32!ReadFile frame",
116    },
117    // kernel32!WriteFile calling ntdll!NtWriteFile
118    FrameTemplate {
119        name: "WriteFile",
120        module: "kernel32.dll",
121        function: "WriteFile",
122        offset: 0x40,
123        extra_slots: 4,
124        description: "kernel32!WriteFile frame",
125    },
126    // kernel32!VirtualAlloc calling ntdll!NtAllocateVirtualMemory
127    FrameTemplate {
128        name: "VirtualAlloc",
129        module: "kernel32.dll",
130        function: "VirtualAlloc",
131        offset: 0x30,
132        extra_slots: 2,
133        description: "kernel32!VirtualAlloc frame",
134    },
135    // kernel32!VirtualProtect calling ntdll!NtProtectVirtualMemory
136    FrameTemplate {
137        name: "VirtualProtect",
138        module: "kernel32.dll",
139        function: "VirtualProtect",
140        offset: 0x30,
141        extra_slots: 2,
142        description: "kernel32!VirtualProtect frame",
143    },
144    // kernel32!OpenProcess calling ntdll!NtOpenProcess
145    FrameTemplate {
146        name: "OpenProcess",
147        module: "kernel32.dll",
148        function: "OpenProcess",
149        offset: 0x40,
150        extra_slots: 3,
151        description: "kernel32!OpenProcess frame",
152    },
153    // kernel32!BaseThreadInitThunk (common thread start)
154    FrameTemplate {
155        name: "BaseThreadInitThunk",
156        module: "kernel32.dll",
157        function: "BaseThreadInitThunk",
158        offset: 0x14,
159        extra_slots: 1,
160        description: "kernel32!BaseThreadInitThunk frame",
161    },
162    // ntdll!RtlUserThreadStart (thread entry)
163    FrameTemplate {
164        name: "RtlUserThreadStart",
165        module: "ntdll.dll",
166        function: "RtlUserThreadStart",
167        offset: 0x21,
168        extra_slots: 1,
169        description: "ntdll!RtlUserThreadStart frame",
170    },
171];
172
173/// synthesized stack layout for spoofing
174#[derive(Debug)]
175pub struct SyntheticStack {
176    /// the fake frames from bottom (oldest) to top (newest)
177    frames: Vec<FakeFrame>,
178    /// total stack size needed
179    total_size: usize,
180    /// buffer containing the synthesized stack data
181    data: Vec<usize>,
182}
183
184impl SyntheticStack {
185    /// create an empty synthetic stack
186    pub fn new() -> Self {
187        Self {
188            frames: Vec::new(),
189            total_size: 0,
190            data: Vec::new(),
191        }
192    }
193
194    /// add a frame to the top of the stack
195    pub fn push_frame(&mut self, frame: FakeFrame) {
196        self.total_size += frame.size();
197        self.frames.push(frame);
198    }
199
200    /// build the final stack layout
201    /// returns the stack data with proper rbp chain
202    pub fn build(&mut self) -> &[usize] {
203        self.data.clear();
204
205        // build frames from bottom to top
206        // each frame's saved_rbp points to the previous frame's RBP location
207        let mut prev_rbp: usize = 0;
208
209        for frame in &self.frames {
210            // record position of this frame's saved RBP
211            let rbp_pos = self.data.len();
212
213            // push saved RBP (points to previous frame)
214            self.data.push(prev_rbp);
215            // push return address
216            self.data.push(frame.return_address);
217            // push extra values
218            for &val in &frame.extra_values {
219                self.data.push(val);
220            }
221
222            // update prev_rbp to point to this frame's saved RBP
223            // this would be the stack address, but we're using indices here
224            prev_rbp = rbp_pos;
225        }
226
227        &self.data
228    }
229
230    /// get the number of frames
231    pub fn frame_count(&self) -> usize {
232        self.frames.len()
233    }
234
235    /// get total size in bytes
236    pub fn total_size_bytes(&self) -> usize {
237        self.total_size
238    }
239}
240
241impl Default for SyntheticStack {
242    fn default() -> Self {
243        Self::new()
244    }
245}
246
247/// high-level stack spoofer that creates believable call stacks
248pub struct StackSpoofer {
249    /// resolved frame templates
250    templates: Vec<(FrameTemplate, Option<FakeFrame>)>,
251}
252
253impl StackSpoofer {
254    /// create a new stack spoofer
255    pub fn new() -> Self {
256        Self {
257            templates: COMMON_FRAME_TEMPLATES
258                .iter()
259                .map(|t| (t.clone(), None))
260                .collect(),
261        }
262    }
263
264    /// resolve all templates to actual addresses
265    pub fn resolve_all(&mut self) -> Result<()> {
266        for (template, resolved) in &mut self.templates {
267            match template.resolve() {
268                Ok(frame) => *resolved = Some(frame),
269                Err(_) => continue, // skip unresolvable templates
270            }
271        }
272        Ok(())
273    }
274
275    /// get a resolved frame by template name
276    pub fn get_frame(&self, name: &str) -> Option<&FakeFrame> {
277        self.templates
278            .iter()
279            .find(|(t, _)| t.name == name)
280            .and_then(|(_, f)| f.as_ref())
281    }
282
283    /// build a synthetic stack for a specific call pattern
284    pub fn build_stack_for(&self, pattern: &[&str]) -> Result<SyntheticStack> {
285        let mut stack = SyntheticStack::new();
286
287        for &name in pattern {
288            if let Some(frame) = self.get_frame(name) {
289                stack.push_frame(frame.clone());
290            } else {
291                return Err(WraithError::SyscallEnumerationFailed {
292                    reason: format!("template '{}' not found or not resolved", name),
293                });
294            }
295        }
296
297        Ok(stack)
298    }
299
300    /// build a generic "thread start" stack that looks legitimate
301    pub fn build_thread_start_stack(&self) -> Result<SyntheticStack> {
302        // typical thread start: RtlUserThreadStart -> BaseThreadInitThunk -> user code
303        self.build_stack_for(&["RtlUserThreadStart", "BaseThreadInitThunk"])
304    }
305
306    /// build a stack for VirtualAlloc-like calls
307    pub fn build_memory_alloc_stack(&self) -> Result<SyntheticStack> {
308        self.build_stack_for(&["RtlUserThreadStart", "BaseThreadInitThunk", "VirtualAlloc"])
309    }
310
311    /// build a stack for file operations
312    pub fn build_file_io_stack(&self) -> Result<SyntheticStack> {
313        self.build_stack_for(&["RtlUserThreadStart", "BaseThreadInitThunk", "CreateFileW"])
314    }
315}
316
317impl Default for StackSpoofer {
318    fn default() -> Self {
319        Self::new()
320    }
321}
322
323/// helper to find good return addresses in a module
324pub fn find_return_address_in_module(module_name: &str, function_name: &str) -> Result<usize> {
325    let peb = Peb::current()?;
326    let query = ModuleQuery::new(&peb);
327    let module = query.find_by_name(module_name)?;
328    module.get_export(function_name)
329}
330
331/// create a simple fake frame pointing to a known function
332pub fn create_frame_at_function(
333    module_name: &str,
334    function_name: &str,
335    offset: usize,
336) -> Result<FakeFrame> {
337    let addr = find_return_address_in_module(module_name, function_name)?;
338    Ok(FakeFrame::simple(
339        addr + offset,
340        0,
341        "custom frame",
342    ))
343}
344
345#[cfg(test)]
346mod tests {
347    use super::*;
348
349    #[test]
350    fn test_frame_template_resolve() {
351        // try to resolve a kernel32 template
352        for template in COMMON_FRAME_TEMPLATES {
353            if template.module == "kernel32.dll" {
354                match template.resolve() {
355                    Ok(frame) => {
356                        assert!(frame.return_address > 0, "should have valid return address");
357                        break;
358                    }
359                    Err(_) => continue,
360                }
361            }
362        }
363    }
364
365    #[test]
366    fn test_synthetic_stack_build() {
367        let mut stack = SyntheticStack::new();
368
369        stack.push_frame(FakeFrame::simple(0x12345678, 0, "test frame 1"));
370        stack.push_frame(FakeFrame::simple(0x87654321, 0, "test frame 2"));
371
372        let data = stack.build();
373        assert!(!data.is_empty(), "should build stack data");
374        assert_eq!(stack.frame_count(), 2);
375    }
376
377    #[test]
378    fn test_stack_spoofer() {
379        let mut spoofer = StackSpoofer::new();
380
381        // this might fail if modules aren't loaded, which is ok
382        let _ = spoofer.resolve_all();
383    }
384
385    #[test]
386    fn test_find_return_address() {
387        // try to find a known function
388        if let Ok(addr) = find_return_address_in_module("kernel32.dll", "VirtualAlloc") {
389            assert!(addr > 0, "should find VirtualAlloc");
390        }
391    }
392}