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
This commit is contained in:
əlemi 2023-03-28 19:11:36 +02:00
parent b5236f7d27
commit d3f08ba22a
Signed by: alemi
GPG key ID: A4895B84D311642C
4 changed files with 169 additions and 43 deletions

View file

@ -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"

68
src/needle/explorers.rs Normal file
View file

@ -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<usize> {
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<usize> {
let libc = std::fs::read(path).expect("could not read libc");
let headers = ElfBytes::<AnyEndian>::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);
}

View file

@ -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<u64>;
}
pub fn step_to_syscall(pid: Pid) -> Result<usize> {
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)?;
}
}

View file

@ -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<usize>,
/// base address of libc in memory (minus offset), calculated with /proc/<pid>/maps if not given
#[arg(long)]
base: Option<usize>,
/// offset address of dlopen inside libc, calculated reading libc ELF if not given
#[arg(long)]
offset: Option<usize>,
/// path of libc shared object on disk, used to calculate symbol offset in ELF
#[arg(long)]
path: Option<PathBuf>,
}
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());
}
}