From 246c52cd29eef9c181b50b272664a093e4cfe7d9 Mon Sep 17 00:00:00 2001 From: qbLedger Date: Wed, 18 Dec 2024 17:09:22 +0100 Subject: [PATCH] Add fuzzing in CI Change submodules to track using https instead of git protocol, since ClusterFuzzLite does not support git submodules. Copy from https://github.com/LedgerHQ/app-plugin-boilerplate: - /.clusterfuzzlite/ - /.github/workflows/cflite_{cron,pr}.yml - /fuzzing/ Change "plugin.h" imports to "kiln_plugin.h" in `/fuzzing/`. --- .clusterfuzzlite/Dockerfile | 14 +++ .clusterfuzzlite/build.sh | 9 ++ .clusterfuzzlite/project.yaml | 1 + .github/workflows/cflite_cron.yml | 41 ++++++++ .github/workflows/cflite_pr.yml | 43 +++++++++ .gitmodules | 4 +- fuzzing/CMakeLists.txt | 117 +++++++++++++++++++++++ fuzzing/README.md | 87 +++++++++++++++++ fuzzing/fuzz_plugin.c | 154 ++++++++++++++++++++++++++++++ fuzzing/mocks.c | 52 ++++++++++ 10 files changed, 520 insertions(+), 2 deletions(-) create mode 100644 .clusterfuzzlite/Dockerfile create mode 100644 .clusterfuzzlite/build.sh create mode 100644 .clusterfuzzlite/project.yaml create mode 100644 .github/workflows/cflite_cron.yml create mode 100644 .github/workflows/cflite_pr.yml create mode 100644 fuzzing/CMakeLists.txt create mode 100644 fuzzing/README.md create mode 100644 fuzzing/fuzz_plugin.c create mode 100644 fuzzing/mocks.c diff --git a/.clusterfuzzlite/Dockerfile b/.clusterfuzzlite/Dockerfile new file mode 100644 index 0000000..e44aff1 --- /dev/null +++ b/.clusterfuzzlite/Dockerfile @@ -0,0 +1,14 @@ +FROM ghcr.io/ledgerhq/ledger-app-builder/ledger-app-builder-lite:latest AS LITE_BUILDER + +# Base image with clang toolchain +FROM gcr.io/oss-fuzz-base/base-builder:v1 + +# Copy the project's source code. +COPY . $SRC/app-plugin-boilerplate +COPY --from=LITE_BUILDER /opt/nanox-secure-sdk $SRC/app-plugin-boilerplate/BOLOS_SDK + +# Working directory for build.sh +WORKDIR $SRC/app-plugin-boilerplate + +# Copy build.sh into $SRC dir. +COPY ./.clusterfuzzlite/build.sh $SRC/ diff --git a/.clusterfuzzlite/build.sh b/.clusterfuzzlite/build.sh new file mode 100644 index 0000000..3a819ae --- /dev/null +++ b/.clusterfuzzlite/build.sh @@ -0,0 +1,9 @@ +#!/bin/bash -eu + +# build fuzzers + +pushd fuzzing +cmake -DBOLOS_SDK=../BOLOS_SDK -Bbuild -H. +make -C build +mv ./build/fuzz "${OUT}" +popd \ No newline at end of file diff --git a/.clusterfuzzlite/project.yaml b/.clusterfuzzlite/project.yaml new file mode 100644 index 0000000..e196c5c --- /dev/null +++ b/.clusterfuzzlite/project.yaml @@ -0,0 +1 @@ +language: c \ No newline at end of file diff --git a/.github/workflows/cflite_cron.yml b/.github/workflows/cflite_cron.yml new file mode 100644 index 0000000..44ac10c --- /dev/null +++ b/.github/workflows/cflite_cron.yml @@ -0,0 +1,41 @@ +name: ClusterFuzzLite cron tasks +on: + workflow_dispatch: + push: + branches: + - main # Use your actual default branch here. + schedule: + - cron: '0 13 * * 6' # At 01:00 PM, only on Saturday +permissions: read-all +jobs: + Fuzzing: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - mode: batch + sanitizer: address + - mode: batch + sanitizer: memory + - mode: prune + sanitizer: address + - mode: coverage + sanitizer: coverage + steps: + - name: Build Fuzzers (${{ matrix.mode }} - ${{ matrix.sanitizer }}) + id: build + uses: google/clusterfuzzlite/actions/build_fuzzers@v1 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + language: c # Change this to the language you are fuzzing. + sanitizer: ${{ matrix.sanitizer }} + - name: Run Fuzzers (${{ matrix.mode }} - ${{ matrix.sanitizer }}) + id: run + uses: google/clusterfuzzlite/actions/run_fuzzers@v1 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + fuzz-seconds: 300 # 5 minutes + mode: ${{ matrix.mode }} + sanitizer: ${{ matrix.sanitizer }} + \ No newline at end of file diff --git a/.github/workflows/cflite_pr.yml b/.github/workflows/cflite_pr.yml new file mode 100644 index 0000000..f70175e --- /dev/null +++ b/.github/workflows/cflite_pr.yml @@ -0,0 +1,43 @@ +name: ClusterFuzzLite PR fuzzing +on: + pull_request: + paths: + - '**' +permissions: read-all +jobs: + PR: + runs-on: ubuntu-latest + concurrency: + group: ${{ github.workflow }}-${{ matrix.sanitizer }}-${{ github.ref }} + cancel-in-progress: true + strategy: + fail-fast: false + matrix: + sanitizer: [address, undefined, memory] # Override this with the sanitizers you want. + steps: + - name: Build Fuzzers (${{ matrix.sanitizer }}) + id: build + uses: google/clusterfuzzlite/actions/build_fuzzers@v1 + with: + language: c # Change this to the language you are fuzzing. + github-token: ${{ secrets.GITHUB_TOKEN }} + sanitizer: ${{ matrix.sanitizer }} + # Optional but recommended: used to only run fuzzers that are affected + # by the PR. + # storage-repo: https://${{ secrets.PERSONAL_ACCESS_TOKEN }}@github.com/OWNER/STORAGE-REPO-NAME.git + # storage-repo-branch: main # Optional. Defaults to "main" + # storage-repo-branch-coverage: gh-pages # Optional. Defaults to "gh-pages". + - name: Run Fuzzers (${{ matrix.sanitizer }}) + id: run + uses: google/clusterfuzzlite/actions/run_fuzzers@v1 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + fuzz-seconds: 300 # 5 minutes + mode: 'code-change' + sanitizer: ${{ matrix.sanitizer }} + output-sarif: true + # Optional but recommended: used to download the corpus produced by + # batch fuzzing. + # storage-repo: https://${{ secrets.PERSONAL_ACCESS_TOKEN }}@github.com/OWNER/STORAGE-REPO-NAME.git + # storage-repo-branch: main # Optional. Defaults to "main" + # storage-repo-branch-coverage: gh-pages # Optional. Defaults to "gh-pages". \ No newline at end of file diff --git a/.gitmodules b/.gitmodules index 60c0a2f..b3b46f4 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,6 @@ [submodule "ethereum-plugin-sdk"] path = ethereum-plugin-sdk - url = git@github.com:LedgerHQ/ethereum-plugin-sdk.git + url = https://github.com/LedgerHQ/ethereum-plugin-sdk [submodule "app-ethereum"] path = app-ethereum - url = git@github.com:LedgerHQ/app-ethereum.git + url = https://github.com/LedgerHQ/app-ethereum diff --git a/fuzzing/CMakeLists.txt b/fuzzing/CMakeLists.txt new file mode 100644 index 0000000..5f2e4d5 --- /dev/null +++ b/fuzzing/CMakeLists.txt @@ -0,0 +1,117 @@ +cmake_minimum_required(VERSION 3.10) + +if(${CMAKE_VERSION} VERSION_LESS 3.10) + cmake_policy(VERSION ${CMAKE_MAJOR_VERSION}.${CMAKE_MINOR_VERSION}) +endif() + +# project information +project(Fuzzer + VERSION 1.0 + DESCRIPTION "Contract parser of Boilerplate plugin app" + LANGUAGES C) + +# guard against bad build-type strings +if (NOT CMAKE_BUILD_TYPE) + set(CMAKE_BUILD_TYPE "Release") +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() + +# 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() + +# specify C standard +set(CMAKE_C_STANDARD 11) +set(CMAKE_C_STANDARD_REQUIRED True) + +# compatible with ClusterFuzzLite +if (NOT DEFINED ENV{LIB_FUZZING_ENGINE}) + set(COMPILATION_FLAGS_ "-fsanitize=fuzzer,address,undefined,signed-integer-overflow") +else() + set(COMPILATION_FLAGS_ "$ENV{LIB_FUZZING_ENGINE} $ENV{CFLAGS}") +endif() +string(REPLACE " " ";" COMPILATION_FLAGS ${COMPILATION_FLAGS_}) + +add_compile_options(-Wall -Wextra -g -pedantic) +# Just to limit compilation warnings of the plugin sources +add_compile_options(-Wno-implicit-function-declaration) +# Flag depending on the Build Type +set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -O0") +set(CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE} -ggdb2 -O3") + +set(SRC_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../src") +set(ETH_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../ethereum-plugin-sdk") + +add_compile_definitions( + APPNAME="PluginBoilerplate" +) + +add_compile_definitions( + IO_HID_EP_LENGTH=64 + HAVE_ECC + HAVE_ECC_WEIERSTRASS + HAVE_SECP_CURVES + HAVE_ECC_TWISTED_EDWARDS + HAVE_ED_CURVES + HAVE_ECDSA + HAVE_EDDSA + HAVE_HASH + HAVE_BLAKE2 + HAVE_SHA224 + HAVE_SHA256 + HAVE_SHA3 + HAVE_SHA512 +) + +include_directories( + ${BOLOS_SDK}/include + ${BOLOS_SDK}/lib_standard_app + ${BOLOS_SDK}/lib_cxng/include + ${BOLOS_SDK}/lib_cxng/src + ${BOLOS_SDK}/target/nanox/include + ${ETH_DIR}/src + ${SRC_DIR} +) + +# Take all source files from the application and the sdk +file(GLOB_RECURSE APPLICATION_SRC + # Take all plugin sources + ${SRC_DIR}/*.c + + # Take all sdk sources + ${ETH_DIR}/src/*.c +) +# Filter out main.c from the SDK, the fuzzing has its own main +list(FILTER APPLICATION_SRC EXCLUDE REGEX "${ETH_DIR}/src/main") + +add_executable(fuzz + ${APPLICATION_SRC} + + # fuzzing specific files + fuzz_plugin.c + mocks.c + + # sdk utils + ${BOLOS_SDK}/src/ledger_assert.c + ${BOLOS_SDK}/lib_standard_app/format.c + + # cxng + ${BOLOS_SDK}/lib_cxng/src/cx_hash.c + ${BOLOS_SDK}/lib_cxng/src/cx_sha256.c + ${BOLOS_SDK}/lib_cxng/src/cx_sha512.c + ${BOLOS_SDK}/lib_cxng/src/cx_sha3.c + ${BOLOS_SDK}/lib_cxng/src/cx_blake2b.c + ${BOLOS_SDK}/lib_cxng/src/cx_utils.c + ${BOLOS_SDK}/lib_cxng/src/cx_ram.c +) + +target_compile_options(fuzz PUBLIC ${COMPILATION_FLAGS}) +target_link_options(fuzz PUBLIC ${COMPILATION_FLAGS}) diff --git a/fuzzing/README.md b/fuzzing/README.md new file mode 100644 index 0000000..29a7dd5 --- /dev/null +++ b/fuzzing/README.md @@ -0,0 +1,87 @@ +# Fuzzing on transaction parser + +[//]: # (Comment) +[//]: # (This file when in the plugin-boilerplate repository is included in the Ethereum Plugin SDK Github page, keep that in mind when editing it.) + +Fuzzing allows us to test how a program behaves when provided with invalid, unexpected, or random data as input. + +In the case of `ledger-plugin-coinbase-staking` we want to test the code that is responsible for handling the contract data. +The fuzzer 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, 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. + +## Manual usage based on Ledger container + +### Preparation + +Before being able to use the fuzzing tests, the environment must be prepared with all submodules. +To install them, use the following command in the repository root directory: + +```shell +git submodule update --init +``` + +The fuzzer can run from the docker `ledger-app-builder-legacy`. You can download it from the `ghcr.io` docker repository: + +```shell +sudo docker pull ghcr.io/ledgerhq/ledger-app-builder/ledger-app-builder-legacy:latest +``` + +You can then enter this development environment by executing the following command from the repository root directory: + +```shell +sudo docker run --rm -ti --user "$(id -u):$(id -g)" -v "$(realpath .):/app" ghcr.io/ledgerhq/ledger-app-builder/ledger-app-builder-legacy:latest +``` + +### Compilation + +Once in the container, go into the `fuzzing` folder to compile the fuzzer: + +```shell +cd fuzzing + +# cmake initialization +cmake -DBOLOS_SDK=/opt/nanox-secure-sdk -DCMAKE_C_COMPILER=/usr/bin/clang -Bbuild -H. + +# Fuzzer compilation +make -C build +``` + +### Run + +```shell +./build/fuzz +``` + +## Full usage based on `clusterfuzzlite` container + +Exactly the same context as the CI, directly using the `clusterfuzzlite` environment. + +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. + +```shell +# Prepare directory tree +mkdir fuzzing/{corpus,out} +# Container generation +docker build -t app-plugin-boilerplate --file .clusterfuzzlite/Dockerfile . +``` + +### Compilation + +```shell +docker run --rm --privileged -e FUZZING_LANGUAGE=c -v "$(realpath .)/fuzzing/out:/out" -ti app-plugin-boilerplate +``` + +### Run + +```shell +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 +``` diff --git a/fuzzing/fuzz_plugin.c b/fuzzing/fuzz_plugin.c new file mode 100644 index 0000000..2ff90e6 --- /dev/null +++ b/fuzzing/fuzz_plugin.c @@ -0,0 +1,154 @@ +#include "kiln_plugin.h" + +// set a small size to detect possible overflows +#define NAME_LENGTH 3u +#define VERSION_LENGTH 3u + +void handle_init_contract(ethPluginInitContract_t *parameters); +void handle_provide_parameter(ethPluginProvideParameter_t *parameters); +void handle_finalize(ethPluginFinalize_t *parameters); +void handle_provide_token(ethPluginProvideInfo_t *parameters); +void handle_query_contract_id(ethQueryContractID_t *parameters); +void handle_query_contract_ui(ethQueryContractUI_t *parameters); + +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { + ethPluginInitContract_t init_contract = {0}; + ethPluginProvideParameter_t provide_param = {0}; + ethPluginFinalize_t finalize = {0}; + ethPluginProvideInfo_t provide_info = {0}; + ethQueryContractID_t query_id = {0}; + ethQueryContractUI_t query_ui = {0}; + txContent_t content = {0}; + + // Fake sha3 context + cx_sha3_t sha3; + + ethPluginSharedRO_t shared_ro; + shared_ro.txContent = &content; + + ethPluginSharedRW_t shared_rw; + shared_rw.sha3 = &sha3; + + context_t context; + const uint8_t address[ADDRESS_LENGTH] = {0xee, 0xee, 0xee, 0xee, 0xee, 0xee, 0xee, + 0xee, 0xee, 0xee, 0xee, 0xee, 0xee, 0xee, + 0xee, 0xee, 0xee, 0xee, 0xee, 0xee}; + + // see fullAmount / fullAddress in + char title[32] = {0}; + char msg[79] = {0}; // 2^256 is 78 digits long + + // for token lookups + extraInfo_t item1 = {0}; + extraInfo_t item2 = {0}; + + char name[NAME_LENGTH] = {0}; + char version[VERSION_LENGTH] = {0}; + + // data must be big enough to hold a selector and the txcontent + if (size < 4 + sizeof(txContent_t)) { + return 0; + } + memcpy(&content, data + 4, sizeof(txContent_t)); + + init_contract.interfaceVersion = ETH_PLUGIN_INTERFACE_VERSION_LATEST; + init_contract.selector = data; + init_contract.pluginSharedRO = &shared_ro; + init_contract.pluginSharedRW = &shared_rw; + init_contract.pluginContext = (uint8_t *) &context; + init_contract.pluginContextLength = sizeof(context); + + handle_init_contract(&init_contract); + if (init_contract.result != ETH_PLUGIN_RESULT_OK) { + return 0; + } + + size_t i = 4 + sizeof(txContent_t); + // potentially save space for token lookups + while (size - i >= 32 + sizeof(extraInfo_t) * 2) { + provide_param.parameter = data + i; + provide_param.parameterOffset = i; + provide_param.pluginContext = (uint8_t *) &context; + provide_param.pluginSharedRO = &shared_ro; + provide_param.pluginSharedRW = &shared_rw; + handle_provide_parameter(&provide_param); + if (provide_param.result != ETH_PLUGIN_RESULT_OK) { + return 0; + } + i += 32; + } + + finalize.pluginContext = (uint8_t *) &context; + finalize.address = address; + finalize.pluginSharedRO = &shared_ro; + finalize.pluginSharedRW = &shared_rw; + handle_finalize(&finalize); + if (finalize.result != ETH_PLUGIN_RESULT_OK) { + return 0; + } + + if (finalize.tokenLookup1 || finalize.tokenLookup2) { + provide_info.pluginContext = (uint8_t *) &context; + provide_info.pluginSharedRO = &shared_ro; + provide_info.pluginSharedRW = &shared_rw; + if (finalize.tokenLookup1) { + if (size - i >= sizeof(extraInfo_t)) { + provide_info.item1 = &item1; + + memcpy(provide_info.item1, data + i, sizeof(extraInfo_t)); + provide_info.item1->token.ticker[MAX_TICKER_LEN - 1] = '\0'; + i += sizeof(extraInfo_t); + } + } + + if (finalize.tokenLookup2) { + if (size - i >= sizeof(extraInfo_t)) { + provide_info.item2 = &item2; + + memcpy(provide_info.item2, data + i, sizeof(extraInfo_t)); + provide_info.item2->token.ticker[MAX_TICKER_LEN - 1] = '\0'; + i += sizeof(extraInfo_t); + } + } + + handle_provide_token(&provide_info); + if (provide_info.result != ETH_PLUGIN_RESULT_OK) { + return 0; + } + } + + query_id.pluginContext = (uint8_t *) &context; + query_id.pluginSharedRO = &shared_ro; + query_id.pluginSharedRW = &shared_rw; + query_id.name = name; + query_id.nameLength = sizeof(name); + query_id.version = version; + query_id.versionLength = sizeof(version); + handle_query_contract_id(&query_id); + + if (query_id.result != ETH_PLUGIN_RESULT_OK) { + return 0; + } + + printf("name: %s\n", query_id.name); + printf("version: %s\n", query_id.version); + + for (int i = 0; i < finalize.numScreens + provide_info.additionalScreens; i++) { + query_ui.title = title; + query_ui.titleLength = sizeof(title); + query_ui.msg = msg; + query_ui.msgLength = sizeof(msg); + query_ui.pluginContext = (uint8_t *) &context; + query_ui.pluginSharedRO = &shared_ro; + query_ui.pluginSharedRW = &shared_rw; + + query_ui.screenIndex = i; + handle_query_contract_ui(&query_ui); + if (query_ui.result != ETH_PLUGIN_RESULT_OK) { + return 0; + } + printf("%s: %s\n", title, msg); + } + + return 0; +} diff --git a/fuzzing/mocks.c b/fuzzing/mocks.c new file mode 100644 index 0000000..0c84129 --- /dev/null +++ b/fuzzing/mocks.c @@ -0,0 +1,52 @@ +#include "kiln_plugin.h" +#include "lcx_common.h" +#include "lcx_hash.h" +#include + +size_t strlcat(char *dst, const char *src, size_t size) { + size_t srclen; /* Length of source string */ + size_t dstlen; /* Length of destination string */ + + dstlen = strlen(dst); + size -= dstlen + 1; + + if (!size) return (dstlen); /* No room, return immediately... */ + + srclen = strlen(src); + + if (srclen > size) srclen = size; + + memcpy(dst + dstlen, src, srclen); + dst[dstlen + srclen] = '\0'; + + return (dstlen + srclen); +} + +size_t strlcpy(char *dst, const char *src, size_t size) { + size_t srclen; /* Length of source string */ + + size--; + + srclen = strlen(src); + + if (srclen > size) srclen = size; + + memcpy(dst, src, srclen); + dst[srclen] = '\0'; + + return (srclen); +} + +cx_err_t cx_keccak_256_hash_iovec(const cx_iovec_t *iovec, + size_t iovec_len, + uint8_t digest[static CX_KECCAK_256_SIZE]) { + return CX_OK; +} + +void os_sched_exit(bolos_task_status_t exit_code) { + return; +} + +void *pic(void *p) { + return p; +}