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
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
//! The error module contains all the error types used by steroid.

use std::ffi::NulError;
use std::io::Error as IOError;
use std::path::PathBuf;

use thiserror::Error;

use crate::breakpoint::BreakpointId;
use crate::mapping::{PermissionMatcher, Permissions};
use crate::process::Pid;
use crate::run::{PtraceOption, Reason};

/// Error occuring when trying to allocate a buffer in the remote process address space.
#[derive(Debug, Error)]
pub enum CouldNotAllocate {
    /// The call to mmap could not be performed.
    #[error(transparent)]
    CouldNotPerformSyscall(#[from] CouldNotResume),
    /// The call to mmap returned an error.
    #[error("remote mmap in process (PID: {pid}) failed with error: {source}")]
    MmapFailed {
        pid: Pid,
        #[source]
        source: IOError,
    },
}

/// Error occuring when trying to create an asynchronous reader or writer.
///
/// This error type is raised by [`AsyncReader::try_new`] or [`AsyncWriter::try_new`] that does not
/// try to change the permissions of the [`MemoryMapping`] in remote process.
///
/// [`AsyncReader::try_new`]: ../buffer/struct.AsyncReader.html#method.try_new
/// [`AsyncWriter::try_new`]: ../buffer/struct.AsyncWriter.html#method.try_new
/// [`MemoryMapping`]: ../mapping/struct.MemoryMapping.html
#[derive(Debug, Error)]
pub enum CouldNotCreateAsyncReaderWriter {
    #[error("the address range {start:#x}-{end:#x} is not mapped in process (PID: {pid}) memory")]
    InvalidAddressRange { pid: Pid, start: usize, end: usize },

    #[error("expected a {expected} memory mapping, but it was {got}")]
    PermissionsMismatch {
        expected: PermissionMatcher,
        got: Permissions,
    },

    #[error(transparent)]
    CouldNotReadMemoryMapping(#[from] MappingError),
}

/// Error occuring when trying to attach to a running process.
#[derive(Error, Debug)]
pub enum CouldNotAttachToPid {
    /// The executable file from which the process was created was not found.
    #[error("executable file `{0}` not found")]
    ExecutableNotFound(PathBuf),
    /// The file `/proc/{pid}/cmdline` was not found.
    #[error("command-line file `{0}` not found")]
    CmdlineFileNotFound(PathBuf),
    /// The steroid client could not attach to the remote process. The most common reason is that
    /// another process is already attached to the target process.
    #[error("could not attach process (PID: {pid}), error: {source}")]
    CouldNotAttach {
        pid: Pid,
        #[source]
        source: IOError,
    },
    /// The remote process could not be stopped by the steroid client.
    #[error("could not stop process (PID: {pid}), error: {source}")]
    CouldNotStop {
        pid: Pid,
        #[source]
        source: IOError,
    },
    /// The steroid client could not resume the remote process' execution.
    #[error("could not restart process (PID: {pid}), error: {source}")]
    CouldNotRestart {
        pid: Pid,
        #[source]
        source: IOError,
    },
    /// The steroid client could not read the `/proc/{pid}/cmdline` file.
    #[error("could not read `/proc/{pid}/cmdline`, error: {source}")]
    CouldNotReadCmdLineFile {
        pid: Pid,
        #[source]
        source: IOError,
    },
    /// No process was found with the given PID.
    #[error("process (PID: {pid}) not found")]
    ProcessNotFound { pid: Pid },
}

/// Error occuring when trying to create a breakpoint.
#[derive(Debug, Error)]
pub enum CouldNotCreateBreakpoint {
    #[error(transparent)]
    CouldNotReadMemory(#[from] CouldNotRead),
    #[error(transparent)]
    CouldNotWriteTrap(#[from] CouldNotWrite),
    #[error("a breakpoint already exist in process (PID: {pid} at address {address:#x}")]
    BreakpointAlreadyExists { pid: Pid, address: usize },
    #[error("process (PID: {pid}) has already been killed: {source}")]
    ProcessKilled {
        pid: Pid,
        #[source]
        source: IOError,
    },
}

impl From<ReadWriteError> for CouldNotCreateBreakpoint {
    fn from(value: ReadWriteError) -> Self {
        match value {
            ReadWriteError::CouldNotRead(e) => Self::from(e),
            ReadWriteError::CouldNotWrite(e) => Self::from(e),
            ReadWriteError::ProcessKilled { pid, source } => Self::ProcessKilled { pid, source },
        }
    }
}

#[derive(Debug, Error)]
#[error("could not get the signal info about process (PID: {pid}), errno: {source}")]
pub struct CouldNotGetSignalInfo {
    pub pid: Pid,
    #[source]
    pub source: IOError,
}

#[derive(Debug, Error)]
pub enum CouldNotDetermineState {
    #[error(transparent)]
    CouldNotReadStatFile(#[from] IOError),
    #[error(transparent)]
    CouldNotReadRegisters(#[from] CouldNotReadRegisters),
    #[error(transparent)]
    CouldNotGetSignalInfo(#[from] CouldNotGetSignalInfo),
}

/// Error occuring when trying to read from the remote process' address space.
#[derive(Debug, Error)]
#[error("could not read memory at address {address} in process (PID: {pid}), errno: {source}")]
pub struct CouldNotRead {
    pub address: usize,
    pub pid: Pid,
    #[source]
    pub source: IOError,
}

/// Error occuring when trying to write in the remote process' address space.
#[derive(Debug, Error)]
#[error("could not write memory at address {address} in process (PID: {pid}), errno: {source}")]
pub struct CouldNotWrite {
    pub address: usize,
    pub pid: Pid,
    #[source]
    pub source: IOError,
}

/// Error occuring when trying to read the remote process' registers.
#[derive(Debug, Error)]
#[error("could not read registers from process (PID: {pid}), errno: {source}")]
pub struct CouldNotReadRegisters {
    pub pid: Pid,
    #[source]
    pub source: IOError,
}

/// Error occuring when trying to write the remote process' registers.
#[derive(Debug, Error)]
#[error("could not write registers into process (PID: {pid}), errno: {source}")]
pub struct CouldNotWriteRegisters {
    pub pid: Pid,
    #[source]
    pub source: IOError,
}

/// Error when trying to deallocate a buffer from a remote process.
#[derive(Debug, Error)]
pub enum CouldNotDeallocate {
    /// The system call could not be done in the remote process.
    #[error(transparent)]
    CouldNotPerformSyscall(#[from] CouldNotResume),
    /// [`munmap(2)`] failed to unmap the buffer from the process' address space.
    ///
    /// [`munmap(2)`]: https://man7.org/linux/man-pages/man2/mmap.2.html
    #[error("remote munmap in process (PID: {pid}) failed with error: {source}")]
    MunmapFailed {
        pid: Pid,
        #[source]
        source: IOError,
    },
}

/// Error occuring while trying to execute a program.
#[derive(Debug, Error)]
pub enum CouldNotExecute {
    /// The steroid client could not fork.
    #[error("could not fork process: {source}")]
    CouldNotFork {
        #[source]
        source: IOError,
    },
    /// An argument given to the soon-to-be-executed process contained a null byte, which is valid
    /// in a rust [`String`] but not in a [C string].
    ///
    /// [C string]: std::ffi::CString
    #[error("argument `{string:?}` contained a null byte at `{}`", source.nul_position())]
    NullInString {
        string: String,
        #[source]
        source: NulError,
    },
    /// The executable file was not found.
    #[error("executable file `{0}` not found")]
    ExecutableNotFound(PathBuf),
    /// The remote process could not be traced.
    #[error("could not trace process (PID: {pid}), errno: {source}")]
    CouldNotTrace {
        pid: Pid,
        #[source]
        source: IOError,
    },
    /// The path to the executable file is not a valid utf8 string.
    #[error("invalid UTF-8 path `{0}`")]
    InvalidUTF8Path(PathBuf),
}

/// Error occuring when trying to access the controller of an exited process.
#[derive(Debug, Error, PartialEq, Eq)]
#[error("the process has exited unexpectedly: {reason}")]
pub struct UnexpectedExit {
    pub reason: Reason,
}

/// Error type of the mapping module. It can be either a parsing error or an IO error.
#[derive(Debug, Error)]
#[allow(clippy::module_name_repetitions)]
pub enum MappingError {
    #[error(transparent)]
    IO(#[from] IOError),
    #[error(transparent)]
    Parse(#[from] CouldNotParseMappingFile),
}

/// Error in the parsing of the maps file.
#[derive(Clone, Copy, Debug, Error, PartialEq, Eq)]
pub enum CouldNotParseMappingFile {
    #[error("could not parse mapping' address range")]
    AddressRange,
    #[error("could not parse mapping' device")]
    Device,
    #[error("could not parse mapping' end address")]
    EndAddress,
    #[error("could not parse mapping' inode")]
    Inode,
    #[error("could not parse mapping' offset")]
    Offset,
    #[error("could not parse mapping' permissions")]
    Permissions,
    #[error("could not parse mapping' stack thread ID")]
    StackTID,
    #[error("could not parse mapping' start address")]
    StartAddress,
    #[error("unexpected end of line")]
    UnexpectedEndOfLine,
}

impl CouldNotParseMappingFile {
    /// Create a [`Result`] from the given option, returning [`UnexpectedEndOfLine`]
    /// when it is [`None`].
    ///
    /// [`UnexpectedEndOfLine`]: CouldNotParseMappingFile::UnexpectedEndOfLine
    #[allow(clippy::missing_errors_doc)]
    pub fn from_option<T>(value: Option<T>) -> Result<T, Self> {
        value.map_or_else(|| Err(Self::UnexpectedEndOfLine), |v| Ok(v))
    }
}

/// Type for errors [`mprotect(2)`] may encounter when called to protect a buffer.
#[derive(Debug, Error)]
#[allow(clippy::module_name_repetitions)]
pub enum MprotectError {
    /// The pointer is either invalid or not a multiple of the system page size.
    #[error("{address:#x} is not a valid pointer, or not a multiple of the system page size")]
    InvalidPointer { pid: Pid, address: usize },

    /// The memory mapping is specified as both growing up and growing down, which is impossible.
    #[error("protections specified as both grows-up and grows-down")]
    GrowsUpAndDown { perm: Permissions },

    /// The maximum amount allowed of mappings has been reached trying to split a memory mapping
    /// into two to three new ones with different permissions.
    ///
    /// For instance, if the user wants to protect as writable a portion in the middle of a large
    /// readable-only mapping. This creates three mappings: a first one, readable ; a second one,
    /// readable and writable ; and the last one, readable.
    #[error("maximum amount allowed of mappings reached")]
    TooManyMappings,
}

/// Error occuring when trying to change the permissions of a [`MemoryMapping`] in a remote process.
///
/// This error type is raised either by [`AsyncReader::restore_permissions`] or [`AsyncReader::new`]
/// that changes the permissions as well.
///
/// [`AsyncReader::new`]: ../buffer/struct.AsyncReader.html#method.new
/// [`AsyncReader::restore_permissions`]: ../buffer/struct.AsyncReader.html#method.restore_permissions
/// [`MemoryMapping`]: ../mapping/struct.MemoryMapping.html
#[derive(Debug, Error)]
pub enum CouldNotRestorePermissions {
    #[error("the address range {start:#x}-{end:#x} is not mapped in process (PID: {pid}) memory")]
    InvalidAddressRange { pid: Pid, start: usize, end: usize },

    #[error("expected a {expected} memory mapping, but it was {got}")]
    PermissionsMismatch {
        expected: PermissionMatcher,
        got: Permissions,
    },

    #[error(transparent)]
    CouldNotReadMemoryMapping(#[from] MappingError),

    #[error("mprotect in process (PID: {pid}) failed with error: {source}")]
    MprotectFailed {
        pid: Pid,
        #[source]
        source: MprotectError,
    },

    #[error(transparent)]
    CouldNotRestart(#[from] CouldNotResume),
}

#[derive(Debug, Error)]
#[error("could not set ptrace options to process (PID: {pid}), error: {source}")]
pub struct CouldNotSetOptions {
    pub pid: Pid,
    pub options: Vec<PtraceOption>,
    #[source]
    pub source: IOError,
}

/// Error when trying to read or write in the remote process' address space.
#[derive(Debug, Error)]
#[allow(clippy::module_name_repetitions)]
pub enum ReadWriteError {
    #[error(transparent)]
    CouldNotRead(#[from] CouldNotRead),
    #[error(transparent)]
    CouldNotWrite(#[from] CouldNotWrite),
    #[error("process (PID: {pid}) has been killed before the operation finished")]
    ProcessKilled {
        pid: Pid,
        #[source]
        source: IOError,
    },
}

/// Error when trying to read or write the remote process' registers.
#[derive(Debug, Error)]
pub enum CouldNotReadWriteRegister {
    #[error(transparent)]
    CouldNotRead(#[from] CouldNotReadRegisters),
    #[error(transparent)]
    CouldNotWrite(#[from] CouldNotWriteRegisters),
}

/// Error occuring when trying to create a remote view in a remote process.
#[derive(Debug, Error)]
pub enum CouldNotCreateRemoteView {
    #[error("the address range {start:#x}-{end:#x} is not mapped in process (PID: {pid}) memory")]
    InvalidAddressRange { pid: Pid, start: usize, end: usize },

    #[error(transparent)]
    CouldNotReadMemoryMapping(#[from] MappingError),
}

/// Error occuring when trying to remove a breakpoint from a remote process.
#[derive(Debug, Error)]
pub enum CouldNotRemoveBreakpoint {
    /// No breakpoint with the specified ID exists in the remote process.
    #[error("breakpoint {id:?} does not exist in process (PID: {pid})")]
    BreakpointNotFound { id: BreakpointId, pid: Pid },
    /// An error occured while manipulating the remote process to remove the breakpoint.
    #[error(transparent)]
    WriteError {
        #[from]
        source: ReadWriteError,
    },
}

/// Error occuring when trying to resume the remote process' execution.
///
/// When trying to resume an execution, it is possible that steroid manages breakpoints. As such,
/// many types of errror can occur.
#[derive(Debug, Error)]
pub enum CouldNotResume {
    #[error(transparent)]
    CouldNotRead(#[from] CouldNotRead),
    #[error(transparent)]
    CouldNotWrite(#[from] CouldNotWrite),
    /// The steroid client could not actually restart the remote process. See [`ptrace(2)`].
    ///
    /// [`ptrace`]: https://man7.org/linux/man-pages/man2/ptrace.2.html
    #[error("could not restart process (PID: {pid}), errno: {source}")]
    CouldNotRestart {
        pid: Pid,
        #[source]
        source: IOError,
    },
    #[error(transparent)]
    CouldNotRemoveBreakpoint(#[from] CouldNotRemoveBreakpoint),
    #[error(transparent)]
    CouldNotReadRegisters(#[from] CouldNotReadRegisters),
    #[error(transparent)]
    CouldNotWriteRegisters(#[from] CouldNotWriteRegisters),
    #[error(transparent)]
    CouldNotStop(#[from] CouldNotWait),
}

impl From<CouldNotReadWriteRegister> for CouldNotResume {
    fn from(value: CouldNotReadWriteRegister) -> Self {
        match value {
            CouldNotReadWriteRegister::CouldNotRead(e) => Self::CouldNotReadRegisters(e),
            CouldNotReadWriteRegister::CouldNotWrite(e) => Self::CouldNotWriteRegisters(e),
        }
    }
}

impl From<ReadWriteError> for CouldNotResume {
    fn from(value: ReadWriteError) -> Self {
        match value {
            ReadWriteError::CouldNotRead(e) => Self::CouldNotRead(e),
            ReadWriteError::CouldNotWrite(e) => Self::CouldNotWrite(e),
            ReadWriteError::ProcessKilled { pid, source } => Self::CouldNotRestart { pid, source },
        }
    }
}

/// Error occuring when trying to create a synchronous reader.
///
/// Note that, unlike [`AsyncReader`] or [`AsyncWriter`], the synchronous reader does not need
/// special permissions over the memory mapping. Only the validity of the address range must be
/// checked.
///
/// [`AsyncReader`]: ../buffer/struct.AsyncReader.html
/// [`AsyncWriter`]: ../buffer/struct.AsyncWriter.html
#[derive(Debug, Error)]
pub enum CouldNotCreateSyncReaderWriter {
    #[error("the address range {start:#x}-{end:#x} is not mapped in process (PID: {pid}) memory")]
    InvalidAddressRange { pid: Pid, start: usize, end: usize },

    #[error(transparent)]
    CouldNotReadMemoryMapping(#[from] MappingError),
}

/// Error occuring when waiting for a remote thread to stop.
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum CouldNotWait {
    /// No process with the given PID exists.
    #[error("thread (TID: {pid}) does not exists, errno: {source}")]
    ProcessNotFound {
        pid: Pid,
        #[source]
        source: IOError,
    },
}

#[derive(Debug, Error)]
pub enum CouldNotWaitForProcess {
    #[error(transparent)]
    CouldNotWaitForThread(#[from] CouldNotWait),
    #[error(transparent)]
    CouldNotDetermineState(#[from] CouldNotDetermineState),
}

/// Error occuring when trying to access a thread in a [`TargetProcess`] using a wrong TID.
///
/// [`TargetProcess`]: crate::process::TargetProcess
#[derive(Debug, Error)]
#[error("could not find thread (TID: {tid}) in process (PID: {pid})")]
pub struct ThreadNotFound {
    pub pid: Pid,
    pub tid: Pid,
}