SEN attacks at ludicrous speed

This commit is contained in:
noproto
2025-12-01 18:08:23 -05:00
parent 70f05ae4a8
commit 39c841289e
8 changed files with 235 additions and 38 deletions

View File

@@ -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 {

View File

@@ -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);
@@ -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,
@@ -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,
@@ -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;
@@ -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)) {

View File

@@ -251,10 +251,14 @@ bool load_nested_nonces(
MfClassicNonce res = {0};
res.attack = static_encrypted;
int sector_num = 0;
char key_type = 'A';
int parsed = sscanf(
line,
"Sec %*d key %*c cuid %" PRIx32 " nt0 %" PRIx32 " ks0 %" PRIx32
"Sec %d key %c cuid %" PRIx32 " nt0 %" PRIx32 " ks0 %" PRIx32
" par0 %4[01] nt1 %" PRIx32 " ks1 %" PRIx32 " par1 %4[01]",
&sector_num,
&key_type,
&res.uid,
&res.nt0,
&res.ks1_1_enc,
@@ -263,11 +267,14 @@ bool load_nested_nonces(
&res.ks1_2_enc,
res.par_2_str);
if(parsed >= 4) { // At least one nonce is present
// Calculate key_idx from sector and key type (for static encrypted: key_idx = sector * 2 + key_offset)
res.key_idx = (uint8_t)(sector_num * 2 + (key_type == 'B' ? 1 : 0));
if(parsed >= 6) { // At least one nonce is present (sector, key, uid, nt0, ks0, par0)
res.par_1 = binaryStringToInt(res.par_1_str);
res.uid_xor_nt0 = res.uid ^ res.nt0;
if(parsed == 7) { // Both nonces are present
if(parsed == 9) { // Both nonces are present
res.attack = static_nested;
res.par_2 = binaryStringToInt(res.par_2_str);
res.uid_xor_nt1 = res.uid ^ res.nt1;

View File

@@ -69,17 +69,22 @@ static inline void flush_key_buffer(ProgramState *program_state)
{
if (program_state->key_buffer && program_state->key_buffer_count > 0 && program_state->cuid_dict)
{
// Pre-allocate exact size needed: 12 hex chars + 1 newline per key
size_t total_size = program_state->key_buffer_count * 13;
// Pre-allocate exact size needed: 2 hex chars (key_idx) + 12 hex chars (key) + 1 newline per key
size_t total_size = program_state->key_buffer_count * 15;
//FURI_LOG_I(TAG, "Flushing key buffer: %d keys", program_state->key_buffer_count);
//FURI_LOG_I(TAG, "Total size: %d bytes", total_size);
char* batch_buffer = malloc(total_size + 1); // +1 for null terminator
char* ptr = batch_buffer;
const char hex_chars[] = "0123456789ABCDEF";
for (size_t i = 0; i < program_state->key_buffer_count; i++)
{
// Write key_idx as 2 hex chars
uint8_t key_idx = program_state->key_idx_buffer[i];
*ptr++ = hex_chars[key_idx >> 4];
*ptr++ = hex_chars[key_idx & 0x0F];
// Convert key to hex string directly into buffer
for (size_t j = 0; j < sizeof(MfClassicKey); j++)
{
@@ -90,18 +95,18 @@ static inline void flush_key_buffer(ProgramState *program_state)
*ptr++ = '\n';
}
*ptr = '\0';
// Write all keys at once by directly accessing the stream
Stream* stream = program_state->cuid_dict->stream;
uint32_t actual_pos = stream_tell(stream);
if (stream_seek(stream, 0, StreamOffsetFromEnd) &&
if (stream_seek(stream, 0, StreamOffsetFromEnd) &&
stream_write(stream, (uint8_t*)batch_buffer, total_size) == total_size)
{
// Update total key count
program_state->cuid_dict->total_keys += program_state->key_buffer_count;
}
// May not be needed
stream_seek(stream, actual_pos, StreamOffsetFromStart);
free(batch_buffer);
@@ -158,11 +163,12 @@ check_state(struct Crypto1State *t, MfClassicNonce *n, ProgramState *program_sta
// Found key candidate
crypto1_get_lfsr(t, &(n->key));
program_state->num_candidates++;
// Use key buffer - buffer is guaranteed to be available for static_encrypted
program_state->key_buffer[program_state->key_buffer_count] = n->key;
program_state->key_idx_buffer[program_state->key_buffer_count] = n->key_idx;
program_state->key_buffer_count++;
// Flush buffer when full
if (program_state->key_buffer_count >= program_state->key_buffer_size)
{
@@ -785,17 +791,18 @@ bool recover(MfClassicNonce *n, int ks2, unsigned int in, ProgramState *program_
if (n->attack == static_encrypted)
{
size_t available_ram = memmgr_heap_get_max_free_block();
// Each key becomes 12 hex chars + 1 newline = 13 bytes in the batch string
// Plus original 6 bytes in buffer = 19 bytes total per key
// Each key becomes 2 hex chars (key_idx) + 12 hex chars (key) + 1 newline = 15 bytes in the batch string
// Plus original 6 bytes (key) + 1 byte (key_idx) in buffer = 22 bytes total per key
// Add extra safety margin for string overhead and other allocations
const size_t safety_threshold = STATIC_ENCRYPTED_RAM_THRESHOLD;
const size_t bytes_per_key = sizeof(MfClassicKey) + 13; // buffer + string representation
const size_t bytes_per_key = sizeof(MfClassicKey) + sizeof(uint8_t) + 15; // buffer + string representation
if (available_ram > safety_threshold)
{
program_state->key_buffer_size = (available_ram - safety_threshold) / bytes_per_key;
program_state->key_buffer = malloc(program_state->key_buffer_size * sizeof(MfClassicKey));
program_state->key_idx_buffer = malloc(program_state->key_buffer_size * sizeof(uint8_t));
program_state->key_buffer_count = 0;
if (!program_state->key_buffer)
if (!program_state->key_buffer || !program_state->key_idx_buffer)
{
// Free the allocated blocks before returning
for (int i = 0; i < num_blocks; i++)
@@ -824,6 +831,7 @@ bool recover(MfClassicNonce *n, int ks2, unsigned int in, ProgramState *program_
else
{
program_state->key_buffer = NULL;
program_state->key_idx_buffer = NULL;
program_state->key_buffer_size = 0;
program_state->key_buffer_count = 0;
}
@@ -875,7 +883,9 @@ bool recover(MfClassicNonce *n, int ks2, unsigned int in, ProgramState *program_
{
flush_key_buffer(program_state);
free(program_state->key_buffer);
free(program_state->key_idx_buffer);
program_state->key_buffer = NULL;
program_state->key_idx_buffer = NULL;
program_state->key_buffer_size = 0;
program_state->key_buffer_count = 0;
}

View File

@@ -59,6 +59,7 @@ typedef struct
FuriThread *mfkeythread;
KeysDict *cuid_dict;
MfClassicKey *key_buffer;
uint8_t *key_idx_buffer;
size_t key_buffer_size;
size_t key_buffer_count;
} ProgramState;
@@ -79,6 +80,7 @@ typedef struct
uint32_t nt1; // tag challenge second
uint32_t uid_xor_nt0; // uid ^ nt0
uint32_t uid_xor_nt1; // uid ^ nt1
uint8_t key_idx; // key index (for static encrypted nonces)
union
{
// Mfkey32