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

Draft
wants to merge 59 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 and UI 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 [WIP]
  • Hardnested (hard PRNG): Verify nonces are stored at /ext/nfc/.nested.log
  • Static encrypted (backdoor): Using static encrypted tag and MFKey [WIP]

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)

@noproto
Copy link
Contributor Author

noproto commented Sep 4, 2024

I’d consider this PR to be beta quality now. A review would be helpful in guiding the next steps and prioritizing fixes.

Some FAQ:

How do I crack the nonces?: I am working on MFKey 3.0 and a fork of FlipperNestedRecovery. These will be the primary methods of key recovery when I am finished.

It gets stuck on collecting nonces.: This is a calibration issue and will be fixed. The process isn't actually stuck — it's taking longer than expected due to difficulties finding a candidate range.

It crashes after a few reads.: There may be a memory leak. Any help in identifying this issue would be appreciated. Solved: 13411da

There's no progress bar.: This is a UI issue and will be fixed. In the meantime, you can track progress using the header, which shows the current phase and how many keys have been recovered. Solved: c1cdd49 You can also interrupt nonce collection, if you want to only run the accelerated dictionary attack/backdoor read. I'd like to add a preference to toggle nonce collection on/off (default: on).

Hardnested is slow.: I'll be optimizing this over the next few days. The slowdown is related to how Hardnested nonces are collected, but I've identified several ways to possibly improve it. I will run some tests to find the fastest/most reliable approach. Solved: 18f8cfb

@@ -2040,6 +2041,7 @@ Function,-,initstate,char*,"unsigned, char*, size_t"
Function,+,input_get_key_name,const char*,InputKey
Function,+,input_get_type_name,const char*,InputType
Function,-,iprintf,int,"const char*, ..."
Function,+,is_weak_prng_nonce,_Bool,uint32_t
Copy link
Member

Choose a reason for hiding this comment

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

We need to prefix all symbols in crypto1 package if symbols are exported to api symbol table

@@ -5,6 +5,8 @@

#define TAG "NfcMfClassicDictAttack"

// TODO: Update progress bar with nested attacks
Copy link
Member

Choose a reason for hiding this comment

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

TODOs will fail CI/CD lint stage, normally we don't leave them without appropriate issue in task tracker.

command = instance->callback(instance->general_event, instance->context);
// Nested entrypoint
// TODO: Ensure nested attack isn't run if tag is fully read
if(dict_attack_ctx->nested_phase == MfClassicNestedPhaseNone ||
Copy link
Member

Choose a reason for hiding this comment

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

image

@@ -3,13 +3,39 @@
#include "mf_classic_poller.h"
#include <lib/nfc/protocols/iso14443_3a/iso14443_3a_poller_i.h>
#include <bit_lib/bit_lib.h>
#include "nfc/helpers/iso14443_crc.h"
Copy link
Member

Choose a reason for hiding this comment

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

<> - for global includes(outside of this file folder)
"" - for local includes(in this file folder)

@@ -113,7 +113,7 @@ void bit_buffer_copy_bytes_with_parity(BitBuffer* buf, const uint8_t* data, size
uint8_t bit =
FURI_BIT(data[bits_processed / BITS_IN_BYTE + 1], bits_processed % BITS_IN_BYTE);

if(bits_processed % BITS_IN_BYTE) {
if((bits_processed % BITS_IN_BYTE) == 0) {
Copy link
Member

Choose a reason for hiding this comment

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

unit tests maybe?

@skotopes skotopes marked this pull request as draft September 7, 2024 12:58
@skotopes
Copy link
Member

skotopes commented Sep 7, 2024

@noproto that is some very cool thing that you've made )

@skotopes
Copy link
Member

skotopes commented Sep 7, 2024

We'll include this one in next release after @gornekich review and QA testing.
Also we may need to free some RAM to make it more stable, I'll think about it.

Copy link
Member

@gornekich gornekich left a comment

Choose a reason for hiding this comment

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

Hi @noproto
Thank you for working on that! I started reviewing the code, I will come back with the review soon

Comment on lines +539 to +567
size_t current_key_index =
mf_classic_backdoor_keys_count - 1; // Default to the last valid index

// Find the current key in the backdoor_keys array
for(size_t i = 0; i < mf_classic_backdoor_keys_count; i++) {
if(memcmp(
dict_attack_ctx->current_key.data,
mf_classic_backdoor_keys[i].key.data,
sizeof(MfClassicKey)) == 0) {
current_key_index = i;
break;
}
}

// Choose the next key to try
size_t next_key_index = (current_key_index + 1) % mf_classic_backdoor_keys_count;
uint8_t backdoor_version = mf_classic_backdoor_keys[next_key_index].type - 1;

FURI_LOG_E(TAG, "Trying backdoor v%d", backdoor_version);
dict_attack_ctx->current_key = mf_classic_backdoor_keys[next_key_index].key;

// Attempt backdoor authentication
MfClassicError error = mf_classic_poller_auth(
instance, 0, &dict_attack_ctx->current_key, MfClassicKeyTypeA, NULL, true);
if((next_key_index == 0) &&
(error == MfClassicErrorProtocol || error == MfClassicErrorTimeout)) {
FURI_LOG_E(TAG, "No backdoor identified");
dict_attack_ctx->backdoor = MfClassicBackdoorNone;
instance->state = MfClassicPollerStateRequestKey;
Copy link
Member

Choose a reason for hiding this comment

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

@noproto Could you check this part?

Now I can see that you check only first backdoor key (MfClassicBackdoorAuth3), and if backdoor auth is failed we don't check other 2 known backdoor keys. Also the instance->mode_ctx.dict_attack_ctx is always 0-initialized at this point, so I think the current_key_index will be always mf_classic_backdoor_keys_count - 1

Copy link
Contributor Author

@noproto noproto Sep 11, 2024

Choose a reason for hiding this comment

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

It depends on how backdoor authentication fails. If the tag does not respond with a nonce to the backdoor authentication command, we skip detection (as it is an unrecognized command by the tag) and save time. If it does, we continue unless we've failed to authenticate with all known backdoor keys. current_keys_index is initialized to 2 initially, so that the "next" key is 0 (since current_key is initially not a backdoor key) and starts the loop through the backdoor key array.

Verified with v1, v3, and a tag with no backdoor present:

v1:

59810309 [E][MfClassicPoller] Trying backdoor v3
59810423 [E][MfClassicPoller] Trying backdoor v1
59810428 [E][MfClassicPoller] Backdoor identified: v1

v3:

20070 [E][MfClassicPoller] Trying backdoor v3
20075 [E][MfClassicPoller] Backdoor identified: v3

No backdoor:

15543 [E][MfClassicPoller] Trying backdoor v3
15547 [E][MfClassicPoller] No backdoor identified

Copy link
Contributor Author

Choose a reason for hiding this comment

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

MfClassicErrorProtocol is the error code when a nonce is not sent by the tag. Let me know if I can test anything else for this.

@noproto
Copy link
Contributor Author

noproto commented Sep 11, 2024

Here is what I'll resolve before moving this PR back out of draft, will keep this list updated:

  • Fix crash when skipping tag, then re-reading it
  • Make Hardnested nonce collection faster (Faster now but we can still buffer log writes for another 20%-30% speedup)
  • Resolve all TODOs in code (12 remaining)
  • Fix calibration issue, and eliminate outliers
  • Add UI progress bar
  • Resolve all reviews (5 remaining)

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.

5 participants