Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MIFARE Classic Key Recovery Improvements #3822

Open
wants to merge 91 commits into
base: dev
Choose a base branch
from

Conversation

noproto
Copy link
Contributor

@noproto noproto commented Aug 4, 2024

What's new

  • MIFARE Classic Accelerated dictionary attack: dictionary attacks reduced to several seconds - checks ~3500 keys per second
  • MIFARE Classic Nested attack support: collects nested nonces to be cracked by MFKey, no longer requiring users to downgrade to FW 0.93.0
  • MIFARE Classic Static encrypted backdoor support: collects static encrypted nonces to be cracked by MFKey using NXP/Fudan backdoor, allowing key recovery of all non-hardened MIFARE Classic tags on-device

Verification

This PR currently contains the minimum necessary code to achieve the intended functionality. It will be superseded with performance improvements.

  • Accelerated dictionary attack: Benchmark against standard dictionary attack
    • Note: This PR adds nonce collection methods which trade some of the time reclaimed by this improvement
  • Nested (weak PRNG): Using any weak PRNG tag and MFKey (v3.0 available for testing here)
  • Hardnested (hard PRNG): Verify nonces are stored at /ext/nfc/.nested.log (can be cracked using HardnestedRecovery)
  • Static encrypted (backdoor): Using static encrypted tag and MFKey

Checklist (For Reviewer)

  • PR has description of feature/bug or link to Confluence/Jira task
  • Description contains actions to verify feature/bugfix
  • I've built this code, uploaded it to the device and verified feature/bugfix

@noproto
Copy link
Contributor Author

noproto commented Aug 10, 2024

This PR also resolves bit_buffer_copy_bytes_with_parity improperly storing parity bits: 8dd3daf#diff-1d0dbb15ed26364f785d68ba753267b5326c95629d29641ef53276ac32336f7e

@hedger hedger added the NFC NFC-related label Aug 12, 2024
@noproto
Copy link
Contributor Author

noproto commented Aug 21, 2024

The accelerated dictionary attack is mostly working. I'm tracking down a minor bug in it and making sure the UI reflects the state of the attack.

The fork takes an average of 10 seconds to run dictionary attacks in my tests (1 second of the average is backdoor detection - separate from the dictionary attack). OFW 0.105.0 takes an average of 3 minutes 10 seconds. On real tags the number of unknown keys and the offset in the dictionary are all different, here are five benchmarks on random MIFARE Classic tags:

OFW 0.105.0: 4 min 44 seconds (found 19/32 keys, 16 sectors read)
nestednonces 26845cb: 10 seconds (found 19/32 keys, 16 sectors read)
---
OFW 0.105.0: 5 min 8 seconds (found 18/32 keys, 2 sectors read) 
nestednonces 26845cb: 25 seconds (found 18/32 keys, 2 sectors read)
---
OFW 0.105.0: 22 seconds (found 32/32 keys, 16 sectors read) 
nestednonces 26845cb: 3 seconds (found 32/32 keys, 16 sectors read)
---
OFW 0.105.0: 35 seconds (found 32/32 keys, 16 sectors read) 
nestednonces 26845cb: 10 seconds (found 32/32 keys, 16 sectors read)
---
OFW 0.105.0: 5 min 3 seconds (found 18/32 keys, 16 sectors read) 
nestednonces 26845cb: 10 seconds (found 18/32 keys, 16 sectors read)

@@ -29,7 +34,9 @@ NfcCommand nfc_dict_attack_worker_callback(NfcGenericEvent event, void* context)
} else if(mfc_event->type == MfClassicPollerEventTypeRequestMode) {
const MfClassicData* mfc_data =
nfc_device_get_data(instance->nfc_device, NfcProtocolMfClassic);
mfc_event->data->poller_mode.mode = MfClassicPollerModeDictAttack;
mfc_event->data->poller_mode.mode = (instance->nfc_dict_context.enhanced_dict) ?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see one place where this enhanced_dict is set to true on line 175, which means that we will start from MfClassicPollerModeDictAttackEnhanced. And how MfClassicPollerModeDictAttackStandard will be performed now? I'm asking because the only place where enhanced_dict is set to false is on line 385, in _on_exit callback.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This flag is to indicate whether nested attacks should be attempted. Nested attacks are disabled during the CUID dictionary attack to force the default behavior (MfClassicPollerModeDictAttackStandard), as all of the keys in the CUID dictionary match the properties that the enhanced attack searches for. After enhanced_dict is set to true, it should no longer be set to false except upon exit as the CUID dictionary is tested first - the only time it is needed. The variable is necessary to inform the poller how it should behave during initial phase of the dictionary attack. Standard attacks are still attempted in MfClassicPollerModeDictAttackEnhanced, up until the point the first key is found.

@@ -222,7 +304,27 @@ bool nfc_scene_mf_classic_dict_attack_on_event(void* context, SceneManagerEvent
} else if(event.event == NfcCustomEventDictAttackSkip) {
const MfClassicData* mfc_data = nfc_poller_get_data(instance->poller);
nfc_device_set_data(instance->nfc_device, NfcProtocolMfClassic, mfc_data);
if(state == DictAttackStateUserDictInProgress) {
bool ran_nested_dict = instance->nfc_dict_context.nested_phase !=
Copy link
Contributor

@RebornedBrain RebornedBrain Oct 10, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my opinion, this one is not needed here because we have the same on line 264 which we can move out from if to line 262.

Copy link
Contributor

@RebornedBrain RebornedBrain Oct 10, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And by the way, maybe it would be better to change type of nested_phase to appropriate enum instead of uint8_t? I mean this one https://github.com/flipperdevices/flipperzero-firmware/pull/3822/files#r1795343085

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my opinion, this one is not needed here because we have the same on line 264 which we can move out from if to line 262.

This exists to reduce computation so we don't do the comparison on every custom event.

And by the way, maybe it would be better to change type of nested_phase to appropriate enum instead of uint8_t? I mean this one https://github.com/flipperdevices/flipperzero-firmware/pull/3822/files#r1795343085

Will follow up on your other comment: #3822 (comment)

bool is_nested) {
bool is_nested,
bool backdoor_auth,
bool early_ret) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From my point of view it's not very good to put bool vars into function params. This means that this function doesn't respond to single responsibility principle. Also reading and understanding of such functions requires more effort, in my opinion.
Despite the diff, which says that this problem was here even before (is_nested), we must keep our api as much clean as possible without such things, that's why I spent some time and tried to remove those booleans, it required some efforts, but looks like that it works fine.
Here is the patch with things which I've done: nestednonces_api_patch.patch
Could you please apply it and check whether everything still works as expected?
This is the most important blocker for merging now.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gornekich it would be great if you will take a look, after PR will be updated and approve such changes

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Skimming the patch, dict_attack.h duplicates the include on line 6 and backdoor detection uses FURI_LOG_E (indicating an error) instead of the currently defined log levels. My understanding is that API version 85 in api_symbols.csv is a development artifact. I'm closely reviewing how it handles nested authentication, then I'll test it on each type of card.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The changes in the patch to nfc_scene_mf_classic_dict_attack.c increase total computation performed. #3822 (comment)

mf_classic_poller_send_frame_callback in nfc_test.c needs to have the call updated to match the new prototype of mf_classic_poller_auth.

Copy link
Contributor Author

@noproto noproto Oct 12, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@RebornedBrain As mentioned in my earlier comment, I was closely reviewing how the changes in the patch affected nested authentication. The changes affect a subtle but critical part of the code.

The device must perform almost exactly the same operations in order for the calibration code to have the same runtime as the nested nonce collection. This is a very sensitive operation: the LFSR on the tag (assuming its not static or static encrypted) shifts every ~9.4395 µs. The function call overhead of calling two functions versus one (mf_classic_poller_auth_nested+mf_classic_poller_get_nt_and_update_auth_context versus just mf_classic_poller_get_nt_and_update_auth_context) introduces a timing discrepancy that cannot be detected by the device. During nonce collection, you'll collect nonces but they'll either be offset by an unknown amount (I am not able to get any clocksource on the device to calculate the difference with sufficient precision) or unsolvable.

To verify this issue exists, I used a non-static weak card which I was able to successfully solve with the original PR. I flashed the changes in the patch, it collected multiple invalid nonce pairs during collection (as far as I could tell this affected a majority of collected nonce pairs). Only by flashing back the original changes am I able to find the key immediately.

Further, because the nonce is not located in the correct position, I noted only one third of the possible nonce pairs were collected across the retry attempts (resulting in longer collection times and fewer logged pairs).

I do not believe we should merge this patch if it will disrupt collection for one category of cards: cards with regular weak PRNGs.

Copy link
Contributor

@RebornedBrain RebornedBrain Oct 14, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Skimming the patch, dict_attack.h duplicates the include on line 6 and backdoor detection uses FURI_LOG_E (indicating an error) instead of the currently defined log levels. My understanding is that API version 85 in api_symbols.csv is a development artifact. I'm closely reviewing how it handles nested authentication, then I'll test it on each type of card.

Oh, sorry for that, I did patch before pulling your latest changes, that's why there were duplications, and Log level I increased for tests only, and I agree that it shouldn't be changed

I will investigate your feedback on patch more precisely today.

@@ -505,6 +557,128 @@ NfcCommand mf_classic_poller_handler_request_read_sector_blocks(MfClassicPoller*
return command;
}

NfcCommand mf_classic_poller_handler_analyze_backdoor(MfClassicPoller* instance) {
NfcCommand command = NfcCommandReset;
Copy link
Contributor

@RebornedBrain RebornedBrain Oct 11, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like this logic can be simplified and moved to mf_classic api or even to a separate module, smth like mf_classic_backdoor.c

In that module all logic of testing backdoor can be done, and as a public function there can be something like: mf_classic_test_backdoor(MfClassicBackdoor backdoor_type); or even better MfClassicBackdoor backdoor = mf_classic_find_backdoor(); which will return us value from enum and do all the magic under the hood

This can be placed there: https://github.com/flipperdevices/flipperzero-firmware/pull/3822/files#r1797349885

#define MF_CLASSIC_MAX_BUFF_SIZE (64)

// Ordered by frequency, labeled chronologically
const MfClassicBackdoorKeyPair mf_classic_backdoor_keys[] = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be moved to a separate module with backdoor logic

@noproto noproto mentioned this pull request Oct 13, 2024
3 tasks
@noproto
Copy link
Contributor Author

noproto commented Oct 13, 2024

My understanding of where we're at:

  1. Improved dictionary attack/nested nonce collection. This PR MIFARE Classic Key Recovery Improvements #3822 which is functionally working. The API implementation is being discussed in this review thread. I will open a new PR for additional code-related improvements and performance optimizations.
  2. MFKey included for key recovery. Add MFKey utility for NFC app flow #3943 was opened separately but intended to be merged alongside MIFARE Classic Key Recovery Improvements #3822 so they support each other.
  3. MFKey flow: I'd write this if I could. I don't know how to unload the currently running app and launch a new app from the active application (NFC app). I'm thinking this should be a menu item of "Recover keys" or "Crack keys" that is listed after partially reading a MIFARE Classic card, and launches the MFKey application included in Add MFKey utility for NFC app flow #3943.

@skotopes
Copy link
Member

@noproto sorry for delay, @RebornedBrain and QA being testing this PR against our card library. They found bunch of cards that cannot be read with this PR. They'll bring details later.

@noproto
Copy link
Contributor Author

noproto commented Oct 14, 2024

That is unfortunate. I tested it on 110 MIFARE Classic cards here. Very interested to find out the issue so I can remedy it.

@noproto
Copy link
Contributor Author

noproto commented Oct 16, 2024

@RebornedBrain Could I have an update on this PR? Which cards no longer read specifically?

For example - if it is MIFARE Plus, is it SL1 or SL2? I do not possess a MIFARE Plus card in my collection (all of my cards scan), but I can find members of the community who have certain cards for further testing and to assist us with locating any issue.

Please let me know if there's anything I can do. I've paused development while I wait on your feedback (both on the cards which no longer read and on this thread: #3822 (review) ).

@skotopes
Copy link
Member

@noproto hold on, I'll bring report and test cards here in next couple hours

@skotopes
Copy link
Member

So, @RebornedBrain @doomwastaken did some tests. Here is what they found:

Test 1: no plugins, stuck at particular key

Tests were performed without plugins (remove folder Plugins at "SD Card/apps_data/nfc")
This card stuck at one particular key. In logs I see that process still goes, but
key remains the same, but before keys were changing. See details on the screenshot.

test_1.zip

Test 2: MFC 4k, crash

This MFC 4k (not magic) card where first sector was protected with key A "DEADBEAFFFFF"
which is not present in any dictionaries. Result - crash "Wrong sector num", repeats
every time. This card has backdoor v2.

test_2.zip

Test 3: MFC 1k, can't find key

This MFC 1k (magic) card where first sector was also protected with key A "DEADBEAFFFFF".
Result Flipper cannot find this key. This card has no any backdoor.

test_3.zip

Test 4: Test with plugins

Flipper reads this card totally when there is no plugins (it passes nested attack and etc).
When plugins are present Flipper unable to read it and in logs I see some auth errors.

test_4.zip

Test 5: On this card Flipper stuck

On this card Flipper stuck

test_5.zip

@noproto let us know if you need any additional help/info/etc...

@noproto
Copy link
Contributor Author

noproto commented Oct 16, 2024

Awesome, on it and I'll follow up shortly with fixes and feedback. Thank you for testing the PR!

@noproto
Copy link
Contributor Author

noproto commented Oct 18, 2024

All issues resolved that I could reproduce, please re-test this PR @skotopes @RebornedBrain @doomwastaken:

So, @RebornedBrain @doomwastaken did some tests. Here is what they found:

Test 1: no plugins, stuck at particular key

Tests were performed without plugins (remove folder Plugins at "SD Card/apps_data/nfc") This card stuck at one particular key. In logs I see that process still goes, but key remains the same, but before keys were changing. See details on the screenshot.

test_1.zip

Test 2: MFC 4k, crash

This MFC 4k (not magic) card where first sector was protected with key A "DEADBEAFFFFF" which is not present in any dictionaries. Result - crash "Wrong sector num", repeats every time. This card has backdoor v2.

test_2.zip

Steps taken

  1. Noted power of 2 in both sector and keys found. Possible issue with datatype. Decided to not remove plugins for initial test.
  2. Using NFC Magic, cloned Plantan_white.nfc to a Gen4 UMC magic card
  3. Read card with NFC app. Crashes on sectors read 32/40, keys found 43/80, 33/40 on progress bar, "[CRASH][NfcWorker] furi_check failed" after "Found key candidate"
  4. Dumped call stack:
__furi_crash_implementation@0x08012280 (/home/ubuntu/Flipper/nestednonces/flipperzero-firmware/furi/core/check.c:170)
mf_classic_get_device_name@0x08036452 (/home/ubuntu/Flipper/nestednonces/flipperzero-firmware/lib/nfc/protocols/mf_classic/mf_classic.c:340)
mf_classic_get_first_block_num_of_sector@0x08036d6a (/home/ubuntu/Flipper/nestednonces/flipperzero-firmware/lib/nfc/protocols/mf_classic/mf_classic.c:565)
mf_classic_poller_handler_key_reuse_auth_key_a@0x080420fe (/home/ubuntu/Flipper/nestednonces/flipperzero-firmware/lib/nfc/protocols/mf_classic/mf_classic_poller.c:916)
(..)
  1. Identified issue: reuse_key_sector assigned to 43, when tag only has 40 sectors (0-39). This is because of a bad assumption (sector number = block number / 4, is not true for MFC 4K)
  2. Fixed in 4be9e79
  3. Re-ran dictionary attack, reproduced test 2 crash at end of nonce collection: "[CRASH][NfcWorker] Wrong sector num"
  4. Dumped call stack:
__furi_crash_implementation@0x08012280 (/home/ubuntu/Flipper/nestednonces/flipperzero-firmware/furi/core/check.c:170)
mf_classic_get_sector_trailer_num_by_sector@0x0803666c (/home/ubuntu/Flipper/nestednonces/flipperzero-firmware/lib/nfc/protocols/mf_classic/mf_classic.c:403)
mf_classic_get_sector_trailer_num_by_sector@0x0803666c (/home/ubuntu/Flipper/nestednonces/flipperzero-firmware/lib/nfc/protocols/mf_classic/mf_classic.c:395)
mf_classic_poller_handler_nested_collect_nt_enc@0x080427fa (/home/ubuntu/Flipper/nestednonces/flipperzero-firmware/lib/nfc/protocols/mf_classic/mf_classic_poller.c:1362)
(..)
  1. Identified issue: off by one in nested_target_key, doesn't crash on MFC 1K since no effect besides reduced performance (same issue as "TODO: Fix rare nested_target_key 64 bug")
  2. Fixed in 4be9e79
  3. Test: Read tag, recovered 29 keys with MFKey (78/80 keys), read tag, recovered 2 keys with MFKey, 80/80 keys 40/40 sectors read. Working.
  4. Began re-testing original issues. Removed plugins folder, unable to reproduce. Removed user dictionary, unable to reproduce.

Conclusion

Two issues: faulty state machine logic, target sector on 4K cards. Fixed in 4be9e79

Test 3: MFC 1k, can't find key

This MFC 1k (magic) card where first sector was also protected with key A "DEADBEAFFFFF". Result Flipper cannot find this key. This card has no any backdoor.

test_3.zip

Steps taken

  1. Verified Prox.nfc has sector 1 key A DEADBEAFFFFF
  2. Using NFC Magic, cloned Prox.nfc to a Gen1a magic card
  3. Read card with NFC app. 31/32 keys. 1 nonce collected in .nested.log
  4. Cracked nonce in MFKey.
  5. Returned to NFC app and read card. 32/32, 16/16 sectors read
  6. Flashed second Flipper device with nestednonces fork
  7. Same result

Conclusion

Possible cache issue? Verify no keys are cached for this tag.
If issue persists, debug logs would be useful.

Test 4: Test with plugins

Flipper reads this card totally when there is no plugins (it passes nested attack and etc). When plugins are present Flipper unable to read it and in logs I see some auth errors.

test_4.zip

Steps taken

  1. Ensured NFC plugins folder present on Flipper device
  2. Using NFC Magic, cloned Small_troyka.nfc to a Gen4 UMC magic card
  3. Read card with NFC app. 32/32 keys.
  4. Flashed second Flipper device with nestednonces fork
  5. Reproduced auth errors. Narrowed issue to interaction between nestednonces and troika_parser.fal NFC plugin.
  6. Identified issue: inconsistent assignment of known key and known key type/sector. This is because of a bad assumption (key found in dictionary attack was assumed to be first key)
  7. Fixed in 897817a and db26c85
  8. Re-ran dictionary attack on all card types, works. No longer able to reproduce original issue.

Conclusion

Inconsistent assignment of known key and known key type/sector led to repeated failed authentication attempts. Keys were provided by specific NFC plugins instead of the dictionary attack. Fixed in 897817a and db26c85

Test 5: On this card Flipper stuck

On this card Flipper stuck

test_5.zip

Steps taken

  1. Ensured NFC plugins folder present on Flipper device
  2. Using NFC Magic, cloned Disappeared_corridor.nfc to a Gen4 UMC magic card
  3. Read card with NFC app. 70/80 keys.
  4. Flashed second Flipper device with nestednonces fork
  5. Same result. Troyka card recognized
  6. Removed plugins folder.
  7. Read card with NFC app. 70/80 keys.
  8. Added a user dictionary file to the device with 1 irrelevant key
  9. Read card with NFC app. 70/80 keys. Unable to reproduce original issue.

Conclusion

Likely related to the other (now fixed) issues. Please re-test.
If issue persists, debug logs would be useful.

@noproto
Copy link
Contributor Author

noproto commented Oct 19, 2024

Since #3961 (comment) will delay 1.1, is it still possible to squeeze this PR in too if the issues identified in QA are resolved by #3822 (comment) ?

These changes would allow me to share an early, significantly easier process with the users as well as limit the scope of each PR versus rolling up more changes into this single PR making it unwieldy to do QA. Additionally, it would be useful to collect feedback from users so we can identify any rare or uncommon issues (thanks to the exceptional QA already done, every issue that was reported to me - even prior to the review - has been resolved).

RE: how users could use the nonces collected by this PR, PR 243 to good-faps would make a complete process available: flipperdevices/flipperzero-good-faps#243

Wanted to bring this up for your consideration. I understand if it's not possible. For context, this is what is required for reading cards today. It becomes a two step process with this PR (3822) plus PR 243 to good-faps.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
NFC NFC-related
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants