Trace calls to shared C libraries with ltrace?

I'm debugging a binding library that's making calls into a shared library of which I don't have the source. ltrace is not capturing any of the library calls. Is there any configuration I can do to my crate to get it to display calls?

Unfortunately I've never been able to get ltrace to work, ever.

What command are you using?

  • blind guess: ltrace cargo run won't work, as that traces the execution of cargo.

    cargo build && ltrace ./target/debug/crate-name
    # or
    cargo build --release && ltrace ./target/release/crate-name
    # or, if targeting x86_64-unknown-linux-gnu :
    CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUNNER=ltrace cargo run
    

Also, can you tell us what the output of ldd ./your_binary is?

Yup, I've been debugging using target/debug/crate-name.

ldd confirms that my binary is indeed linked to the system crate /usr/lib/libname.so.

If ltrace isn't an option, is there any other way I can get rust binaries to trace C library calls?

If you know what functions you want to trace (e.g. because you have the header file), an alternative solution is to use LD_PRELOAD. This environment variable tells the linker to first check the specified dynamic library when resolving symbols.

What you can do is create a dynamic library which exports functions with the same name, then when those functions are called you'll add your debugging code before using dlopen() and dlsym() to invoke the original function.

This is used all the time when reverse engineering 3rd party applications or as a hack to patch functionality (e.g. malloc and free when troubleshooting memory issues). Dynamic linker tricks: Using LD_PRELOAD to cheat, inject features and investigate programs may point you in the right direction.

1 Like

Indeed! I thought that ltrace kind of relied on that too, though (it shims the dynamic linker)


Aside

This is so wrong, however, I think it deserves to be mentioned: shadowing dynamically linked functions is not enough and thus "useless" for reliable sandboxing, since the program can avoid using them:

  • Counterexample

    "How?

    One way to achieve this is by adding a -static compilation flag, to ensure the linking happens at at compile-time (in the malware's author's hands) rather than at runtime (thus no longer in the sandboxer's hands):

    cc -static -o random_num{,.c}
    

    At the end of the day, the interaction between a program and its environment happens through system calls, which one can issue directly or by statically linking against libraries that so do. So sandboxing a malware can only begin to happen at that level, and even then, there are many caveats.

TL,DR: Much alike cryptography, sandboxing is not something that should be done casually, and should rather be left to "professional" tools / libraries specialised for the task.

Agreed. I'd consider things like LD_PRELOAD and LD_LIBRARY_PATH to be hacks you can use to override or inspect an application's function calls, and not intended for sandboxing.

I'd rather leave that to kernel mechanisms like cgroups, chroot, and containers (which build on the previous two), although even the Linux Kernel isn't bulletproof. I feel like the "best" solution would be to just not run untrusted code on your machine in the first place. If the code never runs, it can't do nefarious things or escape your sandbox. Otherwise WASM+WASI seems promising.

1 Like

I have implemented a macro to wrap the extern block definitions with, so that a tracing similar to LD_LIBRARY_PATH's "hack" can happen without needing ltrace.

For instance,

use ::libc::c_int;

traced! {
    extern "C" {
        /// Some docstring
        pub
        fn abs (x: c_int) -> c_int;
    }
}

fn main ()
{
    let _ = unsafe { abs(-42) };
}

Yields:

abs(
    x: c_int = -42,
): c_int = 42