diff --git a/.clusterfuzzlite/Dockerfile b/.clusterfuzzlite/Dockerfile new file mode 100644 index 000000000..32efe99a5 --- /dev/null +++ b/.clusterfuzzlite/Dockerfile @@ -0,0 +1,17 @@ +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 + +# Add the ethereum-plugin-sdk submodule +RUN git clone https://github.com/LedgerHQ/ethereum-plugin-sdk.git $SRC/app-ethereum/ethereum-plugin-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 000000000..6fc75a929 --- /dev/null +++ b/.clusterfuzzlite/build.sh @@ -0,0 +1,9 @@ +#!/bin/bash -eu + +# build fuzzers + +pushd tests/fuzzing +cmake -DBOLOS_SDK=$(pwd)/../../BOLOS_SDK -B build -S . +cmake --build build +mv ./build/fuzzer "${OUT}" +popd diff --git a/.clusterfuzzlite/project.yaml b/.clusterfuzzlite/project.yaml new file mode 100644 index 000000000..b455aa397 --- /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 000000000..17c1e65a2 --- /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 000000000..09f91dafe --- /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/.gitignore b/.gitignore index 20f25ff19..ed206c628 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,12 @@ __version__.py .vscode .idea + +# Fuzzing +tests/fuzzing/corpus/ +tests/fuzzing/out/ +default.profraw +default.profdata +fuzz-*.log +crash-* +report.html diff --git a/src/shared_context.h b/src/shared_context.h index eaac84fb0..d329d043f 100644 --- a/src/shared_context.h +++ b/src/shared_context.h @@ -16,7 +16,7 @@ #include "nbgl_types.h" #endif -extern void app_exit(); +extern void app_exit(void); extern void common_app_init(void); #define SELECTOR_LENGTH 4 diff --git a/src_features/generic_tx_parser/cmd_tx_info.h b/src_features/generic_tx_parser/cmd_tx_info.h index a093669b4..d8c13442c 100644 --- a/src_features/generic_tx_parser/cmd_tx_info.h +++ b/src_features/generic_tx_parser/cmd_tx_info.h @@ -2,6 +2,7 @@ #define CMD_TX_INFO_H_ #include +#include "gtp_tx_info.h" uint16_t handle_tx_info(uint8_t p1, uint8_t p2, uint8_t lc, const uint8_t *payload); void gcs_cleanup(void); diff --git a/src_features/generic_tx_parser/gtp_param_datetime.c b/src_features/generic_tx_parser/gtp_param_datetime.c index ca3fdade0..2242aded1 100644 --- a/src_features/generic_tx_parser/gtp_param_datetime.c +++ b/src_features/generic_tx_parser/gtp_param_datetime.c @@ -68,7 +68,7 @@ bool format_param_datetime(const s_param_datetime *param, const char *name) { s_parsed_value_collection collec; char *buf = strings.tmp.tmp; size_t buf_size = sizeof(strings.tmp.tmp); - uint8_t time_buf[sizeof(uint32_t)]; + uint8_t time_buf[sizeof(uint32_t)] = {0}; time_t timestamp; uint256_t block_height; diff --git a/src_features/generic_tx_parser/gtp_param_duration.c b/src_features/generic_tx_parser/gtp_param_duration.c index d55a94e58..01eae53a5 100644 --- a/src_features/generic_tx_parser/gtp_param_duration.c +++ b/src_features/generic_tx_parser/gtp_param_duration.c @@ -60,7 +60,7 @@ bool format_param_duration(const s_param_duration *param, const char *name) { uint8_t minutes; uint8_t seconds; uint64_t remaining; - uint8_t raw_buf[sizeof(remaining)]; + uint8_t raw_buf[sizeof(remaining)] = {0}; int off; if (!value_get(¶m->value, &collec)) { diff --git a/src_features/generic_tx_parser/gtp_param_trusted_name.c b/src_features/generic_tx_parser/gtp_param_trusted_name.c index 5fffb9fe2..4c6c27989 100644 --- a/src_features/generic_tx_parser/gtp_param_trusted_name.c +++ b/src_features/generic_tx_parser/gtp_param_trusted_name.c @@ -78,7 +78,7 @@ bool format_param_trusted_name(const s_param_trusted_name *param, const char *na char *buf = strings.tmp.tmp; size_t buf_size = sizeof(strings.tmp.tmp); uint64_t chain_id; - uint8_t addr[ADDRESS_LENGTH]; + uint8_t addr[ADDRESS_LENGTH] = {0}; const char *tname; e_param_type param_type; diff --git a/src_features/generic_tx_parser/gtp_tx_info.c b/src_features/generic_tx_parser/gtp_tx_info.c index 7913ab08d..bb523ff54 100644 --- a/src_features/generic_tx_parser/gtp_tx_info.c +++ b/src_features/generic_tx_parser/gtp_tx_info.c @@ -55,7 +55,7 @@ static bool handle_version(const s_tlv_data *data, s_tx_info_ctx *context) { static bool handle_chain_id(const s_tlv_data *data, s_tx_info_ctx *context) { uint64_t chain_id; - uint8_t buf[sizeof(chain_id)]; + uint8_t buf[sizeof(chain_id)] = {0}; if (data->length > sizeof(buf)) { return false; @@ -72,7 +72,7 @@ static bool handle_chain_id(const s_tlv_data *data, s_tx_info_ctx *context) { } static bool handle_contract_addr(const s_tlv_data *data, s_tx_info_ctx *context) { - uint8_t buf[ADDRESS_LENGTH]; + uint8_t buf[ADDRESS_LENGTH] = {0}; if (data->length > sizeof(buf)) { return false; @@ -165,7 +165,7 @@ static bool handle_contract_name(const s_tlv_data *data, s_tx_info_ctx *context) } static bool handle_deploy_date(const s_tlv_data *data, s_tx_info_ctx *context) { - uint8_t buf[sizeof(uint32_t)]; + uint8_t buf[sizeof(uint32_t)] = {0}; time_t timestamp; if (data->length > sizeof(buf)) { diff --git a/src_features/provideDynamicNetwork/network_dynamic.c b/src_features/provideDynamicNetwork/network_dynamic.c index 3ff7d7c09..45f5474ad 100644 --- a/src_features/provideDynamicNetwork/network_dynamic.c +++ b/src_features/provideDynamicNetwork/network_dynamic.c @@ -288,6 +288,7 @@ static uint16_t parse_icon_buffer(void) { uint8_t digest[CX_SHA256_SIZE]; const uint8_t *data = g_network_icon[g_current_slot].bitmap; const uint16_t field_len = g_icon_payload.received_size; + cx_err_t error = CX_INTERNAL_ERROR; // Check the icon header sw = check_icon_header(data, field_len, &img_len); @@ -298,7 +299,7 @@ static uint16_t parse_icon_buffer(void) { CHECK_FIELD_OVERFLOW("NETWORK_ICON", g_network_icon[g_current_slot].bitmap); // Check icon hash - cx_sha256_hash(data, field_len, digest); + CX_CHECK(cx_sha256_hash(data, field_len, digest)); if (memcmp(digest, g_network_icon[g_current_slot].hash, CX_SHA256_SIZE) != 0) { PRINTF("NETWORK_ICON hash mismatch!\n"); return APDU_RESPONSE_INVALID_DATA; @@ -313,7 +314,9 @@ static uint16_t parse_icon_buffer(void) { DYNAMIC_NETWORK_INFO[g_current_slot].icon.isFile = true; COPY_FIELD(DYNAMIC_NETWORK_INFO[g_current_slot].icon.bitmap); print_icon_info(); - return APDU_RESPONSE_OK; + error = APDU_RESPONSE_OK; +end: + return error; } /** @@ -376,6 +379,12 @@ static uint16_t handle_next_icon_chunk(const uint8_t *data, uint8_t length) { */ static uint16_t handle_icon_chunks(uint8_t p1, const uint8_t *data, uint8_t length) { uint16_t sw = APDU_RESPONSE_UNKNOWN; + uint8_t hash[CX_SHA256_SIZE] = {0}; + + if (memcmp(g_network_icon[g_current_slot].hash, hash, CX_SHA256_SIZE) == 0) { + PRINTF("Error: Icon hash not set!\n"); + return APDU_RESPONSE_INVALID_DATA; + } // Check the received chunk index if (p1 == P1_FIRST_CHUNK) { @@ -432,16 +441,24 @@ static bool verify_signature(s_sig_ctx *sig_ctx) { CX_CHECK( cx_hash_no_throw((cx_hash_t *) &sig_ctx->hash_ctx, CX_LAST, NULL, 0, hash, INT256_LENGTH)); +#ifdef HAVE_LEDGER_PKI CX_CHECK(check_signature_with_pubkey("Dynamic Network", hash, sizeof(hash), LEDGER_SIGNATURE_PUBLIC_KEY, sizeof(LEDGER_SIGNATURE_PUBLIC_KEY), -#ifdef HAVE_LEDGER_PKI CERTIFICATE_PUBLIC_KEY_USAGE_COIN_META, -#endif (uint8_t *) (sig_ctx->sig), sig_ctx->sig_size)); +#else + CX_CHECK(check_signature_with_pubkey("Dynamic Network", + hash, + sizeof(hash), + LEDGER_SIGNATURE_PUBLIC_KEY, + sizeof(LEDGER_SIGNATURE_PUBLIC_KEY), + (uint8_t *) (sig_ctx->sig), + sig_ctx->sig_size)); +#endif ret_code = true; end: diff --git a/src_features/provideTrustedName/cmd_provide_trusted_name.c b/src_features/provideTrustedName/cmd_provide_trusted_name.c index a540c2369..d0015e973 100644 --- a/src_features/provideTrustedName/cmd_provide_trusted_name.c +++ b/src_features/provideTrustedName/cmd_provide_trusted_name.c @@ -822,7 +822,7 @@ static bool parse_tlv(const s_tlv_payload *payload, {.tag = NFT_ID, .func = &handle_nft_id}, }; e_tlv_step step = TLV_TAG; - s_tlv_data data; + s_tlv_data data = {0}; size_t offset = 0; size_t tag_start_off; diff --git a/src_features/provide_enum_value/enum_value.c b/src_features/provide_enum_value/enum_value.c index 64cf2fc30..db0dfb9a1 100644 --- a/src_features/provide_enum_value/enum_value.c +++ b/src_features/provide_enum_value/enum_value.c @@ -28,7 +28,7 @@ static bool handle_version(const s_tlv_data *data, s_enum_value_ctx *context) { } static bool handle_chain_id(const s_tlv_data *data, s_enum_value_ctx *context) { - uint8_t buf[sizeof(context->enum_value.entry.chain_id)]; + uint8_t buf[sizeof(context->enum_value.entry.chain_id)] = {0}; if (data->length > sizeof(buf)) { return false; diff --git a/src_features/signTx/logic_signTx.c b/src_features/signTx/logic_signTx.c index 1f9085346..6d3c304ca 100644 --- a/src_features/signTx/logic_signTx.c +++ b/src_features/signTx/logic_signTx.c @@ -343,7 +343,7 @@ __attribute__((noreturn)) void send_swap_error(uint8_t error_code, const char *str1, const char *str2) { uint32_t tx = 0; - uint len = 0; + size_t len = 0; PRINTF("APDU_RESPONSE_MODE_CHECK_FAILED: 0x%x\n", error_code); // Set RAPDU error codes G_io_apdu_buffer[tx++] = error_code; diff --git a/tests/fuzzing/CMakeLists.txt b/tests/fuzzing/CMakeLists.txt new file mode 100644 index 000000000..f27ae3401 --- /dev/null +++ b/tests/fuzzing/CMakeLists.txt @@ -0,0 +1,218 @@ +cmake_minimum_required(VERSION 3.14) + +# project information +project(EthereumAppFuzzer + VERSION 1.0 + DESCRIPTION "Eth Fuzzer" + LANGUAGES C) + +if (NOT CMAKE_C_COMPILER_ID MATCHES "Clang") + message(FATAL_ERROR "Fuzzer needs to be built with Clang") +endif() + +# guard against bad build-type strings +if (NOT CMAKE_BUILD_TYPE) + set(CMAKE_BUILD_TYPE "Debug") +endif() + +# default fuzz device target +if (NOT TARGET_DEVICE) + set(TARGET_DEVICE "flex") +endif() + +if (NOT DEFINED BOLOS_SDK) + set(BOLOS_SDK /opt/${TARGET_DEVICE}-secure-sdk) +endif() + +# compatible with ClusterFuzzLite +if (NOT DEFINED ENV{LIB_FUZZING_ENGINE}) + set(COMPILATION_FLAGS -g -O0 -Wall -Wextra -fprofile-instr-generate -fcoverage-mapping) + if (SANITIZER MATCHES "address") + set(COMPILATION_FLAGS ${COMPILATION_FLAGS} -fsanitize=fuzzer,address,undefined) + elseif (SANITIZER MATCHES "memory") + set(COMPILATION_FLAGS ${COMPILATION_FLAGS} -fsanitize=fuzzer,memory,undefined -fsanitize-memory-track-origins -fsanitize=fuzzer-no-link) + else() + message(FATAL_ERROR "Unkown sanitizer type. It must be set to `address` or `memory`.") + endif() +else() + set(COMPILATION_FLAGS "$ENV{LIB_FUZZING_ENGINE} $ENV{CFLAGS}") + separate_arguments(COMPILATION_FLAGS) +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() + +set(DEFINES + gcc + APPNAME=\"Fuzzing\" + API_LEVEL=21 + TARGET=\"flex\" + TARGET_NAME=\"TARGET_FUZZ\" + 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 +) + +add_compile_definitions(${DEFINES}) + +FILE( + GLOB_RECURSE SDK_STD_SOURCES + ${BOLOS_SDK}/lib_standard_app/*.c + ${CMAKE_SOURCE_DIR}/../../ethereum-plugin-sdk/src/*.c + ./src/mock.c +) +list( + REMOVE_ITEM SDK_STD_SOURCES + ${BOLOS_SDK}/lib_standard_app/io.c + ${CMAKE_SOURCE_DIR}/../../ethereum-plugin-sdk/src/main.c + ${BOLOS_SDK}/lib_standard_app/main.c + ${BOLOS_SDK}/lib_standard_app/crypto_helpers.c +) + +include_directories( + ${CMAKE_SOURCE_DIR}/../../ethereum-plugin-sdk/src/ + ${CMAKE_SOURCE_DIR}/../../src + ${CMAKE_SOURCE_DIR}/../../src_features/generic_tx_parser/ + ${CMAKE_SOURCE_DIR}/../../src_features/provide_enum_value/ + ${CMAKE_SOURCE_DIR}/../../src_features/provideDynamicNetwork/ + ${CMAKE_SOURCE_DIR}/../../src_features/signTx/ + ${CMAKE_SOURCE_DIR}/../../src_features/provideTrustedName/ + ${CMAKE_SOURCE_DIR}/../../src_features/getChallenge/ + ${CMAKE_SOURCE_DIR}/../../src_features/signMessageEIP712/ + ${BOLOS_SDK}/include + ${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 + ${BOLOS_SDK}/lib_standard_app/ + ${CMAKE_SOURCE_DIR}/src/ +) + +FILE(GLOB_RECURSE SOURCES + ${CMAKE_SOURCE_DIR}/../../src_features/generic_tx_parser/*.c + ${CMAKE_SOURCE_DIR}/../../src_features/provideTrustedName/*.c + ${CMAKE_SOURCE_DIR}/../../src_features/getChallenge/*.c + ${CMAKE_SOURCE_DIR}/../../src_features/provide_enum_value/*.c + ${CMAKE_SOURCE_DIR}/../../src_features/provideDynamicNetwork/*.c + ${CMAKE_SOURCE_DIR}/../../src_features/provideNFTInformation/*.c + ${CMAKE_SOURCE_DIR}/../../src/mem.c + ${CMAKE_SOURCE_DIR}/../../src/mem_utils.c + ${CMAKE_SOURCE_DIR}/../../src/network.c + ${CMAKE_SOURCE_DIR}/../../src/tlv.c + ${CMAKE_SOURCE_DIR}/../../src/tlv_apdu.c + ${CMAKE_SOURCE_DIR}/../../src/uint128.c + ${CMAKE_SOURCE_DIR}/../../src/uint256.c + ${CMAKE_SOURCE_DIR}/../../src/time_format.c + ${CMAKE_SOURCE_DIR}/../../src/uint_common.c + ${CMAKE_SOURCE_DIR}/../../src/utils.c + ${CMAKE_SOURCE_DIR}/../../src/manage_asset_info.c + ${CMAKE_SOURCE_DIR}/../../src/hash_bytes.c +) + +add_executable(fuzzer src/fuzzer.c ${SDK_STD_SOURCES} ${SOURCES}) +target_compile_options(fuzzer PRIVATE ${COMPILATION_FLAGS}) +target_link_options(fuzzer PRIVATE ${COMPILATION_FLAGS}) diff --git a/tests/fuzzing/README.md b/tests/fuzzing/README.md new file mode 100644 index 000000000..89db1e283 --- /dev/null +++ b/tests/fuzzing/README.md @@ -0,0 +1,84 @@ +# 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. + + +## 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 -DSANITIZER=[address|memory] -B build -S . + +# Fuzzer compilation +cmake --build build +``` + +### Run + +```console +./build/fuzzer -max_len=8192 +``` + +If you want to do a fuzzing campain on more than one core and compute the coverage results, you can use the `local_run.sh` script within the container (it'll only run the address and UB sanitizers). + +## 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 fuzzer +``` diff --git a/tests/fuzzing/local_run.sh b/tests/fuzzing/local_run.sh new file mode 100755 index 000000000..cbc179247 --- /dev/null +++ b/tests/fuzzing/local_run.sh @@ -0,0 +1,38 @@ +#!/bin/bash + +# Clean +rm -rf build + +# Build the fuzzer +cmake -B build -S . -DCMAKE_C_COMPILER=/usr/bin/clang -DSANITIZER=address +cmake --build build + +# Create the corpus directory if it doesn't exist +if ! [ -d ./corpus ]; then + mkdir corpus +fi + +# Run the fuzzer on half CPU cores +ncpus=$(nproc) +jobs=$(($ncpus/2)) +echo "The fuzzer will start very soon, press Ctrl-C when you want to stop it and compute the coverage" +./build/fuzzer -max_len=8192 -jobs="$jobs" ./corpus + + +read -p "Would you like to compute coverage (y/n)? " -n 1 -r +echo +if [[ $REPLY =~ ^[Nn]$ ]] +then + exit 0 +fi + +# Remove previous artifcats +rm default.profdata default.profraw + +# Run profiling on the corpus +./build/fuzzer -max_len=8192 -runs=0 ./corpus + +# Compute coverage +llvm-profdata merge -sparse *.profraw -o default.profdata +llvm-cov show build/fuzzer -instr-profile=default.profdata -format=html -ignore-filename-regex='ethereum-plugin-sdk\/|secure-sdk\/' > report.html +llvm-cov report build/fuzzer -instr-profile=default.profdata -ignore-filename-regex='ethereum-plugin-sdk\/|secure-sdk\/' diff --git a/tests/fuzzing/src/fuzzer.c b/tests/fuzzing/src/fuzzer.c new file mode 100644 index 000000000..3b024c80b --- /dev/null +++ b/tests/fuzzing/src/fuzzer.c @@ -0,0 +1,140 @@ +#include +#include + +#include "network_dynamic.h" + +#include "cmd_field.h" +#include "cmd_tx_info.h" +#include "cmd_enum_value.h" + +#include "gtp_field.h" +#include "gtp_tx_info.h" +#include "enum_value.h" + +#include "shared_context.h" +#include "tlv.h" +#include "apdu_constants.h" + +// Fuzzing harness interface +typedef int (*harness)(const uint8_t *data, size_t size); + +// Global state required by the app features +cx_sha3_t sha3; +unsigned char G_io_apdu_buffer[260]; +tmpContent_t tmpContent; +txContext_t txContext; +txContent_t txContent; +chain_config_t config = { + .coinName = "FUZZ", + .chainId = 0x42, +}; +const chain_config_t *chainConfig = &config; +uint8_t appState; +tmpCtx_t tmpCtx; +strings_t strings; + +int fuzzGenericParserFieldCmd(const uint8_t *data, size_t size) { + s_field field = {0}; + s_field_ctx ctx = {0}; + ctx.field = &field; + + if (!tlv_parse(data, size, (f_tlv_data_handler) &handle_field_struct, &ctx)) return 1; + + if (!verify_field_struct(&ctx)) return 1; + + return format_field(&field); +} + +int fuzzGenericParserTxInfoCmd(const uint8_t *data, size_t size) { + s_tx_info tx_info = {0}; + s_tx_info_ctx ctx = {0}; + ctx.tx_info = &tx_info; + + if (!tlv_parse(data, size, (f_tlv_data_handler) &handle_tx_info_struct, &ctx)) return 1; + + return verify_tx_info_struct(&ctx); +} + +int fuzzGenericParserEnumCmd(const uint8_t *data, size_t size) { + s_enum_value_ctx ctx = {0}; + + if (!tlv_parse(data, size, (f_tlv_data_handler) &handle_enum_value_struct, &ctx)) return 1; + + return verify_enum_value_struct(&ctx); +} + +int fuzzDynamicNetworks(const uint8_t *data, size_t size) { + 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; + if (handleNetworkConfiguration(p1, p2, data + offset, len, &tx) != APDU_RESPONSE_OK) + return 1; + offset += len; + } + return 0; +} + +int fuzzTrustedNames(const uint8_t *data, size_t size) { + size_t offset = 0; + size_t len = 0; + uint8_t p1; + + while (size - offset > 3) { + if (data[offset++] == 0) break; + p1 = data[offset++]; + len = data[offset++]; + if (size - offset < len) return 0; + if (handle_provide_trusted_name(p1, data + offset, len) != APDU_RESPONSE_OK) return 1; + offset += len; + } + return 0; +} + +int fuzzNFTInfo(const uint8_t *data, size_t size) { + unsigned int tx; + return handleProvideNFTInformation(data, size, &tx) != APDU_RESPONSE_OK; +} + +// Array of fuzzing harness functions +harness harnesses[] = { + fuzzGenericParserFieldCmd, + fuzzGenericParserTxInfoCmd, + fuzzGenericParserEnumCmd, + fuzzDynamicNetworks, + fuzzTrustedNames, + fuzzNFTInfo, +}; + +/* Main fuzzing handler called by libfuzzer */ +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { + // Clear global structures to ensure a clean state for each fuzzing iteration + explicit_bzero(&tmpContent, sizeof(tmpContent_t)); + explicit_bzero(&txContext, sizeof(txContext_t)); + explicit_bzero(&txContent, sizeof(txContent_t)); + explicit_bzero(&tmpCtx, sizeof(tmpCtx_t)); + explicit_bzero(&strings, sizeof(strings_t)); + explicit_bzero(&G_io_apdu_buffer, 260); + explicit_bzero(&sha3, sizeof(sha3)); + + uint8_t target; + + txContext.content = &txContent; + txContext.sha3 = &sha3; + + // Determine which harness function to call based on the first byte of data + if (size < 1) return 0; + target = data[0]; + if (target >= sizeof(harnesses) / sizeof(harnesses[0])) return 0; + + // Call the selected harness function with the remaining data (which can be of size 0) + return harnesses[target](++data, --size); +} diff --git a/tests/fuzzing/src/glyphs.h b/tests/fuzzing/src/glyphs.h new file mode 100644 index 000000000..e69de29bb diff --git a/tests/fuzzing/src/mock.c b/tests/fuzzing/src/mock.c new file mode 100644 index 000000000..cfcd65c83 --- /dev/null +++ b/tests/fuzzing/src/mock.c @@ -0,0 +1,169 @@ +#include +#include + +#include "cx_errors.h" +#include "cx_sha256.h" +#include "cx_sha3.h" + +/** MemorySanitizer does not wrap explicit_bzero https://github.com/google/sanitizers/issues/1507 + * which results in false positives when running MemorySanitizer. + */ +void memset_s(void *buffer, char c, size_t n) { + if (buffer == NULL) return; + + volatile char *ptr = buffer; + while (n--) *ptr++ = c; +} + +size_t strlcpy(char *dst, const char *src, size_t size) { + const char *s = src; + size_t n = size; + + if (n != 0) { + while (--n != 0) { + if ((*dst++ = *s++) == '\0') { + break; + } + } + } + + if (n == 0) { + if (size != 0) { + *dst = '\0'; + } + while (*s++) + ; + } + + return (s - src - 1); +} + +size_t strlcat(char *dst, const char *src, size_t size) { + char *d = dst; + const char *s = src; + size_t n = size; + size_t dsize; + + while (n-- != 0 && *d != '\0') { + d++; + } + dsize = d - dst; + n = size - dsize; + + if (n == 0) { + return (dsize + strlen(s)); + } + + while (*s != '\0') { + if (n != 1) { + *d++ = *s; + n--; + } + s++; + } + *d = '\0'; + + return (dsize + (s - src)); +} + +cx_err_t cx_sha256_init_no_throw(cx_sha256_t *hash) { + memset_s(hash, 0, sizeof(cx_sha256_t)); + return CX_OK; +} + +cx_err_t cx_sha3_init_no_throw(cx_sha3_t *hash PLENGTH(sizeof(cx_sha3_t)), size_t size) { + UNUSED(size); + memset_s(hash, 0, sizeof(cx_sha3_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) { + UNUSED(hash); + UNUSED(mode); + memset_s(out, 0, out_len); // let's initialize the buffer + // if arrays are not empty, read the last element of in and write it in the last element of out + if (len > 0 && out_len > 0) out[out_len - 1] = in[len - 1]; + return CX_OK; +} + +void assert_exit(bool confirm) { + UNUSED(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]) { + UNUSED(iovec); + UNUSED(iovec_len); + memset_s(digest, 0, CX_SHA256_SIZE); + 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]) { + UNUSED(iovec); + UNUSED(iovec_len); + memset_s(digest, 0, CX_SHA256_SIZE); + 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) { + UNUSED(tag); + UNUSED(buffer); + UNUSED(bufLen); + UNUSED(PubKey); +#ifdef HAVE_LEDGER_PKI + UNUSED(keyUsageExp); +#endif + UNUSED(keyLen); + UNUSED(signature); + UNUSED(sigLen); + return CX_OK; +} + +void *pic(void *addr) { + return addr; +} + +cx_err_t cx_math_mult_no_throw(uint8_t *r, const uint8_t *a, const uint8_t *b, size_t len) { + UNUSED(r); + UNUSED(a); + UNUSED(b); + UNUSED(len); + return CX_OK; +} + +void cx_rng_no_throw(uint8_t *buffer, size_t len) { + memset_s(buffer, 0, len); +} + +uint16_t get_public_key(uint8_t *out, uint8_t outLength) { + memset_s(out, 0, outLength); + return 0; +} + +void ui_gcs_cleanup(void) { +} + +size_t cx_hash_sha256(const uint8_t *in, size_t in_len, uint8_t *out, size_t out_len) { + memset_s(out, 0, out_len); // let's initialize the buffer + // if arrays are not empty, read the last element of in and write it in the last element of out + if (in_len > 0 && out_len > 0) out[out_len - 1] = in[in_len - 1]; + return CX_OK; +}