slog debugger
I'm suggesting so highly rn.
motivaption
Making your own debugger is criminally easy. For certain simple cases, it is much more complex to configure<br>an<br>existing debugger to work for you than it is to just write your own, and once you have your own debugger you<br>can easily modify it to work for new projects, since you understand all the code.
Most of this slog post consists of me telling you about cool libc functions. It is your job to<br>put them together into a debugger. You are encouraged to explore things by/for yourself.
exec
First we want to be able to execute a program. This is pretty simple and to do this we'll use the<br>exec libc functions.<br>There are many exec<br>functions but they all replace the current process with a new process.
use nix::unistd::execv;
execv(c"/bin/echo", &[c"echo", c"Hi!"])?;
unreachable!();
The console output will look something like this:
Hi!
execl* functions accept arguments variadic-ly. execv* functions accept arguments<br>as a list. exec*e functions allow you to set the environment variables of the new process.<br>exec*p functions will search for the program to be executed in the PATH<br>environment variable.
So now we can execute a program. Note the presence of that unreachable!<br>statement. Since exec replaces the entire process, any code after a call to it will never be<br>run!
fork
fork is<br>simple. It duplicates the process. If you are now running as the parent, the child<br>process id is returned. If you are now running as the child, 0 is returned.
use nix::libc::fork;
println!("Hello, World!");
let pid = unsafe { fork() };<br>if pid == 0 {<br>println!("I am the child process (PID={pid})");<br>} else {<br>println!("I am the parent process (PID={pid})");
The console output will look something like this:
Hello, World!<br>I am the parent process (PID=172471)<br>I am the child process (PID=0)
Now we can pair exec and fork together to spawn a process.
use nix::libc::fork;<br>use nix::unistd::execv;
let pid = unsafe { fork() };<br>if pid == 0 {<br>execv(c"/bin/echo", &[c"echo", c"Hi from child!"])?;<br>} else {<br>println!("Hi from parent!");
println!("All done!");
The console output will look something like this:
Hi from parent!<br>Hi from child!<br>All done!
ptrace
ptrace is the most<br>important function here. It's going to allow us to hook into our target program and start messing with it.<br>The child process will call ptrace(PTRACE_TRACEME, ...) which allows the parent process to<br>trace it.
use nix::libc::fork;<br>use nix::sys::ptrace;<br>use nix::unistd::execv;
let pid = unsafe { fork() };
if pid == 0 {<br>ptrace::traceme()?;<br>execv(c"/path/to/program", &[c"program"])?;<br>} else {<br>// run tracer functions
After a child process (tracee) has called PTRACE_TRACEME the parent process (tracer) can call<br>ptrace<br>functions to<br>inspect and manipulate the child.
waitpid
waitpid<br>waits for a process to change status, usually by a system interrupt. We can use this to wait for certain<br>events to fire on our tracee.
After an event is fired, the tracee will be paused and the tracer has to unpause it.
use nix::sys::ptrace;<br>use nix::sys::wait::{WaitPidFlag, WaitStatus, waitpid};
// wait for an event<br>let status = waitpid(child_pid, Some(WaitPidFlag::WNOHANG))?;
// do some stuff<br>if let WaitStatus::Exited(_, _) = status {<br>println!("The process exited.");
// unpause tracee<br>ptrace::cont(child_pid, None)?;
personality
personality sets a<br>bunch<br>of different execution options for the current process. We will be using the ADDR_NO_RANDOMIZE<br>flag to disable<br>ASLR. This makes reading and<br>interpreting the addresses of functions much easier.
use nix::libc::{ADDR_NO_RANDOMIZE, personality};
unsafe { personality(ADDR_NO_RANDOMIZE as u64) };
example code
Let's make a program that waits for an event, sets the rax register to<br>0x12345,<br>and then continues execution of the tracee.
use nix::libc::{ADDR_NO_RANDOMIZE, fork, personality};<br>use nix::sys::ptrace;<br>use nix::sys::wait::waitpid;<br>use nix::unistd::{Pid, execv};
let pid = unsafe { fork() };
if pid == 0 {<br>unsafe { personality(ADDR_NO_RANDOMIZE as u64) };<br>ptrace::traceme()?;<br>execv(c"/path/to/program", &[c"program"])?;<br>} else {<br>// wait for event<br>let child_pid = Pid::from_raw(pid);<br>_ = waitpid(child_pid, None)?;<br>// after event tracee is paused
// set register<br>let mut regs = ptrace::getregs(child_pid)?;<br>regs.rax = 0x12345;<br>ptrace::setregs(child_pid, regs)?;
// unpause tracee (continue)<br>ptrace::cont(child_pid, None)?;
There are much more useful ptrace functions. I'll list a few here that you might want to look into.
ptrace(PTRACE_CONT, ...) -<br>resume execution of the tracee.
ptrace(PTRACE_STEP, ...) - resume exection but stop on the next instruction or system call<br>(configurable)
ptrace(PTRACE_GETREGS, ...) - gets the registers of the tracee process
ptrace(PTRACE_SETREGS, ...) - sets the registers of the tracee process
ptrace(PTRACE_PEEKDATA, ...) - reads the memory of the tracee process
ptrace(PTRACE_POKEDATA, ...) - writes the memory of the tracee process
The control flow...