Implement puppeting

This commit is contained in:
Michael Mikovsky
2025-08-23 11:02:01 -06:00
parent 0e36538e84
commit 5f38d85e6c
15 changed files with 644 additions and 115 deletions
+112 -9
View File
@@ -1,13 +1,33 @@
use std::ffi::CString;
use std::io;
use std::net::TcpListener;
use std::time::Duration;
use std::{
io::{Read, Write},
net::{TcpListener, TcpStream},
net::TcpStream,
thread,
};
use log::{error, info, trace};
use nix::libc::{self, MAP_ANONYMOUS, MAP_PRIVATE, PROT_READ, PROT_WRITE, user_regs_struct};
use nix::unistd::Pid;
use subprocess::{Popen, PopenConfig, Redirection};
use syscall_lib::Syscall;
use syscalls::Sysno;
fn main() {
println!("This program has PID: {}", std::process::id());
use crate::proc::UserProcess;
mod proc;
mod sleeper;
fn main() -> io::Result<()> {
pretty_env_logger::formatted_builder()
.filter_level(log::LevelFilter::Trace)
.init();
log::info!("This program has PID: {}", std::process::id());
// run()?;
let listener = TcpListener::bind("127.0.0.1:1234").unwrap();
@@ -16,16 +36,20 @@ fn main() {
thread::spawn(move || {
let _ = handle_connection(&mut stream);
println!("Connection from {} closed", stream.peer_addr().unwrap());
info!("Connection from {} closed", stream.peer_addr().unwrap());
});
}
Ok(())
}
fn handle_connection(stream: &mut TcpStream) -> Result<(), std::io::Error> {
println!("Got connection from {}", stream.peer_addr()?);
info!("Got connection from {}", stream.peer_addr()?);
// let mut memory = ProgramMemory::default();
let puppet = create_new_proc();
loop {
let mut size_buf = [0u8; 4];
stream.read_exact(&mut size_buf)?;
@@ -35,19 +59,38 @@ fn handle_connection(stream: &mut TcpStream) -> Result<(), std::io::Error> {
stream.read_exact(&mut buf)?;
let decoded = Syscall::decode(&buf).unwrap();
let syscall = Syscall::decode(&buf).unwrap();
println!("{:?} -> ", decoded);
trace!("{:?}", syscall);
// let result = match decoded {
// Syscall::Write(..) => 0,
// _ => syscall_exec::execute_syscall(decoded),
// };
let result = unsafe { decoded.execute_syscall() };
let args = syscall.to_syscall_args();
let result = puppet.sys_call(
args[0], args[1], args[2], args[3], args[4], args[5], args[6],
);
let result = match result {
Ok(n) => n,
Err(e) => {
error!("Error: {}", e.to_string());
return Ok(());
}
};
let result = result.rax;
// let result: u64 = result.unwrap().rax;
// result.r
// let result = 0;
println!("{:?}", result);
trace!("{:?} -> {:?}", syscall, result);
let bytes: [u8; 8] = result.to_be_bytes();
stream.write_all(&bytes)?;
@@ -85,3 +128,63 @@ fn handle_connection(stream: &mut TcpStream) -> Result<(), std::io::Error> {
// println!("{:?}", decoded);
}
}
fn create_new_proc() -> UserProcess {
//TODO: Improve
let mut p = Popen::create(
&["sleep", "9999999999s"],
PopenConfig {
// stdout: Redirection::Pipe,
..Default::default()
},
)
.unwrap();
let pid = Pid::from_raw(p.pid().unwrap() as libc::pid_t);
// Attach to the process
UserProcess::attach(pid).unwrap()
}
// fn run() -> io::Result<()> {
// // Refactor out the expect later, but the input should never fail because we know the input does not contain an internal 0 byte.
// let output_message = CString::new("/tmp").expect("CString::new failed");
// // We want the bytes of the Cstring.
// let output_message = output_message.as_bytes();
// // Allocate 8 bytes of data, i64 is 8 bytes
// let mut user_memory = user_process
// .allocate_memory(
// 0,
// output_message.len() as u64,
// (PROT_READ | PROT_WRITE) as u64,
// (MAP_PRIVATE | MAP_ANONYMOUS) as u64,
// u64::MAX,
// 0,
// )
// .unwrap();
// log::info!("UserMemory Result Address: {:#X}", user_memory.address());
// // Read the memory and demonstrate it is zero'd out
// let read = user_process
// .read_user_memory(&user_memory, user_memory.len() as usize)
// .unwrap();
// log::info!("Allocated Memory: {:?}", read);
// // Write to the memory out cstring
// user_process
// .write_user_memory(&mut user_memory, 0, output_message)
// .unwrap();
// // We can check if the call succeeded by the resultant rax value.
// let result = user_process
// .sys_call(Sysno::chdir, user_memory.address(), 0, 0, 0, 0, 0)
// .unwrap()
// .rax;
// log::info!("Result {result:?}");
// Ok(())
// }
+273
View File
@@ -0,0 +1,273 @@
// Full credit to https://github.com/ohchase/ptrace_syscalls/tree/master
use std::vec;
use nix::{
libc::user_regs_struct,
sys::{
ptrace,
signal::Signal::SIGTRAP,
wait::{WaitStatus, waitpid},
},
unistd::Pid,
};
use syscalls::Sysno;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum HostError {
#[error("Process not found `{0}`")]
ProcessNotFound(String),
#[error("Nix Error `{0}`")]
NixError(#[from] nix::errno::Errno),
#[error("Unexpected Wait Status `{0:#?}`")]
UnexpectedWaitStatus(WaitStatus),
#[error("StdIo Error `{0}`")]
Io(#[from] std::io::Error),
#[error("Mmap Error `{0:#?}`")]
MmapBadAddress(u64),
#[error("Munmap Error `{0:#?}`")]
MunmapFailed(u64),
}
pub type HostResult<T> = Result<T, HostError>;
pub struct UserProcessMemory<'a> {
address: u64,
owner: &'a UserProcess,
len: u64,
}
impl<'a> UserProcessMemory<'a> {
/// Getter for UserProcessMemory's start address
pub fn address(&self) -> u64 {
self.address
}
/// Getter for UserProcessMemory's length
#[allow(clippy::len_without_is_empty)]
pub fn len(&self) -> u64 {
self.len
}
}
impl<'a> Drop for UserProcessMemory<'a> {
/// Munmap's the memory on drop
fn drop(&mut self) {
match self.owner.deallocate_memory(self.address, self.len) {
Ok(()) => log::trace!(
"Successfully deallocated memory {:#X} with length {}",
self.address,
self.len
),
Err(err) => log::error!(
"Failed to deallocate memory {:#X} with length {}, Error: {:#?}",
self.address,
self.len,
err
),
}
}
}
pub struct UserProcess {
pid: Pid,
}
impl UserProcess {
/// Initializes the UserProcess
/// Given you have a UserProcess instance it means the attach must of succeeded.
/// Attach should be the only way to acquire a UserProcess.
pub fn attach(pid: Pid) -> HostResult<Self> {
ptrace::attach(pid)?;
log::info!("New UserProcess successfully attached to pid: {}", pid);
Ok(Self { pid })
}
/// Getter for UserProcess actively connected pid
pub fn pid(&self) -> Pid {
self.pid
}
/// Uses mmap syscall with ptrace to allocate user process memory
pub fn allocate_memory(
&self,
address: u64,
len: u64,
prot: u64,
flags: u64,
fd: u64,
offset: u64,
) -> HostResult<UserProcessMemory> {
let mmap_result =
self.sys_call(Sysno::mmap as u64, address, len, prot, flags, fd, offset)?;
let mmap_result = mmap_result.rax;
// invalid address
// TODO not quite right...
if mmap_result == 0 {
return Err(HostError::MmapBadAddress(address));
}
Ok(UserProcessMemory {
address: mmap_result,
owner: self,
len,
})
}
/// Uses munmap syscall with ptrace to deallocate user process memory
fn deallocate_memory(&self, address: u64, len: u64) -> HostResult<()> {
let munmap_result = self.sys_call(Sysno::munmap as u64, address, len, 0, 0, 0, 0)?;
let munmap_result = munmap_result.rax;
// not a zero return value means a failure
if munmap_result != 0 {
return Err(HostError::MunmapFailed(address));
}
Ok(())
}
/// Write to the user process memory
pub fn write_user_memory(
&self,
user_memory: &mut UserProcessMemory,
offset: u64,
bytes: &[u8],
) -> HostResult<usize> {
self.write_memory(user_memory.address + offset, bytes)
}
/// Read from user process memory
pub fn read_user_memory(
&self,
user_memory: &UserProcessMemory,
len: usize,
) -> HostResult<Vec<u8>> {
self.read_memory(user_memory.address, len)
}
/// String to the proc's memory file
/// Reference: https://crates.io/crates/pete
fn proc_mem_path(&self) -> String {
format!("/proc/{}/mem", self.pid.as_raw() as u32)
}
/// Common wrapper around writing memory
/// Returns the amount of bytes written
/// Reference: https://crates.io/crates/pete
fn write_memory(&self, addr: u64, data: &[u8]) -> HostResult<usize> {
use std::os::unix::fs::FileExt;
let mem = std::fs::OpenOptions::new()
.read(true)
.write(true)
.open(self.proc_mem_path())?;
let len = mem.write_at(data, addr)?;
Ok(len)
}
/// Common wrapper around reading memory
/// Returns a vector to the memory bytes
/// Reference: https://crates.io/crates/pete
fn read_memory(&self, addr: u64, len: usize) -> HostResult<Vec<u8>> {
use std::os::unix::fs::FileExt;
let mut data = vec![0u8; len];
let mem = std::fs::File::open(self.proc_mem_path())?;
let len_read = mem.read_at(&mut data, addr)?;
data.truncate(len_read);
Ok(data)
}
/// Invokes a syscall in the userprocess
/// Accepts up to six arguments
#[allow(clippy::too_many_arguments)]
pub fn sys_call(
&self,
sys_call: u64,
rdi: u64,
rsi: u64,
rdx: u64,
r10: u64,
r8: u64,
r9: u64,
) -> HostResult<user_regs_struct> {
log::trace!("UserProcess {} Syscall: {:#?}", self.pid, sys_call);
let syscall_instruction = [0x0Fu8, 0x05u8];
// Cache original registers, original instruction pointer (rip), and the original instructions
let original_registers = ptrace::getregs(self.pid)?;
let original_ip = original_registers.rip;
let original_instructions = self.read_memory(original_ip, syscall_instruction.len())?;
// Write over our shell code 0x0F05 for sys call
self.write_memory(original_ip, &syscall_instruction)?;
// Create a copy of the original registers
// Set the sys_call index and args[0..5] (six arguments)
let mut new_registers = original_registers;
new_registers.rax = sys_call;
new_registers.rdi = rdi;
new_registers.rsi = rsi;
new_registers.rdx = rdx;
new_registers.r10 = r10;
new_registers.r8 = r8;
new_registers.r9 = r9;
// Apply the new registers, and new instructions then single step waiting for SIG_TRAP
ptrace::setregs(self.pid, new_registers)?;
// Single step the process and wait for a SIGTRAP signal
self.single_step()?;
// Cache the resultant registers
let result = ptrace::getregs(self.pid)?;
// Restore original instructions, and original registers to continue normal program control flow
self.write_memory(original_ip, &original_instructions)?;
ptrace::setregs(self.pid, original_registers)?;
Ok(result)
}
/// Single steps the process
/// Fails if the next signal is not a SIGTRAP
fn single_step(&self) -> HostResult<()> {
ptrace::step(self.pid, None)?;
self.wait_trap()
}
/// Waits for the next signal
/// Returns an error if that signal is not a SIGTRAP
fn wait_trap(&self) -> HostResult<()> {
match self.wait()? {
WaitStatus::Stopped(_, SIGTRAP) => Ok(()),
status => Err(HostError::UnexpectedWaitStatus(status)),
}
}
/// Waits on the current process
fn wait(&self) -> HostResult<WaitStatus> {
waitpid(self.pid, None).map_err(HostError::NixError)
}
}
impl Drop for UserProcess {
/// Drop implementation for the UserProcess
/// Attempts to detach from the process, does not panic on error instead only logs
fn drop(&mut self) {
if let Err(err) = ptrace::detach(self.pid, None) {
log::error!(
"UserProcess failed to detach from: {}, with Err: {:#?}",
self.pid,
err
);
} else {
log::trace!("UserProcess successfully detached from: {}", self.pid);
}
}
}
+105
View File
@@ -0,0 +1,105 @@
use std::{
fs::File,
io::{self, Write},
};
const FILENAME: &'static str = "/tmp/sleeper";
pub fn create_sleeper() -> io::Result<()> {
// ELF header for x86_64 Linux executable
let mut binary_bytes = Vec::new();
// ELF Magic number and identification
binary_bytes.extend_from_slice(&[
0x7f, 0x45, 0x4c, 0x46, // ELF magic
0x02, // 64-bit
0x01, // Little endian
0x01, // ELF version
0x00, // System V ABI
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Padding
]);
// ELF header continuation
binary_bytes.extend_from_slice(&[
0x02, 0x00, // Executable file
0x3e, 0x00, // x86_64
0x01, 0x00, 0x00, 0x00, // Version 1
]);
// Entry point (0x401000)
binary_bytes.extend_from_slice(&[0x00, 0x10, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00]);
// Program header offset (64 bytes)
binary_bytes.extend_from_slice(&[0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]);
// Section header offset (0 - no sections)
binary_bytes.extend_from_slice(&[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]);
// Flags, header size, program header size, program header count
binary_bytes.extend_from_slice(&[
0x00, 0x00, 0x00, 0x00, // Flags
0x40, 0x00, // ELF header size (64 bytes)
0x38, 0x00, // Program header size (56 bytes)
0x01, 0x00, // Program header count (1)
0x00, 0x00, // Section header size
0x00, 0x00, // Section header count
0x00, 0x00, // String table index
]);
// Program header (LOAD segment)
binary_bytes.extend_from_slice(&[
0x01, 0x00, 0x00, 0x00, // PT_LOAD
0x05, 0x00, 0x00, 0x00, // PF_X | PF_R (executable + readable)
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Offset in file
0x00, 0x10, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, // Virtual address
0x00, 0x10, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, // Physical address
0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Size in file
0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Size in memory
0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Alignment
]);
// Pad to entry point (0x401000 - current position)
while binary_bytes.len() < 0x1000 {
binary_bytes.push(0x00);
}
// Assembly code that loops and sleeps
// This is x86_64 assembly for:
// loop:
// mov rax, 35 ; sys_nanosleep
// mov rdi, timespec ; pointer to timespec struct
// mov rsi, 0 ; remaining time (NULL)
// syscall
// jmp loop
//
// timespec struct (1 second sleep):
// .quad 1 ; seconds
// .quad 0 ; nanoseconds
binary_bytes.extend_from_slice(&[
// Main loop
0x48, 0xc7, 0xc0, 0x23, 0x00, 0x00, 0x00, // mov rax, 35 (sys_nanosleep)
0x48, 0xc7, 0xc7, 0x18, 0x10, 0x40, 0x00, // mov rdi, 0x401018 (timespec address)
0x48, 0x31, 0xf6, // xor rsi, rsi (NULL)
0x0f, 0x05, // syscall
0xeb, 0xf1, // jmp -15 (back to mov rax, 35)
// timespec struct at 0x401018
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 1 second
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0 nanoseconds
]);
// Write the binary to /tmp
let mut file = File::create(FILENAME)?;
file.write_all(&binary_bytes)?;
// Make it executable
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = file.metadata()?.permissions();
perms.set_mode(0o755);
std::fs::set_permissions(FILENAME, perms)?;
}
Ok(())
}