steroid 0.5.0

A lightweight framework for dynamic binary instrumentation
Documentation
//! The breakpoint module contains everything needed to manipulate breakpoints.
//!
//! Breakpoints are a mechanism that allows a steroid client to take the control of the remote
//! process when the latter executes the instruction at a given address. This means that a
//! breakpoint replaces the instruction at a given address by another one pausing the remote process
//! and giving back the control to the steroid client - this a trap instruction. Breakpoints in
//! steroid work roughly like breakpoints in debuggers such a GDB.
//!
//! What makes steroid's breakpoints special is the way [`Breakpoint`] objects are created and
//! handled. Breakpoints are registered into the [`TargetProcess`] they are controlling. This means
//! the process object holds a collection of all the breakpoints that exist in the remote process at
//! any given time. Moreover, in order to manipulate breakpoints anywhere in a steroid client, the
//! user is provided with [`BreakpointId`], an abstract ID for a breakpoint, that enables the user
//! to access the breakpoint in the process object when it is possible for them to do so. In
//! addition, it ensures that no [`Breakpoint`] is freely available while being also removed from
//! the process.
//!
//! Breakpoints are created using the function [`breakpoint`].
//!
//! ```
//! use std::cell::Ref;
//! # use std::path::PathBuf;
//! # use anyhow::Error;
//! # use steroid::process::spawn_process;
//! # use steroid::run::Executing;
//! # use steroid::breakpoint::{breakpoint, Breakpoint, Mode};
//! #
//! # let mut SOME_PROCESS = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
//! # SOME_PROCESS.push("resources/test/say_hello_no_pie");
//! # let SOME_ADDRESS: usize = 0x401126;
//! let mut process = spawn_process(SOME_PROCESS, [""])?;
//! let mut ctrl = process.wait()?.assume_alive()?;
//!
//! // Note that the process must be stopped to put a breakpoint.
//! let brk: Ref<Breakpoint> = breakpoint(&mut ctrl, SOME_ADDRESS, Mode::OneShot)?;
//! # Ok::<(), Error>(())
//! ```
//!
//! Once a breakpoint is set, its usage is relatively transparent. The user must use the method
//! [`wait`] of [`TargetProcess`] to wait for the process to be paused, eventually on the
//! breakpoint. At this point, the [`TargetController`] returned by the [`wait`] method will have an
//! associated breakpoint set:
//!
//! ```
//! # use std::path::PathBuf;
//! # use anyhow::Error;
//! # use steroid::process::spawn_process;
//! # use steroid::run::Executing;
//! # use steroid::breakpoint::{breakpoint, Breakpoint, Mode};
//! #
//! # let mut SOME_PROCESS = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
//! # SOME_PROCESS.push("resources/test/say_hello_no_pie");
//! # let SOME_ADDRESS: usize = 0x401126;
//! # let process = spawn_process(SOME_PROCESS, [""])?;
//! let mut ctrl = process.wait()?.assume_alive()?;
//! breakpoint(&mut ctrl, SOME_ADDRESS, Mode::OneShot)?;
//! let process = ctrl.resume()?;
//! let mut ctrl_brk = process.wait()?.assume_alive()?;
//! assert!(ctrl_brk.breakpoint().is_some());
//! # Ok::<(), Error>(())
//! ```
//!
//! When the process execution is resumed, using [`resume`] or [`singlestep`], the controller takes
//! care of the breakpoint. If the execution was paused on a breakpoint, the original instruction is
//! written back in order to be executed by the remote process as it should be. Afterwards, the
//! breakpoint is set again, if it is a [`Mode::Persistent`] breakpoint.
//!
//! [`TargetProcess`]: ../process/struct.TargetProcess.html
//! [`TargetController`]: ../process/struct.TargetController.html
//! [`wait`]: ../process/struct.TargetProcess.html#method.wait
//! [`resume`]: ../process/struct.TargetController.html#method.resume
//! [`singlestep`]: ../process/struct.TargetController.html#method.singlestep

use std::cell::Ref;
use std::fmt::Debug;
use std::sync::atomic::{AtomicUsize, Ordering};

use crate::error::CouldNotCreateBreakpoint;
use crate::process::TargetController;
use crate::run::Executing;

/// x86-64 opcode for the trap instruction that enables the use of breakpoints.
pub const TRAP_OPCODE: u8 = 0xCC;

/// Type of breakpoint that one wants to use.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Mode {
    /// Breakpoint that is used once and is not placed back after the process stopped at it.
    OneShot,
    /// Breakpoint that is placed back when the process restarts after encountering it. This is the
    /// default behavior of debuggers' breakpoints. Each time the process will try to execute the
    /// instruction at the breakpoint's address, it will be stopped.
    Persistent,
}

/// A breakpoint is a mechanism that enables a program, such as a debugger, to pause the execution
/// of the target process in order to regain its control once the process' instruction pointer
/// reaches the breakpoint's address.
#[derive(Debug, PartialEq, Eq)]
pub struct Breakpoint {
    /// The address at which the breakpoint has been placed.
    address: usize,
    /// The byte that was overwritten to place the breakpoint. It is written again in order to
    /// perform a singlestep execution of the program to pass the breakpoint and maybe overwritten
    /// again to place back the breakpoint.
    saved_byte: u8,
    /// The type of breakpoint: oneshot or persisten.
    mode: Mode,
    /// The unique id of the breakpoint.
    id: BreakpointId,
}

static BREAKPOINT_ID_COUNTER: AtomicUsize = AtomicUsize::new(1);

pub(crate) fn new_breakpoint_id() -> BreakpointId {
    BreakpointId {
        id: BREAKPOINT_ID_COUNTER.fetch_add(1, Ordering::Relaxed),
    }
}

/// Identifier token to access a breakpoint in a [`TargetProcess`].
///
/// [`TargetProcess`]: ../process/struct.TargetProcess.html
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
#[allow(clippy::module_name_repetitions)] // Id does not make much sense by itself.
pub struct BreakpointId {
    pub(crate) id: usize,
}

/// Create a new breakpoint at the given address in the remote process. When this function returns,
/// the breakpoint has been set and if the process execution encounters it, it will be paused.
///
/// # Errors
///
/// A [`CouldNotCreateBreakpoint::BreakpointAlreadyExists`] is returned if a breakpoint has already
/// been registered into the remote process at the same address. Otherwise, if any IO error occurs,
/// the corresponding error is returned:
///
/// - [`CouldNotCreateBreakpoint::CouldNotReadMemory`] if the address is not valid
/// - [`CouldNotCreateBreakpoint::CouldNotWriteTrap`] if the trap instruction could not be written
/// - [`CouldNotCreateBreakpoint::ProcessKilled`] if the process has already been killed
pub fn breakpoint<E>(
    ctrl: &mut TargetController<E>,
    address: usize,
    mode: Mode,
) -> Result<Ref<Breakpoint>, CouldNotCreateBreakpoint>
where
    E: Executing,
{
    let saved_byte = ctrl.read(address, 1)?;
    ctrl.write(address, &[TRAP_OPCODE])?;

    let brk = Breakpoint {
        address,
        saved_byte: saved_byte[0],
        mode,
        id: new_breakpoint_id(),
    };

    ctrl.register_breakpoint(brk)
}

impl Breakpoint {
    /// Return the address in the remote process at which the breakpoint is set.
    #[must_use]
    pub const fn address(&self) -> usize {
        self.address
    }

    /// Return the type of the breakpoint.
    #[must_use]
    pub const fn mode(&self) -> Mode {
        self.mode
    }

    /// Return the byte that was originally written in the remote process before the trap
    /// instruction overrid it.
    #[must_use]
    pub const fn saved_byte(&self) -> u8 {
        self.saved_byte
    }

    /// Return the breakpoint ID of the pointer.
    #[must_use]
    pub const fn id(&self) -> BreakpointId {
        self.id
    }
}

#[cfg(test)]
mod tests {
    use std::path::PathBuf;

    use anyhow::Error as AnyError;

    use crate::process::spawn_process;
    use crate::run::Reason;

    use super::*;

    const MAIN_FUNCTION: usize = 0x0040_113c;
    const HELLO_FUNCTION: usize = 0x0040_1126;

    #[test]
    fn use_one_shot_breakpoint() -> Result<(), AnyError> {
        let mut path_buf = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
        path_buf.push("resources/test/say_hello_no_pie");
        let process = spawn_process::<_, _, &str>(path_buf, vec![])?;
        let mut ctrl_start = process.wait()?.assume_alive()?;

        {
            let brk = breakpoint(&mut ctrl_start, MAIN_FUNCTION, Mode::OneShot)?;
            let id = brk.id();

            assert_eq!(
                *brk,
                Breakpoint {
                    address: MAIN_FUNCTION,
                    saved_byte: 0x55,
                    mode: Mode::OneShot,
                    id
                }
            );
        }

        let process = ctrl_start.resume()?;

        let ctrl_main = process.wait()?.assume_alive()?;
        let regs = ctrl_main.get_registers()?;

        assert_eq!(MAIN_FUNCTION + 1, regs.rip as usize);

        let process = ctrl_main.resume()?;
        let state = process.wait()?;
        assert!(state.has_exited(), "{}", state.reason());

        Ok(())
    }

    #[test]
    fn resume_and_do_things() -> Result<(), AnyError> {
        let mut path_buf = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
        path_buf.push("resources/test/say_hello_no_pie");
        let process = spawn_process::<_, _, &str>(path_buf, vec![])?;
        let mut ctrl_start = process.wait()?.assume_alive()?;

        {
            let brk_main = breakpoint(&mut ctrl_start, MAIN_FUNCTION, Mode::Persistent)?;
            let id_main = brk_main.id();

            assert_eq!(
                *brk_main,
                Breakpoint {
                    address: MAIN_FUNCTION,
                    saved_byte: 0x55,
                    mode: Mode::Persistent,
                    id: id_main
                }
            );
        }

        {
            let brk_hello = breakpoint(&mut ctrl_start, HELLO_FUNCTION, Mode::OneShot)?;
            let id_hello = brk_hello.id();

            assert_eq!(
                *brk_hello,
                Breakpoint {
                    address: HELLO_FUNCTION,
                    saved_byte: 0x55,
                    mode: Mode::OneShot,
                    id: id_hello
                }
            );
        }

        let process = ctrl_start.resume()?;

        let ctrl_main = process.wait()?.assume_alive()?;
        let regs = ctrl_main.get_registers()?;

        assert_eq!(MAIN_FUNCTION + 1, regs.rip as usize);
        let process = ctrl_main.resume()?;

        let ctrl_hello = process.wait()?.assume_alive()?;
        let regs = ctrl_hello.get_registers()?;

        assert_eq!(HELLO_FUNCTION + 1, regs.rip as usize);
        let process = ctrl_hello.resume()?;

        let state = process.wait()?;
        assert!(state.has_exited(), "{}", state.reason());

        Ok(())
    }

    #[test]
    fn new_breakpoint_is_registered() -> Result<(), AnyError> {
        let mut path_buf = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
        path_buf.push("resources/test/say_hello_no_pie");
        let process = spawn_process::<_, _, &str>(path_buf, vec![])?;
        let mut ctrl = process.wait()?.assume_alive()?;

        let id = breakpoint(&mut ctrl, MAIN_FUNCTION, Mode::OneShot)?.id();

        let brks: Vec<Ref<Breakpoint>> = ctrl.process().breakpoints().collect();
        assert_eq!(brks.len(), 1);
        assert_eq!(brks[0].id(), id);

        Ok(())
    }

    #[test]
    fn singlestep_over_breakpoint() -> Result<(), AnyError> {
        let mut path_buf = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
        path_buf.push("resources/test/say_hello_no_pie");
        let process = spawn_process::<_, _, &str>(path_buf, vec![])?;
        let mut ctrl_start = process.wait()?.assume_alive()?;

        breakpoint(&mut ctrl_start, MAIN_FUNCTION, Mode::OneShot)?;

        let process = ctrl_start.resume()?;
        let mut ctrl = process.wait()?.assume_alive()?;

        let first_regs = ctrl.get_registers()?;
        ctrl.singlestep()?;

        let mut second_regs = ctrl.get_registers()?;
        // Reverse the `push %rbp`
        second_regs.rsp += 8;

        assert_eq!(first_regs, second_regs);

        Ok(())
    }

    #[test]
    fn resume_after_breakpoint_removal() -> Result<(), AnyError> {
        let mut path_buf = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
        path_buf.push("resources/test/say_hello_no_pie");
        let process = spawn_process::<_, _, &str>(path_buf, vec![])?;
        let mut ctrl_start = process.wait()?.assume_alive()?;

        let id = breakpoint(&mut ctrl_start, MAIN_FUNCTION, Mode::Persistent)?.id();

        let process = ctrl_start.resume()?;

        let mut ctrl = process.wait()?.assume_alive()?;
        let regs = ctrl.get_registers()?;

        assert_eq!(*ctrl.reason(), Reason::Breakpoint(id));
        assert_eq!(regs.rip as usize - 1, MAIN_FUNCTION);
        assert!(ctrl.breakpoint().is_some());
        assert_eq!(ctrl.process().breakpoints().count(), 1);

        {
            let brk_opt = ctrl.breakpoint();
            assert!(brk_opt.is_some());
            let brk = brk_opt.unwrap();
            assert_eq!(brk.id(), id);
            assert_eq!(brk.saved_byte(), 0x55);
            assert_eq!(brk.address(), MAIN_FUNCTION);
        }

        ctrl.remove_breakpoint(id)?;

        let bytes = ctrl.read(MAIN_FUNCTION, 4)?;
        assert_eq!(bytes, vec![0x55, 0x48, 0x89, 0xe5]);
        assert_eq!(ctrl.process().breakpoints().count(), 0);

        let process = ctrl.resume()?;
        assert_eq!(process.breakpoints().count(), 0);

        let state = process.wait()?;
        assert!(state.has_exited(), "{}", state.reason());

        Ok(())
    }
}