Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions applications/main/nfc/nfc_app_i.h
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,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 {
Expand Down
154 changes: 140 additions & 14 deletions applications/main/nfc/scenes/nfc_scene_mf_classic_dict_attack.c
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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) {
Expand Down Expand Up @@ -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(!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;
}

if(keys_dict_get_total_keys(instance->nfc_dict_context.dict) == 0) {
keys_dict_free(instance->nfc_dict_context.dict);
// 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);
Expand Down Expand Up @@ -266,6 +377,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,
Expand Down Expand Up @@ -310,6 +425,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,
Expand Down Expand Up @@ -367,6 +486,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;
Expand All @@ -382,6 +507,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)) {
Expand Down
60 changes: 53 additions & 7 deletions lib/nfc/protocols/mf_classic/mf_classic_poller.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
Expand All @@ -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 =
Expand All @@ -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;
}
}
}

Expand All @@ -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;
Expand Down Expand Up @@ -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;
}

Expand Down
2 changes: 2 additions & 0 deletions lib/nfc/protocols/mf_classic/mf_classic_poller.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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;

Expand Down
2 changes: 2 additions & 0 deletions lib/nfc/protocols/mf_classic/mf_classic_poller_i.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down