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.
[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 theno_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
Platform | Can theoretically work | Tested 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
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.
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
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 cratecrates/tauri-fuzz-cli
that builds the binarycargo-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
asmini-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 cratecrates/tauri-fuzz-cli
that builds the binarycargo-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.
- Create the fuzz directory
mkdir -p mini-app/src-tauri/fuzz
- 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
- 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)
}
- Add
build.rs
andtauri.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
asmini-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.
Class | Policy | Usage | Description |
---|---|---|---|
Generic | No Policy | tauri-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 Helper | Block on entry | tauri-fuzz-policies::block_on_entry() | The function monitored with this rule will just automatically crash when called. |
File System policies | No file access | tauri-fuzz-policies::file_policy::no_file_access() | Any access to file system will provoke a crash. |
Read only access | tauri-fuzz-policies::file_policy::read_only_access() | Any access to file system with write access will provoke a crash. | |
No access to filenames | tauri-fuzz-policies::file_policy::no_access_to_filenames(filenames) | Any access to the files given as parameter will provoke a crash. | |
Child process | Invocation of child process through Rust std is blocked | tauri-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 monitored | tauri-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 error | tauri-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 error | tauri-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 | |
Generic | Block any calls to the host system that returns an error | tauri-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
- Rust Fuzzers
- Java Serialization Fuzzers
- Javascript Engine Fuzzers
- Fuzzer for Windows
- Fuzzer Composition
- Concurrency
- Webapp fuzzing
- Trusted Environment Fuzzing
- Fuzzing during RTL development stage (specifications)
- Spec fuzzing
- Resources
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
- 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
- LibAFL
- Fuzzers Like Lego (CCC Talk)
- Tauri Commands Documentation
- LibAFL paper from 2022
- Fuzzy 101
- AFL++ doc
- AFL++ modules explanation
Web applications fuzzing
- Challenges
- Types of fuzzer for webapps
- Industry solution
- WebFuzz
- Witcher
- BlackWidow
- REST API fuzzing
- Typical attacks
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
- detecting inputs that triggers vulnerabilities
- 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
- collecting coverage information
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
- Pros/Cons
- 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
andform
elements - retrieve inputs from
input
,textarea
andoption
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 thejavascrip:
label - executable attribute that starts with the
on
prefix (e.g.onclick
) - script elements
- link attribute (e.g.
- fuzzer injects XSS payloads in the HTTP requests to call
alert()
- fuzzer detector check for any calls to
alert()
- fuzzer detector check for any calls to
- look for stored and reflective XSS vulnerabilities
- 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()
orpassthru()
will be passed todash
- 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 byReqr
into the fuzzing dictionary
Evaluation
- blackbox vs greybox: Outperforms
Blurp
in vulnerabilities found - Covers more code than
BlackWidow
andwebFuzz
- 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
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
SQL injection
Command injection
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
- Google sanitizers for LLVM
- ASAN, MSAN
- 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:
- Give a unique label to all basic block
- Store any data related edge coverage to a global var
- At the beginning of each label xor current label and previous one
- This value is the edge label and is used as index for map coverage
- At the end of a basic block rightshift current label
- this is to prevent 0-value label if basic block jumps on itself
- Store the rightshifted value as "previous visited block"
- 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 techniquecargo-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
- insert
- uses the Rust profiler runtime
- enabled by default on the
+nightly
channel
- enabled by default on the
- needs to use a Rust demangler:
rustfilt
- can be provided to llvm options
Using it
- 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
- Run the binary compiled
- it should produce a file
default_*.profraw
- or name it with
LLVM_PROFILE_FILE="toto.profraw"
- it should produce a file
- Process coverage data with
llvm-profdata
- can be installed with
rustup
llvm-profdata merge -sparse toto.profraw -o toto.profdata
- can be installed with
- 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
- can be installed with
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
- will insert a call to
- 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 instrumentationlibafl_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
.
cargo libafl init
- create a directory
fuzz_targets
- create a directory
cargo libafl run <fuzz target name>
exec_build
givesRUSTFLAGS="-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 > LLVM > gcc plugin > gcc mode
- Important checking the coverage of the fuzzing
- use
afl-showmap
- Section 3.g of AFL Guide to Fuzzing in Depth
- use
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++
andafl-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
orsyscall
with syscall number in eax/rax - through the libc
Types of system calls
- Process control
- create process:
fork
- terminate process
- load, execute
- get/set process attributes
- wait for time/event, signal event
- allocate and free memory
- create process:
- File management
- Device management
- Information maintenance
- get
- Communication
- Protection
- get/set file permissions
Intercepting system calls
- using frida
- track for
int 0x80
orsyscalls
- calls to
libc
- track for
- tools to intercept syscalls
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
- package
- 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
- in this situation you can use
- its role it to attach/hijack the target program to be able to interact with it
- 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
- Stalker
- code tracing enging
- capture every function/code/instruction that is executed
- Interceptor
- function hooking engine
- MemoryAccessMonitor
- Stalker
Panic in Rust
panic!
std::panic::panic_any(msg)
- if exists, panic hook is called
std::panic::set_hook
- if panic hook returns, unwind the thread stack
- 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
- some frames may be marked as "catching" the unwind
- marked via
std::panic::catch_unwind()
- marked via
- 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
- if it's main thread then
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 eventtauri::app::on_event_loop_event
could be used to fuzz the Tauri app by calling it directly from maintauri::app::Builder::on_page_load
could be used to fuzz the Tauri app by calling it directly from maintauri::tauri-runtime-wry
is the default implementation of the runtimetauri::tauri-runtime
is the runtime interfacewry-runtime
event loop receives different type of events:tao::Event
receives from TAOtao::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 fieldsjs_event_listeners
ipc_handler
- Check
wry
crate- webview sends message to the Rust host using
window.ipc.postMessage("message")
- webview sends message to the Rust host using
- 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
andsendmsg
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
- we see traces of
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 sockets file in
- Checking
tauri
,wry
andtao
code to see where IPC comes from- connect to local version of wry and tauri
tao::EventLoop::run_return
when spawning x11 thread containslet (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/*
- at build time of the webview these handlers will generate methods
that can called via
- From Lucas suggestion
tauri::window::Window::on_message
can trigger commandhttps://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 mademini-app/src-tauri/src/main.rs
an emptymain(){}
function - change the architecture
- remove
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 incargo 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 freerun_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
orExitKind::Ok
- the fuzzer is not restarted and corpus may ran out because not regenerated
- When harness crashes with
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
- for
- 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
- Split
mini-app
to have both a binary and a lib - Use the in-memory fuzzing to call functions from the lib
- Split
- 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 processwindow::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 isapp::Window::DetachedWindow::Dispatcher::close()
- it will send the message
Message::CloseWindow
withrun_on_main_thread
- the mockruntime intercept it and sends
RuntimeEvent::ExitRequested
to theapp
- the
app
will process some stuff inon_event_loop_event
- then the event
RuntimeEvent::ExitRequested
will be sent to the closure given toapp::run
at the beginning
- it will send the message
- you can break out of the loop from
run
in theMockruntime
- by sending a message
Message::CloseWindow
- then sending another message which is not
ExitRequestedEventAction::Prevent
- by sending a message
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
- now the application can add an almost empty
- 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
- check your rustc llvm with
- output llvm with
rustc
+ vanilla compilation withafl-clang-lto
fails and not practical - trying with
.cargo/config.toml
[target.x86_64-unknown-linux-gnu] linker = "afl-clang-lto"
- version of rustc llvm and the llvm you want to use need to match
- 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 ");
- files are compiled with
- 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 inlibafl_targets
- it place calls to
- 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-argsrustc -C llvm-args="--help-hidden" | nvim -
16
-
cargo-libafl
is a fork ofcargo-fuzz
-
How does it work with libfuzzer
init
command creates afuzz
directory withfuzz_targets
with harness using thefuzz_target!
macroCargo.toml
containing dependency tolibfuzzer-sys
libfuzzer-sys
can refer to the original fromcrates.io
or to the ported version fromlibafl
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:
- Compile the
fuzz_targets
with the commandRUSTFLAGS="-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"
- Run the
fuzz_targets
with the commandRUSTFLAGS="-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"
- Compile the
- Working when using the deprecrated original
-
fuzz_target!
macro definition is incargo-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
orcargo-libafl
to use their framework to easily fuzz Tauri applications
17
- Use
cargo-fuzz
as frontend for the fuzzing then uselibafl
as a backend replacinglibfuzzer
- Installing
rustup component add llvm-preview-tools
to see information about code coveragecargo fuzz run fuzz_target
cargo fuzz coverage fuzz_target
- 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 attarget/arch_triple/coverage/arch_triple/release/fuzz_target
- profile file is generated atcoverage/fuzz_target/coverage.profdata
- 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 withlibafl_libfuzzer
version- doc for options in the
LibAFL/libafl_libfuzzer/src/lib.rs
- doc for options in the
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 triggeron_message
- issue with using
Cores("0")
but works fine with other corescores.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'
- to use the binary directly and linking with the dynamic
- 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
- Check any "calls" and destination address
- 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 butopen
- 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
- the
- 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 thepanic_hook
- we dodge the issue by rewriting the
panic_hook
- it's difficult to set the
- this happens with the stalker
- this happens in the fuzzer
28
- Trying to refact
fuzzer.rs
to have the same code to usefuzz_one
orLauncher
- 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 nestedimpl
- 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
- using
- 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 workpanic::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())
- Issue is that tests that crash actually are handled by the fuzzer and actually
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
- fuzzer and security policy code moved to the application being fuzzed
- 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
- error message triggered is
- 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/
tomini-app-fuzz
- seamless transition, just had to change dependency in the workspace
Cargo.toml
- seamless transition, just had to change dependency in the workspace
- 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 inlibafl_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 submoduleLibAFL
is also a Rust workspace itself so we had toexclude = ["LibAFl"]
it from the rootCargo.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 slidesexamples/
with mini-app and its fuzz codecrates/
withLibAFL
,policies
,fuzzer
- Create a TOML configuration file for the fuzzer
- more simple intermediary type to
libafl_bolts::FuzzerOptions
- more simple intermediary type to
- Why is our code coverage not working for the moment?
- the
harness
andlibs_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 bylibafl_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
- currently the precision is per
- the
- 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 testscargo 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 asfs: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
- the
- 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
- 3 env variables per plugin used
- 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
- path to permission manifests from each plugin are stored in environment variables
- InvokeRequest
url
- to have request that are deemed
Local
usetauri://localhost
- to have request that are deemed
- 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 thanisize::max
- Bug in libafl_frida which trigger this situation when
stalker_is_enabled
is set to true inlibafl_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 bygum_sys
is empty - this provokes a panic from Rust 1.78
- current fix is to disable coverage but not good enough
- in
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
returnsInvokeBody::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)
expectsv
to bebytes of JSON text
(fromserde_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 fromfs::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
- I need to install
- 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
- that should be
- Error found
TaskEntryDialog
entrypoint could not be found- running the fuzzer from windows ui
- or using cbc
- Dependency walker shows the missing modules
- one of the main missing module is
API-MS-WIN-CORE
- one of the main missing module is
- Using
ProcessMonitor
with a filter ontauri_cmd_1.exe
- run the executable and you get all the related events
- running the fuzzer from windows ui
- Big chances it is related to
tauri-build
which does a lot in windows- reintroduce a
build.rs
file withtauri_build::build()
- Find a way to have a generic and minimal
tauri.conf.json
for the fuzz directory
- reintroduce a
frida_gum
does not find any symbol or export in the Rust binary
- check symbols manually with equivalent of
nm
which isdumpbin.exe
- use developer terminal to use
dumpbin.exe
easily - Windows executables are stripped of any export symbols
- use developer terminal to use
- 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
- It's in
- Find the function called
c::CreateFileW
used t - in the
c
directory find thatCreateFileW
comes from thekernel32
dll
- fs.rs file needs to be provided.
- 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
- fails to run from workspace directory with
- 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
- use the
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"]
- linker options added in
- 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
- in the file
Tools for debugging
ProcessMonitor
to see all the events related to a processDependencyWalker
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
- this is lower level that rust
-
All functions from
exec
family callsexecve
- from this implementation of libc https://github.com/zerovm/glibc/blob/master/posix/execv.c
-
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 a rule that never blocks
- 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
- with a rule that always blocks
- with fuzz_test
- 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
withls_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
- in
__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
- pure asm code that push registers on the stack
-
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
- Contrary to what the docs says, calling
-
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 ofexecve
- 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 thanfn
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 implementingClone
- 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
byArc
- 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 track
- 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
- we track
- 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
andwaitpid
to track error status
- we don't detect invocation of ext binaries through libc
- we detect any new creation process
-
We monitor
wait
andwaitpid
- 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
orwaitpid
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 =(
- for example in SQL an RPC is used to talk to SQL server and no
- we are using the assumption that a child process will return 0 as exit status when the execution went well. Is it always true?
- ideally we would track
- this is a superset of monitoring rust
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
thenexecve
- one way to get the return status of these child processes is to capture calls to
wait
from the parent process
- from my knowledge these child processes are invoked using
- 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 callopen
- this is unlikely since
tauri_plugin_fs
uses this Rust codelet file = std::fs::OpenOptions::from(open_options.options)
- this is unlikely since
- 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