Merge remote-tracking branch 'xero/dev' into mntm-dev

This commit is contained in:
WillyJL
2025-12-10 00:02:49 +01:00
6 changed files with 201 additions and 21 deletions
+2
View File
@@ -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)
+2
View File
@@ -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 {
@@ -2,12 +2,22 @@
#include <bit_lib/bit_lib.h>
#include <dolphin/dolphin.h>
#include <toolbox/stream/buffered_file_stream.h>
#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)) {
@@ -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;
}
@@ -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;
@@ -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;