feat: search for symbols in exported but also elf

this allows finding non-exported symbols on non-stripped elfs. requires
being able to read the executable that created this process. also added
example code in C to target with this tool
This commit is contained in:
əlemi 2023-03-30 01:38:21 +02:00
parent 28778ab2e1
commit fab29c5423
Signed by: alemi
GPG key ID: A4895B84D311642C
3 changed files with 130 additions and 43 deletions

View file

@ -21,4 +21,4 @@ retour = "0.1" # plain detour doesn't work on latest nightly? idk
elf = "0.7.2" elf = "0.7.2"
nix = "0.26.2" nix = "0.26.2"
proc-maps = "0.3.0" proc-maps = "0.3.0"
dlopen = "0.1.8" libloading = "0.7.4"

34
examples/victim.c Normal file
View file

@ -0,0 +1,34 @@
#define _GNU_SOURCE
#include <dlfcn.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <stdlib.h>
int load_secret();
int main(int argc, char** argv) {
int secret;
Dl_info info;
if (argc > 1 && dladdr(dlopen, &info)) {
printf("> dlopen addr: %p\n", info.dli_saddr);
}
puts("> working...");
srand(42);
while (1) {
printf("> generating secret ");
fflush(stdout);
for (int i = 0; i < 20; i++) {
usleep(200 * 1000);
printf(".");
fflush(stdout);
}
secret = load_secret();
printf(" saved!\n");
}
}
int load_secret() {
return rand();
}

View file

@ -1,58 +1,111 @@
use std::{error::Error, ffi::c_int}; use std::{error::Error, ffi::c_int, path::{Path, PathBuf}};
use dlopen::symbor::Library; use elf::{ElfBytes, endian::AnyEndian, ParseError};
use nix::libc::{socklen_t, sockaddr}; use libloading::os::unix::{Library, Symbol};
use retour::static_detour; use proc_maps::get_process_maps;
use retour::{static_detour, Function};
static_detour! { static_detour! {
static SOCKET_HOOK : unsafe extern "C" fn(i32, i32, i32) -> i32; static HOOK : unsafe extern "C" fn() -> c_int;
static CONNECT_HOOK : unsafe extern "C" fn(c_int, *const sockaddr, socklen_t) -> c_int;
static LOAD_EXT_HOOK : unsafe extern "C" fn(c_int) -> c_int;
} }
// extern "C" { #[ctor::ctor] // our entrypoint is the library constructor, invoked by dlopen
// fn load_ext() -> (); 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>> { fn add_hooks() -> Result<(), Box<dyn Error>> {
let exec = Library::open_self()?; let ptr = find_symbol("load_secret")?;
let load_ext_sym = unsafe { exec.symbol::<unsafe extern "C" fn(c_int) -> c_int>("load_ext") }; unsafe {
HOOK.initialize(ptr, || {
unsafe { let secret = HOOK.call();
SOCKET_HOOK.initialize(nix::libc::socket, |dom, tp, proto| { eprint!(" ( ͡° ͜ʖ ͡°) its {} ", secret);
eprintln!("caught socket({}, {}, {}) call", dom, tp, proto); secret
SOCKET_HOOK.call(dom, tp, proto)
})?; })?;
SOCKET_HOOK.enable()?; HOOK.enable()?;
CONNECT_HOOK.initialize(nix::libc::connect, |fd, info, len| {
eprintln!("caught connect({}, ??, {}) call", fd, len);
CONNECT_HOOK.call(fd, info, len)
})?;
CONNECT_HOOK.enable()?;
match load_ext_sym {
Ok(sym) => {
LOAD_EXT_HOOK.initialize(*sym, |x| { eprintln!("intercepted load_ext!"); x })?;
LOAD_EXT_HOOK.enable()?;
},
Err(e) => {
eprintln!("[!] skipping load_ext hook : {}", e);
},
}
} }
Ok(()) Ok(())
} }
fn find_symbol<T : Function>(name: &str) -> Result<T, Box<dyn Error>> {
// try to find it among exported symbols
#[ctor::ctor] let this = Library::this(); // TODO don't reopen it every time
fn constructor() { let sym : Result<Symbol<T>, libloading::Error> = unsafe { this.get(name.as_bytes()) };
println!("Infected!"); if let Ok(s) = sym {
return Ok(*s);
if let Err(e) = add_hooks() {
eprintln!("[!] Could not add hooks : {}", e);
} }
// 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)
} }