Bring back python bindings for sugar,qemu (partially revert #2005) (#2020)

* Bring back python bindings for sugar,qemu (partially revert #2005)

* sugarman, won't you hurry

* Test?
This commit is contained in:
Dominik Maier 2024-04-08 19:36:54 +02:00 committed by GitHub
parent e8fe5bb614
commit f19302c9b1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
32 changed files with 1159 additions and 81 deletions

View File

@ -280,7 +280,32 @@ jobs:
- name: Run smoke test
run: ./libafl_concolic/test/smoke_test.sh
fuzzers:
python-bindings:
runs-on: ubuntu-latest
steps:
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
- name: Remove existing clang and LLVM
run: sudo apt purge llvm* clang*
- name: Install LLVM and Clang
uses: KyleMayes/install-llvm-action@v1
with:
directory: ${{ runner.temp }}/llvm
version: 17
- name: Install deps
run: sudo apt-get install -y ninja-build python3-dev python3-pip python3-venv libz3-dev
- name: Install maturin
run: python3 -m pip install maturin
- uses: actions/checkout@v3
- uses: Swatinem/rust-cache@v2
- name: Run a maturin build
run: export LLVM_CONFIG=llvm-config-16 && cd ./bindings/pylibafl && python3 -m venv .env && . .env/bin/activate && pip install --upgrade --force-reinstall . && ./test.sh
- name: Run python test
run: . ./bindings/pylibafl/.env/bin/activate && cd ./fuzzers/baby_fuzzer && python3 baby_fuzzer.py 2>&1 | grep "Bye"
fuzzers:
strategy:
matrix:
os: [ubuntu-latest]

1
bindings/pylibafl/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
dist/

View File

@ -0,0 +1,23 @@
[package]
name = "pylibafl"
version = "0.11.2"
edition = "2021"
[dependencies]
pyo3 = { version = "0.18.3", features = ["extension-module"] }
pyo3-log = "0.8.1"
libafl_sugar = { path = "../../libafl_sugar", version = "0.11.2", features = ["python"] }
libafl_bolts = { path = "../../libafl_bolts", version = "0.11.2", features = ["python"] }
[target.'cfg(target_os = "linux")'.dependencies]
libafl_qemu = { path = "../../libafl_qemu", version = "0.11.2", features = ["python"] }
[build-dependencies]
pyo3-build-config = { version = "0.17" }
[lib]
name = "pylibafl"
crate-type = ["cdylib"]
[profile.dev]
panic = "abort"

View File

@ -0,0 +1,31 @@
# How to use python bindings
## First time setup
```bash
# Install maturin
pip install maturin
# Create virtual environment
python3 -m venv .env
```
## Build bindings
```
# Activate virtual environment
source .env/bin/activate
# Build python module
maturin develop
```
This is going to install `pylibafl` python module into this venv.
## Use bindings
### Example: Running baby_fuzzer in fuzzers/baby_fuzzer/baby_fuzzer.py
First, make sure the python virtual environment is activated. If not, run `source .env/bin/activate
`. Running `pip freeze` at this point should display the following (versions may differ):
```
maturin==0.12.6
pylibafl==0.7.0
toml==0.10.2
```
Then simply run
```
python PATH_TO_BABY_FUZZER/baby_fuzzer.py
```
The crashes directory will be created in the directory from which you ran the command.

View File

@ -0,0 +1,26 @@
[build-system]
requires = ["maturin[patchelf]>=0.14.10,<0.15"]
build-backend = "maturin"
[project]
name = "PyLibAFL"
version = "0.10.1"
description = "Advanced Fuzzing Library for Python"
readme = "README.md"
requires-python = ">=3.8"
license = {text = "Apache-2.0"}
classifiers = [
"License :: OSI Approved :: Apache Software License",
"License :: OSI Approved :: MIT License",
"Programming Language :: Rust",
"Topic :: Security",
]
[project.urls]
repository = "https://github.com/AFLplusplus/LibAFL.git"
[tool.maturin]
bindings = "pylibafl"
manifest-path = "Cargo.toml"
python-source = "python"
all-features = true

View File

@ -0,0 +1,33 @@
use pyo3::prelude::*;
/// Setup python modules for `libafl_qemu` and `libafl_sugar`.
///
/// # Errors
/// Returns error if python libafl setup failed.
#[pymodule]
#[pyo3(name = "pylibafl")]
pub fn python_module(py: Python, m: &PyModule) -> PyResult<()> {
pyo3_log::init();
let modules = py.import("sys")?.getattr("modules")?;
let sugar_module = PyModule::new(py, "sugar")?;
libafl_sugar::python_module(py, sugar_module)?;
m.add_submodule(sugar_module)?;
modules.set_item("pylibafl.sugar", sugar_module)?;
#[cfg(target_os = "linux")]
{
let qemu_module = PyModule::new(py, "qemu")?;
libafl_qemu::python_module(py, qemu_module)?;
m.add_submodule(qemu_module)?;
modules.set_item("pylibafl.qemu", qemu_module)?;
}
let bolts_module = PyModule::new(py, "libafl_bolts")?;
libafl_bolts::pybind::python_module(py, bolts_module)?;
m.add_submodule(bolts_module)?;
modules.set_item("pylibafl.libafl_bolts", bolts_module)?;
Ok(())
}

View File

@ -0,0 +1,7 @@
import pylibafl.sugar as sugar
import ctypes
import platform
print("Starting to fuzz from python!")
fuzzer = sugar.InMemoryBytesCoverageSugar(input_dirs=["./in"], output_dir="out", broker_port=1337, cores=[0,1])
fuzzer.run(lambda b: print("foo"))

14
bindings/pylibafl/test.sh Executable file
View File

@ -0,0 +1,14 @@
#!/usr/bin/env bash
mkdir in || true
echo "a" > ./in/a
timeout 10 python3 ./test.py
export exit_code=$?
if [ $exit_code -eq 124 ]; then
# 124 = timeout happened. All good.
exit 0
else
exit $exit_code
fi

View File

@ -0,0 +1,14 @@
#include <stdint.h>
#include <stdlib.h>
#include <stdio.h>
int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) {
// printf("Got %ld bytes.\n", Size);
if (Size >= 4 && *(uint32_t *)Data == 0xaabbccdd) { abort(); }
return 0;
}
int main() {
char buf[10] = {0};
LLVMFuzzerTestOneInput(buf, 10);
}

View File

@ -0,0 +1,43 @@
# from the maturin venv, after running 'maturin develop' in the pylibafl directory
from pylibafl import sugar, qemu
import lief
MAX_SIZE = 0x100
BINARY_PATH = './a.out'
emu = qemu.Emulator(['qemu-x86_64', BINARY_PATH], [])
elf = lief.parse(BINARY_PATH)
test_one_input = elf.get_function_address("LLVMFuzzerTestOneInput")
if elf.is_pie:
test_one_input += emu.load_addr()
print('LLVMFuzzerTestOneInput @ 0x%x' % test_one_input)
emu.set_breakpoint(test_one_input)
emu.run()
sp = emu.read_reg(qemu.regs.Rsp)
print('SP = 0x%x' % sp)
retaddr = int.from_bytes(emu.read_mem(sp, 8), 'little')
print('RET = 0x%x' % retaddr)
inp = emu.map_private(0, MAX_SIZE, qemu.mmap.ReadWrite)
assert(inp > 0)
emu.remove_breakpoint(test_one_input)
emu.set_breakpoint(retaddr)
def harness(b):
if len(b) > MAX_SIZE:
b = b[:MAX_SIZE]
emu.write_mem(inp, b)
emu.write_reg(qemu.regs.Rsi, len(b))
emu.write_reg(qemu.regs.Rdi, inp)
emu.write_reg(qemu.regs.Rsp, sp)
emu.write_reg(qemu.regs.Rip, test_one_input)
emu.run()
fuzz = sugar.QemuBytesCoverageSugar(['./in'], './out', 3456, [0,1,2,3])
fuzz.run(emu, harness)

View File

@ -35,6 +35,9 @@ derive = ["libafl_derive"]
## If set, libafl_bolt's `rand` implementations will implement `rand::Rng`
rand_trait = ["rand_core"]
## Will build the `pyo3` bindings
python = ["pyo3", "std"]
## Expose `libafl::prelude` for direct access to all types without additional `use` directives
prelude = []
@ -113,6 +116,8 @@ uuid = { version = "1.4", optional = true, features = ["serde", "v4"] }
clap = {version = "4.5", features = ["derive", "wrap_help"], optional = true} # CLI parsing, for libafl_bolts::cli / the `cli` feature
log = "0.4.20"
pyo3 = { version = "0.18", optional = true, features = ["serde", "macros"] }
# optional-dev deps (change when target.'cfg(accessible(::std))'.test-dependencies will be stable)
serial_test = { version = "2", optional = true, default-features = false, features = ["logging"] }

View File

@ -611,6 +611,22 @@ impl From<windows::core::Error> for Error {
}
}
#[cfg(feature = "python")]
impl From<pyo3::PyErr> for Error {
fn from(err: pyo3::PyErr) -> Self {
pyo3::Python::with_gil(|py| {
if err.matches(
py,
pyo3::types::PyType::new::<pyo3::exceptions::PyKeyboardInterrupt>(py),
) {
Self::shutting_down()
} else {
Self::illegal_state(format!("Python exception: {err:?}"))
}
})
}
}
#[cfg(all(not(nightly), feature = "std"))]
impl std::error::Error for Error {}
@ -1022,6 +1038,148 @@ pub unsafe fn set_error_print_panic_hook(new_stderr: RawFd) {
}));
}
#[cfg(feature = "python")]
#[allow(missing_docs)]
pub mod pybind {
use pyo3::{pymodule, types::PyModule, PyResult, Python};
#[macro_export]
macro_rules! unwrap_me_body {
($wrapper:expr, $name:ident, $body:block, $wrapper_type:ident, { $($wrapper_option:tt),* }) => {
match &$wrapper {
$(
$wrapper_type::$wrapper_option(py_wrapper) => {
Python::with_gil(|py| -> PyResult<_> {
let borrowed = py_wrapper.borrow(py);
let $name = &borrowed.inner;
Ok($body)
})
.unwrap()
}
)*
}
};
($wrapper:expr, $name:ident, $body:block, $wrapper_type:ident, { $($wrapper_option:tt),* }, { $($wrapper_optional:tt($pw:ident) => $code_block:block)* }) => {
match &$wrapper {
$(
$wrapper_type::$wrapper_option(py_wrapper) => {
Python::with_gil(|py| -> PyResult<_> {
let borrowed = py_wrapper.borrow(py);
let $name = &borrowed.inner;
Ok($body)
})
.unwrap()
}
)*
$($wrapper_type::$wrapper_optional($pw) => { $code_block })*
}
};
}
#[macro_export]
macro_rules! unwrap_me_mut_body {
($wrapper:expr, $name:ident, $body:block, $wrapper_type:ident, { $($wrapper_option:tt),*}) => {
match &mut $wrapper {
$(
$wrapper_type::$wrapper_option(py_wrapper) => {
Python::with_gil(|py| -> PyResult<_> {
let mut borrowed = py_wrapper.borrow_mut(py);
let $name = &mut borrowed.inner;
Ok($body)
})
.unwrap()
}
)*
}
};
($wrapper:expr, $name:ident, $body:block, $wrapper_type:ident, { $($wrapper_option:tt),*}, { $($wrapper_optional:tt($pw:ident) => $code_block:block)* }) => {
match &mut $wrapper {
$(
$wrapper_type::$wrapper_option(py_wrapper) => {
Python::with_gil(|py| -> PyResult<_> {
let mut borrowed = py_wrapper.borrow_mut(py);
let $name = &mut borrowed.inner;
Ok($body)
})
.unwrap()
}
)*
$($wrapper_type::$wrapper_optional($pw) => { $code_block })*
}
};
}
#[macro_export]
macro_rules! impl_serde_pyobjectwrapper {
($struct_name:ident, $inner:tt) => {
const _: () = {
use alloc::vec::Vec;
use pyo3::prelude::*;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
impl Serialize for $struct_name {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let buf = Python::with_gil(|py| -> PyResult<Vec<u8>> {
let pickle = PyModule::import(py, "pickle")?;
let buf: Vec<u8> =
pickle.getattr("dumps")?.call1((&self.$inner,))?.extract()?;
Ok(buf)
})
.unwrap();
serializer.serialize_bytes(&buf)
}
}
struct PyObjectVisitor;
impl<'de> serde::de::Visitor<'de> for PyObjectVisitor {
type Value = $struct_name;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter
.write_str("Expecting some bytes to deserialize from the Python side")
}
fn visit_byte_buf<E>(self, v: Vec<u8>) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
let obj = Python::with_gil(|py| -> PyResult<PyObject> {
let pickle = PyModule::import(py, "pickle")?;
let obj = pickle.getattr("loads")?.call1((v,))?.to_object(py);
Ok(obj)
})
.unwrap();
Ok($struct_name::new(obj))
}
}
impl<'de> Deserialize<'de> for $struct_name {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_byte_buf(PyObjectVisitor)
}
}
};
};
}
#[pymodule]
#[pyo3(name = "libafl_bolts")]
/// Register the classes to the python module
pub fn python_module(py: Python, m: &PyModule) -> PyResult<()> {
crate::rands::pybind::register(py, m)?;
Ok(())
}
}
#[cfg(test)]
mod tests {

View File

@ -433,3 +433,92 @@ mod tests {
log::info!("random value: {}", mutator.rng.next_u32());
}
}
#[cfg(feature = "python")]
#[allow(clippy::unnecessary_fallible_conversions, unused_qualifications)]
#[allow(missing_docs)]
/// `Rand` Python bindings
pub mod pybind {
use pyo3::prelude::*;
use serde::{Deserialize, Serialize};
use super::Rand;
use crate::{current_nanos, rands::StdRand};
#[pyclass(unsendable, name = "StdRand")]
#[allow(clippy::unsafe_derive_deserialize)]
#[derive(Serialize, Deserialize, Debug, Clone)]
/// Python class for StdRand
pub struct PythonStdRand {
/// Rust wrapped StdRand object
pub inner: StdRand,
}
#[pymethods]
impl PythonStdRand {
#[staticmethod]
fn with_current_nanos() -> Self {
Self {
inner: StdRand::with_seed(current_nanos()),
}
}
#[staticmethod]
fn with_seed(seed: u64) -> Self {
Self {
inner: StdRand::with_seed(seed),
}
}
fn as_rand(slf: Py<Self>) -> PythonRand {
PythonRand::new_std(slf)
}
}
#[derive(Serialize, Deserialize, Debug, Clone)]
enum PythonRandWrapper {
Std(Py<PythonStdRand>),
}
/// Rand Trait binding
#[pyclass(unsendable, name = "Rand")]
#[allow(clippy::unsafe_derive_deserialize)]
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct PythonRand {
wrapper: PythonRandWrapper,
}
macro_rules! unwrap_me_mut {
($wrapper:expr, $name:ident, $body:block) => {
crate::unwrap_me_mut_body!($wrapper, $name, $body, PythonRandWrapper, { Std })
};
}
#[pymethods]
impl PythonRand {
#[staticmethod]
fn new_std(py_std_rand: Py<PythonStdRand>) -> Self {
Self {
wrapper: PythonRandWrapper::Std(py_std_rand),
}
}
}
impl Rand for PythonRand {
fn set_seed(&mut self, seed: u64) {
unwrap_me_mut!(self.wrapper, r, { r.set_seed(seed) });
}
#[inline]
fn next(&mut self) -> u64 {
unwrap_me_mut!(self.wrapper, r, { r.next() })
}
}
/// Register the classes to the python module
pub fn register(_py: Python, m: &PyModule) -> PyResult<()> {
m.add_class::<PythonStdRand>()?;
m.add_class::<PythonRand>()?;
Ok(())
}
}

View File

@ -12,7 +12,7 @@ edition = "2021"
categories = ["development-tools::testing", "emulators", "embedded", "os", "no-std"]
[package.metadata.docs.rs]
features = ["document-features", "default", "x86_64", "usermode"]
features = ["document-features", "default", "python", "x86_64", "usermode"]
rustdoc-args = ["--cfg", "docsrs"]
[features]
@ -24,6 +24,8 @@ document-features = ["dep:document-features"]
#! ### General Features
## Find injections during fuzzing
injections = ["serde_yaml", "toml"]
## Python bindings support
python = ["pyo3", "pyo3-build-config", "libafl_qemu_sys/python"]
## Fork support
fork = ["libafl/fork"]
## Build libqasan for address sanitization
@ -85,10 +87,12 @@ paste = "1"
enum-map = "2.7"
serde_yaml = { version = "0.8", optional = true } # For parsing the injections yaml file
toml = { version = "0.4.2", optional = true } # For parsing the injections toml file
pyo3 = { version = "0.18", optional = true }
# Document all features of this crate (for `cargo doc`)
document-features = { version = "0.2", optional = true }
[build-dependencies]
pyo3-build-config = { version = "0.18", optional = true }
rustversion = "1.0"
bindgen = "0.69"

View File

@ -30,6 +30,8 @@ be = []
usermode = []
systemmode = []
python = ["pyo3", "pyo3-build-config"]
slirp = [ "systemmode", "libafl_qemu_build/slirp" ] # build qemu with host libslirp (for user networking)
shared = [ "libafl_qemu_build/shared" ]
@ -41,6 +43,8 @@ num_enum = "0.7"
libc = "0.2"
strum = "0.25"
strum_macros = "0.25"
pyo3 = { version = "0.18", optional = true }
[build-dependencies]
libafl_qemu_build = { path = "../libafl_qemu_build", version = "0.11.2" }
pyo3-build-config = { version = "0.18", optional = true }

View File

@ -121,6 +121,7 @@ pub type GuestVirtAddr = crate::vaddr;
pub type GuestHwAddrInfo = crate::qemu_plugin_hwaddr;
#[repr(C)]
#[cfg_attr(feature = "python", pyclass(unsendable))]
pub struct MapInfo {
start: GuestAddr,
end: GuestAddr,
@ -207,6 +208,7 @@ extern_c_checked! {
pub fn libafl_qemu_gdb_reply(buf: *const u8, len: usize);
}
#[cfg_attr(feature = "python", pymethods)]
impl MapInfo {
#[must_use]
pub fn start(&self) -> GuestAddr {
@ -282,3 +284,11 @@ impl MmapPerms {
)
}
}
#[cfg(feature = "python")]
impl IntoPy<PyObject> for MmapPerms {
fn into_py(self, py: Python) -> PyObject {
let n: i32 = self.into();
n.into_py(py)
}
}

View File

@ -3,6 +3,8 @@ use std::sync::OnceLock;
use capstone::arch::BuildsCapstone;
use enum_map::{enum_map, EnumMap};
use num_enum::{IntoPrimitive, TryFromPrimitive};
#[cfg(feature = "python")]
use pyo3::prelude::*;
pub use strum_macros::EnumIter;
pub use syscall_numbers::aarch64::*;
@ -71,6 +73,14 @@ impl Regs {
pub const Lr: Regs = Regs::X30;
}
#[cfg(feature = "python")]
impl IntoPy<PyObject> for Regs {
fn into_py(self, py: Python) -> PyObject {
let n: i32 = self.into();
n.into_py(py)
}
}
/// Return an ARM64 ArchCapstoneBuilder
pub fn capstone() -> capstone::arch::arm64::ArchCapstoneBuilder {
capstone::Capstone::new()

View File

@ -3,6 +3,8 @@ use std::sync::OnceLock;
use capstone::arch::BuildsCapstone;
use enum_map::{enum_map, EnumMap};
use num_enum::{IntoPrimitive, TryFromPrimitive};
#[cfg(feature = "python")]
use pyo3::prelude::*;
pub use strum_macros::EnumIter;
pub use syscall_numbers::arm::*;
@ -61,6 +63,14 @@ impl Regs {
pub const Cpsr: Regs = Regs::R25;
}
#[cfg(feature = "python")]
impl IntoPy<PyObject> for Regs {
fn into_py(self, py: Python) -> PyObject {
let n: i32 = self.into();
n.into_py(py)
}
}
/// Return an ARM ArchCapstoneBuilder
pub fn capstone() -> capstone::arch::arm::ArchCapstoneBuilder {
capstone::Capstone::new()

View File

@ -352,6 +352,9 @@ impl From<libafl_qemu_sys::MemOpIdx> for MemAccessInfo {
}
}
#[cfg(feature = "python")]
use pyo3::prelude::*;
pub const SKIP_EXEC_HOOK: u64 = u64::MAX;
pub use libafl_qemu_sys::{CPUArchState, CPUState};
@ -360,11 +363,33 @@ use crate::sync_backdoor::{SyncBackdoor, SyncBackdoorError};
// syshook_ret
#[repr(C)]
#[cfg_attr(feature = "python", pyclass)]
#[cfg_attr(feature = "python", derive(FromPyObject))]
pub struct SyscallHookResult {
pub retval: GuestAddr,
pub skip_syscall: bool,
}
#[cfg(feature = "python")]
#[pymethods]
impl SyscallHookResult {
#[new]
#[must_use]
pub fn new(value: Option<GuestAddr>) -> Self {
value.map_or(
Self {
retval: 0,
skip_syscall: false,
},
|v| Self {
retval: v,
skip_syscall: true,
},
)
}
}
#[cfg(not(feature = "python"))]
impl SyscallHookResult {
#[must_use]
pub fn new(value: Option<GuestAddr>) -> Self {
@ -625,25 +650,25 @@ pub struct HookData(u64);
impl<T> From<Pin<&mut T>> for HookData {
fn from(value: Pin<&mut T>) -> Self {
unsafe { HookData(transmute::<Pin<&mut T>, u64>(value)) }
unsafe { HookData(core::mem::transmute(value)) }
}
}
impl<T> From<Pin<&T>> for HookData {
fn from(value: Pin<&T>) -> Self {
unsafe { HookData(transmute::<Pin<&T>, u64>(value)) }
unsafe { HookData(core::mem::transmute(value)) }
}
}
impl<T> From<&'static mut T> for HookData {
fn from(value: &'static mut T) -> Self {
unsafe { HookData(transmute::<&mut T, u64>(value)) }
unsafe { HookData(core::mem::transmute(value)) }
}
}
impl<T> From<&'static T> for HookData {
fn from(value: &'static T) -> Self {
unsafe { HookData(transmute::<&T, u64>(value)) }
unsafe { HookData(core::mem::transmute(value)) }
}
}
@ -903,7 +928,7 @@ impl Qemu {
}
}
}
#[allow(clippy::missing_transmute_annotations)]
fn post_run(&self) -> Result<QemuExitReason, QemuExitReasonError> {
let exit_reason = unsafe { libafl_get_exit_reason() };
if exit_reason.is_null() {
@ -1718,3 +1743,190 @@ where
// self.qemu.write_function_argument(conv, idx, val)
// }
// }
#[cfg(feature = "python")]
pub mod pybind {
use pyo3::{exceptions::PyValueError, prelude::*, types::PyInt};
use super::{GuestAddr, GuestUsize, MmapPerms, SyscallHookResult};
static mut PY_SYSCALL_HOOK: Option<PyObject> = None;
static mut PY_GENERIC_HOOKS: Vec<(GuestAddr, PyObject)> = vec![];
extern "C" fn py_syscall_hook_wrapper(
_data: u64,
sys_num: i32,
a0: u64,
a1: u64,
a2: u64,
a3: u64,
a4: u64,
a5: u64,
a6: u64,
a7: u64,
) -> SyscallHookResult {
unsafe { PY_SYSCALL_HOOK.as_ref() }.map_or_else(
|| SyscallHookResult::new(None),
|obj| {
let args = (sys_num, a0, a1, a2, a3, a4, a5, a6, a7);
Python::with_gil(|py| {
let ret = obj.call1(py, args).expect("Error in the syscall hook");
let any = ret.as_ref(py);
if any.is_none() {
SyscallHookResult::new(None)
} else {
let a: Result<&PyInt, _> = any.downcast();
if let Ok(i) = a {
SyscallHookResult::new(Some(
i.extract().expect("Invalid syscall hook return value"),
))
} else {
SyscallHookResult::extract(any)
.expect("The syscall hook must return a SyscallHookResult")
}
}
})
},
)
}
extern "C" fn py_generic_hook_wrapper(idx: u64, _pc: GuestAddr) {
let obj = unsafe { &PY_GENERIC_HOOKS[idx as usize].1 };
Python::with_gil(|py| {
obj.call0(py).expect("Error in the hook");
});
}
#[pyclass(unsendable)]
pub struct Qemu {
pub qemu: super::Qemu,
}
#[pymethods]
impl Qemu {
#[allow(clippy::needless_pass_by_value)]
#[new]
fn new(args: Vec<String>, env: Vec<(String, String)>) -> PyResult<Qemu> {
let qemu = super::Qemu::init(&args, &env)
.map_err(|e| PyValueError::new_err(format!("{e}")))?;
Ok(Qemu { qemu })
}
fn write_mem(&self, addr: GuestAddr, buf: &[u8]) {
unsafe {
self.qemu.write_mem(addr, buf);
}
}
fn read_mem(&self, addr: GuestAddr, size: usize) -> Vec<u8> {
let mut buf = vec![0; size];
unsafe {
self.qemu.read_mem(addr, &mut buf);
}
buf
}
fn num_regs(&self) -> i32 {
self.qemu.num_regs()
}
fn write_reg(&self, reg: i32, val: GuestUsize) -> PyResult<()> {
self.qemu.write_reg(reg, val).map_err(PyValueError::new_err)
}
fn read_reg(&self, reg: i32) -> PyResult<GuestUsize> {
self.qemu.read_reg(reg).map_err(PyValueError::new_err)
}
fn set_breakpoint(&self, addr: GuestAddr) {
self.qemu.set_breakpoint(addr);
}
fn entry_break(&self, addr: GuestAddr) {
self.qemu.entry_break(addr);
}
fn remove_breakpoint(&self, addr: GuestAddr) {
self.qemu.remove_breakpoint(addr);
}
fn g2h(&self, addr: GuestAddr) -> u64 {
self.qemu.g2h::<*const u8>(addr) as u64
}
fn h2g(&self, addr: u64) -> GuestAddr {
self.qemu.h2g(addr as *const u8)
}
fn binary_path(&self) -> String {
self.qemu.binary_path().to_owned()
}
fn load_addr(&self) -> GuestAddr {
self.qemu.load_addr()
}
fn flush_jit(&self) {
self.qemu.flush_jit();
}
fn map_private(&self, addr: GuestAddr, size: usize, perms: i32) -> PyResult<GuestAddr> {
if let Ok(p) = MmapPerms::try_from(perms) {
self.qemu
.map_private(addr, size, p)
.map_err(PyValueError::new_err)
} else {
Err(PyValueError::new_err("Invalid perms"))
}
}
fn map_fixed(&self, addr: GuestAddr, size: usize, perms: i32) -> PyResult<GuestAddr> {
if let Ok(p) = MmapPerms::try_from(perms) {
self.qemu
.map_fixed(addr, size, p)
.map_err(PyValueError::new_err)
} else {
Err(PyValueError::new_err("Invalid perms"))
}
}
fn mprotect(&self, addr: GuestAddr, size: usize, perms: i32) -> PyResult<()> {
if let Ok(p) = MmapPerms::try_from(perms) {
self.qemu
.mprotect(addr, size, p)
.map_err(PyValueError::new_err)
} else {
Err(PyValueError::new_err("Invalid perms"))
}
}
fn unmap(&self, addr: GuestAddr, size: usize) -> PyResult<()> {
self.qemu.unmap(addr, size).map_err(PyValueError::new_err)
}
fn set_syscall_hook(&self, hook: PyObject) {
unsafe {
PY_SYSCALL_HOOK = Some(hook);
}
self.qemu
.add_pre_syscall_hook(0u64, py_syscall_hook_wrapper);
}
fn set_hook(&self, addr: GuestAddr, hook: PyObject) {
unsafe {
let idx = PY_GENERIC_HOOKS.len();
PY_GENERIC_HOOKS.push((addr, hook));
self.qemu
.set_hook(idx as u64, addr, py_generic_hook_wrapper, true);
}
}
fn remove_hooks_at(&self, addr: GuestAddr) -> usize {
unsafe {
PY_GENERIC_HOOKS.retain(|(a, _)| *a != addr);
}
self.qemu.remove_hooks_at(addr, true)
}
}
}

View File

@ -3,11 +3,12 @@ use std::{cell::OnceCell, slice::from_raw_parts, str::from_utf8_unchecked};
use libafl_qemu_sys::{
exec_path, free_self_maps, guest_base, libafl_dump_core_hook, libafl_force_dfl, libafl_get_brk,
libafl_load_addr, libafl_maps_first, libafl_maps_next, libafl_qemu_run, libafl_set_brk,
mmap_next_start, read_self_maps, strlen, GuestAddr, GuestUsize, MapInfo, MmapPerms,
VerifyAccess,
libafl_load_addr, libafl_maps_next, libafl_qemu_run, libafl_set_brk, mmap_next_start,
read_self_maps, strlen, GuestAddr, GuestUsize, MapInfo, MmapPerms, VerifyAccess,
};
use libc::c_int;
#[cfg(feature = "python")]
use pyo3::prelude::*;
use crate::{
emu::{HasExecutions, State},
@ -23,6 +24,7 @@ pub enum HandlerError {
MultipleInputDefinition,
}
#[cfg_attr(feature = "python", pyclass(unsendable))]
pub struct GuestMaps {
maps_root: *const c_void,
maps_node: *const c_void,
@ -59,6 +61,17 @@ impl Iterator for GuestMaps {
}
}
#[cfg(feature = "python")]
#[pymethods]
impl GuestMaps {
fn __iter__(slf: PyRef<Self>) -> PyRef<Self> {
slf
}
fn __next__(mut slf: PyRefMut<Self>) -> Option<PyObject> {
Python::with_gil(|py| slf.next().map(|x| x.into_py(py)))
}
}
impl Drop for GuestMaps {
fn drop(&mut self) {
unsafe {

View File

@ -2,6 +2,8 @@ use std::sync::OnceLock;
use enum_map::{enum_map, EnumMap};
use num_enum::{IntoPrimitive, TryFromPrimitive};
#[cfg(feature = "python")]
use pyo3::prelude::*;
pub use strum_macros::EnumIter;
use crate::{sync_backdoor::BackdoorArgs, CallingConvention};

View File

@ -3,6 +3,8 @@ use std::{mem::size_of, sync::OnceLock};
use capstone::arch::BuildsCapstone;
use enum_map::{enum_map, EnumMap};
use num_enum::{IntoPrimitive, TryFromPrimitive};
#[cfg(feature = "python")]
use pyo3::prelude::*;
pub use strum_macros::EnumIter;
pub use syscall_numbers::x86::*;
@ -47,6 +49,14 @@ impl Regs {
pub const Pc: Regs = Regs::Eip;
}
#[cfg(feature = "python")]
impl IntoPy<PyObject> for Regs {
fn into_py(self, py: Python) -> PyObject {
let n: i32 = self.into();
n.into_py(py)
}
}
/// Return an X86 ArchCapstoneBuilder
pub fn capstone() -> capstone::arch::x86::ArchCapstoneBuilder {
capstone::Capstone::new()

View File

@ -131,3 +131,33 @@ pub fn filter_qemu_args() -> Vec<String> {
}
args
}
#[cfg(feature = "python")]
use pyo3::prelude::*;
#[cfg(feature = "python")]
#[pymodule]
#[pyo3(name = "libafl_qemu")]
#[allow(clippy::items_after_statements, clippy::too_many_lines)]
pub fn python_module(py: Python, m: &PyModule) -> PyResult<()> {
let regsm = PyModule::new(py, "regs")?;
for r in Regs::iter() {
let v: i32 = r.into();
regsm.add(&format!("{r:?}"), v)?;
}
m.add_submodule(regsm)?;
let mmapm = PyModule::new(py, "mmap")?;
for r in emu::MmapPerms::iter() {
let v: i32 = r.into();
mmapm.add(&format!("{r:?}"), v)?;
}
m.add_submodule(mmapm)?;
m.add_class::<emu::MapInfo>()?;
m.add_class::<emu::GuestMaps>()?;
m.add_class::<emu::SyscallHookResult>()?;
m.add_class::<emu::pybind::Qemu>()?;
Ok(())
}

View File

@ -2,6 +2,8 @@ use std::sync::OnceLock;
use enum_map::{enum_map, EnumMap};
use num_enum::{IntoPrimitive, TryFromPrimitive};
#[cfg(feature = "python")]
use pyo3::prelude::*;
pub use strum_macros::EnumIter;
pub use syscall_numbers::mips::*;
@ -70,6 +72,14 @@ impl Regs {
pub const Zero: Regs = Regs::R0;
}
#[cfg(feature = "python")]
impl IntoPy<PyObject> for Regs {
fn into_py(self, py: Python) -> PyObject {
let n: i32 = self.into();
n.into_py(py)
}
}
/// Return an MIPS ArchCapstoneBuilder
pub fn capstone() -> capstone::arch::mips::ArchCapstoneBuilder {
capstone::Capstone::new().mips()

View File

@ -2,6 +2,8 @@ use std::sync::OnceLock;
use enum_map::{enum_map, EnumMap};
use num_enum::{IntoPrimitive, TryFromPrimitive};
#[cfg(feature = "python")]
use pyo3::prelude::*;
pub use strum_macros::EnumIter;
pub use syscall_numbers::powerpc::*;
@ -110,6 +112,14 @@ impl Regs {
pub const Sp: Regs = Regs::R1;
}
#[cfg(feature = "python")]
impl IntoPy<PyObject> for Regs {
fn into_py(self, py: Python) -> PyObject {
let n: i32 = self.into();
n.into_py(py)
}
}
/// Return an MIPS ArchCapstoneBuilder
pub fn capstone() -> capstone::arch::ppc::ArchCapstoneBuilder {
capstone::Capstone::new().ppc()

View File

@ -3,6 +3,8 @@ use std::{mem::size_of, sync::OnceLock};
use capstone::arch::BuildsCapstone;
use enum_map::{enum_map, EnumMap};
use num_enum::{IntoPrimitive, TryFromPrimitive};
#[cfg(feature = "python")]
use pyo3::prelude::*;
pub use strum_macros::EnumIter;
pub use syscall_numbers::x86_64::*;
@ -55,6 +57,14 @@ impl Regs {
pub const Pc: Regs = Regs::Rip;
}
#[cfg(feature = "python")]
impl IntoPy<PyObject> for Regs {
fn into_py(self, py: Python) -> PyObject {
let n: i32 = self.into();
n.into_py(py)
}
}
/// Return an X86 `ArchCapstoneBuilder`
#[must_use]
pub fn capstone() -> capstone::arch::x86::ArchCapstoneBuilder {

View File

@ -9,12 +9,14 @@ readme = "../README.md"
license = "MIT OR Apache-2.0"
keywords = ["fuzzing"]
edition = "2021"
build = "build.rs"
categories = ["development-tools::testing", "emulators", "embedded", "os", "no-std"]
[package.metadata.docs.rs]
all-features = true
[features]
python = ["pyo3", "libafl_qemu/python", "pyo3-build-config"]
default = []
# for libafl_qemu
@ -27,12 +29,16 @@ mips = ["libafl_qemu/mips"] # build qemu for mips (el, use with the 'be' feature
ppc = ["libafl_qemu/ppc"] # build qemu for powerpc
hexagon = ["libafl_qemu/hexagon"] # build qemu for hexagon
[build-dependencies]
pyo3-build-config = { version = "0.18", optional = true }
[dependencies]
libafl = { path = "../libafl", version = "0.11.2" }
libafl_bolts = { path = "../libafl_bolts", version = "0.11.2" }
libafl_targets = { path = "../libafl_targets", version = "0.11.2" }
typed-builder = "0.16" # Implement the builder pattern at compiletime
pyo3 = { version = "0.18", optional = true }
log = "0.4.20"
[target.'cfg(target_os = "linux")'.dependencies]

4
libafl_sugar/build.rs Normal file
View File

@ -0,0 +1,4 @@
fn main() {
#[cfg(feature = "python")]
pyo3_build_config::add_extension_module_link_args();
}

View File

@ -299,3 +299,80 @@ impl<'a> ForkserverBytesCoverageSugar<'a> {
}
}
}
/// The python bindings for this sugar
#[cfg(feature = "python")]
pub mod pybind {
use std::path::PathBuf;
use libafl_bolts::core_affinity::Cores;
use pyo3::prelude::*;
use crate::forkserver;
/// Python bindings for the `LibAFL` forkserver sugar
#[pyclass(unsendable)]
#[derive(Debug)]
struct ForkserverBytesCoverageSugar {
input_dirs: Vec<PathBuf>,
output_dir: PathBuf,
broker_port: u16,
cores: Cores,
use_cmplog: Option<bool>,
iterations: Option<u64>,
tokens_file: Option<PathBuf>,
timeout: Option<u64>,
}
#[pymethods]
impl ForkserverBytesCoverageSugar {
/// Create a new [`ForkserverBytesCoverageSugar`]
#[new]
#[allow(clippy::too_many_arguments)]
fn new(
input_dirs: Vec<PathBuf>,
output_dir: PathBuf,
broker_port: u16,
cores: Vec<usize>,
use_cmplog: Option<bool>,
iterations: Option<u64>,
tokens_file: Option<PathBuf>,
timeout: Option<u64>,
) -> Self {
Self {
input_dirs,
output_dir,
broker_port,
cores: cores.into(),
use_cmplog,
iterations,
tokens_file,
timeout,
}
}
/// Run the fuzzer
#[allow(clippy::needless_pass_by_value)]
pub fn run(&self, program: String, arguments: Vec<String>) {
forkserver::ForkserverBytesCoverageSugar::builder()
.input_dirs(&self.input_dirs)
.output_dir(self.output_dir.clone())
.broker_port(self.broker_port)
.cores(&self.cores)
.program(program)
.arguments(&arguments)
.use_cmplog(self.use_cmplog)
.timeout(self.timeout)
.tokens_file(self.tokens_file.clone())
.iterations(self.iterations)
.build()
.run();
}
}
/// Register the module
pub fn register(_py: Python, m: &PyModule) -> PyResult<()> {
m.add_class::<ForkserverBytesCoverageSugar>()?;
Ok(())
}
}

View File

@ -348,3 +348,87 @@ where
}
}
}
/// Python bindings for this sugar
#[cfg(feature = "python")]
pub mod pybind {
use std::path::PathBuf;
use libafl_bolts::core_affinity::Cores;
use pyo3::{prelude::*, types::PyBytes};
use crate::inmemory;
/// In-Memory fuzzing made easy.
/// Use this sugar for scaling `libfuzzer`-style fuzzers.
#[pyclass(unsendable)]
#[derive(Debug)]
struct InMemoryBytesCoverageSugar {
input_dirs: Vec<PathBuf>,
output_dir: PathBuf,
broker_port: u16,
cores: Cores,
use_cmplog: Option<bool>,
iterations: Option<u64>,
tokens_file: Option<PathBuf>,
timeout: Option<u64>,
}
#[pymethods]
impl InMemoryBytesCoverageSugar {
/// Create a new [`InMemoryBytesCoverageSugar`]
#[new]
#[allow(clippy::too_many_arguments)]
fn new(
input_dirs: Vec<PathBuf>,
output_dir: PathBuf,
broker_port: u16,
cores: Vec<usize>,
use_cmplog: Option<bool>,
iterations: Option<u64>,
tokens_file: Option<PathBuf>,
timeout: Option<u64>,
) -> Self {
Self {
input_dirs,
output_dir,
broker_port,
cores: cores.into(),
use_cmplog,
iterations,
tokens_file,
timeout,
}
}
/// Run the fuzzer
#[allow(clippy::needless_pass_by_value)]
pub fn run(&self, harness: PyObject) {
inmemory::InMemoryBytesCoverageSugar::builder()
.input_dirs(&self.input_dirs)
.output_dir(self.output_dir.clone())
.broker_port(self.broker_port)
.cores(&self.cores)
.harness(|buf| {
Python::with_gil(|py| -> PyResult<()> {
let args = (PyBytes::new(py, buf),); // TODO avoid copy
harness.call1(py, args)?;
Ok(())
})
.unwrap();
})
.use_cmplog(self.use_cmplog)
.timeout(self.timeout)
.tokens_file(self.tokens_file.clone())
.iterations(self.iterations)
.build()
.run();
}
}
/// Register the module
pub fn register(_py: Python, m: &PyModule) -> PyResult<()> {
m.add_class::<InMemoryBytesCoverageSugar>()?;
Ok(())
}
}

View File

@ -78,3 +78,23 @@ pub const DEFAULT_TIMEOUT_SECS: u64 = 1200;
/// Default cache size for the corpus in memory.
/// Anything else will be on disk.
pub const CORPUS_CACHE_SIZE: usize = 4096;
#[cfg(feature = "python")]
use pyo3::prelude::*;
/// The sugar python module
#[cfg(feature = "python")]
#[pymodule]
#[pyo3(name = "libafl_sugar")]
pub fn python_module(py: Python, m: &PyModule) -> PyResult<()> {
inmemory::pybind::register(py, m)?;
#[cfg(target_os = "linux")]
{
qemu::pybind::register(py, m)?;
}
#[cfg(unix)]
{
forkserver::pybind::register(py, m)?;
}
Ok(())
}

View File

@ -437,3 +437,86 @@ where
launcher.build().launch().expect("Launcher failed");
}
}
/// python bindings for this sugar
#[cfg(feature = "python")]
pub mod pybind {
use std::path::PathBuf;
use libafl_bolts::core_affinity::Cores;
use libafl_qemu::emu::pybind::Qemu;
use pyo3::{prelude::*, types::PyBytes};
use crate::qemu;
#[pyclass(unsendable)]
#[derive(Debug)]
struct QemuBytesCoverageSugar {
input_dirs: Vec<PathBuf>,
output_dir: PathBuf,
broker_port: u16,
cores: Cores,
use_cmplog: Option<bool>,
iterations: Option<u64>,
tokens_file: Option<PathBuf>,
timeout: Option<u64>,
}
#[pymethods]
impl QemuBytesCoverageSugar {
/// Create a new [`QemuBytesCoverageSugar`]
#[new]
#[allow(clippy::too_many_arguments)]
fn new(
input_dirs: Vec<PathBuf>,
output_dir: PathBuf,
broker_port: u16,
cores: Vec<usize>,
use_cmplog: Option<bool>,
iterations: Option<u64>,
tokens_file: Option<PathBuf>,
timeout: Option<u64>,
) -> Self {
Self {
input_dirs,
output_dir,
broker_port,
cores: cores.into(),
use_cmplog,
iterations,
tokens_file,
timeout,
}
}
/// Run the fuzzer
#[allow(clippy::needless_pass_by_value)]
pub fn run(&self, qemu: &Qemu, harness: PyObject) {
qemu::QemuBytesCoverageSugar::builder()
.input_dirs(&self.input_dirs)
.output_dir(self.output_dir.clone())
.broker_port(self.broker_port)
.cores(&self.cores)
.harness(|buf| {
Python::with_gil(|py| -> PyResult<()> {
let args = (PyBytes::new(py, buf),); // TODO avoid copy
harness.call1(py, args)?;
Ok(())
})
.unwrap();
})
.use_cmplog(self.use_cmplog)
.timeout(self.timeout)
.tokens_file(self.tokens_file.clone())
.iterations(self.iterations)
.build()
.run(&qemu.qemu);
}
}
/// Register this class
pub fn register(_py: Python, m: &PyModule) -> PyResult<()> {
m.add_class::<QemuBytesCoverageSugar>()?;
Ok(())
}
}