- app_net.c: replace bare httpd_req_recv with recv_body_capped/alloc helpers (TCP-safe, full-body reads); add OPTIONS/* CORS preflight handler; bump WS broadcast buffer to 2048; add CORS Allow-Methods - CMakeLists (net_service): add http_parser dep for HTTP_OPTIONS - nfc_engine/pn532_core: add nfc_access_lock/unlock mutex, board-RGB quiet helper, UL type detection, general-status improvements - pn532_transport: minor cleanup - main.c: call board_rgb_led_quiet() at boot to kill onboard LED - sdkconfig.defaults: add board RGB Kconfig defaults - README, docs/LIMITATIONS, docs/PINOUT: expand and correct Made-with: Cursor
254 lines
8.6 KiB
C
254 lines
8.6 KiB
C
#include "nfc_engine/nfc_brute.h"
|
|
#include "esp_log.h"
|
|
#include "esp_task_wdt.h"
|
|
#include "esp_timer.h"
|
|
#include "freertos/FreeRTOS.h"
|
|
#include "freertos/task.h"
|
|
#include <string.h>
|
|
|
|
static const char *TAG = "nfc_brute";
|
|
|
|
/* Community default keys (subset from public Proxmark3 / MCT-style lists). */
|
|
static const uint8_t k_builtin[][6] = {
|
|
{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}, {0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
|
|
{0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5}, {0xA5, 0xA4, 0xA3, 0xA2, 0xA1, 0xA0},
|
|
{0x89, 0xEC, 0xA9, 0x7F, 0x8C, 0x2A}, {0x5C, 0x8F, 0xF9, 0x99, 0x0D, 0xA2},
|
|
{0x75, 0xCC, 0xB5, 0x9C, 0x9B, 0xED}, {0xD0, 0x1A, 0xFE, 0xEB, 0x89, 0x0A},
|
|
{0x4B, 0x79, 0x1B, 0xEA, 0x7B, 0xCC}, {0x26, 0x12, 0xC6, 0xDE, 0x84, 0xCA},
|
|
{0x70, 0x7B, 0x11, 0xFC, 0x14, 0x81}, {0x03, 0xF9, 0x06, 0x76, 0x46, 0xAE},
|
|
{0x23, 0x52, 0xC5, 0xB5, 0x6D, 0x85}, {0xB0, 0xB1, 0xB2, 0xB3, 0xB4, 0xB5},
|
|
{0xC0, 0xC1, 0xC2, 0xC3, 0xC4, 0xC5}, {0xD0, 0xD1, 0xD2, 0xD3, 0xD4, 0xD5},
|
|
{0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF}, {0x4D, 0x3A, 0x99, 0xC3, 0x51, 0xDD},
|
|
{0x1A, 0x98, 0x2C, 0x7E, 0x45, 0x9A}, {0xFA, 0xFA, 0xFA, 0xFA, 0xFA, 0xFA},
|
|
{0xFB, 0xFB, 0xFB, 0xFB, 0xFB, 0xFB}, {0xD3, 0xF7, 0xD3, 0xF7, 0xD3, 0xF7},
|
|
{0x5A, 0x1B, 0x85, 0xFC, 0xE2, 0x0A}, {0x71, 0x4C, 0x5C, 0x88, 0x6E, 0x97},
|
|
{0x58, 0x7E, 0xE5, 0xF9, 0x35, 0x0F}, {0xA0, 0x47, 0x8C, 0xC3, 0x90, 0x91},
|
|
{0x53, 0x3C, 0xB6, 0xC7, 0x23, 0xF6}, {0x8F, 0xD0, 0xA4, 0xF2, 0x56, 0xE9},
|
|
{0xE0, 0x00, 0x00, 0x00, 0x00, 0x00}, {0xE7, 0xD6, 0x06, 0x4C, 0x58, 0x60},
|
|
{0xB2, 0x7C, 0xCA, 0xB3, 0x0D, 0xBD}, {0xD2, 0xEC, 0xE8, 0xB9, 0x39, 0x5E},
|
|
{0x14, 0x94, 0xE8, 0x16, 0x63, 0xD7}, {0x7C, 0x9F, 0xB8, 0x47, 0x42, 0x42},
|
|
{0x56, 0x93, 0x69, 0xC5, 0xA0, 0xE5}, {0x63, 0x21, 0x93, 0xBE, 0x1C, 0x3C},
|
|
{0x8E, 0x26, 0x5B, 0xE2, 0x45, 0xBF}, {0xF4, 0x6B, 0x6D, 0xC0, 0xD6, 0xC4},
|
|
{0x2A, 0xA0, 0x5E, 0xD1, 0x85, 0x6F}, {0xAE, 0x3F, 0xF4, 0xEE, 0xA0, 0xDB},
|
|
};
|
|
|
|
#define NBUILTIN (sizeof(k_builtin) / sizeof(k_builtin[0]))
|
|
#define MAX_VARIANTS_PER_KEY 14
|
|
#define MAX_TRIES_BEFORE_WDT 48
|
|
|
|
static bool add_sector_hit(cJSON *hits, uint8_t sector, const uint8_t key[6], const char *key_type)
|
|
{
|
|
if (!hits || !key || !key_type) {
|
|
return false;
|
|
}
|
|
char hx[16];
|
|
for (int i = 0; i < 6; i++) {
|
|
snprintf(hx + i * 2, 3, "%02X", key[i]);
|
|
}
|
|
hx[12] = 0;
|
|
cJSON *h = cJSON_CreateObject();
|
|
if (!h) {
|
|
return false;
|
|
}
|
|
cJSON_AddNumberToObject(h, "sector", sector);
|
|
cJSON_AddStringToObject(h, "keyHex", hx);
|
|
cJSON_AddStringToObject(h, "keyType", key_type);
|
|
cJSON_AddItemToArray(hits, h);
|
|
return true;
|
|
}
|
|
|
|
static int push_variant(const uint8_t base[6], int idx, uint8_t out[6])
|
|
{
|
|
memcpy(out, base, 6);
|
|
switch (idx) {
|
|
case 0:
|
|
return 0;
|
|
case 1:
|
|
out[5] ^= 0xFF;
|
|
return 0;
|
|
case 2:
|
|
out[0] ^= 0xFF;
|
|
return 0;
|
|
case 3:
|
|
out[5] ^= 0xAA;
|
|
return 0;
|
|
case 4:
|
|
out[5] ^= 0x55;
|
|
return 0;
|
|
default: {
|
|
int n = idx - 5;
|
|
if (n >= 0 && n < 10) {
|
|
out[5] = (uint8_t)((out[5] & 0xF0) | (uint8_t)n);
|
|
return 0;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
static bool try_key_on_trailer(nfc_tag_info_t *tag, uint8_t trailer, const uint8_t key[6], bool key_b)
|
|
{
|
|
nfc_mifare_key_t k;
|
|
memcpy(k.key, key, 6);
|
|
k.key_b = key_b;
|
|
return nfc_mifare_authenticate_block(tag, trailer, &k) == ESP_OK;
|
|
}
|
|
|
|
/** Trailer block for MIFARE Classic sector index (0..15 for 1K, 0..39 for 4K). */
|
|
static uint8_t classic_trailer_for_sector(const nfc_tag_info_t *tag, uint8_t sec)
|
|
{
|
|
if (nfc_tag_is_mifare_classic_4k(tag)) {
|
|
if (sec <= 31) {
|
|
return (uint8_t)(sec * 4 + 3);
|
|
}
|
|
if (sec <= 39) {
|
|
return (uint8_t)(128 + (sec - 32) * 16 + 15);
|
|
}
|
|
return 0xFF;
|
|
}
|
|
return (uint8_t)(sec * 4 + 3);
|
|
}
|
|
|
|
cJSON *nfc_mifare_dictionary_attack(nfc_tag_info_t *tag, uint8_t sector_first, uint8_t sector_last,
|
|
const uint8_t *extra, size_t extra_n, bool variations,
|
|
int *attempts_out)
|
|
{
|
|
int64_t t0_us = esp_timer_get_time();
|
|
int attempts = 0;
|
|
cJSON *root = cJSON_CreateObject();
|
|
cJSON *hits = cJSON_CreateArray();
|
|
if (!root || !hits) {
|
|
cJSON_Delete(root);
|
|
cJSON_Delete(hits);
|
|
return NULL;
|
|
}
|
|
|
|
if (!tag) {
|
|
cJSON_Delete(root);
|
|
cJSON_Delete(hits);
|
|
return NULL;
|
|
}
|
|
|
|
if (!nfc_tag_is_mifare_classic(tag)) {
|
|
cJSON_AddStringToObject(root, "error", "not_classic_sak_hint");
|
|
cJSON_AddItemToObject(root, "sectorHits", hits);
|
|
if (attempts_out) {
|
|
*attempts_out = 0;
|
|
}
|
|
return root;
|
|
}
|
|
|
|
uint8_t sector_max = nfc_tag_is_mifare_classic_4k(tag) ? 39 : 15;
|
|
|
|
if (sector_first > sector_last) {
|
|
uint8_t t = sector_first;
|
|
sector_first = sector_last;
|
|
sector_last = t;
|
|
}
|
|
if (sector_first > sector_max) {
|
|
sector_first = sector_max;
|
|
}
|
|
if (sector_last > sector_max) {
|
|
sector_last = sector_max;
|
|
}
|
|
|
|
for (int sec_i = sector_first; sec_i <= sector_last; sec_i++) {
|
|
uint8_t sec = (uint8_t)sec_i;
|
|
uint8_t trailer = classic_trailer_for_sector(tag, sec);
|
|
if (trailer == 0xFF) {
|
|
continue;
|
|
}
|
|
bool got = false;
|
|
|
|
for (size_t bi = 0; bi < NBUILTIN && !got; bi++) {
|
|
uint8_t trial[6];
|
|
int maxv = variations ? MAX_VARIANTS_PER_KEY : 1;
|
|
for (int vi = 0; vi < maxv; vi++) {
|
|
if (push_variant(k_builtin[bi], vi, trial) != 0) {
|
|
break;
|
|
}
|
|
attempts++;
|
|
if (try_key_on_trailer(tag, trailer, trial, false)) {
|
|
if (!add_sector_hit(hits, sec, trial, "A")) {
|
|
cJSON_Delete(hits);
|
|
cJSON_Delete(root);
|
|
return NULL;
|
|
}
|
|
got = true;
|
|
break;
|
|
}
|
|
if (try_key_on_trailer(tag, trailer, trial, true)) {
|
|
if (!add_sector_hit(hits, sec, trial, "B")) {
|
|
cJSON_Delete(hits);
|
|
cJSON_Delete(root);
|
|
return NULL;
|
|
}
|
|
got = true;
|
|
break;
|
|
}
|
|
if ((attempts % MAX_TRIES_BEFORE_WDT) == 0) {
|
|
esp_task_wdt_reset();
|
|
vTaskDelay(pdMS_TO_TICKS(1));
|
|
}
|
|
}
|
|
}
|
|
|
|
for (size_t ei = 0; ei < extra_n && !got; ei++) {
|
|
const uint8_t *ek = extra + ei * 6;
|
|
uint8_t trial[6];
|
|
int maxv = variations ? MAX_VARIANTS_PER_KEY : 1;
|
|
for (int vi = 0; vi < maxv; vi++) {
|
|
if (push_variant(ek, vi, trial) != 0) {
|
|
break;
|
|
}
|
|
attempts++;
|
|
if (try_key_on_trailer(tag, trailer, trial, false)) {
|
|
if (!add_sector_hit(hits, sec, trial, "A")) {
|
|
cJSON_Delete(hits);
|
|
cJSON_Delete(root);
|
|
return NULL;
|
|
}
|
|
got = true;
|
|
break;
|
|
}
|
|
if (try_key_on_trailer(tag, trailer, trial, true)) {
|
|
if (!add_sector_hit(hits, sec, trial, "B")) {
|
|
cJSON_Delete(hits);
|
|
cJSON_Delete(root);
|
|
return NULL;
|
|
}
|
|
got = true;
|
|
break;
|
|
}
|
|
if ((attempts % MAX_TRIES_BEFORE_WDT) == 0) {
|
|
esp_task_wdt_reset();
|
|
vTaskDelay(pdMS_TO_TICKS(1));
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!got) {
|
|
cJSON *h = cJSON_CreateObject();
|
|
if (!h) {
|
|
cJSON_Delete(hits);
|
|
cJSON_Delete(root);
|
|
return NULL;
|
|
}
|
|
cJSON_AddNumberToObject(h, "sector", sec);
|
|
cJSON_AddBoolToObject(h, "miss", true);
|
|
cJSON_AddItemToArray(hits, h);
|
|
}
|
|
}
|
|
|
|
cJSON_AddItemToObject(root, "sectorHits", hits);
|
|
cJSON_AddNumberToObject(root, "attempts", attempts);
|
|
cJSON_AddNumberToObject(root, "durationMs", (double)((esp_timer_get_time() - t0_us) / 1000));
|
|
cJSON_AddStringToObject(root, "note",
|
|
"Dictionary + bounded variants only — not exhaustive 48-bit keyspace.");
|
|
if (attempts_out) {
|
|
*attempts_out = attempts;
|
|
}
|
|
ESP_LOGI(TAG, "dictionary attack attempts=%d", attempts);
|
|
return root;
|
|
}
|