Expand description
Steroid is a dynamic binary instrumentation library with a strong emphasis on safety.
This library aims at instrumenting other running processes, giving the user a high-level
interface to system calls such as ptrace(2) and the possibilities it gives. Steroid uses
lifetimes and mutability a lot in order to make sense of a remote process’ running
state. The two main data types are TargetProcess and TargetController.
§The Process and Controller tandem
The former represents a running process. An instance of this type is normally mutably available
while a process in running. This means the process can be stopped. Once a process is stopped
using wait, a RunningState is returned. This object tells the user whether the process
is stopped but still alive or if it is dead because it has exited or was killed.
match process.wait()? {
RunningState::Alive(_) => println!("We can manipulate the stopped process"),
RunningState::Exited { reason, .. } => println!("The process has exited: {}", reason),
}If the process is just stopped, the user is provided with a TargetController. This
controller takes the ownership of the process, forbiding any manipulation of the
TargetProcess that requires the process running. On the other hand, the controller enables
any manipulation that requires a stopped process, such as reading the process’ registers. The
method resume of the controller consumes the latter and returns the TargetProcess,
allowing the process to be manipulated again. A steroid client usually consists of alternating
manipulations of a process and its successive controllers.
use steroid::process::spawn_process;
use steroid::run::{Executing, RunningState, Reason};
let process = spawn_process("/bin/ls", ["-l"])?;
if let RunningState::Alive(mut ctrl) = process.wait()? {
// The process is now inaccessible as mutable.
let regs = ctrl.get_registers()?;
println!("{:#x?}", regs);
let process = ctrl.resume()?;
// The controller is dead, the process is available again.
let pid = process.pid();
// The wait method returns the process state:
// * alive (with a controller)
// * dead (with the reason why it died)
let state = process.wait()?;
assert!(state.has_exited());
}In some cases, the user knows that when calling wait, the process must be stopped and an
exit must be considered an error. Instead of using match and writing complicated code,
RunningState has a method assume_alive that returns a TargetController if the
process is indeed alive or fails with an error if it has exited.
use steroid::process::spawn_process;
use steroid::run::Executing;
let process = spawn_process("/bin/ls", ["-l"])?;
let mut ctrl = process.wait()?.assume_alive()?;
// The process is now inaccessible as mutable.
let regs = ctrl.get_registers()?;
println!("{:#x?}", regs);§ptrace implies !Send + !Sync
Steroid heavily uses the system call ptrace(2) to control remote processes. It is important
to understand that a tracee is traced by a tracer thread. This means that in a steroid client,
only the thread that created a TargetProcess, a Thread or a TargetController can
actually use them. For this reason, these three types are marked as both !Send and
!Sync.
§Remote system calls
Steroid has a whole module dedicated to call syscalls from the remote process it is controlling. The insight is to take advantage of the process being stopped at some point to make it execute pieces of code in its own context. For instance, it would be possible to make the remote process allocate memory for future use by the steroid client:
use nix::libc::{MAP_ANONYMOUS, MAP_PRIVATE, PROT_READ, PROT_WRITE};
use steroid::syscall;
let mut ctrl = process.wait()?.assume_alive()?;
let flags = (MAP_PRIVATE | MAP_ANONYMOUS) as u64;
let prot = (PROT_READ | PROT_WRITE) as u64; // permissions: read and write
let size = 1000;
let fd = -1_i64 as u64; // fd must be -1, but syscall::mmap takes u64. It's ok here.
let address = syscall::mmap(&mut ctrl, 0, size, prot, flags, fd, 0)?;
println!("New page mapped at address {:#x}", address);
let process = ctrl.resume()?;§Limitations and useful crates
Steroid is a dynamic binary instrumentation library that relies heavily on Linux features such
as ptrace(2) and procfs(5). Therefore, it will only work on Linux. Some efforts can be
made to port it to Unix operating systems such as the main BSDs. In addition to that, steroid
only targets x86_64. It may be possible to port it to different architectures but it is far
from being a priority right now.
Steroid is still very early in its development and does not provide many features. Nonetheless some excellent crates complete it very well, giving missing features that fall out of the strict scope of steroid’s goals:
Modules§
- breakpoint
- The breakpoint module contains everything needed to manipulate breakpoints.
- buffer
- The buffer module contains high-level constructs to perform IO in the remote process’ memory.
- error
- The error module contains all the error types used by steroid.
- mapping
- The mapping module provides convenient data types to manipulate the memory mapping of a process.
- process
- The process module contains everything needed to manipulate a remote process.
- run
- The run module contains traits and types to manipulate running and stopped execution entities.
- syscall
- The syscall module contains functions to execute system calls in the target process.
- thread
- The thread module contains everything related to the manipulation of threads.
Macros§
- update_
registers - Take a controller and use it to update certain registers within the target process. The following example gives an insight of the syntax used in this macro.
- with_
registers - Take a controller and some register assignments and then apply the given function using these registers. The idea is to be able to execute code in the target process using the given registers and then write back the registers as they were before the manipulation. Note that the instruction pointer is saved as well as the other registers.