From 09f8a7375105e748a09b11421e95a6b33b03466c Mon Sep 17 00:00:00 2001 From: noproto Date: Sun, 4 Aug 2024 23:53:21 -0400 Subject: [PATCH] Use median calibrated distance, collect parity bits --- .../protocols/mf_classic/mf_classic_poller.c | 249 +++++++++++++----- .../mf_classic/mf_classic_poller_i.h | 5 +- 2 files changed, 191 insertions(+), 63 deletions(-) diff --git a/lib/nfc/protocols/mf_classic/mf_classic_poller.c b/lib/nfc/protocols/mf_classic/mf_classic_poller.c index c0daffe75..3f7f083a7 100644 --- a/lib/nfc/protocols/mf_classic/mf_classic_poller.c +++ b/lib/nfc/protocols/mf_classic/mf_classic_poller.c @@ -889,7 +889,7 @@ NfcCommand mf_classic_poller_handler_nested_analyze_prng(MfClassicPoller* instan MfClassicPollerDictAttackContext* dict_attack_ctx = &instance->mode_ctx.dict_attack_ctx; // Analyze PRNG by collecting nt - uint8_t nonce_limit = 10; + uint8_t nonce_limit = 5; if(dict_attack_ctx->nt_count > 0) { if(!validate_prng_nonce(dict_attack_ctx->nt_prev)) dict_attack_ctx->hard_nt_count++; @@ -900,7 +900,7 @@ NfcCommand mf_classic_poller_handler_nested_analyze_prng(MfClassicPoller* instan return command; } - if(dict_attack_ctx->hard_nt_count > 3) { + if(dict_attack_ctx->hard_nt_count >= 3) { dict_attack_ctx->prng_type = MfClassicPrngTypeHard; // FIXME: E -> D FURI_LOG_E(TAG, "Detected Hard PRNG"); @@ -947,9 +947,162 @@ NfcCommand mf_classic_poller_handler_nested_collect_nt(MfClassicPoller* instance return command; } +void bubble_sort(uint16_t arr[], int n) { + for(int i = 0; i < n - 1; i++) { + for(int j = 0; j < n - i - 1; j++) { + if(arr[j] > arr[j + 1]) { + uint16_t temp = arr[j]; + arr[j] = arr[j + 1]; + arr[j + 1] = temp; + } + } + } +} + +uint16_t get_median(uint16_t arr[], int n) { + bubble_sort(arr, n); + return arr[n / 2]; +} + +NfcCommand mf_classic_poller_handler_nested_calibrate(MfClassicPoller* instance) { + // TODO: Calibrate backdoored tags too + NfcCommand command = NfcCommandContinue; + MfClassicPollerDictAttackContext* dict_attack_ctx = &instance->mode_ctx.dict_attack_ctx; + uint8_t nt_enc_calibration_cnt = 11; + uint32_t nt_enc_temp_arr[nt_enc_calibration_cnt]; + + uint16_t d_min = UINT16_MAX; + uint16_t d_max = 0; + uint16_t d_all[nt_enc_calibration_cnt - 1]; + uint8_t d_all_cnt = 0; + 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); + + MfClassicAuthContext auth_ctx = {}; + MfClassicError error; + + uint32_t nt_prev = 0; + uint32_t nt_enc_prev = 0; + uint32_t same_nt_enc_cnt = 0; + bool static_encrypted = false; + + // 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"); + instance->state = MfClassicPollerStateNestedCalibrate; + return command; + } + + FURI_LOG_E(TAG, "Full authentication successful"); + + nt_prev = bit_lib_bytes_to_num_be(auth_ctx.nt.data, sizeof(MfClassicNt)); + + // Step 2: Perform nested authentication multiple times + for(uint8_t collection_cycle = 0; collection_cycle < nt_enc_calibration_cnt; + collection_cycle++) { + error = mf_classic_poller_auth_nested( + 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 nested authentication %u", collection_cycle); + continue; + } + + uint32_t nt_enc = bit_lib_bytes_to_num_be(auth_ctx.nt.data, sizeof(MfClassicNt)); + if(nt_enc == nt_enc_prev) { + same_nt_enc_cnt++; + if(same_nt_enc_cnt > 3) { + static_encrypted = true; + break; + } + } else { + same_nt_enc_cnt = 0; + nt_enc_prev = nt_enc; + } + nt_enc_temp_arr[collection_cycle] = nt_enc; + } + + if(static_encrypted) { + FURI_LOG_E(TAG, "Static encrypted nonce detected"); + if(dict_attack_ctx->backdoor == MfClassicBackdoorFM11RF08S) { + // TODO: Backdoor static nested attack calibration + dict_attack_ctx->calibrated = true; + instance->state = MfClassicPollerStateNestedController; + return command; + } else { + // TODO: Log these + dict_attack_ctx->calibrated = true; + instance->state = MfClassicPollerStateNestedController; + return command; + } + } + + // Find the distance between each nonce + FURI_LOG_E(TAG, "Calculating distance between nonces"); + uint64_t known_key = bit_lib_bytes_to_num_be(dict_attack_ctx->current_key.data, 6); + Crypto1 crypto; + crypto1_init(&crypto, known_key); + for(uint32_t collection_cycle = 0; collection_cycle < nt_enc_calibration_cnt; + collection_cycle++) { + bool found = false; + for(int i = 0; i < 65535; i++) { + Crypto1 crypto_temp = {.odd = crypto.odd, .even = crypto.even}; + uint32_t nth_successor = prng_successor(nt_prev, i); + if((nth_successor ^ crypto1_word(&crypto_temp, cuid ^ nth_successor, 0)) != + nt_enc_temp_arr[collection_cycle]) { + continue; + } + if(collection_cycle > 0) { + FURI_LOG_E(TAG, "nt_enc (plain) %08lx", nth_successor); + FURI_LOG_E(TAG, "dist from nt prev: %i", i); + d_all[d_all_cnt++] = i; + if(i < d_min) d_min = i; + if(i > d_max) d_max = i; + } + nt_prev = nth_successor; + found = true; + break; + } + if(!found) { + FURI_LOG_E( + TAG, + "Failed to find distance for nt_enc %08lx", + nt_enc_temp_arr[collection_cycle]); + FURI_LOG_E( + TAG, "using key %06llx and uid %08lx, nt_prev is %08lx", known_key, cuid, nt_prev); + } + } + + dict_attack_ctx->d_median = get_median(d_all, d_all_cnt); + dict_attack_ctx->calibrated = true; + instance->state = MfClassicPollerStateNestedController; + + mf_classic_poller_halt(instance); + FURI_LOG_E( + TAG, + "Calibration completed: med=%u min=%u max=%u static=%s", + dict_attack_ctx->d_median, + d_min, + d_max, + (d_min == d_max) ? "true" : "false"); + + return command; +} + NfcCommand mf_classic_poller_handler_nested_collect_nt_enc(MfClassicPoller* instance) { // TODO: Collect parity - // TODO: Use the mode (most frequently observed) value and parity to reliably get a single candidate state + // TODO: Use d_median value and parity to get candidate states. d_median is the center, keep adding +/- 1 until a parity match is found. /* uint32_t ncount = 0; uint32_t nttest = prng_successor(nt1, dmin - 1); @@ -966,11 +1119,10 @@ NfcCommand mf_classic_poller_handler_nested_collect_nt_enc(MfClassicPoller* inst NfcCommand command = NfcCommandContinue; MfClassicPollerDictAttackContext* dict_attack_ctx = &instance->mode_ctx.dict_attack_ctx; MfClassicNestedNonceArray result = {NULL, 0}; - // TODO: Target sector, key A/B do { if(dict_attack_ctx->prng_type == MfClassicPrngTypeHard) { - FURI_LOG_E(TAG, "Hard PRNG, skipping calibration"); + FURI_LOG_E(TAG, "Hard PRNG, skipping"); // TODO: Collect hardnested nonces (cuid, sec, par, nt_enc, A/B) // https://github.com/AloneLiberty/FlipperNested/blob/eba6163d7ef22adef5f9fe4d77a78ccfcc27a952/lib/nested/nested.c#L564-L645 break; @@ -983,8 +1135,10 @@ NfcCommand mf_classic_poller_handler_nested_collect_nt_enc(MfClassicPoller* inst MfClassicAuthContext auth_ctx = {}; MfClassicError error; - const uint32_t rounds = 11; - uint32_t nt_enc_arr[rounds]; + uint8_t nt_enc_per_collection = 2; + uint32_t nt_enc_arr[nt_enc_per_collection]; + uint8_t par_arr[nt_enc_per_collection]; + uint8_t parity = 0; uint32_t nt_prev = 0; uint32_t nt_enc_prev = 0; uint32_t same_nt_enc_cnt = 0; @@ -1007,8 +1161,16 @@ NfcCommand mf_classic_poller_handler_nested_collect_nt_enc(MfClassicPoller* inst nt_prev = bit_lib_bytes_to_num_be(auth_ctx.nt.data, sizeof(MfClassicNt)); - // Step 2: Perform nested authentication multiple times - for(uint32_t round_no = 0; round_no < rounds; round_no++) { + // Step 2: Perform nested authentication a variable number of times to get nt_enc at a different PRNG offset + // eg. Collect most commonly observed nonce from 3 auths to known sector and 4th to target, then separately the + // most commonly observed nonce from 4 auths to known sector and 5th to target. This gets us a nonce pair, + // at a known distance (confirmed by parity bits) telling us the nt_enc plain. + // We also have backup values for the distance in case of rare issues. + //printf("ks: %08x\n", nth_successor ^ nt_enc); + //MfClassicKeyType target_key_type = + // (dict_attack_ctx->nested_target_key % 2 == 0) ? MfClassicKeyTypeA : MfClassicKeyTypeB; + //uint8_t target_block = (4 * (dict_attack_ctx->nested_target_key / 2)) + 3; + for(uint32_t round_no = 0; round_no < nt_enc_per_collection; round_no++) { error = mf_classic_poller_auth_nested( instance, block, @@ -1032,6 +1194,12 @@ NfcCommand mf_classic_poller_handler_nested_collect_nt_enc(MfClassicPoller* inst same_nt_enc_cnt = 0; nt_enc_prev = nt_enc; } + // Collect parity bits + const uint8_t* parity_data = bit_buffer_get_parity(instance->rx_encrypted_buffer); + for(int i = 0; i < 4; i++) { + parity |= ((parity_data[i / 8] >> (i % 8)) & 0x01) << i; + } + par_arr[round_no] = parity; nt_enc_arr[round_no] = nt_enc; } @@ -1057,59 +1225,10 @@ NfcCommand mf_classic_poller_handler_nested_collect_nt_enc(MfClassicPoller* inst } } - // Find the distance between each nonce - // TODO: Distance calculation - // TODO: Avoid repeated calibration - FURI_LOG_E(TAG, "Calibrating distance between nonces"); - // Checking for d_min == 0 is more of a workaround for not having a proper calibration process - if(dict_attack_ctx->d_min == 0) dict_attack_ctx->d_min = UINT16_MAX; - uint64_t known_key = bit_lib_bytes_to_num_be(dict_attack_ctx->current_key.data, 6); - Crypto1 crypto; - crypto1_init(&crypto, known_key); - for(uint32_t rtr = 0; rtr < rounds; rtr++) { - bool found = false; - for(int i = 0; i < 65535; i++) { - Crypto1 crypto_temp = {.odd = crypto.odd, .even = crypto.even}; - uint32_t nth_successor = prng_successor(nt_prev, i); - if((nth_successor ^ crypto1_word(&crypto_temp, cuid ^ nth_successor, 0)) != - nt_enc_arr[rtr]) { - continue; - } - if(rtr > 0) { - FURI_LOG_E(TAG, "nt_enc (plain) %08lx", nth_successor); - FURI_LOG_E(TAG, "dist from nt prev: %i", i); - if(i > dict_attack_ctx->d_max) { - dict_attack_ctx->d_max = i; - } - if(i < dict_attack_ctx->d_min) { - dict_attack_ctx->d_min = i; - } - } - nt_prev = nth_successor; - found = true; - break; - } - if(!found) { - FURI_LOG_E(TAG, "Failed to find distance for nt_enc %08lx", nt_enc_arr[rtr]); - FURI_LOG_E( - TAG, - "using key %06llx and uid %08lx, nt_prev is %08lx", - known_key, - cuid, - nt_prev); - } + for(uint8_t i = 0; i < nt_enc_per_collection; i++) { + FURI_LOG_E(TAG, "nt_enc: %08lx", nt_enc_arr[i]); + FURI_LOG_E(TAG, "parity: %02x", par_arr[i]); } - bool is_static = (dict_attack_ctx->d_min == dict_attack_ctx->d_max); - - FURI_LOG_E( - TAG, - "Calibration completed: min=%u max=%u static: %s", - dict_attack_ctx->d_min, - dict_attack_ctx->d_max, - is_static ? "true" : "false"); - - //printf("ks: %08x\n", nth_successor ^ nt_enc); - //dict_attack_ctx->nested_target_key } while(false); instance->state = MfClassicPollerStateNestedController; @@ -1203,6 +1322,7 @@ NfcCommand mf_classic_poller_handler_nested_log(MfClassicPoller* instance) { NfcCommand mf_classic_poller_handler_nested_controller(MfClassicPoller* instance) { // Iterate through keys + // TODO: Fix infinite loop somewhere NfcCommand command = NfcCommandContinue; MfClassicPollerDictAttackContext* dict_attack_ctx = &instance->mode_ctx.dict_attack_ctx; if(dict_attack_ctx->nested_nonce.count > 0) { @@ -1218,6 +1338,12 @@ NfcCommand mf_classic_poller_handler_nested_controller(MfClassicPoller* instance instance->state = MfClassicPollerStateNestedAnalyzeBackdoor; return command; } + // 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; + return command; + } // Target all sectors, key A and B for(uint8_t key_idx = 0; key_idx <= (instance->sectors_total * 2); key_idx++) { dict_attack_ctx->nested_target_key = key_idx; @@ -1271,6 +1397,7 @@ static const MfClassicPollerReadHandler [MfClassicPollerStateNestedAnalyzePRNG] = mf_classic_poller_handler_nested_analyze_prng, [MfClassicPollerStateNestedAnalyzeBackdoor] = mf_classic_poller_handler_nested_analyze_backdoor, + [MfClassicPollerStateNestedCalibrate] = mf_classic_poller_handler_nested_calibrate, [MfClassicPollerStateNestedCollectNt] = mf_classic_poller_handler_nested_collect_nt, [MfClassicPollerStateNestedController] = mf_classic_poller_handler_nested_controller, [MfClassicPollerStateNestedCollectNtEnc] = mf_classic_poller_handler_nested_collect_nt_enc, 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 d29b92921..f24f3496e 100644 --- a/lib/nfc/protocols/mf_classic/mf_classic_poller_i.h +++ b/lib/nfc/protocols/mf_classic/mf_classic_poller_i.h @@ -82,6 +82,7 @@ typedef enum { // Enhanced dictionary attack states MfClassicPollerStateNestedAnalyzePRNG, MfClassicPollerStateNestedAnalyzeBackdoor, + MfClassicPollerStateNestedCalibrate, MfClassicPollerStateNestedCollectNt, MfClassicPollerStateNestedController, MfClassicPollerStateNestedCollectNtEnc, @@ -119,8 +120,8 @@ typedef struct { uint8_t hard_nt_count; uint8_t nested_target_key; MfClassicNestedNonceArray nested_nonce; - uint16_t d_min; - uint16_t d_max; + bool calibrated; + uint16_t d_median; } MfClassicPollerDictAttackContext; typedef struct {