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