Hpmason / retour-rs

A cross-platform detour library written in Rust
Other
99 stars 18 forks source link

Need instruction how to use GenericDetour on Linux #49

Closed HerlinCoder closed 3 months ago

HerlinCoder commented 6 months ago

Hi, I want to implement a hot patch feature for my rust program, and I'm really interested in this retour-rs crate. So I write a poc binary and a share library to try to prove the effect, but this failed. I'm not familiar with this crate, and have encounter some problems.

I have a function defined in a common crate which is used in both share library and poc binary. I am using Linux with stable version rust, so I use GenericDetour instead of static_detour!. I tried to use it in the share library, to create a hook from the original function to a new hook function. It success when I use LD_PRELOAD to load the share library, and enable the detour in the share library constructor using ctor. When I try to call the function in the share library constructor, the hook is effective. But when I call the function in my poc binary after that, it seems go back to the original function.

I wonder if the function address is different between share library and my poc binary, but i don't know how to get the address from a binary. I have read those examples but still not found a solution.

Any suggestion? Can this crate be used for hot patch feature? Thx.

Hpmason commented 6 months ago

One issue you may be running into is that when a GenericDetour goes out of scope and is dropped, it cleans itself up and disables the detour. To test this theory, you can use mem::forget to avoid the drop implementation from running.

fn add5(val: i32) -> i32 {
  val + 5
}

fn add10(val: i32) -> i32 {
  val + 10
}

#[ctor]
fn constructor() {
    let mut hook = unsafe { GenericDetour::<fn(i32) -> i32>::new(add5, add10).unwrap() };
    unsafe { hook.enable().unwrap() };
    std::mem::forget(hook);
}

The issue with creating/enabling the detour from the poc binary could be finding the correct address after the shared library is loaded. In that case, I think minidl could be useful to get the address of a specific function from a library. If your shared library is written in Rust, make sure to use #[no_mangle] pub extern "C" so that it follows the C ABI and that the symbol matches the function name. The Rust ABI is not stable, so it should not be relied on between binarys/libraries, so that's why you'd have to specify a specific ABI.

Let me know how these steps turn out or if you have other questions, I'd be happy to help. It may also be a good to add another example more oriented towards Linux and GenericDetours to help others that have similar problems.

HerlinCoder commented 6 months ago

Thanks for your reply! I have tried several methods this week, including adding "mem::forget", but still failed. I'm afraid I am still on a wrong way to do this. Here is my code:

// src/printer/src/lib.rs
pub fn print_func() {
    println!("Original Func");
}
// src/hook/src/lib.rs
use ctor::ctor;
use retour::GenericDetour;
use printer::print_func;

pub fn print_hook() {
    println!("Hook Func");
}

#[ctor]
fn init() {
    let hook =
        unsafe { GenericDetour::<fn() -> ()>::new(print_func, print_hook).unwrap() };
    unsafe { hook.enable().unwrap() };
    std::mem::forget(hook);
    print_func();
}
// src/poc/src/main.rs
use std::thread;
use std::time;

use printer;

fn main() {
    for _i in 0..100 {
        printer::print_func();
        thread::sleep(time::Duration::from_millis(5000));
    }
}

I want to change the output content of print function when poc process in running. Using ptrace may be a good choice to inject the share library, and I'm just using LD_PRELOAD to check if my share library is effective.

When I called LD_PRELOAD=./hook.so ./poc, I could only get one line of "Hook Func", which is called in share library constructor, and all other output is still "Original Func". That means the hook itself is effective, but the usage is not correct.

I think it is because the "printer::print_func" function address in share library constructor and in poc process are different. Is that mean I have to get function address of the poc process dynamically to build the detour hook? Just like injecting the share library with a parameter as function address?

Thanks.

HerlinCoder commented 6 months ago

I've figure out why I failed. I should get function address of process dynamically in the share library rather than use printer::print_func directly. This can be solved by:

  1. find symbol in binary to get the offset
  2. find base address of binary in the running process
  3. calculate to get the function address in the running process

Then when I call LD_PRELOAD=./hook.so ./poc, original function is replaced by hook function.

And I also try to use ptrace to inject this share library to the process, it still success.

Thanks for your suggestion! I will put the code here later if anyone need help.

HerlinCoder commented 6 months ago

Here is the code.

use std::fs::File;
use std::io::{self, BufRead, BufReader};
use std::path::Path;

use ctor::ctor;
use object::{Object, ObjectSymbol};
use retour::GenericDetour;

pub fn print_hook() {
    println!("Hook Func");
}

fn find_binary_base_address(pid: u32, binary_name: &str) -> io::Result<Option<u64>> {
    let maps_path = Path::new("/proc").join(pid.to_string()).join("maps");
    let file = File::open(maps_path)?;
    let reader = BufReader::new(file);

    for res in reader.lines() {
        if let Ok(line) = res {
            // Find lines containing binary names
            if line.contains(binary_name) {
                // Intercept the beginning of an address range
                if let Some(range) = line.split(' ').next() {
                    if let Some(address_str) = range.split('-').next() {
                        // Convert address strings to numbers
                        if let Ok(address) = u64::from_str_radix(address_str, 16) {
                            return Ok(Some(address));
                        }
                    }
                }
            }
        }
    }
    Ok(None)
}

fn find_symbol_in_binary(binary_path: &str, symbol_name: &str) -> Option<u64> {
    let file = std::fs::read(binary_path).expect("Failed to read file");
    let binary = object::File::parse(&*file).expect("Failed to parse file");
    for symbol in binary.symbols() {
        if let Ok(name) = symbol.name() {
            if name.contains(symbol_name) {
                let address = symbol.address();
                println!("symbol_name is {}, address is {}", name, address);
                return Some(address);
            }
        }
    }

    None
}

#[ctor]
fn init() {
    let pid = std::process::id();

    let binary_base_address = find_binary_base_address(pid, "poc")
        .expect("open binary failed")
        .expect("find binary base address");

    let binary_func_address =
        find_symbol_in_binary("./poc", "print_func").expect("find symbol in binary");

    let process_func_address = binary_base_address + binary_func_address;
    let func = unsafe { std::mem::transmute(process_func_address) };

    let hook = unsafe { GenericDetour::<fn() -> ()>::new(func, print_hook).unwrap() };
    unsafe { hook.enable().unwrap() };
    std::mem::forget(hook);
}
SkyLeite commented 5 months ago

One issue you may be running into is that when a GenericDetour goes out of scope and is dropped, it cleans itself up and disables the detour

I ran into this, and wrapping the detour in a ManuallyDrop fixed the issue.

Hpmason commented 3 months ago

@HerlinCoder is this issue good to close out?