Skip to content

Commit

Permalink
Merge pull request #108 from Arthamys/breakpoints
Browse files Browse the repository at this point in the history
Breakpoint support on Linux
  • Loading branch information
bjorn3 committed Sep 30, 2020
2 parents dff82e4 + f555d99 commit e4eb3f3
Show file tree
Hide file tree
Showing 8 changed files with 445 additions and 5 deletions.
44 changes: 44 additions & 0 deletions examples/repl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ mod example {
Stepi|si: (),
/// Continue the program being debugged
Continue|cont: (),
/// Set a breakpoint at symbol or address
Breakpoint|b: String,
// FIXME move the `read:` part before the `--` in the help
/// read: List registers and their content for the current stack frame
Registers|regs: String,
Expand Down Expand Up @@ -151,6 +153,14 @@ mod example {
}
}

fn mut_remote(&mut self) -> Result<&mut LinuxTarget, Box<dyn std::error::Error>> {
if let Some(remote) = &mut self.remote {
Ok(remote)
} else {
Err("No running process".to_string().into())
}
}

fn set_remote(&mut self, remote: LinuxTarget) {
// FIXME kill/detach old remote
self.remote = Some(remote);
Expand Down Expand Up @@ -335,6 +345,7 @@ mod example {
context.remote = None;
}
ReplCommand::Kill(()) => println!("{:?}", context.remote()?.kill()?),
ReplCommand::Breakpoint(location) => set_breakpoint(context, &location)?,
ReplCommand::Stepi(()) => {
println!("{:?}", context.remote()?.step()?);
return print_source_for_top_of_stack_symbol(context, 3);
Expand Down Expand Up @@ -385,6 +396,39 @@ mod example {
Ok(())
}

fn set_breakpoint(
context: &mut Context,
location: &str,
) -> Result<(), Box<dyn std::error::Error>> {
context.load_debuginfo_if_necessary()?;

if let Ok(addr) = {
usize::from_str_radix(&location, 10)
.map(|addr| addr as usize)
.map_err(|e| Box::new(e))
.or_else(|_e| {
if location.starts_with("0x") {
let raw_num = location.trim_start_matches("0x");
usize::from_str_radix(raw_num, 16)
.map(|addr| addr as usize)
.map_err(|_e| Box::new(format!("Invalid address format.")))
} else {
context
.debuginfo()
.get_symbol_address(&location)
.ok_or(Box::new(format!("No such symbol {}", location)))
}
})
} {
context.mut_remote()?.set_breakpoint(addr)?;
} else {
Err(format!(
"Breakpoints must be set on a symbol or at a given address. For example `b main` or `b 0x0000555555559394` or even `b 93824992252820`"
))?
}
Ok(())
}

/// Patch the `pause` instruction inside a function called `breakpoint` to be a
/// breakpoint. This is useful while we don't have support for setting breakpoints at
/// runtime yet.
Expand Down
125 changes: 122 additions & 3 deletions src/target/linux.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
mod hardware_breakpoint;
mod memory;
mod readmem;
mod software_breakpoint;
mod writemem;

use crate::target::thread::Thread;
use crate::target::unix::{self, UnixTarget};
use nix::sys::ptrace;
use nix::sys::wait::{waitpid, WaitStatus};
use nix::unistd::{getpid, Pid};
use procfs::process::{Process, Task};
use procfs::ProcError;
use std::cell::RefCell;
use std::collections::HashMap;
use std::{
ffi::CString,
fs::File,
Expand All @@ -20,6 +24,7 @@ pub use hardware_breakpoint::{
HardwareBreakpoint, HardwareBreakpointError, HardwareBreakpointSize, HardwareBreakpointType,
};
pub use readmem::ReadMemory;
pub use software_breakpoint::Breakpoint;
pub use writemem::WriteMemory;

lazy_static::lazy_static! {
Expand Down Expand Up @@ -71,6 +76,10 @@ impl Thread for LinuxThread {
pub struct LinuxTarget {
pid: Pid,
hardware_breakpoints: [Option<HardwareBreakpoint>; SUPPORTED_HARDWARE_BREAKPOINTS],
breakpoints: RefCell<HashMap<usize, Breakpoint>>,
// Some if the target stopped because of a breakpoint
// None if stopped for another status
hit_breakpoint: RefCell<Option<Breakpoint>>,
}

/// This structure is used to pass options to attach
Expand All @@ -85,13 +94,75 @@ impl UnixTarget for LinuxTarget {
fn pid(&self) -> Pid {
self.pid
}

fn unpause(&self) -> Result<WaitStatus, Box<dyn std::error::Error>> {
self.handle_breakpoint()?;
ptrace::cont(self.pid(), None)?;
let status = waitpid(self.pid(), None)?;

// We may have hit a user defined breakpoint
if let WaitStatus::Stopped(_, nix::sys::signal::Signal::SIGTRAP) = status {
if let Some(bp) = self
.breakpoints
.borrow_mut()
.get_mut(&(self.read_regs()?.rip as usize - 1))
{
// Register which breakpoint was hit
self.hit_breakpoint.borrow_mut().replace(bp.clone());

// Restore the program to its uninstrumented state
self.restore_breakpoint(bp)?;
};
}
Ok(status)
}

fn step(&self) -> Result<WaitStatus, Box<dyn std::error::Error>> {
let status = self.handle_breakpoint()?;
match status {
None => {
ptrace::step(self.pid(), None).map_err(|e| Box::new(e))?;
Ok(waitpid(self.pid(), None)?)
}
Some(status) => Ok(status),
}
}
}

impl LinuxTarget {
/// executes the code instrumented by a breakpoint if one has been hit
/// and resets the breakpoint
/// returns true if it executed an instruction, false otherwise
/// The function may not execute the instrumented instruction if the
/// P.C is not where the breakpoint was set
fn handle_breakpoint(&self) -> Result<Option<WaitStatus>, Box<dyn std::error::Error>> {
let mut status = None;

let bp = { self.hit_breakpoint.borrow_mut().take() };
// if we have hit a breakpoint previously
if let Some(mut bp) = bp {
let rip = self.read_regs()?.rip as usize;
// if we are not a the right address, we simply set the breakpoint again, and
// carry on
if (rip == bp.addr) && bp.is_enabled() {
assert!(!bp.is_armed());
ptrace::step(self.pid(), None)?;
status = Some(waitpid(self.pid(), None)?);
}
// else user has modified rip and we're not at the breakpoint anymore
if bp.is_enabled() {
bp.set()?;
}
}
Ok(status)
}

fn new(pid: Pid) -> Self {
Self {
pid,
hardware_breakpoints: Default::default(),
breakpoints: RefCell::new(HashMap::new()),
hit_breakpoint: RefCell::new(None),
}
}

Expand All @@ -110,7 +181,7 @@ impl LinuxTarget {
pub fn attach(
pid: Pid,
options: AttachOptions,
) -> Result<(LinuxTarget, nix::sys::wait::WaitStatus), Box<dyn std::error::Error>> {
) -> Result<(LinuxTarget, WaitStatus), Box<dyn std::error::Error>> {
let status = unix::attach(pid)?;
let target = LinuxTarget::new(pid);

Expand Down Expand Up @@ -182,8 +253,8 @@ impl LinuxTarget {
)?;

// Perform syscall
nix::sys::ptrace::step(self.pid(), None)?;
nix::sys::wait::waitpid(self.pid(), None)?;
ptrace::step(self.pid(), None)?;
waitpid(self.pid(), None)?;

// Read return value
let res = self.read_regs()?.rax;
Expand Down Expand Up @@ -406,6 +477,54 @@ impl LinuxTarget {
Err(Box::new(HardwareBreakpointError::UnsupportedPlatform))
}

/// Set a breakpoint at a given address
/// This modifies the Target's memory by writting an INT3 instr. at `addr`
/// The returned Breakpoint object can then be used to disable the breakpoint
/// or query its state
pub fn set_breakpoint(&self, addr: usize) -> Result<Breakpoint, Box<dyn std::error::Error>> {
let bp = Breakpoint::new(addr, self.pid())?;
let existing_breakpoint = {
let hdl = self.breakpoints.borrow();
hdl.get(&addr).map(|val| val.clone())
};

let mut bp = match existing_breakpoint {
None => {
self.breakpoints.borrow_mut().insert(addr, bp.clone());
bp
}
// If there is already a breakpoint set, we give back the existing one
Some(breakpoint) => breakpoint,
};
bp.set()?;
Ok(bp)
}

/// Restore the instruction shadowed by `bp` & rollback the P.C by 1
fn restore_breakpoint(&self, bp: &mut Breakpoint) -> Result<(), Box<dyn std::error::Error>> {
if !bp.is_armed() {
// Fail silently if restoring an inactive breakpoint
return Ok(());
}
// restore the instruction
bp.unset()?;

// rollback the P.C
let mut regs = self.read_regs()?;
regs.rip -= 1;
self.write_regs(regs)?;

Ok(())
}

/// Disable the breakpoint. Call `set()` on the Breakpoint to enable it again
pub fn disable_breakpoint(
&self,
bp: &mut Breakpoint,
) -> Result<(), Box<dyn std::error::Error>> {
bp.disable().map_err(|e| e.into())
}

// Temporary function until ptrace_peekuser is fixed in nix crate
#[cfg(target_arch = "x86_64")]
fn ptrace_peekuser(
Expand Down
102 changes: 102 additions & 0 deletions src/target/linux/software_breakpoint.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
//! Bundles functionalities related to software breakpoints
//! Software Breakpoints work by overwriting the target program's memory;
//! replacing an instruction with one that causes a signal to be raised by the
//! cpu.

use crate::target::{LinuxTarget, ReadMemory};
use nix::sys::ptrace;
use nix::unistd::Pid;
use std::cell::Cell;
use std::rc::Rc;
const INT3: libc::c_long = 0xcc;

#[derive(Debug, Clone)]
pub struct Breakpoint {
/// The address at which the debugger should insert this breakpoint
pub addr: usize,
/// The original instruction overwriten by the breakpoint
pub(super) shadow: i64,
pid: Pid,
user_enabled: Rc<Cell<bool>>,
}

impl Breakpoint {
/// Set a breakpoint at a given address
pub(crate) fn new(addr: usize, pid: Pid) -> Result<Self, BreakpointError> {
let mut shadow = 0_i64;
unsafe {
ReadMemory::new(&LinuxTarget::new(pid))
.read(&mut shadow, addr)
.apply()
}
.map_err(|_e| BreakpointError::IoError)?;
Ok(Breakpoint {
addr,
shadow,
pid,
user_enabled: Rc::new(Cell::new(false)),
})
}

/// Put in place the trap instruction
pub fn set(&mut self) -> Result<(), BreakpointError> {
// We don't allow setting breakpoint twice
// else it would be possible to create a breakpoint that
// would 'restore' an `INT3` instruction
if !self.is_armed() {
let instr = ptrace::read(self.pid, self.addr as *mut _)?;
self.shadow = instr;
let trap_instr = (instr & !0xff) | INT3;
ptrace::write(self.pid, self.addr as *mut _, trap_instr as *mut _)?;
}
self.user_enabled.set(true);
Ok(())
}

pub fn unset(&self) -> Result<(), BreakpointError> {
if self.is_armed() {
ptrace::write(self.pid, self.addr as *mut _, self.shadow as *mut _)?;
}
Ok(())
}

/// Restore the previous instruction for the breakpoint.
pub fn disable(&self) -> Result<(), BreakpointError> {
self.unset()?;
self.user_enabled.set(false);
Ok(())
}

pub fn is_enabled(&self) -> bool {
self.user_enabled.get()
}

/// Whether this breakpoint has instrumented the target's code
pub fn is_armed(&self) -> bool {
let instr = ptrace::read(self.pid, self.addr as *mut _)
.map_err(|err| eprintln!("Failed to check if breakpoint is armed ({})", err))
.unwrap_or(0);
(instr & 0xff) == INT3
}
}

#[derive(Debug)]
pub enum BreakpointError {
NoSuchSymbol,
IoError,
NixError(nix::Error),
}

impl std::convert::From<nix::Error> for BreakpointError {
fn from(error: nix::Error) -> Self {
BreakpointError::NixError(error)
}
}

impl std::fmt::Display for BreakpointError {
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(fmt, "{:?}", self)
}
}

impl std::error::Error for BreakpointError {}
2 changes: 2 additions & 0 deletions tests/fixed_breakpoint.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
//! This is a simple test for waiting for a fixed breakpoint in a child process.
//! Here the testee has hardcoded INT3 instructions that should trigger breaks
//! so that headcrab can gain control at certain key points of execution.

mod test_utils;

Expand Down
Loading

0 comments on commit e4eb3f3

Please sign in to comment.