Crate steroid

Crate steroid 

Source
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:

  • elf: crate providing a safe interface to read ELF object files
  • gimli: crate providing features to read and write DWARF debugging format
  • capstone: bindings to the capstone library disassembly framework

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.