Skip to main content

ptrace_do/
lib.rs

1mod arch;
2use arch::UserRegs;
3
4use libc::{pid_t, ptrace, PTRACE_ATTACH, PTRACE_CONT, PTRACE_DETACH};
5use std::{io, mem};
6use thiserror::Error;
7
8/// Utility function that converts the usize inputs individually with the appropriate endianness into a Vec<u8>
9fn usize_arr_to_u8(data: &[usize]) -> Vec<u8> {
10    let mut arr: Vec<u8> = Vec::new();
11    for p in data {
12        if cfg!(target_endian = "big") {
13            arr.extend_from_slice(&p.to_be_bytes());
14        } else {
15            arr.extend_from_slice(&p.to_le_bytes());
16        }
17    }
18    arr
19}
20
21/// Enum containing all errors tracing can witness
22#[derive(Debug, Error)]
23pub enum TraceError {
24    /// Error spawning from syscall interactions
25    #[error("Ptrace error: `{0}`")]
26    Ptrace(io::Error),
27
28    /// Error during read and writing of process's memory
29    #[error("IO Error: `{0}`")]
30    Io(#[from] io::Error),
31
32    /// Contexted error
33    #[error("General `{0}`")]
34    General(&'static str),
35}
36
37/// Internal Result type
38pub type TraceResult<T> = Result<T, TraceError>;
39
40/// The available wait options
41#[allow(unused)]
42enum WaitOptions {
43    None,
44    NoHang,
45    Untraced,
46    Continued,
47}
48
49/// A wait option can be extracted from an i32
50impl From<WaitOptions> for i32 {
51    fn from(val: WaitOptions) -> Self {
52        match val {
53            WaitOptions::None => 0,
54            WaitOptions::NoHang => libc::WNOHANG,
55            WaitOptions::Untraced => libc::WUNTRACED,
56            WaitOptions::Continued => libc::WCONTINUED,
57        }
58    }
59}
60
61/// Wait status result of a ptrace step
62struct WaitStatus(i32);
63
64impl WaitStatus {
65    /// is stopped status
66    fn is_stop(&self) -> bool {
67        libc::WIFSTOPPED(self.0)
68    }
69
70    /// is signaled status
71    fn is_signaled(&self) -> bool {
72        libc::WIFSIGNALED(self.0)
73    }
74
75    /// is continued status
76    fn is_continued(&self) -> bool {
77        libc::WIFCONTINUED(self.0)
78    }
79
80    /// is exited status
81    fn is_exited(&self) -> bool {
82        libc::WIFEXITED(self.0)
83    }
84
85    /// is stopcode status
86    fn stop_code(&self) -> i32 {
87        libc::WSTOPSIG(self.0)
88    }
89}
90
91impl std::fmt::Debug for WaitStatus {
92    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
93        f.debug_struct("WaitStatus")
94            .field("is_stopped", &self.is_stop())
95            .field("is_signaled", &self.is_signaled())
96            .field("is_continued", &self.is_continued())
97            .field("is_exited", &self.is_exited())
98            .field("stop_code", &self.stop_code())
99            .finish()
100    }
101}
102
103/// Represents a traced process which is in the process of building a `frame`.
104/// `frame` can be thought of a mutable view into a stopped process's execution.
105/// Given you own a process frame, it is an appropriate time to edit registers, change instructions, and edit memory.
106pub struct ProcessFrame {
107    process: TracedProcess,
108}
109
110impl ProcessFrame {
111    /// aarch64 specific get registers functionality.
112    /// uses iovec's and GETREGSET
113    #[cfg(target_arch = "aarch64")]
114    pub fn query_registers(&mut self) -> TraceResult<UserRegs> {
115        let mut registers: UserRegs = unsafe { mem::zeroed() };
116        let mut iovec = libc::iovec {
117            iov_base: &mut registers as *mut _ as *mut core::ffi::c_void,
118            iov_len: std::mem::size_of::<UserRegs>() as libc::size_t,
119        };
120
121        let result = unsafe { ptrace(libc::PTRACE_GETREGSET, self.process.pid(), 1, &mut iovec) };
122
123        match result == -1 {
124            true => Err(TraceError::Ptrace(std::io::Error::last_os_error())),
125            false => Ok(registers),
126        }
127    }
128
129    /// aarch64 specific set registers functionality.
130    /// uses iovec's and SETREGSET
131    #[cfg(target_arch = "aarch64")]
132    pub fn set_registers(&mut self, mut registers: UserRegs) -> TraceResult<()> {
133        let mut iovec = libc::iovec {
134            iov_base: &mut registers as *mut _ as *mut core::ffi::c_void,
135            iov_len: std::mem::size_of::<UserRegs>() as libc::size_t,
136        };
137
138        let result = unsafe { ptrace(libc::PTRACE_SETREGSET, self.process.pid(), 1, &mut iovec) };
139
140        match result == -1 {
141            true => Err(TraceError::Ptrace(std::io::Error::last_os_error())),
142            false => Ok(()),
143        }
144    }
145
146    /// Attempts to invoke a remote function with the provided parameters.
147    /// Internally this is an os cfg controlled function that write the inputted parameters according to the architectures expectations.
148    /// Additionally it prepares the provided return address
149    #[cfg(target_arch = "aarch64")]
150    pub fn invoke_remote(
151        mut self,
152        func_address: usize,
153        return_address: usize,
154        parameters: &[usize],
155    ) -> TraceResult<(UserRegs, ProcessFrame)> {
156        use std::mem::size_of;
157
158        const REGISTER_ARGUMENTS: usize = 8;
159        let mut current_registers = self.query_registers()?;
160        tracing::trace!(
161            "Initial registers acquired Current PC: {:X?}",
162            current_registers.program_counter()
163        );
164
165        let cached_registers = current_registers.clone();
166        current_registers.set_stack_pointer(current_registers.stack_pointer() & !0xfusize);
167        for (i, param) in parameters[..std::cmp::min(parameters.len(), REGISTER_ARGUMENTS)]
168            .iter()
169            .enumerate()
170        {
171            let reg: usize = *param;
172            current_registers.regs[i] = reg as u64;
173            tracing::trace!("Applying register {i} with param {}", reg);
174        }
175
176        if parameters.len() > REGISTER_ARGUMENTS {
177            let stack_arguments = &parameters[REGISTER_ARGUMENTS..];
178            tracing::trace!("Remaining stack arguments: {:?}", stack_arguments);
179
180            // adjust stack pointer
181            current_registers.set_stack_pointer(
182                current_registers.stack_pointer()
183                    - (((stack_arguments.len() + 1) & !1usize) * size_of::<usize>()),
184            );
185
186            self.write_memory(
187                current_registers.stack_pointer(),
188                usize_arr_to_u8(stack_arguments).as_slice(),
189            )?;
190        };
191
192        // set registers cached_registers
193        current_registers.set_program_counter(func_address);
194        if (current_registers.program_counter() & 1) != 0 {
195            current_registers.set_program_counter(current_registers.program_counter() & !1);
196            current_registers
197                .set_cpsr((current_registers.cpsr() as u32 | (arch::CPSR_T_MASK)) as usize);
198        } else {
199            current_registers
200                .set_cpsr((current_registers.cpsr() as u32 & !(arch::CPSR_T_MASK)) as usize);
201        }
202        current_registers.set_lr(return_address);
203        tracing::trace!(
204            "Executing with PC: {:X?}, and arguments {parameters:?}",
205            func_address
206        );
207
208        self.set_registers(current_registers)?;
209        tracing::trace!("Registers successfully injected.");
210
211        let mut frame = self.step_cont()?;
212        let result_regs = frame.query_registers()?;
213        tracing::trace!("Result {result_regs:#?}");
214
215        frame.set_registers(cached_registers)?;
216        Ok((result_regs, frame))
217    }
218
219    /// gets process frame registers.
220    /// internally uses GETREGS
221    #[cfg(any(target_arch = "x86", target_arch = "x86_64", target_arch = "arm"))]
222    pub fn query_registers(&mut self) -> TraceResult<UserRegs> {
223        let mut registers: UserRegs = unsafe { mem::zeroed() };
224        let result = unsafe { ptrace(libc::PTRACE_GETREGS, self.process.pid(), 0, &mut registers) };
225
226        match result == -1 {
227            true => Err(TraceError::Ptrace(std::io::Error::last_os_error())),
228            false => Ok(registers),
229        }
230    }
231
232    /// sets a process frame registers.
233    /// internally uses SETREGS
234    #[cfg(any(target_arch = "x86", target_arch = "x86_64", target_arch = "arm"))]
235    pub fn set_registers(&mut self, registers: UserRegs) -> TraceResult<()> {
236        let result = unsafe { ptrace(libc::PTRACE_SETREGS, self.process.pid(), 0, &registers) };
237
238        match result == -1 {
239            true => Err(TraceError::Ptrace(std::io::Error::last_os_error())),
240            false => Ok(()),
241        }
242    }
243
244    /// Attempts to invoke a remote function with the provided parameters.
245    /// Internally this is an os cfg controlled function that write the inputted parameters according to the architectures expectations.
246    /// Additionally it prepares the provided return address
247    #[cfg(target_arch = "arm")]
248    pub fn invoke_remote(
249        mut self,
250        func_address: usize,
251        return_address: usize,
252        parameters: &[usize],
253    ) -> TraceResult<(UserRegs, ProcessFrame)> {
254        use std::mem::size_of;
255
256        const REGISTER_ARGUMENTS: usize = 4;
257        let mut current_registers = self.query_registers()?;
258        tracing::debug!(
259            "Initial registers acquired Current PC: {:X?}",
260            current_registers.program_counter()
261        );
262
263        let cached_registers = current_registers.clone();
264        current_registers.set_stack_pointer(current_registers.stack_pointer() & !0xfusize);
265        for (i, param) in parameters[..std::cmp::min(parameters.len(), REGISTER_ARGUMENTS)]
266            .iter()
267            .enumerate()
268        {
269            let reg: u32 = (*param).try_into().unwrap();
270            current_registers.regs[i] = reg;
271            tracing::trace!("Applying register {i} with param {}", reg);
272        }
273
274        if parameters.len() > REGISTER_ARGUMENTS {
275            let stack_arguments = &parameters[REGISTER_ARGUMENTS..];
276            tracing::trace!("Remaining stack arguments: {:?}", stack_arguments);
277
278            // adjust stack pointer
279            current_registers.set_stack_pointer(
280                current_registers.stack_pointer()
281                    - (((stack_arguments.len() + 3) & !3usize) * size_of::<usize>()),
282            );
283
284            self.write_memory(
285                current_registers.stack_pointer(),
286                usize_arr_to_u8(stack_arguments).as_slice(),
287            )?;
288        };
289
290        // set registers cached_registers
291        current_registers.set_program_counter(func_address);
292        if (current_registers.program_counter() & 1) != 0 {
293            current_registers.set_program_counter(current_registers.program_counter() & !1);
294            current_registers
295                .set_cpsr((current_registers.cpsr() as u32 | (arch::CPSR_T_MASK)) as usize);
296        } else {
297            current_registers
298                .set_cpsr((current_registers.cpsr() as u32 & !(arch::CPSR_T_MASK)) as usize);
299        }
300        current_registers.set_lr(return_address);
301        tracing::trace!(
302            "Executing with PC: {:X?}, and arguments {parameters:?}",
303            func_address
304        );
305
306        self.set_registers(current_registers)?;
307        tracing::trace!("Registers successfully injected.");
308
309        let mut frame = self.step_cont()?;
310        let result_regs = frame.query_registers()?;
311        tracing::trace!("Result {result_regs:#?}");
312
313        frame.set_registers(cached_registers)?;
314        Ok((result_regs, frame))
315    }
316
317    /// Attempts to invoke a remote function with the provided parameters.
318    /// Internally this is an os cfg controlled function that write the inputted parameters according to the architectures expectations.
319    /// Additionally it prepares the provided return address
320    #[cfg(target_arch = "x86")]
321    pub fn invoke_remote(
322        mut self,
323        func_address: usize,
324        return_address: usize,
325        parameters: &[usize],
326    ) -> TraceResult<(UserRegs, ProcessFrame)> {
327        use std::mem::size_of;
328
329        let mut current_registers = self.query_registers()?;
330        tracing::trace!(
331            "Initial registers acquired Current PC: {:X?}",
332            current_registers.program_counter()
333        );
334
335        let cached_registers = current_registers.clone();
336        let param_count = parameters.len();
337        current_registers.set_stack_pointer(current_registers.stack_pointer() & !0xfusize);
338
339        tracing::trace!("Function parameters: {:?}", parameters);
340
341        if param_count > 0 {
342            // adjust stack pointer
343            current_registers.set_stack_pointer(
344                current_registers.stack_pointer()
345                    - (((param_count + 3) & !3usize) * size_of::<usize>()),
346            );
347            self.write_memory(
348                current_registers.stack_pointer(),
349                usize_arr_to_u8(parameters).as_slice(),
350            )?;
351        }
352
353        // return address is bottom of stack!
354        current_registers.set_stack_pointer(current_registers.stack_pointer() - size_of::<usize>());
355        self.write_memory(
356            current_registers.stack_pointer(),
357            &return_address.to_le_bytes(),
358        )?;
359
360        current_registers.eax = 0;
361        current_registers.orig_eax = 0;
362
363        // set registers cached_registers
364        current_registers.set_program_counter(func_address);
365        tracing::trace!(
366            "Executing with PC: {:X?}, and arguments {parameters:?}",
367            func_address
368        );
369
370        self.set_registers(current_registers)?;
371        tracing::trace!("Registers successfully injected.");
372
373        let mut frame = self.step_cont()?;
374        let result_regs = frame.query_registers()?;
375        tracing::trace!("Result {result_regs:#?}");
376
377        frame.set_registers(cached_registers)?;
378        Ok((result_regs, frame))
379    }
380
381    /// Attempts to invoke a remote function with the provided parameters.
382    /// Internally this is an os cfg controlled function that write the inputted parameters according to the architectures expectations.
383    /// Additionally it prepares the provided return address
384    #[cfg(target_arch = "x86_64")]
385    pub fn invoke_remote(
386        mut self,
387        func_address: usize,
388        return_address: usize,
389        parameters: &[usize],
390    ) -> TraceResult<(UserRegs, ProcessFrame)> {
391        use std::mem::size_of;
392
393        const REGISTER_ARGUMENTS: usize = 6;
394        let mut current_registers = self.query_registers()?;
395        tracing::debug!(
396            "Initial registers acquired Current PC: {:X?}",
397            current_registers.program_counter()
398        );
399
400        let cached_registers = current_registers.clone();
401        let param_count = parameters.len();
402        current_registers.set_stack_pointer(current_registers.stack_pointer() & !0xfusize);
403
404        // You gotta a better idea???????
405        if param_count > 0 {
406            current_registers.rdi = parameters[0] as u64;
407        }
408        if param_count > 1 {
409            current_registers.rsi = parameters[1] as u64;
410        }
411        if param_count > 2 {
412            current_registers.rdx = parameters[2] as u64;
413        }
414        if param_count > 3 {
415            current_registers.rcx = parameters[3] as u64;
416        }
417        if param_count > 4 {
418            current_registers.r8 = parameters[4] as u64;
419        }
420        if param_count > 5 {
421            current_registers.r9 = parameters[5] as u64;
422        }
423
424        if parameters.len() > REGISTER_ARGUMENTS {
425            let stack_arguments = &parameters[REGISTER_ARGUMENTS..];
426            tracing::trace!("Remaining stack arguments: {:?}", stack_arguments);
427
428            // adjust stack pointer
429            current_registers.set_stack_pointer(
430                current_registers.stack_pointer()
431                    - (((stack_arguments.len() + 1) & !1usize) * size_of::<usize>()),
432            );
433            self.write_memory(
434                current_registers.stack_pointer(),
435                usize_arr_to_u8(stack_arguments).as_slice(),
436            )?;
437        };
438
439        // return address is bottom of stack!
440        current_registers.set_stack_pointer(current_registers.stack_pointer() - size_of::<usize>());
441        self.write_memory(
442            current_registers.stack_pointer(),
443            &return_address.to_le_bytes(),
444        )?;
445
446        current_registers.rax = 0;
447        current_registers.orig_rax = 0;
448
449        // set registers cached_registers
450        current_registers.set_program_counter(func_address);
451        tracing::trace!(
452            "Executing with PC: {:X?}, and arguments {parameters:?}",
453            func_address
454        );
455
456        self.set_registers(current_registers)?;
457        tracing::trace!("Registers successfully injected.");
458
459        let mut frame = self.step_cont()?;
460        let result_regs = frame.query_registers()?;
461        tracing::debug!("Result {result_regs:#?}");
462
463        frame.set_registers(cached_registers)?;
464        Ok((result_regs, frame))
465    }
466
467    /// Attempts to read a process's memory from fs
468    pub fn read_memory(&mut self, addr: usize, len: usize) -> TraceResult<Vec<u8>> {
469        let mut data = vec![0; len];
470        let len_read = self.read_memory_mut(addr, &mut data)?;
471        data.truncate(len_read);
472        Ok(data)
473    }
474
475    /// Attempts to read a mutable section of a process's memory from fs
476    pub fn read_memory_mut(&self, addr: usize, data: &mut [u8]) -> TraceResult<usize> {
477        use std::os::unix::fs::FileExt;
478        let mem = std::fs::File::open(self.process.proc_mem_path())?;
479        let len = mem.read_at(data, addr.try_into().unwrap())?;
480        Ok(len)
481    }
482
483    /// Attempts to write to a section of the process's memory
484    pub fn write_memory(&mut self, addr: usize, data: &[u8]) -> TraceResult<usize> {
485        use std::os::unix::fs::FileExt;
486        let mem = std::fs::OpenOptions::new()
487            .write(true)
488            .open(self.process.proc_mem_path())?;
489        let len = mem.write_at(data, addr.try_into().unwrap())?;
490        Ok(len)
491    }
492
493    /// Continue the process frame, consuming self.
494    fn step_cont(mut self) -> TraceResult<ProcessFrame> {
495        self.process.cont()?;
496        self.process.next_frame()
497    }
498}
499
500/// A process actively being traced.
501pub struct TracedProcess {
502    pid: pid_t,
503}
504
505// Drop implementation that attempts to ptrace detach from the process.
506// On failure there is a warning, but this does not ever panic.
507impl Drop for TracedProcess {
508    fn drop(&mut self) {
509        let pid = self.pid();
510        match self.detach() {
511            Ok(()) => tracing::debug!("Successfully detached from Pid: {pid}"),
512            Err(e) => tracing::error!("Failed to detach from Pid: {pid}, {e:#?}"),
513        }
514    }
515}
516
517impl TracedProcess {
518    /// Attempt to detach from the traced process
519    fn detach(&mut self) -> TraceResult<()> {
520        let result = unsafe { ptrace(PTRACE_DETACH, self.pid(), 0, 0) };
521        match result == -1 {
522            true => Err(TraceError::Ptrace(std::io::Error::last_os_error())),
523            false => Ok(()),
524        }
525    }
526
527    /// Attempt to attach from the traced process
528    pub fn attach(pid: pid_t) -> TraceResult<Self> {
529        let result = unsafe { ptrace(PTRACE_ATTACH, pid, 0, 0) };
530        match result == -1 {
531            true => Err(TraceError::Ptrace(std::io::Error::last_os_error())),
532            false => Ok(Self { pid }),
533        }
534    }
535
536    /// pid of the actively traced process
537    pub fn pid(&self) -> pid_t {
538        self.pid
539    }
540
541    /// continue execution
542    fn cont(&mut self) -> TraceResult<()> {
543        let result = unsafe { ptrace(PTRACE_CONT, self.pid(), 0, 0) };
544        match result == -1 {
545            true => Err(TraceError::Ptrace(std::io::Error::last_os_error())),
546            false => Ok(()),
547        }
548    }
549
550    /// wait for a status
551    fn wait(&mut self, options: WaitOptions) -> TraceResult<WaitStatus> {
552        let mut raw_status = 0;
553        let result = unsafe { libc::waitpid(self.pid, &mut raw_status, options.into()) };
554        if result == -1 {
555            return Err(TraceError::Ptrace(std::io::Error::last_os_error()));
556        }
557
558        let status = WaitStatus(raw_status);
559        tracing::trace!("wait status {status:?}");
560        Ok(status)
561    }
562
563    /// wait for an untraced status consuming self and opening a process frame
564    pub fn next_frame(mut self) -> TraceResult<ProcessFrame> {
565        let wait_status = self.wait(WaitOptions::Untraced)?;
566        if wait_status.is_exited() {
567            return Err(TraceError::General("Waiting stop received an exit signal"));
568        }
569
570        Ok(ProcessFrame { process: self })
571    }
572
573    /// path to the process's memory
574    fn proc_mem_path(&self) -> String {
575        format!("/proc/{}/mem", self.pid)
576    }
577}