diff --git a/.clusterfuzzlite/Dockerfile b/.clusterfuzzlite/Dockerfile index 1bbdb7d5a..0ebd40670 100644 --- a/.clusterfuzzlite/Dockerfile +++ b/.clusterfuzzlite/Dockerfile @@ -1,14 +1,17 @@ -FROM ghcr.io/ledgerhq/ledger-app-builder/ledger-app-builder-lite:latest AS LITE_BUILDER +FROM ghcr.io/ledgerhq/ledger-app-builder/ledger-app-dev-tools:latest AS app-builder # Base image with clang toolchain FROM gcr.io/oss-fuzz-base/base-builder:v1 +RUN pip3 install --break-system-packages --no-cache-dir pillow>=3.4.0 +RUN apt update && apt install -y ninja-build zip + # Copy the project's source code. -COPY . $SRC/app-boilerplate -COPY --from=LITE_BUILDER /opt/ledger-secure-sdk $SRC/app-boilerplate/BOLOS_SDK +COPY . /app +COPY --from=app-builder /opt/flex-secure-sdk /ledger-secure-sdk # Working directory for build.sh -WORKDIR $SRC/app-boilerplate +WORKDIR /app # Copy build.sh into $SRC dir. COPY ./.clusterfuzzlite/build.sh $SRC/ diff --git a/.clusterfuzzlite/build.sh b/.clusterfuzzlite/build.sh index 08b092a69..703d7fa7c 100644 --- a/.clusterfuzzlite/build.sh +++ b/.clusterfuzzlite/build.sh @@ -2,8 +2,26 @@ # build fuzzers +export BOLOS_SDK=/ledger-secure-sdk + pushd fuzzing -cmake -DBOLOS_SDK=../BOLOS_SDK -Bbuild -H. -make -C build -mv ./build/fuzz_tx_parser "${OUT}" +cmake -S . -B build -DCMAKE_C_COMPILER=clang -DCMAKE_BUILD_TYPE=Debug \ + -G Ninja -DCMAKE_EXPORT_COMPILE_COMMANDS=On \ + -DBOLOS_SDK=${BOLOS_SDK} -DTARGET=flex \ + -DAPP_BUILD_PATH=/app + +# Generates .zip for initial corpus in clusterFuzz +for dir in harness/*; do + if [ -d "$dir" ]; then + fuzzer_name=$(basename "$dir") + zip_name="${fuzzer_name}_seed_corpus.zip" + echo "Zipping corpus from $dir into $zip_name" + + (cd "$dir" && zip -q -r "$zip_name" .) + + mv "$dir/$zip_name" "$OUT/" + fi +done +cmake --build build +mv ./build/fuzz_* "${OUT}" popd diff --git a/.gitignore b/.gitignore index 1e3520cde..5d8eec1bc 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,7 @@ unit-tests/coverage.info fuzzing/build/ fuzzing/corpus/ fuzzing/out/ +fuzzing/macros/generated/ # Python *.pyc[cod] diff --git a/fuzzing/CMakeLists.txt b/fuzzing/CMakeLists.txt index 702d20f6a..12d59f916 100644 --- a/fuzzing/CMakeLists.txt +++ b/fuzzing/CMakeLists.txt @@ -1,48 +1,53 @@ -cmake_minimum_required(VERSION 3.10) +include_guard() +cmake_minimum_required(VERSION 3.14) -if(${CMAKE_VERSION} VERSION_LESS 3.10) - cmake_policy(VERSION ${CMAKE_MAJOR_VERSION}.${CMAKE_MINOR_VERSION}) +if(${CMAKE_VERSION} VERSION_LESS 3.14) + cmake_policy(VERSION ${CMAKE_MAJOR_VERSION}.${CMAKE_MINOR_VERSION}) endif() # project information -project(FuzzTxParser - VERSION 1.0 - DESCRIPTION "Fuzzing of transaction parser" - LANGUAGES C) - -# guard against bad build-type strings -if (NOT CMAKE_BUILD_TYPE) - set(CMAKE_BUILD_TYPE "Debug") -endif() - -if (NOT CMAKE_C_COMPILER_ID MATCHES "Clang") - message(FATAL_ERROR "Fuzzer needs to be built with Clang") -endif() - -if (NOT DEFINED BOLOS_SDK) - message(FATAL_ERROR "BOLOS_SDK environment variable not found.") -endif() +project( + BoilerPlateFuzzer + VERSION 1.0 + DESCRIPTION "App Boilerplate example Fuzzer" + LANGUAGES C) -# guard against in-source builds -if(${CMAKE_SOURCE_DIR} STREQUAL ${CMAKE_BINARY_DIR}) - message(FATAL_ERROR "In-source builds not allowed. Please make a new directory (called a build directory) and run CMake from there. You may need to remove CMakeCache.txt. ") -endif() +set(DEFINES FUZZ) -# compatible with ClusterFuzzLite -if (NOT DEFINED ENV{LIB_FUZZING_ENGINE}) - set(COMPILATION_FLAGS_ "-g -Wall -fsanitize=fuzzer,address,undefined") -else() - set(COMPILATION_FLAGS_ "$ENV{LIB_FUZZING_ENGINE} $ENV{CXXFLAGS}") +if(NOT DEFINED BOLOS_SDK) + message(FATAL_ERROR "BOLOS_SDK must be defined, CMake will exit.") + return() endif() -set(CMAKE_EXPORT_COMPILE_COMMANDS ON) - -string(REPLACE " " ";" COMPILATION_FLAGS ${COMPILATION_FLAGS_}) - -include(extra/TxParser.cmake) - -add_executable(fuzz_tx_parser fuzz_tx_parser.c) - -target_compile_options(fuzz_tx_parser PUBLIC ${COMPILATION_FLAGS}) -target_link_options(fuzz_tx_parser PUBLIC ${COMPILATION_FLAGS}) -target_link_libraries(fuzz_tx_parser PUBLIC txparser) +add_subdirectory(${BOLOS_SDK}/fuzzing ${CMAKE_CURRENT_BINARY_DIR}/ledger-secure-sdk EXCLUDE_FROM_ALL) + +file(GLOB_RECURSE C_SOURCES "${CMAKE_SOURCE_DIR}/../src/*.c" "${CMAKE_SOURCE_DIR}/mock/*.c") +list(REMOVE_ITEM C_SOURCES "${CMAKE_SOURCE_DIR}/../src/app_main.c") + +add_library(code_lib ${C_SOURCES}) + +target_include_directories( + code_lib + PUBLIC ${CMAKE_SOURCE_DIR}/../src/ + ${CMAKE_SOURCE_DIR}/../src/apdu/ + ${CMAKE_SOURCE_DIR}/../src/swap/ + ${CMAKE_SOURCE_DIR}/../src/handler/ + ${CMAKE_SOURCE_DIR}/../src/helper/ + ${CMAKE_SOURCE_DIR}/../src/transaction/ + ${CMAKE_SOURCE_DIR}/../src/ui/ + ${CMAKE_SOURCE_DIR}/../src/ui/action/ + ${CMAKE_SOURCE_DIR}/mock/ + ${CMAKE_SOURCE_DIR}/) + +target_link_libraries(code_lib PUBLIC secure_sdk) +target_compile_definitions(code_lib PUBLIC ${DEFINES} FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION=1) + +# fuzz_dispatcher +add_executable(fuzz_dispatcher "${CMAKE_SOURCE_DIR}/harness/fuzz_dispatcher.c") +target_compile_definitions(fuzz_dispatcher PUBLIC macros FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION=1) +target_link_libraries(fuzz_dispatcher PUBLIC secure_sdk code_lib) + +# fuzz_tx_parser +add_executable(fuzz_tx_parser "${CMAKE_SOURCE_DIR}/harness/fuzz_tx_parser.c") +target_compile_definitions(fuzz_tx_parser PUBLIC macros FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION=1) +target_link_libraries(fuzz_tx_parser PUBLIC secure_sdk code_lib) diff --git a/fuzzing/README.md b/fuzzing/README.md index 9b0b5325a..424d67a8e 100644 --- a/fuzzing/README.md +++ b/fuzzing/README.md @@ -1,72 +1,126 @@ + + # Fuzzing on transaction parser ## Fuzzing -Fuzzing allows us to test how a program behaves when provided with invalid, unexpected, or random data as input. +Fuzzing allows us to test how a program behaves when provided with invalid, unexpected, or random +data as input. + +In the case of the harness `fuzz_tx_parser.c`, we want to test the code that is responsible for +parsing the transaction data, which is `transaction_deserialize()`. + +To test `transaction_deserialize()`, our fuzz target, `fuzz_tx_parser.c`, needs to implement +`int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)`, which provides an array of random +bytes that can be used to simulate a serialized transaction. -In the case of `app-boilerplate` we want to test the code that is responsible for parsing the transaction data, -which is `transaction_deserialize()`. -To test `transaction_deserialize()`, our fuzz target, `fuzz_tx_parser.c`, -needs to implement `int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)`, -which provides an array of random bytes that can be used to simulate a serialized transaction. -If the application crashes, or a [sanitizer](https://github.com/google/sanitizers) detects any kind of -access violation, the fuzzing process is stopped, a report regarding the vulnerability is shown, +If the application crashes, or a [sanitizer](https://github.com/google/sanitizers) detects any kind +of access violation, the fuzzing process is stopped, a report regarding the vulnerability is shown, and the input that triggered the bug is written to disk under the name `crash-*`. + The vulnerable input file created can be passed as an argument to the fuzzer to triage the issue. > **Note**: Usually we want to write a separate fuzz target for each functionality. +However, it is also possible to target the main function/dispatcher, so that we can cover more code, +as it is done in `fuzz_dispatcher.c`. + ## Manual usage based on Ledger container ### Preparation -The fuzzer can run from the docker `ledger-app-builder-legacy`. You can download it from the `ghcr.io` docker repository: +The fuzzer can be run using the Docker image `ledger-app-dev-tools`. You can download it from the +`ghcr.io` docker repository: -```console -sudo docker pull ghcr.io/ledgerhq/ledger-app-builder/ledger-app-builder-legacy:latest +```bash +docker pull ghcr.io/ledgerhq/ledger-app-builder/ledger-app-dev-tools:latest ``` -You can then enter this development environment by executing the following command from the repository root directory: +You can then enter this development environment by executing the following command from the +repository root directory: -```console -sudo docker run --rm -ti --user "$(id -u):$(id -g)" -v "$(realpath .):/app" ghcr.io/ledgerhq/ledger-app-builder/ledger-app-builder-legacy:latest +```bash +docker run --rm -ti -v "$(realpath .):/app" ghcr.io/ledgerhq/ledger-app-builder/ledger-app-dev-tools:latest ``` -### Compilation +### Writing your Harness -Once in the container, go into the `fuzzing` folder to compile the fuzzer: +When writing your harness, keep the following points in mind: -```console -cd fuzzing +- An SDK's interface for compilation is provided via the target `secure_sdk` in CMakeLists.txt +- If you are running it for the first time, consider using the script `local_run` from inside the + Docker container using the flag build=1, if you need to manually + add/remove macros you can then do it using the files macros/add_macros.txt or + macros/exclude_macros.txt and rerunning it, or directly change the macros/generated/macros.txt. +- A typical harness looks like this: -# cmake initialization -cmake -DBOLOS_SDK=/opt/ledger-secure-sdk -DCMAKE_C_COMPILER=/usr/bin/clang -Bbuild -H. + ```C -# Fuzzer compilation -make -C build -``` + int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { + if (sigsetjmp(fuzz_exit_jump_ctx.jmp_buf, 1)) return 0; -### Run + ### harness code ### + + return 0; + } + + ``` -```console -./build/fuzz_tx_parser + This allows a return point when the `os_sched_exit()` function is mocked. + +- To provide an SDK interface, we automatically generate syscall mock functions located in + `SECURE_SDK_PATH/fuzzing/mock/generated/generated_syscalls.c`, if you need a more specific mock, + you can define it in `APP_PATH/fuzzing/mock` with the same name and without the WEAK attribute. + +### Compile and run the fuzzer from the container + +Once inside the container, navigate to the `fuzzing` folder to compile the fuzzer: + +```bash +export BOLOS_SDK=/opt/flex-secure-sdk + +cd fuzzing + +${BOLOS_SDK}/fuzzing/local_run.sh --build=1 \ + --BOLOS_SDK=${BOLOS_SDK} \ + --fuzzer=build/fuzz_dispatcher \ + --j=4 \ + --run-fuzzer=1 \ + --compute-coverage=1 ``` -## Full usage based on `clusterfuzzlite` container +### About local_run.sh + +| Parameter | Type | Description | +| :--------------------- | :------------------ | :------------------------------------------------------------------- | +| `--BOLOS_SDK` | `PATH TO BOLOS SDK` | **Required**. Path to the BOLOS SDK | +| `--build` | `bool` | **Optional**. Whether to build the project (default: 0) | +| `--fuzzer` | `PATH` | **Required**. Path to the fuzzer binary | +| `--compute-coverage` | `bool` | **Optional**. Whether to compute coverage after fuzzing (default: 0) | +| `--run-fuzzer` | `bool` | **Optional**. Whether to run or not the fuzzer (default: 0) | +| `--run-crash` | `FILENAME` | **Optional**. Run the on a specific crash input file (default: 0) | +| `--sanitizer` | `address or memory` | **Optional**. Compile with sanitizer (default: address) | +| `--j` | `int` | **Optional**. N-parallel jobs for build and fuzzing (default: 1) | +| `--help` | | **Optional**. Display help message | + +### Visualizing code coverage + +After running your fuzzer, if `--compute-coverage=1` the coverage will be available in your browser. + +## Full usage based on `clusterfuzzlite` container - TODO after SDK FUZZING RELEASE Exactly the same context as the CI, directly using the `clusterfuzzlite` environment. -More info can be found here: - +More info can be found here: ### Preparation The principle is to build the container, and run it to perform the fuzzing. -> **Note**: The container contains a copy of the sources (they are not cloned), -> which means the `docker build` command must be re-executed after each code modification. +> **Note**: The container contains a copy of the sources (they are not cloned), which means the +> `docker build` command must be re-executed after each code modification. -```console +```bash # Prepare directory tree mkdir fuzzing/{corpus,out} # Container generation @@ -75,12 +129,14 @@ docker build -t app-boilerplate --file .clusterfuzzlite/Dockerfile . ### Compilation -```console +```bash docker run --rm --privileged -e FUZZING_LANGUAGE=c -v "$(realpath .)/fuzzing/out:/out" -ti app-boilerplate ``` ### Run -```console -docker run --rm --privileged -e FUZZING_ENGINE=libfuzzer -e RUN_FUZZER_MODE=interactive -v "$(realpath .)/fuzzing/corpus:/tmp/fuzz_corpus" -v "$(realpath .)/fuzzing/out:/out" -ti gcr.io/oss-fuzz-base/base-runner run_fuzzer fuzz_tx_parser +```bash +docker run --rm --privileged -e FUZZING_ENGINE=libfuzzer -e RUN_FUZZER_MODE=interactive -v + "$(realpath .)/fuzzing/corpus:/tmp/fuzz_corpus" -v "$(realpath .)/fuzzing/out:/out" + -ti gcr.io/oss-fuzz-base/base-runner run_fuzzer fuzz_tx_parser ``` diff --git a/fuzzing/extra/TxParser.cmake b/fuzzing/extra/TxParser.cmake deleted file mode 100644 index 27349a0c8..000000000 --- a/fuzzing/extra/TxParser.cmake +++ /dev/null @@ -1,31 +0,0 @@ -# project information -project(TxParser - VERSION 1.0 - DESCRIPTION "Transaction parser of Boilerplate app" - LANGUAGES C) - -# specify C standard -set(CMAKE_C_STANDARD 11) -set(CMAKE_C_STANDARD_REQUIRED True) -set(CMAKE_C_FLAGS_DEBUG - "${CMAKE_C_FLAGS_DEBUG} -Wall -Wextra -Wno-unused-function -DFUZZ -pedantic -g -O0" -) - -add_library(txparser - ${BOLOS_SDK}/lib_standard_app/format.c - ${BOLOS_SDK}/lib_standard_app/buffer.c - ${BOLOS_SDK}/lib_standard_app/read.c - ${BOLOS_SDK}/lib_standard_app/varint.c - ${BOLOS_SDK}/lib_standard_app/bip32.c - ${BOLOS_SDK}/lib_standard_app/write.c - ${CMAKE_CURRENT_SOURCE_DIR}/../src/transaction/utils.c - ${CMAKE_CURRENT_SOURCE_DIR}/../src/transaction/deserialize.c -) - -set_target_properties(txparser PROPERTIES SOVERSION 1) - -target_include_directories(txparser PUBLIC - ${CMAKE_CURRENT_SOURCE_DIR}/../src - ${CMAKE_CURRENT_SOURCE_DIR}/../src/transaction - ${BOLOS_SDK}/lib_standard_app -) diff --git a/fuzzing/harness/fuzz_dispatcher.c b/fuzzing/harness/fuzz_dispatcher.c new file mode 100644 index 000000000..042a7763b --- /dev/null +++ b/fuzzing/harness/fuzz_dispatcher.c @@ -0,0 +1,46 @@ +#include +#include +#include +#include +#include "globals.h" +#include "dispatcher.h" +#include "mocks.h" + +global_ctx_t G_context; +const internal_storage_t N_storage_real; + +#ifndef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION +#message "Use this macro for code only needed in fuzz targets" +#endif + +void print_apdu(command_t cmd) { + printf("=> CLA=%02x, INS=%02x, P1=%02x, P2=%02x, LC=%02x, DATA=", + cmd.cla, + cmd.ins, + cmd.p1, + cmd.p2, + cmd.lc); + + for (int i = 0; i < cmd.lc; i++) { + printf("%02X", cmd.data[i]); + } + printf("\n"); +} + +// Fuzz entry point +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { + if (sigsetjmp(fuzz_exit_jump_ctx.jmp_buf, 1)) return 0; + + if (size < 4) return 0; + + command_t cmd; + cmd.cla = data[0]; + cmd.ins = data[1]; + cmd.p1 = data[2]; + cmd.p2 = data[3]; + cmd.lc = size - 4; + if (size > 4) cmd.data = (uint8_t *) &data[4]; + // print_apdu(cmd); + apdu_dispatcher(&cmd); + return 0; +} diff --git a/fuzzing/fuzz_tx_parser.c b/fuzzing/harness/fuzz_tx_parser.c similarity index 67% rename from fuzzing/fuzz_tx_parser.c rename to fuzzing/harness/fuzz_tx_parser.c index 1d43f5c43..e56293e2a 100644 --- a/fuzzing/fuzz_tx_parser.c +++ b/fuzzing/harness/fuzz_tx_parser.c @@ -3,12 +3,17 @@ #include #include -#include "deserialize.h" -#include "utils.h" -#include "tx_types.h" +#include "transaction/deserialize.h" +#include "transaction/utils.h" +#include "transaction/tx_types.h" #include "format.h" +#include "mock/mocks.h" +#include +jmp_buf fuzz_exit_jump_buf; int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { + if (sigsetjmp(fuzz_exit_jump_ctx.jmp_buf, 1)) return 0; + buffer_t buf = {.ptr = data, .size = size, .offset = 0}; transaction_t tx; parser_status_e status; @@ -23,13 +28,13 @@ int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { if (status == PARSING_OK) { format_u64(nonce, sizeof(nonce), tx.nonce); - printf("nonce: %s\n", nonce); + // printf("nonce: %s\n", nonce); format_hex(tx.to, ADDRESS_LEN, address, sizeof(address)); - printf("address: %s\n", address); + // printf("address: %s\n", address); format_fpu64(amount, sizeof(amount), tx.value, 3); // exponent of smallest unit is 3 - printf("amount: %s\n", amount); + // printf("amount: %s\n", amount); transaction_utils_format_memo(tx.memo, tx.memo_len, tx_memo, sizeof(tx_memo)); - printf("memo: %s\n", tx_memo); + // printf("memo: %s\n", tx_memo); } return 0; diff --git a/fuzzing/macros/add_macros.txt b/fuzzing/macros/add_macros.txt new file mode 100644 index 000000000..e69de29bb diff --git a/fuzzing/macros/exclude_macros.txt b/fuzzing/macros/exclude_macros.txt new file mode 100644 index 000000000..d2c773362 --- /dev/null +++ b/fuzzing/macros/exclude_macros.txt @@ -0,0 +1,2 @@ +HAVE_SHA512_WITH_BLOCK_ALT_METHOD +PRINTF(...)= diff --git a/fuzzing/mock/mocks.c b/fuzzing/mock/mocks.c new file mode 100644 index 000000000..c399a21d2 --- /dev/null +++ b/fuzzing/mock/mocks.c @@ -0,0 +1,38 @@ +#include "mocks.h" + +// APPs expect a specific length +cx_err_t cx_ecdomain_parameters_length(cx_curve_t cv, size_t *length) { + (void) cv; + *length = (size_t) 32; + return 0x00000000; +} + +// Simulates writing to NVM +void nvm_write(void *dst_adr, void *src_adr, unsigned int src_len) { + if (!dst_adr || !src_adr || src_len == 0) { + return; + } + memcpy(dst_adr, src_adr, src_len); +} + +try_context_t fuzz_exit_jump_ctx = {0}; +try_context_t *G_exception_context = &fuzz_exit_jump_ctx; + +try_context_t *try_context_get(void) { + return G_exception_context; +} + +try_context_t *try_context_set(try_context_t *context) { + try_context_t *previous = G_exception_context; + G_exception_context = context; + return previous; +} + +void __attribute__((noreturn)) +os_sched_exit(bolos_task_status_t exit_code __attribute__((unused))) { + longjmp(fuzz_exit_jump_ctx.jmp_buf, 1); +} + +void __attribute__((noreturn)) os_lib_end(void) { + longjmp(fuzz_exit_jump_ctx.jmp_buf, 1); +} diff --git a/fuzzing/mock/mocks.h b/fuzzing/mock/mocks.h new file mode 100644 index 000000000..c5ea4dfb6 --- /dev/null +++ b/fuzzing/mock/mocks.h @@ -0,0 +1,23 @@ +#include "cx_errors.h" +#include "ox_ec.h" +#include "os_task.h" +#include +#include +#include "exceptions.h" +#include +#include + +// to simulate exiting makes a long_jump to fuzzer harness +extern try_context_t fuzz_exit_jump_ctx; + +try_context_t *try_context_get(void); + +try_context_t *try_context_set(try_context_t *context); + +void __attribute__((noreturn)) os_sched_exit(bolos_task_status_t exit_code); + +void __attribute__((noreturn)) os_lib_end(void); + +cx_err_t cx_ecdomain_parameters_length(cx_curve_t cv, size_t *length); + +void nvm_write(void *dst_adr, void *src_adr, unsigned int src_len);