diff --git a/.clusterfuzzlite/Dockerfile b/.clusterfuzzlite/Dockerfile new file mode 100644 index 0000000000..f65fbeac44 --- /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-ethereum +COPY --from=LITE_BUILDER /opt/ledger-secure-sdk $SRC/app-ethereum/BOLOS_SDK + +# Working directory for build.sh +WORKDIR $SRC/app-ethereum + +# 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 0000000000..782a29b3a2 --- /dev/null +++ b/.clusterfuzzlite/build.sh @@ -0,0 +1,9 @@ +#!/bin/bash -eu + +# build fuzzers + +pushd tests/fuzzing +cmake -DBOLOS_SDK=../BOLOS_SDK -Bbuild -H. +make -C build +mv ./build/fuzz_app_eth "${OUT}" +popd diff --git a/.clusterfuzzlite/project.yaml b/.clusterfuzzlite/project.yaml new file mode 100644 index 0000000000..b455aa397c --- /dev/null +++ b/.clusterfuzzlite/project.yaml @@ -0,0 +1 @@ +language: c diff --git a/.github/workflows/cflite_cron.yml b/.github/workflows/cflite_cron.yml new file mode 100644 index 0000000000..17c1e65a23 --- /dev/null +++ b/.github/workflows/cflite_cron.yml @@ -0,0 +1,40 @@ +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 }} diff --git a/.github/workflows/cflite_pr.yml b/.github/workflows/cflite_pr.yml new file mode 100644 index 0000000000..09f91dafe6 --- /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". diff --git a/tests/fuzzing/CMakeLists.txt b/tests/fuzzing/CMakeLists.txt new file mode 100644 index 0000000000..ea36c8abd0 --- /dev/null +++ b/tests/fuzzing/CMakeLists.txt @@ -0,0 +1,170 @@ +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 "Eth Fuzzer" + LANGUAGES C) + +set(CMAKE_C_COMPILER clang) + +set(CMAKE_BUILD_TYPE "Debug") + +set(CMAKE_C_STANDARD 11) +set(CMAKE_C_FLAGS + "${CMAKE_C_FLAGS} -Wall -Wextra -pedantic -Wno-unused-parameter -Wno-embedded-directive -g -O0 -fsanitize=fuzzer,address,undefined -fprofile-instr-generate -fcoverage-mapping" +) + +# 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(TARGET_DEVICE flex) +set(BOLOS_SDK /opt/${TARGET_DEVICE}-secure-sdk) +if (NOT DEFINED BOLOS_SDK) + message(FATAL_ERROR "BOLOS_SDK environment variable not found.") +endif() + +set(DEFINES + gcc + APPNAME=\"Fuzzing\" + API_LEVEL=21 + TARGET=\"flex\" + TARGET_NAME=\"TARGET_FLEX\" + APPVERSION=\"1.1.0\" + SDK_NAME=\"ledger-secure-sdk\" + SDK_VERSION=\"v21.3.3\" + SDK_HASH=\"d88d4db3c93665f52b5b1f45099d9d36dfaa06ba\" + gcc + __IO=volatile + NDEBUG + HAVE_BAGL_FONT_INTER_REGULAR_28PX + HAVE_BAGL_FONT_INTER_SEMIBOLD_28PX + HAVE_BAGL_FONT_INTER_MEDIUM_36PX + HAVE_INAPP_BLE_PAIRING + HAVE_NBGL + HAVE_PIEZO_SOUND + HAVE_SE_TOUCH + HAVE_SE_EINK_DISPLAY + NBGL_PAGE + NBGL_USE_CASE + SCREEN_SIZE_WALLET + HAVE_FAST_HOLD_TO_APPROVE + HAVE_LEDGER_PKI + HAVE_NES_CRYPT + HAVE_ST_AES + NATIVE_LITTLE_ENDIAN + HAVE_CRC + HAVE_HASH + HAVE_RIPEMD160 + HAVE_SHA224 + HAVE_SHA256 + HAVE_SHA3 + HAVE_SHA384 + HAVE_SHA512 + HAVE_SHA512_WITH_BLOCK_ALT_METHOD + HAVE_SHA512_WITH_BLOCK_ALT_METHOD_M0 + HAVE_BLAKE2 + HAVE_HMAC + HAVE_PBKDF2 + HAVE_AES + HAVE_MATH + HAVE_RNG + HAVE_RNG_RFC6979 + HAVE_RNG_SP800_90A + HAVE_ECC + HAVE_ECC_WEIERSTRASS + HAVE_ECC_TWISTED_EDWARDS + HAVE_ECC_MONTGOMERY + HAVE_SECP256K1_CURVE + HAVE_SECP256R1_CURVE + HAVE_SECP384R1_CURVE + HAVE_SECP521R1_CURVE + HAVE_FR256V1_CURVE + HAVE_STARK256_CURVE + HAVE_BRAINPOOL_P256R1_CURVE + HAVE_BRAINPOOL_P256T1_CURVE + HAVE_BRAINPOOL_P320R1_CURVE + HAVE_BRAINPOOL_P320T1_CURVE + HAVE_BRAINPOOL_P384R1_CURVE + HAVE_BRAINPOOL_P384T1_CURVE + HAVE_BRAINPOOL_P512R1_CURVE + HAVE_BRAINPOOL_P512T1_CURVE + HAVE_BLS12_381_G1_CURVE + HAVE_CV25519_CURVE + HAVE_CV448_CURVE + HAVE_ED25519_CURVE + HAVE_ED448_CURVE + HAVE_ECDH + HAVE_ECDSA + HAVE_EDDSA + HAVE_ECSCHNORR + HAVE_X25519 + HAVE_X448 + HAVE_AES_GCM + HAVE_CMAC + HAVE_AES_SIV + COIN_VARIANT=1 + HAVE_BOLOS_APP_STACK_CANARY + IO_SEPROXYHAL_BUFFER_SIZE_B=300 + HAVE_BLE + BLE_COMMAND_TIMEOUT_MS=2000 + HAVE_BLE_APDU + BLE_SEGMENT_SIZE=32 + HAVE_DEBUG_THROWS + NBGL_QRCODE + MAJOR_VERSION=1 + MINOR_VERSION=1 + PATCH_VERSION=0 + IO_HID_EP_LENGTH=64 + HAVE_SPRINTF + HAVE_SNPRINTF_FORMAT_U + HAVE_IO_USB + HAVE_L4_USBLIB + IO_USB_MAX_ENDPOINTS=4 + HAVE_USB_APDU + USB_SEGMENT_SIZE=64 + HAVE_WEBUSB + WEBUSB_URL_SIZE_B=0 + WEBUSB_URL= + OS_IO_SEPROXYHAL + STANDARD_APP_SYNC_RAPDU + HAVE_GENERIC_TX_PARSER + HAVE_TRUSTED_NAME + HAVE_DYN_MEM_ALLOC + HAVE_SWAP + HAVE_ENUM_VALUE + HAVE_NFT_SUPPORT +) +set(DEFINE ${DEFINES} HAVE_PRINTF PRINTF=printf) + +add_compile_definitions(${DEFINES}) + +FILE(GLOB_RECURSE SDK_STD_SOURCES ${BOLOS_SDK}/lib_standard_app/write.c src/mock.c) + + +include_directories( + ${CMAKE_SOURCE_DIR}/../../ethereum-plugin-sdk/src/ + ${CMAKE_SOURCE_DIR}/../../src + ${CMAKE_SOURCE_DIR}/../../src_features/provideDynamicNetwork/ + ${BOLOS_SDK}/include + ${BOLOS_SDK}/lib_standard_app + ${BOLOS_SDK}/target/${TARGET_DEVICE}/include + ${BOLOS_SDK}/lib_cxng/include + ${BOLOS_SDK}/lib_cxng/src + ${BOLOS_SDK}/lib_ux_nbgl + ${BOLOS_SDK}/lib_nbgl/include + ${CMAKE_SOURCE_DIR}/src +) + +FILE(GLOB_RECURSE SOURCES + ${CMAKE_SOURCE_DIR}/../../src_features/provideDynamicNetwork/*.c + ${CMAKE_SOURCE_DIR}/../../src/hash_bytes.c +) + +add_executable(fuzz_app_eth src/fuzz_app_eth.c ${SDK_STD_SOURCES} ${SOURCES}) diff --git a/tests/fuzzing/README.md b/tests/fuzzing/README.md new file mode 100644 index 0000000000..7dc17ab567 --- /dev/null +++ b/tests/fuzzing/README.md @@ -0,0 +1,83 @@ +# Fuzzing Tests + +## Fuzzing + +Fuzzing allows us to test how a program behaves when provided with invalid, unexpected, or random data as input. + +Our fuzz target 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 buffer. +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 + +The fuzzer can run from the docker `ledger-app-builder-legacy`. 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 +``` + +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 +``` + +### Compilation + +Once in the container, go into the `tests/fuzzing` folder to compile the fuzzer: + +```console +cd tests/fuzzing + +# cmake initialization +cmake -DBOLOS_SDK=/opt/ledger-secure-sdk -DCMAKE_C_COMPILER=/usr/bin/clang -Bbuild -H. + +# Fuzzer compilation +make -C build +``` + +### Run + +```console +./build/fuzz_app_eth +``` + +## 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. + +```console +# Prepare directory tree +mkdir tests/fuzzing/{corpus,out} +# Container generation +docker build -t app-ethereum --file .clusterfuzzlite/Dockerfile . +``` + +### Compilation + +```console +docker run --rm --privileged -e FUZZING_LANGUAGE=c -v "$(realpath .)/tests/fuzzing/out:/out" -ti app-ethereum +``` + +### Run + +```console +docker run --rm --privileged -e FUZZING_ENGINE=libfuzzer -e RUN_FUZZER_MODE=interactive -v "$(realpath .)/tests/fuzzing/corpus:/tmp/fuzz_corpus" -v "$(realpath .)/tests/fuzzing/out:/out" -ti gcr.io/oss-fuzz-base/base-runner run_fuzzer fuzz_app_eth +``` diff --git a/tests/fuzzing/src/fuzz_app_eth.c b/tests/fuzzing/src/fuzz_app_eth.c new file mode 100644 index 0000000000..c4f55501aa --- /dev/null +++ b/tests/fuzzing/src/fuzz_app_eth.c @@ -0,0 +1,36 @@ +#include +#include +#include +#include +#include +#include + +#include "shared_context.h" +#include "network_dynamic.h" + +unsigned char G_io_apdu_buffer[IO_APDU_BUFFER_SIZE]; +tmpContent_t tmpContent; +const chain_config_t *chainConfig; +txContext_t txContext; + +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { + explicit_bzero(G_io_apdu_buffer, 500); + explicit_bzero(&tmpContent, sizeof(tmpContent_t)); + explicit_bzero(&txContext, sizeof(txContext_t)); + size_t offset = 0; + size_t len = 0; + uint8_t p1; + uint8_t p2; + unsigned int tx; + + while (size - offset > 4) { + if (data[offset++] == 0) break; + p1 = data[offset++]; + p2 = data[offset++]; + len = data[offset++]; + if (size - offset < len) return 0; + handleNetworkConfiguration(p1, p2, data + offset, len, &tx); + offset += len; + } + return 0; +} diff --git a/tests/fuzzing/src/glyphs.h b/tests/fuzzing/src/glyphs.h new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/fuzzing/src/mock.c b/tests/fuzzing/src/mock.c new file mode 100644 index 0000000000..886990a21b --- /dev/null +++ b/tests/fuzzing/src/mock.c @@ -0,0 +1,93 @@ +#include + +#include "cx_errors.h" +#include "cx_sha256.h" +#include "cx_sha3.h" + +cx_err_t cx_sha256_init_no_throw(cx_sha256_t *hash) { + memset(hash, 0, sizeof(cx_sha256_t)); + return CX_OK; +} + +cx_err_t cx_hash_no_throw(cx_hash_t *hash, + uint32_t mode, + const uint8_t *in, + size_t len, + uint8_t *out, + size_t out_len) { + if (len > 0 && out_len > 0) out[out_len - 1] = in[len - 1]; + return CX_OK; +} + +void assert_exit(bool confirm) { + exit(1); +} + +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]) { + digest[CX_KECCAK_256_SIZE - 1] = 0; + return CX_OK; +} + +cx_err_t cx_sha256_hash_iovec(const cx_iovec_t *iovec, + size_t iovec_len, + uint8_t digest[static CX_SHA256_SIZE]) { + digest[CX_SHA256_SIZE - 1] = 0; + return CX_OK; +} + +int check_signature_with_pubkey(const char *tag, + uint8_t *buffer, + const uint8_t bufLen, + const uint8_t *PubKey, + const uint8_t keyLen, +#ifdef HAVE_LEDGER_PKI + const uint8_t keyUsageExp, +#endif + uint8_t *signature, + const uint8_t sigLen) { + return CX_OK; +} + +uint64_t u64_from_BE(const uint8_t *in, uint8_t size) { + uint8_t i = 0; + uint64_t res = 0; + + while (i < size && i < sizeof(res)) { + res <<= 8; + res |= in[i]; + i++; + } + + return res; +} + +bool u64_to_string(uint64_t src, char *dst, uint8_t dst_size) { + // Copy the numbers in ASCII format. + uint8_t i = 0; + do { + // Checking `i + 1` to make sure we have enough space for '\0'. + if (i + 1 >= dst_size) { + return false; + } + dst[i] = src % 10 + '0'; + src /= 10; + i++; + } while (src); + + // Null terminate string + dst[i] = '\0'; + + // Revert the string + i--; + uint8_t j = 0; + while (j < i) { + char tmp = dst[i]; + dst[i] = dst[j]; + dst[j] = tmp; + i--; + j++; + } + return true; +}