steroid 0.5.0

A lightweight framework for dynamic binary instrumentation
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
//! The run module contains traits and types to manipulate running and stopped execution entities.

use std::collections::HashMap;
use std::error::Error;
use std::fmt::Result as FmtResult;
use std::fmt::{Display, Formatter};
use std::mem::transmute;

use nix::sys::ptrace::Options as NixPtraceOptions;
use nix::sys::ptrace::{getevent, Event};
use nix::sys::signal::Signal as NixSignal;
use nix::sys::wait::WaitStatus;

use crate::breakpoint::BreakpointId;
use crate::error::{CouldNotResume, CouldNotSetOptions, UnexpectedExit};
use crate::process::{Pid, TargetController, TargetProcess};

/// Signals used by the operating system.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[repr(i32)]
#[non_exhaustive]
pub enum Signal {
    SIGHUP = 1,
    SIGINT,
    SIGQUIT,
    SIGILL,
    SIGTRAP,
    SIGABRT,
    SIGIOT,
    SIGBUS,
    SIGFPE,
    SIGKILL,
    SIGUSR1,
    SIGSEGV,
    SIGUSR2,
    SIGPIPE,
    SIGALRM,
    SIGTERM,
    SIGSTKFLT,
    SIGCHLD,
    SIGCLD,
    SIGCONT,
    SIGSTOP,
    SIGTSTP,
    SIGTTIN,
    SIGTTOU,
    SIGURG,
    SIGXCPU,
    SIGXFSZ,
    SIGVTALRM,
    SIGPROF,
    SIGWINCH,
    SIGPOLL,
    SIGIO,
    SIGPWR,
    SIGSYS,
}

impl From<NixSignal> for Signal {
    fn from(value: NixSignal) -> Self {
        // A nix Signal already represents a valid signal value, so a transmutation is valid in this
        // case.
        unsafe { transmute(value) }
    }
}

/// Reason why a remote process has been stopped.
///
/// This type is used in [`TargetController`]s.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum Reason {
    /// The remote process has encountered a breakpoint set by the steroid client.
    Breakpoint(BreakpointId),
    /// The remote process exited with the given exit code.
    Exited { exit_code: i32 },
    /// The remote process has been stopped by the given signal but is still alive.
    Stopped { signal: Signal },
    /// The remote process was killed by the given signal.
    Signaled {
        /// The signal that killed the process.
        signal: Signal,
        /// The signal generated a core dump (or not).
        dumped: bool,
    },
    /// The remote process is stopped by a trap instruction, but it does not correspond to a known
    /// breakpoint.
    Trapped,
    /// The remote process used [`clone(2)`] to create a new thread.
    ///
    /// [`clone(2)`]: https://man7.org/linux/man-pages/man2/clone.2.html
    ThreadCreated { tid: Pid },
}

impl Display for Reason {
    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
        match self {
            Self::Breakpoint(id) => write!(f, "stopped at breakpoint {id:?}"),
            Self::Exited { exit_code } => write!(f, "exited with code: {exit_code}"),
            Self::Stopped { signal } => write!(f, "stopped with signal {signal:?}"),
            Self::Signaled { signal, .. } => write!(f, "killed by signal {signal:?}"),
            Self::Trapped => write!(f, "encountered a trap instruction"),
            Self::ThreadCreated { tid } => write!(f, "created a new thread (TID={tid})"),
        }
    }
}

impl Reason {
    pub(super) fn from_wait_status<E>(ctrl: &mut TargetController<E>, status: WaitStatus) -> Self
    where
        E: Executing,
    {
        match status {
            WaitStatus::Exited(_, exit_code) => Self::Exited { exit_code },
            WaitStatus::Signaled(_, signal, dumped) => Self::Signaled {
                signal: signal.into(),
                dumped,
            },
            WaitStatus::Stopped(_, NixSignal::SIGTRAP) => {
                let rip = ctrl.get_registers().unwrap().rip as usize;

                ctrl.process()
                    .breakpoints()
                    .find(|brk| brk.address() == rip - 1)
                    .map_or(Self::Trapped, |brk| Self::Breakpoint(brk.id()))
            }
            WaitStatus::Stopped(_, signal) => Self::Stopped {
                signal: signal.into(),
            },
            WaitStatus::PtraceEvent(_, _, event) => match unsafe { transmute(event) } {
                Event::PTRACE_EVENT_CLONE => {
                    let tid = getevent(ctrl.process().pid().into()).unwrap();
                    Self::ThreadCreated {
                        tid: Pid::from_raw(tid as i32),
                    }
                }
                _ => todo!(),
            },
            WaitStatus::PtraceSyscall(_) => todo!(),
            WaitStatus::Continued(_) => {
                unreachable!("The remote process should not be 'continued' if it was stopped.")
            }
            WaitStatus::StillAlive => unreachable!("Steroid's wait is not WNOHANG"),
        }
    }
}

/// Result of [`Executing::wait`].
///
/// This type represents the state of the remote process once the steroid client has waited for it
/// to stop and give back control. A process can either be exited (i.e. not valid anymore) or
/// alive. If a process is still alive, the user is provided with a controller to manipulate the
/// process. Otherwise, they can only access the reason why the process exited.
#[must_use]
pub enum RunningState<E>
where
    E: Executing,
{
    Alive(TargetController<E>),
    Exited { tid: Pid, reason: Reason },
}

impl<E> RunningState<E>
where
    E: Executing,
{
    /// Helper function that allow the user to get the controller of their remote process if they
    /// know it must still be running.
    ///
    /// ```
    /// # use anyhow::Error;
    /// # use steroid::process::spawn_process;
    /// # use steroid::run::Executing;
    /// # let process = spawn_process("/usr/bin/ls", ["-l"])?;
    /// let ctrl = process.wait()?.assume_alive()?;
    /// # Ok::<_, Error>(())
    /// ```
    ///
    /// # Errors
    ///
    /// If the user assumes the process to be alive while it is not, a [`UnexpectedExit`] is
    /// returned.
    #[allow(clippy::missing_const_for_fn)]
    pub fn assume_alive(self) -> Result<TargetController<E>, UnexpectedExit> {
        match self {
            Self::Alive(ctrl) => Ok(ctrl),
            Self::Exited { tid: _, reason } => Err(UnexpectedExit { reason }),
        }
    }

    #[must_use]
    pub const fn has_exited(&self) -> bool {
        matches!(self, Self::Exited { .. })
    }

    #[must_use]
    pub const fn is_alive(&self) -> bool {
        matches!(self, Self::Alive(_))
    }

    #[must_use]
    pub const fn reason(&self) -> &Reason {
        match self {
            Self::Alive(ctrl) => ctrl.reason(),
            Self::Exited { reason, .. } => reason,
        }
    }
}

/// Strategy used by the steroid [`Thread`] to react to different [`ptrace(2)`] events such as the
/// creation of new threads, child processes, the thread about to exit, etc.
///
/// [`Thread`]: crate::thread::Thread
/// [`ptrace(2)`]: https://man7.org/linux/man-pages/man2/ptrace.2.html
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PtraceEventStrategy {
    /// Unset the corresponding option. The tracing thread will not be notified when the event
    /// occurs.
    Unset,
    /// Set the option in order to notify the tracing thread that the event occured. If a new thread
    /// or a child process has been created in the remote process and has been automatically
    /// attached by the tracing thread, it is detached.
    NotifyOnly,
    /// Set the option in order to notify the tracing thread the event occured. If a new thread or a
    /// child process has been created in the remote process, it is attached by steroid.
    Trace,
}

impl Default for PtraceEventStrategy {
    fn default() -> Self {
        Self::Unset
    }
}

/// Enumeration of all the possible [`ptrace(2)`] options.
///
/// [`ptrace(2)`]: https://man7.org/linux/man-pages/man2/ptrace.2.html
#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq)]
pub enum PtraceOption {
    TraceSysGood,
    /// Stop tracee at next fork and start tracing the forked process.
    TraceFork,
    /// Stop tracee at next vfork call and trace the vforked process.
    TraceVFork,
    /// Stop tracee at next clone call and trace the cloned process.
    TraceClone,
    /// Stop tracee at next execve call.
    TraceExec,
    /// Stop tracee at vfork completion.
    TraceVForkDone,
    /// Stop tracee at next exit call. Stops before exit commences allowing tracer to see location
    /// of exit and register states.
    TraceExit,
    /// Stop tracee when a SECCOMP_RET_TRACE rule is triggered. See [`seccomp`] for more details.
    ///
    /// [`seccomp`]: https://man7.org/linux/man-pages/man2/seccomp.2.html
    TraceSeccomp,
    /// Send a SIGKILL to the tracee if the tracer exits. This is useful for ptrace jailers to
    /// prevent tracees from escaping their control.
    ExitKill,
}

impl From<PtraceOption> for NixPtraceOptions {
    fn from(value: PtraceOption) -> Self {
        match value {
            PtraceOption::TraceSysGood => Self::PTRACE_O_TRACESYSGOOD,
            PtraceOption::TraceFork => Self::PTRACE_O_TRACEFORK,
            PtraceOption::TraceVFork => Self::PTRACE_O_TRACEVFORK,
            PtraceOption::TraceClone => Self::PTRACE_O_TRACECLONE,
            PtraceOption::TraceExec => Self::PTRACE_O_TRACEEXEC,
            PtraceOption::TraceVForkDone => Self::PTRACE_O_TRACEVFORKDONE,
            PtraceOption::TraceExit => Self::PTRACE_O_TRACEEXIT,
            PtraceOption::TraceSeccomp => Self::PTRACE_O_TRACESECCOMP,
            PtraceOption::ExitKill => Self::PTRACE_O_EXITKILL,
        }
    }
}

/// Mapping of [`ptrace(2)`] options to the associated [`strategy`][PtraceEventStrategy].
///
/// [`ptrace(2)`]: https://man7.org/linux/man-pages/man2/ptrace.2.html
#[derive(Default)]
pub struct PtraceOptionMap(HashMap<PtraceOption, PtraceEventStrategy>);

impl PtraceOptionMap {
    #[must_use]
    pub fn get(&self, option: PtraceOption) -> PtraceEventStrategy {
        self.0.get(&option).copied().unwrap_or_default()
    }

    pub fn set(
        &mut self,
        option: PtraceOption,
        strategy: PtraceEventStrategy,
    ) -> PtraceEventStrategy {
        self.0.insert(option, strategy).unwrap_or_default()
    }

    #[must_use]
    pub(crate) fn all_set_options(options: NixPtraceOptions) -> Vec<PtraceOption> {
        let matches = |constant: NixPtraceOptions| -> bool { !(options & constant).is_empty() };

        let constants = [
            (
                NixPtraceOptions::PTRACE_O_TRACESYSGOOD,
                PtraceOption::TraceSysGood,
            ),
            (
                NixPtraceOptions::PTRACE_O_TRACEFORK,
                PtraceOption::TraceFork,
            ),
            (
                NixPtraceOptions::PTRACE_O_TRACEVFORK,
                PtraceOption::TraceVFork,
            ),
            (
                NixPtraceOptions::PTRACE_O_TRACECLONE,
                PtraceOption::TraceClone,
            ),
            (
                NixPtraceOptions::PTRACE_O_TRACEEXEC,
                PtraceOption::TraceExec,
            ),
            (
                NixPtraceOptions::PTRACE_O_TRACEVFORKDONE,
                PtraceOption::TraceVForkDone,
            ),
            (
                NixPtraceOptions::PTRACE_O_TRACEEXIT,
                PtraceOption::TraceExit,
            ),
            (
                NixPtraceOptions::PTRACE_O_TRACESECCOMP,
                PtraceOption::TraceSeccomp,
            ),
            (NixPtraceOptions::PTRACE_O_EXITKILL, PtraceOption::ExitKill),
        ];

        constants
            .iter()
            .filter_map(|(nix, opt)| if matches(*nix) { Some(*opt) } else { None })
            .collect()
    }

    #[must_use]
    pub(crate) fn as_ptrace_options(&self) -> NixPtraceOptions {
        self.0
            .iter()
            .fold(NixPtraceOptions::empty(), |options, (k, v)| {
                options | v.to_ptrace_flag(*k)
            })
    }
}

impl PtraceEventStrategy {
    #[must_use]
    pub(crate) fn to_ptrace_flag(self, option: PtraceOption) -> NixPtraceOptions {
        match self {
            Self::Unset => NixPtraceOptions::empty(),
            _ => option.into(),
        }
    }
}

/// Trait to represent what is considered an executing entity in steroid.
///
/// While the name is a bit abstract, it actually corresponds to threads and processes.
pub trait Executing: Sized {
    /// Type used to store the executing entity in a [`TargetController`] while it is stopped.
    ///
    /// The main purpose of this type is to store information about all the threads when the whole
    /// process is stopped.
    type StoppedRepresentation: AsRef<Self>;
    type WaitError: Error;

    /// Get a reference to the process the executing entity belongs to.
    ///
    /// For a [`TargetProcess`], it is a reference to itself. For a [`Thread`] it is a reference to
    /// the whole process.
    ///
    /// [`Thread`]: crate::thread::Thread
    fn process(&self) -> &TargetProcess;

    /// Returns the process groud identifier of this [`Executing`] entity.
    ///
    /// The PGID is the same as the PID of the main thread of the process.
    #[must_use]
    fn pgid(&self) -> Pid {
        self.process().pid()
    }

    /// Returns the PID of this [`Executing`] entity.
    ///
    /// For [`TargetProcess`] it corresponds to the main thread's PID, i.e. the
    /// [`Executing::pgid`] entity.
    fn pid(&self) -> Pid;

    /// Set the given [`ptrace(2)`] option for the given executing entity.
    ///
    /// The old value is returned.
    ///
    /// # Errors
    ///
    /// This function calls [`ptrace(2)`] to set the options in the remote process. Any ptrace
    /// related error can thus make this function fail.
    ///
    /// [`ptrace(2)`]: https://man7.org/linux/man-pages/man2/ptrace.2.html
    fn set_ptrace_option(
        ctrl: &mut TargetController<Self>,
        option: PtraceOption,
        strategy: PtraceEventStrategy,
    ) -> Result<PtraceEventStrategy, CouldNotSetOptions>;

    /// Wait for an event from the target executing entity, such as an exit, a trap or a stop. The
    /// thread - or whole process - being stopped, a [`TargetController`] is returned, allowing to
    /// make modification to the target executing entity. If the thread exited, only the [`reason`]
    /// why it exited is returned. See [`RunningState`].
    ///
    /// # Errors
    ///
    /// This function will fail if the pid given to [`waitpid(2)`] is neither the pid of a living
    /// process nor a process the steroid client is attached to. This should not happen as a
    /// [`TargetProcess`] cannot be created from an invalid pid and [`TargetProcess::try_from`] will
    /// fail if the client cannot attach to the remote process, neither can a [`Thread`] be created
    /// from an invalid pid since its creation is handled by the parent [`Thread`]. Nonetheless, if
    /// the situation occurs due to external causes, an error is returned accordingly.
    ///
    /// # Panics
    ///
    /// Not all the options and quicks of [`waitpid(2)`] are currently supported by steroid. If the
    /// user does not try to thwart steroid's safety model, no panic should occur. Otherwise, a
    /// [`todo!`] panic will remind the developers to handle all the possible outcomes of
    /// [`waitpid(2)`].
    ///
    /// [`reason`]: crate::run::Reason
    /// [`waitpid(2)`]: https://man7.org/linux/man-pages/man2/waitpid.2.html
    /// [`TargetController`]: crate::process::TargetController
    /// [`TargetProcess::try_from`]: ../process/struct.TargetProcess.html#method.try_from
    /// [`Thread`]: crate::thread::Thread
    fn wait(self) -> Result<RunningState<Self>, Self::WaitError>;

    /// Resume the executing entity's execution, consuming the [`TargetController`]. This function
    /// consider the breakpoints and do the necessary manipulations to resume the execution
    /// correctly and put back the trap instructions.
    ///
    /// # Errors
    ///
    /// This function may try to pass over a breakpoint, consequently any manipulation of the
    /// breakpoint that may fail can make `resume` fail too. Moreover, restarting the execution
    /// involves a call to [`ptrace(2)`] which can fail itself. In summary, the possible failures
    /// are the following:
    ///
    /// - the registers could not be read or written
    /// - the function could not singlestep over the breakpoint
    /// - the memory at the breakpoint's address could not be overwritten
    /// - the breakpoint could not be removed by steroid (see [`TargetController::remove_breakpoint`])
    /// - the execution could not be restarted because of [`ptrace(2)`]
    ///
    /// [`ptrace(2)`]: https://man7.org/linux/man-pages/man2/ptrace.2.html
    fn resume(ctrl: TargetController<Self>) -> Result<Self, CouldNotResume>;
}