Skip to content

Commit

Permalink
[core] rewrite fuzzing logic
Browse files Browse the repository at this point in the history
Rewrite the bit-rotted fuzzing code. Summary of changes:
1. Fuzzing is now enabled similar to sanitizers, with -DENABLE_FUZZER=ON parameter to cmake
2. When fuzzing is enabled, the output binary is `subzero_fuzz` rather than `subzero`.
3. When fuzzing is enabled, all log output is suppressed, per recommendation from libfuzzer docs.
4. When fuzzing is enabled, debug symbols are included, per recommendation from libfuzzer docs.
5. When fuzzing is enabled, -O1 optimization level is used, per recommendation from libfuzzer docs.
6. When fuzzing is enabled, QR signature check failures are ignored, so we can fuzz the post-signature-check code paths.
7. When fuzzing is enabled, ignore AES-GCM decryption errors, so we can fuzz the post-decryption code paths.
8. Added new options --generate-fuzzing-corpus and --fuzzing-corpus-output-dir to the GUI.
   These can be used to generate an initial fuzzing corpus (together with --signtx-test and --generate-wallet-files-test).
9. Fixed a bug with --generate-wallet-files-test - it needs to initialize screens in order to work.
10. Added a new fuzz testing section to documentation page.

Potential future work:
- use google's libprotobuf-mutator for structure-aware fuzzing
  • Loading branch information
ivmaykov committed Jul 13, 2023
1 parent 210e00e commit e177a62
Show file tree
Hide file tree
Showing 15 changed files with 372 additions and 112 deletions.
1 change: 1 addition & 0 deletions core/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@
cmake-build-*
build/**
venv/**
fuzzing_corpus
44 changes: 36 additions & 8 deletions core/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ option(ENABLE_ASAN "Enable Address Sanitizer" OFF) # enable with -DENABLE_ASAN=O
option(ENABLE_MSAN "Enable Uninitialized Memory Sanitizer" OFF) # enable with -DENABLE_MSAN=ON
option(ENABLE_UBSAN "Enable Undefined Behavior Sanitizer" OFF) # enable with -DENABLE_UBSAN=ON
option(ENABLE_COVERAGE "Enable code coverage" OFF) # enable with -DENABLE_COVERAGE=ON
option(ENABLE_FUZZER "Enable fuzzer via LLVM's libfuzzer" OFF) # enable with -DENABLE_FUZZER=ON

if ($ENV{TARGET} MATCHES "nCipher")
message("Building for nCipher(powerpc32) architecture.")
Expand All @@ -21,7 +22,7 @@ if ($ENV{TARGET} MATCHES "nCipher")
endif ()
elseif($ENV{TARGET} MATCHES "dev")
message("Building for the host architecture")
else()
else ()
message(FATAL_ERROR "Unsupported TARGET value.")
endif ()

Expand Down Expand Up @@ -53,8 +54,33 @@ if (ENABLE_UBSAN)
list(APPEND FSANITIZE "undefined")
list(APPEND EXTRA_SANITIZER_FLAGS "-fno-sanitize-recover=undefined")
endif ()

if (ENABLE_ASAN OR ENABLE_MSAN OR ENABLE_UBSAN)

if (ENABLE_FUZZER)
# Note that Apple does NOT ship a full Clang toolchain on MacOS (tested on 13.4.1)!
# In order to get libfuzzer to link properly, you need to install the full LLVM toolchain
# with homebrew, then switch your environment to use the homebrew versions of clang for
# building and re-run cmake. It's not recommended to do this globally, as using a 3rd party
# clang for everything can break your system, so just do it in a single shell - consider
# writing a script which does this.
# On my machine this looks like:
# export PATH="/opt/homebrew/opt/llvm/bin:$PATH"
# export LDFLAGS="-L/opt/homebrew/opt/llvm/lib/c++ -Wl,-rpath,/opt/homebrew/opt/llvm/lib/c++ -L/opt/homebrew/opt/llvm/lib"
# export CFLAGS="-I/opt/homebrew/opt/llvm/include"
# export CPPFLAGS="-I/opt/homebrew/opt/llvm/include"
# export CXXFLAGS="-I/opt/homebrew/opt/llvm/include"
# export CC=`which clang`
# export CXX=`which clang`
if (NOT CMAKE_C_COMPILER_ID MATCHES "Clang")
message(FATAL_ERROR "LLVM's libfuzzer requires non-Apple Clang")
endif ()
message(STATUS "Enabling fuzzing via LLVM's libfuzzer")
list(APPEND FSANITIZE "fuzzer")
list(APPEND EXTRA_SANITIZER_FLAGS "-g") # fuzzing needs debug symbols
list(APPEND EXTRA_SANITIZER_FLAGS "-O1") # make fuzzer faster
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DFUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION")
endif ()

if (ENABLE_ASAN OR ENABLE_MSAN OR ENABLE_UBSAN OR ENABLE_FUZZER)
string(REPLACE ";" "," FSANITIZE_STR "${FSANITIZE}")
add_compile_options(-fsanitize=${FSANITIZE_STR} ${EXTRA_SANITIZER_FLAGS})
add_link_options(-fsanitize=${FSANITIZE_STR})
Expand All @@ -81,7 +107,7 @@ if (ENABLE_COVERAGE)
# globally, as using a 3rd party clang for everything can break your system, so just do
# it in a single shell - consider writing a script which does this.
# On my machine this looks like:
# export PATH="/opt/homebrew/opt/llvm/bin:/opt/homebrew/opt/libiconv/bin:$PATH"
# export PATH="/opt/homebrew/opt/llvm/bin:$PATH"
# export LDFLAGS="-L/opt/homebrew/opt/llvm/lib/c++ -Wl,-rpath,/opt/homebrew/opt/llvm/lib/c++ -L/opt/homebrew/opt/llvm/lib"
# export CFLAGS="-I/opt/homebrew/opt/llvm/include"
# export CPPFLAGS="-I/opt/homebrew/opt/llvm/include"
Expand Down Expand Up @@ -233,16 +259,18 @@ else ()

endif ()

if ($ENV{TARGET} MATCHES "fuzz")
if (ENABLE_FUZZER)
list(APPEND main_SRC "src/fuzz.c")
set(BINARY_NAME subzero_fuzzer)
else ()
list(APPEND main_SRC "src/main.c")
set(BINARY_NAME subzero)
endif ()

add_executable(subzero ${main_SRC})
add_executable(${BINARY_NAME} ${main_SRC})

if ($ENV{TARGET} MATCHES "nCipher")
target_link_libraries(subzero TrezorCrypto SubzeroProtos)
target_link_libraries(${BINARY_NAME} TrezorCrypto SubzeroProtos)
else()
target_link_libraries(subzero TrezorCrypto SubzeroProtos aes_gcm_dev GladmanGCM)
target_link_libraries(${BINARY_NAME} TrezorCrypto SubzeroProtos aes_gcm_dev GladmanGCM)
endif ()
72 changes: 40 additions & 32 deletions core/include/log.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,42 +5,50 @@
// Part 2/2 of magic to drop the full path from __FILE__
#define __FILENAME__ (&__FILE__[SOURCE_PATH_SIZE])

#ifdef BTC_TESTNET
// Print DEBUG to stdout in cyan
#define DEBUG(...) \
do { \
printf("\033[0;36m"); \
printf("[DEBUG] %s:%d ", __FILENAME__, __LINE__); \
printf(__VA_ARGS__); \
printf("\033[0m\n"); \
} while (0)
#ifndef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION
#ifdef BTC_TESTNET
// Print DEBUG to stdout in cyan
#define DEBUG(...) \
do { \
printf("\033[0;36m"); \
printf("[DEBUG] %s:%d ", __FILENAME__, __LINE__); \
printf(__VA_ARGS__); \
printf("\033[0m\n"); \
} while (0)

#define DEBUG_(...) \
do { \
printf("\033[0;36m"); \
printf(__VA_ARGS__); \
printf("\033[0m"); \
} while (0)

#else
#define DEBUG(...) do {snprintf(NULL, 0, __VA_ARGS__);} while(0)
#define DEBUG_(...) do {snprintf(NULL, 0, __VA_ARGS__);} while(0)
#endif

#define DEBUG_(...) \
do { \
printf("\033[0;36m"); \
printf(__VA_ARGS__); \
printf("\033[0m"); \
// Print INFO to stdout in green
#define INFO(...) \
do { \
printf("\033[0;32m"); \
printf("[INFO] %s:%d ", __FILENAME__, __LINE__); \
printf(__VA_ARGS__); \
printf("\033[0m\n"); \
} while (0)

// Print ERROR to stdout in red
#define ERROR(...) \
do { \
printf("\033[0;31m"); \
printf("[ERROR] %s:%d ", __FILENAME__, __LINE__); \
printf(__VA_ARGS__); \
printf("\033[0m\n"); \
} while (0)
#else
// When fuzzing, suppress all log output
#define DEBUG(...) do {snprintf(NULL, 0, __VA_ARGS__);} while(0)
#define DEBUG_(...) do {snprintf(NULL, 0, __VA_ARGS__);} while(0)
#define INFO(...) do {snprintf(NULL, 0, __VA_ARGS__);} while(0)
#define ERROR(...) do {snprintf(NULL, 0, __VA_ARGS__);} while(0)
#endif

// Print INFO to stdout in green
#define INFO(...) \
do { \
printf("\033[0;32m"); \
printf("[INFO] %s:%d ", __FILENAME__, __LINE__); \
printf(__VA_ARGS__); \
printf("\033[0m\n"); \
} while (0)

// Print ERROR to stdout in red
#define ERROR(...) \
do { \
printf("\033[0;31m"); \
printf("[ERROR] %s:%d ", __FILENAME__, __LINE__); \
printf(__VA_ARGS__); \
printf("\033[0m\n"); \
} while (0)
17 changes: 17 additions & 0 deletions core/src/dev/aes_gcm.c
Original file line number Diff line number Diff line change
Expand Up @@ -180,11 +180,28 @@ Result aes_gcm_decrypt(M_KeyID keyId, const uint8_t *ciphertext, size_t cipherte
tag, sizeof(tag),
ctx))
{
#ifndef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION
ERROR("gcm_decrypt_message failed");
memzero(aes_gcm_buffer, sizeof(aes_gcm_buffer));
memzero(iv, sizeof(iv));
memzero(tag, sizeof(tag));
return Result_UNKNOWN_INTERNAL_FAILURE;
#else
// Suppress decryption errors when fuzzing is enabled,
// so we can hit the post-decryption code paths with the fuzzer.
// This will happen when the fuzzer bit-flips the IV, tag, and/or ciphertext.
// By suppressing the error we will just get random values for the master seed
// and/or pubkey.
gcm_end(ctx);
memcpy(plaintext, aes_gcm_buffer, expected_plaintext_len);

*bytes_written = expected_plaintext_len;
memzero(aes_gcm_buffer, sizeof(aes_gcm_buffer));
memzero(iv, sizeof(iv));
memzero(tag, sizeof(tag));

return Result_SUCCESS;
#endif
}

if (RETURN_GOOD != gcm_end(ctx))
Expand Down
54 changes: 6 additions & 48 deletions core/src/fuzz.c
Original file line number Diff line number Diff line change
Expand Up @@ -10,57 +10,15 @@
#include <sys/stat.h>
#include <unistd.h>

/* This is useful for debugging the fuzz driver. from nanopb docs. */
static bool fwrite_callback(pb_ostream_t *stream, const uint8_t *buf, size_t count) {
FILE *file = (FILE*) stream->state;
return fwrite(buf, 1, count, file) == count;
}

/**
* Entry point for AFL fuzzing
* It takes a single argument: A file to read with an InternalCommandRequest
* protobuf. It does not write the output anywhere.
*/
int main(int argc, char **argv) {
if (argc != 2) {
printf("usage: fuzz <input>\n");
return -1;
}

int fd = open(argv[1], O_RDONLY);
if (fd < 0) {
printf("open %s failed: %s\n", argv[1], strerror(errno));
return -2;
}

struct stat s;
if (fstat(fd, &s) < 0) {
printf("stat %s failed: %s\n", argv[1], strerror(errno));
return -3;
}
extern int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size);

void* buf = malloc(s.st_size);
if(buf == NULL) {
printf("malloc %lld bytes failed\n", s.st_size);
return -4;
}
static uint8_t response_buf[4096] = { 0 };

ssize_t count = read(fd, buf, s.st_size);
if(count != s.st_size) {
printf("Failed to read full amount: %zd is not expected %lld\n", count, s.st_size);
return -5;
}

pb_istream_t istream = pb_istream_from_buffer(buf, s.st_size);

/* Callback may be null to drop the output */
//pb_ostream_t ostream = {0, 0, SIZE_MAX, 0, 0};

pb_ostream_t ostream = {&fwrite_callback, stderr, SIZE_MAX, 0, 0};
int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
pb_istream_t istream = pb_istream_from_buffer(data, size);
pb_ostream_t ostream = pb_ostream_from_buffer(response_buf, sizeof(response_buf));

handle_incoming_message(&istream, &ostream);

printf("output size %zu\n", ostream.bytes_written);

return 0;
return 0; // Values other than 0 and -1 are reserved for future use.
}
2 changes: 2 additions & 0 deletions core/src/protect.c
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ Result protect_pubkey(char xpub[static XPUB_SIZE],
EncryptedPubKey *encrypted_pub_key) {
// Insert magic string for binary static analysis.
// This is extremely hacky, but works. ¯\_(ツ)_/¯
#ifndef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION
printf(MAGIC);
#endif

if (NULL == encrypted_pub_key) {
ERROR("%s: null encrypted_pub_key input", __func__);
Expand Down
24 changes: 16 additions & 8 deletions core/src/qrsignatures.c
Original file line number Diff line number Diff line change
Expand Up @@ -27,29 +27,30 @@ bool check_qrsignature_pub(
const size_t signature_len,
const uint8_t* const pubkey,
const size_t pubkey_len) {
bool result = false;
if (data_len == 0) {
ERROR("Input length is zero.");
return false;
goto out;
}
if (data == NULL) {
ERROR("Input data is null.");
return false;
goto out;
}
if (signature_len != QRSIGNATURE_LEN) {
ERROR("%s: signature_len is: %zu, expected: %zu", __func__, signature_len, QRSIGNATURE_LEN);
return false;
goto out;
}
if (signature == NULL) {
ERROR("Signature is null");
return false;
goto out;
}
if (pubkey_len != QRSIGNATURE_PUBKEY_LEN) {
ERROR("%s: pubkey_len is: %zu, expected: %zu", __func__, pubkey_len, QRSIGNATURE_PUBKEY_LEN);
return false;
goto out;
}
if (pubkey == NULL) {
ERROR("pub is null.");
return false;
goto out;
}

int result_verify = ecdsa_verify(
Expand All @@ -60,10 +61,17 @@ bool check_qrsignature_pub(
data,
(uint32_t)data_len);

if(result_verify == 0){
if (result_verify == 0) {
result = true;
DEBUG("QR signature verification successful.");
} else {
DEBUG("QR signature verification failed.");
}
return (result_verify == 0)? true: false;

out:
#ifdef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION
// When fuzzing, accept all invalid QR signatures
result = true;
#endif
return result;
}
Loading

0 comments on commit e177a62

Please sign in to comment.