AFLplusplus / LibAFL

Advanced Fuzzing Library - Slot your Fuzzer together in Rust! Scales across cores and machines. For Windows, Android, MacOS, Linux, no_std, ...
Other
1.97k stars 304 forks source link

Openssl's heartbleed vulnerability AFL++ can be exploited, but LibAFL cannot be exploited. #1975

Closed 0xsmirk closed 5 months ago

0xsmirk commented 5 months ago

Question:Openssl's heartbleed vulnerability AFL++ can be exploited, but LibAFL cannot be exploited

LibAFL opensslheartbleed

LibAFL的代码如下

use libafl_bolts::rands::StdRand;
use libafl_bolts::shmem::{ShMem, ShMemProvider, StdShMemProvider};
use libafl_bolts::tuples::tuple_list;
use libafl_bolts::{current_nanos, AsMutSlice};
use libafl::corpus::{Corpus, InMemoryCorpus, OnDiskCorpus};
use libafl::events::SimpleEventManager;
use libafl::executors::{ForkserverExecutor, TimeoutForkserverExecutor};
use libafl::feedbacks::{MaxMapFeedback, TimeFeedback, TimeoutFeedback};
use libafl::inputs::BytesInput;
use libafl::monitors::SimpleMonitor;
use libafl::mutators::{havoc_mutations, StdScheduledMutator};
use libafl::observers::{HitcountsMapObserver, StdMapObserver, TimeObserver};
use libafl::schedulers::{IndexesLenTimeMinimizerScheduler, QueueScheduler};
use libafl::stages::StdMutationalStage;
use libafl::state::{HasCorpus, StdState};
use libafl::{feedback_and_fast, feedback_or, Error, Fuzzer, StdFuzzer};
use std::path::PathBuf;
use std::time::Duration;

/// size of the shared memory mapping used as the coverage map
const MAP_SIZE: usize = 65536;

fn main() -> Result<(), Error> {

    println!("openssl heartbleed libafl fuzzing!!");
    //
    // Component: Corpus
    //

    // path to input corpus
    let corpus_dirs = vec![PathBuf::from("./corpus")];

    // Corpus that will be evolved, we keep it in memory for performance
    let input_corpus = InMemoryCorpus::<BytesInput>::new();

    // Corpus in which we store solutions (timeouts/hangs in this example),
    // on disk so the user can get them after stopping the fuzzer
    let timeouts_corpus = OnDiskCorpus::new(PathBuf::from("./timeouts"))?;

    //
    // Component: Observer
    //

    // Create an observation channel to keep track of the current testcase's execution time
    let time_observer = TimeObserver::new("time");

    // Create an observation channel using the coverage map.
    //
    // The ForkserverExecutor gets a pointer to shared memory from the __AFL_SHM_ID environment
    // variable.
    //
    // further explanation from toka: the edges map pointed by __AFL_SHM_ID is inserted by
    // afl-clang-fast, if you use afl-clang-fast, you can use __AFL_SHM_ID to get the ptr to the
    // map

    // The shmem provider supported by AFL++ for shared memory
    let mut shmem_provider = StdShMemProvider::new()?;

    // The coverage map shared between observer and executor
    let mut shmem = shmem_provider.new_shmem(MAP_SIZE)?;

    // let the forkserver know the shmid
    shmem.write_to_env("__AFL_SHM_ID")?;
    let shmem_buf = shmem.as_mut_slice();

    // Create an observation channel using the signals map
    let edges_observer =
        unsafe { HitcountsMapObserver::new(StdMapObserver::new("shared_mem", shmem_buf)) };

    //
    // Component: Feedback
    //

    // A Feedback, in most cases, processes the information reported by one or more observers to
    // decide if the execution is interesting. This one is composed of two Feedbacks using a logical
    // OR.
    //
    // Due to the fact that TimeFeedback can never classify a testcase as interesting on its own,
    // we need to use it alongside some other Feedback that has the ability to perform said
    // classification. These two feedbacks are combined to create a boolean formula, i.e. if the
    // input triggered a new code path, OR, false.
    let mut feedback = feedback_or!(
        // New maximization map feedback (attempts to maximize the map contents) linked to the
        // edges observer. This one will track indexes, but will not track novelties,
        // i.e. new_tracking(... true, false).
        MaxMapFeedback::tracking(&edges_observer, true, false),
        // Time feedback, this one never returns true for is_interesting, However, it does keep
        // track of testcase execution time by way of its TimeObserver
        TimeFeedback::with_observer(&time_observer)
    );

    // A feedback is used to choose if an input should be added to the corpus or not. In the case
    // below, we're saying that in order for a testcase's input to be added to the corpus, it must:
    //   1: be a timeout
    //        AND
    //   2: have created new coverage of the binary under test
    //
    // The goal is to do similar deduplication to what AFL does
    //
    // The feedback_and_fast macro combines the two feedbacks with a fast AND operation, which
    // means only enough feedback functions will be called to know whether or not the objective
    // has been met, i.e. short-circuiting logic.
    let mut objective =
        feedback_and_fast!(TimeoutFeedback::new(), MaxMapFeedback::new(&edges_observer));

    //
    // Component: Monitor
    //

    // MultiMonitor displays cumulative and per-client statistics (used to be named
    // SimpleStats/MultiStats). It uses LLMP for communication between broker / client(s). It
    // displays 2 clients are connected, even when only a single client is active.
    //
    // further explanation from domenukk: The 0th client is the client that opens a network socket
    // and listens for other clients and potentially brokers. It's still a client from llmp's
    // perspective, so it's more or less an implementation detail.
    let monitor = SimpleMonitor::new(|s| println!("{s}"));

    //
    // Component: EventManager
    //

    // The event manager handles the various events generated during the fuzzing loop
    // such as the notification of the addition of a new testcase to the corpus.
    // The SimpleEventManager is the simplest event manager available to us.
    let mut mgr = SimpleEventManager::new(monitor);

    //
    // Component: State
    //

    // Creates a new State, taking ownership of all of the individual components during fuzzing.
    //
    // On the initial pass, setup_restarting_mgr_std returns (None, LlmpRestartingEventManager).
    // On each successive execution (i.e. on a fuzzer restart), it returns the state from the prior
    // run that was saved off in shared memory. The code below handles the initial None value
    // by providing a default StdState. After the first restart, we'll simply unwrap the
    // Some(StdState) returned from the call to setup_restarting_mgr_std
    let mut state = StdState::new(
        // random number generator with a time-based seed
        StdRand::with_seed(current_nanos()),
        input_corpus,
        timeouts_corpus,
        // States of the feedbacks that store the data related to the feedbacks that should be
        // persisted in the State.
        &mut feedback,
        &mut objective,
    )?;

    //
    // Component: Scheduler
    //

    // A minimization + queue policy to get test cases from the corpus
    //
    // IndexesLenTimeMinimizerCorpusScheduler is a MinimizerCorpusScheduler with a
    // LenTimeMulFavFactor that prioritizes quick and small Testcases that exercise all the
    // entries registered in the MapIndexesMetadata
    //
    // a QueueCorpusScheduler walks the corpus in a queue-like fashion
    let scheduler = IndexesLenTimeMinimizerScheduler::new(QueueScheduler::new());

    //
    // Component: Fuzzer
    //

    // A fuzzer with feedback, objectives, and a corpus scheduler
    let mut fuzzer = StdFuzzer::new(scheduler, feedback, objective);

    //
    // Component: Executor
    //

    // Create an in-process executor. The TimeoutExecutor wraps the InProcessExecutor and sets a
    // timeout before each run. This gives us an executor that will execute a bunch of testcases
    // within the same process, eliminating a lot of the overhead associated with a fork/exec or
    // forkserver execution model.
    let fork_server = ForkserverExecutor::builder()
        .program("./afl_handshake_fuzzer")
        .parse_afl_cmdline(["@@"])
        .coverage_map_size(MAP_SIZE)
        .build(tuple_list!(time_observer, edges_observer))?;

    let timeout = Duration::from_secs(5);

    // wrap the fork server executor and its associated timeout limit
    let mut executor = TimeoutForkserverExecutor::new(fork_server, timeout)?;

    // In case the corpus is empty (i.e. on first run), load existing test cases from on-disk
    // corpus
    if state.corpus().count() < 1 {
        state
            .load_initial_inputs(&mut fuzzer, &mut executor, &mut mgr, &corpus_dirs)
            .unwrap_or_else(|err| {
                panic!(
                    "Failed to load initial corpus at {:?}: {:?}",
                    &corpus_dirs, err
                )
            });
        println!("We imported {} inputs from disk.", state.corpus().count());
    }

    //
    // Component: Mutator
    //

    // Setup a mutational stage with a basic bytes mutator
    let mutator = StdScheduledMutator::new(havoc_mutations());

    //
    // Component: Stage
    //

    let mut stages = tuple_list!(StdMutationalStage::new(mutator));

    fuzzer.fuzz_loop(&mut stages, &mut executor, &mut state, &mut mgr)?;

    Ok(())
}

LibAFL running picture

image

AFL++ openssl heartbleed

image

Same point

The input corpora are all -

image
s1341 commented 5 months ago

Your objective definition appears wrong. Don't you want to use a CrashFeedback?

0xsmirk commented 5 months ago

Your objective definition appears wrong. Don't you want to use a CrashFeedback?

I'll try it

tokatoka commented 5 months ago

try

    let mut objective =
        feedback_and_fast!(CrashFeedback::new(), MaxMapFeedback::new(&edges_observer));
0xsmirk commented 5 months ago

try

    let mut objective =
        feedback_and_fast!(CrashFeedback::new(), MaxMapFeedback::new(&edges_observer));

I try code

 let mut objective =
        feedback_and_fast!(CrashFeedback::new(), MaxMapFeedback::new(&edges_observer));

After running for 4 hours, oobjectives is still 0

image
s1341 commented 5 months ago

How is your clients 1? @tokatoka isn't clients always at least 2? Perhaps the client timed out and was removed and you are not actually fuzzing?

0xsmirk commented 5 months ago

How is your clients 1? @tokatoka isn't clients always at least 2? Perhaps the client timed out and was removed and you are not actually fuzzing?您的客户怎么样 1?@tokatoka客户不是总是至少 2 个吗?也许客户端超时并被删除,而您实际上并没有模糊测试?

LibAFL code

    use libafl_bolts::rands::StdRand;
    use libafl_bolts::shmem::{ShMem, ShMemProvider, StdShMemProvider};
    use libafl_bolts::tuples::tuple_list;
    use libafl_bolts::{current_nanos, AsMutSlice};
    use libafl::corpus::{Corpus, InMemoryCorpus, OnDiskCorpus};
    use libafl::events::SimpleEventManager;
    use libafl::executors::{ForkserverExecutor, TimeoutForkserverExecutor};
    use libafl::feedbacks::{MaxMapFeedback, TimeFeedback, TimeoutFeedback};
    use libafl::inputs::BytesInput;
    use libafl::monitors::SimpleMonitor;
    use libafl::mutators::{havoc_mutations, StdScheduledMutator};
    use libafl::observers::{HitcountsMapObserver, StdMapObserver, TimeObserver};
    use libafl::schedulers::{IndexesLenTimeMinimizerScheduler, QueueScheduler};
    use libafl::stages::StdMutationalStage;
    use libafl::state::{HasCorpus, StdState};
    use libafl::{feedback_and_fast, feedback_or, Error, Fuzzer, StdFuzzer, feedback_or_fast};
    use std::path::PathBuf;
    use std::time::Duration;

    use libafl::feedbacks::CrashFeedback;

    /// size of the shared memory mapping used as the coverage map
    const MAP_SIZE: usize = 65536;

    fn main() -> Result<(), Error> {

        println!("openssl heartbleed libafl fuzzing!!");
        //
        // Component: Corpus
        //

        // path to input corpus
        let corpus_dirs = vec![PathBuf::from("./corpus")];

        // Corpus that will be evolved, we keep it in memory for performance
        let input_corpus = InMemoryCorpus::<BytesInput>::new();

        // Corpus in which we store solutions (timeouts/hangs in this example),
        // on disk so the user can get them after stopping the fuzzer
        let timeouts_corpus = OnDiskCorpus::new(PathBuf::from("./timeouts"))?;

        //
        // Component: Observer
        //

        // Create an observation channel to keep track of the current testcase's execution time
        let time_observer = TimeObserver::new("time");

        // Create an observation channel using the coverage map.
        //
        // The ForkserverExecutor gets a pointer to shared memory from the __AFL_SHM_ID environment
        // variable.
        //
        // further explanation from toka: the edges map pointed by __AFL_SHM_ID is inserted by
        // afl-clang-fast, if you use afl-clang-fast, you can use __AFL_SHM_ID to get the ptr to the
        // map

        // The shmem provider supported by AFL++ for shared memory
        let mut shmem_provider = StdShMemProvider::new()?;

        // The coverage map shared between observer and executor
        let mut shmem = shmem_provider.new_shmem(MAP_SIZE)?;

        // let the forkserver know the shmid
        shmem.write_to_env("__AFL_SHM_ID")?;
        let shmem_buf = shmem.as_mut_slice();

        // Create an observation channel using the signals map
        let edges_observer =
            unsafe { HitcountsMapObserver::new(StdMapObserver::new("shared_mem", shmem_buf)) };

        //
        // Component: Feedback
        //

        // A Feedback, in most cases, processes the information reported by one or more observers to
        // decide if the execution is interesting. This one is composed of two Feedbacks using a logical
        // OR.
        //
        // Due to the fact that TimeFeedback can never classify a testcase as interesting on its own,
        // we need to use it alongside some other Feedback that has the ability to perform said
        // classification. These two feedbacks are combined to create a boolean formula, i.e. if the
        // input triggered a new code path, OR, false.
        let mut feedback = feedback_or!(
            // New maximization map feedback (attempts to maximize the map contents) linked to the
            // edges observer. This one will track indexes, but will not track novelties,
            // i.e. new_tracking(... true, false).
            MaxMapFeedback::tracking(&edges_observer, true, false),
            // Time feedback, this one never returns true for is_interesting, However, it does keep
            // track of testcase execution time by way of its TimeObserver
            TimeFeedback::with_observer(&time_observer)
        );

        // A feedback is used to choose if an input should be added to the corpus or not. In the case
        // below, we're saying that in order for a testcase's input to be added to the corpus, it must:
        //   1: be a timeout
        //        AND
        //   2: have created new coverage of the binary under test
        //
        // The goal is to do similar deduplication to what AFL does
        //
        // The feedback_and_fast macro combines the two feedbacks with a fast AND operation, which
        // means only enough feedback functions will be called to know whether or not the objective
        // has been met, i.e. short-circuiting logic.
        // let mut objective =
        //     feedback_and_fast!(TimeoutFeedback::new(), MaxMapFeedback::new(&edges_observer));

        let mut objective =
            feedback_and_fast!(CrashFeedback::new(), MaxMapFeedback::new(&edges_observer));

        //
        // Component: Monitor
        //

        // MultiMonitor displays cumulative and per-client statistics (used to be named
        // SimpleStats/MultiStats). It uses LLMP for communication between broker / client(s). It
        // displays 2 clients are connected, even when only a single client is active.
        //
        // further explanation from domenukk: The 0th client is the client that opens a network socket
        // and listens for other clients and potentially brokers. It's still a client from llmp's
        // perspective, so it's more or less an implementation detail.
        let monitor = SimpleMonitor::new(|s| println!("{s}"));

        //
        // Component: EventManager
        //

        // The event manager handles the various events generated during the fuzzing loop
        // such as the notification of the addition of a new testcase to the corpus.
        // The SimpleEventManager is the simplest event manager available to us.
        let mut mgr = SimpleEventManager::new(monitor);

        //
        // Component: State
        //

        // Creates a new State, taking ownership of all of the individual components during fuzzing.
        //
        // On the initial pass, setup_restarting_mgr_std returns (None, LlmpRestartingEventManager).
        // On each successive execution (i.e. on a fuzzer restart), it returns the state from the prior
        // run that was saved off in shared memory. The code below handles the initial None value
        // by providing a default StdState. After the first restart, we'll simply unwrap the
        // Some(StdState) returned from the call to setup_restarting_mgr_std
        let mut state = StdState::new(
            // random number generator with a time-based seed
            StdRand::with_seed(current_nanos()),
            input_corpus,
            timeouts_corpus,
            // States of the feedbacks that store the data related to the feedbacks that should be
            // persisted in the State.
            &mut feedback,
            &mut objective,
        )?;

        //
        // Component: Scheduler
        //

        // A minimization + queue policy to get test cases from the corpus
        //
        // IndexesLenTimeMinimizerCorpusScheduler is a MinimizerCorpusScheduler with a
        // LenTimeMulFavFactor that prioritizes quick and small Testcases that exercise all the
        // entries registered in the MapIndexesMetadata
        //
        // a QueueCorpusScheduler walks the corpus in a queue-like fashion
        let scheduler = IndexesLenTimeMinimizerScheduler::new(QueueScheduler::new());

        //
        // Component: Fuzzer
        //

        // A fuzzer with feedback, objectives, and a corpus scheduler
        let mut fuzzer = StdFuzzer::new(scheduler, feedback, objective);

        //
        // Component: Executor
        //

        // Create an in-process executor. The TimeoutExecutor wraps the InProcessExecutor and sets a
        // timeout before each run. This gives us an executor that will execute a bunch of testcases
        // within the same process, eliminating a lot of the overhead associated with a fork/exec or
        // forkserver execution model.
        let fork_server = ForkserverExecutor::builder()
            .program("./afl_handshake_fuzzer")
            .parse_afl_cmdline(["@@"])
            .coverage_map_size(MAP_SIZE)
            .build(tuple_list!(time_observer, edges_observer))?;

        let timeout = Duration::from_secs(5);

        // wrap the fork server executor and its associated timeout limit
        let mut executor = TimeoutForkserverExecutor::new(fork_server, timeout)?;

        // In case the corpus is empty (i.e. on first run), load existing test cases from on-disk
        // corpus
        if state.corpus().count() < 1 {
            state
                .load_initial_inputs(&mut fuzzer, &mut executor, &mut mgr, &corpus_dirs)
                .unwrap_or_else(|err| {
                    panic!(
                        "Failed to load initial corpus at {:?}: {:?}",
                        &corpus_dirs, err
                    )
                });
            println!("We imported {} inputs from disk.", state.corpus().count());
        }

        //
        // Component: Mutator
        //

        // Setup a mutational stage with a basic bytes mutator
        let mutator = StdScheduledMutator::new(havoc_mutations());

        //
        // Component: Stage
        //

        let mut stages = tuple_list!(StdMutationalStage::new(mutator));

        fuzzer.fuzz_loop(&mut stages, &mut executor, &mut state, &mut mgr)?;

        Ok(())
    }

Files appear in the timeouts directory and crash cannot be triggered.

image
s1341 commented 5 months ago

timeouts_corpus is the solutions corpus. Your objectives are only crashes with new coverage. I would define objectives as follows:

let mut objective = CrashFeedback::new();

And rename your timeouts directory 'solutions'....

0xsmirk commented 5 months ago

timeouts_corpus is the solutions corpus. Your objectives are only crashes with new coverage. I would define objectives as follows:timeouts_corpus 是解决方案语料库。你的目标只是具有新覆盖范围的崩溃。我将目标定义如下:

let mut objective = CrashFeedback::new();

And rename your timeouts directory 'solutions'....并将超时目录重命名为“solutions”...。

I'll try it. Thanks

0xsmirk commented 5 months ago

timeouts_corpus is the solutions corpus. Your objectives are only crashes with new coverage. I would define objectives as follows:timeouts_corpus 是解决方案语料库。你的目标只是具有新覆盖范围的崩溃。我将目标定义如下:

let mut objective = CrashFeedback::new();

And rename your timeouts directory 'solutions'....并将超时目录重命名为“solutions”...。

no crash

image
tokatoka commented 5 months ago

it works for me

[Testcase #0] run time: 0h-2m-34s, clients: 1, corpus: 22, objectives: 2, executions: 19938, exec/sec: 128.8
[Objective #0] run time: 0h-2m-35s, clients: 1, corpus: 22, objectives: 3, executions: 20044, exec/sec: 128.7
tokatoka commented 5 months ago
s1341 commented 5 months ago

I was just about to ask if you compiled with asan....

0xsmirk commented 5 months ago

it works for me 它对我有用

[Testcase #0] run time: 0h-2m-34s, clients: 1, corpus: 22, objectives: 2, executions: 19938, exec/sec: 128.8
[Objective #0] run time: 0h-2m-35s, clients: 1, corpus: 22, objectives: 3, executions: 20044, exec/sec: 128.7

Shocked, I'll try again

0xsmirk commented 5 months ago

I was just about to ask if you compiled with asan....我正想问你是不是用asan编译的......

No

0xsmirk commented 5 months ago

Thanks ☕

0xsmirk commented 5 months ago

Yes, ASAN enable 2 minutes crashed

image