From f19302c9b11c6c42a7802766a73fd05a841211ee Mon Sep 17 00:00:00 2001 From: Dominik Maier Date: Mon, 8 Apr 2024 19:36:54 +0200 Subject: [PATCH] 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? --- .github/workflows/build_and_test.yml | 169 +++++++++++-------- bindings/pylibafl/.gitignore | 1 + bindings/pylibafl/Cargo.toml | 23 +++ bindings/pylibafl/README.md | 31 ++++ bindings/pylibafl/pyproject.toml | 26 +++ bindings/pylibafl/src/lib.rs | 33 ++++ bindings/pylibafl/test.py | 7 + bindings/pylibafl/test.sh | 14 ++ fuzzers/python_qemu/fuzz.c | 14 ++ fuzzers/python_qemu/fuzzer.py | 43 +++++ libafl_bolts/Cargo.toml | 5 + libafl_bolts/src/lib.rs | 158 ++++++++++++++++++ libafl_bolts/src/rands.rs | 89 ++++++++++ libafl_qemu/Cargo.toml | 6 +- libafl_qemu/libafl_qemu_sys/Cargo.toml | 4 + libafl_qemu/libafl_qemu_sys/src/lib.rs | 10 ++ libafl_qemu/src/aarch64.rs | 10 ++ libafl_qemu/src/arm.rs | 10 ++ libafl_qemu/src/emu.rs | 222 ++++++++++++++++++++++++- libafl_qemu/src/emu/usermode.rs | 19 ++- libafl_qemu/src/hexagon.rs | 2 + libafl_qemu/src/i386.rs | 10 ++ libafl_qemu/src/lib.rs | 30 ++++ libafl_qemu/src/mips.rs | 10 ++ libafl_qemu/src/ppc.rs | 10 ++ libafl_qemu/src/x86_64.rs | 10 ++ libafl_sugar/Cargo.toml | 6 + libafl_sugar/build.rs | 4 + libafl_sugar/src/forkserver.rs | 77 +++++++++ libafl_sugar/src/inmemory.rs | 84 ++++++++++ libafl_sugar/src/lib.rs | 20 +++ libafl_sugar/src/qemu.rs | 83 +++++++++ 32 files changed, 1159 insertions(+), 81 deletions(-) create mode 100644 bindings/pylibafl/.gitignore create mode 100644 bindings/pylibafl/Cargo.toml create mode 100644 bindings/pylibafl/README.md create mode 100644 bindings/pylibafl/pyproject.toml create mode 100644 bindings/pylibafl/src/lib.rs create mode 100644 bindings/pylibafl/test.py create mode 100755 bindings/pylibafl/test.sh create mode 100644 fuzzers/python_qemu/fuzz.c create mode 100644 fuzzers/python_qemu/fuzzer.py create mode 100644 libafl_sugar/build.rs diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index 544a777d4b..0ac0fffd99 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -280,79 +280,104 @@ jobs: - name: Run smoke test run: ./libafl_concolic/test/smoke_test.sh - fuzzers: - strategy: - matrix: - os: [ubuntu-latest] - fuzzer: - - ./fuzzers/fuzzbench_fork_qemu - - ./fuzzers/libfuzzer_stb_image_sugar - - ./fuzzers/nyx_libxml2_standalone - - ./fuzzers/baby_fuzzer_gramatron - - ./fuzzers/tinyinst_simple - - ./fuzzers/baby_fuzzer_with_forkexecutor - - ./fuzzers/baby_no_std - - ./fuzzers/baby_fuzzer_swap_differential - - ./fuzzers/baby_fuzzer_grimoire - - ./fuzzers/baby_fuzzer - - ./fuzzers/libfuzzer_libpng_launcher - - ./fuzzers/libfuzzer_libpng_accounting - - ./fuzzers/forkserver_libafl_cc - - ./fuzzers/libfuzzer_libpng_tcp_manager - - ./fuzzers/backtrace_baby_fuzzers - - ./fuzzers/fuzzbench_qemu - - ./fuzzers/nyx_libxml2_parallel - - ./fuzzers/qemu_launcher - - ./fuzzers/frida_gdiplus - - ./fuzzers/libfuzzer_stb_image_concolic - - ./fuzzers/nautilus_sync - # - ./fuzzers/qemu_cmin - # - ./fuzzers/qemu_systemmode - - ./fuzzers/push_harness - - ./fuzzers/libfuzzer_libpng_centralized - - ./fuzzers/baby_fuzzer_nautilus - - ./fuzzers/fuzzbench_text - - ./fuzzers/libfuzzer_libpng_cmin - - ./fuzzers/forkserver_simple - - ./fuzzers/baby_fuzzer_unicode - - ./fuzzers/libfuzzer_libpng_norestart - - ./fuzzers/baby_fuzzer_multi - - ./fuzzers/libafl_atheris - - ./fuzzers/frida_libpng - - ./fuzzers/fuzzbench_ctx - - ./fuzzers/fuzzbench_forkserver_cmplog - - ./fuzzers/push_stage_harness - - ./fuzzers/libfuzzer_libmozjpeg - - ./fuzzers/libfuzzer_libpng_aflpp_ui - - ./fuzzers/libfuzzer_libpng - - ./fuzzers/baby_fuzzer_wasm - - ./fuzzers/fuzzbench - - ./fuzzers/libfuzzer_stb_image - - ./fuzzers/fuzzbench_forkserver - - ./fuzzers/libfuzzer_windows_asan - - ./fuzzers/baby_fuzzer_minimizing - # - ./fuzzers/qemu_coverage - - ./fuzzers/frida_executable_libpng - - ./fuzzers/tutorial - - ./fuzzers/baby_fuzzer_tokens - - ./fuzzers/backtrace_baby_fuzzers/rust_code_with_inprocess_executor - - ./fuzzers/backtrace_baby_fuzzers/c_code_with_fork_executor - - ./fuzzers/backtrace_baby_fuzzers/command_executor - - ./fuzzers/backtrace_baby_fuzzers/forkserver_executor - - ./fuzzers/backtrace_baby_fuzzers/c_code_with_inprocess_executor - - ./fuzzers/backtrace_baby_fuzzers/rust_code_with_fork_executor - runs-on: ${{ matrix.os }} + python-bindings: + runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: ./.github/workflows/fuzzer-tester-prepare - - name: Symlink Headers - if: runner.os == 'Linux' - shell: bash - run: sudo ln -s /usr/include/asm-generic /usr/include/asm - - name: Build and run example fuzzers (Linux) - if: runner.os == 'Linux' - shell: bash - run: RUN_ON_CI=1 LLVM_CONFIG=llvm-config ./scripts/test_all_fuzzers.sh ${{ matrix.fuzzer }} + - 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] + fuzzer: + - ./fuzzers/fuzzbench_fork_qemu + - ./fuzzers/libfuzzer_stb_image_sugar + - ./fuzzers/nyx_libxml2_standalone + - ./fuzzers/baby_fuzzer_gramatron + - ./fuzzers/tinyinst_simple + - ./fuzzers/baby_fuzzer_with_forkexecutor + - ./fuzzers/baby_no_std + - ./fuzzers/baby_fuzzer_swap_differential + - ./fuzzers/baby_fuzzer_grimoire + - ./fuzzers/baby_fuzzer + - ./fuzzers/libfuzzer_libpng_launcher + - ./fuzzers/libfuzzer_libpng_accounting + - ./fuzzers/forkserver_libafl_cc + - ./fuzzers/libfuzzer_libpng_tcp_manager + - ./fuzzers/backtrace_baby_fuzzers + - ./fuzzers/fuzzbench_qemu + - ./fuzzers/nyx_libxml2_parallel + - ./fuzzers/qemu_launcher + - ./fuzzers/frida_gdiplus + - ./fuzzers/libfuzzer_stb_image_concolic + - ./fuzzers/nautilus_sync + # - ./fuzzers/qemu_cmin + # - ./fuzzers/qemu_systemmode + - ./fuzzers/push_harness + - ./fuzzers/libfuzzer_libpng_centralized + - ./fuzzers/baby_fuzzer_nautilus + - ./fuzzers/fuzzbench_text + - ./fuzzers/libfuzzer_libpng_cmin + - ./fuzzers/forkserver_simple + - ./fuzzers/baby_fuzzer_unicode + - ./fuzzers/libfuzzer_libpng_norestart + - ./fuzzers/baby_fuzzer_multi + - ./fuzzers/libafl_atheris + - ./fuzzers/frida_libpng + - ./fuzzers/fuzzbench_ctx + - ./fuzzers/fuzzbench_forkserver_cmplog + - ./fuzzers/push_stage_harness + - ./fuzzers/libfuzzer_libmozjpeg + - ./fuzzers/libfuzzer_libpng_aflpp_ui + - ./fuzzers/libfuzzer_libpng + - ./fuzzers/baby_fuzzer_wasm + - ./fuzzers/fuzzbench + - ./fuzzers/libfuzzer_stb_image + - ./fuzzers/fuzzbench_forkserver + - ./fuzzers/libfuzzer_windows_asan + - ./fuzzers/baby_fuzzer_minimizing + # - ./fuzzers/qemu_coverage + - ./fuzzers/frida_executable_libpng + - ./fuzzers/tutorial + - ./fuzzers/baby_fuzzer_tokens + - ./fuzzers/backtrace_baby_fuzzers/rust_code_with_inprocess_executor + - ./fuzzers/backtrace_baby_fuzzers/c_code_with_fork_executor + - ./fuzzers/backtrace_baby_fuzzers/command_executor + - ./fuzzers/backtrace_baby_fuzzers/forkserver_executor + - ./fuzzers/backtrace_baby_fuzzers/c_code_with_inprocess_executor + - ./fuzzers/backtrace_baby_fuzzers/rust_code_with_fork_executor + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v3 + - uses: ./.github/workflows/fuzzer-tester-prepare + - name: Symlink Headers + if: runner.os == 'Linux' + shell: bash + run: sudo ln -s /usr/include/asm-generic /usr/include/asm + - name: Build and run example fuzzers (Linux) + if: runner.os == 'Linux' + shell: bash + run: RUN_ON_CI=1 LLVM_CONFIG=llvm-config ./scripts/test_all_fuzzers.sh ${{ matrix.fuzzer }} nostd-build: runs-on: ubuntu-latest diff --git a/bindings/pylibafl/.gitignore b/bindings/pylibafl/.gitignore new file mode 100644 index 0000000000..849ddff3b7 --- /dev/null +++ b/bindings/pylibafl/.gitignore @@ -0,0 +1 @@ +dist/ diff --git a/bindings/pylibafl/Cargo.toml b/bindings/pylibafl/Cargo.toml new file mode 100644 index 0000000000..836761a441 --- /dev/null +++ b/bindings/pylibafl/Cargo.toml @@ -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" diff --git a/bindings/pylibafl/README.md b/bindings/pylibafl/README.md new file mode 100644 index 0000000000..3e3922251e --- /dev/null +++ b/bindings/pylibafl/README.md @@ -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. diff --git a/bindings/pylibafl/pyproject.toml b/bindings/pylibafl/pyproject.toml new file mode 100644 index 0000000000..7e62ce01e8 --- /dev/null +++ b/bindings/pylibafl/pyproject.toml @@ -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 diff --git a/bindings/pylibafl/src/lib.rs b/bindings/pylibafl/src/lib.rs new file mode 100644 index 0000000000..017dae5211 --- /dev/null +++ b/bindings/pylibafl/src/lib.rs @@ -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(()) +} diff --git a/bindings/pylibafl/test.py b/bindings/pylibafl/test.py new file mode 100644 index 0000000000..41d90c9e3e --- /dev/null +++ b/bindings/pylibafl/test.py @@ -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")) \ No newline at end of file diff --git a/bindings/pylibafl/test.sh b/bindings/pylibafl/test.sh new file mode 100755 index 0000000000..e02a03da8b --- /dev/null +++ b/bindings/pylibafl/test.sh @@ -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 + diff --git a/fuzzers/python_qemu/fuzz.c b/fuzzers/python_qemu/fuzz.c new file mode 100644 index 0000000000..2f4632db1b --- /dev/null +++ b/fuzzers/python_qemu/fuzz.c @@ -0,0 +1,14 @@ +#include +#include +#include + +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); +} diff --git a/fuzzers/python_qemu/fuzzer.py b/fuzzers/python_qemu/fuzzer.py new file mode 100644 index 0000000000..fd0245a497 --- /dev/null +++ b/fuzzers/python_qemu/fuzzer.py @@ -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) diff --git a/libafl_bolts/Cargo.toml b/libafl_bolts/Cargo.toml index 9be219fe9b..8e24ea3a8d 100644 --- a/libafl_bolts/Cargo.toml +++ b/libafl_bolts/Cargo.toml @@ -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"] } diff --git a/libafl_bolts/src/lib.rs b/libafl_bolts/src/lib.rs index 9501095a5d..26271a8af3 100644 --- a/libafl_bolts/src/lib.rs +++ b/libafl_bolts/src/lib.rs @@ -611,6 +611,22 @@ impl From for Error { } } +#[cfg(feature = "python")] +impl From for Error { + fn from(err: pyo3::PyErr) -> Self { + pyo3::Python::with_gil(|py| { + if err.matches( + py, + pyo3::types::PyType::new::(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(&self, serializer: S) -> Result + where + S: Serializer, + { + let buf = Python::with_gil(|py| -> PyResult> { + let pickle = PyModule::import(py, "pickle")?; + let buf: Vec = + 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(self, v: Vec) -> Result + where + E: serde::de::Error, + { + let obj = Python::with_gil(|py| -> PyResult { + 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(deserializer: D) -> Result + 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 { diff --git a/libafl_bolts/src/rands.rs b/libafl_bolts/src/rands.rs index e19143d0d3..430a4af0fc 100644 --- a/libafl_bolts/src/rands.rs +++ b/libafl_bolts/src/rands.rs @@ -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) -> PythonRand { + PythonRand::new_std(slf) + } + } + + #[derive(Serialize, Deserialize, Debug, Clone)] + enum PythonRandWrapper { + Std(Py), + } + + /// 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) -> 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::()?; + m.add_class::()?; + Ok(()) + } +} diff --git a/libafl_qemu/Cargo.toml b/libafl_qemu/Cargo.toml index b375b7d6d2..5f34422e9a 100644 --- a/libafl_qemu/Cargo.toml +++ b/libafl_qemu/Cargo.toml @@ -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" diff --git a/libafl_qemu/libafl_qemu_sys/Cargo.toml b/libafl_qemu/libafl_qemu_sys/Cargo.toml index c754688d20..1d1dd57bfb 100644 --- a/libafl_qemu/libafl_qemu_sys/Cargo.toml +++ b/libafl_qemu/libafl_qemu_sys/Cargo.toml @@ -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 } diff --git a/libafl_qemu/libafl_qemu_sys/src/lib.rs b/libafl_qemu/libafl_qemu_sys/src/lib.rs index f12b706a98..cf08702d68 100644 --- a/libafl_qemu/libafl_qemu_sys/src/lib.rs +++ b/libafl_qemu/libafl_qemu_sys/src/lib.rs @@ -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 for MmapPerms { + fn into_py(self, py: Python) -> PyObject { + let n: i32 = self.into(); + n.into_py(py) + } +} diff --git a/libafl_qemu/src/aarch64.rs b/libafl_qemu/src/aarch64.rs index d204303981..3793146d4a 100644 --- a/libafl_qemu/src/aarch64.rs +++ b/libafl_qemu/src/aarch64.rs @@ -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 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() diff --git a/libafl_qemu/src/arm.rs b/libafl_qemu/src/arm.rs index 4237af854f..fcdf9b1a06 100644 --- a/libafl_qemu/src/arm.rs +++ b/libafl_qemu/src/arm.rs @@ -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 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() diff --git a/libafl_qemu/src/emu.rs b/libafl_qemu/src/emu.rs index 3abc72b730..ec7a709818 100644 --- a/libafl_qemu/src/emu.rs +++ b/libafl_qemu/src/emu.rs @@ -352,6 +352,9 @@ impl From 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) -> 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) -> Self { @@ -625,25 +650,25 @@ pub struct HookData(u64); impl From> for HookData { fn from(value: Pin<&mut T>) -> Self { - unsafe { HookData(transmute::, u64>(value)) } + unsafe { HookData(core::mem::transmute(value)) } } } impl From> for HookData { fn from(value: Pin<&T>) -> Self { - unsafe { HookData(transmute::, u64>(value)) } + unsafe { HookData(core::mem::transmute(value)) } } } impl 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 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 { 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 = 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, env: Vec<(String, String)>) -> PyResult { + 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 { + 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 { + 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 { + 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 { + 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) + } + } +} diff --git a/libafl_qemu/src/emu/usermode.rs b/libafl_qemu/src/emu/usermode.rs index 51bf9673ed..bc82164408 100644 --- a/libafl_qemu/src/emu/usermode.rs +++ b/libafl_qemu/src/emu/usermode.rs @@ -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) -> PyRef { + slf + } + fn __next__(mut slf: PyRefMut) -> Option { + Python::with_gil(|py| slf.next().map(|x| x.into_py(py))) + } +} + impl Drop for GuestMaps { fn drop(&mut self) { unsafe { diff --git a/libafl_qemu/src/hexagon.rs b/libafl_qemu/src/hexagon.rs index f083295ce3..d931876034 100644 --- a/libafl_qemu/src/hexagon.rs +++ b/libafl_qemu/src/hexagon.rs @@ -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}; diff --git a/libafl_qemu/src/i386.rs b/libafl_qemu/src/i386.rs index a4144f74ed..d70578987a 100644 --- a/libafl_qemu/src/i386.rs +++ b/libafl_qemu/src/i386.rs @@ -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 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() diff --git a/libafl_qemu/src/lib.rs b/libafl_qemu/src/lib.rs index b42b35a954..3c21f3ae08 100644 --- a/libafl_qemu/src/lib.rs +++ b/libafl_qemu/src/lib.rs @@ -131,3 +131,33 @@ pub fn filter_qemu_args() -> Vec { } 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::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + + Ok(()) +} diff --git a/libafl_qemu/src/mips.rs b/libafl_qemu/src/mips.rs index 8619529fa4..96b464d9fd 100644 --- a/libafl_qemu/src/mips.rs +++ b/libafl_qemu/src/mips.rs @@ -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 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() diff --git a/libafl_qemu/src/ppc.rs b/libafl_qemu/src/ppc.rs index 5427bce59f..9fdd4def74 100644 --- a/libafl_qemu/src/ppc.rs +++ b/libafl_qemu/src/ppc.rs @@ -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 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() diff --git a/libafl_qemu/src/x86_64.rs b/libafl_qemu/src/x86_64.rs index bef59a1dd2..d6ac5aac0d 100644 --- a/libafl_qemu/src/x86_64.rs +++ b/libafl_qemu/src/x86_64.rs @@ -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 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 { diff --git a/libafl_sugar/Cargo.toml b/libafl_sugar/Cargo.toml index f4ac7d89ac..521af3ab22 100644 --- a/libafl_sugar/Cargo.toml +++ b/libafl_sugar/Cargo.toml @@ -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] diff --git a/libafl_sugar/build.rs b/libafl_sugar/build.rs new file mode 100644 index 0000000000..294fe34040 --- /dev/null +++ b/libafl_sugar/build.rs @@ -0,0 +1,4 @@ +fn main() { + #[cfg(feature = "python")] + pyo3_build_config::add_extension_module_link_args(); +} diff --git a/libafl_sugar/src/forkserver.rs b/libafl_sugar/src/forkserver.rs index 70c27c24cc..a9fe528959 100644 --- a/libafl_sugar/src/forkserver.rs +++ b/libafl_sugar/src/forkserver.rs @@ -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, + output_dir: PathBuf, + broker_port: u16, + cores: Cores, + use_cmplog: Option, + iterations: Option, + tokens_file: Option, + timeout: Option, + } + + #[pymethods] + impl ForkserverBytesCoverageSugar { + /// Create a new [`ForkserverBytesCoverageSugar`] + #[new] + #[allow(clippy::too_many_arguments)] + fn new( + input_dirs: Vec, + output_dir: PathBuf, + broker_port: u16, + cores: Vec, + use_cmplog: Option, + iterations: Option, + tokens_file: Option, + timeout: Option, + ) -> 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) { + 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::()?; + Ok(()) + } +} diff --git a/libafl_sugar/src/inmemory.rs b/libafl_sugar/src/inmemory.rs index 05bd7d5575..7a7fab47e7 100644 --- a/libafl_sugar/src/inmemory.rs +++ b/libafl_sugar/src/inmemory.rs @@ -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, + output_dir: PathBuf, + broker_port: u16, + cores: Cores, + use_cmplog: Option, + iterations: Option, + tokens_file: Option, + timeout: Option, + } + + #[pymethods] + impl InMemoryBytesCoverageSugar { + /// Create a new [`InMemoryBytesCoverageSugar`] + #[new] + #[allow(clippy::too_many_arguments)] + fn new( + input_dirs: Vec, + output_dir: PathBuf, + broker_port: u16, + cores: Vec, + use_cmplog: Option, + iterations: Option, + tokens_file: Option, + timeout: Option, + ) -> 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::()?; + Ok(()) + } +} diff --git a/libafl_sugar/src/lib.rs b/libafl_sugar/src/lib.rs index 115ba85071..fb41e4a5aa 100644 --- a/libafl_sugar/src/lib.rs +++ b/libafl_sugar/src/lib.rs @@ -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(()) +} diff --git a/libafl_sugar/src/qemu.rs b/libafl_sugar/src/qemu.rs index 5608eb7b07..e9bb51e498 100644 --- a/libafl_sugar/src/qemu.rs +++ b/libafl_sugar/src/qemu.rs @@ -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, + output_dir: PathBuf, + broker_port: u16, + cores: Cores, + use_cmplog: Option, + iterations: Option, + tokens_file: Option, + timeout: Option, + } + + #[pymethods] + impl QemuBytesCoverageSugar { + /// Create a new [`QemuBytesCoverageSugar`] + #[new] + #[allow(clippy::too_many_arguments)] + fn new( + input_dirs: Vec, + output_dir: PathBuf, + broker_port: u16, + cores: Vec, + use_cmplog: Option, + iterations: Option, + tokens_file: Option, + timeout: Option, + ) -> 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::()?; + Ok(()) + } +}