tauri-fuzz

The goal of this project is to provide a tool to easily fuzz Tauri applications. tauri-fuzz fuzzes your Tauri app with a special runtime that detects when security boundaries are breached. By security boundaries we mean unsafe interactions with the host system resources.

Fuzzing applications security boundaries

[Disclaimer] tauri-fuzz was tailored to be used with Tauri applications but the fuzzing principles should be reusable to fuzz other types of applications.

Origin of the project

Applications are now a growing part of our daily lives. Many vulnerabilities are present in them which can be used to harm the users. To minimize such vulnerabilities developers need to thoroughly test their applications. One of the most popular way to automatically test your software is called fuzzing.

The principle of a fuzzer is to test a software by executing it with a very large amount of semi-random inputs and to detect any problematic behaviours during these runs. Currently most fuzzers are used to detect memory safety vulnerabilities for popular C libraries.

Why are fuzzers not used for applications?

We see two main reasons:

  • Fuzzing can be hard to setup and requires experience and/or time to be used effectively.
  • Applications are often developed with technologies that are less prone to memory vulnerabilities. So fuzzer default error detection mechanisms do not translate well for applications.

Goal of the project

This project aims to fill this gap and provide fuzzing to applications:

  • We try to facilitate as much as possible the process of fuzzing for Tauri apps
  • We provide a cross-platform runtime that detects any behaviour that breaches the security boundaries of a fuzzed application

Components of tauri-fuzz

More details can be found in the Principles section.

tauri-fuzz

tauri-fuzz contains the runtime which is used during fuzzing to detect whenever the application interacts with external components It uses Frida interceptors to monitor any interactions to a set of system resources specified by a provided fuzz policy. It also contains utilities that facilitate the integration with the web application framework Tauri and the fuzzer framework LibAFL.

tauri-fuzz-policies

A framework to use and create security policies used by the runtime during fuzz time. A fuzz policy defines the security boundaries that will be enforced by the runtime on the application while being fuzzed. For example we can provide to tauri-fuzz a policy that prevents any interaction with the file system. In this configuration, anytime the fuzzed application try to use the file system it will get intercepted by the runtime and get reported as a security breach.

tauri-fuzz-cli

A command line utility that simplifies as much as possible the steps to fuzz a Tauri app. It handles both setting up the fuzzing environment and starting fuzzing instances for a Tauri app.

tl;dr of tauri-fuzz

Observation

Fuzzing is not used during application development

Why?

  • Fuzzing requires time and experience to obtain results
  • Most fuzzers only detect memory corruption and crashes which are less relevant for applications

How do we try to solve this?

  • Make fuzzing as easy as possible with tauri-fuzz-cli that can start fuzzing a Tauri application with few commands
  • Provide a runtime that monitors the interactions between an application and its host system. The runtime will block unsafe interactions which are defined by a provided policy.
  • Provide a generic policy no_error_policy that can be used for all applications. no_error_policy will block and report any interactions with the host system that result into an error. Motivation behind the no_error_policy is that if an application enables errors to happen when interacting with system resources then a malicious attacker could potentially exploit the application to control the system resources to its advantage.

Prerequisites

Dependencies

[Note] tauri-fuzz only works with Tauri 2.x.

Supported platforms

PlatformCan theoretically workTested on
Linux
Windows
MacOS
Android
iOS

Slides

Slide deck that was presented internally at CrabNebula.

[Note] This slide deck is not updated regularly and may contain inaccurate information at the time of read.

Principles

In this section we present the ideas that support our goal of enabling fuzzing for Tauri applications.

Application Fuzzing

The project aims to provide fuzzing as an automatic testing tool for applications. In this section we describe what we think is necessary to popularize the use of fuzzing in application development.

Current state of fuzzing

From our knowledge a majority of fuzzers use memory sanitizers and crashes from the tested software to detect issues during the fuzzing process. This is really effective when fuzzing C/C++ code since it is evaluated that 70% of found vulnerabilities in those languages are memory safety bugs.

However application development are usually done with technologies which are less impacted by memory errors. Therefore usual fuzzing techniques are not suited to test applications.

Another reason where fuzzing is not often used in the applicative world is that fuzzing requires domain knowledge (regarding fuzzing and the tested program) to setup and obtain results. While it makes sense to spend time to fuzz libraries that are shared between numerous projects it is not clear that fuzzing applications is cost effective.

Enabling fuzzing to applications

We believe that popularizing an automatic testing tool such as fuzzing during application development could improve the current state of application security overall. As we said above fuzzing is not integrated into application development for two main reasons: complex to use and not suited.

Make fuzzing Tauri applications as easy as possible

We build a CLI tool tauri-fuzz-cli that tries to make fuzzing as easy as possible for Tauri applications. This CLI does two things:

  • setting up an environment for fuzzing in a Tauri project with one command
  • fuzzing a Tauri command with one command

tauri-fuzz-cli is inspired from cargo-fuzz.

More details on tauri-fuzz-cli can be found in the next chapter.

Fuzzing meaningful safety properties

We propose a different fuzzing technique which tests the security boundaries of an application. Applications are executed on a host system (laptops, smartphones...) which exposes different shared resources to these applications. An example of such resources are host file system, shell, network... The idea behind it is that we want to avoid situations where malicious attackers are able to leverage an application vulnerabilities to access more of the system resources than what the application is supposed to do. To summarize the goal of our fuzzing project is to detect vulnerabilities that allow an attacker to access more of the system resources than intended.

Example: detect illegal interactions with the file system

Fuzzing applications security boundaries

Here is an example. We want to test an application that is able to interact with the network but is not supposed to interact with the file system. We give to tauri-fuzz a policy that the application should not interact with the file system. Hence while being fuzzed, any interactions with the file system will be blocked and reported to the tester. However since we did not tell our fuzzer to monitor interactions with the network, these will be accepted by the fuzzer.

The next section will provide information on how these interactions with the host system are monitored and how can a developer provide a policy.

Security Policies

In this section we present the inner workings of tauri-fuzz.

Security policies

We said in the previous section that we want to fuzz applications security boundaries. Security boundaries define what an application should be allowed to interact with on the host system. But each application have distinct security boundaries. An application A could be allowed interactions with the file system but not the network, application B could be allowed interactions with the file system but only in a specific directory, application C could be allowed interactions with the shell...

How can we fuzz applications effectively knowing that each application have different security boundaries?

Our proposal for this is to describe security boundaries through policies implemented in tauri-fuzz-policies. You can implement and customize these policies to fuzz your Tauri application more effectively.

How to provide a suitable policy to fuzz my Tauri application

We propose three ways to use a suitable policy for your application:

  • create a policy manually
  • derive a policy from Tauri configuration
  • use the generic policy

Create a policy tailored to your application

Policies that are provided to tauri-fuzz are defined in our crate tauri-fuzz-policies. These policies can be implemented by the application developer and provided to the fuzzer. A basic set of policies have already been implemented in tauri-fuzz-policies and can be reused by the user.

A list of available policies are presented in the user guide. A guide on how to create your own policy is also available here.

Create a policy based on the Tauri app configuration

[Disclaimer] This is still ongoing work and has not been implemented in tauri-fuzz

Tauri applications have a capability system which describes what features the application frontend is allowed to use. These capabilities can also be seen as the security boundaries of a Tauri application frontend.

The objective would be able to automatically derive a policy for fuzzing from the capability configuration of a Tauri app. While this is not an exact mapping of the security boundaries of the whole application this is still an approximation.

Generic policy: no error policy

[Disclaimer] This is still ongoing work and is not complete yet

Since our goal was to make fuzzing Tauri applications as easy as possible we don't want to force users to write their own policy to fuzz their application. We came up with a policy no_error_policy that is relevant enough to fuzz most applications.

The no_error_policy monitors all the interaction with the system resources and blocks only when an interaction returns a status error.

No error on return policy

In the schema we can see the situation where the application interacts with the file system. In one case the file system access went well and is let through by our fuzzer runtime. In the other case an error occurred which results in a return error status. The fuzzer runtime will block this return error and report the occurrence in the fuzzing report as a case to investigate.

The motivation behind the no_error_policy is that if an application enables errors to happen when accessing system resources then a malicious attacker could potentially leverage the application to control the system resources to its advantage. The no_error_policy aims to detect vulnerabilities that could be exploited via input manipulation by a malicious attacker. Since fuzzing uses pseudo random data we expect that most of the time these vulnerabilities would appear as syntax errors.

This idea was inspired from the fuzzer Witcher.

Runtime

In this section we explain how our runtime monitors interactions between the fuzzed application and the host system.

Concept

The concept of our runtime is simple; the runtime monitors calls to a specific set of functions of the target program during fuzzing. The set of functions monitored is chosen based on the policy provided by the user. tauri-fuzz runtime has fine-grained monitoring. It can not only detect function calls of target functions but can also inspect their parameters during a call or the return value when returning from them.

Example

Runtime monitors calls to open and open64

For example, a user provides a policy which forbids interactions with the file named foo.txt. On Linux, the runtime will start monitoring the libc functions that are mandatory to interact with the file system access which are open and open64. Moreover since the policy provided specifies that we want to block access to foo.txt the runtime will only block calls to open and open64 where foo.txt is the target file.

Frida

We use the binary instrumentation toolkit Frida to monitor function calls. Frida interceptors are used to inspect: arguments of function calls or return value of function return. The reasons why we used Frida are two folds:

  • Frida works on multiple platform: Linux, Windows, MacOS, Android, iOS. So tauri-fuzz can also be cross-platform.
  • LibAFL a state-of-the-art fuzzer also has integration with Frida. This allows us to build a performant fuzzer through LibAFL which shares the same binary instrumentation toolkit with our runtime.

The Fuzzer

In this section we explain how our runtime and policies are integrated into LibAFL.

LibAFL

For simplicity tauri-fuzz provides a default implementation of a fuzzer which is built using LibAFL. LibAFL is a framework to build a fuzzers and integrate state-of-the-art tools to do so.

Moreover LibAFL has a crate libafl_frida to build Frida-based fuzzers. These fuzzers possess features to improve fuzzing efficiency such as code coverage or logging of conditional statements. Since our runtime is also based on Frida, integration of our runtime with libafl_frida is simpler and our default fuzzer benefits from the performance of LibAFL. This also gives us the possibility to fuzz our applications in the platforms supported by Frida: Linux, Windows, MacOS, Android and IOS.

Can we use other fuzzers?

While LibAFL and tauri-fuzz are both using Frida they still use different parts of it. tauri-fuzz uses Frida Interceptors to monitor function calls while libafl_frida uses Frida stalker to do dynamic code instrumentation. Therefore we believe it's possible to provide a variant of our runtime that could work with other fuzzers without too much issues.

This has not been investigated and is still work in progress so take these claims with a pinch of salt.

User Guide

This section presents how to use tauri-fuzz on your Tauri app.

Quick Start

Goal

We will fuzz a very minimal Tauri application. The repository features a minimal example called mini-app. This example app will be used to showcase how to setup fuzzing with tauri-fuzz.

Fuzzing a Tauri application

We are using mini-app that implements a simple Tauri command, that will later be fuzzed.

Tauri app structure
mini-app
- ...
- src/
- src-tauri/
    - src/
        - lib.rs
        - main.rs
        - tauri_commands/
            - file_access.rs
            - read_foo_file
            - ...
    - Cargo.toml

Make the Tauri Application Accessible to the Fuzzer

The Tauri app backend must be compiled as a crate such that the Tauri commands are exposed to the fuzzer.

For example we want to fuzz the Tauri commands called read_foo_file:

`mini-app/src-tauri/Cargo.toml`
[package]
name = "mini-app"
version = "0.0.0"
description = "A Tauri App"

# This section is automatic in Tauri v2
[lib]
crate-type = ["staticlib", "cdylib", "rlib"]
`mini-app/src-tauri/lib.rs`
/// define the module
pub mod tauri_commands;

/// publicly re-export the Taur command `read_foo_file`
pub use tauri_commands::file_access::read_foo_file
`mini-app/src-tauri/src-tauri/tauri_commands/file_access.rs`
#[tauri::command]
/// Mark the function as public
pub fn read_foo_file() -> String {
    let path        = get_foo_path();
    let mut content = String::new();
    let mut file    = File::open(path).unwrap();
    file.read_to_string(&mut content).unwrap();
    content
}

Fuzzing our Tauri app, quick guide

[Note] This section requires the CLI utility cargo-tauri-fuzz The project contains the crate crates/tauri-fuzz-cli that builds the binary cargo-tauri-fuzz. If any issue arises from using the CLI we recommend you follow the manual steps guide

1. Create fuzz directory

Execute cargo-tauri-fuzz init in mini-app/src-tauri.

Tauri app structure with fuzz directory
Project
- ...
- src/
  - ...
- src-tauri/
    - src/
        - lib.rs
        - main.rs
        - tauri_commands/
            - file_access.rs
            - read_foo_file
            - ...
    - fuzz/
        - build.rs
        - Cargo.toml
        - fuzz_targets/
            - _template_.rs
            - _template_full_.rs
        - fuzzer_config.toml
        - README.md
        - tauri.conf.json
    - Cargo.toml

2. Write your fuzz target

  • Copy mini-app/src-tauri/fuzz/fuzz_targets/_template_.rs as mini-app/src-tauri/fuzz/fuzz_targets/fuzz_read_foo.rs
  • Fill mini-app/src-tauri/fuzz/fuzz_targets/fuzz_read_foo.rs with relevant information
`mini-app/src-tauri/fuzz/fuzz_targets/fuzz_read_foo.rs`

Here we will fuzz the Tauri command read_foo against a policy that does not allow any file access.

// Copyright 2023-2024 CrabNebula Ltd., Alexandre Dang
// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0

// This is a template to create a fuzz target
//
// Steps:
// 1. Copy this file and rename it
// 2. Change the target details below
// 3. Add the new fuzz target in [[bin]] table in Cargo.toml of your project
//
// Note: you may need to implement [FromRandomBytes] for your command argument types.

tauri_fuzz::fuzz_tauri_command! {
    // Name of the tauri command you want to fuzz
    command: "read_foo_file",
    // Pointer to the tauri command you want to fuzz
    path: {{crate_name_underscored}}::file_access::read_foo_file,
    // Parameters names and types to the tauri command
    parameters: {
        name: String,
    },
    // Policy chosen for the fuzzing
    // Here the policy will not allow any access to the filesystem
    policy: tauri_fuzz_policies::filesystem::no_file_access(),
}

3. Add the fuzz target as binary

Add fuzz_read_foo as a binary in mini-app/src-tauri/fuzz/Cargo.toml

`mini-app/src-tauri/fuzz/Cargo.toml`
[package]
name = "{{ crate_name }}-fuzz"
version = "0.0.0"
publish = false
edition = "2021"

[workspace]

[build-dependencies]
tauri-build = "2.0"

[dependencies]
{{ crate_name }} = { path = ".." }
tauri-fuzz-policies = { git = "ssh://git@github.com/crabnebula-dev/tauri-fuzz.git" }
tauri-fuzz = { git = "ssh://git@github.com/crabnebula-dev/tauri-fuzz.git", features = ["tauri"] }
tauri = { version = "2.0", features = ["test"]}
libafl = "0.13"

# Uncomment this block to add `fuzz_read_foo` as a fuzz target
# [[bin]]
# name = "fuzz_read_foo"
# path = "fuzz_targets/fuzz_read_foo.rs"
# doc = false

4. Start fuzzing

Start fuzzing by executing one of these commands.

From mini-app/src-tauri/ directory:

cargo-tauri-fuzz fuzz fuzz_read_foo

Or from mini-app/src-tauri/fuzz/ directory:

cargo r --bin fuzz_read_foo

5. Check your solutions

You should see this result repeatedly on your terminal:

Fuzzing results
[ERROR policies::engine] Policy was broken at function [open].
    Description: Access to [open] denied
    Rule: Rule::OnEntry
    Context: Function entry with parameters: [140736965046336, 524288]
The application panicked (crashed).
Message:  Intercepting call to [open].
Policy was broken at function [open].
Description: Access to [open] denied
Rule: Rule::OnEntry
Context: Function entry with parameters: [140736965046336, 524288]

This is the expected result. The Tauri command we fuzz, read_foo_file, tries to read foo.txt but got intercepted since we are fuzzing with a policy that does not allow any access to the filesystem.

More precisely the message specifies Policy was broken at function [open]. Indeed read_foo_file tried to use the libc function open that is used to access files and got intercepted.

The inputs used by the fuzzer which provokes a policy breach are stored in mini-app/src-tauri/fuzz/fuzz_solutions. Those inputs can be then investigated to understand why the policy breach happened.

Manual Steps to Fuzz your Tauri App

This is an extension to the [Quick Start guide][quick_start.md]. The different steps to fuzz a Tauri app are detailed here.

We will fuzz a very minimal Tauri application. The repository features a minimal example called mini-app. This example app will be used to showcase how to setup fuzzing with tauri-fuzz.

Prepare your Tauri App

Tauri app structure
mini-app
- ...
- src/
- src-tauri/
    - src/
        - lib.rs
        - main.rs
        - tauri_commands/
            - file_access.rs
            - read_foo_file
            - ...
    - Cargo.toml

The Tauri app backend must be compiled as a crate such that the Tauri commands are exposed to the fuzzer.

For example we want to fuzz the Tauri commands called read_foo_file:

`mini-app/src-tauri/Cargo.toml`
[package]
name = "mini-app"
version = "0.0.0"
description = "A Tauri App"

# This section is automatic in Tauri v2
[lib]
crate-type = ["staticlib", "cdylib", "rlib"]
`mini-app/src-tauri/lib.rs`
/// define the module
pub mod tauri_commands;

/// publicly re-export the Taur command `read_foo_file`
pub use tauri_commands::file_access::read_foo_file
`mini-app/src-tauri/src-tauri/tauri_commands/file_access.rs`
#[tauri::command]
/// Mark the function as public
pub fn read_foo_file() -> String {
    let path        = get_foo_path();
    let mut content = String::new();
    let mut file    = File::open(path).unwrap();
    file.read_to_string(&mut content).unwrap();
    content
}

Create the application fuzz package

We will obtain this project structure:

Tauri app structure with fuzz directory
Project
- ...
- src-tauri
    - src
        - lib.rs
        - main.rs
        - tauri_commands
            - file_access.rs
            - read_foo_file
            - ...
    - fuzz
        - build.rs
        - Cargo.toml
        - fuzz_targets/
            - _template_.rs
            - _template_full_.rs
        - fuzzer_config.toml
        - README.md
        - tauri.conf.json
    - Cargo.toml

With the CLI cargo-tauri-fuzz

[Note] This section requires the CLI utility cargo-tauri-fuzz The project contains the crate crates/tauri-fuzz-cli that builds the binary cargo-tauri-fuzz. If any issue arises from using the CLI we recommend you follow the manual steps guide

Execute cargo-tauri-fuzz init in mini-app/src-tauri.

Setup the fuzz directory manually

You can copy-paste the example in the repo.

  1. Create the fuzz directory
mkdir -p mini-app/src-tauri/fuzz
  1. Add Cargo.toml in the fuzz directory
`mini-app/src-tauri/fuzz/Cargo.toml`
[package]
name = "{{ crate_name }}-fuzz"
version = "0.0.0"
publish = false
edition = "2021"

[workspace]

[build-dependencies]
tauri-build = "2.0"

[dependencies]
{{ crate_name }} = { path = ".." }
tauri-fuzz-policies = { git = "ssh://git@github.com/crabnebula-dev/tauri-fuzz.git" }
tauri-fuzz = { git = "ssh://git@github.com/crabnebula-dev/tauri-fuzz.git", features = ["tauri"] }
tauri = { version = "2.0", features = ["test"]}
libafl = "0.13"

# Uncomment this block to add `fuzz_read_foo` as a fuzz target
# [[bin]]
# name = "fuzz_read_foo"
# path = "fuzz_targets/fuzz_read_foo.rs"
# doc = false
  1. Add fuzz_targets directory with templates
mkdir -p mini-app/src-tauri/fuzz/fuzz_targets
touch mini-app/src-tauri/fuzz_targets/_template_.rs
touch mini-app/src-tauri/fuzz_targets/_template_full_.rs
`mini-app/src-tauri/fuzz/fuzz_targets/_template_.rs`
// Copyright 2023-2024 CrabNebula Ltd., Alexandre Dang
// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0

// This is a template to create a fuzz target
//
// Steps:
// 1. Copy this file and rename it
// 2. Change the target details below
// 3. Add the new fuzz target in [[bin]] table in Cargo.toml of your project
//
// Note: you may need to implement [FromRandomBytes] for your command argument types.

tauri_fuzz::fuzz_tauri_command! {
    // Name of the tauri command you want to fuzz
    command: "read_foo_file",
    // Pointer to the tauri command you want to fuzz
    path: {{crate_name_underscored}}::file_access::read_foo_file,
    // Parameters names and types to the tauri command
    parameters: {
        name: String,
    },
    // Policy chosen for the fuzzing
    // Here the policy will not allow any access to the filesystem
    policy: tauri_fuzz_policies::filesystem::no_file_access(),
}
`mini-app/src-tauri/fuzz/fuzz_targets/_template_full_.rs`
// Copyright 2023-2024 CrabNebula Ltd., Alexandre Dang
// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0

use libafl::inputs::{BytesInput, HasMutatorBytes};
use libafl::prelude::ExitKind;
use tauri::test::{mock_builder, mock_context, noop_assets, MockRuntime};
use tauri::webview::InvokeRequest;
use tauri::WebviewWindow;
/// This is a template to create a fuzz target
///
/// Steps:
/// 1. Copy this file and rename it
/// 2. Change `COMMAND_NAME` const value on line 25
/// 3. Change the path to your command in `tauri::generate_handler` on line 44
/// 4. Modify `create_request` to create arguments for your command on line 63
/// 5. Finally add the new fuzz target in [[bin]] table in Cargo.toml of your project
///
/// Note: you may need to implement [FromRandomBytes] for your command argument types.
///
use tauri_fuzz::tauri::{
    create_invoke_request, invoke_command_minimal, CommandArgs, FromRandomBytes,
};
use tauri_fuzz::SimpleFuzzerConfig;

const COMMAND_NAME: &str = "read_foo_file";

fn main() {
    let fuzz_dir = std::path::PathBuf::from(std::env!("CARGO_MANIFEST_DIR"));
    let fuzz_config_file = fuzz_dir.join("fuzzer_config.toml");
    let options = SimpleFuzzerConfig::from_toml(fuzz_config_file, COMMAND_NAME, fuzz_dir).into();
    tauri_fuzz::fuzz_main(
        harness,
        &options,
        harness as *const () as usize,
        tauri_fuzz_policies::filesystem::no_file_access(),
        false,
    );
}

// Setup the Tauri application mockruntime and an associated "main" webview
fn setup_mock() -> WebviewWindow<MockRuntime> {
    let app = mock_builder()
        .invoke_handler(
            tauri::generate_handler![{{crate_name_underscored}}::file_access::read_foo_file],
        )
        .build(mock_context(noop_assets()))
        .expect("Failed to init Tauri app");
    let webview = tauri::WebviewWindowBuilder::new(&app, "main", Default::default())
        .build()
        .unwrap();
    webview
}

// Harness function that will be repeated extensively by the fuzzer with semi-random bytes
// inputs
fn harness(input: &BytesInput) -> ExitKind {
    let webview = setup_mock();
    let _ = invoke_command_minimal(webview, create_request(input.bytes()));
    ExitKind::Ok
}

// Helper code to create an `InvokeRequest` to send to the Tauri app backend
fn create_request(bytes: &[u8]) -> InvokeRequest {
    let mut params = CommandArgs::new();

    let param = String::from_random_bytes(&bytes).unwrap();
    params.insert("name", param);

    create_invoke_request(None, COMMAND_NAME, params)
}
  1. Add build.rs and tauri.conf.json
`mini-app/src-tauri/fuzz/build.rs`
// Copyright 2023-2024 CrabNebula Ltd., Alexandre Dang
// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0

/// Important: keep it here.
/// Currently this is necessary for Windows
fn main() {
    tauri_build::build()
}
`mini-app/src-tauri/fuzz/tauri.conf.json`
{
  "productName": "{{ crate_name }}-fuzz",
  "version": "0.0.0",
  "identifier": "dev.crabnebula.{{ crate_name }}-fuzz",
  "bundle": {
    "active": true,
    "targets": "all",
    "icon": ["../icons/icon.ico"]
  }
}

Writing a Fuzz Target

We will finally create our fuzz target. We fuzz the Tauri commands read_foo_file which tries to read the file foo.txt. The fuzz policy that we will choose is tauri-fuzz-policies::file_policy::no_file_access() that do not allow access to the filesystem.

There are two ways to write the fuzz target:

  • with a Rust macro fuzz_tauri_command
  • manually by filling the template

Fill the template with macro

  • Copy mini-app/src-tauri/fuzz/fuzz_targets/_template_.rs as mini-app/src-tauri/fuzz/fuzz_targets/fuzz_read_foo.rs
  • Fill mini-app/src-tauri/fuzz/fuzz_targets/fuzz_read_foo.rs with relevant information
`mini-app/src-tauri/fuzz/fuzz_targets/fuzz_read_foo.rs`

Here we will fuzz the Tauri command read_foo against a policy that does not allow any file access.

// Copyright 2023-2024 CrabNebula Ltd., Alexandre Dang
// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0

// This is a template to create a fuzz target
//
// Steps:
// 1. Copy this file and rename it
// 2. Change the target details below
// 3. Add the new fuzz target in [[bin]] table in Cargo.toml of your project
//
// Note: you may need to implement [FromRandomBytes] for your command argument types.

tauri_fuzz::fuzz_tauri_command! {
    // Name of the tauri command you want to fuzz
    command: "read_foo_file",
    // Pointer to the tauri command you want to fuzz
    path: {{crate_name_underscored}}::file_access::read_foo_file,
    // Parameters names and types to the tauri command
    parameters: {
        name: String,
    },
    // Policy chosen for the fuzzing
    // Here the policy will not allow any access to the filesystem
    policy: tauri_fuzz_policies::filesystem::no_file_access(),
}

[Disclaimer] Our macro is not stable yet it may not work for complex cases. For more control over the fuzzing we suggest that you write the fuzz target manually by following the next section.

Fill the template with no macro

We are going to copy and modify the template file provided by

`crates/cli/template/fuzz_targets/_template_full_.rs`
// Copyright 2023-2024 CrabNebula Ltd., Alexandre Dang
// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0

use libafl::inputs::{BytesInput, HasMutatorBytes};
use libafl::prelude::ExitKind;
use tauri::test::{mock_builder, mock_context, noop_assets, MockRuntime};
use tauri::webview::InvokeRequest;
use tauri::WebviewWindow;
/// This is a template to create a fuzz target
///
/// Steps:
/// 1. Copy this file and rename it
/// 2. Change `COMMAND_NAME` const value on line 25
/// 3. Change the path to your command in `tauri::generate_handler` on line 44
/// 4. Modify `create_request` to create arguments for your command on line 63
/// 5. Finally add the new fuzz target in [[bin]] table in Cargo.toml of your project
///
/// Note: you may need to implement [FromRandomBytes] for your command argument types.
///
use tauri_fuzz::tauri::{
    create_invoke_request, invoke_command_minimal, CommandArgs, FromRandomBytes,
};
use tauri_fuzz::SimpleFuzzerConfig;

const COMMAND_NAME: &str = "read_foo_file";

fn main() {
    let fuzz_dir = std::path::PathBuf::from(std::env!("CARGO_MANIFEST_DIR"));
    let fuzz_config_file = fuzz_dir.join("fuzzer_config.toml");
    let options = SimpleFuzzerConfig::from_toml(fuzz_config_file, COMMAND_NAME, fuzz_dir).into();
    tauri_fuzz::fuzz_main(
        harness,
        &options,
        harness as *const () as usize,
        tauri_fuzz_policies::filesystem::no_file_access(),
        false,
    );
}

// Setup the Tauri application mockruntime and an associated "main" webview
fn setup_mock() -> WebviewWindow<MockRuntime> {
    let app = mock_builder()
        .invoke_handler(
            tauri::generate_handler![{{crate_name_underscored}}::file_access::read_foo_file],
        )
        .build(mock_context(noop_assets()))
        .expect("Failed to init Tauri app");
    let webview = tauri::WebviewWindowBuilder::new(&app, "main", Default::default())
        .build()
        .unwrap();
    webview
}

// Harness function that will be repeated extensively by the fuzzer with semi-random bytes
// inputs
fn harness(input: &BytesInput) -> ExitKind {
    let webview = setup_mock();
    let _ = invoke_command_minimal(webview, create_request(input.bytes()));
    ExitKind::Ok
}

// Helper code to create an `InvokeRequest` to send to the Tauri app backend
fn create_request(bytes: &[u8]) -> InvokeRequest {
    let mut params = CommandArgs::new();

    let param = String::from_random_bytes(&bytes).unwrap();
    params.insert("name", param);

    create_invoke_request(None, COMMAND_NAME, params)
}

Next steps:

  • Fill COMMAND_NAME with read_foo_file
const COMMAND_NAME: &str = "read_foo_file";
  • in the harness, generate the Tauri app with the handle mini-app::tauri_commands::file_access::read_foo_file
.invoke_handler(tauri::generate_handler![mini_app::tauri_commands::file_access::read_foo_file])
  • fill the create_payload to invoke your Tauri command with the right parameters
fn create_payload(_bytes: &[u8]) -> InvokePayload {
    let args = CommandArgs::new();
    create_invoke_payload(None, COMMAND_NAME, args)
}
  • specify the policy you want to apply
tauri-fuzz-policies::file_policy::no_file_access()

Start Fuzzing

Start fuzzing by executing one of these commands.

From mini-app/src-tauri/ directory:

cargo-tauri-fuzz fuzz fuzz_read_foo

Or from mini-app/src-tauri/fuzz/ directory:

cargo r --bin fuzz_read_foo

Validate Fuzzing Results

You should see this result repeatedly on your terminal:

Fuzzing results
[ERROR policies::engine] Policy was broken at function [open].
    Description: Access to [open] denied
    Rule: Rule::OnEntry
    Context: Function entry with parameters: [140736965046336, 524288]
The application panicked (crashed).
Message:  Intercepting call to [open].
Policy was broken at function [open].
Description: Access to [open] denied
Rule: Rule::OnEntry
Context: Function entry with parameters: [140736965046336, 524288]

This is the expected result. The Tauri command we fuzz, read_foo_file, tries to read foo.txt but got intercepted since we are fuzzing with a policy that does not allow any access to the filesystem.

More precisely the message specifies Policy was broken at function [open]. Indeed read_foo_file tried to use the libc function open that is used to access files and got intercepted.

The inputs used by the fuzzer which provokes a policy breach are stored in mini-app/src-tauri/fuzz/fuzz_solutions. Those inputs can be then investigated to understand why the policy breach happened.

Available policies

These list the policy that are currently available in our fuzzing. The policies can be combined to get more complex policies.

ClassPolicyUsageDescription
GenericNo Policytauri-fuzz-policies::no_policy()No functions are monitored and this will not provoke crashes. Used if your fuzz target can inherently crash and you just want to investigate those.
Rule HelperBlock on entrytauri-fuzz-policies::block_on_entry()The function monitored with this rule will just automatically crash when called.
File System policiesNo file accesstauri-fuzz-policies::file_policy::no_file_access()Any access to file system will provoke a crash.
Read only accesstauri-fuzz-policies::file_policy::read_only_access()Any access to file system with write access will provoke a crash.
No access to filenamestauri-fuzz-policies::file_policy::no_access_to_filenames(filenames)Any access to the files given as parameter will provoke a crash.
Child processInvocation of child process through Rust std is blockedtauri-fuzz-policies::external_process::block_on_entry()Any child process created through Rust std::process is blocked
Invocation of child process through Rust std is monitoredtauri-fuzz-policies::external_process::block_monitored_binaries(binaries)Any child process created through Rust std::process is monitored and specified binaries are blocked
Block any child process created through Rust std returning an errortauri-fuzz-policies::external_process::block_rust_api_return_error()Any child process created through Rust std::process will be blocked if returning an error status
Block any child process returning an errortauri-fuzz-policies::external_process::block_on_libc_wait_error_status()Any child process created and waited with wait, waitpid or waitid will be blocked if returning an error status
GenericBlock any calls to the host system that returns an errortauri-fuzz-policies::no_error_policy()We plan to monitor: child processes, file system and networking (ongoing work)

Write your own policy

You can write a policy of your own that suits your needs. To do so we recommend you write your own policy. A template is available at tauri-fuzz-policies/src/policies/policy_template.rs.

// Copyright 2023-2024 CrabNebula Ltd., Alexandre Dang
// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0

/// A template to create a `FuzzPolicy`

// A function that will create our `FuzzPolicy` at runtime
pub fn no_file_access() -> FuzzPolicy {
    // A `FuzzPolicy` is a vector of `FunctionPolicy`.
    //
    // A `FunctionPolicy` will attached itself on a function and its
    // rule will be checked when executing the function.
    vec![
        FunctionPolicy {
            // Name of the function monitored
            name: "open".into(),

            // Library in which the function monitored resides.
            // If it's a Rust crate, due to static linking the lib will
            // corresponds to the binary
            // If it's libc it's a dynamic library you can give the libc name directly
            lib: LIBC.into(),

            // Rule that the function will need to follow to respect the `FunctionPolicy`
            rule: Rule::OnEntry(block_on_entry),

            // Description used when an execution does not respect the rule specified above
            description: "Access to [fopen] denied".into(),

            // Number of parameters the function takes
            nb_parameters: 2,

            // Specify if we are monitoring a Rust function
            is_rust_function: false,
        },
        // We also monitor a second function that can violate our security policy
        FunctionPolicy {
            name: "open64".into(),
            lib: LIBC.into(),
            rule: Rule::OnEntry(block_on_entry),
            description: "Access to [open64] denied".into(),
            nb_parameters: 2,
            is_rust_function: false,
        },
    ]
}

tauri-fuzz-cli

We created a cli cargo-tauri-fuzz to facilitate the setting up the fuzzing environment of your Tauri application.

Setup the fuzz directory

Create the src-tauri/fuzz/ directory in your Tauri app backend code src-tauri/.

cargo-tauri-fuzz init

Fuzz

Fuzz a target that is specified in src-tauri/fuzz/Cargo.toml

cargo-tauri-fuzz fuzz [fuzz_target]

Analyze the fuzz results

Check the results in src-tauri/fuzz/fuzz_solutions/[fuzz target]_solutions/.

Bibliography

This is the bibliography on fuzzing.

Fuzzer

Generic Fuzzers

  • hongfuzz
  • Afl++
  • LibAFL
  • Jackalope:
    • framework to easily build a black-box fuzzer
    • uses TinyInst

Rust Fuzzers

  • cargo-fuzz: tool to invoke libfuzzer
  • libfuzzer: archived
  • cargodd
  • afl.rs: crate from AFL
  • cargo-libafl: wrapper around simple libafl fuzzer
  • fuzzcheck: not updated since a year

Java Serialization Fuzzers

Problems

  • Java serialization is flawed and input stream are converted to Object
  • Attacker can feed any kind of byte stream to be deserialized and can trigger gadget execution
  • This is much more difficult in Rust since target type for deserialization is defined at compile time
  • This could be done if the deserialization has intricate invariant checking

Tools

ODDFuzz

  • ODD for Open Dynamic deserialization
  • uses lightweight taint analysis to identify potential gadget chains
  • new guided fuzzing towards sensitive code rather than coverage

Javascript Engine Fuzzers

  • Fuzzilli
    • generates synctatically and semantically valid JS scripts for fuzzing
    • mutates over a custom intermediate language rather than source or AST
  • JIT-Picking
    • Differential Fuzzing of JavaScript Engines
    • differential fuzz JS engines with and without JIT optimizations
    • transparent probing of the state so it does not interfere with JIT optimizations
    • an execution hash depending on the observed variables values/types is calculated along the execution and sent to the fuzzer at the end for comparison
  • Montage
    • neural network guided fuzzer
    • fuzz JS engines

Fuzzer for Windows

  • WinAFL: AFL-based fuzzer for Windows

Fuzzer Composition

Definition

  • there are no generic best fuzzers
  • fuzzers perform differently depending on targets and resource usage

Tools

autofz: compose a set of fuzzers to use depending on the target and fuzz "trend" at runtime

Concurrency

DDrace: specialized in use-after free (UAF)

  • reduce search space by targeting potentially vulnerable block code
  • new metric to calculate "UAF distance"

Webapp fuzzing

in detail in next chapter

Trusted Environment Fuzzing

  • Trusted App (TA) using Trusted Execution Environment (TEE)
    • Challenge: this is harder than blackbox because the TEE prevents runtime analysis
    • you can only use inputs and outputs coming out from the TEE
    • TEEzz

Fuzzing during RTL development stage (specifications)

  • Advantage: fuzzing is done before production of the system therefore patching is less costly
  • SpecDoctor
    • focuses against transient vulnerabilities
    • proposes a fuzzing template to emulate different scenarios (which part of the system is compromised)
    • uses differential fuzzing to identify side-channel behaviour

Spec fuzzing

use fuzzing to test the completeness of a specification

Fast

  • Fast produces mutations on a program code we call CODE
  • the goal is
  • CODE mutants of the target program are both tested against
    • the original program test suite
    • against the Move prover
      • the Move prover takes both CODE and SPEC
      • CODE and SPEC will be compiled into Boogie
      • you can then uses an SMT solver to solve the Boogie input
  • results from both the test suite and the move prover can be compared to point out potential omission in the SPEC

Resources

Web applications fuzzing

Challenges

What challenges are specific to web applications?

  • webapps have many components that we don't want to fuzz
    • web server that takes HTTP request
    • data storage
    • most likely a code runtime
    • the app we want to test
  • Enabling fuzzing for webapps
    • detecting inputs that triggers vulnerabilities
      • binary fuzzing usually detects segfault
    • generating valid inputs for end-to-end execution
      • inputs need to be valid HTTP requests
      • inputs need to possess the necessary input parameters for the webapp logic
  • Improving fuzzing for webapps
    • collecting coverage information
      • not always possible with web applications
    • mutating inputs effectively
      • little research has been done on mutation strategy on webapps currently

Types of fuzzer for webapps

Fuzzing in web apps is still young.

  • Blackbox
    • Pros/Cons
      • ++ you don't need source code
      • -- the inputs space is restrained in webapps and need manual meddling
      • -- vulnerabilities are inferred based on the output of the webapp which is not precise
  • Whitebox
    • no recent papers using this approach
    • Pros/Cons
      • -- requires source code
      • -- usually uses language model making them language-specific
      • -- requires more effort to implement
      • -- does not scale well to real-word applications
      • ++ the fuzzing is the most complete
  • Greybox
    • really few papers of this type but it looks promising
    • Pros/Cons
      • ++ you don't necessarily need source code
      • ++ extra information makes the fuzzing more efficient
      • ++ scales well

Industry solution

WebFuzz

Date: 2021 Github

Greybox fuzzer targeted at PHP web applications specialized for XSS vulnerabilities

Contributions

  • greybox fuzzer targeted at PHP web applications specialized for XSS vulnerabilities
  • bug injection technique in PHP code
    • useful to evaluate webFuzz and other bug-finding techniques in webapps

Fuzzer

  • uses edge coverage on PHP server code
  • workflow
    • fuzzer fetches any GET or POST request that has been uncovered by a crawler
    • sends the request to the webapp
    • reads its HTTP response and coverage feedback
      • http is parsed to uncover new potential HTTP requests and XSS vulnerabilities
      • if feedback is favorable, store the HTTP request for further mutations
    • loop
  • HTTP requests mutation
    • modify parameters of POST and GET request
    • 5 mutations techniques are employed
      • insertion of real XSS payloads
      • mixing GET or POST parameters from previously interesting requests
      • insertion of randomly generated strings
      • insertion of HTML, JS or PHP tokens
      • altering the type of a parameter
  • web crawling
    • HTTP responses are parsed and analysed to crawl the whole app
    • extract new fuzz targets from anchor and form elements
    • retrieve inputs from input, textarea and option elements
  • vulnerability detection
    • look for stored and reflective XSS vulnerabilities
      • stored XSS when JS is stored in the webapp data
      • reflective XSS vuln when JS from an HTTP request is reflected on the webapp
    • HTML responses are parsed and analysed to discover code in
      • link attribute (e.g. href) that start with the javascrip: label
      • executable attribute that starts with the on prefix (e.g. onclick)
      • script elements
    • fuzzer injects XSS payloads in the HTTP requests to call alert()
      • fuzzer detector check for any calls to alert()
  • corpus selection criteria
    • coverage score: number of labels triggered
    • mutated score: difference of code coverage with its parent request it was mutated from
    • sinks present: if the request managed to find their way in the HTTPS response
    • execution time: round-trip time of the request
    • size: number of char in the request
    • picked score: number of times it was picked for further mutations

Witcher

Date: 2023 Greybox fuzzing

Really good paper.

  • Context and challenges are explained clearly.
  • first paper to fuzz against SQL and code injection
  • bibliography is pleasant to read

Contributions

  • framework to ease the integration of coverage-guided fuzzing on webapps
  • fuzzer that can detect multiple type of vulnerabilities in both server-side binary and interpreted web applications
    • SQL injection, command injection, memory corruption vulnerability (in C)

Enable fuzzing in webapp for SQL and command injection

Fault Escalator

We want to detect when an input makes the webapp transitions into an unsafe state. Usually for binary fuzzing we detect segfault and memory corruption. Witcher uses fault escalation of syntax errors to detect when a SQL or code injection has been executed by the fuzzer.

SQL fault escalation
  • instrument an SQL database to trigger a segfault when a syntax error has been triggered
  • illegal sql injection from the fuzzer has a high change to trigger a syntax error
  • valid sql access shouldn't form ill-formed requests
Command injection escalation
  • dash is instrumented to escalate parsing error to segfault
  • any code injection that calls exec(), system() or passthru() will be passed to dash
  • Witcher version of dash has 3 lines of code difference from the original
Extend fault escalation

Syntax errors have been used for both SQL and command injection. This can apply also to any type of warning, error or pattern. Ex: detect file system usage by triggering segfault when a non-ascii value has been used

XSS
  • Not handled
  • browsers are really permissive when parsing HTML
  • makes XSS vulnerabilities hard to detect

Request Crawler

Uses Reqr

  • extracts HTTP requests from all types of web application.
  • uses Puppeteer to simulate user actions
  • static analyze the rendered HTML to detect HTML elements that create HTTP requests or parameters
  • trigger all HTML elements that trigger user action
  • randomly fires user event inputs

Request Harness

Witcher’s HTTP harnesses translates fuzzer generated inputs into valid requests

  • CGI requests are used for PHP and CGI binaries
  • HTTP requests are used for Python, Java, Node.js and Qemu-based binaries

Translating fuzzer input into a Request

  • create seeds to fuzz
    • field for cookies
    • query parameters
    • post variables
    • header values
  • sets the variables for the webapp to operate correctly (e.g. cookies)

Augmenting Fuzzing for web injection vulnerabilities

Coverage Accountant

It is hard to do code coverage for interpreted languages. Instrumentations to the interpreters add unnecessary noises.

  • augmented bytecode interpreter for interpreted languages
    • linenumber, opcode and parameters are collected at runtime
  • CGI binaries
    • source code available, uses AFL instrumentation
    • without source code uses dynamic QEMU instrumentation

HTTP-specific Input mutations

Add two HTTP-specific mutations stages to AFL

  • HTTP parameter mutator
    • cross-pollinates unique parameter name and values between interesting test cases stored in the corpus
    • more likely to trigger new execution rather than random byte mutations
  • HTTP dictionary mutator
    • endpoints usually serve multiple purposes hence an endpoint may have several requests that use different HTTP variables
    • for a given endpoint, Witcher places all the HTTP variables discovered by Reqr into the fuzzing dictionary

Evaluation

  • blackbox vs greybox: Outperforms Blurp in vulnerabilities found
  • Covers more code than BlackWidow and webFuzz
    • they both specialize in XSS so we can't compare

Limitations

  • there are other web vulnerabilities
    • XSS
    • path traversal
    • local file inclusion
    • remote code evaluation
  • only detect reflected injection vulnerabilities
    • when user input flows directly to a sensitive sink during a HTTP request
    • no detection of second-order vulnerabilities where there is a first step to store the injection in the webapp data
      • stored SQL injection
    • fault escalation would trigger but hard to investigate the actual input that stored the malicious injection
  • does not reason about the application state
    • fuzzes one URL at a time
    • does not reason about multi-state actions

BlackWidow

Date: 2021 BlackWidow Github

TODO

REST API fuzzing

A bit of context, most cloud services are accessible through REST APIs making them increasingly common. REST APIs are specified using the OpenAPI specification. Swagger tools uses OpenAPI specs to produce docs, testcases, ...

Challenges

  • modeling the REST API
    • using captured traffic to derive a model
    • dynamic crawler to derive a model
  • it's hard to trigger long sequence valid requests to trigger hard-to reach states
  • it's hard to forge high-quality requests that that pass the cloud service checking

BackREST

Date: 2021 Greybox fuzzing

Contributions

  • fully automated model-based for web applications
  • state-aware crawler to automatically infer REST APis
  • uses both coverage feedback and taint-analysis to guide the fuzzing
    • taint-analysis to guide the fuzzing
    • coverage feedback to skip inputs in the corpus (more for performance)

Taint-Analysis

  • NodeProf.js instrumentation framework that runs on the GraalVM runtime
  • Sensitive sinks are setup manually
    • if part of an input reach a sink then it alarms the fuzzer How does taint-analysis detect SQLi, XSS and command injection? Without too many false positives? TODO

Architecture

BackREST Architecture

Miner

TODO

  • uses data history to guide fuzzing
  • uses AI attention model to produce param-value list for each request
  • uses request response checker to keep interesting testcase

RESTler

Date: 2019 There are more recent papers on RESTler Github

Stateful REST APIs fuzzing.

  • an input is sequence of HTTP requests
  • dependencies between requests are inferred from the Swagger specification
  • HTTP responses are dynamically analyzed to produce new inputs
    • ex: avoid a combination of requests that are not allowed

Cefuzz

TODO

Typical attacks

Fuzz Vectors

OWASP

SQL injection

List of examples

Command injection

List of examples

Program instrumentation

The goal is to instrument the fuzzed program to obtain metrics during fuzzing. These metrics are either to guide mutation of inputs or detecting "dangerous" behaviour. Programs needs to be instrumented to give this kind of info. Instrumentation can be done at different levels:

  • at source code
  • during compilation, usually AST
  • binary

Bug oracles

Metrics that tells the fuzzer that it has detected a potential bug:

  • segfaults and signals
  • memory sanitizer
  • assertions in the code
  • different behaviour in differential fuzzing
    • memory state
    • message passing

Metrics to improve fuzzing

Metrics that are collected and use to improve the selection of future inputs:

  • code coverage
  • code targeting: how fast it is to access specific code
  • "distance" to certain type of vulnerabilities
  • logging for better understanding of the program
  • power consumption leaks

Code coverage

Multiple possible granularities:

  • Basic block
    • def: maximal sequence of consecutive statements that are always executed together
    • measure which basic block get executed
    • this provides least granularity since the coverage does not cover basic block order of execution
  • Branch/Edge coverage
    • measure the pair of consecutive blocks executed
    • a pair of basic block is called an edge
    • more precise and try to execute all conditional branches
    • algo:
      1. Give a unique label to all basic block
      2. Store any data related edge coverage to a global var
      3. At the beginning of each label xor current label and previous one
      4. This value is the edge label and is used as index for map coverage
      5. At the end of a basic block rightshift current label
        • this is to prevent 0-value label if basic block jumps on itself
      6. Store the rightshifted value as "previous visited block"
      7. At the end of program exec, print/send/store map coverage feedback

Tools for runtime instrumentation/tracing

This is used for blackbox fuzzing where you don't have access to the source code.

Mainly from afl++ doc:

  • Frida: dynamic code instrumentation toolkit
    • you can inject JS script into your native apps
    • debug and script live process
    • usable on many platforms: Windows, Mac, Linux, iOS, Android, QNX
  • Qemu: dynamic code injection using hooks
    • emulator
  • TinyInst: runtime dynamic instrumentation library
    • more lightweight than other tools
    • easier to use but does not fit every usecase
    • MacOS and Windows only
  • Nyx: only on Linux
  • Unicorn: fork of Qemu
  • Wine + Qemu: to run Win32 binaries
  • Unicorn: fork of Qemu
  • Tracing at runtime
    • Pintool: Intel x32/x64 on Linux, MacOS and Windows
    • Dynamorio
      • Intel x32/x64 on Linux, MacOS and Windows
      • Arm and AArch64
      • faster than Pintool but still slow
    • Intel-PT
      • use intels processor trace
      • downsides: buffer is small and debug info is complex
      • two AFL implementations: afl-pt and ptfuzzer
    • Coresight: ARM processor trace

Binary instrumentation

Also for blackbox fuzzing. Instrumentation is done only once, having better performance than with runtime instrumentation.

Mainly from AFL++ documentation

  • Dyninst
    • instruments the target at load time
    • save the binary with instrumentations
  • Retrowrite: x86 binaries, decompiles to ASM which can be instrumented with afl-gcc
  • Zafl: x86 binaries, decompiles to ASM which can be instrumented with afl-gcc

Compile-time instrumentation

Multiple advantages:

  • speed: compiler can still optimize code after instrumentation
  • portability: the instrumentation is architecture independent

Rust options

Two code coverage options:

  • a GCC-compatible, gcov-based coverage implementation, enabled with -Z profile, which derives coverage data based on DebugInfo
  • a source-based code coverage implementation, enabled with -C instrument-coverage, which uses LLVM's native, efficient coverage instrumentation to generate very precise coverage data

Rust Source-based coverage

  • cargo-fuzz uses this technique
    • cargo-fuzz is not a fuzzer but a framework to call a fuzzer
    • the only supported fuzzer is libFuzzer
    • through the libfuzzer-sys crate
  • done on MIR
  • based on llvm source-based code coverage
  • rustc -C instrument-coverage does:
    • insert llvm.instrprof.increment at control-flows
    • add a map in each library and binary to keep track of coverage information
    • use symbol mangling v0
  • uses the Rust profiler runtime
    • enabled by default on the +nightly channel
  • needs to use a Rust demangler: rustfilt
    • can be provided to llvm options
Using it
  1. Compile with cargo
    • RUSTFLAGS="-C instrument-coverage" cargo build
    • may be necessary to use the profiler runtime: RUSTC=$HOME/rust/build/x86_64-unknown-linux-gnu/stage1/bin/rustc
  2. Run the binary compiled
    • it should produce a file default_*.profraw
    • or name it with LLVM_PROFILE_FILE="toto.profraw"
  3. Process coverage data with llvm-profdata
    • can be installed with rustup
    • llvm-profdata merge -sparse toto.profraw -o toto.profdata
  4. Create reports with llvm-cov
    • can be installed with rustup
    • create a report when combining profdata with the binary
    • llvm-cov show -Xdemangler=rustfilt target/debug/examples/toto \ -instr-profile=toto.profdata \ -show-line-counts-or-regions \ -show-instantiations \ -name=add_quoted_string

LLVM options

LLVM has multiple options to instrument program during compilation

  • Source Based Coverage
  • Sanitizer Coverage
  • gcov: A GCC-compatible coverage implementation which operates on DebugInfo. This is enabled by -ftest-coverage or --coverage

Source-Based Coverage

Operates on AST and preprocessor information directly

  • better to map lines of Rust source code to coverage reports
  • -fprofile-instr-generate -fcoverage-mapping

Sanitizer Coverage

  • operates on LLVM IR
  • -fsanitize-coverage=trace-pc-guard to trace with guards/closures
    • will insert a call to __sanitizer_cov_trace_pc_guard(&guard_variable) on every edge
    • __sanitizer_cov_trace_pc_guard(&guard_variable) can be
      • implemented by user
      • defaulted to a counter with -fsanitize-coverage=inline-8bit-counters
      • defualted to a boolean flag with -fsanitize-coverage=inline-bool-flag
  • partial instrumentation with -fsanitize-coverage-allowlist=allowlist.txt and -fsanitize-coverage-ignorelist=blocklist.txt
    • these lists are filled with function names

LibAFL tools

LibAFL project has directories such as:

  • libafl_targets that can be used for instrumentation
  • libafl_cc a library that provide facilities to wrap compilers

cargo-libafl

This is a replacement to cargo-fuzz which went into maintenance. cargo-libafl is just a framework to prepare fuzzing. The actual fuzzer is libfuzzer-sys that is maintained in libafl_targets.

  1. cargo libafl init
    • create a directory fuzz_targets
  2. cargo libafl run <fuzz target name>
    • exec_build gives RUSTFLAGS="-Cpasses=sancov-module -Cllvm-args=-sanitizer-coverage-level=4 -Cllvm-args=-sanitizer-coverage-inline-8bit-counters -Cllvm-args=-sanitizer-coverage-pc-table -L /home/adang/.local/share/cargo-libafl/rustc-1.70.0-90c5418/cargo-libafl-0.1.8/cargo-libafl -lcargo_libafl_runtime -Cllvm-args=-sanitizer-coverage-trace-compares --cfg fuzzing -Clink-dead-code -Cllvm-args=-sanitizer-coverage-stack-depth -Cdebug-assertions -C codegen-units=1" "cargo" "build" "--manifest-path" "/home/adang/boum/fuzzy/playground/rust-url/fuzz/Cargo.toml" "--target" "x86_64-unknown-linux-gnu" "--release" "--bin" "fuzz_target_1"

AFL tools

Recommendation from AFL Guide to Fuzzing in Depth

LTO mode

  • called afl-clang-lto/afl-clang-lto++
  • works with llvm11 or newer
  • instrumentation at link time
  • autodictionary feature
    • while compiling a dictionary is generated
    • in fuzzing a dictionary are base inputs that will help improve code coverage
  • improve efficiency of the fuzzer by avoiding basic block label collision
    • classic coverage labels blocks randomly
    • lto-mode instrument the files and avoid block label collision
  • only con is that is has long compile time

LLVM mode

gcc plugin mode

Called afl-gcc-fast/afl-g++-fast Instrument the target with the help of gcc plugins.

gcc/clang mode

The base version without any special features.

  • afl-gcc/afl-g++ and afl-clang/afl-clang++

AFL options

CmpLog

Log comparison operands in shared memory for the different mutators. Many comparison operands check whether part of the state are equal to a preset byte range. These byte ranges are called magic bytes.

CmpLog guide mutations by linking the inputs to the states magic bytes.

laf-intel

"De-optimize" certain LLVM passes to increase code coverage.

persistent-mode

Fuzz a program in a single forked process instead of forking a new process for each fuzz execution. Really important if we fuzz with fork server, speed improvements x10-x20

partial instrumentation

Select which part of the code you want to instrument

Input Generation / Mutation

Target programs usually only accept certain kinds of inputs. The goal

The usual main concerns:

  • interesting inputs: input that produce new behaviour during fuzzing
    • this is really hard because it is often target-dependent
    • there can be dynamic optimizations of the mutations during the fuzzing using fuzzing metrics
  • syntactically correct: input should be valid
    • can be solved given a grammar
    • grammar can be inferred
  • semantically correct: input has to make sense
    • usually mutation over already valid inputs

Tools

Syzkaller

Current fuzzer used in Linux Kernel development

  • requires a grammar for syscalls

FuzzNG

Fuzzer for linux kernel that does not require a grammar

  • FuzzNG reshapes the input space without needing a grammar
  • at runtime FuzzNG aware of which file descriptors and pointers are accessed
  • before function calls, FuzzND pauses this process to populate these locations

Darwin

Optimization of the mutation at runtime

  • issues:
    • optimal mutations are target-dependent
    • runtime algorithms to optimize mutations may have a performance cost that outweighs the benefits
  • leverages a variant of Evolution strategy
    • mix between evolution and intensifications
    • evolution optimizes over a set of inputs
      • discover promising areas and avoid local optima
      • Algo:
        • start with primary population of inputs
        • natural selection of the population by selecting best inputs over a metric
        • mutate these best inputs to obtain a new population
        • reiterate the selection until certain criteria is met
    • intensification optimize a promising area
      • focus on current best solution
      • mutate to find the better neighboring solutions

Nautilus

Combines usage of grammar + code coverage results

  • improve the probability of having syntactically and semantically valid inputs

Grimoire

TODO

Benchmarks

Fuzzers efficiency are compared over different metrics/conditions Overall there is no best fuzzer. Fuzzers performance vary a lot depending on the fuzzing environment:

  • resources given
  • time give
  • type of vulnerabilities
  • target program

Metrics used

From Unifuzz paper:

  • quantity of unique bugs
  • quality of bugs: rare bugs are worth more
  • speed of finding bugs
  • stability of finding bugs: can a fuzzer find the same bug repeatedly?
  • coverage
  • overheard

Tools

  • Magma: real-world vulnerabilities with their ground truth to verify genuine software defects
  • Unifuzz: real-world vulnerabilities with their ground truth to verify genuine software defects
  • LAVA-M: benches of programs with synctatically injected memory errors
  • Cyber Grand Challenge: benches of binaries with synthetic bugs
  • Fuzzer Test Suite (FTS): real-world programs and vulnerabilities
  • FuzzBench: improvement of FTS with a frontend for easier integration

System Calls Interception

The goal is to provide our fuzzer a runtime that intercept any system calls.

Difficulty

  • Intercept only system calls that originates from the fuzzed program and not from the fuzzer
  • Try to not impact performance too much

System calls

Syscalls on Linux

  • in x86 asm: int $0x80 or syscall with syscall number in eax/rax
  • through the libc

Types of system calls

  1. Process control
    • create process: fork
    • terminate process
    • load, execute
    • get/set process attributes
    • wait for time/event, signal event
    • allocate and free memory
  2. File management
  3. Device management
  4. Information maintenance
    • get
  5. Communication
  6. Protection
    • get/set file permissions

Intercepting system calls

With LibAFL Frida intercept any instruction

syscall_intercept

  • library to intercept and hook on system calls
  • not maintained
  • only on Linux

extrasafe

  • wrapper around seccomp
  • only Linux

LD_PRELOAD

  • Load specified shared object instead of default one
  • Can be used to override libc
  • This is specific to Unix systems
    • on Windows you may use DLL injection

Tools used

  • capstone
    • multi-platform, multi-architecture disassembly framework
    • frida is used for binary analysis and the rely on capstone to disassemble the instruction to be able to operate on them
  • frida
    • dynamic code instrumentation toolkit
    • allows you to inject snippet of code in native apps
    • [frida-core]
      • its role it to attach/hijack the target program to be able to interact with it
        • package frida-gum as a shared lib which is injected into the target program
        • then provide a two way communication to interact with frida-gum with your scripts
      • this is the common way to use frida
      • sometimes it's not possible to do so (jail iOS or Android)
        • in this situation you can use frida-gadget
    • frida-gadget
      • shared library meant to be loaded by the target program
      • multiple way to load this lib
        • modify the source code
        • LD_PRELOAD
        • patching one of the target program library
      • it's started as soon as the dynamic linker call its constructor function
    • frida-gum
      • instrumentation and introspection lib
      • C lib but provide a JS api to interact with it
      • 3 instrumentation core

Panic in Rust

panic!

  1. std::panic::panic_any(msg)
  2. if exists, panic hook is called
    • std::panic::set_hook
  3. if panic hook returns, unwind the thread stack
  4. if the registers are messed up
    • the unwinding fails and thread aborts
    • else, per frame the data is dropped
      • if a panic is hit while dropping data then thread aborts
  5. some frames may be marked as "catching" the unwind
    • marked via std::panic::catch_unwind()
  6. When the thread has finished unwinding
    • if it's main thread then core::intrisics::abort
    • else, thread is terminated and can be collected with std::thread::join

panic is memory costly

To unwind the stack some debugging information is added to the stack

  • debug information in DWARF format
  • in embedded panic = abort is used

Diary

This is a diary that records the events and thoughts processes that went during the project. This is mostly used to reorganize our thoughts and also keep a trace of how we handled certain issues when we bump into them again at a later time.

1

Playing with the Tauri mock runtime

  • Exploration of Tauri code

    • tauri::app::App::run_iteration exists to react to a single event
    • tauri::app::on_event_loop_event could be used to fuzz the Tauri app by calling it directly from main
    • tauri::app::Builder::on_page_load could be used to fuzz the Tauri app by calling it directly from main
    • tauri::tauri-runtime-wry is the default implementation of the runtime
    • tauri::tauri-runtime is the runtime interface
    • wry-runtime event loop receives different type of events:
      • tao::Event receives from TAO
      • tao::EventLoopWindowTarget ?
      • tao::ControlFlow: Poll, Wait, WaitUntil, Exit
  • playing with mini-app and mock runtime

    • new fuzz branch in Tauri
    • make the mockruntime public
    • rust-gdb can be used to break on points such as: tauri::app::App::run_iteration::hc9a795e571e144bc
    • trying to hack the function tauri::app:on_event_loop_event
      • events are only for window stuff, to interact with command check manager

2

  • try to trigger a command programmatically
  • PendingWindow has fields
    • js_event_listeners
    • ipc_handler
  • Check wry crate
    • webview sends message to the Rust host using window.ipc.postMessage("message")
  • Try to capture IPC using wireshark
    • listening on the loopback interface
    • did not work, certainly tauri does not use internet socket
  • Try to capture IPC using strace
    • we see traces of recvmsg and sendmsg syscalls
    • using ss -pO | grep mini/WebKit we see existences of open sockets for these processes
    • Unix sockets can be tracked using this sockdump
      • sockdump can output to pcap format that is readable by wireshark

3

  • Trying to sockdump the mini-app sockets
    • checking sockets file in /proc/$PID/fd
    • lsof -p $PID lists open files for a process
    • tauri command does not seem to pass through unix sockets
      • ss show that the open sockets have no data going through them
      • this is confirmed using sockdump
  • Checking tauri, wry and tao code to see where IPC comes from
    • connect to local version of wry and tauri
    • tao::EventLoop::run_return when spawning x11 thread contains let (device_tx, device_rx) = glib::MainContext::channel(glib::Priority::default());

4

  • IPC manager add to Webkit IPC handlers
    • at build time of the webview these handlers will generate methods that can called via window.webkit.messageHandlers.funcName.postMessage(args)
    • examples can be seen in wry/examples/*
  • From Lucas suggestion
    • tauri::window::Window::on_message can trigger command
    • https://github.com/tauri-apps/tauri-invoke-http to use http over localhost instead of default Tauri
  • Using tauri::window::Window::on_message we manage to run the app and trigger command without webview

5

  • import tauri-fork in the fuzz-proto dir
  • reinstall necessary tools for new computers
  • modify Dockerfile
    • remove cargo chef, don't know why but it made mini-app/src-tauri/src/main.rs an empty main(){} function
    • change the architecture

6

  • modify Dockerfile to have missing dependencies
  • tauri::test::assert_ipc_response should be checked to also handle response from the command invoked

Question to Lucas

  • IPC lifecycle?
    • on init of webview, tauri register in the webview tauri handles
    • this tauri handles can be called via postMessage in webkitgtk
    • What kind of Linux IPC are actually used in webkitgtk

      ipc are actually handled by the webview

  • Mockruntime
    • essentially what is it? emulation of Wry
    • if we want to fuzz the windowing system in the future could it be interesting

      fork the mockruntime if you want to fuzz the windowing system rather than forking wry

  • HTTP
    • does it make the 2 process communicate over http on localhost
    • when is it used?

      websockets, local devserver could be useful for a man-in-the-middle fuzzer that is able to fuzz both the backend and the webview by sending them HTTP requests

  • Architecture questions
    • why do use Window and WindowHandle, App and AppHandle

7

  • libdw is not used in cargo b --release because there are no debug info in release profile
  • fix byte conversion error were the copy_from_slice involved 2 arrays of different sizes
  • libafl::bolts::launcher::Launcher is used to launch fuzzing on multiple cores for free
    • run_client() is the closure ran by every core
  • Fuzzer behaviour depending on harness result
    • When harness crashes with panic
      • the fuzzer state is restarted
      • re-generating initial corpus
    • When harness does not crash but return ExitKind::Crash or ExitKind::Ok
      • the fuzzer is not restarted and corpus may ran out because not regenerated
  • libafl::state::StdState::generate_initial_inputs_forced create new inputs even if they are not "interesting"
    • useful when not using feedback

8

  • x86_64 calling convention checked
    • for &str length is store in rsi and pointer in rdi
    • for u32 value is stored directly in rdi
  • environment variable LIBAFL_DEBUG_OUTPUT helps with debugging

9

  • libdw issue
    • In the docker container it works in release but not in debug
    • In local it does not work in both release and debug and this issue is triggered in both cases
  • libafl_qemu::Emulator does not crash itself when the emulated program crash
    • no way to catch a crash in the emulator?
  • Add InProcess fuzzing
    • we avoid the dependency issue
    • we don't deal with qemu emulator anymore
    • steps
      1. Split mini-app to have both a binary and a lib
      2. Use the in-memory fuzzing to call functions from the lib
  • separate mini-app into a lib and binary

10

  • Flow between app and mockruntime
    • app::run() - runtime::run() - app::on_event_loop_event - callback
  • diff between:
    • App::run_on_main_thread/RuntimeContext::run_on_main_thread, run stuff on the window process
    • window::on_message: pass message to backend process
  • need to have a harness that does not exit at the end of the function
  • In the mockruntime there is app::Window::DetachedWindow::Dispatcher::close()
    • it will send the message Message::CloseWindow with run_on_main_thread
    • the mockruntime intercept it and sends RuntimeEvent::ExitRequested to the app
    • the app will process some stuff in on_event_loop_event
    • then the event RuntimeEvent::ExitRequested will be sent to the closure given to app::run at the beginning
  • you can break out of the loop from run in the Mockruntime
    • by sending a message Message::CloseWindow
    • then sending another message which is not ExitRequestedEventAction::Prevent

11

  • Move code that setup and calls tauri commands to the fuzzer
    • now the application can add an almost empty lib.rs file to to be fuzzed
  • Refactor and clean code
  • Bibliography
    • tinyinst

12

  • Bibliography
  • Mdbook
  • Plan for the future with Github issues

13

  • Read AFL++ docs for code instrumentation
  • Redo the dockerfile
    • Change to higher version of Debian to have llvm14 - Fail, llvm14 is not new enough to compile rust code
    • Change to Ubuntu container 23.04
    • Pin the Rust version to 17.0
    • Pin compiler version for AFL++ to llvm-16
  • Compile with afl-clang-lto
    • version of rustc llvm and the llvm you want to use need to match
      • check your rustc llvm with rustc --version --verbose
    • output llvm with rustc + vanilla compilation with afl-clang-lto fails and not practical
    • trying with .cargo/config.toml
      • [target.x86_64-unknown-linux-gnu] linker = "afl-clang-lto"
  • Checking if coverage worked by checking asm
  • afl-clang-lto needs more instrumention before in the pipeline
  • we need to check cargo-afl

14

  • in cargo-afl
    • files are compiled with let mut rustflags = format!( "-C debug-assertions \ -C overflow_checks \ -C passes={passes} \ -C codegen-units=1 \ -C llvm-args=-sanitizer-coverage-level=3 \ -C llvm-args=-sanitizer-coverage-trace-pc-guard \ -C llvm-args=-sanitizer-coverage-prune-blocks=0 \ -C llvm-args=-sanitizer-coverage-trace-compares \ -C opt-level=3 \ -C target-cpu=native " ); rustflags.push_str("-Clink-arg=-fuse-ld=gold ");
  • Compile mini-app with the function above
    • issue all crates are instrumented
    • export RUSTFLAGS="-C debug-assertions -C overflow_checks -C passes=sancov-module -C codegen-units=1 -C llvm-args=-sanitizer-coverage-level=3 -C llvm-args=-sanitizer-coverage-trace-pc-guard -C llvm-args=-sanitizer-coverage-prune-blocks=0 -C llvm-args=-sanitizer-coverage-trace-compares -C opt-level=3 -C target-cpu=native --cfg fuzzing -Clink-arg=-fuse-ld=gold -l afl-llvm-rt -L /home/adang/.local/share/afl.rs/rustc-1.70.0-90c5418/afl.rs-0.13.3/afl-llvm-rt"
    • we need to make -fsanitize-coverage-allowlist= work

15

  • Check LibAFL
    • libafl_targets
    • libafl_cc
  • Compile with -C llvm-args=-sanitizer-coverage-trace-pc-guard
    • it place calls to __sanitizer_cov_trace_pc_guard at every edge (by default)
    • libafl_targets implements __sanitizer_cov_trace_pc_guard
    • flags
      • export RUSTFLAGS="-C debug-assertions -C overflow_checks -C passes=sancov-module -C codegen-units=1 -C llvm-args=-sanitizer-coverage-level=3 -C llvm-args=-sanitizer-coverage-trace-pc-guard -C llvm-args=-sanitizer-coverage-prune-blocks=0 -C llvm-args=-sanitizer-coverage-trace-compares -C opt-level=3 -C target-cpu=native --cfg fuzzing -C llvm-artg=-D__sanitizer_cov_trace_pc_guard_init"
    • sanitize-coverage-allowlist=coverage_allowlist.txt not supported with rust
    • linking error, ld does not find symbols in libafl_targets
  • Selective instrumentation
    • try allowlist but not working
    • cargo rustc, which only affects your crate and not its dependencies.
      • https://stackoverflow.com/questions/64242625/how-do-i-compile-rust-code-without-linking-i-e-produce-object-files
  • From Discord:
    • "I had good experience with using cargo-fuzz and https://github.com/AFLplusplus/LibAFL/pull/981 together"
    • "So cargo-fuzz will instrument everything and that branch has a libfuzzer compatible runtime"
    • "In a default cargo-fuzz project, just depend on that LibAFL libfuzzer version instead of the one from crates.io."
    • "There is also the (somewhat unmaintained) cargo-libafl crate that could give some pointers"
  • rustc llvm-args
    • rustc -C llvm-args="--help-hidden" | nvim -

16

  • cargo-libafl is a fork of cargo-fuzz

  • How does it work with libfuzzer

    1. init command creates a fuzz directory with
      • fuzz_targets with harness using the fuzz_target! macro
      • Cargo.toml containing dependency to libfuzzer-sys
      • libfuzzer-sys can refer to the original from crates.io or to the ported version from libafl
    2. cargo-fuzz run command to fuzz the targets
      • Working when using the deprecrated original libfuzzer-sys
      • Failing to link with the version from libafl
      • Same error when using cargo-libafl
      • Steps:
        1. Compile the fuzz_targets with the command RUSTFLAGS="-Cpasses=sancov-module -Cllvm-args=-sanitizer-coverage-level=4 -Cllvm-args=-sanitizer-coverage-inline-8bit-counters -Cllvm-args=-sanitizer-coverage-pc-table -Cllvm-args=-sanitizer-coverage-trace-compares --cfg fuzzing -Clink-dead-code -Cllvm-args=-sanitizer-coverage-stack-depth -Cdebug-assertions -C codegen-units=1" "cargo" "build" "--manifest-path" "/home/adang/boum/fuzzy/playground/rust-url/fuzz/Cargo.toml" "--target" "x86_64-unknown-linux-gnu" "--release" "--bin" "fuzz_target_1"
        2. Run the fuzz_targets with the command RUSTFLAGS="-Cpasses=sancov-module -Cllvm-args=-sanitizer-coverage-level=4 -Cllvm-args=-sanitizer-coverage-inline-8bit-counters -Cllvm-args=-sanitizer-coverage-pc-table -Cllvm-args=-sanitizer-coverage-trace-compares --cfg fuzzing -Clink-dead-code -Cllvm-args=-sanitizer-coverage-stack-depth -Cdebug-assertions -C codegen-units=1" "cargo" "run" "--manifest-path" "/home/adang/boum/fuzzy/playground/rust-url/fuzz/Cargo.toml" "--target" "x86_64-unknown-linux-gnu" "--release" "--bin" "fuzz_target_1" "--" "-artifact_prefix=/home/adang/boum/fuzzy/playground/rust-url/fuzz/artifacts/fuzz_target_1/" "/home/adang/boum/fuzzy/playground/rust-url/fuzz/corpus/fuzz_target_1"
  • fuzz_target! macro definition is in cargo-libafl/cargo-libafl-helper

  • To have a more complete fuzzer with memory sanitizer and else check cargo-libafl/cargo-libafl/cargo-libafl-runtime

  • Fork cargo-fuzz or cargo-libafl to use their framework to easily fuzz Tauri applications

17

  • Use cargo-fuzz as frontend for the fuzzing then use libafl as a backend replacing libfuzzer
  • Installing rustup component add llvm-preview-tools to see information about code coverage
    1. cargo fuzz run fuzz_target
    2. cargo fuzz coverage fuzz_target
    3. Show code coverage with llvm-cov show > llvm-cov show \ -instr-profile=coverage/fuzz_target_1/coverage.profdata \ -Xdemangler=rustfilt target/x86_64-unknown-linux-gnu/coverage/x86_64-unknown-linux-gnu/release/fuzz_target_1 \ -use-color --ignore-filename-regex='/.cargo/registry' \ -output-dir=coverage/fuzz_target_1/report -format=html \ -show-line-counts-or-regions \ -ignore-filename-regex='/rustc/.+' - docs on https://llvm.org/docs/CommandGuide/llvm-cov.html#llvm-cov-show - bin with coverage information is generated at target/arch_triple/coverage/arch_triple/release/fuzz_target - profile file is generated at coverage/fuzz_target/coverage.profdata
    4. Create a summary report with llvm-cov report > llvm-cov report \ -instr-profile=coverage/fuzz_target_2/coverage.profdata \ -use-color --ignore-filename-regex='/.cargo/registry' \ -Xdemangler=rustfilt target/x86_64-unknown-linux-gnu/coverage/x86_64-unknown-linux-gnu/release/fuzz_target_2
  • Swap libfuzzer backend with libafl_libfuzzer version
    • doc for options in the LibAFL/libafl_libfuzzer/src/lib.rs

18

  • Clone dash
  • Clone sqlite
  • Modify dash to make it crash

19

Frida

Frida is a binary analyser with 2 main features - Stalker code-tracing engine - follow threads and trace every instruction that are being called - uses a technique called dynamic recompilation - while a program is running the current basic block is copied and stored in caches - these copy can be modified and executed on demand - the original instructions are unmodified - Interceptor hooks - allows inspection and modification of the flow of function calls - different possible techniques but most common are trampoline based hooks - code is inserted at the beginning of a function A to execute another function B so function B is "inserted" in the middle of function A

Strong points

  • Portability: frida works/exists on almost all platforms
  • Frida is binary analysis
    • works directly on binaries and do not require special compilation

Libafl-frida

  • libafl-frida uses frida ability to modify the code to
    • provide coverage
    • provide asan
    • provide cmplog
  • to create more behaviour we just need to implement the FridaRuntime and add it to the possible runtimes
    • for example a runtime that crash on system call
  • libafl-frida has been made to fuzz C libraries
    • no easy way to fuzz a Rust crate

20

Syscall isolation runtime

Intercepting syscalls

  • using ldpreload trick
  • intercept all libc and syscall instruction

Preventing too many false positive

  • SET a flag every time you change of running environment (disable flag when running fuzzer code)
    • needs to be run single-threaded
  • Check for stack trace to see if it came from the Tauri app
    • can be costly
  • Use fork fuzzing to not have syscalls from the fuzzer?
  • EBPF could be a solution to filter false positive? There may be already existing ebpf rules that exist that we could reuse
  • Using libafl minimizer

21

tauri-for-fuzzy

  • window.close() has different behaviour in 1.5 and 2.0

Fuzzer on macos

  • tauri::Window other than "main" can't trigger on_message
  • issue with using Cores("0") but works fine with other cores
    • cores.set_affinity() not supported for MacOS
    • I have a hunch that Cores("0") represent inmemory fuzzing

Ideas for Frida

  • For injecting library dependency on PE, Mach0 or ELF
  • https://github.com/lief-project/LIEF

Interesting project

  • ziggy
    • fuzzer manager for Rust project

22

  • Update docs on syscalls
  • Compile mini-app as a dylib
    • libafl prevent instrumenting its own crate to prevent weird recursion
  • Clean the mini-app fuzzing code
  • Make mini-app dynamic
    • to use the binary directly and linking with the dynamic libmini_app.so
    • LD_LIBRARY_PATH='/home/user/tauri-fuzzer/mini-app/src-tauri/fuzz/target/debug/deps:/home/user/.rustup/toolchains/1.70-x86_64-unknown-linux-gnu/lib:/home/user/tauri-fuzzer/mini-app/src-tauri/fuzz/target/debug:/home/user/.rustup/toolchains/1.70-x86_64-unknown-linux-gnu/lib'
  • Create a tauri command that do a system call without using the libc

23

  • Create a separate crate tauri_fuzz_tools for helper functions
    • this function connect Tauri to LibAFL
  • Change whole repo to a workspace
  • Catch a call to libc
    • Check any "calls" and destination address
      • we don't need to instrument libc
      • we may miss hidden calls
    • Instrument the libc and verify the instruction location
      • we need to instrument libc and all libc instructions will be analysed
      • easier to implement
  • Found how to get libc symbols through friga_gum::Module::enumerate_exports
  • Strange "double crash bug"
    • does not appear when removing coverage from the runtimes

24

  • Inspect minimization
    • misunderstanding of what minimization is
    • thought that minimization would reduce the number of solutions found to only keep ones with different coverage
    • Real use of minimization:
      • reduce size of the "interesting" inputs while preserving the code coverage
      • removes the "noise" in inputs for easier analysis and mutations
    • Docs and examples can be found at:
      • https://docs.rs/libafl/latest/libafl/corpus/minimizer/trait.CorpusMinimizer.html
      • an example fuzzer in: "LibAFL/fuzzers/libfuzzer_libpng_cmin/src/lib.rs"

25

  • on Windows
    • change visibility for the different modules
    • make sure that given paths are portable
  • Noticed that when opening a file fopen is not called but open
  • Another issue is that interceptor do not distinguish between calls from the crates and the code we are targeting
    • we need to have an interceptor that sets up a flag on the tauri command we are fuzzing (then it's single threaded?)

26

  • Trying to setup the interceptor only when the harness functions are entered
    • when entering the tauri command we are fuzzing
    • when we are entering the harness: setup_tauri_mock + on_msg
  • In our mental model it's one thread per harness executed
    • the SyscallIsolationRuntime is initiated for each thread
    • we should be able to have one flag per SyscallIsolationRuntime to setup when the harness function has been entered
  • Bug but maybe disable other runtime

27

  • Finding function symbol in the runtime with a pointer rather than a name
    • name mangling make it harder
    • more precise
  • the fuzzer intercepts the open syscall
    • this happens in the fuzzer panic_hook to write state to disk
      • it's difficult to set the SyscallIsolationRuntime flag from the panic_hook
      • we dodge the issue by rewriting the panic_hook
    • this happens with the stalker

28

  • Trying to refact fuzzer.rs to have the same code to use fuzz_one or Launcher
    • really difficult due to the numerous traits used by LibAFL
    • the trick they use is to use a closure so we don't need to precise a type for all objects used
    • but to turn this into a function
      • using impl return type does not work due to Rust not supporting nested impl
      • returning generic type not really working either since the return type is clearly defined in the function body
      • using exact type is super difficult too due to the complexity of the types in LibAFL
    • I think I need a rust expert for this
  • Writing tests for our fuzz targets
    • Issue is that tests that crash actually are handled by the fuzzer and actually libc::_exit(134)
    • This is not handled by cargo tests
    • What I've tried
      • #[should_panic] this is not a panic so it does not work
      • panic::setup_hook(panic!) this is rewritten by the fuzzer =(
      • uses abort rather than panic does not work either
    • Solved by wrapping the test in another process and using and self calling the binary with Command::new(std::env::current_exe())

29

  • Working on fuzzing policy
    • Need a more generic and flexible way to give a security policy, need the security team for their inputs
    • security policies should be provided as constants for performance
  • Restructure the project
    • fuzzer and security policy code moved to the application being fuzzed fuzz directory
    • user can now directly see the fuzzer and the policy used rather than looking at external crate
  • Another race condition happened
    • be sure to drop the harness flag before calling any function that might panic
  • For conditions we're currently using functions rather than closures
    • this is to avoid any rust issue with trait object
    • this should be improved in the future

Call Tauri inbuilt command such as fs_readFile

  • Improve create_invoke_payload
    • allow to have an argument specifying a module
    • distinguish between an invocation between a custom command and an inbuilt one
  • These commands requires a shared state to be managed by the Tauri mock runtime
    • error message triggered is state() called before manage() for given type
    • we can't use our helper function mock_builder_minimal
    • use mock_builder instead
  • The InvokeRequest looks like
InvokeRequest {
    cmd: "plugin:fs|read_file",
    callback: CallbackFn(
        2482586317,
    ),
    error: CallbackFn(
        1629968881,
    ),
    url: Url {
        scheme: "http",
        cannot_be_a_base: false,
        username: "",
        password: None,
        host: Some(
            Ipv4(
                127.0.0.1,
            ),
        ),
        port: Some(
            1430,
        ),
        path: "/",
        query: None,
        fragment: None,
    },
    body: Json(
        Object {
            "options": Object {
                "dir": String("toto"),
            },
            "path": String("foo.txt"),
        },
    ),
    headers: {
        "content-type": "application/json",
        "origin": "http://127.0.0.1:1430",
        "referer": "http://127.0.0.1:1430/",
        "accept": "*/*",
        "user-agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15",
        "tauri-callback": "2482586317",
        "tauri-error": "1629968881",
        "tauri-invoke-key": "[Ic/:jX^L^q#hDgJd7)U",
    },
    invoke_key: "[Ic/:jX^L^q#hDgJd7)U",
}
  • Don't forget to configure the allowlist to allow the scope

30

  • Move mini-app/src-tauri/fuzz/ to mini-app-fuzz
    • seamless transition, just had to change dependency in the workspace Cargo.toml
  • Writing a presentation with Reveal.js
    • presentation added to the mdbook
  • Bump to Rust version 1.76
    • Update VM to Rust 1.70 -> 1.76
    • Unroll the clap package version in libafl_bolts: ~4.4 -> 4.0 (4.5)
      • We pinned it because it was not compatible with last version of Rust I was using
  • Make LibAFL a submodule
    • LibAFL is also a Rust workspace itself so we had to exclude = ["LibAFl"] it from the root Cargo.toml
    • git config submodule.recurse = true do not seem to work to pull recursively the last LibAFL commit
  • Writing user guide to

31

  • Restructure the repo with a classical monorepo architecture
    • docs/ with mdbook and slides
    • examples/ with mini-app and its fuzz code
    • crates/ with LibAFL, policies, fuzzer
  • Create a TOML configuration file for the fuzzer
    • more simple intermediary type to libafl_bolts::FuzzerOptions
  • Why is our code coverage not working for the moment?
    • the harness and libs_to_instrument options were empty meaning the stalker was not applied on any part of executable
    • cmplog module is not implemented for x86-64
    • even when adding the executable to harness, it is removed by libafl_frida to avoid the stalker from analyzing its own code and get recursion
      • this is annoying with rust where you usually use static libraries so you get one big executable
      • a solution would be to make LibAFL a dynamic lib
      • with a quick try without persevering we get some link errors
        • this is not a mess I want to invest time in currently
      • another solution would be to be able to give exact memory ranges that we want frida stalker to work on
        • currently the precision is per Module
        • a module is more a less a library
        • for Rust it signifies the whole executable with all its crates + basic C libraries
        • ideally we would have the stalker on the main binary and not on any of its crate
        • We could make a PR for that
  • When running our binaries the fuzz_solutions are written in the wrong directory
    • cargo test executes in the root directory of the crate containing the tests
    • cargo run takes current directory where command is executed as root directory

Porting to 2.0

  • InvokeRequest new format
### Template for a plugin InvokeRequest
InvokeRequest {
    cmd: "plugin:fs|read_file",
    callback: CallbackFn(
        3255320200,
    ),
    error: CallbackFn(
        3097067861,
    ),
    url: Url {
        scheme: "http",
        cannot_be_a_base: false,
        username: "",
        password: None,
        host: Some(
            Ipv4(
                127.0.0.1,
            ),
        ),
        port: Some(
            1430,
        ),
        path: "/",
        query: None,
        fragment: None,
    },
    body: Json(
        Object {
            "options": Object {},
            "path": String("README.md"),
        },
    ),
    headers: {},
}
  • Calling plugin commands with the MockRuntime (such as fs:readFile)
    • Scope can be modified programmatically using
  let scope = app.fs_scope();
  scope.allow_file("/home/adang/boum/playground/rust/tauri2/src-tauri/assets/foo.txt");
  • RuntimeAuthority requires an acl and resolved acl
    • the RuntimeAuthority.acl
      • isn't modifiable programmatically
      • defines which permissions are allowed to be used by the application capabilities
      • ACL from the runtime authority is generated at buildtime in the Context
      • code generation to get the Tauri app context is located at tauri-codegen::context::context_codegen
    • Resolved
      • commands that are allowed/denied
      • scopes associated to these commands
      • it is initialized from the complete acl and the capabilities declared by the application
  • When building a Tauri v2 app tauri-build :
    • path to permission manifests from each plugin are stored in environment variables
      • 3 env variables per plugin used
        • DEP_TAURI_PLUGIN_FS_PERMISSION_FILES_PATH
          • where the permissions declaration for this plugin are declared
        • DEP_TAURI_PLUGIN_FS_GLOBAL_API_SCRIPT_PATH
          • JS script containing the API to call commands from the plugin
          • I think this is only used when the option withGlobalTauri is set
        • DEP_TAURI_PLUGIN_FS_GLOBAL_SCOPE_SCHEMA_PATH
          • schema for the scopes of the plugin
    • the permissions manifests are parsed
      • manifests contain all the permissions declared by plugins
    • parse the capabilities file
    • check that declared capabilities are compatible with information given by the manifests
  • InvokeRequest url
    • to have request that are deemed Local use tauri://localhost
  • Fuzzer does not need to tauri_app_builder.run(...) just if
    • we don't need an event loop
    • we don't need to setup the app
  • we don't need to interact with the app state
  • Url for InvokeRequest for local tauri commands is
    • "http://tauri.localhost" for windows and android
    • "tauri://localhost" for the rest

32

  • Github actions
    • use act to run github actions locally
    • to run test as github actions locally
    • with linux container: act -W ".github/workflows/build_and_test.yml" -j Build-and-test-Fuzzer -P ubuntu-latest=catthehacker/ubuntu:act-latest
    • on windows host: act -W ".github/workflows/build_and_test.yml" -j Build-and-test-Fuzzer -P windows-latest=self-hosted --pull=false
    • always do the command twice, the first one usually fails for unknown reasons
  • Bug with Rust 1.78
    • Rust 1.78 enables debug assertions in std by default
    • slice::from_raw_parts panics when given a pointer which is not aligned/null/bigger than isize::max
    • Bug in libafl_frida which trigger this situation when
      • stalker_is_enabled is set to true in libafl_frida/src/helper.rs
      • and no module is specified to be stalked
      • as a reminder stalker is enabled if we want to use the code coverage
    • Bug for coverage when stalker is enabled
      • in libafl_frida/src/helper.rs::FridaInstrumentationHelperBuilder::build
      • the instrument_module_predicate return true for the harness
      • but the ModuleMap returned by gum_sys is empty
      • this provokes a panic from Rust 1.78
      • current fix is to disable coverage but not good enough

33

  • Generating test for cli

    • issue killing the fuzzer process after launching it with cli
    • how do we get the pid of the fuzzer process which is a different process from the binary ran by cargo run
    • rust does not have command with timeout
    • We do it by querying the system for process with certain exact name
      • this is not super robust
      • behaviour is also platform dependent
      • we limit this test to linux platform to avoid future complications
  • New issue introduced with Tauri 2.0.0-beta.22

    • fs::read_file returns InvokeBody::Raw(Vec<u8>)
    • to get Rust type from this raw value, Tauri provides this function
    pub fn deserialize<T: DeserializeOwned>(self) -> serde_json::Result<T> {
      match self {
          ...
          InvokeBody::Raw(v) => serde_json::from_slice(&v),
      }
    }
    • this is flawed as serde_json::from_slice(&v) expects v to be bytes of JSON text (from serde_json documentation)
    • what was given from fs::read_file are raw bytes of the content of a file and this triggers a serialization error
    • for the function deserialize to work we need an additional conversion of the raw bytes into bytes of json text
    • a proposal that does not completely fix the issue but at least allow us to recuperate a Vec<u8> that can be used for further conversion:
    pub fn deserialize<T: DeserializeOwned>(self) -> serde_json::Result<T> {
      match self {
        ...
        InvokeBody::Raw(v) => {
          let json_string = serde_json::to_string(&v).unwrap();
          serde_json::from_slice(&json_string.into_bytes())
        }
      }
    }
    • either the function deserialize in Tauri is wrong or what is returned from fs::read_file is wrong

Windows

Issues

Troubles building fuzzer for windows with LibAFL

  • execution issue which does not appear when commenting calls to the LibAFL fuzzer
  • using the msvc toolchain
    • building works fine
    • we get (exit code: 0xc0000139, STATUS_ENTRYPOINT_NOT_FOUND) when running the binary
    • this happens when windows fails to load a dll
      • dependencywalker to investigate can help but now is deprecated
      • make sure that there is no discrepancy between loader version and compilation toolchain
  • using the windows-gnu toolchain
    • I need to install gcc for linking
  • what toolchain should I use?
    • depends on which dynamic library I need to link to
    • look into libafl repo for hints
    • in github action we see that they use the windows default stable toolchain
      • that should be msvc
  • Error found TaskEntryDialog entrypoint could not be found
    • running the fuzzer from windows ui
    • Dependency walker shows the missing modules
      • one of the main missing module is API-MS-WIN-CORE
    • Using ProcessMonitor with a filter on tauri_cmd_1.exe
      • run the executable and you get all the related events
  • Big chances it is related to tauri-build which does a lot in windows
    • reintroduce a build.rs file with tauri_build::build()
    • Find a way to have a generic and minimal tauri.conf.json for the fuzz directory

frida_gum does not find any symbol or export in the Rust binary

  • check symbols manually with equivalent of nm which is dumpbin.exe
    • use developer terminal to use dumpbin.exe easily
    • Windows executables are stripped of any export symbols
  • Our previous approach used debug symbols to find the native pointer to the harness
    • debug symbols are not available on windows (in the file directly but separate ".pdb" file)
    • We change so we use the raw address provided at the beginning to create the NativePointer

No display from crash result

  • When running the fuzzer the crash happens but nothing is displayed
  • We change the panic hook order such that original panic hook is executed before the fuzzer panic hook

Error status of crashed program in fuzzer

  • In windows the error status chosen by LibAFL is 1 instead of 134

Find equivalent of libc functions

  • Example with finding a CRT function that is used to open a file
  • Debug a function that is opening a file with Visual Studio and tracks the execution
    • fs.rs file needs to be provided.
      • It's in C:\Users\alex-cn\.rustup\toolchains\stable-x86_64-pc-windows-msvc\lib\rustlib\src\rust\library\std\src\sys\windows\fs.rs
    • Find the function called c::CreateFileW used t
    • in the c directory find that CreateFileW comes from the kernel32 dll
  • Check Rust source code and finds the OS-specific implementation

Tests show tauri_fuzz_tools-917323a62e294d07.exe write_foo (exit code: 0xc0000139, STATUS_ENTRYPOINT_NOT_FOUND)

  • This is similar error message to previous issue which was missing tauri_build::build()
    • checked that build script is executed to build the tests
    • issue seems to come from the tauri-fuzz-tools crate
  • From experiments tauri_fuzz_tools tests
    • fails to run from workspace directory with cargo t
      • executable produced is bigger than the successful one
    • run fine from workspace directory with cargo t -p tauri_fuzz_tools
      • executable produced is smaller than the failing one
    • run fine when executing cargo t from the crate directory
    • runs fine when putting tauri_fuzz_tools as the sole default member of the workspace
    • fails when putting tauri_fuzz_tools as default member with any other member
  • Adding a Windows Manifest file works to remove the error message
    • https://github.com/tauri-apps/tauri/pull/4383/files
    • Does not explain why the compilation worked in certain cases but not in other =(
  • Tried with crate embed-manifest
    • crate seems outdated contain build instruction not recognized

Fetching values from register does not give expected value

  • the policy "block_file_by_names" does not work
  • Windows do not use utf-8 encoding but utf-16 for strings
    • use the windows crate to import correct windows type and do type conversion

Conflicting C runtime library during linking

= note: LINK : warning LNK4098: defaultlib "LIBCMT" conflicts with use of other libs; use /NODEFAULTLIB:library
          LINK : error LNK1218: warning treated as error; no output file generated
  • This seems to happen
  • I don't really know what made this bug appear
    • one suspicion is the upgrade to Rust 1.78
    • Amr had this first and I only got it when I manually updated my rustup
  • Cause of the event
    • conflicting lib c runtime have been found
    • I see in the compilation logs that we already link against the "msvcrt.lib" c runtime
    • my guess is that some library is trying to link against "libcmt" on top
  • Solution found
    • linker options added in .cargo/config.toml file
    [target.x86_64-pc-windows-msvc]
    rustflags = ["-C", "link-args=/NODEFAULTLIB:libcmt.lib"]
    
  • to be honest I don't really understand what's happening precisely and I don't want to dig further. But I'm happy to have found a solution quickly but I expect this to bite me back in the future

NtCreateFile use flags different from the doc

  • doc: https://learn.microsoft.com/en-us/windows/win32/api/winternl/nf-winternl-ntcreatefile
  • from the doc NtCreateFile is supposed to use flags such as:
    • FILE_GENERIC_READ: 0x00120089
    • FILE_GENERIC_WRITE: 0x00120116
    • FILE_READ_DATA: 0x00000001
  • from the experimentations we get values such as:
    • open file in read only: 0x80100080
    • open file in write only: 0x40100080
  • this matches other known windows constants that exist are:
    • GENERIC_READ: 0x80000000
    • GENERIC_WRITE: 0x40000000
  • we will use these flags eventhough this is different from what described from the doc

Docker on Windows

  • Docker daemon can be started by launching Docker desktop
  • docker-credential-desktop not installed or not available in PATH
    • in the file C:\Users\user\.docker\config.json
    • delete credsStore field

Tools for debugging

  • ProcessMonitor to see all the events related to a process
  • DependencyWalker to investigate issue related to modules/dlls

Default policy

  • We want to have a default policy that catches any calls to an external binary that returns an error

    • our intuition is a call to an external binary that can result into a syntax error also has a chance to be vulnerable to an exploit
    • with the fuzzer there is a high chance "vulnerable" calls to external process will result in syntax error
  • We want to attach to Rust std::process::Command::spawn/output

    • I don't see the symbol of these functions in the binary, I don't really get why
  • Maybe the solution is to attach to execv family of calls and monitor the return status of the call

    • this is lower level that rust Command, we can catch more external interactions from the app we monitor
    • I believe this is called by rust Command but I need to check that
  • All functions from exec family calls execve

  • Fuzzer crashes when monitoring execv

    • it does not crash when monitoring other functions
    • it crashes in the fuzzer code
      • with fuzz_test
        • with a rule that never blocks
          • it crashes in the harness and is captured by the fuzzer
        • with a rule that always blocks
          • it crashes in the harness too
          • actually the harness has time to finish, corruption appears after the harness
          • *** stack smashing detected ***: terminated
      • with fuzz_main
        • with a rule that always blocks
          • it crashes in the harness when the tauri command is finished but the harness has not finished yet
          • *** stack smashing detected ***: terminated
        • with a rule that never blocks
          • it crashes in the harness when the tauri command is finished but the harness has not finished yet
          • *** stack smashing detected ***: terminated
    • I think that after the harness the fuzzer calls execve before the flag is removed
    • Call order starting from when the harness is being called
      • in libafl::Executor::run_target: let ret = (self.harness_fn.borrow_mut())(input);
      • libafl::executors::inprocess::GenericInProcessExecutor
      • core::ops::function::FnMut::call_mut
      • ls_with_rust::harness with ls_with_rust the binary being executed
        • _gum_function_context_begin_invocation
          • gum_tls_key_get_value
          • pthread_getspecific
          • gum_tls_key_set_value
          • get_interceptor_thread_context
          • gum_thread_get_system_error
          • gum_invocation_stack_push
            • gum_sign_code_pointer
          • gum_rust_invocation_listener_on_enter
            • frida_gum::interceptor::invocation_listener::call_on_enter
              • libafl_frida::syscall_isolation_rt::HarnessListener::on_enter
          • gum_thread_set_system_error
            • __errno_location@plt
          • gum_tls_key_set_value
          • pthread_setspecific
        • harness code ...
      • pure asm code that push registers on the stack
        • that looks like context switch with context being saved on the stack
      • _gum_function_context_end_invocation
        • gum_tls_key_set_value
          • pthread_setspecific@plt
        • gum_thread_get_system_error
          • __errno_location@plt
        • get_interceptor_thread_context
          • _frida_g_private_get
            • g_private_get_impl
            • pthread_getspecific@plt
        • gum_sign_code_pointer
        • gum_rust_invocation_listener_on_leave
          • frida_gum::interceptor::invocation_listener::call_on_leave
            • frida_gum::interceptor::invocation_listener::InvocationContext::from_raw
            • libafl_frida::syscall_isolation_rt::HarnessListener::on_leave
        • gum_thread_set_system_error
          • _errno_location@plt
        • _frida_g_array_set_size
        • gum_tls_key_set_value
          • pthread_setspecific
      • pure asm code that pop stack values into registers
        • restore context switch
    • __execvpe_common.isra: here we crash
  • Correct execution trace at the end of the harness:

    • pure asm code that push registers on the stack
      • that looks like context switch with context being saved on the stack
    • _gum_function_context_end_invocation
      • gum_tls_key_set_value
        • pthread_setspecific@plt
      • gum_thread_get_system_error
        • __errno_location@plt
      • get_interceptor_thread_context
        • _frida_g_private_get
          • g_private_get_impl
          • pthread_getspecific@plt
      • gum_sign_code_pointer
      • gum_rust_invocation_listener_on_leave
        • frida_gum::interceptor::invocation_listener::call_on_leave
          • frida_gum::interceptor::invocation_listener::InvocationContext::from_raw
          • libafl_frida::syscall_isolation_rt::HarnessListener::on_leave
      • gum_thread_set_system_error
        • _errno_location@plt
      • _frida_g_array_set_size
      • gum_tls_key_set_value
        • pthread_setspecific
    • pure asm code that pop stack values into registers
    • here we don't crash contrary to above
  • New approach where we detach the frida listeners of monitored functions instead of deactivating them

    • Contrary to what the docs says, calling Gum::obtain produce a deadlock (in doc it's supposed to do a no-op)
    • Without Gum::obtain we can't detach the monitored function listeners
  • Weirdest thing ever: the crash does not appear anymore with gdb when putting a breakpoint on execve

  • I'm temporarily giving up on monitoring execv

    • I still think it's the
  • Trying with __execve instead of execve

    • maybe C weak links mess up with Frida
    • not working either
  • Ok I just notice that my approach was wrong anyway

    • execve usually called in the child process after being forked
    • Frida rust bindings do not support monitoring the child process anyway
    • I still don't know why there was a bug

Improving engine code

  • Our rules now use Fn closure trait rather than fn object
  • this allow us to make rules that are more flexible with captured variables and arguments
  • the main issue was to use Box<dyn Fn>> that were also implementing Clone
    • inspiration from Tauri code with all the callbacks
    • this thread helped us solve the issue: https://users.rust-lang.org/t/how-to-clone-a-boxed-closure/31035/7
    • replace Box by Arc
    • we could also create manual cloneable Box<dyn Fn>> like this example
      • https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=6ca48c4cff92370c907ecf4c548ee33c

Improve tests

  • Refactor tests to avoid too much repetition
  • All tests are gathered in a single crate to avoid too much disk usage

Default policy

  • We have a new approach where we monitor the std::process::Command API

    • we detect any new creation process
      • we track Command::status, Command::output, Command::spawn
      • ideally we could track a single function: std::sys::windows/unix::process::Command
        • all the above functions call this private function
        • unfortunately this function is private and we can't attach to it with Frida
        • actually it seems we can! Just found this in the symbols
        "SymbolDetails _ZN3std3sys3pal4unix7process13process_inner71_$LT$impl$u20$std..sys..pal..unix..process..process_common..Command$GT$5spawn17hffc9080bc0517252E: [0x0000555740c67360, 0x0000555740c680c1]",
        
    • we can also detect error status of external process
      • we track Command::output, Command::status, Child::wait, Child::try_wait, Child::wait_with_output,
      • an issue is that we don't know from which binary we returned from
    • Limit of our current approach is that we can only detect invocation of external binaries from the Rust API
      • we don't detect invocation of ext binaries through libc fork + execve
      • but we could monitor wait and waitpid to track error status
  • We monitor wait and waitpid

    • this is a superset of monitoring rust std::process::Command
    • we had to modify the policy engine to add a storage that can store function parameter at entry that can then be reused when analysing the function at exit
      • this is necessary due to common C pattern that store results of a function in a mutable pointer given as parameter
    • Question: Do libc usually call wait or waitpid after spawning a child process?
      • they should otherwise it would create zombie process
    • Can we do better?
      • ideally we would track fork + execve but it seems too complex with Frida
      • external process can be called by other means than creating a child process
        • for example in SQL an RPC is used to talk to SQL server and no fork is ever used
        • we also need to track networking then =(
      • we are using the assumption that a child process will return 0 as exit status when the execution went well. Is it always true?

libc wait

  • we want to also capture error status of child processes that were invoked through the libc API
    • from my knowledge these child processes are invoked using fork then execve
    • one way to get the return status of these child processes is to capture calls to wait from the parent process
  • the issue with wait is that the child exit status is returned through mutating a variable that was sent as argument and not through the return value
  • to fix that we may need to store the pointer that was provided as argument to be able to check it on exit
    • we implemented that and it works great

Bug with plugin fs_readFile

  • For unknown reason when accessing the filesystem with tauri_plugin_fs the interception does not occur
    • this does not occur when accessing the filesystem with other functions
  • Possible reasons for that:
    • tauri_plugin_fs::read_file does not call open
      • this is unlikely since tauri_plugin_fs uses this Rust code let file = std::fs::OpenOptions::from(open_options.options)
    • Tauri plugins are executed in a context which are not tracked by Frida
      • In another process?
      • Let's check the Tauri changelog
  • We solve this in another PR
  • From our investigation it seems that listener to the harness does not function
    • it works when giving it a pointer to the Tauri commands we want to fuzz
    • it does not seem to work when giving it the whole harness
    • the address of the harness we give to the fuzzer and the one found in the binary seem to differ, I don't know the cause
    • I believe because we improved the code to be polymorphic
      • Due to monomorphisation there should be multiple implementation of our generic function
    • We changed the way we take harness pointer, make it a function rather than a closure

Removing LibAFL fork from the project

  • the project is more about having a runtime that detects anomalies during fuzzing than creating a fuzzer in itself
  • we can decouple the project from LibAFL furthermore and remove our fork of LibAFL to be sync with the upstream version
  • for convenience we still are couple with libafl_frida by implementing the