Compare commits

..

8 Commits

Author SHA1 Message Date
grugnoymeme
6f39fd4803 removed problematics and fixed f3
All checks were successful
Build Dev Firmware / build (push) Successful in 6m34s
2026-03-18 19:45:16 +01:00
grugnoymeme
41d10f9b3d Merge remote-tracking branch 'refs/remotes/origin/main' 2026-03-18 19:26:36 +01:00
grugnoymeme
1f97aa2e3c reduced datarate for F3, renamed 1,2,A1,F1,F3, introduced AU_1,RF_1 for test 2026-03-18 19:26:21 +01:00
D4rk$1d3
5b9038173b Update README.md 2026-03-18 15:04:38 -03:00
grugnoymeme
fde0a57595 forgtten to close a }
All checks were successful
Build Dev Firmware / build (push) Successful in 6m30s
2026-03-18 16:23:59 +01:00
MX
3fb40944e6 NFC: Add Mifare Ultralight C Write Support
by haw8411
2026-03-18 16:16:04 +01:00
grugnoymeme
e61cfa765a bft force seed value @MMX 2026-03-18 16:07:20 +01:00
grugnoymeme
fd0dd6c324 subghz fix very big issue with tx on read screen @MMX 2026-03-18 16:04:22 +01:00
12 changed files with 360 additions and 37 deletions

View File

@@ -182,7 +182,7 @@ Contributions are welcome if they:
> Non-automotive features are considered out-of-scope for now.
### This code is a mess!
![Talk is cheap, submit patches](arf_pictures/send_patches.jpeg)
![Talk is cheap, submit patches](.arf_pictures/send_patches.jpeg)
---
## Citations & References

View File

@@ -14,7 +14,9 @@ enum {
SubmenuIndexUnlock = SubmenuIndexCommonMax,
SubmenuIndexUnlockByReader,
SubmenuIndexUnlockByPassword,
SubmenuIndexDictAttack
SubmenuIndexDictAttack,
SubmenuIndexWriteKeepKey, // ULC: write data pages, keep target card's existing key
SubmenuIndexWriteCopyKey, // ULC: write all pages including key from source card
};
enum {
@@ -214,8 +216,26 @@ static void nfc_scene_read_and_saved_menu_on_enter_mf_ultralight(NfcApp* instanc
if(is_locked ||
(data->type != MfUltralightTypeNTAG213 && data->type != MfUltralightTypeNTAG215 &&
data->type != MfUltralightTypeNTAG216 && data->type != MfUltralightTypeUL11 &&
data->type != MfUltralightTypeUL21 && data->type != MfUltralightTypeOrigin)) {
data->type != MfUltralightTypeUL21 && data->type != MfUltralightTypeOrigin &&
data->type != MfUltralightTypeMfulC)) {
submenu_remove_item(submenu, SubmenuIndexCommonWrite);
} else if(data->type == MfUltralightTypeMfulC) {
// Replace the generic Write item with two ULC-specific options so the user
// can choose whether to keep or overwrite the target card's 3DES key.
// This avoids any mid-write dialog/view-switching complexity entirely.
submenu_remove_item(submenu, SubmenuIndexCommonWrite);
submenu_add_item(
submenu,
"Write (Keep Key)",
SubmenuIndexWriteKeepKey,
nfc_protocol_support_common_submenu_callback,
instance);
submenu_add_item(
submenu,
"Write (Copy Key)",
SubmenuIndexWriteCopyKey,
nfc_protocol_support_common_submenu_callback,
instance);
}
if(is_locked) {
@@ -291,6 +311,14 @@ static bool nfc_scene_read_and_saved_menu_on_event_mf_ultralight(
scene_manager_next_scene(instance->scene_manager, NfcSceneMfUltralightCDictAttack);
}
consumed = true;
} else if(event.event == SubmenuIndexWriteKeepKey) {
instance->mf_ultralight_c_write_context.copy_key = false;
scene_manager_next_scene(instance->scene_manager, NfcSceneWrite);
consumed = true;
} else if(event.event == SubmenuIndexWriteCopyKey) {
instance->mf_ultralight_c_write_context.copy_key = true;
scene_manager_next_scene(instance->scene_manager, NfcSceneWrite);
consumed = true;
}
}
return consumed;
@@ -307,12 +335,139 @@ static NfcCommand
if(mf_ultralight_event->type == MfUltralightPollerEventTypeRequestMode) {
mf_ultralight_event->data->poller_mode = MfUltralightPollerModeWrite;
furi_string_reset(instance->text_box_store);
if(instance->mf_ultralight_c_dict_context.dict) {
keys_dict_free(instance->mf_ultralight_c_dict_context.dict);
}
instance->mf_ultralight_c_dict_context.dict = NULL;
instance->mf_ultralight_c_write_context.dict_state = NfcMfUltralightCWriteDictIdle;
view_dispatcher_send_custom_event(instance->view_dispatcher, NfcCustomEventCardDetected);
} else if(mf_ultralight_event->type == MfUltralightPollerEventTypeAuthRequest) {
// Skip auth during the read phase of write - we'll authenticate
// against the target card in RequestWriteData using source key or dict attack
mf_ultralight_event->data->auth_context.skip_auth = true;
} else if(mf_ultralight_event->type == MfUltralightPollerEventTypeRequestKey) {
// Dict attack key provider - user dict first, then system dict
if(!instance->mf_ultralight_c_dict_context.dict &&
instance->mf_ultralight_c_write_context.dict_state == NfcMfUltralightCWriteDictIdle) {
if(keys_dict_check_presence(NFC_APP_MF_ULTRALIGHT_C_DICT_USER_PATH)) {
instance->mf_ultralight_c_dict_context.dict = keys_dict_alloc(
NFC_APP_MF_ULTRALIGHT_C_DICT_USER_PATH,
KeysDictModeOpenExisting,
sizeof(MfUltralightC3DesAuthKey));
instance->mf_ultralight_c_write_context.dict_state = NfcMfUltralightCWriteDictUser;
}
if(!instance->mf_ultralight_c_dict_context.dict) {
instance->mf_ultralight_c_dict_context.dict = keys_dict_alloc(
NFC_APP_MF_ULTRALIGHT_C_DICT_SYSTEM_PATH,
KeysDictModeOpenExisting,
sizeof(MfUltralightC3DesAuthKey));
instance->mf_ultralight_c_write_context.dict_state =
NfcMfUltralightCWriteDictSystem;
}
}
MfUltralightC3DesAuthKey key = {};
bool got_key = false;
if(instance->mf_ultralight_c_dict_context.dict) {
got_key = keys_dict_get_next_key(
instance->mf_ultralight_c_dict_context.dict,
key.data,
sizeof(MfUltralightC3DesAuthKey));
}
if(!got_key &&
instance->mf_ultralight_c_write_context.dict_state == NfcMfUltralightCWriteDictUser) {
// Exhausted user dict, switch to system dict
if(instance->mf_ultralight_c_dict_context.dict) {
keys_dict_free(instance->mf_ultralight_c_dict_context.dict);
}
instance->mf_ultralight_c_dict_context.dict = keys_dict_alloc(
NFC_APP_MF_ULTRALIGHT_C_DICT_SYSTEM_PATH,
KeysDictModeOpenExisting,
sizeof(MfUltralightC3DesAuthKey));
instance->mf_ultralight_c_write_context.dict_state = NfcMfUltralightCWriteDictSystem;
if(instance->mf_ultralight_c_dict_context.dict) {
got_key = keys_dict_get_next_key(
instance->mf_ultralight_c_dict_context.dict,
key.data,
sizeof(MfUltralightC3DesAuthKey));
}
}
if(got_key) {
mf_ultralight_event->data->key_request_data.key = key;
mf_ultralight_event->data->key_request_data.key_provided = true;
FURI_LOG_D(
"MfULC",
"Trying dict key: "
"%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X",
key.data[0],
key.data[1],
key.data[2],
key.data[3],
key.data[4],
key.data[5],
key.data[6],
key.data[7],
key.data[8],
key.data[9],
key.data[10],
key.data[11],
key.data[12],
key.data[13],
key.data[14],
key.data[15]);
} else {
mf_ultralight_event->data->key_request_data.key_provided = false;
FURI_LOG_D("MfULC", "Dict exhausted - no more keys");
if(instance->mf_ultralight_c_dict_context.dict) {
keys_dict_free(instance->mf_ultralight_c_dict_context.dict);
instance->mf_ultralight_c_dict_context.dict = NULL;
}
instance->mf_ultralight_c_write_context.dict_state =
NfcMfUltralightCWriteDictExhausted;
}
} else if(mf_ultralight_event->type == MfUltralightPollerEventTypeRequestWriteData) {
mf_ultralight_event->data->write_data =
nfc_device_get_data(instance->nfc_device, NfcProtocolMfUltralight);
// Reset dict context so RequestKey starts fresh for the write-phase auth
if(instance->mf_ultralight_c_dict_context.dict) {
keys_dict_free(instance->mf_ultralight_c_dict_context.dict);
instance->mf_ultralight_c_dict_context.dict = NULL;
}
instance->mf_ultralight_c_write_context.dict_state = NfcMfUltralightCWriteDictIdle;
} else if(mf_ultralight_event->type == MfUltralightPollerEventTypeWriteKeyRequest) {
// Apply the user's key choice - read from static, not scene state (scene manager
// resets state to 0 on scene entry, wiping any value set before next_scene).
bool keep_key = !instance->mf_ultralight_c_write_context.copy_key;
mf_ultralight_event->data->write_key_skip = keep_key;
if(mf_ultralight_event->data->key_request_data.key_provided) {
MfUltralightC3DesAuthKey found_key = mf_ultralight_event->data->key_request_data.key;
FURI_LOG_D(
"MfULC",
"WriteKeyRequest: target key = "
"%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X",
found_key.data[0],
found_key.data[1],
found_key.data[2],
found_key.data[3],
found_key.data[4],
found_key.data[5],
found_key.data[6],
found_key.data[7],
found_key.data[8],
found_key.data[9],
found_key.data[10],
found_key.data[11],
found_key.data[12],
found_key.data[13],
found_key.data[14],
found_key.data[15]);
}
FURI_LOG_D(
"MfULC",
"WriteKeyRequest: decision = %s (copy_key=%d)",
keep_key ? "KEEP target key (pages 44-47 NOT written)" :
"OVERWRITE with source key (pages 44-47 WILL be written)",
(int)instance->mf_ultralight_c_write_context.copy_key);
} else if(mf_ultralight_event->type == MfUltralightPollerEventTypeCardMismatch) {
furi_string_set(instance->text_box_store, "Card of the same\ntype should be\n presented");
view_dispatcher_send_custom_event(instance->view_dispatcher, NfcCustomEventWrongCard);
@@ -323,6 +478,7 @@ static NfcCommand
view_dispatcher_send_custom_event(instance->view_dispatcher, NfcCustomEventPollerFailure);
command = NfcCommandStop;
} else if(mf_ultralight_event->type == MfUltralightPollerEventTypeWriteFail) {
view_dispatcher_send_custom_event(instance->view_dispatcher, NfcCustomEventPollerFailure);
command = NfcCommandStop;
} else if(mf_ultralight_event->type == MfUltralightPollerEventTypeWriteSuccess) {
furi_string_reset(instance->text_box_store);
@@ -334,9 +490,18 @@ static NfcCommand
}
static void nfc_scene_write_on_enter_mf_ultralight(NfcApp* instance) {
// Free any dict the write callback opened (dict_state != Idle means we own it).
// After a DictAttack scene, on_exit now NULLs the pointer so a simple NULL check
// is safe here too — but the state enum is the authoritative ownership record.
if(instance->mf_ultralight_c_write_context.dict_state != NfcMfUltralightCWriteDictIdle &&
instance->mf_ultralight_c_dict_context.dict) {
keys_dict_free(instance->mf_ultralight_c_dict_context.dict);
}
instance->mf_ultralight_c_dict_context.dict = NULL;
instance->mf_ultralight_c_write_context.dict_state = NfcMfUltralightCWriteDictIdle;
furi_string_set(instance->text_box_store, "\nApply the\ntarget\ncard now");
instance->poller = nfc_poller_alloc(instance->nfc, NfcProtocolMfUltralight);
nfc_poller_start(instance->poller, nfc_scene_write_poller_callback_mf_ultralight, instance);
furi_string_set(instance->text_box_store, "Apply the initial\ncard only");
}
const NfcProtocolSupportBase nfc_protocol_support_mf_ultralight = {

View File

@@ -126,6 +126,18 @@ typedef struct {
size_t dict_keys_current;
} NfcMfUltralightCDictContext;
typedef enum {
NfcMfUltralightCWriteDictIdle, /**< No dict open; safe to open either dict. */
NfcMfUltralightCWriteDictUser, /**< User dict currently open. */
NfcMfUltralightCWriteDictSystem, /**< System dict currently open. */
NfcMfUltralightCWriteDictExhausted, /**< All dicts tried; do not re-open. */
} NfcMfUltralightCWriteDictState;
typedef struct {
bool copy_key; /**< True = overwrite target 3DES key with source key pages. */
NfcMfUltralightCWriteDictState dict_state; /**< Which dict is open for write-phase auth. */
} NfcMfUltralightCWriteContext;
struct NfcApp {
DialogsApp* dialogs;
Storage* storage;
@@ -165,6 +177,7 @@ struct NfcApp {
SlixUnlock* slix_unlock;
NfcMfClassicDictAttackContext nfc_dict_context;
NfcMfUltralightCDictContext mf_ultralight_c_dict_context;
NfcMfUltralightCWriteContext mf_ultralight_c_write_context;
Mfkey32Logger* mfkey32_logger;
MfUserDict* mf_user_dict;
MfClassicKeyCache* mfc_key_cache;

View File

@@ -77,6 +77,15 @@ void nfc_scene_mf_ultralight_c_dict_attack_prepare_view(NfcApp* instance) {
// Set attack type to Ultralight C
dict_attack_set_type(instance->dict_attack, DictAttackTypeMfUltralightC);
// Guard: if a previous write phase left a dict handle open, close it now.
// Without this, navigating write->back->read->dict-attack would open the same
// file twice, corrupting VFS state and causing a ViewPort lockup.
if(instance->mf_ultralight_c_dict_context.dict) {
keys_dict_free(instance->mf_ultralight_c_dict_context.dict);
instance->mf_ultralight_c_dict_context.dict = NULL;
instance->mf_ultralight_c_write_context.dict_state = NfcMfUltralightCWriteDictIdle;
}
if(state == DictAttackStateUserDictInProgress) {
do {
if(!keys_dict_check_presence(NFC_APP_MF_ULTRALIGHT_C_DICT_USER_PATH)) {
@@ -167,6 +176,7 @@ bool nfc_scene_mf_ultralight_c_dict_attack_on_event(void* context, SceneManagerE
nfc_poller_stop(instance->poller);
nfc_poller_free(instance->poller);
keys_dict_free(instance->mf_ultralight_c_dict_context.dict);
instance->mf_ultralight_c_dict_context.dict = NULL;
scene_manager_set_scene_state(
instance->scene_manager,
NfcSceneMfUltralightCDictAttack,
@@ -199,6 +209,7 @@ bool nfc_scene_mf_ultralight_c_dict_attack_on_event(void* context, SceneManagerE
nfc_poller_stop(instance->poller);
nfc_poller_free(instance->poller);
keys_dict_free(instance->mf_ultralight_c_dict_context.dict);
instance->mf_ultralight_c_dict_context.dict = NULL;
scene_manager_set_scene_state(
instance->scene_manager,
NfcSceneMfUltralightCDictAttack,
@@ -230,6 +241,7 @@ void nfc_scene_mf_ultralight_c_dict_attack_on_exit(void* context) {
NfcSceneMfUltralightCDictAttack,
DictAttackStateUserDictInProgress);
keys_dict_free(instance->mf_ultralight_c_dict_context.dict);
instance->mf_ultralight_c_dict_context.dict = NULL;
instance->mf_ultralight_c_dict_context.dict_keys_total = 0;
instance->mf_ultralight_c_dict_context.dict_keys_current = 0;
instance->mf_ultralight_c_dict_context.auth_success = false;

View File

@@ -92,19 +92,30 @@ Hopping_Preset: FM95
# Custom preset
# format for CC1101 "Custom_preset_data:" XX YY XX YY .. 00 00 ZZ ZZ ZZ ZZ ZZ ZZ ZZ ZZ, where: XX-register, YY - register data, 00 00 - end load register, ZZ - 8 byte Pa table register
Custom_preset_name: A1
Custom_preset_name: OOK_LR
Custom_preset_module: CC1101
Custom_preset_data: 02 0D 0B 06 08 32 07 04 14 00 13 00 12 30 11 F8 10 C9 15 14 18 18 19 16 1D 91 1C 00 1B 07 20 FB 22 00 21 55 00 00 00 С0 00 00 00 00 00 00
Custom_preset_data: 02 0D 07 04 08 32 0B 06 10 F8 11 32 12 30 14 00 15 00 18 18 19 16 1B 07 1C 00 1D B1 20 FB 21 B6 22 11 00 00 00 C0 00 00 00 00 00 00
Custom_preset_name: FM95
Custom_preset_name: OOK_U
Custom_preset_module: CC1101
Custom_preset_data: 02 0D 0B 06 08 32 07 04 14 00 13 02 12 04 11 83 10 67 15 24 18 18 19 16 1D 91 1C 00 1B 07 20 FB 22 10 21 56 00 00 C0 00 00 00 00 00 00 00
Custom_preset_data: 02 0D 07 04 08 32 0B 06 10 F8 11 32 12 30 14 00 15 00 17 0C 18 18 19 16 1B 07 1C 00 1D B1 20 FB 21 B6 22 11 2C 81 2D 35 2E 09 00 00 00 C0 00 00 00 00 00 00
Custom_preset_name: AM_1
Custom_preset_module: CC1101
Custom_preset_data: 02 0D 07 04 08 32 0B 06 10 C9 11 F8 12 30 14 00 15 14 18 18 19 16 1B 07 1C 00 1D 91 20 FB 21 55 22 00 00 00 00 C0 00 00 00 00 00 00
Custom_preset_name: FSK_1
Custom_preset_module: CC1101
Custom_preset_data: 02 0D 03 47 08 32 0B 0C 10 C7 11 93 12 00 13 22 14 F8 15 35 18 18 19 1D 1B 04 1C 00 1D 92 20 FB 21 B6 22 17 00 00 12 0E 1D 34 60 84 C8 C0
Custom_preset_name: F3
Custom_preset_module: CC1101
Custom_preset_data: 02 0D 03 47 08 32 0B 06 0C 00 0D 10 0E B0 0F 71 10 CA 11 83 12 80 13 22 14 F8 15 42 16 07 17 30 18 18 19 1D 1A 1C 1B 43 1C 40 1D 91 20 FB 21 B6 22 00 23 E9 24 2A 25 00 26 1F 2C 81 2D 35 2E 09 00 00 12 0E 1D 34 60 84 C8 C0
Custom_preset_data: 02 0D 03 47 08 32 0B 06 0C 00 0D 10 0D 0D 10 C5 11 83 12 80 13 22 14 F8 15 42 16 07 17 30 18 18 19 1D 1A 1C 1B 43 1C 40 1D 91 20 FB 21 B6 22 00 23 E9 24 2A 25 00 26 1F 2C 81 2D 35 2E 09 00 00 12 0E 1D 34 60 84 C8 C0
Custom_preset_name: FM95
Custom_preset_module: CC1101
Custom_preset_data: 02 0D 07 04 08 32 0B 06 10 67 11 83 12 04 13 02 15 24 18 18 19 16 1B 07 1C 00 1D 91 20 FB 21 56 22 10 00 00 C0 00 00 00 00 00 00 00
#2-FSK 200khz BW / 135kHz Filter/ 15.86Khz Deviation + Ramping
Custom_preset_name: FM15k
Custom_preset_module: CC1101
Custom_preset_data: 02 0D 03 47 08 32 0B 06 15 32 14 00 13 00 12 00 11 32 10 A7 18 18 19 1D 1D 92 1C 00 1B 04 20 FB 22 17 21 B6 00 00 00 12 0E 34 60 C5 C1 C0
Custom_preset_data: 02 0D 03 47 08 32 0B 06 10 A7 11 32 12 00 13 00 14 00 15 32 18 18 19 1D 1B 04 1C 00 1D 92 20 FB 21 B6 22 17 00 00 00 12 0E 34 60 C5 C1 C0

View File

@@ -133,21 +133,21 @@ bool subghz_scene_receiver_info_on_event(void* context, SceneManagerEvent event)
}
//CC1101 Stop RX -> Start TX
subghz_txrx_hopper_pause(subghz->txrx);
// key concept: we start endless TX until user release OK button, and after this we send last
// protocols repeats - this guarantee that one press OK will
// be guarantee send the required minimum protocol data packets
// for all of this we use subghz_block_generic_global.endless_tx in protocols _yield function.
subghz->state_notifications = SubGhzNotificationStateTx;
subghz_block_generic_global.endless_tx = true;
if(!subghz_tx_start(
subghz,
subghz_history_get_raw_data(subghz->history, subghz->idx_menu_chosen))) {
subghz_txrx_rx_start(subghz->txrx);
subghz_txrx_hopper_unpause(subghz->txrx);
subghz->state_notifications = SubGhzNotificationStateRx;
} else {
// key concept: we start endless TX until user release OK button, and after this we send last
// protocols repeats - this guarantee that one press OK will
// be guarantee send the required minimum protocol data packets
// for all of this we use subghz_block_generic_global.endless_tx in protocols _yield function.
subghz->state_notifications = SubGhzNotificationStateTx;
subghz_block_generic_global.endless_tx = true;
subghz_block_generic_global.endless_tx = false;
return true;
}
return true;
} else if(event.event == SubGhzCustomEventSceneReceiverInfoTxStop) {
//CC1101 Stop Tx -> next tick event Start RX
// user release OK

View File

@@ -575,13 +575,15 @@ uint8_t mf_ultralight_get_write_end_page(MfUltralightType type) {
furi_assert(
type == MfUltralightTypeUL11 || type == MfUltralightTypeUL21 ||
type == MfUltralightTypeNTAG213 || type == MfUltralightTypeNTAG215 ||
type == MfUltralightTypeNTAG216 || type == MfUltralightTypeOrigin);
type == MfUltralightTypeNTAG216 || type == MfUltralightTypeOrigin ||
type == MfUltralightTypeMfulC);
uint8_t end_page = mf_ultralight_get_config_page_num(type);
if(type == MfUltralightTypeNTAG213 || type == MfUltralightTypeNTAG215 ||
type == MfUltralightTypeNTAG216) {
end_page -= 1;
} else if(type == MfUltralightTypeOrigin) {
} else if(type == MfUltralightTypeOrigin || type == MfUltralightTypeMfulC) {
// ULC: 48 pages total, write pages 4-47 (includes auth config + 3DES key)
end_page = mf_ultralight_features[type].total_pages;
}

View File

@@ -186,7 +186,7 @@ static MfUltralightCommand
uint16_t pages_total = instance->data->pages_total;
MfUltralightCommand command = MfUltralightCommandNotProcessedNAK;
FURI_LOG_T(TAG, "CMD_WRITE");
FURI_LOG_T(TAG, "CMD_WRITE page %d", start_page);
do {
bool do_i2c_check = mf_ultralight_is_i2c_tag(instance->data->type);
@@ -197,12 +197,22 @@ static MfUltralightCommand
break;
}
if(!mf_ultralight_listener_check_access(
instance, start_page, MfUltralightListenerAccessTypeWrite))
break;
// PATCHED: For Ultralight-C, allow writes to pages 44-47 (3DES key area)
// This enables "magic card" emulation for key grabbing
bool is_ulc_key_page = (instance->data->type == MfUltralightTypeMfulC) &&
(start_page >= 44 && start_page <= 47);
if(mf_ultralight_static_lock_check_page(instance->static_lock, start_page)) break;
if(mf_ultralight_dynamic_lock_check_page(instance, start_page)) break;
if(!is_ulc_key_page) {
// Normal access check for all other pages
if(!mf_ultralight_listener_check_access(
instance, start_page, MfUltralightListenerAccessTypeWrite))
break;
if(mf_ultralight_static_lock_check_page(instance->static_lock, start_page)) break;
if(mf_ultralight_dynamic_lock_check_page(instance, start_page)) break;
} else {
FURI_LOG_I(TAG, "MAGIC: Allowing write to ULC key page %d", start_page);
}
const uint8_t* rx_data = bit_buffer_get_data(buffer);
command =

View File

@@ -456,7 +456,8 @@ static NfcCommand mf_ultralight_poller_handler_auth_ultralight_c(MfUltralightPol
if(instance->mfu_event.data->key_request_data.key_provided) {
instance->auth_context.tdes_key = instance->mfu_event.data->key_request_data.key;
} else if(instance->mode == MfUltralightPollerModeDictAttack) {
// TODO: Can logic be rearranged to request this key before reaching mf_ultralight_poller_handler_auth_ultralight_c in poller?
// TODO: -nofl Can logic be rearranged to request this key
// before reaching mf_ultralight_poller_handler_auth_ultralight_c in poller?
FURI_LOG_D(TAG, "No initial key provided, requesting key from dictionary");
// Trigger dictionary key request
instance->mfu_event.type = MfUltralightPollerEventTypeRequestKey;
@@ -697,9 +698,12 @@ static NfcCommand mf_ultralight_poller_handler_request_write_data(MfUltralightPo
instance->mfu_event.type = MfUltralightPollerEventTypeRequestWriteData;
instance->callback(instance->general_event, instance->context);
const MfUltralightData* write_data = instance->mfu_event.data->write_data;
// Save write_data to instance field before any further events clobber the union
instance->write_data = instance->mfu_event.data->write_data;
const MfUltralightData* write_data = instance->write_data;
const MfUltralightData* tag_data = instance->data;
uint32_t features = mf_ultralight_get_feature_support_set(tag_data->type);
instance->write_skip_key = false;
bool check_passed = false;
do {
@@ -737,6 +741,71 @@ static NfcCommand mf_ultralight_poller_handler_request_write_data(MfUltralightPo
check_passed = true;
} while(false);
// ULC: authenticate the target card before writing.
// The read phase left the card unauthenticated (write-mode callback skips read-phase auth).
// Ask callback for keys (cache first, then dict) until one authenticates, then fire
// WriteKeyRequest so the callback can cache the found key and return the skip decision.
if(check_passed &&
mf_ultralight_support_feature(features, MfUltralightFeatureSupportAuthenticate)) {
bool auth_ok = false;
while(!auth_ok) {
// Request next key from callback (tries dict entries)
memset(instance->mfu_event.data, 0, sizeof(MfUltralightPollerEventData));
instance->mfu_event.type = MfUltralightPollerEventTypeRequestKey;
instance->callback(instance->general_event, instance->context);
if(!instance->mfu_event.data->key_request_data.key_provided) {
FURI_LOG_D(TAG, "ULC write: all keys exhausted");
break;
}
instance->auth_context.tdes_key = instance->mfu_event.data->key_request_data.key;
// Halt+activate so the card is in a clean state for auth
iso14443_3a_poller_halt(instance->iso14443_3a_poller);
if(iso14443_3a_poller_activate(instance->iso14443_3a_poller, NULL) !=
Iso14443_3aErrorNone) {
FURI_LOG_E(TAG, "ULC write: card not responding (locked out?)");
break;
}
uint8_t output[MF_ULTRALIGHT_C_AUTH_DATA_SIZE];
uint8_t RndA[MF_ULTRALIGHT_C_AUTH_RND_BLOCK_SIZE] = {0};
furi_hal_random_fill_buf(RndA, sizeof(RndA));
if(mf_ultralight_poller_authenticate_start(instance, RndA, output) !=
MfUltralightErrorNone) {
break;
}
uint8_t decoded_RndA[MF_ULTRALIGHT_C_AUTH_RND_BLOCK_SIZE] = {0};
const uint8_t* RndB = output + MF_ULTRALIGHT_C_AUTH_RND_B_BLOCK_OFFSET;
if(mf_ultralight_poller_authenticate_end(instance, RndB, output, decoded_RndA) !=
MfUltralightErrorNone)
continue;
mf_ultralight_3des_shift_data(RndA);
auth_ok = (memcmp(RndA, decoded_RndA, sizeof(decoded_RndA)) == 0);
FURI_LOG_D(TAG, "ULC write auth attempt: %s", auth_ok ? "success" : "fail");
}
if(auth_ok) {
// Notify callback with the found key: it caches it and returns the skip decision
MfUltralightC3DesAuthKey found_key = instance->auth_context.tdes_key;
memset(instance->mfu_event.data, 0, sizeof(MfUltralightPollerEventData));
instance->mfu_event.data->key_request_data.key = found_key;
instance->mfu_event.data->key_request_data.key_provided = true;
instance->mfu_event.type = MfUltralightPollerEventTypeWriteKeyRequest;
instance->callback(instance->general_event, instance->context);
instance->write_skip_key = instance->mfu_event.data->write_key_skip;
FURI_LOG_D(
TAG,
"ULC write: key %s",
instance->write_skip_key ? "kept (target unchanged)" : "overwrite with source");
} else {
FURI_LOG_E(TAG, "ULC write auth failed - card locked");
check_passed = false;
instance->mfu_event.type = MfUltralightPollerEventTypeCardLocked;
}
}
if(!check_passed) {
iso14443_3a_poller_halt(instance->iso14443_3a_poller);
command = instance->callback(instance->general_event, instance->context);
@@ -751,15 +820,52 @@ static NfcCommand mf_ultralight_poller_handler_write_pages(MfUltralightPoller* i
NfcCommand command = NfcCommandContinue;
do {
const MfUltralightData* write_data = instance->mfu_event.data->write_data;
// Use the saved write_data pointer - the union was overwritten by WriteKeyRequest
const MfUltralightData* write_data = instance->write_data;
uint8_t end_page = mf_ultralight_get_write_end_page(write_data->type);
// If user chose to keep target's key, stop before the ULC key pages (44-47)
if(instance->write_skip_key && write_data->type == MfUltralightTypeMfulC &&
end_page > 44) {
end_page = 44;
}
if(instance->current_page == end_page) {
instance->state = MfUltralightPollerStateWriteSuccess;
break;
}
FURI_LOG_D(TAG, "Writing page %d", instance->current_page);
MfUltralightError error = mf_ultralight_poller_write_page(
instance, instance->current_page, &write_data->page[instance->current_page]);
// For ULC key pages (44-47): byte-order correction required.
// Flipper stores each 8-byte DES sub-key MSB-first; the card expects each half reversed.
// Transform: card_bytes = reverse(flipper[0..7]) || reverse(flipper[8..15])
MfUltralightPage page_to_write;
if(instance->current_page >= 44 && instance->current_page <= 47 &&
write_data->type == MfUltralightTypeMfulC) {
const uint8_t* raw = (const uint8_t*)&write_data->page[44];
uint8_t xformed[16];
for(int i = 0; i < 8; i++) {
xformed[i] = raw[7 - i];
}
for(int i = 0; i < 8; i++) {
xformed[8 + i] = raw[15 - i];
}
uint8_t page_offset = (instance->current_page - 44) * 4;
memcpy(page_to_write.data, xformed + page_offset, 4);
FURI_LOG_D(
TAG,
"Writing KEY page %d (byte-order corrected): %02X %02X %02X %02X",
instance->current_page,
page_to_write.data[0],
page_to_write.data[1],
page_to_write.data[2],
page_to_write.data[3]);
} else {
page_to_write = write_data->page[instance->current_page];
FURI_LOG_D(TAG, "Writing page %d", instance->current_page);
}
MfUltralightError error =
mf_ultralight_poller_write_page(instance, instance->current_page, &page_to_write);
if(error != MfUltralightErrorNone) {
instance->state = MfUltralightPollerStateWriteFail;
instance->error = error;

View File

@@ -28,6 +28,7 @@ typedef enum {
MfUltralightPollerEventTypeWriteSuccess, /**< Poller wrote card successfully. */
MfUltralightPollerEventTypeWriteFail, /**< Poller failed to write card. */
MfUltralightPollerEventTypeRequestKey, /**< Poller requests key for dict attack. */
MfUltralightPollerEventTypeWriteKeyRequest, /**< Poller asks user whether to overwrite 3DES key on target. */
} MfUltralightPollerEventType;
/**
@@ -67,6 +68,7 @@ typedef union {
const MfUltralightData* write_data; /**< Data to be written to card. */
MfUltralightPollerMode poller_mode; /**< Mode to operate in. */
MfUltralightPollerKeyRequestData key_request_data; /**< Key request data. */
bool write_key_skip; /**< Set to true by callback to skip writing 3DES key pages. */
} MfUltralightPollerEventData;
/**

View File

@@ -88,6 +88,8 @@ struct MfUltralightPoller {
uint8_t tearing_flag_read;
uint8_t tearing_flag_total;
uint16_t current_page;
bool write_skip_key; // If true, skip writing pages 44-47 (3DES key) during ULC write
const MfUltralightData* write_data; // Saved pointer to source data for write phase
MfUltralightError error;
mbedtls_des3_context des_context;

View File

@@ -1055,10 +1055,10 @@ static uint32_t subghz_protocol_keeloq_check_remote_controller_selector(
case KEELOQ_LEARNING_SECURE:
bool reset_seed_back = false;
if((strcmp(furi_string_get_cstr(manufacture_code->name), "BFT") == 0)) {
if(instance->seed == 0) {
instance->seed = (fix & 0xFFFFFFF);
reset_seed_back = true;
}
//if(instance->seed == 0) {
instance->seed = (fix & 0xFFFFFFF);
reset_seed_back = true;
//}
}
man = subghz_protocol_keeloq_common_secure_learning(
fix, instance->seed, manufacture_code->key);