From 0af28fb221ee43f9371c264ec2487c27e3465a8e Mon Sep 17 00:00:00 2001 From: noproto Date: Mon, 19 Aug 2024 07:33:17 -0400 Subject: [PATCH] Refactor to nested dictionary attack --- .../protocols/mf_classic/mf_classic_poller.c | 388 ++++++++++++------ .../mf_classic/mf_classic_poller_i.h | 26 +- 2 files changed, 279 insertions(+), 135 deletions(-) diff --git a/lib/nfc/protocols/mf_classic/mf_classic_poller.c b/lib/nfc/protocols/mf_classic/mf_classic_poller.c index 23ca4a326..f4c5a6f60 100644 --- a/lib/nfc/protocols/mf_classic/mf_classic_poller.c +++ b/lib/nfc/protocols/mf_classic/mf_classic_poller.c @@ -681,8 +681,9 @@ NfcCommand mf_classic_poller_handler_key_reuse_start(MfClassicPoller* instance) MfClassicPollerDictAttackContext* dict_attack_ctx = &instance->mode_ctx.dict_attack_ctx; do { - if(!dict_attack_ctx->prng_type) { - instance->state = MfClassicPollerStateNestedAnalyzePRNG; + // Nested entrypoint + if(dict_attack_ctx->nested_phase == MfClassicNestedPhaseNone) { + instance->state = MfClassicPollerStateNestedController; break; } @@ -840,13 +841,17 @@ NfcCommand mf_classic_poller_handler_key_reuse_read_sector(MfClassicPoller* inst static bool add_nested_nonce( MfClassicNestedNonceArray* array, uint32_t cuid, - uint8_t key_idx, + uint16_t key_idx, uint32_t nt, uint32_t nt_enc, uint8_t par, uint16_t dist) { - MfClassicNestedNonce* new_nonces = - realloc(array->nonces, (array->count + 1) * sizeof(MfClassicNestedNonce)); + MfClassicNestedNonce* new_nonces; + if(array->count == 0) { + new_nonces = malloc(sizeof(MfClassicNestedNonce)); + } else { + new_nonces = realloc(array->nonces, (array->count + 1) * sizeof(MfClassicNestedNonce)); + } if(new_nonces == NULL) return false; array->nonces = new_nonces; @@ -860,23 +865,34 @@ static bool add_nested_nonce( return true; } +// Helper function to add key candidate to the array +static bool + add_nested_key_candidate(MfClassicNestedKeyCandidateArray* array, MfClassicKey key_candidate) { + MfClassicKey* new_candidates; + if(array->count == 0) { + new_candidates = malloc(sizeof(MfClassicKey)); + } else { + new_candidates = realloc(array->key_candidates, (array->count + 1) * sizeof(MfClassicKey)); + } + if(new_candidates == NULL) return false; + + array->key_candidates = new_candidates; + array->key_candidates[array->count] = key_candidate; + array->count++; + return true; +} + NfcCommand mf_classic_poller_handler_nested_analyze_prng(MfClassicPoller* instance) { NfcCommand command = NfcCommandContinue; MfClassicPollerDictAttackContext* dict_attack_ctx = &instance->mode_ctx.dict_attack_ctx; + uint8_t hard_nt_count = 0; - // Analyze PRNG by collecting nt - uint8_t nonce_limit = 5; - - if(dict_attack_ctx->nt_count > 0) { - if(!is_weak_prng_nonce(dict_attack_ctx->nt_prev)) dict_attack_ctx->hard_nt_count++; + for(uint8_t i = 0; i < dict_attack_ctx->nested_nonce.count; i++) { + MfClassicNestedNonce* nonce = &dict_attack_ctx->nested_nonce.nonces[i]; + if(!is_weak_prng_nonce(nonce->nt)) hard_nt_count++; } - if(dict_attack_ctx->nt_count < nonce_limit) { - instance->state = MfClassicPollerStateNestedCollectNt; - return command; - } - - if(dict_attack_ctx->hard_nt_count >= 3) { + if(hard_nt_count >= MF_CLASSIC_NESTED_HARD_MINIMUM) { dict_attack_ctx->prng_type = MfClassicPrngTypeHard; // FIXME: E -> D FURI_LOG_E(TAG, "Detected Hard PRNG"); @@ -900,7 +916,7 @@ NfcCommand mf_classic_poller_handler_nested_analyze_backdoor(MfClassicPoller* in MfClassicAuthContext auth_ctx = {}; MfClassicNt nt = {}; - MfClassicKey fm11rf08s_backdoor_key = {.data = {0xa3, 0x96, 0xef, 0xa4, 0xe2, 0x4f}}; + MfClassicKey auth2_backdoor_key = {.data = {0xa3, 0x96, 0xef, 0xa4, 0xe2, 0x4f}}; MfClassicError error; Iso14443_3aError iso_error; bool backdoor_found = false; @@ -945,12 +961,12 @@ NfcCommand mf_classic_poller_handler_nested_analyze_backdoor(MfClassicPoller* in bit_buffer_write_bytes(instance->rx_plain_buffer, nt.data, sizeof(MfClassicNt)); uint32_t nt_enc = bit_lib_bytes_to_num_be(nt.data, sizeof(MfClassicNt)); // Ensure the encrypted nt can be generated by the backdoor - uint32_t decrypted_nt_enc = decrypt_nt_enc(cuid, nt_enc, fm11rf08s_backdoor_key); + uint32_t decrypted_nt_enc = decrypt_nt_enc(cuid, nt_enc, auth2_backdoor_key); backdoor_found = is_weak_prng_nonce(decrypted_nt_enc); } while(false); if(backdoor_found) { FURI_LOG_E(TAG, "Backdoor identified"); - dict_attack_ctx->backdoor = MfClassicBackdoorFM11RF08S; + dict_attack_ctx->backdoor = MfClassicBackdoorAuth2; } else { dict_attack_ctx->backdoor = MfClassicBackdoorNone; } @@ -966,7 +982,6 @@ NfcCommand mf_classic_poller_handler_nested_collect_nt(MfClassicPoller* instance MfClassicError error = mf_classic_poller_get_nt(instance, 0, MfClassicKeyTypeA, &nt); if(error != MfClassicErrorNone) { - instance->state = MfClassicPollerStateKeyReuseStart; dict_attack_ctx->prng_type = MfClassicPrngTypeNoTag; // FIXME: E -> D FURI_LOG_E(TAG, "Failed to collect nt"); @@ -974,11 +989,19 @@ NfcCommand mf_classic_poller_handler_nested_collect_nt(MfClassicPoller* instance // FIXME: E -> D FURI_LOG_E(TAG, "nt: %02x%02x%02x%02x", nt.data[0], nt.data[1], nt.data[2], nt.data[3]); uint32_t nt_data = bit_lib_bytes_to_num_be(nt.data, sizeof(MfClassicNt)); - dict_attack_ctx->nt_prev = nt_data; - dict_attack_ctx->nt_count++; - instance->state = MfClassicPollerStateNestedAnalyzePRNG; + if(!add_nested_nonce( + &dict_attack_ctx->nested_nonce, + iso14443_3a_get_cuid(instance->data->iso14443_3a_data), + 0, + nt_data, + 0, + 0, + 0)) { + dict_attack_ctx->prng_type = MfClassicPrngTypeNoTag; + } } + instance->state = MfClassicPollerStateNestedController; return command; } @@ -1057,7 +1080,7 @@ NfcCommand mf_classic_poller_handler_nested_calibrate(MfClassicPoller* instance) if(dict_attack_ctx->static_encrypted) { FURI_LOG_E(TAG, "Static encrypted nonce detected"); - if(dict_attack_ctx->backdoor == MfClassicBackdoorFM11RF08S) { + if(dict_attack_ctx->backdoor == MfClassicBackdoorAuth2) { // TODO: Backdoor static nested attack calibration dict_attack_ctx->calibrated = true; instance->state = MfClassicPollerStateNestedController; @@ -1139,7 +1162,7 @@ NfcCommand mf_classic_poller_handler_nested_collect_nt_enc(MfClassicPoller* inst if(dict_attack_ctx->static_encrypted) { FURI_LOG_E(TAG, "Static encrypted nonce detected"); - if(dict_attack_ctx->backdoor == MfClassicBackdoorFM11RF08S) { + if(dict_attack_ctx->backdoor == MfClassicBackdoorAuth2) { // TODO: Backdoor static nested attack with calibrated distance break; } else { @@ -1300,46 +1323,50 @@ NfcCommand mf_classic_poller_handler_nested_collect_nt_enc(MfClassicPoller* inst return command; } -MfClassicKey* search_dicts_for_weak_nonce_key( +static void search_dicts_for_nonce_key( + MfClassicNestedKeyCandidateArray* key_candidates, + MfClassicNestedNonceArray* nonce_array, KeysDict* system_dict, KeysDict* user_dict, - uint32_t cuid, - uint32_t nt_enc) { + bool is_weak) { MfClassicKey stack_key; - KeysDict* dicts[] = {system_dict, user_dict}; + KeysDict* dicts[] = {user_dict, system_dict}; for(int i = 0; i < 2; i++) { keys_dict_rewind(dicts[i]); while(keys_dict_get_next_key(dicts[i], stack_key.data, sizeof(MfClassicKey))) { - if(is_weak_prng_nonce(decrypt_nt_enc(cuid, nt_enc, stack_key))) { - MfClassicKey* heap_key = malloc(sizeof(MfClassicKey)); - if(heap_key) { - memcpy(heap_key, &stack_key, sizeof(MfClassicKey)); - return heap_key; + bool full_match = true; + for(uint8_t j = 0; j < nonce_array->count; j++) { + // Verify nonce matches encrypted parity bits for all nonces + uint32_t nt_enc_plain = decrypt_nt_enc( + nonce_array->nonces[j].cuid, nonce_array->nonces[j].nt_enc, stack_key); + if(is_weak) { + full_match &= is_weak_prng_nonce(nt_enc_plain); + if(!full_match) break; } - return NULL; // malloc failed + full_match &= nonce_matches_encrypted_parity_bits( + nt_enc_plain, + nt_enc_plain ^ nonce_array->nonces[j].nt_enc, + nonce_array->nonces[j].par); + if(!full_match) break; + } + if(full_match && !add_nested_key_candidate(key_candidates, stack_key)) { + return; // malloc failed } } } - return NULL; // No matching key found + return; } NfcCommand mf_classic_poller_handler_nested_dict_attack(MfClassicPoller* instance) { // TODO: Handle when nonce is not collected (retry counter? Do not increment nested_dict_target_key) // TODO: Look into using MfClassicNt more + // TODO: A method to try the key candidates when we've collected sufficient nonces NfcCommand command = NfcCommandReset; MfClassicPollerDictAttackContext* dict_attack_ctx = &instance->mode_ctx.dict_attack_ctx; do { - if(dict_attack_ctx->prng_type == MfClassicPrngTypeHard) { - // TODO: We can do this by collecting enough nonces (e.g. 10 per key) with the parity bits, decrypt and ensure they - // all match against a known key before trying it. - // Not a failed situation - FURI_LOG_E(TAG, "Hard PRNG, skipping"); - break; - } - uint8_t block = mf_classic_get_first_block_num_of_sector(dict_attack_ctx->reuse_key_sector); uint32_t cuid = iso14443_3a_get_cuid(instance->data->iso14443_3a_data); @@ -1347,72 +1374,114 @@ NfcCommand mf_classic_poller_handler_nested_dict_attack(MfClassicPoller* instanc MfClassicAuthContext auth_ctx = {}; MfClassicError error; - MfClassicKeyType target_key_type = ((dict_attack_ctx->nested_dict_target_key % 2) == 0) ? - MfClassicKeyTypeA : - MfClassicKeyTypeB; - uint8_t target_block = (4 * (dict_attack_ctx->nested_dict_target_key / 2)) + 3; + bool is_weak = dict_attack_ctx->prng_type == MfClassicPrngTypeWeak; + bool is_last_iter_for_hard_key = + ((!is_weak) && ((dict_attack_ctx->nested_target_key % 8) == 7)); + MfClassicKeyType target_key_type = + (((is_weak) && ((dict_attack_ctx->nested_target_key % 2) == 0)) || + ((!is_weak) && ((dict_attack_ctx->nested_target_key % 16) < 8))) ? + MfClassicKeyTypeA : + MfClassicKeyTypeB; + uint8_t target_block = (is_weak) ? (4 * (dict_attack_ctx->nested_target_key / 2)) + 3 : + (4 * (dict_attack_ctx->nested_target_key / 16)) + 3; uint8_t parity = 0; - // Step 1: Perform full authentication once - error = mf_classic_poller_auth( - instance, - block, - &dict_attack_ctx->current_key, - dict_attack_ctx->current_key_type, - &auth_ctx); + if(((is_weak) && (dict_attack_ctx->nested_key_candidates.count == 0)) || + ((!is_weak) && (!is_last_iter_for_hard_key))) { + // Step 1: Perform full authentication once + error = mf_classic_poller_auth( + instance, + block, + &dict_attack_ctx->current_key, + dict_attack_ctx->current_key_type, + &auth_ctx); - if(error != MfClassicErrorNone) { - FURI_LOG_E(TAG, "Failed to perform full authentication"); - dict_attack_ctx->nested_state = MfClassicNestedStateFailed; - break; + if(error != MfClassicErrorNone) { + FURI_LOG_E(TAG, "Failed to perform full authentication"); + dict_attack_ctx->nested_state = MfClassicNestedStateFailed; + break; + } + + FURI_LOG_E(TAG, "Full authentication successful"); + + // Step 2: Collect nested nt and parity + error = mf_classic_poller_auth_nested( + instance, + target_block, + &dict_attack_ctx->current_key, + target_key_type, + &auth_ctx, + true); + + if(error != MfClassicErrorNone) { + FURI_LOG_E(TAG, "Failed to perform nested authentication"); + dict_attack_ctx->nested_state = MfClassicNestedStateFailed; + break; + } + + uint32_t nt_enc = bit_lib_bytes_to_num_be(auth_ctx.nt.data, sizeof(MfClassicNt)); + // Collect parity bits + const uint8_t* parity_data = bit_buffer_get_parity(instance->rx_plain_buffer); + for(int i = 0; i < 4; i++) { + parity = (parity << 1) | (((parity_data[0] >> i) & 0x01) ^ 0x01); + } + + bool success = add_nested_nonce( + &dict_attack_ctx->nested_nonce, + cuid, + dict_attack_ctx->nested_target_key, + 0, + nt_enc, + parity, + 0); + if(!success) { + FURI_LOG_E(TAG, "Failed to add nested nonce to array. OOM?"); + dict_attack_ctx->nested_state = MfClassicNestedStateFailed; + break; + } + + if(!is_weak) { + dict_attack_ctx->nested_state = MfClassicNestedStatePassed; + instance->state = MfClassicPollerStateNestedDictAttack; + return command; + } } - - FURI_LOG_E(TAG, "Full authentication successful"); - - // Step 2: Collect nested nt and parity - error = mf_classic_poller_auth_nested( - instance, - target_block, - &dict_attack_ctx->current_key, - target_key_type, - &auth_ctx, - true); - - // TODO: Check error? If there is one, return MfClassicNestedStateFailed - - uint32_t nt_enc = bit_lib_bytes_to_num_be(auth_ctx.nt.data, sizeof(MfClassicNt)); - // Collect parity bits - const uint8_t* parity_data = bit_buffer_get_parity(instance->rx_plain_buffer); - for(int i = 0; i < 4; i++) { - parity = (parity << 1) | (((parity_data[0] >> i) & 0x01) ^ 0x01); - } - MfClassicKey* found_key = search_dicts_for_weak_nonce_key( - dict_attack_ctx->mf_classic_system_dict, - dict_attack_ctx->mf_classic_user_dict, - cuid, - nt_enc); - if(found_key) { - uint64_t k = bit_lib_bytes_to_num_be(found_key->data, sizeof(MfClassicKey)); - FURI_LOG_E(TAG, "Found key %06llx for nt_enc %08lx", k, nt_enc); - // TODO: Add to found keys in dictionary attack struct - free(found_key); + // If we have sufficient nonces, search the dictionaries for the key + if((is_weak && (dict_attack_ctx->nested_nonce.count == 1)) || + (is_last_iter_for_hard_key && (dict_attack_ctx->nested_nonce.count == 8))) { + // Identify candidate keys (there may be multiple) and validate them to the currently tested sector + // stopping on the first valid key + search_dicts_for_nonce_key( + &dict_attack_ctx->nested_key_candidates, + &dict_attack_ctx->nested_nonce, + dict_attack_ctx->mf_classic_system_dict, + dict_attack_ctx->mf_classic_user_dict, + is_weak); + for(uint8_t i = 0; i < dict_attack_ctx->nested_key_candidates.count; i++) { + FURI_LOG_E( + TAG, + "Found key candidate %06llx", + bit_lib_bytes_to_num_be( + dict_attack_ctx->nested_key_candidates.key_candidates[i].data, + sizeof(MfClassicKey))); + // TODO: Add to found keys in dictionary attack struct ONCE AUTH IS VALIDATED + } + // FIXME + free(dict_attack_ctx->nested_key_candidates.key_candidates); + dict_attack_ctx->nested_key_candidates.key_candidates = NULL; + dict_attack_ctx->nested_key_candidates.count = 0; + free(dict_attack_ctx->nested_nonce.nonces); + dict_attack_ctx->nested_nonce.nonces = NULL; + dict_attack_ctx->nested_nonce.count = 0; } FURI_LOG_E( TAG, - "Target: %u (key type %s, block %u)", + "Target: %u (key type %s, block %u) cuid: %08lx", dict_attack_ctx->nested_target_key, (target_key_type == MfClassicKeyTypeA) ? "A" : "B", - target_block); - FURI_LOG_E(TAG, "cuid: %08lx", cuid); - FURI_LOG_E(TAG, "nt_enc: %08lx", nt_enc); - FURI_LOG_E( - TAG, - "parity: %u%u%u%u", - ((parity >> 3) & 1), - ((parity >> 2) & 1), - ((parity >> 1) & 1), - (parity & 1)); + target_block, + cuid); } while(false); dict_attack_ctx->nested_state = MfClassicNestedStatePassed; @@ -1493,6 +1562,7 @@ NfcCommand mf_classic_poller_handler_nested_log(MfClassicPoller* instance) { furi_assert(params_saved); free(dict_attack_ctx->nested_nonce.nonces); + dict_attack_ctx->nested_nonce.nonces = NULL; dict_attack_ctx->nested_nonce.count = 0; furi_string_free(temp_str); buffered_file_stream_close(stream); @@ -1502,53 +1572,95 @@ NfcCommand mf_classic_poller_handler_nested_log(MfClassicPoller* instance) { return command; } +static bool mf_classic_all_keys_collected(const MfClassicData* data) { + uint8_t total_sectors = mf_classic_get_total_sectors_num(data->type); + + for(uint8_t sector = 0; sector < total_sectors; sector++) { + if(!mf_classic_is_key_found(data, sector, MfClassicKeyTypeA) || + !mf_classic_is_key_found(data, sector, MfClassicKeyTypeB)) { + return false; + } + } + + return true; +} + NfcCommand mf_classic_poller_handler_nested_controller(MfClassicPoller* instance) { // Iterate through keys NfcCommand command = NfcCommandContinue; MfClassicPollerDictAttackContext* dict_attack_ctx = &instance->mode_ctx.dict_attack_ctx; - if((dict_attack_ctx->prng_type == MfClassicPrngTypeWeak) && - (dict_attack_ctx->nested_nonce.count == 2)) { - instance->state = MfClassicPollerStateNestedDictAttack; - return command; - } else if( - (dict_attack_ctx->prng_type == MfClassicPrngTypeHard) && - (dict_attack_ctx->nested_nonce.count > 0)) { - // TODO: Need to think about the meaning of nested_target_key for hard PRNG - instance->state = MfClassicPollerStateNestedLog; - return command; + if(dict_attack_ctx->nested_phase == MfClassicNestedPhaseNone) { + dict_attack_ctx->nested_phase = MfClassicNestedPhaseAnalyzePRNG; } - if(dict_attack_ctx->backdoor == MfClassicBackdoorUnknown) { - instance->state = MfClassicPollerStateNestedAnalyzeBackdoor; - return command; + if(dict_attack_ctx->nested_phase == MfClassicNestedPhaseAnalyzePRNG) { + if(dict_attack_ctx->nested_nonce.count < MF_CLASSIC_NESTED_ANALYZE_NT_COUNT) { + instance->state = MfClassicPollerStateNestedCollectNt; + return command; + } else if( + (dict_attack_ctx->nested_nonce.count == MF_CLASSIC_NESTED_ANALYZE_NT_COUNT) && + (dict_attack_ctx->prng_type == MfClassicPrngTypeUnknown)) { + instance->state = MfClassicPollerStateNestedAnalyzePRNG; + return command; + } else if(dict_attack_ctx->prng_type == MfClassicPrngTypeNoTag) { + FURI_LOG_E(TAG, "No tag detected"); + // Free nonce array + if(dict_attack_ctx->nested_nonce.nonces) { + free(dict_attack_ctx->nested_nonce.nonces); + dict_attack_ctx->nested_nonce.nonces = NULL; + dict_attack_ctx->nested_nonce.count = 0; + } + instance->state = MfClassicPollerStateKeyReuseStart; + return command; + } + if(dict_attack_ctx->nested_nonce.nonces) { + // Free nonce array + free(dict_attack_ctx->nested_nonce.nonces); + dict_attack_ctx->nested_nonce.nonces = NULL; + dict_attack_ctx->nested_nonce.count = 0; + } + dict_attack_ctx->nested_phase = MfClassicNestedPhaseDictAttack; } // Accelerated Nested dictionary attack - if((dict_attack_ctx->prng_type == MfClassicPrngTypeWeak) && - (dict_attack_ctx->nested_dict_target_key <= (instance->sectors_total * 2))) { - if(dict_attack_ctx->nested_dict_target_key == (instance->sectors_total * 2)) { + uint16_t dict_target_key_max = (dict_attack_ctx->prng_type == MfClassicPrngTypeWeak) ? + (instance->sectors_total * 2) : + (instance->sectors_total * 16); + if((dict_attack_ctx->nested_phase == MfClassicNestedPhaseDictAttack) && + (dict_attack_ctx->nested_target_key <= dict_target_key_max)) { + FURI_LOG_E(TAG, "Targeting key %u", dict_attack_ctx->nested_target_key); // DEBUG + if(dict_attack_ctx->nested_target_key == dict_target_key_max) { if(dict_attack_ctx->mf_classic_system_dict) { keys_dict_free(dict_attack_ctx->mf_classic_system_dict); } if(dict_attack_ctx->mf_classic_user_dict) { keys_dict_free(dict_attack_ctx->mf_classic_user_dict); } - dict_attack_ctx->nested_dict_target_key++; + dict_attack_ctx->nested_target_key = 0; + if(mf_classic_all_keys_collected(instance->data)) { + // TODO: Ensure this works + // All keys have been collected, skip to reading blocks + FURI_LOG_E(TAG, "All keys collected and sectors read"); + dict_attack_ctx->nested_phase = MfClassicNestedPhaseFinished; + instance->state = MfClassicPollerStateSuccess; + return command; + } + dict_attack_ctx->nested_phase = MfClassicNestedPhaseCalibrate; instance->state = MfClassicPollerStateNestedController; return command; } if(dict_attack_ctx->nested_state == MfClassicNestedStateFailed) { dict_attack_ctx->attempt_count++; } else if(dict_attack_ctx->nested_state == MfClassicNestedStatePassed) { - dict_attack_ctx->nested_dict_target_key++; + dict_attack_ctx->nested_target_key++; dict_attack_ctx->attempt_count = 0; } dict_attack_ctx->nested_state = MfClassicNestedStateNone; if(dict_attack_ctx->attempt_count >= 3) { // Unpredictable, skip FURI_LOG_E(TAG, "Failed to collect nonce, skipping key"); - dict_attack_ctx->nested_dict_target_key++; + dict_attack_ctx->nested_target_key++; dict_attack_ctx->attempt_count = 0; } - if(dict_attack_ctx->nested_dict_target_key == 0) { + if(dict_attack_ctx->nested_target_key == 0) { // Note: System dict should always exist bool system_dict_exists = keys_dict_check_presence(MF_CLASSIC_NESTED_SYSTEM_DICT_PATH); bool user_dict_exists = keys_dict_check_presence(MF_CLASSIC_NESTED_USER_DICT_PATH); @@ -1568,15 +1680,36 @@ NfcCommand mf_classic_poller_handler_nested_controller(MfClassicPoller* instance instance->state = MfClassicPollerStateNestedDictAttack; return command; } - // TODO: Skip all remaining phases if we have collected all keys - // TODO: Need to think about how this works for Fudan backdoored tags. - // We could reset the .calibration field every sector to re-calibrate. Calibration function handles backdoor calibration too. - if(dict_attack_ctx->prng_type == MfClassicPrngTypeWeak && !dict_attack_ctx->calibrated) { - instance->state = MfClassicPollerStateNestedCalibrate; + // Analyze tag for NXP/Fudan backdoor + if(dict_attack_ctx->backdoor == MfClassicBackdoorUnknown) { + dict_attack_ctx->nested_phase = MfClassicNestedPhaseAnalyzeBackdoor; + instance->state = MfClassicPollerStateNestedAnalyzeBackdoor; return command; } + // TODO: Need to think about how this works for NXP/Fudan backdoored tags. + // We could reset the .calibration field every sector to re-calibrate. Calibration function handles backdoor calibration too. + // Calibration + if(dict_attack_ctx->nested_phase == MfClassicNestedPhaseCalibrate) { + if((dict_attack_ctx->prng_type == MfClassicPrngTypeWeak) && + (!dict_attack_ctx->calibrated)) { + instance->state = MfClassicPollerStateNestedCalibrate; + return command; + } else { + dict_attack_ctx->nested_phase = MfClassicNestedPhaseCollectNtEnc; + } + } + // Log collected nonces + if(dict_attack_ctx->nested_phase == MfClassicNestedPhaseCollectNtEnc) { + if(((dict_attack_ctx->prng_type == MfClassicPrngTypeWeak) && + (dict_attack_ctx->nested_nonce.count == 2)) || + ((dict_attack_ctx->prng_type == MfClassicPrngTypeHard) && + (dict_attack_ctx->nested_nonce.count > 0))) { + instance->state = MfClassicPollerStateNestedLog; + return command; + } + } // Target all sectors, key A and B, first and second nonce - // TODO: Missing weak condition, target_key logic doesn't apply the same to hard + // TODO: Hardnested nonces logic if(dict_attack_ctx->nested_target_key < (instance->sectors_total * 4)) { if(dict_attack_ctx->nested_state == MfClassicNestedStateFailed) { dict_attack_ctx->attempt_count++; @@ -1585,11 +1718,12 @@ NfcCommand mf_classic_poller_handler_nested_controller(MfClassicPoller* instance dict_attack_ctx->attempt_count = 0; } dict_attack_ctx->nested_state = MfClassicNestedStateNone; - if(dict_attack_ctx->attempt_count >= 20) { + if(dict_attack_ctx->attempt_count >= MF_CLASSIC_NESTED_RETRY_MAXIMUM) { // Unpredictable, skip FURI_LOG_E(TAG, "Failed to collect nonce, skipping key"); if(dict_attack_ctx->nested_nonce.nonces) { free(dict_attack_ctx->nested_nonce.nonces); + dict_attack_ctx->nested_nonce.nonces = NULL; dict_attack_ctx->nested_nonce.count = 0; } dict_attack_ctx->nested_target_key += 2; diff --git a/lib/nfc/protocols/mf_classic/mf_classic_poller_i.h b/lib/nfc/protocols/mf_classic/mf_classic_poller_i.h index f86b843cd..8ce3e6bfb 100644 --- a/lib/nfc/protocols/mf_classic/mf_classic_poller_i.h +++ b/lib/nfc/protocols/mf_classic/mf_classic_poller_i.h @@ -16,6 +16,9 @@ extern "C" { #define MF_CLASSIC_FWT_FC (60000) #define NFC_FOLDER EXT_PATH("nfc") #define NFC_ASSETS_FOLDER EXT_PATH("nfc/assets") +#define MF_CLASSIC_NESTED_ANALYZE_NT_COUNT (5) +#define MF_CLASSIC_NESTED_HARD_MINIMUM (3) +#define MF_CLASSIC_NESTED_RETRY_MAXIMUM (20) #define MF_CLASSIC_NESTED_LOGS_FILE_NAME ".nested.log" #define MF_CLASSIC_NESTED_SYSTEM_DICT_FILE_NAME "mf_classic_dict_nested.nfc" #define MF_CLASSIC_NESTED_USER_DICT_FILE_NAME "mf_classic_dict_user_nested.nfc" @@ -41,6 +44,16 @@ typedef enum { MfClassicNestedStatePassed, } MfClassicNestedState; +typedef enum { + MfClassicNestedPhaseNone, + MfClassicNestedPhaseAnalyzePRNG, + MfClassicNestedPhaseDictAttack, + MfClassicNestedPhaseAnalyzeBackdoor, + MfClassicNestedPhaseCalibrate, + MfClassicNestedPhaseCollectNtEnc, + MfClassicNestedPhaseFinished, +} MfClassicNestedPhase; + typedef enum { MfClassicPrngTypeUnknown, // Tag not yet tested MfClassicPrngTypeNoTag, // No tag detected during test @@ -51,7 +64,8 @@ typedef enum { typedef enum { MfClassicBackdoorUnknown, // Tag not yet tested MfClassicBackdoorNone, // No observed backdoor - MfClassicBackdoorFM11RF08S, // Tag responds to Fudan FM11RF08S backdoor (static encrypted nonce tags) + MfClassicBackdoorAuth1, // Tag responds to v1 auth backdoor + MfClassicBackdoorAuth2, // Tag responds to v2 auth backdoor (static encrypted nonce) } MfClassicBackdoor; typedef struct { @@ -125,7 +139,6 @@ typedef struct { MfClassicBlock tag_block; } MfClassicPollerWriteContext; -// TODO: Investigate reducing the number of members of this struct by moving into a separate struct dedicated to nested dict attack typedef struct { uint8_t current_sector; MfClassicKey current_key; @@ -134,14 +147,11 @@ typedef struct { uint16_t current_block; uint8_t reuse_key_sector; // Enhanced dictionary attack and nested nonce collection + MfClassicNestedPhase nested_phase; MfClassicPrngType prng_type; MfClassicBackdoor backdoor; - uint32_t nt_prev; - uint32_t nt_next; - uint8_t nt_count; - uint8_t hard_nt_count; - uint8_t nested_dict_target_key; - uint8_t nested_target_key; + uint16_t nested_target_key; + MfClassicNestedKeyCandidateArray nested_key_candidates; MfClassicNestedNonceArray nested_nonce; bool static_encrypted; bool calibrated;