frida-rust icon indicating copy to clipboard operation
frida-rust copied to clipboard

Stalker bugs

Open ghost opened this issue 5 months ago • 2 comments

I've found some bugs, not sure if they are caused by the skill issues of mine, the incorrect usage of bindings, or the frida itself

Writing to stdout in the callback passed to the Transformer::from_callback causes the program to panic:

thread '<unnamed>' panicked at library/std/src/io/stdio.rs:860:20:
RefCell already borrowed
stack backtrace:
   0: __rustc::rust_begin_unwind
             at /rustc/9748d87dc70a9a6725c5dbd76ce29d04752b4f90/library/std/src/panicking.rs:697:5
   1: core::panicking::panic_fmt
             at /rustc/9748d87dc70a9a6725c5dbd76ce29d04752b4f90/library/core/src/panicking.rs:75:14
   2: core::cell::panic_already_borrowed::do_panic::runtime
             at /rustc/9748d87dc70a9a6725c5dbd76ce29d04752b4f90/library/core/src/panic.rs:218:21
   3: core::cell::panic_already_borrowed::do_panic
             at /rustc/9748d87dc70a9a6725c5dbd76ce29d04752b4f90/library/core/src/intrinsics/mod.rs:2367:9
   4: core::cell::panic_already_borrowed
             at /rustc/9748d87dc70a9a6725c5dbd76ce29d04752b4f90/library/core/src/panic.rs:223:9
   5: core::cell::RefCell<T>::borrow_mut
             at /rustc/9748d87dc70a9a6725c5dbd76ce29d04752b4f90/library/core/src/cell.rs:1089:25
   6: <std::io::stdio::StdoutLock as std::io::Write>::write_all
             at /rustc/9748d87dc70a9a6725c5dbd76ce29d04752b4f90/library/std/src/io/stdio.rs:860:20
   7: <std::io::default_write_fmt::Adapter<T> as core::fmt::Write>::write_str
             at /rustc/9748d87dc70a9a6725c5dbd76ce29d04752b4f90/library/std/src/io/mod.rs:628:30
   8: core::fmt::write
             at /rustc/9748d87dc70a9a6725c5dbd76ce29d04752b4f90/library/core/src/fmt/mod.rs:1493:23
   ...

When attaching to a thread by id, instr.put_callout will make the program hang indefinitely, probably a segfault/panic; when attaching to the current thread from the interceptor callback, everything works fine (why does interceptor.attach accept a &mut ref, and not a box or something like this? This leads to UB if the value is dropped)

use std::sync::LazyLock;

use ctor::ctor;
use frida_gum::interceptor::{Interceptor, InvocationContext, InvocationListener};
use frida_gum::stalker::{Event, EventMask, EventSink, Stalker, Transformer};
use frida_gum::{Gum, Process};

static GUM: LazyLock<Gum> = LazyLock::new(Gum::obtain);

struct Sink;

impl EventSink for Sink {
    fn query_mask(&mut self) -> EventMask {
        EventMask::None
    }

    fn start(&mut self) {}

    fn process(&mut self, _event: &Event) {}

    fn flush(&mut self) {}

    fn stop(&mut self) {}
}

struct WriteListener {
    followed: bool,
}

impl InvocationListener for WriteListener {
    fn on_enter(&mut self, _context: InvocationContext) {
        if !self.followed {
            self.followed = true;
            println!("Following self...");
            let transformer = Transformer::from_callback(&GUM, |block, _| {
                for instr in block {
                    let ix = instr.instr();
                    let data = ix.bytes();

                    // syscall
                    if data == [0x0f, 0x05] {
                        // causes a panic
                        println!("damn, the program panicked");
                    }

                    instr.keep();
                }
            });
            let mut stalker = Stalker::new(&GUM);
            stalker.follow_me(&transformer, None::<&mut Sink>);
        }

        println!("entered");
    }

    fn on_leave(&mut self, _context: InvocationContext) {
        println!("left");
    }
}

#[ctor]
fn entry() {
    let process = Process::obtain(&GUM);
    let module = process.find_module_by_name("libc.so.6").unwrap();
    let write = module.find_export_by_name("write").unwrap();
    let mut interceptor = Box::leak(Box::new(Interceptor::obtain(&GUM)));

    // Follow a single thread once
    interceptor.attach(
        write,
        Box::leak(Box::new(WriteListener { followed: false })),
    );
}

use std::collections::HashSet;
use std::sync::{LazyLock, Mutex};
use std::thread;
use std::time::Duration;

use ctor::ctor;
use frida_gum::stalker::{Event, EventMask, EventSink, Stalker, Transformer};
use frida_gum::{Gum, Process};

static GUM: LazyLock<Gum> = LazyLock::new(Gum::obtain);

struct SampleEventSink;

impl EventSink for SampleEventSink {
    fn query_mask(&mut self) -> EventMask {
        EventMask::Exec
    }

    fn start(&mut self) {}

    fn process(&mut self, _event: &Event) {}

    fn flush(&mut self) {}

    fn stop(&mut self) {}
}

fn follow_thread(thread_id: usize) {
    let mut stalker = Stalker::new(&GUM);

    let transformer = Transformer::from_callback(&GUM, |basic_block, _output| {
        for instr in basic_block {
            // Uncommenting any of these makes the process hang
            // instr.put_callout(|_| {});

            instr.keep();

            // Uncommenting any of these makes the process hang
            // instr.put_callout(|_| {});
        }
    });

    stalker.follow(thread_id, &transformer, None::<&mut SampleEventSink>);
}

// a static is not needed here as entry() is called only one time
static FOLLOWED: LazyLock<Mutex<HashSet<usize>>> = LazyLock::new(Mutex::default);

fn entry() {
    println!("entry");

    let process = Process::obtain(&GUM);

    let mut followed = FOLLOWED.lock().unwrap();

    let this_thread_id = process.current_thread_id();
    followed.insert(process.current_thread_id() as usize); // do not follow the thread that was created for following

    // uses enumerate_threads() from PR #213, but I'm pretty sure the implementation returns the correct thread ids
    for thread in process.enumerate_threads() {
        let thread_id = thread.id() as usize;

        if !followed.contains(&thread_id) {
            println!("following {thread_id}; this thread id: {this_thread_id}");
            followed.insert(thread_id);

            // thread::spawn(move || follow_thread(thread_id));
            follow_thread(thread_id)
        }
    }
}

#[ctor]
fn main() {
    thread::spawn(entry);

    // wait enough time for the entry function to run
    thread::sleep(Duration::from_millis(100));
}

injection done using the LD_PRELOAD

ghost avatar Jul 23 '25 23:07 ghost

Can you please split this issue in two, one for each bug?

s1341 avatar Jul 24 '25 04:07 s1341

also, make the titles indicative.

s1341 avatar Jul 24 '25 04:07 s1341