diff --git a/CHANGELOG.md b/CHANGELOG.md index 33aa5b7c1..f1f5bf4bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -66,6 +66,7 @@ - Dictionary attack: Uses system and user dictionaries stored under /nfc/assets/ to unlock Ultralight C tags - Key management: Extra Actions → MIFARE Ultralight C Keys in the NFC app allows you to add, list, and remove Ultralight C keys from your Flipper - UI: Dictionary attack scene and menu options + - XERO: Support for MFKey 4.0, MIFARE Classic Static Encrypted Nested attacks run 10x faster (by @noproto) - OFW: FeliCa Service Directory Traverse + Dump All Unencrypted-Readable Services' Blocks (by @zinongli) - OFW: FeliCa Emulation Handle certain Polling commands in firmware (by @dogtopus) - OFW: FeliCa Dump All Systems (by @zinongli) @@ -178,6 +179,7 @@ - OFW: Fix demo_windows.txt for newer version of ai enabled Windows Notepad not able to keep up with default fast input text (by @ase1590) - Desktop: Fix lock screen hang (#438 by @aaronjamt) - NFC: + - XERO: Keys found in key cache are now used in Nested attacks, deleting key cache is no longer required (by @noproto) - Fix incorrect Saflok year formula (#433 by @Eltrick) - Fix read crash with unexpectedly large MFC AUTH(0) response, eg with Chameleon Ultra NTAG emualtion (by @WillyJL) - Fix slashes in prefilled filename (by @WillyJL) diff --git a/applications/main/nfc/nfc_app_i.h b/applications/main/nfc/nfc_app_i.h index 4547fff2b..06bf759c6 100644 --- a/applications/main/nfc/nfc_app_i.h +++ b/applications/main/nfc/nfc_app_i.h @@ -113,6 +113,8 @@ typedef struct { uint16_t nested_target_key; uint16_t msb_count; bool enhanced_dict; + uint16_t current_key_idx; // Current key index for CUID dictionary mode + uint8_t* cuid_key_indices_bitmap; // Bitmap of key indices present in CUID dictionary (256 bits = 32 bytes) } NfcMfClassicDictAttackContext; typedef struct { diff --git a/applications/main/nfc/scenes/nfc_scene_mf_classic_dict_attack.c b/applications/main/nfc/scenes/nfc_scene_mf_classic_dict_attack.c index 500dd759a..fa55ccf74 100644 --- a/applications/main/nfc/scenes/nfc_scene_mf_classic_dict_attack.c +++ b/applications/main/nfc/scenes/nfc_scene_mf_classic_dict_attack.c @@ -2,12 +2,22 @@ #include #include +#include #define TAG "NfcMfClassicDictAttack" +#define BIT(x, n) ((x) >> (n) & 1) // TODO FL-3926: Fix lag when leaving the dictionary attack view after Hardnested // TODO FL-3926: Re-enters backdoor detection between user and system dictionary if no backdoor is found +// KeysDict structure definition for inline CUID dictionary allocation +struct KeysDict { + Stream* stream; + size_t key_size; + size_t key_size_symbols; + size_t total_keys; +}; + typedef enum { DictAttackStateCUIDDictInProgress, DictAttackStateUserDictInProgress, @@ -31,11 +41,22 @@ NfcCommand nfc_dict_attack_worker_callback(NfcGenericEvent event, void* context) instance->nfc_dict_context.is_card_present = false; view_dispatcher_send_custom_event(instance->view_dispatcher, NfcCustomEventCardLost); } else if(mfc_event->type == MfClassicPollerEventTypeRequestMode) { + uint32_t state = + scene_manager_get_scene_state(instance->scene_manager, NfcSceneMfClassicDictAttack); + bool is_cuid_dict = (state == DictAttackStateCUIDDictInProgress); + const MfClassicData* mfc_data = nfc_device_get_data(instance->nfc_device, NfcProtocolMfClassic); - mfc_event->data->poller_mode.mode = (instance->nfc_dict_context.enhanced_dict) ? - MfClassicPollerModeDictAttackEnhanced : - MfClassicPollerModeDictAttackStandard; + + // Select mode based on dictionary type + if(is_cuid_dict) { + mfc_event->data->poller_mode.mode = MfClassicPollerModeDictAttackCUID; + } else if(instance->nfc_dict_context.enhanced_dict) { + mfc_event->data->poller_mode.mode = MfClassicPollerModeDictAttackEnhanced; + } else { + mfc_event->data->poller_mode.mode = MfClassicPollerModeDictAttackStandard; + } + mfc_event->data->poller_mode.data = mfc_data; instance->nfc_dict_context.sectors_total = mf_classic_get_total_sectors_num(mfc_data->type); @@ -46,12 +67,57 @@ NfcCommand nfc_dict_attack_worker_callback(NfcGenericEvent event, void* context) view_dispatcher_send_custom_event( instance->view_dispatcher, NfcCustomEventDictAttackDataUpdate); } else if(mfc_event->type == MfClassicPollerEventTypeRequestKey) { + uint32_t state = + scene_manager_get_scene_state(instance->scene_manager, NfcSceneMfClassicDictAttack); + bool is_cuid_dict = (state == DictAttackStateCUIDDictInProgress); + MfClassicKey key = {}; - if(keys_dict_get_next_key( - instance->nfc_dict_context.dict, key.data, sizeof(MfClassicKey))) { + bool key_found = false; + + if(is_cuid_dict) { + // CUID dictionary: read 7 bytes (1 byte key_idx + 6 bytes key) and filter by exact key_idx + uint16_t target_key_idx = instance->nfc_dict_context.current_key_idx; + + // Check if this key index exists in the bitmap (only valid for 0-255) + if(target_key_idx < 256 && + BIT(instance->nfc_dict_context.cuid_key_indices_bitmap[target_key_idx / 8], + target_key_idx % 8)) { + uint8_t key_with_idx[sizeof(MfClassicKey) + 1]; + + while(keys_dict_get_next_key( + instance->nfc_dict_context.dict, key_with_idx, sizeof(MfClassicKey) + 1)) { + // Extract key_idx from first byte + uint8_t key_idx = key_with_idx[0]; + + instance->nfc_dict_context.dict_keys_current++; + + // Only use key if it matches the exact current key index + if(key_idx == (uint8_t)target_key_idx) { + // Copy the actual key (starts at byte 1) + memcpy(key.data, &key_with_idx[1], sizeof(MfClassicKey)); + key_found = true; + break; + } + } + } + } else { + // Standard dictionary: read 12 bytes + if(keys_dict_get_next_key( + instance->nfc_dict_context.dict, key.data, sizeof(MfClassicKey))) { + key_found = true; + instance->nfc_dict_context.dict_keys_current++; + } + } + + if(key_found) { mfc_event->data->key_request_data.key = key; + // In CUID mode, set key_type based on key_idx (odd = B, even = A) + if(is_cuid_dict) { + uint16_t target_key_idx = instance->nfc_dict_context.current_key_idx; + mfc_event->data->key_request_data.key_type = + (target_key_idx % 2 == 0) ? MfClassicKeyTypeA : MfClassicKeyTypeB; + } mfc_event->data->key_request_data.key_provided = true; - instance->nfc_dict_context.dict_keys_current++; if(instance->nfc_dict_context.dict_keys_current % 10 == 0) { view_dispatcher_send_custom_event( instance->view_dispatcher, NfcCustomEventDictAttackDataUpdate); @@ -72,10 +138,25 @@ NfcCommand nfc_dict_attack_worker_callback(NfcGenericEvent event, void* context) view_dispatcher_send_custom_event( instance->view_dispatcher, NfcCustomEventDictAttackDataUpdate); } else if(mfc_event->type == MfClassicPollerEventTypeNextSector) { + uint32_t state = + scene_manager_get_scene_state(instance->scene_manager, NfcSceneMfClassicDictAttack); + bool is_cuid_dict = (state == DictAttackStateCUIDDictInProgress); + keys_dict_rewind(instance->nfc_dict_context.dict); instance->nfc_dict_context.dict_keys_current = 0; - instance->nfc_dict_context.current_sector = - mfc_event->data->next_sector_data.current_sector; + + // In CUID mode, increment the key index and calculate sector from it + if(is_cuid_dict) { + instance->nfc_dict_context.current_key_idx++; + // Calculate sector from key_idx (each sector has 2 keys: A and B) + instance->nfc_dict_context.current_sector = instance->nfc_dict_context.current_key_idx / 2; + // Write back to event data so poller can read it + mfc_event->data->next_sector_data.current_sector = instance->nfc_dict_context.current_sector; + } else { + instance->nfc_dict_context.current_sector = + mfc_event->data->next_sector_data.current_sector; + } + view_dispatcher_send_custom_event( instance->view_dispatcher, NfcCustomEventDictAttackDataUpdate); } else if(mfc_event->type == MfClassicPollerEventTypeFoundKeyA) { @@ -153,18 +234,48 @@ static void nfc_scene_mf_classic_dict_attack_prepare_view(NfcApp* instance) { break; } - instance->nfc_dict_context.dict = keys_dict_alloc( - furi_string_get_cstr(cuid_dict_path), - KeysDictModeOpenExisting, - sizeof(MfClassicKey)); + // Manually create KeysDict and scan once to count + populate bitmap + KeysDict* dict = malloc(sizeof(KeysDict)); + Storage* storage = furi_record_open(RECORD_STORAGE); + dict->stream = buffered_file_stream_alloc(storage); + dict->key_size = sizeof(MfClassicKey) + 1; + dict->key_size_symbols = dict->key_size * 2 + 1; + dict->total_keys = 0; - if(keys_dict_get_total_keys(instance->nfc_dict_context.dict) == 0) { - keys_dict_free(instance->nfc_dict_context.dict); + if(!buffered_file_stream_open( + dict->stream, furi_string_get_cstr(cuid_dict_path), FSAM_READ_WRITE, FSOM_OPEN_EXISTING)) { + buffered_file_stream_close(dict->stream); + free(dict); state = DictAttackStateUserDictInProgress; break; } + // Allocate and populate bitmap of key indices present in CUID dictionary + instance->nfc_dict_context.cuid_key_indices_bitmap = malloc(32); + memset(instance->nfc_dict_context.cuid_key_indices_bitmap, 0, 32); + + // Scan dictionary once to count keys and populate bitmap + uint8_t key_with_idx[dict->key_size]; + while(keys_dict_get_next_key(dict, key_with_idx, dict->key_size)) { + uint8_t key_idx = key_with_idx[0]; + // Set bit for this key index + instance->nfc_dict_context.cuid_key_indices_bitmap[key_idx / 8] |= + (1 << (key_idx % 8)); + dict->total_keys++; + } + keys_dict_rewind(dict); + + if(dict->total_keys == 0) { + keys_dict_free(dict); + free(instance->nfc_dict_context.cuid_key_indices_bitmap); + instance->nfc_dict_context.cuid_key_indices_bitmap = NULL; + state = DictAttackStateUserDictInProgress; + break; + } + + instance->nfc_dict_context.dict = dict; dict_attack_set_header(instance->dict_attack, "MF Classic CUID Dictionary"); + instance->nfc_dict_context.current_key_idx = 0; // Initialize key index for CUID mode } while(false); furi_string_free(cuid_dict_path); @@ -265,6 +376,10 @@ bool nfc_scene_mf_classic_dict_attack_on_event(void* context, SceneManagerEvent nfc_poller_stop(instance->poller); nfc_poller_free(instance->poller); keys_dict_free(instance->nfc_dict_context.dict); + if(instance->nfc_dict_context.cuid_key_indices_bitmap) { + free(instance->nfc_dict_context.cuid_key_indices_bitmap); + instance->nfc_dict_context.cuid_key_indices_bitmap = NULL; + } scene_manager_set_scene_state( instance->scene_manager, NfcSceneMfClassicDictAttack, @@ -309,6 +424,10 @@ bool nfc_scene_mf_classic_dict_attack_on_event(void* context, SceneManagerEvent nfc_poller_stop(instance->poller); nfc_poller_free(instance->poller); keys_dict_free(instance->nfc_dict_context.dict); + if(instance->nfc_dict_context.cuid_key_indices_bitmap) { + free(instance->nfc_dict_context.cuid_key_indices_bitmap); + instance->nfc_dict_context.cuid_key_indices_bitmap = NULL; + } scene_manager_set_scene_state( instance->scene_manager, NfcSceneMfClassicDictAttack, @@ -366,6 +485,12 @@ void nfc_scene_mf_classic_dict_attack_on_exit(void* context) { keys_dict_free(instance->nfc_dict_context.dict); + // Free CUID bitmap if allocated + if(instance->nfc_dict_context.cuid_key_indices_bitmap) { + free(instance->nfc_dict_context.cuid_key_indices_bitmap); + instance->nfc_dict_context.cuid_key_indices_bitmap = NULL; + } + instance->nfc_dict_context.current_sector = 0; instance->nfc_dict_context.sectors_total = 0; instance->nfc_dict_context.sectors_read = 0; @@ -381,6 +506,7 @@ void nfc_scene_mf_classic_dict_attack_on_exit(void* context) { instance->nfc_dict_context.nested_target_key = 0; instance->nfc_dict_context.msb_count = 0; instance->nfc_dict_context.enhanced_dict = false; + instance->nfc_dict_context.current_key_idx = 0; // Clean up temporary files used for nested dictionary attack if(keys_dict_check_presence(NFC_APP_MF_CLASSIC_DICT_USER_NESTED_PATH)) { diff --git a/lib/nfc/protocols/mf_classic/mf_classic_poller.c b/lib/nfc/protocols/mf_classic/mf_classic_poller.c index b2d9b114a..748ea4627 100644 --- a/lib/nfc/protocols/mf_classic/mf_classic_poller.c +++ b/lib/nfc/protocols/mf_classic/mf_classic_poller.c @@ -163,11 +163,14 @@ NfcCommand mf_classic_poller_handler_start(MfClassicPoller* instance) { instance->mfc_event.type = MfClassicPollerEventTypeRequestMode; command = instance->callback(instance->general_event, instance->context); - if(instance->mfc_event_data.poller_mode.mode == MfClassicPollerModeDictAttackStandard) { + if(instance->mfc_event_data.poller_mode.mode == MfClassicPollerModeDictAttackStandard || + instance->mfc_event_data.poller_mode.mode == MfClassicPollerModeDictAttackCUID) { mf_classic_copy(instance->data, instance->mfc_event_data.poller_mode.data); + instance->mode_ctx.dict_attack_ctx.mode = instance->mfc_event_data.poller_mode.mode; instance->state = MfClassicPollerStateRequestKey; } else if(instance->mfc_event_data.poller_mode.mode == MfClassicPollerModeDictAttackEnhanced) { mf_classic_copy(instance->data, instance->mfc_event_data.poller_mode.data); + instance->mode_ctx.dict_attack_ctx.mode = instance->mfc_event_data.poller_mode.mode; instance->state = MfClassicPollerStateAnalyzeBackdoor; } else if(instance->mfc_event_data.poller_mode.mode == MfClassicPollerModeRead) { instance->state = MfClassicPollerStateRequestReadSector; @@ -590,7 +593,22 @@ NfcCommand mf_classic_poller_handler_analyze_backdoor(MfClassicPoller* instance) (error == MfClassicErrorProtocol || error == MfClassicErrorTimeout)) { FURI_LOG_D(TAG, "No backdoor identified"); dict_attack_ctx->backdoor = MfClassicBackdoorNone; - instance->state = MfClassicPollerStateRequestKey; + + // Check if any keys were cached - if so, go directly to nested attack + bool has_cached_keys = false; + for(uint8_t sector = 0; sector < instance->sectors_total; sector++) { + if(mf_classic_is_key_found(instance->data, sector, MfClassicKeyTypeA) || + mf_classic_is_key_found(instance->data, sector, MfClassicKeyTypeB)) { + has_cached_keys = true; + break; + } + } + + if(has_cached_keys) { + instance->state = MfClassicPollerStateNestedController; + } else { + instance->state = MfClassicPollerStateRequestKey; + } } else if(error == MfClassicErrorNone) { FURI_LOG_I(TAG, "Backdoor identified: v%d", backdoor_version); dict_attack_ctx->backdoor = mf_classic_backdoor_keys[next_key_index].type; @@ -687,7 +705,15 @@ NfcCommand mf_classic_poller_handler_request_key(MfClassicPoller* instance) { command = instance->callback(instance->general_event, instance->context); if(instance->mfc_event_data.key_request_data.key_provided) { dict_attack_ctx->current_key = instance->mfc_event_data.key_request_data.key; - instance->state = MfClassicPollerStateAuthKeyA; + dict_attack_ctx->requested_key_type = instance->mfc_event_data.key_request_data.key_type; + + // In CUID mode, go directly to the appropriate Auth state based on key_type + if(dict_attack_ctx->mode == MfClassicPollerModeDictAttackCUID && + dict_attack_ctx->requested_key_type == MfClassicKeyTypeB) { + instance->state = MfClassicPollerStateAuthKeyB; + } else { + instance->state = MfClassicPollerStateAuthKeyA; + } } else { instance->state = MfClassicPollerStateNextSector; } @@ -701,7 +727,12 @@ NfcCommand mf_classic_poller_handler_auth_a(MfClassicPoller* instance) { if(mf_classic_is_key_found( instance->data, dict_attack_ctx->current_sector, MfClassicKeyTypeA)) { - instance->state = MfClassicPollerStateAuthKeyB; + // In CUID mode, skip directly to RequestKey since we test keys by specific type + if(dict_attack_ctx->mode == MfClassicPollerModeDictAttackCUID) { + instance->state = MfClassicPollerStateRequestKey; + } else { + instance->state = MfClassicPollerStateAuthKeyB; + } } else { uint8_t block = mf_classic_get_first_block_num_of_sector(dict_attack_ctx->current_sector); uint64_t key = @@ -722,7 +753,12 @@ NfcCommand mf_classic_poller_handler_auth_a(MfClassicPoller* instance) { instance->state = MfClassicPollerStateReadSector; } else { mf_classic_poller_halt(instance); - instance->state = MfClassicPollerStateAuthKeyB; + // In CUID mode, skip directly to RequestKey since we test keys by specific type + if(dict_attack_ctx->mode == MfClassicPollerModeDictAttackCUID) { + instance->state = MfClassicPollerStateRequestKey; + } else { + instance->state = MfClassicPollerStateAuthKeyB; + } } } @@ -735,8 +771,11 @@ NfcCommand mf_classic_poller_handler_auth_b(MfClassicPoller* instance) { if(mf_classic_is_key_found( instance->data, dict_attack_ctx->current_sector, MfClassicKeyTypeB)) { - if(mf_classic_is_key_found( - instance->data, dict_attack_ctx->current_sector, MfClassicKeyTypeA)) { + // In CUID mode, just request next key since we iterate by key_idx + if(dict_attack_ctx->mode == MfClassicPollerModeDictAttackCUID) { + instance->state = MfClassicPollerStateRequestKey; + } else if(mf_classic_is_key_found( + instance->data, dict_attack_ctx->current_sector, MfClassicKeyTypeA)) { instance->state = MfClassicPollerStateNextSector; } else { instance->state = MfClassicPollerStateRequestKey; @@ -774,12 +813,19 @@ NfcCommand mf_classic_poller_handler_next_sector(MfClassicPoller* instance) { MfClassicPollerDictAttackContext* dict_attack_ctx = &instance->mode_ctx.dict_attack_ctx; dict_attack_ctx->current_sector++; + if(dict_attack_ctx->current_sector == instance->sectors_total) { instance->state = MfClassicPollerStateSuccess; } else { instance->mfc_event.type = MfClassicPollerEventTypeNextSector; instance->mfc_event_data.next_sector_data.current_sector = dict_attack_ctx->current_sector; command = instance->callback(instance->general_event, instance->context); + + // In CUID mode, NFC app manages sector based on key_idx - read it back + if(dict_attack_ctx->mode == MfClassicPollerModeDictAttackCUID) { + dict_attack_ctx->current_sector = instance->mfc_event_data.next_sector_data.current_sector; + } + instance->state = MfClassicPollerStateRequestKey; } diff --git a/lib/nfc/protocols/mf_classic/mf_classic_poller.h b/lib/nfc/protocols/mf_classic/mf_classic_poller.h index 8efb931aa..5c853e21a 100644 --- a/lib/nfc/protocols/mf_classic/mf_classic_poller.h +++ b/lib/nfc/protocols/mf_classic/mf_classic_poller.h @@ -45,6 +45,7 @@ typedef enum { MfClassicPollerModeRead, /**< Poller reading mode. */ MfClassicPollerModeWrite, /**< Poller writing mode. */ MfClassicPollerModeDictAttackStandard, /**< Poller dictionary attack mode. */ + MfClassicPollerModeDictAttackCUID, /**< Poller CUID dictionary attack mode. */ MfClassicPollerModeDictAttackEnhanced, /**< Poller enhanced dictionary attack mode. */ } MfClassicPollerMode; @@ -129,6 +130,7 @@ typedef struct { */ typedef struct { MfClassicKey key; /**< Key to be used by poller. */ + MfClassicKeyType key_type; /**< Key type (A or B) for CUID dict attack mode. */ bool key_provided; /**< Flag indicating if key is provided. */ } MfClassicPollerEventDataKeyRequest; 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 915c899c3..607b126a0 100644 --- a/lib/nfc/protocols/mf_classic/mf_classic_poller_i.h +++ b/lib/nfc/protocols/mf_classic/mf_classic_poller_i.h @@ -128,10 +128,12 @@ typedef struct { uint8_t current_sector; MfClassicKey current_key; MfClassicKeyType current_key_type; + MfClassicKeyType requested_key_type; // Key type requested from app (for CUID mode) bool auth_passed; uint16_t current_block; uint8_t reuse_key_sector; MfClassicBackdoor backdoor; + MfClassicPollerMode mode; // Current attack mode // Enhanced dictionary attack and nested nonce collection bool enhanced_dict; MfClassicNestedPhase nested_phase;