diff --git a/CHANGELOG.md b/CHANGELOG.md index b4bfed418..142709d7c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,9 @@ This project uses the changelog in accordance with [keepchangelog](http://keepac ## [unreleased][unreleased] - Added `hf 14b tearoff` - interactive ST25TB/SRx monotonic counter tear-off attack (@xNovyz) - Fixed missing `WDT_HIT()` in `Get14443bAnswerFromTag()` DMA polling loop causing hardware watchdog reboot on SSC clock stall (@xNovyz) +- Changed `wiegand encode` / `wiegand decode` - added support for encoding and decoding the new 96-bit ASN.1 encoded format, `--bin` encoding, verbose PACS encoding output, and explicit rejection of raw/binary decodes above 96 bits (@cindersocket) +- Added Mifare Classic support to `hf gallagher` command (@pingu2211) +- Added `hf felica discnodes` command (@kormax) - Added `hf mfp dump` command (@apply-science) - Added `hf felica seacinfo` command (@kormax) - Added `hf mfdes bruteisofid` and `hf mfdes selectisofid` commands (@kormax) diff --git a/README.md b/README.md index 8edee679d..56d5f3ba8 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ The Proxmark3 is the swiss-army tool of RFID, allowing for interactions with the | [Linux - Important notes on ModemManager](/doc/md/Installation_Instructions/ModemManager-Must-Be-Discarded.md) | [Validating Proxmark3 Client Functionality](/doc/md/Use_of_Proxmark/1_Validation.md)| | [macOS - Homebrew & Upgrading HomeBrew Tap Formula](/doc/md/Installation_Instructions/macOS-Homebrew-Installation-Instructions.md) | [First Use and Verification](/doc/md/Use_of_Proxmark/2_Configuration-and-Verification.md)| | [macOS - MacPorts](/doc/md/Installation_Instructions/macOS-MacPorts-Installation-Instructions.md) | [Commands & Features](/doc/md/Use_of_Proxmark/3_Commands-and-Features.md)| -| [macOS - Setup and Build](/doc/md/Installation_Instructions/macOS-Compile-From-Source-Instructions.md) || +| [macOS - Setup and Build](/doc/md/Installation_Instructions/macOS-Compile-From-Source-Instructions.md) |[Standalone Modes](/doc/standalone/)| | [Windows - Setup and Build](/doc/md/Installation_Instructions/Windows-Installation-Instructions.md) || | [Termux / Android - Setup and Build](/doc/termux_notes.md) || | [iOS - Setup and Build](/doc/md/Installation_Instructions/iOS-Installation-Instructions.md) @@ -49,6 +49,7 @@ The Proxmark3 is the swiss-army tool of RFID, allowing for interactions with the + ## Notes / helpful documents | Notes ||| diff --git a/armsrc/Standalone/readme.md b/armsrc/Standalone/readme.md index fe4bdfe1e..3066841b8 100644 --- a/armsrc/Standalone/readme.md +++ b/armsrc/Standalone/readme.md @@ -5,6 +5,9 @@ # Table of Contents - [Standalone Modes](#standalone-modes) - [Table of Contents](#table-of-contents) +- [What are standalone modes?](#what-are-standalone-modes) + - [Individual mode documentation](#individual-mode-documentation) +- [Developing Standalone Modes](#developing-standalone-modes) - [Implementing a standalone mode](#implementing-a-standalone-mode) - [Naming your standalone mode](#naming-your-standalone-mode) - [Update MAKEFILE.HAL](#update-makefilehal) @@ -14,7 +17,67 @@ - [Submitting your code](#submitting-your-code) +Standalone modes run directly on the Proxmark3 device without a connected host computer. +See [Developing Standalone Modes](#developing-standalone-modes) for how to build your own. +> Only one (1) mode can be compiled into the firmware at a time (except via [DANKARMULTI](../../doc/standalone/dankarmulti.md)). +## Individual Mode Documentation + +### LF (Low Frequency — 125 kHz) Standalone Modes + +| Mode ID | Document | Description | Hardware | +|---------|----------|-------------|----------| +| LF_SAMYRUN | [SamyRun](../../doc/standalone/lf_samyrun.md) | HID26 read/clone/simulate (Samy Kamkar) | Generic | +| LF_EM4100EMUL | [EM4100 Emulator](../../doc/standalone/lf_em4100emul.md) | Simulate predefined EM4100 tag IDs | Generic | +| LF_EM4100RSWB | [EM4100 RSWB](../../doc/standalone/lf_em4100rswb.md) | Read/simulate/write/brute EM4100 (4 slots) | RDV4 (flash) | +| LF_EM4100RSWW | [EM4100 RSWW](../../doc/standalone/lf_em4100rsww.md) | Read/simulate/write/wipe/validate EM4100 | RDV4 (flash) | +| LF_EM4100RWC | [EM4100 RWC](../../doc/standalone/lf_em4100rwc.md) | Read/simulate/clone EM4100 (16 slots) | RDV4 (flash) | +| LF_HIDBRUTE | [HID Corporate Brute](../../doc/standalone/lf_hidbrute.md) | HID Corporate 1000 card number bruteforce | Generic | +| LF_HIDFCBRUTE | [HID FC Brute](../../doc/standalone/lf_hidfcbrute.md) | HID facility code bruteforce (0–255) | RDV4 (flash) | +| LF_ICEHID | [IceHID Collector](../../doc/standalone/lf_icehid.md) | Multi-format LF credential collector to flash | RDV4 (flash) | +| LF_MULTIHID | [MultiHID](../../doc/standalone/lf_multihid.md) | HID 26-bit multi-card simulator | Generic | +| LF_NEDAP_SIM | [Nedap Simulator](../../doc/standalone/lf_nedap_sim.md) | Nedap RFID simple tag simulator | Generic | +| LF_NEXID | [NexID Collector](../../doc/standalone/lf_nexid.md) | Nexwatch credential collector to flash | RDV4 (flash) | +| LF_PROXBRUTE | [ProxBrute](../../doc/standalone/lf_proxbrute.md) | HID ProxII card number bruteforce | Generic | +| LF_PROX2BRUTE | [Prox2Brute](../../doc/standalone/lf_prox2brute.md) | HID ProxII bruteforce v2 (faster, configurable) | Generic | +| LF_THAREXDE | [Tharexde EM4x50](../../doc/standalone/lf_tharexde.md) | EM4x50 simulate/read/collect | RDV4 (flash) | +| LF_SKELETON | [Skeleton Template](../../doc/standalone/lf_skeleton.md) | Development template for new LF modes | Generic | + +### HF (High Frequency — 13.56 MHz) Standalone Modes + +| Mode ID | Document | Description | Hardware | +|---------|----------|-------------|----------| +| HF_14ASNIFF | [14A Sniffer](../../doc/standalone/hf_14asniff.md) | ISO14443A passive sniffer to flash | RDV4 (flash) | +| HF_14BSNIFF | [14B Sniffer](../../doc/standalone/hf_14bsniff.md) | ISO14443B passive sniffer to flash | RDV4 (flash) | +| HF_15SNIFF | [15693 Sniffer](../../doc/standalone/hf_15sniff.md) | ISO15693 sniffer to flash | RDV4 (flash) | +| HF_15SIM | [15693 Simulator](../../doc/standalone/hf_15sim.md) | ISO15693 dump and simulate | RDV4 (flash) | +| HF_AVEFUL | [Aveful UL Reader](../../doc/standalone/hf_aveful.md) | MIFARE Ultralight read and emulate | Generic | +| HF_BOG | [BogitoRun Auth Sniffer](../../doc/standalone/hf_bog.md) | 14A sniff with ULC/ULEV1/NTAG auth capture | RDV4 (flash) | +| HF_CARDHOPPER | [CardHopper Relay](../../doc/standalone/hf_cardhopper.md) | Long-range 14A relay over serial/IP | RDV4 (BT) | +| HF_COLIN | [VIGIKPWN](../../doc/standalone/hf_colin.md) | MIFARE Classic ultra-fast sniff/sim/clone | RDV4 (flash) | +| HF_CRAFTBYTE | [CraftByte UID Stealer](../../doc/standalone/hf_craftbyte.md) | Scan and emulate ISO14443A UIDs | Generic | +| HF_DOEGOX_AUTH0 | [UL-C/UL-AES Unlocker](../../doc/standalone/hf_doegox_auth0.md) | Unlock password-protected Ultralight tags | Generic | +| HF_EMVPNG | [EMV Visa Reader/Emulator](../../doc/standalone/hf_emvpng.md) | Read Visa EMV cards and emulate transactions | RDV4 (flash) | +| HF_ICECLASS | [IceClass iCLASS](../../doc/standalone/hf_iceclass.md) | iCLASS multi-mode: sim/dump/attack/config | RDV4 (flash) | +| HF_LEGIC | [Legic Prime Reader](../../doc/standalone/hf_legic.md) | Read and simulate Legic Prime tags | Generic | +| HF_LEGICSIM | [Legic Prime Simulator](../../doc/standalone/hf_legicsim.md) | Simulate Legic Prime dumps from flash (15 slots) | RDV4 (flash) | +| HF_MATTYRUN | [MattyRun MFC Clone](../../doc/standalone/hf_mattyrun.md) | MIFARE Classic key check, dump, and emulate | Generic | +| HF_MFCSIM | [MFC Simulator](../../doc/standalone/hf_mfcsim.md) | Simulate MIFARE Classic 1K from flash (15 slots) | RDV4 (flash) | +| HF_MSDSAL | [MSD Visa Reader](../../doc/standalone/hf_msdsal.md) | Read and emulate Visa MSD cards | Generic | +| HF_REBLAY | [Reblay BT Relay](../../doc/standalone/hf_reblay.md) | ISO14443A relay over Bluetooth | RDV4 (BT) | +| HF_ST25_TEAROFF | [ST25TB Tear-off](../../doc/standalone/hf_st25_tearoff.md) | ST25TB store/restore with counter tear-off | RDV4 (flash) | +| HF_TCPRST | [IKEA Rothult](../../doc/standalone/hf_tcprst.md) | IKEA Rothult ST25TA master key dump/emulation | Generic | +| HF_TMUDFORD | [ISO15693 UID Emulator](../../doc/standalone/hf_tmudford.md) | Read and emulate ISO15693 UIDs | Generic | +| HF_UNISNIFF | [Universal Sniffer](../../doc/standalone/hf_unisniff.md) | Multi-protocol sniffer (14A/14B/15/iCLASS) | RDV4 (flash) | +| HF_YOUNG | [Young MFC Sniff/Sim](../../doc/standalone/hf_young.md) | MIFARE sniff/simulation with 2-bank storage | Generic | + +### Multi-Mode Loader + +| Mode ID | Document | Description | +|---------|----------|-------------| +| DANKARMULTI | [Dankarmulti Loader](../../doc/standalone/dankarmulti.md) | Combine multiple standalone modes into one firmware image | + +# Developing Standalone Modes This contains functionality for different StandAlone modes. The fullimage will be built given the correct compiler flags used. Build targets for these files are contained in `Makefile.inc` and `Makefile.hal` If you want to implement a new standalone mode, you need to implement the methods provided in `standalone.h`. diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index a81f64f0d..794c52646 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -359,6 +359,7 @@ set (TARGET_SOURCES ${PM3_ROOT}/client/src/mifare/desfirecore.c ${PM3_ROOT}/client/src/mifare/desfiretest.c ${PM3_ROOT}/client/src/mifare/gallaghercore.c + ${PM3_ROOT}/client/src/mifare/gallaghertest.c ${PM3_ROOT}/client/src/uart/ringbuffer.c ${PM3_ROOT}/client/src/uart/uart_common.c ${PM3_ROOT}/client/src/uart/uart_posix.c diff --git a/client/Makefile b/client/Makefile index 2fe8669ac..e2e2499ef 100644 --- a/client/Makefile +++ b/client/Makefile @@ -795,6 +795,7 @@ SRCS = mifare/aiddesfire.c \ mifare/desfiresecurechan.c \ mifare/desfiretest.c \ mifare/gallaghercore.c \ + mifare/gallaghertest.c \ mifare/mad.c \ mifare/mfkey.c \ mifare/mifare4.c \ diff --git a/client/dictionaries/mfc_default_keys.dic b/client/dictionaries/mfc_default_keys.dic index dc526e43a..37e57815e 100644 --- a/client/dictionaries/mfc_default_keys.dic +++ b/client/dictionaries/mfc_default_keys.dic @@ -3252,3 +3252,10 @@ E058DC0E2C34 # Yerevan Transport 26ABFC3C24FB # +# Vanderbilt Industries, ACT Line +# "ACTact" in ASCII -- appears to hold creds in Sector 1, Block 4 +# Note: "Acces"+N pattern keys (416363657300-4163636573FF) can be generated +# dynamically using: hf mf keygen -k 6 +414354616374 +4D61071B7254 +34016FAC127D diff --git a/client/experimental_lib/CMakeLists.txt b/client/experimental_lib/CMakeLists.txt index fb36dffaa..e3f9b5c53 100644 --- a/client/experimental_lib/CMakeLists.txt +++ b/client/experimental_lib/CMakeLists.txt @@ -279,6 +279,7 @@ set (TARGET_SOURCES ${PM3_ROOT}/client/src/mifare/desfirecore.c ${PM3_ROOT}/client/src/mifare/desfiretest.c ${PM3_ROOT}/client/src/mifare/gallaghercore.c + ${PM3_ROOT}/client/src/mifare/gallaghertest.c ${PM3_ROOT}/client/src/uart/ringbuffer.c ${PM3_ROOT}/client/src/uart/uart_common.c ${PM3_ROOT}/client/src/uart/uart_posix.c diff --git a/client/resources/aidlist.json b/client/resources/aidlist.json index 53828dbc1..5e4a6bcef 100644 --- a/client/resources/aidlist.json +++ b/client/resources/aidlist.json @@ -1535,6 +1535,14 @@ "Description": "", "Type": "EMV" }, + { + "AID": "A000000476416E64726F6964484345", + "Vendor": "Google", + "Country": "N/A", + "Name": "Android HCE", + "Description": "Available on AOSP-based android devices with active HCE", + "Type": "" + }, { "AID": "A000000476A010", "Vendor": "Google", @@ -2275,8 +2283,8 @@ "AID": "A0000008580102", "Vendor": "Apple", "Country": "", - "Name": "Apple Home Key Framework", - "Description": "Home Key configuration applet. Used for attestation exchange", + "Name": "Apple Home Key Step Up", + "Description": "Used for reading the attestation certificate", "Type": "" }, { @@ -2291,8 +2299,8 @@ "AID": "A0000008580202", "Vendor": "Apple", "Country": "", - "Name": "Apple Access Key Framework", - "Description": "Access Key configuration applet. Used for attestation exchange", + "Name": "Apple Access Key Step Up", + "Description": "Used for reading the attestation certificate", "Type": "" }, { @@ -2307,16 +2315,16 @@ "AID": "A000000909ACCE5502", "Vendor": "Connectivity Standards Alliance (CSA)", "Country": "", - "Name": "Aliro Framework", - "Description": "Used during key provisioning, configuration, attestation exchange", + "Name": "Aliro Step Up", + "Description": "Used to retrieve 'access documents' in case a reader needs to verify the validity of a credential", "Type": "" }, { "AID": "A000000909ACCE5501", "Vendor": "Connectivity Standards Alliance (CSA)", "Country": "", - "Name": "Aliro", - "Description": "", + "Name": "Aliro Expedited", + "Description": "Acts as the primary credential holder applet", "Type": "access" }, { diff --git a/client/src/cmdhf14a.c b/client/src/cmdhf14a.c index 403cadd9d..ff21385be 100644 --- a/client/src/cmdhf14a.c +++ b/client/src/cmdhf14a.c @@ -242,7 +242,7 @@ static const hintAIDList_t hintAIDList[] = { { "\x41\x44\x20\x46\x31", 5, "CIPURSE", "hf cipurse" }, { "\xA0\x00\x00\x09\x09\xAC\xCE\x55\x01", 9, "Aliro", "hf aliro" }, { "\xd2\x76\x00\x00\x85\x01\x00", 7, "desfire", "hf mfdes" }, - { "\x4F\x53\x45\x2E\x56\x41\x53\x2E\x30\x31", 10, "Apple VAS", "hf vas"}, + { "\x4F\x53\x45\x2E\x56\x41\x53\x2E\x30\x31", 10, "OSE.VAS", "hf vas"}, }; // iso14a apdu input frame length diff --git a/client/src/cmdhf15.c b/client/src/cmdhf15.c index 1bbdfb951..2637ce8e7 100644 --- a/client/src/cmdhf15.c +++ b/client/src/cmdhf15.c @@ -881,7 +881,7 @@ static int NxpSysInfo(uint8_t *uid) { static int StCheckSig(uint8_t *uid) { // request to be sent to device/card - uint8_t approxlen = 2 + 8 + 1 + 2; + uint16_t approxlen = 2 + ISO15693_UID_LENGTH + 1 + 2; iso15_raw_cmd_t *packet = (iso15_raw_cmd_t *)calloc(1, sizeof(iso15_raw_cmd_t) + approxlen); if (packet == NULL) { PrintAndLogEx(WARNING, "Failed to allocate memory"); @@ -891,19 +891,27 @@ static int StCheckSig(uint8_t *uid) { // ISO15693 Protocol params packet->raw[packet->rawlen++] = arg_get_raw_flag(ISO15693_UID_LENGTH, false, false, false); packet->raw[packet->rawlen++] = ISO15693_READBLOCK; + // add UID (scan, uid) memcpy(packet->raw + packet->rawlen, uid, ISO15693_UID_LENGTH); packet->rawlen += ISO15693_UID_LENGTH; packet->flags = (ISO15_CONNECT | ISO15_READ_RESPONSE | ISO15_NO_DISCONNECT); + uint16_t blkoff = packet->rawlen; char signature_hex[65] = {0}; - for (int j = 0; j < 17; j++) { + + for (int i = 0; i < 17; i++) { + + // reset rawlength counter packet->rawlen = blkoff; + // block no - packet->raw[packet->rawlen++] = 0x3F + j; - // crc + packet->raw[packet->rawlen++] = 0x3F + i; + + // add crc AddCrc15(packet->raw, packet->rawlen); packet->rawlen += 2; + clearCommandBuffer(); SendCommandNG(CMD_HF_ISO15693_COMMAND, (uint8_t *)packet, ISO15_RAW_LEN(packet->rawlen)); PacketResponseNG resp; @@ -913,27 +921,35 @@ static int StCheckSig(uint8_t *uid) { DropField(); return PM3_ETIMEOUT; } + ISO15_ERROR_HANDLING_RESPONSE uint8_t *d = resp.data.asBytes; ISO15_ERROR_HANDLING_CARD_RESPONSE(d, resp.length) - if (j == 0) { + + if (i == 0) { + if (memcmp(d + 1, "K04S", 4) != 0) { // No signature free(packet); return PM3_ESOFT; } + } else { - memcpy(signature_hex + ((j - 1) * 4), d + 1, 4); + memcpy(signature_hex + ((i - 1) * 4), d + 1, 4); } packet->flags = (ISO15_READ_RESPONSE | ISO15_NO_DISCONNECT); } + free(packet); DropField(); + uint8_t signature[16]; - size_t signature_len; + size_t signature_len = 0; hexstr_to_byte_array(signature_hex, signature, &signature_len); + uint8_t uid_swap[ISO15693_UID_LENGTH]; reverse_array_copy(uid, ISO15693_UID_LENGTH, uid_swap); + int index = originality_check_verify_ex(uid_swap, ISO15693_UID_LENGTH, signature, signature_len, PK_ST25TV, false, true); PrintAndLogEx(NORMAL, ""); return originality_check_print(signature, signature_len, index); @@ -1715,7 +1731,7 @@ static int CmdHF15WriteDsfid(const char *Cmd) { } // request to be sent to device/card - uint8_t approxlen = 2 + 8 + 1 + 2; + uint16_t approxlen = 2 + ISO15693_UID_LENGTH + 1 + 2; iso15_raw_cmd_t *packet = (iso15_raw_cmd_t *)calloc(1, sizeof(iso15_raw_cmd_t) + approxlen); if (packet == NULL) { PrintAndLogEx(WARNING, "Failed to allocate memory"); @@ -1835,7 +1851,7 @@ static int CmdHF15Dump(const char *Cmd) { } // request to be sent to device/card - uint8_t approxlen = 2 + 8 + 1 + 2; + uint16_t approxlen = 2 + ISO15693_UID_LENGTH + 1 + 2; iso15_raw_cmd_t *packet = (iso15_raw_cmd_t *)calloc(1, sizeof(iso15_raw_cmd_t) + approxlen); if (packet == NULL) { PrintAndLogEx(WARNING, "Failed to allocate memory"); @@ -2380,7 +2396,7 @@ static int CmdHF15Readblock(const char *Cmd) { } // request to be sent to device/card - uint8_t approxlen = 2 + 8 + 1 + 2; + uint16_t approxlen = 2 + ISO15693_UID_LENGTH + 1 + 2; iso15_raw_cmd_t *packet = (iso15_raw_cmd_t *)calloc(1, sizeof(iso15_raw_cmd_t) + approxlen); if (packet == NULL) { PrintAndLogEx(WARNING, "Failed to allocate memory"); diff --git a/client/src/cmdhffelica.c b/client/src/cmdhffelica.c index 31db2668e..6ed000b8d 100644 --- a/client/src/cmdhffelica.c +++ b/client/src/cmdhffelica.c @@ -47,6 +47,9 @@ #define FELICA_DEFAULT_TIMEOUT_MS 2000U #define FELICA_DEFAULT_RETRY_COUNT 3U +#define FELICA_DISCOVER_DEFAULT_RETRY_COUNT 5U +#define FELICA_DISCOVERY_RETRY_BACKOFF_MS 1000U +#define FELICA_TARGET_PRESENCE_ATTEMPTS 3U #define FELICA_PLATFORM_INFO_MAX_LEN 64U #define FELICA_PLATFORM_INFO_WITH_MAC_INFO_LEN 25U #define FELICA_PLATFORM_INFO_WITH_MAC_LEN 20U @@ -63,20 +66,211 @@ #define FELICA_SERVICE_ATTRIBUTE_RANDOM_ACCESS (0b001000) #define FELICA_SERVICE_ATTRIBUTE_CYCLIC (0b001100) #define FELICA_SERVICE_ATTRIBUTE_PURSE (0b010000) +#define FELICA_SERVICE_ATTRIBUTE_PIN_REQUIRED (0b100000) #define FELICA_SERVICE_ATTRIBUTE_PURSE_SUBFIELD (0b000110) +#define FELICA_AREA_ATTRIBUTE_CAN_CREATE_SUBAREA 0x00U +#define FELICA_AREA_ATTRIBUTE_CANNOT_CREATE_SUBAREA 0x01U +#define FELICA_AREA_ATTRIBUTE_CAN_CREATE_SUBAREA_WITH_PIN 0x20U +#define FELICA_AREA_ATTRIBUTE_CANNOT_CREATE_SUBAREA_WITH_PIN 0x21U +#define FELICA_AREA_ATTRIBUTE_END_ROOT_AREA 0x3EU +#define FELICA_AREA_ATTRIBUTE_END_SUB_AREA 0x3FU + +#define FELICA_SERVICE_ATTRIBUTE_RANDOM_RW_WITH_KEY 0x08U +#define FELICA_SERVICE_ATTRIBUTE_RANDOM_RW_WITHOUT_KEY 0x09U +#define FELICA_SERVICE_ATTRIBUTE_RANDOM_RO_WITH_KEY 0x0AU +#define FELICA_SERVICE_ATTRIBUTE_RANDOM_RO_WITHOUT_KEY 0x0BU +#define FELICA_SERVICE_ATTRIBUTE_CYCLIC_RW_WITH_KEY 0x0CU +#define FELICA_SERVICE_ATTRIBUTE_CYCLIC_RW_WITHOUT_KEY 0x0DU +#define FELICA_SERVICE_ATTRIBUTE_CYCLIC_RO_WITH_KEY 0x0EU +#define FELICA_SERVICE_ATTRIBUTE_CYCLIC_RO_WITHOUT_KEY 0x0FU +#define FELICA_SERVICE_ATTRIBUTE_PURSE_RW_WITH_KEY 0x10U +#define FELICA_SERVICE_ATTRIBUTE_PURSE_RW_WITHOUT_KEY 0x11U +#define FELICA_SERVICE_ATTRIBUTE_PURSE_CASHBACK_WITH_KEY 0x12U +#define FELICA_SERVICE_ATTRIBUTE_PURSE_CASHBACK_WITHOUT_KEY 0x13U +#define FELICA_SERVICE_ATTRIBUTE_PURSE_DECREMENT_WITH_KEY 0x14U +#define FELICA_SERVICE_ATTRIBUTE_PURSE_DECREMENT_WITHOUT_KEY 0x15U +#define FELICA_SERVICE_ATTRIBUTE_PURSE_RO_WITH_KEY 0x16U +#define FELICA_SERVICE_ATTRIBUTE_PURSE_RO_WITHOUT_KEY 0x17U + +#define FELICA_SERVICE_ATTRIBUTE_RANDOM_RW_WITH_KEY_WITH_PIN 0x28U +#define FELICA_SERVICE_ATTRIBUTE_RANDOM_RW_WITHOUT_KEY_WITH_PIN 0x29U +#define FELICA_SERVICE_ATTRIBUTE_RANDOM_RO_WITH_KEY_WITH_PIN 0x2AU +#define FELICA_SERVICE_ATTRIBUTE_RANDOM_RO_WITHOUT_KEY_WITH_PIN 0x2BU +#define FELICA_SERVICE_ATTRIBUTE_CYCLIC_RW_WITH_KEY_WITH_PIN 0x2CU +#define FELICA_SERVICE_ATTRIBUTE_CYCLIC_RW_WITHOUT_KEY_WITH_PIN 0x2DU +#define FELICA_SERVICE_ATTRIBUTE_CYCLIC_RO_WITH_KEY_WITH_PIN 0x2EU +#define FELICA_SERVICE_ATTRIBUTE_CYCLIC_RO_WITHOUT_KEY_WITH_PIN 0x2FU +#define FELICA_SERVICE_ATTRIBUTE_PURSE_RW_WITH_KEY_WITH_PIN 0x30U +#define FELICA_SERVICE_ATTRIBUTE_PURSE_RW_WITHOUT_KEY_WITH_PIN 0x31U +#define FELICA_SERVICE_ATTRIBUTE_PURSE_CASHBACK_WITH_KEY_WITH_PIN 0x32U +#define FELICA_SERVICE_ATTRIBUTE_PURSE_CASHBACK_WITHOUT_KEY_WITH_PIN 0x33U +#define FELICA_SERVICE_ATTRIBUTE_PURSE_DECREMENT_WITH_KEY_WITH_PIN 0x34U +#define FELICA_SERVICE_ATTRIBUTE_PURSE_DECREMENT_WITHOUT_KEY_WITH_PIN 0x35U +#define FELICA_SERVICE_ATTRIBUTE_PURSE_RO_WITH_KEY_WITH_PIN 0x36U +#define FELICA_SERVICE_ATTRIBUTE_PURSE_RO_WITHOUT_KEY_WITH_PIN 0x37U + +#define FELICA_REQUEST_SERVICE_DISCOVERY_BATCH_SIZE 16U +#define FELICA_MAX_NODE_NUMBER 0x03FFU +#define FELICA_PRESENCE_SERVICE_CODE_LE ((uint16_t)FELICA_SERVICE_ATTRIBUTE_RANDOM_RO_WITHOUT_KEY) + +typedef struct { + uint8_t attribute; + bool is_area; + bool with_key; + bool with_pin; +} felica_request_service_probe_attribute_t; + +static const felica_request_service_probe_attribute_t FELICA_REQUEST_SERVICE_PROBE_ATTRIBUTES[] = { + {FELICA_AREA_ATTRIBUTE_CAN_CREATE_SUBAREA, true, true, false}, + {FELICA_AREA_ATTRIBUTE_CANNOT_CREATE_SUBAREA, true, true, false}, + {FELICA_SERVICE_ATTRIBUTE_RANDOM_RW_WITH_KEY, false, true, false}, + {FELICA_SERVICE_ATTRIBUTE_RANDOM_RW_WITHOUT_KEY, false, false, false}, + {FELICA_SERVICE_ATTRIBUTE_RANDOM_RO_WITH_KEY, false, true, false}, + {FELICA_SERVICE_ATTRIBUTE_RANDOM_RO_WITHOUT_KEY, false, false, false}, + {FELICA_SERVICE_ATTRIBUTE_CYCLIC_RW_WITH_KEY, false, true, false}, + {FELICA_SERVICE_ATTRIBUTE_CYCLIC_RW_WITHOUT_KEY, false, false, false}, + {FELICA_SERVICE_ATTRIBUTE_CYCLIC_RO_WITH_KEY, false, true, false}, + {FELICA_SERVICE_ATTRIBUTE_CYCLIC_RO_WITHOUT_KEY, false, false, false}, + {FELICA_SERVICE_ATTRIBUTE_PURSE_RW_WITH_KEY, false, true, false}, + {FELICA_SERVICE_ATTRIBUTE_PURSE_RW_WITHOUT_KEY, false, false, false}, + {FELICA_SERVICE_ATTRIBUTE_PURSE_CASHBACK_WITH_KEY, false, true, false}, + {FELICA_SERVICE_ATTRIBUTE_PURSE_CASHBACK_WITHOUT_KEY, false, false, false}, + {FELICA_SERVICE_ATTRIBUTE_PURSE_DECREMENT_WITH_KEY, false, true, false}, + {FELICA_SERVICE_ATTRIBUTE_PURSE_DECREMENT_WITHOUT_KEY, false, false, false}, + {FELICA_SERVICE_ATTRIBUTE_PURSE_RO_WITH_KEY, false, true, false}, + {FELICA_SERVICE_ATTRIBUTE_PURSE_RO_WITHOUT_KEY, false, false, false}, + {FELICA_AREA_ATTRIBUTE_CAN_CREATE_SUBAREA_WITH_PIN, true, true, true}, + {FELICA_AREA_ATTRIBUTE_CANNOT_CREATE_SUBAREA_WITH_PIN, true, true, true}, + {FELICA_SERVICE_ATTRIBUTE_RANDOM_RW_WITH_KEY_WITH_PIN, false, true, true}, + {FELICA_SERVICE_ATTRIBUTE_RANDOM_RW_WITHOUT_KEY_WITH_PIN, false, false, true}, + {FELICA_SERVICE_ATTRIBUTE_RANDOM_RO_WITH_KEY_WITH_PIN, false, true, true}, + {FELICA_SERVICE_ATTRIBUTE_RANDOM_RO_WITHOUT_KEY_WITH_PIN, false, false, true}, + {FELICA_SERVICE_ATTRIBUTE_CYCLIC_RW_WITH_KEY_WITH_PIN, false, true, true}, + {FELICA_SERVICE_ATTRIBUTE_CYCLIC_RW_WITHOUT_KEY_WITH_PIN, false, false, true}, + {FELICA_SERVICE_ATTRIBUTE_CYCLIC_RO_WITH_KEY_WITH_PIN, false, true, true}, + {FELICA_SERVICE_ATTRIBUTE_CYCLIC_RO_WITHOUT_KEY_WITH_PIN, false, false, true}, + {FELICA_SERVICE_ATTRIBUTE_PURSE_RW_WITH_KEY_WITH_PIN, false, true, true}, + {FELICA_SERVICE_ATTRIBUTE_PURSE_RW_WITHOUT_KEY_WITH_PIN, false, false, true}, + {FELICA_SERVICE_ATTRIBUTE_PURSE_CASHBACK_WITH_KEY_WITH_PIN, false, true, true}, + {FELICA_SERVICE_ATTRIBUTE_PURSE_CASHBACK_WITHOUT_KEY_WITH_PIN, false, false, true}, + {FELICA_SERVICE_ATTRIBUTE_PURSE_DECREMENT_WITH_KEY_WITH_PIN, false, true, true}, + {FELICA_SERVICE_ATTRIBUTE_PURSE_DECREMENT_WITHOUT_KEY_WITH_PIN, false, false, true}, + {FELICA_SERVICE_ATTRIBUTE_PURSE_RO_WITH_KEY_WITH_PIN, false, true, true}, + {FELICA_SERVICE_ATTRIBUTE_PURSE_RO_WITHOUT_KEY_WITH_PIN, false, false, true}, +}; + +typedef enum { + FELICA_NODE_DISCOVERY_NONE = 0, + FELICA_NODE_DISCOVERY_REQUEST_CODE_LIST, + FELICA_NODE_DISCOVERY_SEARCH_SERVICE_CODE, + FELICA_NODE_DISCOVERY_REQUEST_SERVICE, + FELICA_NODE_DISCOVERY_READ_WITHOUT_ENCRYPTION, +} felica_node_discovery_method_t; + +typedef struct { + bool is_area; + uint16_t node_code_le; + bool has_end_code; + uint16_t end_code_le; +} felica_discovered_node_t; + +typedef int (*felica_node_discovery_visitor_t)(const felica_discovered_node_t *node, void *ctx); +typedef bool (*felica_node_discovery_runner_t)(uint8_t *flags, + const uint8_t *idm, + uint32_t retry_count, + felica_node_discovery_visitor_t visitor, + void *ctx, + uint32_t *discovered_count, + int *stop_status); + +typedef struct { + felica_node_discovery_method_t method; + const char *cli_name; + const char *display_name; + felica_node_discovery_runner_t run; +} felica_node_discovery_method_info_t; + +typedef struct { + uint32_t area_count; + uint32_t service_count; + uint16_t area_end_stack[8]; + int depth; + bool header_printed; +} felica_scsvcode_context_t; + +typedef struct { + uint8_t *flags; + uint8_t block_frame[PM3_CMD_DATA_SIZE]; + uint16_t block_datalen; + uint32_t retry_count; + uint32_t service_count; + uint32_t public_service_count; +} felica_dump_context_t; + +typedef enum { + FELICA_IDM_RESOLVE_STANDALONE = 0, + FELICA_IDM_RESOLVE_CHAINED, +} felica_idm_resolution_mode_t; + + static int CmdHelp(const char *Cmd); static void clear_and_send_command(uint8_t flags, uint16_t datalen, uint8_t *data, bool verbose); static int send_felica_payload_with_retries(uint8_t flags, uint16_t datalen, uint8_t *data, bool verbose, - int expected_response_cmd, uint32_t timeout_ms, uint32_t retries, bool logging, - PacketResponseNG *resp, const char *request_name); + int expected_response_cmd, uint32_t timeout_ms, uint32_t retries, uint32_t backoff_ms, bool logging, + PacketResponseNG *resp, const char *request_name); +static bool felica_discover_nodes_with_request_code_list(uint8_t *flags, + const uint8_t *idm, + uint32_t retry_count, + felica_node_discovery_visitor_t visitor, + void *ctx, + uint32_t *discovered_count, + int *stop_status); +static bool felica_discover_nodes_with_search_service_code(uint8_t *flags, + const uint8_t *idm, + uint32_t retry_count, + felica_node_discovery_visitor_t visitor, + void *ctx, + uint32_t *discovered_count, + int *stop_status); +static bool felica_discover_nodes_with_request_service(uint8_t *flags, + const uint8_t *idm, + uint32_t retry_count, + felica_node_discovery_visitor_t visitor, + void *ctx, + uint32_t *discovered_count, + int *stop_status); +static bool felica_discover_nodes_with_read_without_encryption(uint8_t *flags, + const uint8_t *idm, + uint32_t retry_count, + felica_node_discovery_visitor_t visitor, + void *ctx, + uint32_t *discovered_count, + int *stop_status); +static const felica_node_discovery_method_info_t *felica_get_node_discovery_method_info(felica_node_discovery_method_t method); +static const char *felica_node_discovery_method_display_name(felica_node_discovery_method_t method); +static void felica_print_node_discovery_method_used(felica_node_discovery_method_t method); +static int felica_compare_discovered_nodes(const void *lhs, const void *rhs); static felica_card_select_t last_known_card; static void set_last_known_card(felica_card_select_t card) { last_known_card = card; } +static void felica_set_last_known_idm(const uint8_t *idm) { + if (idm == NULL) { + return; + } + + if (memcmp(last_known_card.IDm, idm, sizeof(last_known_card.IDm)) == 0) { + return; + } + + memset(&last_known_card, 0, sizeof(last_known_card)); + memcpy(last_known_card.IDm, idm, sizeof(last_known_card.IDm)); +} + static void print_status_flag1_interpretation(void) { PrintAndLogEx(NORMAL, ""); PrintAndLogEx(INFO, _CYAN_("Status Flag 1")); @@ -502,7 +696,7 @@ static int info_seac(void) { if (send_felica_payload_with_retries(seac_flags, sizeof(seac_poll_frames[i]), (uint8_t *)seac_poll_frames[i], false, -1, FELICA_SEAC_POLL_TIMEOUT_MS, FELICA_SEAC_POLL_RETRY_COUNT, - false, &resp, NULL) != PM3_SUCCESS) { + 0, false, &resp, NULL) != PM3_SUCCESS) { continue; } @@ -526,20 +720,6 @@ static int info_seac(void) { } -/** - * Adds the last known IDm (8-Byte) to the data frame. - * @param position start of where the IDm is added within the frame. - * @param data frame in where the IDM is added. - * @return true if IDm was added; - */ -static bool add_last_IDm(uint8_t position, uint8_t *data) { - if (last_known_card.IDm[0] != 0 && last_known_card.IDm[1] != 0) { - memcpy(data + position, last_known_card.IDm, sizeof(last_known_card.IDm)); - return true; - } - return false; -} - static int CmdHFFelicaList(const char *Cmd) { return CmdTraceListAlias(Cmd, "hf felica", "felica"); } @@ -627,7 +807,7 @@ static int send_get_container_id(uint8_t flags, uint16_t datalen, uint8_t *data, if (send_felica_payload_with_retries(flags, datalen, data, false, FELICA_GET_CONTAINER_ID_ACK, FELICA_OPTIONAL_CMD_TIMEOUT_MS, FELICA_OPTIONAL_CMD_RETRIES, - false, &resp, "get container id") != PM3_SUCCESS) { + 0, false, &resp, "get container id") != PM3_SUCCESS) { return PM3_ERFTRANS; } @@ -653,7 +833,7 @@ static int send_get_container_property(uint8_t flags, uint16_t datalen, uint8_t if (send_felica_payload_with_retries(flags, datalen, data, false, FELICA_GET_CONTAINER_PROPERTY_ACK, FELICA_OPTIONAL_CMD_TIMEOUT_MS, FELICA_OPTIONAL_CMD_RETRIES, - false, &resp, "get container property") != PM3_SUCCESS) { + 0, false, &resp, "get container property") != PM3_SUCCESS) { return PM3_ERFTRANS; } @@ -693,7 +873,7 @@ static int send_get_container_issue_information(uint8_t flags, uint16_t datalen, if (send_felica_payload_with_retries(flags, datalen, data, false, FELICA_GET_CONTAINER_ISSUE_INFO_ACK, FELICA_OPTIONAL_CMD_TIMEOUT_MS, FELICA_OPTIONAL_CMD_RETRIES, - false, &resp, "get container issue info") != PM3_SUCCESS) { + 0, false, &resp, "get container issue info") != PM3_SUCCESS) { return PM3_ERFTRANS; } @@ -719,7 +899,7 @@ static int send_get_platform_information(uint8_t flags, uint16_t datalen, uint8_ if (send_felica_payload_with_retries(flags, datalen, data, false, FELICA_GETPLATFORMINFO_ACK, FELICA_OPTIONAL_CMD_TIMEOUT_MS, FELICA_OPTIONAL_CMD_RETRIES, - false, &resp, "get platform info") != PM3_SUCCESS) { + 0, false, &resp, "get platform info") != PM3_SUCCESS) { return PM3_ERFTRANS; } @@ -771,7 +951,7 @@ static int send_request_specification_version(uint8_t flags, uint16_t datalen, u if (send_felica_payload_with_retries(flags, datalen, data, verbose, FELICA_REQUEST_SPEC_VERSION_ACK, timeout_ms, retries, - logging, &resp, "request specification version") != PM3_SUCCESS) { + 0, logging, &resp, "request specification version") != PM3_SUCCESS) { return PM3_ERFTRANS; } @@ -1089,19 +1269,6 @@ static void print_read_without_encryption_response(felica_read_without_encryptio } } -/** - * Shared retry log helper. - */ -static void log_felica_retry_attempt(const char *request_name, uint32_t attempt, uint32_t retries) { - if (request_name) { - PrintAndLogEx(WARNING, "Retrying %s (%" PRIu32 "/%" PRIu32 ")", - request_name, attempt + 1, retries); - } else { - PrintAndLogEx(WARNING, "Retrying request (%" PRIu32 "/%" PRIu32 ")", - attempt + 1, retries); - } -} - /** * Generic FeliCa command sender with timeout and retries. * @param flags command flags @@ -1116,30 +1283,49 @@ static void log_felica_retry_attempt(const char *request_name, uint32_t attempt, * @return PM3_SUCCESS on success */ static int send_felica_payload_with_retries(uint8_t flags, uint16_t datalen, uint8_t *data, bool verbose, - int expected_response_cmd, uint32_t timeout_ms, uint32_t retries, bool logging, - PacketResponseNG *resp, const char *request_name) { + int expected_response_cmd, uint32_t timeout_ms, uint32_t retries, uint32_t backoff_ms, bool logging, + PacketResponseNG *resp, const char *request_name) { for (uint32_t attempt = 0; attempt <= retries; attempt++) { + if (attempt > 0) { + if (logging) { + if (request_name) { + PrintAndLogEx(WARNING, "Retrying %s (%" PRIu32 "/%" PRIu32 ")", + request_name, attempt, retries); + } else { + PrintAndLogEx(WARNING, "Retrying request (%" PRIu32 "/%" PRIu32 ")", + attempt, retries); + } + } + uint32_t backoff_delay_ms = 0; + if (backoff_ms > 0) { + static const uint32_t schedule_ms[] = {0U, 100U, 200U, 500U, 1000U}; + size_t index = (size_t)(attempt - 1U); + if (index >= ARRAYLEN(schedule_ms)) { + index = ARRAYLEN(schedule_ms) - 1U; + } + backoff_delay_ms = schedule_ms[index]; + if (backoff_delay_ms > backoff_ms) { + backoff_delay_ms = backoff_ms; + } + } + if (backoff_delay_ms > 0) { + msleep(backoff_delay_ms); + } + } + clear_and_send_command(flags, datalen, data, verbose); if (waitCmdFelicaEx(false, resp, verbose, logging, timeout_ms) == false) { - if (logging && attempt < retries) { - log_felica_retry_attempt(request_name, attempt, retries); - } continue; } if (expected_response_cmd >= 0) { if (resp->length < sizeof(felica_frame_response_noidm_t)) { - if (logging && attempt < retries) { - log_felica_retry_attempt(request_name, attempt, retries); - } continue; } const felica_frame_response_noidm_t *frame_response = (const felica_frame_response_noidm_t *)resp->data.asBytes; if (frame_response->cmd_code[0] != (uint8_t)expected_response_cmd) { - if (logging && attempt < retries) { - log_felica_retry_attempt(request_name, attempt, retries); - } else if (logging) { + if (logging && attempt == retries) { PrintAndLogEx(FAILED, "Bad response cmd 0x%02X (expected 0x%02X).", frame_response->cmd_code[0], (uint8_t)expected_response_cmd); } @@ -1162,10 +1348,10 @@ int send_request_service(uint8_t flags, uint16_t datalen, uint8_t *data, bool ve } PacketResponseNG resp; if (send_felica_payload_with_retries(flags, datalen, data, verbose, - 0x03, - FELICA_DEFAULT_TIMEOUT_MS, 0, - true, - &resp, "request service") != PM3_SUCCESS) { + 0x03, + FELICA_DEFAULT_TIMEOUT_MS, 0, + 0, true, + &resp, "request service") != PM3_SUCCESS) { PrintAndLogEx(ERR, "\nGot no response from card"); return PM3_ERFTRANS; } @@ -1193,13 +1379,15 @@ int send_request_service(uint8_t flags, uint16_t datalen, uint8_t *data, bool ve */ static int send_read_without_encryption_ex(uint8_t flags, uint16_t datalen, uint8_t *data, bool verbose, felica_read_without_encryption_response_t *rd_noCry_resp, - uint32_t timeout_ms, uint32_t retries) { + uint32_t timeout_ms, uint32_t retries, uint32_t backoff_ms, bool logging) { PacketResponseNG resp; if (send_felica_payload_with_retries(flags, datalen, data, verbose, 0x07, timeout_ms, retries, - true, + backoff_ms, logging, &resp, "read without encryption") != PM3_SUCCESS) { - PrintAndLogEx(ERR, "No response from card"); + if (logging) { + PrintAndLogEx(ERR, "No response from card"); + } return PM3_ERFTRANS; } @@ -1214,7 +1402,166 @@ static int send_read_without_encryption_ex(uint8_t flags, uint16_t datalen, uint static int send_read_without_encryption(uint8_t flags, uint16_t datalen, uint8_t *data, bool verbose, felica_read_without_encryption_response_t *rd_noCry_resp) { return send_read_without_encryption_ex(flags, datalen, data, verbose, rd_noCry_resp, - FELICA_DEFAULT_TIMEOUT_MS, 0); + FELICA_DEFAULT_TIMEOUT_MS, 0, 0, true); +} + +static int felica_discover_target(felica_card_select_t *card) { + if (card == NULL) { + return PM3_EINVARG; + } + + int last_status = PM3_ETIMEOUT; + for (uint32_t attempt = 0; attempt < FELICA_TARGET_PRESENCE_ATTEMPTS; attempt++) { + clear_and_send_command(FELICA_CONNECT, 0, NULL, false); + + PacketResponseNG resp; + if (WaitForResponseTimeout(CMD_HF_FELICA_COMMAND, &resp, 2500) == false) { + last_status = PM3_ETIMEOUT; + DropField(); + continue; + } + + if (resp.status != PM3_SUCCESS) { + last_status = resp.status; + DropField(); + continue; + } + + if (resp.length < sizeof(*card)) { + last_status = PM3_ESOFT; + DropField(); + continue; + } + + memcpy(card, resp.data.asBytes, sizeof(*card)); + set_last_known_card(*card); + DropField(); + return PM3_SUCCESS; + } + + return last_status; +} + +// Presence is checked by issuing ReadWithoutEncryption against service number 0 +// with the unauthenticated random-read attribute and verifying the response IDm. +static int felica_presence_check_idm(const uint8_t *idm) { + if (idm == NULL) { + return PM3_EINVARG; + } + + uint8_t data[16] = {0}; + data[0] = sizeof(data); + data[1] = FELICA_RDBLK_REQ; + memcpy(data + 2, idm, 8); + data[10] = 0x01; + data[11] = FELICA_PRESENCE_SERVICE_CODE_LE & 0xFF; + data[12] = (FELICA_PRESENCE_SERVICE_CODE_LE >> 8) & 0xFF; + data[13] = 0x01; + data[14] = 0x80; + data[15] = 0x00; + + PacketResponseNG resp; + const uint8_t flags = FELICA_CONNECT | FELICA_NO_SELECT | FELICA_APPEND_CRC | FELICA_RAW; + const int ret = send_felica_payload_with_retries(flags, sizeof(data), data, false, + FELICA_RDBLK_ACK, + FELICA_DEFAULT_TIMEOUT_MS, FELICA_TARGET_PRESENCE_ATTEMPTS - 1U, + 0, false, + &resp, "presence check"); + DropField(); + if (ret != PM3_SUCCESS) { + return ret; + } + + if (resp.length < sizeof(felica_frame_response_t)) { + return PM3_ESOFT; + } + + const felica_frame_response_t *frame_response = (const felica_frame_response_t *)resp.data.asBytes; + if (memcmp(frame_response->IDm, idm, sizeof(frame_response->IDm)) != 0) { + return PM3_ERFTRANS; + } + + return PM3_SUCCESS; +} + +static int felica_ensure_target_present(const uint8_t *custom_idm, + size_t custom_idm_len, + felica_idm_resolution_mode_t mode, + uint8_t *idm_out) { + if (idm_out == NULL) { + return PM3_EINVARG; + } + + if (custom_idm_len > 0 && custom_idm_len != sizeof(last_known_card.IDm)) { + return PM3_EINVARG; + } + + if (custom_idm_len == sizeof(last_known_card.IDm)) { + memcpy(idm_out, custom_idm, sizeof(last_known_card.IDm)); + + if (mode == FELICA_IDM_RESOLVE_CHAINED) { + PrintAndLogEx(INFO, "Using explicit IDm... " _GREEN_("%s"), + sprint_hex_inrow(idm_out, sizeof(last_known_card.IDm))); + return PM3_SUCCESS; + } + + if (felica_presence_check_idm(idm_out) != PM3_SUCCESS) { + PrintAndLogEx(FAILED, "Tag with explicit IDm not detected: " _YELLOW_("%s"), + sprint_hex_inrow(idm_out, sizeof(last_known_card.IDm))); + return PM3_ERFTRANS; + } + + felica_set_last_known_idm(idm_out); + PrintAndLogEx(INFO, "Using explicit IDm... " _GREEN_("%s"), + sprint_hex_inrow(idm_out, sizeof(last_known_card.IDm))); + return PM3_SUCCESS; + } + + if (mode == FELICA_IDM_RESOLVE_CHAINED) { + if (last_known_card.IDm[0] == 0 || last_known_card.IDm[1] == 0) { + PrintAndLogEx(WARNING, "No last known card! Use `" _YELLOW_("hf felica reader") "` first or set a custom IDm"); + return PM3_EINVARG; + } + + memcpy(idm_out, last_known_card.IDm, sizeof(last_known_card.IDm)); + PrintAndLogEx(INFO, "Using cached IDm.... " _GREEN_("%s"), + sprint_hex_inrow(idm_out, sizeof(last_known_card.IDm))); + return PM3_SUCCESS; + } + + if (last_known_card.IDm[0] != 0 && last_known_card.IDm[1] != 0) { + if (felica_presence_check_idm(last_known_card.IDm) == PM3_SUCCESS) { + memcpy(idm_out, last_known_card.IDm, sizeof(last_known_card.IDm)); + PrintAndLogEx(INFO, "Using cached IDm.... " _GREEN_("%s"), + sprint_hex_inrow(idm_out, sizeof(last_known_card.IDm))); + return PM3_SUCCESS; + } + + PrintAndLogEx(WARNING, "Cached IDm is no longer present. Polling for a new tag..."); + } else { + PrintAndLogEx(WARNING, "No cached IDm available. Polling for a new tag..."); + } + + felica_card_select_t card = {0}; + const int ret = felica_discover_target(&card); + if (ret != PM3_SUCCESS) { + PrintAndLogEx(FAILED, "No FeliCa tag detected while polling."); + return ret; + } + + memcpy(idm_out, card.IDm, sizeof(card.IDm)); + PrintAndLogEx(INFO, "Using polled IDm.... " _GREEN_("%s"), + sprint_hex_inrow(idm_out, sizeof(card.IDm))); + return PM3_SUCCESS; +} + +static int send_request_code_list(uint8_t flags, uint16_t datalen, uint8_t *data, bool verbose, + uint32_t timeout_ms, uint32_t retries, uint32_t backoff_ms, + bool logging, PacketResponseNG *resp) { + return send_felica_payload_with_retries(flags, datalen, data, verbose, + FELICA_GET_NODE_LIST_ACK, + timeout_ms, retries, backoff_ms, + logging, resp, "request code list"); } /** @@ -1229,14 +1576,17 @@ static int send_read_without_encryption(uint8_t flags, uint16_t datalen, uint8_t * @return success if response was received. */ static int send_search_service_code(uint8_t flags, uint16_t datalen, uint8_t *data, bool verbose, - uint32_t timeout_ms, uint32_t retries, + uint32_t timeout_ms, uint32_t retries, uint32_t backoff_ms, + bool logging, felica_search_service_code_response_t *search_sv_resp) { PacketResponseNG resp; if (send_felica_payload_with_retries(flags, datalen, data, verbose, 0x0B, timeout_ms, retries, - true, + backoff_ms, logging, &resp, "search service") != PM3_SUCCESS) { - PrintAndLogEx(ERR, "No response from card"); + if (logging) { + PrintAndLogEx(ERR, "No response from card"); + } return PM3_ERFTRANS; } @@ -1244,22 +1594,818 @@ static int send_search_service_code(uint8_t flags, uint16_t datalen, uint8_t *da return PM3_SUCCESS; } -/** - * Checks if last known card can be added to data and adds it if possible. - * @param custom_IDm - * @param data - * @return - */ -static bool check_last_idm(uint8_t *data, uint16_t datalen) { - if (add_last_IDm(2, data) == false) { - PrintAndLogEx(WARNING, "No last known card! Use `" _YELLOW_("hf felica reader") "` first or set a custom IDm"); +static uint16_t felica_to_network_order(uint16_t value) { + return (uint16_t)(((value & 0xFF00U) >> 8) | ((value & 0x00FFU) << 8)); +} + +static void felica_drop_connect_flag(uint8_t *flags) { + if (flags) { + *flags = FELICA_NO_DISCONNECT | FELICA_APPEND_CRC | FELICA_RAW; + } +} + +static void felica_set_discovered_count(uint32_t *discovered_count, uint32_t count) { + if (discovered_count) { + *discovered_count = count; + } +} + +static void felica_set_stop_status(int *stop_status, int status) { + if (stop_status) { + *stop_status = status; + } +} + +static bool felica_discovery_aborted(int *stop_status) { + if (kbd_enter_pressed()) { + felica_set_stop_status(stop_status, PM3_EOPABORTED); + return true; + } + return false; +} + +static int felica_emit_discovered_node(const felica_discovered_node_t *node, + felica_node_discovery_visitor_t visitor, + void *ctx, + uint32_t *discovered_count, + int *stop_status) { + int ret = visitor(node, ctx); + if (ret != PM3_SUCCESS) { + felica_set_stop_status(stop_status, ret); + return ret; + } + + if (discovered_count) { + (*discovered_count)++; + } + + return PM3_SUCCESS; +} + +static const felica_node_discovery_method_info_t FELICA_NODE_DISCOVERY_METHODS[] = { + { + .method = FELICA_NODE_DISCOVERY_REQUEST_CODE_LIST, + .cli_name = "request_code_list", + .display_name = "RequestCodeList", + .run = felica_discover_nodes_with_request_code_list + }, + { + .method = FELICA_NODE_DISCOVERY_SEARCH_SERVICE_CODE, + .cli_name = "search_service_code", + .display_name = "SearchServiceCode", + .run = felica_discover_nodes_with_search_service_code + }, + { + .method = FELICA_NODE_DISCOVERY_REQUEST_SERVICE, + .cli_name = "request_service", + .display_name = "RequestService", + .run = felica_discover_nodes_with_request_service + }, + { + .method = FELICA_NODE_DISCOVERY_READ_WITHOUT_ENCRYPTION, + .cli_name = "read_without_encryption", + .display_name = "ReadWithoutEncryption", + .run = felica_discover_nodes_with_read_without_encryption + }, +}; + +static const felica_node_discovery_method_info_t *felica_get_node_discovery_method_info(felica_node_discovery_method_t method) { + for (size_t i = 0; i < ARRAYLEN(FELICA_NODE_DISCOVERY_METHODS); i++) { + if (FELICA_NODE_DISCOVERY_METHODS[i].method == method) { + return &FELICA_NODE_DISCOVERY_METHODS[i]; + } + } + return NULL; +} + +static const char *felica_node_discovery_method_display_name(felica_node_discovery_method_t method) { + if (method == FELICA_NODE_DISCOVERY_NONE) { + return "Auto"; + } + const felica_node_discovery_method_info_t *info = felica_get_node_discovery_method_info(method); + return info ? info->display_name : "Auto"; +} + +static void felica_print_node_discovery_method_used(felica_node_discovery_method_t method) { + const char *name = felica_node_discovery_method_display_name(method); + if (method == FELICA_NODE_DISCOVERY_REQUEST_CODE_LIST) { + PrintAndLogEx(INFO, "Node discovery method used: " _GREEN_("%s"), name); + return; + } + if (method == FELICA_NODE_DISCOVERY_REQUEST_SERVICE) { + PrintAndLogEx(INFO, "Node discovery method used: " _YELLOW_("%s"), name); + return; + } + if (method == FELICA_NODE_DISCOVERY_READ_WITHOUT_ENCRYPTION) { + PrintAndLogEx(INFO, "Node discovery method used: " _RED_("%s"), name); + return; + } + PrintAndLogEx(INFO, "Node discovery method used: %s", name); +} + +static bool felica_discover_nodes_with_request_code_list(uint8_t *flags, + const uint8_t *idm, + uint32_t retry_count, + felica_node_discovery_visitor_t visitor, + void *ctx, + uint32_t *discovered_count, + int *stop_status) { + + uint8_t data[14] = {0}; + data[0] = sizeof(data); + data[1] = FELICA_GET_NODE_LIST_REQ; + memcpy(data + 2, idm, 8); + data[10] = 0x00; + data[11] = 0x00; + + bool supported = false; + uint32_t local_count = 0; + + for (uint16_t index = 1; index != 0; index++) { + if (felica_discovery_aborted(stop_status)) { + felica_set_discovered_count(discovered_count, local_count); + return false; + } + + data[12] = index & 0xFF; + data[13] = (index >> 8) & 0xFF; + + PacketResponseNG resp; + if (send_request_code_list(*flags, sizeof(data), data, false, + FELICA_DEFAULT_TIMEOUT_MS, retry_count, + supported ? FELICA_DISCOVERY_RETRY_BACKOFF_MS : 0, supported, &resp) != PM3_SUCCESS) { + felica_set_discovered_count(discovered_count, local_count); + return false; + } + + if (supported == false) { + supported = true; + felica_print_node_discovery_method_used(FELICA_NODE_DISCOVERY_REQUEST_CODE_LIST); + } + felica_drop_connect_flag(flags); + + size_t offset = sizeof(felica_frame_response_t); + if (resp.length < offset + 4) { + felica_set_discovered_count(discovered_count, local_count); + return false; + } + + const uint8_t status_flag1 = resp.data.asBytes[offset++]; + const uint8_t status_flag2 = resp.data.asBytes[offset++]; + if (status_flag1 != 0x00 || status_flag2 != 0x00) { + felica_set_discovered_count(discovered_count, local_count); + return false; + } + + const bool continue_flag = resp.data.asBytes[offset++] != 0x00; + const uint8_t area_count = resp.data.asBytes[offset++]; + const size_t area_bytes = (size_t)area_count * 4U; + + if (resp.length < offset + area_bytes + 1U) { + felica_set_discovered_count(discovered_count, local_count); + return false; + } + + felica_discovered_node_t batch_nodes[128]; + size_t batch_count = 0; + + for (uint8_t i = 0; i < area_count; i++) { + felica_discovered_node_t node = {0}; + node.is_area = true; + node.node_code_le = (uint16_t)resp.data.asBytes[offset] | + ((uint16_t)resp.data.asBytes[offset + 1] << 8); + node.has_end_code = true; + node.end_code_le = (uint16_t)resp.data.asBytes[offset + 2] | + ((uint16_t)resp.data.asBytes[offset + 3] << 8); + offset += 4; + if (batch_count < (sizeof(batch_nodes) / sizeof(batch_nodes[0]))) { + batch_nodes[batch_count++] = node; + } + } + + const uint8_t service_count = resp.data.asBytes[offset++]; + const size_t service_bytes = (size_t)service_count * 2U; + if (resp.length < offset + service_bytes) { + felica_set_discovered_count(discovered_count, local_count); + return false; + } + + for (uint8_t i = 0; i < service_count; i++) { + felica_discovered_node_t node = {0}; + node.is_area = false; + node.node_code_le = (uint16_t)resp.data.asBytes[offset] | + ((uint16_t)resp.data.asBytes[offset + 1] << 8); + node.has_end_code = false; + node.end_code_le = 0; + offset += 2; + if (batch_count < (sizeof(batch_nodes) / sizeof(batch_nodes[0]))) { + batch_nodes[batch_count++] = node; + } + } + + qsort(batch_nodes, batch_count, sizeof(batch_nodes[0]), felica_compare_discovered_nodes); + for (size_t i = 0; i < batch_count; i++) { + if (felica_emit_discovered_node(&batch_nodes[i], visitor, ctx, &local_count, stop_status) != PM3_SUCCESS) { + felica_set_discovered_count(discovered_count, local_count); + return false; + } + } + + if (continue_flag == false) { + break; + } + } + + felica_set_discovered_count(discovered_count, local_count); + + return supported; +} + +static bool felica_discover_nodes_with_search_service_code(uint8_t *flags, + const uint8_t *idm, + uint32_t retry_count, + felica_node_discovery_visitor_t visitor, + void *ctx, + uint32_t *discovered_count, + int *stop_status) { + + uint8_t data[12] = {0}; + data[0] = sizeof(data); + data[1] = FELICA_SRCHSYSCODE_REQ; + memcpy(data + 2, idm, 8); + + bool supported = false; + uint32_t local_count = 0; + + for (uint32_t cursor = 0; cursor <= 0xFFFFU; cursor++) { + if (felica_discovery_aborted(stop_status)) { + felica_set_discovered_count(discovered_count, local_count); + return false; + } + + data[10] = cursor & 0xFF; + data[11] = (cursor >> 8) & 0xFF; + + felica_search_service_code_response_t resp; + if (send_search_service_code(*flags, sizeof(data), data, false, + FELICA_DEFAULT_TIMEOUT_MS, retry_count, + supported ? FELICA_DISCOVERY_RETRY_BACKOFF_MS : 0, + supported, + &resp) != PM3_SUCCESS) { + felica_set_discovered_count(discovered_count, local_count); + return false; + } + + if (supported == false) { + supported = true; + felica_print_node_discovery_method_used(FELICA_NODE_DISCOVERY_SEARCH_SERVICE_CODE); + } + felica_drop_connect_flag(flags); + + const uint8_t frame_len = resp.frame_response.length[0]; + if (frame_len != 0x0C && frame_len != 0x0E) { + felica_set_discovered_count(discovered_count, local_count); + return false; + } + + const uint16_t node_code_le = (uint16_t)resp.payload[0] | ((uint16_t)resp.payload[1] << 8); + if (node_code_le == 0xFFFF) { + break; + } + + felica_discovered_node_t node = {0}; + node.is_area = (frame_len == 0x0E); + node.node_code_le = node_code_le; + node.has_end_code = (frame_len == 0x0E); + node.end_code_le = node.has_end_code ? ((uint16_t)resp.payload[2] | ((uint16_t)resp.payload[3] << 8)) : 0; + + if (felica_emit_discovered_node(&node, visitor, ctx, &local_count, stop_status) != PM3_SUCCESS) { + felica_set_discovered_count(discovered_count, local_count); + return false; + } + } + + felica_set_discovered_count(discovered_count, local_count); + + return supported; +} + +static bool felica_request_service_send_probe_batch(uint8_t *flags, + const uint8_t *idm, + uint32_t retry_count, + const uint16_t *node_codes_le, + const bool *is_area_nodes, + size_t node_count, + felica_node_discovery_visitor_t visitor, + void *ctx, + bool *supported, + uint32_t *discovered_count, + int *stop_status) { + if (node_count == 0 || node_count > FELICA_REQUEST_SERVICE_DISCOVERY_BATCH_SIZE) { return false; } - PrintAndLogEx(INFO, "Using last known IDm... " _GREEN_("%s"), sprint_hex_inrow(data, datalen)); + uint8_t data[1 + 1 + 8 + 1 + (FELICA_REQUEST_SERVICE_DISCOVERY_BATCH_SIZE * 2)] = {0}; + const uint16_t datalen = (uint16_t)(1 + 1 + 8 + 1 + (node_count * 2)); + data[0] = (uint8_t)datalen; + data[1] = FELICA_REQSRV_REQ; + memcpy(data + 2, idm, 8); + data[10] = (uint8_t)node_count; + + for (size_t i = 0; i < node_count; i++) { + data[11 + i * 2] = node_codes_le[i] & 0xFF; + data[12 + i * 2] = (node_codes_le[i] >> 8) & 0xFF; + } + + PacketResponseNG resp; + const bool logging = (supported != NULL) && (*supported); + const uint32_t backoff_ms = logging ? FELICA_DISCOVERY_RETRY_BACKOFF_MS : 0; + if (send_felica_payload_with_retries(*flags, datalen, data, false, + FELICA_REQSRV_ACK, + FELICA_DEFAULT_TIMEOUT_MS, retry_count, + backoff_ms, logging, &resp, "request service") != PM3_SUCCESS) { + return false; + } + + if (supported) { + if (*supported == false) { + felica_print_node_discovery_method_used(FELICA_NODE_DISCOVERY_REQUEST_SERVICE); + } + *supported = true; + } + felica_drop_connect_flag(flags); + + size_t offset = sizeof(felica_frame_response_t); + if (resp.length < offset + 1U) { + return false; + } + + size_t returned_nodes = resp.data.asBytes[offset++]; + size_t available_nodes = (resp.length > offset) ? ((resp.length - offset) / 2U) : 0; + if (returned_nodes > available_nodes) { + returned_nodes = available_nodes; + } + if (returned_nodes > node_count) { + returned_nodes = node_count; + } + + for (size_t i = 0; i < returned_nodes; i++) { + const uint16_t key_version = (uint16_t)resp.data.asBytes[offset + i * 2] | + ((uint16_t)resp.data.asBytes[offset + i * 2 + 1] << 8); + if (key_version == 0xFFFF) { + continue; + } + + felica_discovered_node_t node = {0}; + node.is_area = is_area_nodes[i]; + node.node_code_le = node_codes_le[i]; + node.has_end_code = false; + node.end_code_le = 0; + + if (felica_emit_discovered_node(&node, visitor, ctx, discovered_count, stop_status) != PM3_SUCCESS) { + return false; + } + } + return true; } +static bool felica_discover_nodes_with_request_service(uint8_t *flags, + const uint8_t *idm, + uint32_t retry_count, + felica_node_discovery_visitor_t visitor, + void *ctx, + uint32_t *discovered_count, + int *stop_status) { + bool supported = false; + uint32_t local_count = 0; + + uint16_t batch_codes[FELICA_REQUEST_SERVICE_DISCOVERY_BATCH_SIZE] = {0}; + bool batch_is_area[FELICA_REQUEST_SERVICE_DISCOVERY_BATCH_SIZE] = {0}; + size_t batch_count = 0; + + for (uint16_t number = 0; number <= FELICA_MAX_NODE_NUMBER; number++) { + for (size_t j = 0; j < (sizeof(FELICA_REQUEST_SERVICE_PROBE_ATTRIBUTES) / sizeof(FELICA_REQUEST_SERVICE_PROBE_ATTRIBUTES[0])); j++) { + if (felica_discovery_aborted(stop_status)) { + felica_set_discovered_count(discovered_count, local_count); + return false; + } + + const felica_request_service_probe_attribute_t probe_attr = FELICA_REQUEST_SERVICE_PROBE_ATTRIBUTES[j]; + const uint16_t node_code_le = (uint16_t)((number << 6) | probe_attr.attribute); + + batch_codes[batch_count] = node_code_le; + batch_is_area[batch_count] = probe_attr.is_area; + batch_count++; + + if (batch_count == FELICA_REQUEST_SERVICE_DISCOVERY_BATCH_SIZE) { + if (felica_request_service_send_probe_batch(flags, idm, retry_count, + batch_codes, batch_is_area, batch_count, + visitor, ctx, &supported, &local_count, stop_status) == false) { + felica_set_discovered_count(discovered_count, local_count); + if (stop_status && *stop_status == PM3_EOPABORTED) { + return false; + } + if (supported) { + PrintAndLogEx(WARNING, "Node discovery interrupted due to communication loss."); + } + return false; + } + batch_count = 0; + } + } + } + + if (batch_count > 0) { + if (felica_request_service_send_probe_batch(flags, idm, retry_count, + batch_codes, batch_is_area, batch_count, + visitor, ctx, &supported, &local_count, stop_status) == false) { + felica_set_discovered_count(discovered_count, local_count); + if (stop_status && *stop_status == PM3_EOPABORTED) { + return false; + } + if (supported) { + PrintAndLogEx(WARNING, "Node discovery interrupted due to communication loss."); + } + return false; + } + } + + felica_set_discovered_count(discovered_count, local_count); + + return supported; +} + +static bool felica_discover_nodes_with_read_without_encryption(uint8_t *flags, + const uint8_t *idm, + uint32_t retry_count, + felica_node_discovery_visitor_t visitor, + void *ctx, + uint32_t *discovered_count, + int *stop_status) { + uint8_t data[16] = {0}; + data[0] = sizeof(data); + data[1] = FELICA_RDBLK_REQ; + memcpy(data + 2, idm, 8); + data[10] = 0x01; + data[13] = 0x01; + data[14] = 0x80; + data[15] = 0x00; + + bool supported = false; + uint32_t local_count = 0; + + for (uint16_t node_number = 0; node_number <= FELICA_MAX_NODE_NUMBER; node_number++) { + for (size_t i = 0; i < ARRAYLEN(FELICA_REQUEST_SERVICE_PROBE_ATTRIBUTES); i++) { + if (felica_discovery_aborted(stop_status)) { + felica_set_discovered_count(discovered_count, local_count); + return false; + } + + const felica_request_service_probe_attribute_t probe_attr = FELICA_REQUEST_SERVICE_PROBE_ATTRIBUTES[i]; + if (probe_attr.is_area || probe_attr.with_key || probe_attr.with_pin) { + continue; + } + + const uint16_t service_code_le = (uint16_t)((node_number << 6) | probe_attr.attribute); + data[11] = service_code_le & 0xFF; + data[12] = (service_code_le >> 8) & 0xFF; + + felica_read_without_encryption_response_t resp; + if (send_read_without_encryption_ex(*flags, sizeof(data), data, false, + &resp, + FELICA_DEFAULT_TIMEOUT_MS, retry_count, + supported ? FELICA_DISCOVERY_RETRY_BACKOFF_MS : 0, supported) != PM3_SUCCESS) { + if (supported == false) { + return false; + } + PrintAndLogEx(WARNING, "Stopping ReadWithoutEncryption discovery due to communication loss (possible card removed)."); + if (discovered_count) { + *discovered_count = local_count; + } + return true; + } + + if (supported == false) { + supported = true; + felica_print_node_discovery_method_used(FELICA_NODE_DISCOVERY_READ_WITHOUT_ENCRYPTION); + } + felica_drop_connect_flag(flags); + + /* + * For discovery via ReadWithoutEncryption: + * - A6 (Illegal Service Code List) is treated as "node does not exist". + * - Other status codes (for example A8, B1) still imply the node exists. + */ + if (resp.status_flags.status_flag2[0] == 0xA6) { + continue; + } + + felica_discovered_node_t node = {0}; + node.is_area = false; + node.node_code_le = service_code_le; + node.has_end_code = false; + node.end_code_le = 0; + + if (felica_emit_discovered_node(&node, visitor, ctx, &local_count, stop_status) != PM3_SUCCESS) { + if (discovered_count) { + *discovered_count = local_count; + } + return false; + } + } + } + + if (discovered_count) { + *discovered_count = local_count; + } + + return supported; +} + +static const char *felica_node_discovery_method_cli_name(felica_node_discovery_method_t method) { + if (method == FELICA_NODE_DISCOVERY_NONE) { + return "auto"; + } + const felica_node_discovery_method_info_t *info = felica_get_node_discovery_method_info(method); + return info ? info->cli_name : "auto"; +} + +static int felica_parse_node_discovery_method(const char *method_str, felica_node_discovery_method_t *method_out) { + if (method_out == NULL) { + return PM3_EINVARG; + } + + *method_out = FELICA_NODE_DISCOVERY_NONE; + + if (method_str == NULL || method_str[0] == '\0' || strcmp(method_str, "auto") == 0) { + return PM3_SUCCESS; + } + + for (size_t i = 0; i < ARRAYLEN(FELICA_NODE_DISCOVERY_METHODS); i++) { + if (strcmp(method_str, FELICA_NODE_DISCOVERY_METHODS[i].cli_name) == 0) { + *method_out = FELICA_NODE_DISCOVERY_METHODS[i].method; + return PM3_SUCCESS; + } + } + + PrintAndLogEx(ERR, "Unknown --method `%s`.", method_str); + PrintAndLogEx(INFO, "Valid values: auto, request_code_list, search_service_code, request_service, read_without_encryption"); + return PM3_EINVARG; +} + +static int felica_discover_nodes(const uint8_t *idm, + uint8_t *flags, + uint32_t retry_count, + felica_node_discovery_method_t selected_method, + felica_node_discovery_visitor_t visitor, + void *ctx, + felica_node_discovery_method_t *method_out, + uint32_t *discovered_count_out) { + if (idm == NULL || flags == NULL || visitor == NULL) { + return PM3_EINVARG; + } + + felica_node_discovery_method_t ignored_method = FELICA_NODE_DISCOVERY_NONE; + uint32_t ignored_count = 0; + felica_node_discovery_method_t *const out_method = method_out ? method_out : &ignored_method; + uint32_t *const out_count = discovered_count_out ? discovered_count_out : &ignored_count; + + uint32_t discovered_count = 0; + const bool auto_mode = (selected_method == FELICA_NODE_DISCOVERY_NONE); + for (size_t i = 0; i < ARRAYLEN(FELICA_NODE_DISCOVERY_METHODS); i++) { + const felica_node_discovery_method_info_t *info = &FELICA_NODE_DISCOVERY_METHODS[i]; + if (!auto_mode && selected_method != info->method) { + continue; + } + + discovered_count = 0; + int stop_status = PM3_SUCCESS; + if (info->run(flags, idm, retry_count, visitor, ctx, &discovered_count, &stop_status)) { + *out_method = info->method; + *out_count = discovered_count; + return PM3_SUCCESS; + } + + if (stop_status != PM3_SUCCESS) { + *out_method = info->method; + *out_count = discovered_count; + return stop_status; + } + + if (discovered_count > 0) { + *out_method = info->method; + *out_count = discovered_count; + return PM3_ERFTRANS; + } + + if (!auto_mode) { + *out_method = FELICA_NODE_DISCOVERY_NONE; + *out_count = discovered_count; + return PM3_ERFTRANS; + } + } + + *out_method = FELICA_NODE_DISCOVERY_NONE; + *out_count = 0; + return PM3_ERFTRANS; +} + +static bool felica_format_service_attribute(uint16_t service_code_le, char *attrib_str, size_t attrib_str_size) { + if (attrib_str == NULL || attrib_str_size == 0) { + return false; + } + + const uint8_t attribute = service_code_le & 0x3F; + const bool is_public = (attribute & FELICA_SERVICE_ATTRIBUTE_UNAUTH_READ) != 0; + const bool is_purse = (attribute & FELICA_SERVICE_ATTRIBUTE_PURSE) != 0; + const char *visibility = is_public ? "Public " : "Private"; + const char *group = NULL; + const char *mode = NULL; + + if (is_purse) { + group = "Purse "; + switch ((attribute & FELICA_SERVICE_ATTRIBUTE_PURSE_SUBFIELD) >> 1) { + case 0: + mode = "Direct"; + break; + case 1: + mode = "Cashback"; + break; + case 2: + mode = "Decrement"; + break; + case 3: + mode = "Read Only"; + break; + default: + mode = "Unknown"; + break; + } + } else { + const bool is_random = (attribute & FELICA_SERVICE_ATTRIBUTE_RANDOM_ACCESS) != 0; + const bool is_readonly = (attribute & FELICA_SERVICE_ATTRIBUTE_READ_ONLY) != 0; + group = is_random ? "Random" : "Cyclic"; + mode = is_readonly ? "Read Only" : "Read/Write"; + } + + snprintf(attrib_str, attrib_str_size, "| %s | %s | %s |", visibility, group, mode); + return is_public; +} + +static int felica_compare_discovered_nodes(const void *lhs, const void *rhs) { + const felica_discovered_node_t *a = (const felica_discovered_node_t *)lhs; + const felica_discovered_node_t *b = (const felica_discovered_node_t *)rhs; + + if (a->node_code_le < b->node_code_le) { + return -1; + } + if (a->node_code_le > b->node_code_le) { + return 1; + } + + if (a->is_area != b->is_area) { + return a->is_area ? -1 : 1; + } + + if (a->has_end_code != b->has_end_code) { + return a->has_end_code ? -1 : 1; + } + + if (a->end_code_le < b->end_code_le) { + return -1; + } + if (a->end_code_le > b->end_code_le) { + return 1; + } + + return 0; +} + +static int felica_scsvcode_discovery_visitor(const felica_discovered_node_t *node, void *ctx) { + if (node == NULL || ctx == NULL) { + return PM3_EINVARG; + } + + felica_scsvcode_context_t *scsv_ctx = (felica_scsvcode_context_t *)ctx; + if (scsv_ctx->header_printed == false) { + PrintAndLogEx(INFO, "┌───────────────────────────────────────────────"); + scsv_ctx->header_printed = true; + } + + while (scsv_ctx->depth && node->node_code_le > scsv_ctx->area_end_stack[scsv_ctx->depth]) { + scsv_ctx->depth--; + } + + char prefix[64] = ""; + for (int level = 1; level < scsv_ctx->depth; level++) { + const bool more_siblings = node->node_code_le < scsv_ctx->area_end_stack[level]; + strcat(prefix, more_siblings ? "│ " : " "); + } + strcat(prefix, "├── "); + + const uint16_t node_code_be = felica_to_network_order(node->node_code_le); + const uint16_t node_number = node->node_code_le >> 6; + + if (node->is_area) { + scsv_ctx->area_count++; + if (node->has_end_code) { + const uint16_t end_code_be = felica_to_network_order(node->end_code_le); + const uint16_t end_number = node->end_code_le >> 6; + PrintAndLogEx(INFO, "%sAREA_%04X%04X (%u-%u)", prefix, + node_code_be, end_code_be, + node_number, end_number); + + if (scsv_ctx->depth < 7) { + scsv_ctx->area_end_stack[++scsv_ctx->depth] = node->end_code_le; + } + } else { + PrintAndLogEx(INFO, "%sAREA_%04X (%u-?)", prefix, node_code_be, node_number); + } + } else { + scsv_ctx->service_count++; + PrintAndLogEx(INFO, "%sSVC_%04X (%u)", prefix, node_code_be, node_number); + } + + return PM3_SUCCESS; +} + +static void felica_scsvcode_print_footer(const felica_scsvcode_context_t *scsv_ctx) { + if (scsv_ctx == NULL || scsv_ctx->header_printed == false) { + return; + } + + char bar[128]; + size_t pos = 0; + pos += snprintf(bar + pos, sizeof(bar) - pos, "└"); + for (int i = 0; i < scsv_ctx->depth - 1 && pos < sizeof(bar); i++) { + pos += snprintf(bar + pos, sizeof(bar) - pos, "───┴"); + } + snprintf(bar + pos, sizeof(bar) - pos, "───────────────────────"); + PrintAndLogEx(INFO, "%s", bar); +} + +static int felica_dump_discovery_visitor(const felica_discovered_node_t *node, void *ctx) { + if (node == NULL || ctx == NULL) { + return PM3_EINVARG; + } + + felica_dump_context_t *dump_ctx = (felica_dump_context_t *)ctx; + if (dump_ctx->flags == NULL) { + return PM3_EINVARG; + } + + if (node->is_area) { + return PM3_SUCCESS; + } + + dump_ctx->service_count++; + + char attrib_str[64] = {0}; + const bool is_public = felica_format_service_attribute(node->node_code_le, attrib_str, sizeof(attrib_str)); + PrintAndLogEx(INFO, "Service %04X %s", felica_to_network_order(node->node_code_le), attrib_str); + + if (is_public == false) { + return PM3_SUCCESS; + } + + if ((node->node_code_le & FELICA_SERVICE_ATTRIBUTE_PIN_REQUIRED) != 0) { + PrintAndLogEx(INFO, " PIN protected; skipping unauthenticated read."); + return PM3_SUCCESS; + } + + dump_ctx->public_service_count++; + + PrintAndLogEx(INFO, " block | data "); + PrintAndLogEx(INFO, "-------+----------------------------------------"); + + dump_ctx->block_frame[11] = node->node_code_le & 0xFF; + dump_ctx->block_frame[12] = (node->node_code_le >> 8) & 0xFF; + + for (uint16_t block = 0x00; block < 0xFF; block++) { + if (kbd_enter_pressed()) { + return PM3_EOPABORTED; + } + + dump_ctx->block_frame[15] = block; + felica_read_without_encryption_response_t rd_noCry_resp; + if (send_read_without_encryption_ex(*(dump_ctx->flags), dump_ctx->block_datalen, + dump_ctx->block_frame, false, + &rd_noCry_resp, + FELICA_DEFAULT_TIMEOUT_MS, dump_ctx->retry_count, 0, true) != PM3_SUCCESS) { + break; + } + + if (rd_noCry_resp.status_flags.status_flag1[0] != 0x00 || rd_noCry_resp.status_flags.status_flag2[0] != 0x00) { + break; + } + + print_read_without_encryption_response(&rd_noCry_resp, block); + } + + return PM3_SUCCESS; +} + /** * Sends a write_without_encryption frame to pm3 and stores the response. * @param flags to use for pm3 communication. @@ -1274,7 +2420,7 @@ static int send_write_without_encryption(uint8_t flags, uint16_t datalen, uint8_ if (send_felica_payload_with_retries(flags, datalen, data, verbose, -1, FELICA_DEFAULT_TIMEOUT_MS, 0, - true, + 0, true, &resp, "write block") != PM3_SUCCESS) { PrintAndLogEx(ERR, "no response from card"); return PM3_ERFTRANS; @@ -1309,13 +2455,13 @@ static int CmdHFFelicaAuthentication1(const char *Cmd) { _RED_("INCOMPLETE / EXPERIMENTAL COMMAND!!!"), "hf felica auth1 --an 01 --acl 0000 --sn 01 --scl 8B00 --key AAAAAAAAAAAAAAAABBBBBBBBBBBBBBBB\n" "hf felica auth1 --an 01 --acl 0000 --sn 01 --scl 8B00 --key AAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBAAAAAAAAAAAAAAAA\n" - "hf felica auth1 -i 11100910C11BC407 --an 01 --acl 0000 --sn 01 ..scl 8B00 --key AAAAAAAAAAAAAAAABBBBBBBBBBBBBBBB\n" + "hf felica auth1 --idm 11100910C11BC407 --an 01 --acl 0000 --sn 01 ..scl 8B00 --key AAAAAAAAAAAAAAAABBBBBBBBBBBBBBBB\n" ); void *argtable[] = { arg_param_begin, arg_str0(NULL, "an", "", "number of areas, 1 byte"), arg_str0(NULL, "acl", "", "area code list, 2 bytes"), - arg_str0("i", NULL, "", "set custom IDm"), + arg_str0(NULL, "idm", "", "set custom IDm"), arg_str0(NULL, "sn", "", "number of service, 1 byte"), arg_str0(NULL, "scl", "", "service code list, 2 bytes"), arg_str0("k", "key", "", "3des key, 16 bytes"), @@ -1384,13 +2530,6 @@ static int CmdHFFelicaAuthentication1(const char *Cmd) { data[0] = 0x0C; // Static length data[1] = 0x3E; // Command ID - bool custom_IDm = false; - - if (ilen) { - custom_IDm = true; - memcpy(data + 2, idm, 8); - } - // Length (1), // Command ID (1), // IDm (8), @@ -1403,9 +2542,12 @@ static int CmdHFFelicaAuthentication1(const char *Cmd) { data[0] = (datalen & 0xFF); data[1] = 0x10; // Command ID - if (custom_IDm == false && check_last_idm(data, datalen) == false) { - return PM3_EINVARG; + uint8_t resolved_idm[8] = {0}; + res = felica_ensure_target_present(idm, (size_t)ilen, FELICA_IDM_RESOLVE_STANDALONE, resolved_idm); + if (res != PM3_SUCCESS) { + return res; } + memcpy(data + 2, resolved_idm, sizeof(resolved_idm)); if (anlen) { data[10] = an[0]; @@ -1523,11 +2665,11 @@ static int CmdHFFelicaAuthentication2(const char *Cmd) { _RED_("INCOMPLETE / EXPERIMENTAL COMMAND!!!\n") _RED_("EXPERIMENTAL COMMAND - M2c/P2c will be not checked"), "hf felica auth2 --cc 0102030405060708 --key AAAAAAAAAAAAAAAABBBBBBBBBBBBBBBB\n" - "hf felica auth2 -i 11100910C11BC407 --cc 0102030405060708 --key AAAAAAAAAAAAAAAABBBBBBBBBBBBBBBB\n" + "hf felica auth2 --idm 11100910C11BC407 --cc 0102030405060708 --key AAAAAAAAAAAAAAAABBBBBBBBBBBBBBBB\n" ); void *argtable[] = { arg_param_begin, - arg_str0("i", NULL, "", "set custom IDm"), + arg_str0(NULL, "idm", "", "set custom IDm"), arg_str0("c", "cc", "", "M3c card challenge, 8 bytes"), arg_str0("k", "key", "", "3des M3c decryption key, 16 bytes"), arg_lit0("v", "verbose", "verbose output"), @@ -1569,20 +2711,16 @@ static int CmdHFFelicaAuthentication2(const char *Cmd) { uint8_t data[PM3_CMD_DATA_SIZE]; memset(data, 0, sizeof(data)); - bool custom_IDm = false; - - if (ilen) { - custom_IDm = true; - memcpy(data + 2, idm, 8); - } - uint16_t datalen = 18; // Length (1), Command ID (1), IDm (8), M4c (8) data[0] = (datalen & 0xFF); data[1] = 0x12; // Command ID - if (custom_IDm == false && check_last_idm(data, datalen) == false) { - return PM3_EINVARG; + uint8_t resolved_idm[8] = {0}; + res = felica_ensure_target_present(idm, (size_t)ilen, FELICA_IDM_RESOLVE_CHAINED, resolved_idm); + if (res != PM3_SUCCESS) { + return res; } + memcpy(data + 2, resolved_idm, sizeof(resolved_idm)); if (cclen) { memcpy(data + 16, cc, cclen); @@ -1592,11 +2730,6 @@ static int CmdHFFelicaAuthentication2(const char *Cmd) { memcpy(data + 16, key, keylen); } - - if (custom_IDm == false && check_last_idm(data, datalen) == false) { - return PM3_EINVARG; - } - // M3c (8) == cc // unsigned char m3c[8]; == cc @@ -1684,12 +2817,12 @@ static int CmdHFFelicaWritePlain(const char *Cmd) { " - Mode shall be Mode0.\n" " - Un-/Ssuccessful == Status Flag1 and Flag2", "hf felica wrbl --sn 01 --scl CB10 --bn 01 --ble 8001 -d 0102030405060708090A0B0C0D0E0F10\n" - "hf felica wrbl -i 01100910c11bc407 --sn 01 --scl CB10 --bn 01 --ble 8001 -d 0102030405060708090A0B0C0D0E0F10\n" + "hf felica wrbl --idm 01100910c11bc407 --sn 01 --scl CB10 --bn 01 --ble 8001 -d 0102030405060708090A0B0C0D0E0F10\n" ); void *argtable[] = { arg_param_begin, arg_str0("d", "data", "", "data, 16 hex bytes"), - arg_str0("i", NULL, "", "set custom IDm"), + arg_str0(NULL, "idm", "", "set custom IDm"), arg_str0(NULL, "sn", "", "number of service"), arg_str0(NULL, "scl", "", "service code list"), arg_str0(NULL, "bn", "", "number of block"), @@ -1765,12 +2898,6 @@ static int CmdHFFelicaWritePlain(const char *Cmd) { data[0] = 0x20; // Static length data[1] = 0x08; // Command ID - bool custom_IDm = false; - if (ilen) { - custom_IDm = true; - memcpy(data + 2, idm, sizeof(idm)); - } - // Length (1) // Command ID (1) // IDm (8) @@ -1781,8 +2908,9 @@ static int CmdHFFelicaWritePlain(const char *Cmd) { // Block Data(16) uint16_t datalen = 32; // Length (1), Command ID (1), IDm (8), Number of Service (1), Service Code List(2), Number of Block(1), Block List(3), Block Data(16) - if (custom_IDm == false && check_last_idm(data, datalen) == false) { - return PM3_EINVARG; + res = felica_ensure_target_present(idm, (size_t)ilen, FELICA_IDM_RESOLVE_STANDALONE, data + 2); + if (res != PM3_SUCCESS) { + return res; } if (blelen == 3) { @@ -1847,12 +2975,12 @@ static int CmdHFFelicaReadPlain(const char *Cmd) { " - Unsuccessful == Status Flag1 and Flag2", "hf felica rdbl --sn 01 --scl 8B00 --bn 01 --ble 8000\n" "hf felica rdbl --sn 01 --scl 4B18 --bn 01 --ble 8000 -b\n" - "hf felica rdbl -i 01100910c11bc407 --sn 01 --scl 8B00 --bn 01 --ble 8000\n" + "hf felica rdbl --idm 01100910c11bc407 --sn 01 --scl 8B00 --bn 01 --ble 8000\n" ); void *argtable[] = { arg_param_begin, arg_lit0("b", NULL, "get all block list elements 00 -> FF"), - arg_str0("i", NULL, "", "set custom IDm"), + arg_str0(NULL, "idm", "", "set custom IDm"), arg_lit0("l", "long", "use 3 byte block list element block number"), arg_str0(NULL, "sn", "", "number of service"), arg_str0(NULL, "scl", "", "service code list"), @@ -1924,15 +3052,10 @@ static int CmdHFFelicaReadPlain(const char *Cmd) { data[0] = 0x10; // Static length data[1] = 0x06; // Command ID - bool custom_IDm = false; - if (ilen) { - custom_IDm = true; - memcpy(data + 2, idm, sizeof(idm)); - } - uint16_t datalen = 16; // Length (1), Command ID (1), IDm (8), Number of Service (1), Service Code List(2), Number of Block(1), Block List(3) - if (custom_IDm == false && check_last_idm(data, datalen) == false) { - return PM3_EINVARG; + res = felica_ensure_target_present(idm, (size_t)ilen, FELICA_IDM_RESOLVE_STANDALONE, data + 2); + if (res != PM3_SUCCESS) { + return res; } if (long_block_numbers) { @@ -1995,11 +3118,11 @@ static int CmdHFFelicaRequestResponse(const char *Cmd) { CLIParserInit(&ctx, "hf felica rqresponse", "Use this command to verify the existence of a card and its Mode.\n" " - current mode of the card is returned", - "hf felica rqresponse -i 11100910C11BC407\n" + "hf felica rqresponse --idm 11100910C11BC407\n" ); void *argtable[] = { arg_param_begin, - arg_str0("i", NULL, "", "set custom IDm"), + arg_str0(NULL, "idm", "", "set custom IDm"), arg_param_end }; CLIExecWithReturn(ctx, Cmd, argtable, true); @@ -2019,15 +3142,10 @@ static int CmdHFFelicaRequestResponse(const char *Cmd) { data[0] = 0x0A; // Static length data[1] = 0x04; // Command ID - bool custom_IDm = false; - if (ilen) { - custom_IDm = true; - memcpy(data + 2, idm, sizeof(idm)); - } - uint8_t datalen = 10; // Length (1), Command ID (1), IDm (8) - if (!custom_IDm && !check_last_idm(data, datalen)) { - return PM3_EINVARG; + res = felica_ensure_target_present(idm, (size_t)ilen, FELICA_IDM_RESOLVE_STANDALONE, data + 2); + if (res != PM3_SUCCESS) { + return res; } uint8_t flags = (FELICA_APPEND_CRC | FELICA_RAW); @@ -2068,11 +3186,11 @@ static int CmdHFFelicaRequestSpecificationVersion(const char *Cmd) { "hf felica rqspecver\n" "hf felica rqspecver -r 0001\n" - "hf felica rqspecver -i 11100910C11BC407 \n" + "hf felica rqspecver --idm 11100910C11BC407 \n" ); void *argtable[] = { arg_param_begin, - arg_str0("i", NULL, "", "set custom IDm"), + arg_str0(NULL, "idm", "", "set custom IDm"), arg_str0("r", NULL, "", "set custom reserve"), arg_lit0("v", "verbose", "verbose output"), arg_param_end @@ -2107,20 +3225,14 @@ static int CmdHFFelicaRequestSpecificationVersion(const char *Cmd) { request_specification_version_request.length[0] = sizeof(request_specification_version_request); request_specification_version_request.command_code[0] = FELICA_REQUEST_SPEC_VERSION_REQ; - bool custom_IDm = false; - if (ilen) { - custom_IDm = true; - memcpy(request_specification_version_request.IDm, idm, sizeof(idm)); - } - if (rlen) { memcpy(request_specification_version_request.reserved, reserved, sizeof(reserved)); } - if (custom_IDm == false && - check_last_idm((uint8_t *)&request_specification_version_request, - sizeof(request_specification_version_request)) == false) { - return PM3_EINVARG; + res = felica_ensure_target_present(idm, (size_t)ilen, FELICA_IDM_RESOLVE_STANDALONE, + request_specification_version_request.IDm); + if (res != PM3_SUCCESS) { + return res; } felica_request_specification_version_info_t specification_version_info; @@ -2168,11 +3280,11 @@ static int CmdHFFelicaResetMode(const char *Cmd) { "Use this command to reset Mode to Mode 0.", "hf felica resetmode\n" "hf felica resetmode -r 0001\n" - "hf felica resetmode -i 11100910C11BC407 \n" + "hf felica resetmode --idm 11100910C11BC407 \n" ); void *argtable[] = { arg_param_begin, - arg_str0("i", NULL, "", "set custom IDm"), + arg_str0(NULL, "idm", "", "set custom IDm"), arg_str0("r", NULL, "", "set custom reserve"), arg_lit0("v", "verbose", "verbose output"), arg_param_end @@ -2207,12 +3319,6 @@ static int CmdHFFelicaResetMode(const char *Cmd) { data[0] = 0x0C; // Static length data[1] = 0x3E; // Command ID - bool custom_IDm = false; - - if (ilen) { - custom_IDm = true; - memcpy(data + 2, idm, 8); - } if (rlen) { memcpy(data + 10, reserved, 2); } else { @@ -2221,8 +3327,9 @@ static int CmdHFFelicaResetMode(const char *Cmd) { } uint16_t datalen = 12; // Length (1), Command ID (1), IDm (8), Reserved (2) - if (custom_IDm == false && check_last_idm(data, datalen) == false) { - return PM3_EINVARG; + res = felica_ensure_target_present(idm, (size_t)ilen, FELICA_IDM_RESOLVE_STANDALONE, data + 2); + if (res != PM3_SUCCESS) { + return res; } uint8_t flags = (FELICA_APPEND_CRC | FELICA_RAW); @@ -2259,11 +3366,11 @@ static int CmdHFFelicaRequestSystemCode(const char *Cmd) { " - if a card is divided into more than one System, \n" " this command acquires System Code of each System existing in the card.", "hf felica rqsyscode\n" - "hf felica rqsyscode -i 11100910C11BC407 \n" + "hf felica rqsyscode --idm 11100910C11BC407 \n" ); void *argtable[] = { arg_param_begin, - arg_str0("i", NULL, "", "set custom IDm"), + arg_str0(NULL, "idm", "", "set custom IDm"), arg_param_end }; CLIExecWithReturn(ctx, Cmd, argtable, true); @@ -2283,15 +3390,10 @@ static int CmdHFFelicaRequestSystemCode(const char *Cmd) { data[0] = 0x0A; // Static length data[1] = 0x0C; // Command ID - bool custom_IDm = false; - if (ilen) { - custom_IDm = true; - memcpy(data + 2, idm, sizeof(idm)); - } - uint16_t datalen = 10; // Length (1), Command ID (1), IDm (8) - if (custom_IDm == false && check_last_idm(data, datalen) == false) { - return PM3_EINVARG; + res = felica_ensure_target_present(idm, (size_t)ilen, FELICA_IDM_RESOLVE_STANDALONE, data + 2); + if (res != PM3_SUCCESS) { + return res; } uint8_t flags = (FELICA_APPEND_CRC | FELICA_RAW); @@ -2332,148 +3434,74 @@ static int CmdHFFelicaDump(const char *Cmd) { "Dump all existing Area Code and Service Code.\n" "Only works on services that do not require authentication yet.\n", "hf felica dump\n" - "hf felica dump --retry 5"); + "hf felica dump --retry 5\n" + "hf felica dump --idm 11100910C11BC407"); void *argtable[] = { arg_param_begin, arg_lit0(NULL, "no-auth", "read public services"), arg_u64_0("r", "retry", "", "number of retries"), + arg_str0(NULL, "idm", "", "use custom IDm"), arg_param_end }; CLIExecWithReturn(ctx, Cmd, argtable, true); uint32_t retry_count = arg_get_u32_def(ctx, 2, FELICA_DEFAULT_RETRY_COUNT); + uint8_t idm[8] = {0}; + int ilen = 0; + int res = CLIParamHexToBuf(arg_get_str(ctx, 3), idm, sizeof(idm), &ilen); CLIParserFree(ctx); + if (res) { + return PM3_EINVARG; + } // bool no_auth = arg_get_lit(ctx, 1); - uint8_t data_service_dump[PM3_CMD_DATA_SIZE] = {0}; - data_service_dump[0] = 0x0C; - data_service_dump[1] = 0x0A; - uint16_t service_datalen = 12; - if (!check_last_idm(data_service_dump, service_datalen)) - return PM3_EINVARG; - - uint8_t data_block_dump[PM3_CMD_DATA_SIZE] = {0}; - data_block_dump[0] = 0x10; // Static length - data_block_dump[1] = 0x06; // unauth read block command - data_block_dump[10] = 0x01; // read one service at a time - data_block_dump[13] = 0x01; // read one block at a time - data_block_dump[14] = 0x80; // block list element first byte - uint16_t block_datalen = 16; // Length (1), Command ID (1), IDm (8), Number of Service (1), Service Code List(2), Number of Block(1), Block List(3) - if (!check_last_idm(data_block_dump, block_datalen)) { - return PM3_EINVARG; + res = felica_ensure_target_present(idm, (size_t)ilen, FELICA_IDM_RESOLVE_STANDALONE, idm); + if (res != PM3_SUCCESS) { + return res; } - // Set up field once and keep it up for the entire dump sequence. - // First command connects (with NO_SELECT since we already have IDm). + PrintAndLogEx(INFO, "Press " _GREEN_("") " to abort discovery or dumping"); + uint8_t flags = FELICA_CONNECT | FELICA_NO_SELECT | FELICA_NO_DISCONNECT | FELICA_APPEND_CRC | FELICA_RAW; - int ret = PM3_SUCCESS; - uint16_t cursor = 0x0000; - - felica_search_service_code_response_t resp; - - while (true) { - - data_service_dump[10] = cursor & 0xFF; - data_service_dump[11] = cursor >> 8; - - if (send_search_service_code(flags, service_datalen, data_service_dump, 0, - FELICA_DEFAULT_TIMEOUT_MS, retry_count, - &resp) != PM3_SUCCESS) { - PrintAndLogEx(FAILED, "No response at cursor 0x%04X", cursor); - ret = PM3_ERFTRANS; - break; - } - - // After first command, drop CONNECT flag — field is already up - flags = FELICA_NO_DISCONNECT | FELICA_APPEND_CRC | FELICA_RAW; - uint8_t len = resp.frame_response.length[0]; - uint16_t node_code = resp.payload[0] | (resp.payload[1] << 8); - if (node_code == 0xFFFF) break; - char attrib_str[64] = ""; - switch (len) { - case 0x0E: - break; - case 0x0C: { - uint8_t attribute = node_code & 0x3F; - bool is_public = (attribute & FELICA_SERVICE_ATTRIBUTE_UNAUTH_READ) != 0; - strcat(attrib_str, is_public ? "| Public " : "| Private "); - - bool is_purse = (attribute & FELICA_SERVICE_ATTRIBUTE_PURSE) != 0; - // Subfield bitwise attributes are applicable depending on is PURSE or not - - if (is_purse) { - strcat(attrib_str, "| Purse |"); - switch ((attribute & FELICA_SERVICE_ATTRIBUTE_PURSE_SUBFIELD) >> 1) { - case 0: - strcat(attrib_str, " Direct |"); - break; - case 1: - strcat(attrib_str, " Cashback |"); - break; - case 2: - strcat(attrib_str, " Decrement |"); - break; - case 3: - strcat(attrib_str, " Read Only |"); - break; - default: - strcat(attrib_str, " Unknown |"); - break; - } - } else { - bool is_random = (attribute & FELICA_SERVICE_ATTRIBUTE_RANDOM_ACCESS) != 0; - strcat(attrib_str, is_random ? "| Random |" : "| Cyclic |"); - bool is_readonly = (attribute & FELICA_SERVICE_ATTRIBUTE_READ_ONLY) != 0; - strcat(attrib_str, is_readonly ? " Read Only |" : " Read/Write |"); - } - - PrintAndLogEx(INFO, "Service %04X %s", node_code, attrib_str); - - if (is_public) { - // dump blocks here - PrintAndLogEx(INFO, " block | data "); - PrintAndLogEx(INFO, "-------+----------------------------------------"); - - data_block_dump[11] = resp.payload[0]; // convert service code to little endian - data_block_dump[12] = resp.payload[1]; - - uint16_t last_blockno = 0xFF; - for (uint16_t i = 0x00; i < last_blockno; i++) { - data_block_dump[15] = i; - felica_read_without_encryption_response_t rd_noCry_resp; - if ((send_read_without_encryption_ex(flags, block_datalen, data_block_dump, 0, - &rd_noCry_resp, - FELICA_DEFAULT_TIMEOUT_MS, retry_count) == PM3_SUCCESS)) { - if (rd_noCry_resp.status_flags.status_flag1[0] == 0 && rd_noCry_resp.status_flags.status_flag2[0] == 0) { - print_read_without_encryption_response(&rd_noCry_resp, i); - } else { - break; // no more blocks to read - } - } else { - break; - } - } - } - break; - } - default: - PrintAndLogEx(FAILED, "Unexpected length 0x%02X @ 0x%04X", - len, cursor); - ret = PM3_ERFTRANS; - break; - } - if (ret != PM3_SUCCESS) break; - cursor++; - if (cursor == 0) break; - } + felica_dump_context_t dump_ctx; + memset(&dump_ctx, 0, sizeof(dump_ctx)); + dump_ctx.flags = &flags; + dump_ctx.retry_count = retry_count; + dump_ctx.block_datalen = 16; + dump_ctx.block_frame[0] = dump_ctx.block_datalen; + dump_ctx.block_frame[1] = FELICA_RDBLK_REQ; + memcpy(dump_ctx.block_frame + 2, idm, sizeof(idm)); + dump_ctx.block_frame[10] = 0x01; + dump_ctx.block_frame[13] = 0x01; + dump_ctx.block_frame[14] = 0x80; + uint32_t discovered_nodes = 0; + int ret = felica_discover_nodes(idm, &flags, retry_count, + FELICA_NODE_DISCOVERY_NONE, + felica_dump_discovery_visitor, &dump_ctx, + NULL, &discovered_nodes); DropField(); - if (ret == PM3_SUCCESS) - PrintAndLogEx(SUCCESS, "Unauth service dump complete."); + if (ret == PM3_EOPABORTED) { + PrintAndLogEx(WARNING, "Unauth service dump aborted by user. Discovered %" PRIu32 " node(s), visited %" PRIu32 " service(s), dumped %" PRIu32 " public service(s).", + discovered_nodes, dump_ctx.service_count, dump_ctx.public_service_count); + return ret; + } - return ret; + if (ret != PM3_SUCCESS) { + PrintAndLogEx(FAILED, "Unable to discover nodes using RequestCodeList/SearchServiceCode/RequestService/ReadWithoutEncryption."); + return ret; + } + + if (dump_ctx.public_service_count == 0) { + PrintAndLogEx(WARNING, "No authentication-not-required services discovered."); + } + + PrintAndLogEx(SUCCESS, "Unauth service dump complete. Discovered %" PRIu32 " node(s), visited %" PRIu32 " service(s), dumped %" PRIu32 " public service(s).", + discovered_nodes, dump_ctx.service_count, dump_ctx.public_service_count); + + return PM3_SUCCESS; } @@ -2494,7 +3522,7 @@ static int CmdHFFelicaRequestService(const char *Cmd) { "in the command packet.", "hf felcia rqservice --node 01 --code FFFF\n" "hf felcia rqservice -a --code FFFF\n" - "hf felica rqservice -i 011204126417E405 --node 01 --code FFFF" + "hf felica rqservice --idm 011204126417E405 --node 01 --code FFFF" ); void *argtable[] = { @@ -2502,7 +3530,7 @@ static int CmdHFFelicaRequestService(const char *Cmd) { arg_lit0("a", "all", "auto node number mode, iterates through all nodes 1 < n < 32"), arg_str0("n", "node", "", "Number of Node"), arg_str0("c", "code", "", "Node Code List (little endian)"), - arg_str0("i", "idm", "", "use custom IDm"), + arg_str0(NULL, "idm", "", "use custom IDm"), arg_param_end }; CLIExecWithReturn(ctx, Cmd, argtable, true); @@ -2536,13 +3564,6 @@ static int CmdHFFelicaRequestService(const char *Cmd) { uint8_t data[PM3_CMD_DATA_SIZE]; memset(data, 0, sizeof(data)); - bool custom_IDm = false; - - if (ilen) { - custom_IDm = true; - memcpy(data + 2, idm, 8); - } - if (all_nodes == false) { // Node Number if (nlen == 1) { @@ -2557,15 +3578,12 @@ static int CmdHFFelicaRequestService(const char *Cmd) { uint8_t datalen = 13; // length (1) + CMD (1) + IDm(8) + Node Number (1) + Node Code List (2) - uint8_t flags = (FELICA_APPEND_CRC | FELICA_RAW); - if (custom_IDm) { - flags |= FELICA_NO_SELECT; + res = felica_ensure_target_present(idm, (size_t)ilen, FELICA_IDM_RESOLVE_STANDALONE, data + 2); + if (res != PM3_SUCCESS) { + return res; } - // Todo activate once datalen isn't hardcoded anymore... - if (custom_IDm == false && check_last_idm(data, datalen) == false) { - return PM3_EINVARG; - } + uint8_t flags = (FELICA_APPEND_CRC | FELICA_RAW); data[0] = (datalen & 0xFF); data[1] = 0x02; // Service Request Command ID @@ -2589,130 +3607,158 @@ static int CmdHFFelicaRequestService(const char *Cmd) { * @param Cmd input data of the user. * @return client result code. */ -static int CmdHFFelicaDumpServiceArea(const char *Cmd) { - /* -- CLI boilerplate (unchanged) ------------------------------- */ +static int CmdHFFelicaDiscoverNodes(const char *Cmd) { + /* -- CLI boilerplate (method-aware discovery) ------------------- */ CLIParserContext *ctx; - CLIParserInit(&ctx, "hf felica scsvcode", - "Dump all existing Area Code and Service Code.\n", - "hf felica scsvcode\n" - "hf felica scsvcode --retry 5"); + CLIParserInit(&ctx, "hf felica discnodes", + "Dump all existing Area Code and Service Code.\n" + "Method: auto | request_code_list | search_service_code | request_service | read_without_encryption", + "hf felica discnodes\n" + "hf felica discnodes --retry 5\n" + "hf felica discnodes --method request_service\n" + "hf felica discnodes --idm 11100910C11BC407"); void *argtable[] = { arg_param_begin, arg_u64_0("r", "retry", "", "number of retries"), + arg_str0("m", "method", "", "node discovery method"), + arg_str0(NULL, "idm", "", "use custom IDm"), + arg_param_end + }; + CLIExecWithReturn(ctx, Cmd, argtable, true); + uint32_t retry_count = arg_get_u32_def(ctx, 1, FELICA_DISCOVER_DEFAULT_RETRY_COUNT); + char method_str[64] = {0}; + int method_len = 0; + int method_str_status = CLIParamStrToBuf(arg_get_str(ctx, 2), (uint8_t *)method_str, sizeof(method_str) - 1, &method_len); + uint8_t idm[8] = {0}; + int ilen = 0; + int res = CLIParamHexToBuf(arg_get_str(ctx, 3), idm, sizeof(idm), &ilen); + felica_node_discovery_method_t selected_method = FELICA_NODE_DISCOVERY_NONE; + int method_parse_status = PM3_EINVARG; + if (method_str_status == PM3_SUCCESS) { + method_str[method_len] = '\0'; + method_parse_status = felica_parse_node_discovery_method(method_str, &selected_method); + } + CLIParserFree(ctx); + if (res != PM3_SUCCESS) { + return PM3_EINVARG; + } + if (method_str_status != PM3_SUCCESS || method_parse_status != PM3_SUCCESS) { + return method_parse_status; + } + + res = felica_ensure_target_present(idm, (size_t)ilen, FELICA_IDM_RESOLVE_STANDALONE, idm); + if (res != PM3_SUCCESS) { + return res; + } + + PrintAndLogEx(HINT, "Area and service codes are printed in network order."); + PrintAndLogEx(INFO, "Press " _GREEN_("") " to abort discovery"); + + uint8_t flags = FELICA_CONNECT | FELICA_NO_SELECT | FELICA_NO_DISCONNECT | FELICA_APPEND_CRC | FELICA_RAW; + felica_scsvcode_context_t scsv_ctx; + memset(&scsv_ctx, 0, sizeof(scsv_ctx)); + scsv_ctx.area_end_stack[0] = 0xFFFF; + + uint32_t discovered_nodes = 0; + felica_node_discovery_method_t used_method = FELICA_NODE_DISCOVERY_NONE; + uint64_t discovery_started = msclock(); + int ret = felica_discover_nodes(idm, &flags, retry_count, + selected_method, + felica_scsvcode_discovery_visitor, &scsv_ctx, + &used_method, &discovered_nodes); + uint64_t discovery_duration_ms = msclock() - discovery_started; + + DropField(); + + if (ret == PM3_EOPABORTED) { + felica_scsvcode_print_footer(&scsv_ctx); + PrintAndLogEx(WARNING, "Node discovery aborted by user after %" PRIu64 " ms. Discovered %" PRIu32 " node(s): %" PRIu32 " area(s), %" PRIu32 " service(s).", + discovery_duration_ms, discovered_nodes, scsv_ctx.area_count, scsv_ctx.service_count); + return ret; + } + + if (ret != PM3_SUCCESS) { + if (selected_method != FELICA_NODE_DISCOVERY_NONE) { + PrintAndLogEx(FAILED, "Node discovery failed with --method %s.", felica_node_discovery_method_cli_name(selected_method)); + } else { + PrintAndLogEx(FAILED, "Unable to discover nodes using RequestCodeList/SearchServiceCode/RequestService/ReadWithoutEncryption."); + } + return ret; + } + + felica_scsvcode_print_footer(&scsv_ctx); + PrintAndLogEx(INFO, "Node discovery duration: %" PRIu64 " ms", discovery_duration_ms); + + PrintAndLogEx(SUCCESS, "Service code and area dump complete. Discovered %" PRIu32 " node(s): %" PRIu32 " area(s), %" PRIu32 " service(s).", + discovered_nodes, scsv_ctx.area_count, scsv_ctx.service_count); + return PM3_SUCCESS; +} + +/** + * Command parser for scsvcode. + * @param Cmd input data of the user. + * @return client result code. + */ +static int CmdHFFelicaDumpServiceArea(const char *Cmd) { + CLIParserContext *ctx; + CLIParserInit(&ctx, "hf felica scsvcode", + "Dump all existing Area Code and Service Code.", + "hf felica scsvcode\n" + "hf felica scsvcode --retry 5\n" + "hf felica scsvcode --idm 11100910C11BC407"); + void *argtable[] = { + arg_param_begin, + arg_u64_0("r", "retry", "", "number of retries"), + arg_str0(NULL, "idm", "", "use custom IDm"), arg_param_end }; CLIExecWithReturn(ctx, Cmd, argtable, true); uint32_t retry_count = arg_get_u32_def(ctx, 1, FELICA_DEFAULT_RETRY_COUNT); + uint8_t idm[8] = {0}; + int ilen = 0; + int res = CLIParamHexToBuf(arg_get_str(ctx, 2), idm, sizeof(idm), &ilen); CLIParserFree(ctx); - - /* -- build static part of Search-Service frame ---------------- */ - uint8_t data[PM3_CMD_DATA_SIZE] = {0}; - data[0] = 0x0C; /* LEN */ - data[1] = 0x0A; /* CMD = 0x0A */ - uint16_t datalen = 12; /* LEN + CMD + IDm + cursor */ - - if (!check_last_idm(data, datalen)) + if (res) { return PM3_EINVARG; + } + + res = felica_ensure_target_present(idm, (size_t)ilen, FELICA_IDM_RESOLVE_STANDALONE, idm); + if (res != PM3_SUCCESS) { + return res; + } PrintAndLogEx(HINT, "Area and service codes are printed in network order."); - PrintAndLogEx(INFO, "┌───────────────────────────────────────────────"); + PrintAndLogEx(INFO, "Press " _GREEN_("") " to abort discovery"); - // Set up field once and keep it up for the entire traversal. - // First command connects (with NO_SELECT since we already have IDm). uint8_t flags = FELICA_CONNECT | FELICA_NO_SELECT | FELICA_NO_DISCONNECT | FELICA_APPEND_CRC | FELICA_RAW; + felica_scsvcode_context_t scsv_ctx; + memset(&scsv_ctx, 0, sizeof(scsv_ctx)); + scsv_ctx.area_end_stack[0] = 0xFFFF; - int ret = PM3_SUCCESS; - - /* -- traversal state ------------------------------------------ */ - uint16_t cursor = 0x0000; - uint16_t area_end_stack[8] = {0xFFFF}; /* root "end" = 0xFFFF */ - int depth = 0; /* current stack depth */ - - felica_search_service_code_response_t resp; - - while (true) { - - /* insert cursor LE */ - data[10] = cursor & 0xFF; - data[11] = cursor >> 8; - - if (send_search_service_code(flags, datalen, data, 0, - FELICA_DEFAULT_TIMEOUT_MS, retry_count, - &resp) != PM3_SUCCESS) { - PrintAndLogEx(FAILED, "No response at cursor 0x%04X", cursor); - ret = PM3_ERFTRANS; - break; - } - - // After first command, drop CONNECT flag — field is already up - flags = FELICA_NO_DISCONNECT | FELICA_APPEND_CRC | FELICA_RAW; - - uint8_t len = resp.frame_response.length[0]; - uint16_t node_code = resp.payload[0] | (resp.payload[1] << 8); /* LE for traversal */ - uint16_t node_code_net = (resp.payload[0] << 8) | resp.payload[1]; /* BE for display */ - uint16_t node_number = node_code >> 6; /* upper 10 bits in host order */ - - if (node_code == 0xFFFF) break; /* end-marker */ - - /* pop finished areas */ - while (depth && node_code > area_end_stack[depth]) depth--; - - - /* ----- compose nice prefix ------------------------------------ */ - char prefix[64] = ""; - for (int i = 1; i < depth; i++) { - bool more_siblings = (cursor < area_end_stack[i]); - strcat(prefix, more_siblings ? "│ " : " "); - } - /* decide glyph for this line (areas always use └──) */ - const char *line_glyph = "├── "; - strcat(prefix, line_glyph); - - /* ----- print --------------------------------------------------- */ - if (len == 0x0E) { /* AREA node */ - uint16_t end_code = resp.payload[2] | (resp.payload[3] << 8); - uint16_t end_number = end_code >> 6; - PrintAndLogEx(INFO, "%sAREA_%02X%02X%02X%02X (%u-%u)", prefix, - resp.payload[0], resp.payload[1], resp.payload[2], resp.payload[3], - node_number, end_number); - - if (depth < 7) { - area_end_stack[++depth] = end_code; - } - } else if (len == 0x0C) { /* SERVICE */ - PrintAndLogEx(INFO, "%sSVC_%04X (%u)", prefix, node_code_net, node_number); - } else { - PrintAndLogEx(FAILED, "Unexpected length 0x%02X @ 0x%04X", - len, cursor); - ret = PM3_ERFTRANS; - break; - } - cursor++; - if (cursor == 0) break; /* overflow safety */ - } + uint32_t discovered_nodes = 0; + int ret = felica_discover_nodes(idm, &flags, retry_count, + FELICA_NODE_DISCOVERY_SEARCH_SERVICE_CODE, + felica_scsvcode_discovery_visitor, &scsv_ctx, + NULL, &discovered_nodes); DropField(); - /* draw closing bar └─┴─... based on final depth */ - char bar[128]; /* large enough for depth ≤ 7 */ - size_t pos = 0; + if (ret == PM3_EOPABORTED) { + felica_scsvcode_print_footer(&scsv_ctx); + PrintAndLogEx(WARNING, "Service code and area dump aborted by user. Discovered %" PRIu32 " node(s): %" PRIu32 " area(s), %" PRIu32 " service(s).", + discovered_nodes, scsv_ctx.area_count, scsv_ctx.service_count); + return ret; + } - /* leading corner */ - pos += snprintf(bar + pos, sizeof(bar) - pos, "└"); + if (ret != PM3_SUCCESS) { + PrintAndLogEx(FAILED, "Unable to discover nodes using SearchServiceCode."); + return ret; + } - /* one segment per level-1 */ - for (int i = 0; i < depth - 1 && pos < sizeof(bar); i++) - pos += snprintf(bar + pos, sizeof(bar) - pos, "───┴"); - - /* tail */ - snprintf(bar + pos, sizeof(bar) - pos, "───────────────────────"); - - PrintAndLogEx(INFO, "%s", bar); - - if (ret == PM3_SUCCESS) - PrintAndLogEx(SUCCESS, "Service code and area dump complete."); - - return ret; + felica_scsvcode_print_footer(&scsv_ctx); + PrintAndLogEx(SUCCESS, "Service code and area dump complete. Discovered %" PRIu32 " node(s): %" PRIu32 " area(s), %" PRIu32 " service(s).", + discovered_nodes, scsv_ctx.area_count, scsv_ctx.service_count); + return PM3_SUCCESS; } static int CmdHFFelicaSniff(const char *Cmd) { @@ -3345,7 +4391,7 @@ static int CmdHFFelicaAuthenticationLite(const char *Cmd) { CLIParserContext *ctx; CLIParserInit(&ctx, "hf felica liteauth", "Authenticate", - "hf felica liteauth -i 11100910C11BC407\n" + "hf felica liteauth --idm 11100910C11BC407\n" "hf felica liteauth --key 46656c69436130313233343536616263\n" "hf felica liteauth --key 46656c69436130313233343536616263 -k\n" "hf felica liteauth -c 701185c59f8d30afeab8e4b3a61f5cc4 --key 46656c69436130313233343536616263" @@ -3354,7 +4400,7 @@ static int CmdHFFelicaAuthenticationLite(const char *Cmd) { arg_param_begin, arg_str0(NULL, "key", "", "set card key, 16 bytes"), arg_str0("c", "", "", "set random challenge, 16 bytes"), - arg_str0("i", "", "", "set custom IDm"), + arg_str0(NULL, "idm", "", "set custom IDm"), arg_lit0("k", "", "keep signal field ON after receive"), arg_param_end }; @@ -3391,13 +4437,9 @@ static int CmdHFFelicaAuthenticationLite(const char *Cmd) { CLIParserFree(ctx); - if (!ilen) { - if (last_known_card.IDm[0] != 0 && last_known_card.IDm[1] != 0) { - memcpy(idm, last_known_card.IDm, sizeof(idm)); - } else { - PrintAndLogEx(WARNING, "No last known card! Use `" _YELLOW_("hf felica reader") "` first or set a custom IDm"); - return PM3_EINVARG; - } + res = felica_ensure_target_present(idm, (size_t)ilen, FELICA_IDM_RESOLVE_STANDALONE, idm); + if (res != PM3_SUCCESS) { + return res; } int ret = PM3_SUCCESS; @@ -3593,7 +4635,7 @@ static int CmdHFFelicaDumpLite(const char *Cmd) { ); void *argtable[] = { arg_param_begin, - arg_str0("i", "", "", "set custom IDm"), + arg_str0(NULL, "idm", "", "set custom IDm"), arg_str0(NULL, "key", "", "set card key, 16 bytes"), arg_param_end }; @@ -3620,13 +4662,9 @@ static int CmdHFFelicaDumpLite(const char *Cmd) { CLIParserFree(ctx); if (keylen != 0) { - if (!ilen) { - if (last_known_card.IDm[0] != 0 && last_known_card.IDm[1] != 0) { - memcpy(idm, last_known_card.IDm, sizeof(idm)); - } else { - PrintAndLogEx(WARNING, "No last known card! Use `" _YELLOW_("hf felica reader") "` first or set a custom IDm"); - return PM3_EINVARG; - } + res = felica_ensure_target_present(idm, (size_t)ilen, FELICA_IDM_RESOLVE_STANDALONE, idm); + if (res != PM3_SUCCESS) { + return res; } uint8_t rc[FELICA_BLK_SIZE] = {0}; @@ -3832,6 +4870,7 @@ static command_t CommandTable[] = { {"wrbl", CmdHFFelicaWritePlain, IfPm3Felica, "write block data to an authentication-not-required Service."}, {"-----------", CmdHelp, AlwaysAvailable, "----------------------- " _CYAN_("FeliCa Standard") " -----------------------"}, {"dump", CmdHFFelicaDump, IfPm3Felica, "Wait for and try dumping FeliCa"}, + {"discnodes", CmdHFFelicaDiscoverNodes, IfPm3Felica, "discover Area Code and Service Code nodes."}, {"rqservice", CmdHFFelicaRequestService, IfPm3Felica, "verify the existence of Area and Service, and to acquire Key Version."}, {"rqresponse", CmdHFFelicaRequestResponse, IfPm3Felica, "verify the existence of a card and its Mode."}, {"scsvcode", CmdHFFelicaDumpServiceArea, IfPm3Felica, "acquire Area Code and Service Code."}, diff --git a/client/src/cmdhfgallagher.c b/client/src/cmdhfgallagher.c index 898cd45d9..055a310f6 100644 --- a/client/src/cmdhfgallagher.c +++ b/client/src/cmdhfgallagher.c @@ -20,20 +20,28 @@ #include "cmdhfgallagher.h" #include "generator.h" #include "mifare.h" +#include "mifare/mifare4.h" #include "mifare/desfirecore.h" +#include "mifare/mifarehost.h" #include "mifare/gallaghercore.h" +#include "mifare/gallaghertest.h" +#include "mifare/mad.h" +#include "crc.h" #include #include #include "common.h" #include "commonutil.h" #include "cmdparser.h" #include "cliparser.h" +#include "comms.h" #include "ui.h" static int CmdHelp(const char *cmd); // Application ID for the Gallagher Card Application Directory -static const uint32_t CAD_AID = 0x2F81F4; +#define DESFIRE_CAD_AID 0x2F81F4 +#define CLASSIC_CAD_AID 0x4811 +#define CLASSIC_CRED_AID 0x4812 // Default MIFARE site key (16 bytes) static const uint8_t DEFAULT_SITE_KEY[] = { @@ -53,8 +61,8 @@ static const uint8_t DEFAULT_SITE_KEY[] = { * @param key_output Buffer to copy the diversified key into (must be 16 bytes). * @return PM3_SUCCESS if successful, PM3_EINVARG if an argument is invalid. */ -int hfgal_diversify_key(uint8_t *site_key, uint8_t *uid, uint8_t uid_len, - uint8_t key_num, uint32_t aid, uint8_t *key_output) { +int hfgal_diversify_desfire_key(uint8_t *site_key, uint8_t *uid, uint8_t uid_len, + uint8_t key_num, uint32_t aid, uint8_t *key_output) { // Generate diversification input uint8_t kdf_input_len = 11; int res = mfdes_kdf_input_gallagher(uid, uid_len, key_num, aid, key_output, &kdf_input_len); @@ -62,7 +70,7 @@ int hfgal_diversify_key(uint8_t *site_key, uint8_t *uid, uint8_t uid_len, uint8_t key[sizeof(DEFAULT_SITE_KEY)] = {0}; if (site_key == NULL) { - PrintAndLogEx(INFO, "hfgal_diversify_key is using default site key"); + PrintAndLogEx(INFO, "hfgal_diversify_desfire_key is using default site key"); memcpy(key, DEFAULT_SITE_KEY, sizeof(key)); } else { memcpy(key, site_key, sizeof(key)); @@ -261,7 +269,7 @@ static uint32_t find_available_gallagher_aid(DesfireContext_t *ctx, bool verbose for (uint8_t aid_increment = 0x20; aid_increment < 0xFF; aid_increment++) { uint32_t aid = 0x0081F4 | (aid_increment << 16); - if (aid == CAD_AID) { + if (aid == DESFIRE_CAD_AID) { continue; } @@ -403,7 +411,7 @@ static int hfgal_create_creds_app(DesfireContext_t *ctx, uint8_t *site_key, uint for (int i = 2; i >= 0; i -= 2) { // Diversify key uint8_t buf[CRYPTO_AES128_KEY_SIZE] = {0}; - res = hfgal_diversify_key(site_key, ctx->uid, ctx->uidlen, i, aid, buf); + res = hfgal_diversify_desfire_key(site_key, ctx->uid, ctx->uidlen, i, aid, buf); PM3_RET_IF_ERR_WITH_MSG(res, "Failed diversifying key %d for AID %06X", i, aid); PrintAndLogEx(INFO, "Diversified key %d for AID %06X: " _GREEN_("%s"), @@ -491,10 +499,10 @@ static int hfgal_create_creds_file(DesfireContext_t *ctx, uint8_t *site_key, uin * @param dest_buf_len Size of dest_buf. Must be at least 108 bytes. * @param num_entries Will be set to the number of entries in the CAD. */ -static int hfgal_read_cad(DesfireContext_t *ctx, uint8_t *dest_buf, - uint8_t dest_buf_len, uint8_t *num_entries_out, bool verbose) { +static int hfgal_read_desfire_cad(DesfireContext_t *ctx, uint8_t *dest_buf, + uint8_t dest_buf_len, uint8_t *num_entries_out, bool verbose) { if (dest_buf_len < 3 * 36) { - PrintAndLogEx(ERR, "hfgal_read_cad destination buffer is incorrectly sized. Received len %d, must be at least %d", + PrintAndLogEx(ERR, "hfgal_read_desfire_cad destination buffer is incorrectly sized. Received len %d, must be at least %d", dest_buf_len, 3 * 36 ); @@ -502,15 +510,15 @@ static int hfgal_read_cad(DesfireContext_t *ctx, uint8_t *dest_buf, } // Get card AIDs from Card Application Directory (which contains 1 to 3 files) - int res = select_aid(ctx, CAD_AID, verbose); - PM3_RET_IF_ERR_WITH_MSG(res, "Failed selecting Card Application Directory, does AID %06X exist?", CAD_AID); + int res = select_aid(ctx, DESFIRE_CAD_AID, verbose); + PM3_RET_IF_ERR_WITH_MSG(res, "Failed selecting Card Application Directory, does AID %06X exist?", DESFIRE_CAD_AID); // Read up to 3 files with 6x 6-byte entries each for (uint8_t i = 0; i < 3; i++) { size_t read_len; res = DesfireReadFile(ctx, i, 0, 36, &dest_buf[i * 36], &read_len); if (res != PM3_SUCCESS && res != PM3_EAPDU_FAIL) { - PM3_RET_ERR(res, "Failed reading file %d in Card Application Directory (AID %06X)", i, CAD_AID); + PM3_RET_ERR(res, "Failed reading file %d in Card Application Directory (AID %06X)", i, DESFIRE_CAD_AID); } // end if the last entry is NULL @@ -553,8 +561,8 @@ static int hfgal_read_cad(DesfireContext_t *ctx, uint8_t *dest_buf, * @param key MIFARE site key, or custom CAD key. * @param should_diversify True if using a site_key, false if using a custom CAD key. */ -static int hfgal_create_cad(DesfireContext_t *ctx, uint8_t *key, - bool should_diversify, bool verbose) { +static int hfgal_create_desfire_cad(DesfireContext_t *ctx, uint8_t *key, + bool should_diversify, bool verbose) { // Check that card UID has been set if (ctx->uidlen == 0) { PM3_RET_ERR(PM3_EINVARG, "Card UID must be set in DesfireContext (required for key div)"); @@ -571,39 +579,39 @@ static int hfgal_create_cad(DesfireContext_t *ctx, uint8_t *key, uint8_t ks2 = (DesfireKeyAlgoToType(app_algo) << 6) | num_keys;; uint8_t data[5] = {0}; - DesfireAIDUintToByte(CAD_AID, &data[0]); + DesfireAIDUintToByte(DESFIRE_CAD_AID, &data[0]); data[3] = ks1; data[4] = ks2; DesfireSetCommMode(ctx, DCMMACed); res = DesfireCreateApplication(ctx, data, ARRAYLEN(data)); - PM3_RET_IF_ERR_WITH_MSG(res, "Failed creating Card Application Directory (AID " _YELLOW_("%06X")"). Does it already exist?", CAD_AID); + PM3_RET_IF_ERR_WITH_MSG(res, "Failed creating Card Application Directory (AID " _YELLOW_("%06X")"). Does it already exist?", DESFIRE_CAD_AID); if (verbose) { PrintAndLogEx(INFO, "Created Card Application Directory (AID " _YELLOW_("%06X") ", empty contents & blank keys)", - CAD_AID + DESFIRE_CAD_AID ); } // Select application & authenticate uint8_t blank_key[DESFIRE_MAX_KEY_SIZE] = {0}; - res = select_aid_and_auth_with_key(ctx, CAD_AID, blank_key, 0, false, verbose); + res = select_aid_and_auth_with_key(ctx, DESFIRE_CAD_AID, blank_key, 0, false, verbose); PM3_RET_IF_ERR(res); uint8_t buf[CRYPTO_AES128_KEY_SIZE] = {0}; if (should_diversify) { // Diversify key - res = hfgal_diversify_key(key, ctx->uid, ctx->uidlen, 0, CAD_AID, buf); - PM3_RET_IF_ERR_WITH_MSG(res, "Failed diversifying key 0 for AID %06X", CAD_AID); + res = hfgal_diversify_desfire_key(key, ctx->uid, ctx->uidlen, 0, DESFIRE_CAD_AID, buf); + PM3_RET_IF_ERR_WITH_MSG(res, "Failed diversifying key 0 for AID %06X", DESFIRE_CAD_AID); PrintAndLogEx(INFO, "Diversified key " _YELLOW_("0") " for CAD (AID " _YELLOW_("%06X") "): " _GREEN_("%s"), - CAD_AID, + DESFIRE_CAD_AID, sprint_hex_inrow(buf, ARRAYLEN(buf)) ); key = buf; } else if (verbose) { PrintAndLogEx(INFO, "Using provided key " _YELLOW_("0") " for CAD (AID " _YELLOW_("%06X") "): " _GREEN_("%s"), - CAD_AID, + DESFIRE_CAD_AID, sprint_hex_inrow(key, CRYPTO_AES128_KEY_SIZE) ); } @@ -617,7 +625,7 @@ static int hfgal_create_cad(DesfireContext_t *ctx, uint8_t *key, PrintAndLogEx(INFO, "Successfully set key " _YELLOW_("0") " for CAD"); } - PrintAndLogEx(INFO, "Successfully created Card Application Directory (AID " _YELLOW_("%06X") ")", CAD_AID); + PrintAndLogEx(INFO, "Successfully created Card Application Directory (AID " _YELLOW_("%06X") ")", DESFIRE_CAD_AID); return PM3_SUCCESS; } @@ -634,12 +642,12 @@ static int hfgal_add_aid_to_cad(DesfireContext_t *ctx, uint8_t *key, bool should // Check if CAD exists uint8_t cad[36 * 3] = {0}; uint8_t num_entries = 0; - if (aid_exists(ctx, CAD_AID, false)) { + if (aid_exists(ctx, DESFIRE_CAD_AID, false)) { if (verbose) { PrintAndLogEx(INFO, "Card Application Directory exists, reading entries..."); } - int res = hfgal_read_cad(ctx, cad, ARRAYLEN(cad), &num_entries, verbose); + int res = hfgal_read_desfire_cad(ctx, cad, ARRAYLEN(cad), &num_entries, verbose); PM3_RET_IF_ERR(res); // Check that there is space for the new entry @@ -653,7 +661,7 @@ static int hfgal_add_aid_to_cad(DesfireContext_t *ctx, uint8_t *key, bool should PrintAndLogEx(INFO, "Card Application Directory does not exist, creating it now..."); } - int res = hfgal_create_cad(ctx, key, should_diversify, verbose); + int res = hfgal_create_desfire_cad(ctx, key, should_diversify, verbose); PM3_RET_IF_ERR(res); } @@ -686,7 +694,7 @@ static int hfgal_add_aid_to_cad(DesfireContext_t *ctx, uint8_t *key, bool should } // Select application & authenticate - int res = select_aid_and_auth_with_key(ctx, CAD_AID, key, 0, should_diversify, verbose); + int res = select_aid_and_auth_with_key(ctx, DESFIRE_CAD_AID, key, 0, should_diversify, verbose); PM3_RET_IF_ERR(res); // Create file if necessary @@ -710,7 +718,7 @@ static int hfgal_add_aid_to_cad(DesfireContext_t *ctx, uint8_t *key, bool should // Create file res = DesfireCreateFile(ctx, file_type, data, ARRAYLEN(data), false); - PM3_RET_IF_ERR_WITH_MSG(res, "Failed creating file %d in CAD (AID %06X)", file_id, CAD_AID); + PM3_RET_IF_ERR_WITH_MSG(res, "Failed creating file %d in CAD (AID %06X)", file_id, DESFIRE_CAD_AID); if (verbose) { PrintAndLogEx(INFO, "Created file " _YELLOW_("%d") " in CAD (empty contents)", file_id); @@ -722,7 +730,7 @@ static int hfgal_add_aid_to_cad(DesfireContext_t *ctx, uint8_t *key, bool should // Write file res = DesfireWriteFile(ctx, file_id, entry_num * 6, 6, entry); } - PM3_RET_IF_ERR_WITH_MSG(res, "Failed writing data to file %d in CAD AID %06X)", file_id, CAD_AID); + PM3_RET_IF_ERR_WITH_MSG(res, "Failed writing data to file %d in CAD AID %06X)", file_id, DESFIRE_CAD_AID); PrintAndLogEx(INFO, "Successfully added new entry for " _YELLOW_("%06X") " to the Card Application Directory", aid); return PM3_SUCCESS; @@ -740,7 +748,7 @@ static int hfgal_remove_aid_from_cad(DesfireContext_t *ctx, uint8_t *key, // Read CAD uint8_t cad[36 * 3] = {0}; uint8_t num_entries = 0; - int res = hfgal_read_cad(ctx, cad, ARRAYLEN(cad), &num_entries, verbose); + int res = hfgal_read_desfire_cad(ctx, cad, ARRAYLEN(cad), &num_entries, verbose); PM3_RET_IF_ERR(res); // Check if facility already exists in CAD @@ -763,7 +771,7 @@ static int hfgal_remove_aid_from_cad(DesfireContext_t *ctx, uint8_t *key, memset(&cad[ARRAYLEN(cad) - 6], 0, 6); // Select application & authenticate - res = select_aid_and_auth_with_key(ctx, CAD_AID, key, 0, should_diversify, verbose); + res = select_aid_and_auth_with_key(ctx, DESFIRE_CAD_AID, key, 0, should_diversify, verbose); PM3_RET_IF_ERR(res); // Determine what files we need to update @@ -774,7 +782,7 @@ static int hfgal_remove_aid_from_cad(DesfireContext_t *ctx, uint8_t *key, for (uint8_t file_id = file_id_start; file_id <= file_id_stop - delete_last_file; file_id++) { // Write file res = DesfireWriteFile(ctx, file_id, 0, 36, &cad[file_id * 36]); - PM3_RET_IF_ERR_WITH_MSG(res, "Failed writing data to file %d in CAD (AID %06X)", file_id, CAD_AID); + PM3_RET_IF_ERR_WITH_MSG(res, "Failed writing data to file %d in CAD (AID %06X)", file_id, DESFIRE_CAD_AID); if (verbose) { PrintAndLogEx(INFO, "Updated file " _YELLOW_("%d") " in CAD", file_id); @@ -787,12 +795,12 @@ static int hfgal_remove_aid_from_cad(DesfireContext_t *ctx, uint8_t *key, DesfireSetCommMode(ctx, DCMMACed); res = DesfireDeleteFile(ctx, file_id); - PM3_RET_IF_ERR_WITH_MSG(res, "Failed deleting file %d from CAD (AID %06X)", file_id, CAD_AID); + PM3_RET_IF_ERR_WITH_MSG(res, "Failed deleting file %d from CAD (AID %06X)", file_id, DESFIRE_CAD_AID); if (verbose) { PrintAndLogEx(INFO, "Deleted unnecessary file " _YELLOW_("%d") " from CAD (AID " _YELLOW_("%06X")")", file_id, - CAD_AID + DESFIRE_CAD_AID ); } } @@ -801,6 +809,7 @@ static int hfgal_remove_aid_from_cad(DesfireContext_t *ctx, uint8_t *key, return PM3_SUCCESS; } + /** * @brief Read credentials from a Gallagher card. * @@ -810,7 +819,7 @@ static int hfgal_remove_aid_from_cad(DesfireContext_t *ctx, uint8_t *key, * @param quiet Suppress error messages. Used when in continuous reader mode. */ // iceman, verbose and quiet... one should be enough. -static int hfgal_read_card(uint32_t aid, uint8_t *site_key, bool verbose, bool quiet) { +static int hfgal_read_desfire_card(uint32_t aid, uint8_t *site_key, bool verbose, bool quiet) { DropField(); clearCommandBuffer(); @@ -829,7 +838,7 @@ static int hfgal_read_card(uint32_t aid, uint8_t *site_key, bool verbose, bool q cad_aid_uint_to_byte(aid, &cad[3]); num_entries = 1; } else { - res = hfgal_read_cad(&dctx, cad, ARRAYLEN(cad), &num_entries, verbose); + res = hfgal_read_desfire_cad(&dctx, cad, ARRAYLEN(cad), &num_entries, verbose); PM3_RET_IF_ERR_MAYBE_MSG(res, !quiet, "Failed reading Card Application Directory"); } @@ -866,7 +875,7 @@ static int hfgal_read_card(uint32_t aid, uint8_t *site_key, bool verbose, bool q ", card number: " _GREEN_("%u") ", issue level: " _GREEN_("%u"), current_aid, - 'A' + creds.region_code, + 'A' + creds.region_code, creds.region_code, creds.facility_code, creds.card_number, @@ -876,13 +885,341 @@ static int hfgal_read_card(uint32_t aid, uint8_t *site_key, bool verbose, bool q return PM3_SUCCESS; } + +// Gallagher MIFARE Classic fixed keys +static const uint8_t GALLAGHER_MFC_KEY_A[] = {0x16, 0x0A, 0x91, 0xD2, 0x9A, 0x9C}; +static const uint8_t GALLAGHER_MFC_KEY_B[] = {0xB7, 0xBF, 0x0C, 0x13, 0x06, 0x6E}; + +// Gallagher MIFARE Classic access bits: 0x787788 +static const uint8_t GALLAGHER_ACCESS_BITS[] = {0x78, 0x77, 0x88}; + +// "www.cardax.com " string for block 1 +static const uint8_t CARDAX_STRING[] = { + 0x77, 0x77, 0x77, 0x2E, 0x63, 0x61, 0x72, 0x64, + 0x61, 0x78, 0x2E, 0x63, 0x6F, 0x6D, 0x20, 0x20 +}; + +/** + * @brief Write a Gallagher site-specific sector to a MIFARE Classic card. + * + * Writes blocks 0-2 of the sector using Key B (which has write access). + * Block 0: encoded credentials + bitwise inverse + * Block 1: "www.cardax.com " + * Block 2: MES block (if enabled) or zeroes + * Block 3: sector trailer with Gallagher keys and access bits + * + * @param sector_num Sector number to write. + * @param creds Gallagher cardholder credentials. + * @param write_key Key to authenticate with for writing (6 bytes). + * @param write_key_type Key type (MF_KEY_A or MF_KEY_B). + * @param verbose Verbose output. + * @return PM3_SUCCESS on success. + */ +static int hfgal_write_site_specific_sector(uint8_t sector_num, GallagherCredentials_t *creds, + const uint8_t *write_key, uint8_t write_key_type, + bool verbose) { + if (mfNumBlocksPerSector(sector_num) > 4) { + PrintAndLogEx(ERR, "Only 4-block sectors are supported (sectors 0-31)"); + return PM3_ENOTIMPL; + } + + uint8_t sector_data[4 * MFBLOCK_SIZE]; + memset(sector_data, 0, sizeof(sector_data)); + + // Block 0: encoded credential (8 bytes) + bitwise inverse (8 bytes) + uint8_t *block0 = §or_data[0]; + gallagher_encode_creds(block0, creds); + for (int i = 0; i < 8; i++) { + block0[i + 8] = block0[i] ^ 0xFF; + } + + // Block 1: "www.cardax.com " + memcpy(§or_data[MFBLOCK_SIZE], CARDAX_STRING, MFBLOCK_SIZE); + + // Block 2: MES block (if enabled) or zeroes + if (creds->mes) { + int res = gallagher_encode_mes(§or_data[2 * MFBLOCK_SIZE], creds); + if (res != PM3_SUCCESS) { + PrintAndLogEx(ERR, "Failed to encode MES block"); + return res; + } + } + + // Block 3: sector trailer + uint8_t *trailer = §or_data[3 * MFBLOCK_SIZE]; + memcpy(trailer, GALLAGHER_MFC_KEY_A, MIFARE_KEY_SIZE); + memcpy(trailer + 6, GALLAGHER_ACCESS_BITS, 3); + trailer[9] = creds->mes ? 0x1D : 0xC1; // user byte + memcpy(trailer + 10, GALLAGHER_MFC_KEY_B, MIFARE_KEY_SIZE); + + // Write blocks 0-2 (data blocks) using the provided key + uint8_t first_block = mfFirstBlockOfSector(sector_num); + for (int i = 0; i < 3; i++) { + int res = mf_write_block(first_block + i, write_key_type, write_key, §or_data[i * MFBLOCK_SIZE]); + if (res != PM3_SUCCESS) { + PrintAndLogEx(ERR, "Failed writing block %d (sector %d, block %d)", first_block + i, sector_num, i); + return res; + } + } + + // Write block 3 (sector trailer) to set keys and access bits + int res = mf_write_block(first_block + 3, write_key_type, write_key, trailer); + if (res != PM3_SUCCESS) { + PrintAndLogEx(ERR, "Failed writing sector trailer (sector %d)", sector_num); + return res; + } + + if (verbose) { + PrintAndLogEx(SUCCESS, "Wrote Gallagher credentials to sector " _YELLOW_("%d"), sector_num); + } + return PM3_SUCCESS; +} + +/** + * @brief Update the MAD sector 0 to register Gallagher AIDs. + * + * Reads the existing MAD, sets AID entries for the credential and + * optionally CAD sectors, recalculates the CRC, and writes it back. + * + * @param cred_sector Sector number for the credential data (AID 0x4812). + * @param cad_sector Sector number for the CAD, or 0 to skip. + * @param mad_key Key B for MAD sector 0 (6 bytes). + * @param mad_key_type Key type for MAD sector 0. + * @param verbose Verbose output. + * @return PM3_SUCCESS on success. + */ +static int hfgal_update_mad(uint8_t cred_sector, uint8_t cad_sector, + const uint8_t *mad_key, uint8_t mad_key_type, + bool verbose) { + // Read current MAD (sector 0) + uint8_t sector0[4 * MFBLOCK_SIZE] = {0}; + int res = mf_read_sector(0, mad_key_type, mad_key, sector0); + if (res != PM3_SUCCESS) { + PrintAndLogEx(ERR, "Failed reading MAD sector 0"); + return res; + } + + // Set AID for credential sector (0x4812) + if (cred_sector >= 1 && cred_sector <= 15) { + sector0[16 + 2 + (cred_sector - 1) * 2] = CLASSIC_CRED_AID & 0xFF; + sector0[16 + 2 + (cred_sector - 1) * 2 + 1] = (CLASSIC_CRED_AID >> 8) & 0xFF; + } + + // Set AID for CAD sector (0x4811) + if (cad_sector >= 1 && cad_sector <= 15) { + sector0[16 + 2 + (cad_sector - 1) * 2] = CLASSIC_CAD_AID & 0xFF; + sector0[16 + 2 + (cad_sector - 1) * 2 + 1] = (CLASSIC_CAD_AID >> 8) & 0xFF; + } + + // Recalculate CRC over bytes 17..47 (info byte + 15 AID entries) + sector0[16] = CRC8Mad(§or0[16 + 1], 15 + 16); + + // Write blocks 1 and 2 of sector 0 back (block 0 is manufacturer block, don't touch) + res = mf_write_block(1, mad_key_type, mad_key, §or0[MFBLOCK_SIZE]); + if (res != PM3_SUCCESS) { + PrintAndLogEx(ERR, "Failed writing MAD block 1"); + return res; + } + + res = mf_write_block(2, mad_key_type, mad_key, §or0[2 * MFBLOCK_SIZE]); + if (res != PM3_SUCCESS) { + PrintAndLogEx(ERR, "Failed writing MAD block 2"); + return res; + } + + if (verbose) { + PrintAndLogEx(SUCCESS, "Updated MAD sector 0"); + } + return PM3_SUCCESS; +} + +/** + * @brief Write Gallagher credentials to a MIFARE Classic card. + * + * @param creds Gallagher cardholder credentials (with csn, site_key populated). + * @param cred_sector Sector to write credentials to. + * @param cad_sector Sector for CAD (0 to skip CAD update). + * @param update_mad Whether to update MAD sector 0. + * @param verbose Verbose output. + * @return PM3_SUCCESS on success. + */ +static int hfgal_write_classic_card(GallagherCredentials_t *creds, uint8_t cred_sector, + uint8_t cad_sector, bool update_mad, bool verbose) { + clearCommandBuffer(); + + // Select card to get UID + SendCommandMIX(CMD_HF_ISO14443A_READER, ISO14A_CONNECT, 0, 0, NULL, 0); + PacketResponseNG resp; + if (WaitForResponseTimeout(CMD_ACK, &resp, 2500) == false) { + PrintAndLogEx(ERR, "Card select timeout"); + return PM3_ETIMEOUT; + } + + iso14a_card_select_t card; + memcpy(&card, (iso14a_card_select_t *)resp.data.asBytes, sizeof(iso14a_card_select_t)); + + uint64_t select_status = resp.oldarg[0]; + if (select_status == 0) { + PrintAndLogEx(ERR, "Card select failed"); + return PM3_EFAILED; + } + + DropField(); + + // Populate CSN in credentials + memcpy(creds->csn, card.uid, card.uidlen); + creds->csn_len = card.uidlen; + + if (verbose) { + PrintAndLogEx(INFO, "Card UID: %s", sprint_hex(card.uid, card.uidlen)); + } + + // For a fresh sector, try writing with the default Key B first (blank card), + // then try Gallagher Key B (already-formatted card) + static const uint8_t default_key[MIFARE_KEY_SIZE] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; + + int res = hfgal_write_site_specific_sector(cred_sector, creds, GALLAGHER_MFC_KEY_B, MF_KEY_B, verbose); + if (res != PM3_SUCCESS) { + if (verbose) { + PrintAndLogEx(INFO, "Gallagher Key B failed, trying default key..."); + } + res = hfgal_write_site_specific_sector(cred_sector, creds, default_key, MF_KEY_B, verbose); + if (res != PM3_SUCCESS) { + res = hfgal_write_site_specific_sector(cred_sector, creds, default_key, MF_KEY_A, verbose); + } + } + + if (res != PM3_SUCCESS) { + PrintAndLogEx(ERR, "Failed writing credential sector %d", cred_sector); + return res; + } + + // Update MAD if requested + if (update_mad) { + // Try MAD Key B first, then default key + static const uint8_t mad_default_b[MIFARE_KEY_SIZE] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; + res = hfgal_update_mad(cred_sector, cad_sector, g_mifare_mad_key_b, MF_KEY_B, verbose); + if (res != PM3_SUCCESS) { + if (verbose) { + PrintAndLogEx(INFO, "MAD Key B failed, trying default key..."); + } + res = hfgal_update_mad(cred_sector, cad_sector, mad_default_b, MF_KEY_B, verbose); + if (res != PM3_SUCCESS) { + res = hfgal_update_mad(cred_sector, cad_sector, mad_default_b, MF_KEY_A, verbose); + } + } + if (res != PM3_SUCCESS) { + PrintAndLogEx(WARNING, "Failed updating MAD - credentials were written but MAD was not updated"); + } + } + + return PM3_SUCCESS; +} + +static int hfgal_read_site_specific_sector(uint8_t sector_num, GallagherCredentials_t cred, uint8_t *csn, uint8_t *diversified_site_key, bool verbose) { + uint8_t keyA[16] = {0x16, 0x0A, 0x91, 0xD2, 0x9A, 0x9C}; + // uint8_t keyB[16] = {0xB7,0xBF,0x0C,0x13,0x06,0x6E}; + + + uint8_t cardax_string[16] = {0x77, 0x77, 0x77, 0x2E, 0x63, 0x61, 0x72, 0x64, 0x61, 0x78, 0x2E, 0x63, 0x6F, 0x6D, 0x20, 0x20}; + uint8_t data[4 * MFBLOCK_SIZE] = {0}; + uint8_t *block0 = &data[0]; + uint8_t *block1 = &data[16]; + uint8_t *block2 = &data[32]; + uint8_t *block3 = &data[48]; + + if (mfNumBlocksPerSector(sector_num) > 4) { + return PM3_ENOTIMPL; + } + + GallagherCredentials_t MES_cred = {0}; + int res = mf_read_sector(sector_num * 4, 0, keyA, data); + if (res != PM3_SUCCESS) { + return res; + } + // check if block1 is an ascii string containing "www.cardax.com " + if (memcmp(block1, cardax_string, 16) != 0) { + if (verbose) { + PrintAndLogEx(FAILED, "Sector %d does not contain a Gallagher Site Key block", sector_num); + } + return PM3_ENODATA; + } + + gallagher_decode_creds(block0, &cred); + if (verbose) { + print_gallagher_creds(&cred); + } + + if (block3[9] == 0x1D) { + if (verbose) { + PrintAndLogEx(INFO, "Sector %d contains a MIFARE Enhanced Security block", sector_num); + } + if (csn != NULL && diversified_site_key != NULL) { + gallagher_decode_mes(block2, &MES_cred); + if (cred.card_number == MES_cred.card_number && + cred.facility_code == MES_cred.facility_code && + cred.issue_level == MES_cred.issue_level && + cred.region_code == MES_cred.region_code) { + PrintAndLogEx(INFO, "MIFARE Enhanced Security block matches Site Specific block\nSite Key is correct"); + } + } else { + PrintAndLogEx(INFO, "No Site Key or CSN provided, cannot verify MIFARE Enhanced Security block"); + } + } else { + PrintAndLogEx(INFO, "Sector %d does not contain a MIFARE Enhanced Security block", sector_num); + } + return PM3_SUCCESS; +} + +static int hfgal_read_classic_card(uint8_t *site_key, bool verbose, bool quiet) { + DropField(); + clearCommandBuffer(); + GallagherCredentials_t creds = {0}; + + // Select card + SendCommandMIX(CMD_HF_ISO14443A_READER, ISO14A_CONNECT, 0, 0, NULL, 0); + PacketResponseNG resp; + if (WaitForResponseTimeout(CMD_ACK, &resp, 2500) == false) { + if (!quiet) { + PrintAndLogEx(DEBUG, "iso14443a card select timeout"); + } + return PM3_ETIMEOUT; + } + + iso14a_card_select_t card; + memcpy(&card, (iso14a_card_select_t *)resp.data.asBytes, sizeof(iso14a_card_select_t)); + + /* + 0: couldn't read + 1: OK, with ATS + 2: OK, no ATS + 3: proprietary Anticollision + */ + uint64_t select_status = resp.oldarg[0]; + + if (select_status == 0) { + if (!quiet) { + PrintAndLogEx(DEBUG, "iso14443a card select failed"); + } + return select_status; + } + + // Brute Force reading all blocks and printing credentials, + for (uint8_t i = 0; i < MIFARE_1K_MAXSECTOR; i++) { + hfgal_read_site_specific_sector(i, creds, card.uid, site_key, true); + } + return PM3_SUCCESS; +} + static int CmdGallagherReader(const char *cmd) { CLIParserContext *ctx; CLIParserInit(&ctx, "hf gallagher reader", - "Read a Gallagher DESFire tag from the Card Application Directory, CAD\n" + "Read a Gallagher tag from the Card Application Directory, CAD\n" "Specify site key is required if using non-default key\n", - "hf gallagher reader -@ -> continuous reader mode\n" - "hf gallagher reader --aid 2081f4 --sitekey 00112233445566778899aabbccddeeff -> skip CAD\n" + "hf gallagher reader -@ -> DESFIRE(default): continuous reader mode\n" + "hf gallagher reader -c -@ -> CLASSIC: continuous reader mode\n" + "hf gallagher reader --aid 2081f4 --sitekey 00112233445566778899aabbccddeeff -> DESFIRE: skip CAD\n" + "" ); void *argtable[] = { @@ -892,6 +1229,7 @@ static int CmdGallagherReader(const char *cmd) { arg_lit0("@", "continuous", "Continuous reader mode"), arg_lit0(NULL, "apdu", "Show APDU requests and responses"), arg_lit0("v", "verbose", "Verbose output"), + arg_lit0("c", "classic", "Read Gallagher mifare Classic card"), arg_param_end }; CLIExecWithReturn(ctx, cmd, argtable, true); @@ -917,17 +1255,30 @@ static int CmdGallagherReader(const char *cmd) { bool continuous_mode = arg_get_lit(ctx, 3); SetAPDULogging(arg_get_lit(ctx, 4)); bool verbose = arg_get_lit(ctx, 5); + bool read_classic_card = arg_get_lit(ctx, 6); CLIParserFree(ctx); if (continuous_mode == false) { // Read single card - return hfgal_read_card(aid, site_key, verbose, false); + if (read_classic_card) { + // Read classic card + return hfgal_read_classic_card(site_key, verbose, false); + } else { + // Read DESFire card + return hfgal_read_desfire_card(aid, site_key, verbose, false); + } } // Loop until is pressed PrintAndLogEx(INFO, "Press " _GREEN_("") " to exit"); while (kbd_enter_pressed() == false) { - hfgal_read_card(aid, site_key, verbose, !verbose); + if (read_classic_card) { + // Read classic card + return hfgal_read_classic_card(site_key, verbose, !verbose); + } else { + // Read DESFire card + return hfgal_read_desfire_card(aid, site_key, verbose, !verbose); + } } return PM3_SUCCESS; } @@ -935,37 +1286,48 @@ static int CmdGallagherReader(const char *cmd) { static int CmdGallagherClone(const char *cmd) { CLIParserContext *ctx; CLIParserInit(&ctx, "hf gallagher clone", - "Clone Gallagher credentials to a writable DESFire card\n" + "Clone Gallagher credentials to a writable DESFire or MIFARE Classic card\n" + "Use -c for MIFARE Classic cards\n" "Specify site key is required if using non-default key\n" - "Key, lengths for the different crypto: \n" + "DESFire key lengths for the different crypto: \n" " DES 8 bytes\n" " 2TDEA or AES 16 bytes\n" " 3TDEA 24 bytes\n" "AID, default finds lowest available in range 0x??81F4, where ?? >= 0x20.", - "hf gallagher clone --rc 1 --fc 22 --cn 3333 --il 4 --sitekey 00112233445566778899aabbccddeeff" + "hf gallagher clone --rc 1 --fc 22 --cn 3333 --il 4 --sitekey 00112233445566778899aabbccddeeff\n" + "hf gallagher clone -c --rc 1 --fc 22 --cn 3333 --il 4\n" + "hf gallagher clone -c --rc 12 --fc 4919 --cn 61453 --il 1 --sector 15 --nomes\n" + "hf gallagher clone -c --rc 1 --fc 22 --cn 3333 --il 4 --nomad" ); void *argtable[] = { arg_param_begin, - arg_int0("n", "keynum", "", "PICC key number [default = 0]"), - arg_str0("t", "algo", "", "PICC crypt algo: DES, 2TDEA, 3TDEA, AES"), - arg_str0("k", "key", "", "Key for authentication to the PICC to create applications"), - arg_u64_1(NULL, "rc", "", "Region code. 4 bits max"), - arg_u64_1(NULL, "fc", "", "Facility code. 2 bytes max"), - arg_u64_1(NULL, "cn", "", "Card number. 3 bytes max"), - arg_u64_1(NULL, "il", "", "Issue level. 4 bits max"), - arg_str0(NULL, "aid", "", "Application ID to write (3 bytes) [default automatically chooses]"), - arg_str0(NULL, "sitekey", "", "Site key to compute diversified keys (16 bytes)"), - arg_str0(NULL, "cadkey", "", "Custom AES key 0 to modify the Card Application Directory (16 bytes)"), - arg_lit0(NULL, "nocadupdate", "Don't modify the Card Application Directory (only creates the app)"), - arg_lit0(NULL, "noappcreate", "Don't create the application (only modifies the CAD)"), - arg_lit0(NULL, "apdu", "Show APDU requests and responses"), - arg_lit0("v", "verbose", "Verbose output"), + arg_lit0("c", "classic", "Write to MIFARE Classic card instead of DESFire"), + arg_int0("n", "keynum", "", "DESFire: PICC key number [default = 0]"), + arg_str0("t", "algo", "", "DESFire: PICC crypt algo"), + arg_str0("k", "key", "", "DESFire: Key for authentication to the PICC"), + arg_u64_1(NULL, "rc", "", "Region code. 4 bits max"), + arg_u64_1(NULL, "fc", "", "Facility code. 2 bytes max"), + arg_u64_1(NULL, "cn", "", "Card number. 3 bytes max"), + arg_u64_1(NULL, "il", "", "Issue level. 4 bits max"), + arg_str0(NULL, "aid", "", "DESFire: Application ID to write (3 bytes) [default auto]"), + arg_str0(NULL, "sitekey", "", "Site key to compute diversified keys (16 bytes)"), + arg_str0(NULL, "cadkey", "", "DESFire: Custom AES key 0 for CAD (16 bytes)"), + arg_lit0(NULL, "nocadupdate", "DESFire: Don't modify the CAD (only creates the app)"), + arg_lit0(NULL, "noappcreate", "DESFire: Don't create the app (only modifies the CAD)"), + arg_lit0(NULL, "apdu", "Show APDU requests and responses"), + arg_lit0("v", "verbose", "Verbose output"), + arg_int0(NULL, "sector", "", "Classic: Sector number [default = 15]"), + arg_int0(NULL, "cadsector", "", "Classic: CAD sector number [default = 0, skip]"), + arg_lit0(NULL, "nomes", "Classic: Don't include MIFARE Enhanced Security block"), + arg_lit0(NULL, "nomad", "Classic: Don't update the MAD"), arg_param_end }; CLIExecWithReturn(ctx, cmd, argtable, false); uint8_t arg = 1; + bool classic_mode = arg_get_lit(ctx, arg++); + int picc_key_num = arg_get_int_def(ctx, arg++, 0); int picc_key_algo = T_DES; @@ -1023,12 +1385,47 @@ static int CmdGallagherClone(const char *cmd) { SetAPDULogging(arg_get_lit(ctx, arg++)); bool verbose = arg_get_lit(ctx, arg++); + + int cred_sector = arg_get_int_def(ctx, arg++, 15); + int cad_sector = arg_get_int_def(ctx, arg++, 0); + bool no_mes = arg_get_lit(ctx, arg++); + bool no_mad = arg_get_lit(ctx, arg++); + CLIParserFree(ctx); if (gallagher_is_valid_creds(region_code, facility_code, card_number, issue_level) == false) { return PM3_EINVARG; } + // --- MIFARE Classic path --- + if (classic_mode) { + if (cred_sector < 1 || cred_sector > 31) { + PM3_RET_ERR(PM3_EINVARG, "Sector must be between 1 and 31"); + } + if (cad_sector < 0 || cad_sector > 31) { + PM3_RET_ERR(PM3_EINVARG, "CAD sector must be between 0 and 31"); + } + + GallagherCredentials_t creds = { + .region_code = (uint8_t) region_code, + .facility_code = (uint16_t) facility_code, + .card_number = (uint32_t) card_number, + .issue_level = (uint8_t) issue_level, + .mes = !no_mes, + }; + memcpy(creds.site_key, site_key, 16); + + int res = hfgal_write_classic_card(&creds, (uint8_t) cred_sector, (uint8_t) cad_sector, !no_mad, verbose); + if (res != PM3_SUCCESS) { + return res; + } + + PrintAndLogEx(SUCCESS, "Done!"); + PrintAndLogEx(HINT, "Hint: Try `" _YELLOW_("hf gallagher reader -c") "` to verify"); + return PM3_SUCCESS; + } + + // --- DESFire path --- GallagherCredentials_t creds = { .region_code = region_code, .facility_code = facility_code, @@ -1238,7 +1635,7 @@ static int CmdGallagherDiversify(const char *cmd) { // Diversify key uint8_t key[CRYPTO_AES128_KEY_SIZE] = {0}; - int res = hfgal_diversify_key(site_key, uid, uid_len, key_num, aid, key); + int res = hfgal_diversify_desfire_key(site_key, uid, uid_len, key_num, aid, key); PM3_RET_IF_ERR_WITH_MSG(res, "Failed diversifying key"); char *key_str = sprint_hex_inrow(key, ARRAYLEN(key)); @@ -1302,7 +1699,7 @@ static int CmdGallagherDecode(const char *cmd) { ", facility: " _GREEN_("%u") ", card number: " _GREEN_("%u") ", issue level: " _GREEN_("%u"), - 'A' + creds.region_code, + 'A' + creds.region_code, creds.region_code, creds.facility_code, creds.card_number, @@ -1357,15 +1754,35 @@ static int CmdGallagherEncode(const char *cmd) { return PM3_SUCCESS; } +static int CmdGallagherTest(const char *cmd) { + CLIParserContext *ctx; + CLIParserInit(&ctx, "hf gallagher test", + "Test the function of Gallagher Mifare Core\n" + "", + "hf gallagher test" + ); + + void *argtable[] = { + arg_param_begin, + arg_param_end + }; + CLIExecWithReturn(ctx, cmd, argtable, true); + CLIParserFree(ctx); + + + return (GallagherTest(false) ? PM3_SUCCESS : PM3_EFAILED); +} + static command_t CommandTable[] = { {"help", CmdHelp, AlwaysAvailable, "This help"}, - {"reader", CmdGallagherReader, IfPm3Iso14443, "Read & decode all Gallagher credentials on a DESFire card"}, - {"clone", CmdGallagherClone, IfPm3Iso14443, "Add Gallagher credentials to a DESFire card"}, + {"reader", CmdGallagherReader, IfPm3Iso14443, "Read & decode all Gallagher credentials on a DESFire or Classic card"}, + {"clone", CmdGallagherClone, IfPm3Iso14443, "Clone Gallagher credentials to a DESFire or Classic card"}, {"delete", CmdGallagherDelete, IfPm3Iso14443, "Delete Gallagher credentials from a DESFire card"}, {"diversifykey", CmdGallagherDiversify, AlwaysAvailable, "Diversify Gallagher key"}, {"decode", CmdGallagherDecode, AlwaysAvailable, "Decode Gallagher credential block"}, {"encode", CmdGallagherEncode, AlwaysAvailable, "Encode Gallagher credential block"}, + {"test", CmdGallagherTest, AlwaysAvailable, "Test the function of Gallagher Mifare Core"}, {NULL, NULL, NULL, NULL} }; diff --git a/client/src/cmdhfgallagher.h b/client/src/cmdhfgallagher.h index c6e9a8fb3..c6ce04626 100644 --- a/client/src/cmdhfgallagher.h +++ b/client/src/cmdhfgallagher.h @@ -37,8 +37,8 @@ int CmdHFGallagher(const char *cmd); * @param keyOut Buffer to copy the diversified key into (must be 16 bytes). * @return PM3_SUCCESS if successful, PM3_EINVARG if an argument is invalid. */ -int hfgal_diversify_key(uint8_t *site_key, uint8_t *uid, uint8_t uid_len, - uint8_t key_num, uint32_t aid, uint8_t *key_output); +int hfgal_diversify_desfire_key(uint8_t *site_key, uint8_t *uid, uint8_t uid_len, + uint8_t key_num, uint32_t aid, uint8_t *key_output); // The response code when an invalid key is used for authentication // Returned in /client/src/mifare/desfirecore.c, line 1185 (if DesfireExchangeEx fails) diff --git a/client/src/cmdhficlass.c b/client/src/cmdhficlass.c index 472046c09..b30961b65 100644 --- a/client/src/cmdhficlass.c +++ b/client/src/cmdhficlass.c @@ -4621,8 +4621,6 @@ static int iclass_recover(uint8_t key[8], uint32_t index_start, uint32_t loop, u while (repeat == true) { uint32_t payload_size = sizeof(iclass_recover_req_t); - uint8_t aa2_standard_key[PICOPASS_BLOCK_SIZE] = {0}; - memcpy(aa2_standard_key, iClass_Key_Table[1], PICOPASS_BLOCK_SIZE); iclass_recover_req_t *payload = calloc(1, payload_size); payload->req.use_raw = true; payload->req.use_elite = false; diff --git a/client/src/cmdhflist.c b/client/src/cmdhflist.c index 8a8599c8e..0b45fd81b 100644 --- a/client/src/cmdhflist.c +++ b/client/src/cmdhflist.c @@ -2037,6 +2037,12 @@ void annotateFelica(char *exp, size_t size, uint8_t *cmd, uint8_t cmdsize) { case FELICA_WRTSEC_ACK: snprintf(exp, size, "WRITE ACK"); break; + case FELICA_GET_NODE_LIST_REQ: + snprintf(exp, size, "REQUEST CODE LIST"); + break; + case FELICA_GET_NODE_LIST_ACK: + snprintf(exp, size, "REQUEST CODE LIST ACK"); + break; case FELICA_REQSRV2_REQ: snprintf(exp, size, "REQUEST SERVICE v2"); break; diff --git a/client/src/cmdwiegand.c b/client/src/cmdwiegand.c index 125735bf1..191a31d38 100644 --- a/client/src/cmdwiegand.c +++ b/client/src/cmdwiegand.c @@ -27,6 +27,7 @@ #include "protocols.h" #include "parity.h" // oddparity #include "cmdhflist.h" // annotations +#include "commonutil.h" // ARRAYLEN #include "wiegand_formats.h" #include "wiegand_formatutils.h" #include "util.h" @@ -34,45 +35,104 @@ static int CmdHelp(const char *Cmd); #define PACS_EXTRA_LONG_FORMAT 18 // 144 bits -#define PACS_LONG_FORMAT 12 // 96 bits +#define PACS_LONG_FORMAT 13 // 96 bits + 1 byte pad #define PACS_FORMAT 6 // 44 bits -static int wiegand_new_pacs(uint8_t *padded_pacs, uint8_t plen) { +#define PACS_MAX_WIEGAND_BITS 96 +#define WIEGAND_MAX_ENCODED_BITS (PACS_MAX_WIEGAND_BITS + 8) - uint8_t d[PACS_EXTRA_LONG_FORMAT] = {0}; - memcpy(d, padded_pacs, plen); - - uint8_t pad = d[0]; - - char *binstr = (char *)calloc((PACS_EXTRA_LONG_FORMAT * 8) + 1, sizeof(uint8_t)); - if (binstr == NULL) { - PrintAndLogEx(WARNING, "Failed to allocate memory"); - return PM3_EMALLOC; +static void wiegand_packed_to_binstr(const wiegand_message_t *packed, char *binstr) { + for (uint8_t i = 0; i < packed->Length; i++) { + binstr[i] = get_bit_by_position((wiegand_message_t *)packed, i) ? '1' : '0'; } + binstr[packed->Length] = '\0'; +} - uint8_t n = plen - 1; +static int wiegand_print_new_pacs_verbose(const wiegand_message_t *packed, const uint8_t *pacs, size_t pacs_len) { + char binstr[PACS_MAX_WIEGAND_BITS + 1] = {0}; + char rawbin[WIEGAND_MAX_ENCODED_BITS + 1] = {0}; + uint8_t raw[(WIEGAND_MAX_ENCODED_BITS + 7) / 8] = {0}; + size_t raw_len = 0; - bytes_2_binstr(binstr, d + 1, n); + wiegand_packed_to_binstr(packed, binstr); + rawbin[0] = '1'; + memcpy(rawbin + 1, binstr, packed->Length); + binstr_2_bytes(raw, &raw_len, rawbin); + bytes_2_binstr(rawbin, raw, raw_len); - binstr[strlen(binstr) - pad] = '\0'; + PrintAndLogEx(INFO, "----------------------- " _CYAN_("PACS Encoding") " ------------------------"); + PrintAndLogEx(SUCCESS, "New PACS......... " _GREEN_("0x %s"), sprint_hex_inrow(pacs, pacs_len)); + PrintAndLogEx(INFO, "With Sentinel.... " _GREEN_("0b %s") " (%zu-bit)", rawbin, strlen(rawbin)); + PrintAndLogEx(SUCCESS, "Wiegand --raw.... " _YELLOW_("0x %s"), sprint_hex_inrow(raw, raw_len)); + PrintAndLogEx(INFO, "Without Sentinel. " _GREEN_("0b %s") " (%zu-bit)", binstr, strlen(binstr)); + return PM3_SUCCESS; +} - size_t tlen = 0; - uint8_t tmp[16] = {0}; - binstr_2_bytes(tmp, &tlen, binstr); - PrintAndLogEx(SUCCESS, "Wiegand raw.... " _YELLOW_("%s"), sprint_hex_inrow(tmp, tlen)); +static int wiegand_encode_new_pacs(const wiegand_message_t *packed, bool verbose) { - uint32_t top = 0, mid = 0, bot = 0; - if (binstring_to_u96(&top, &mid, &bot, binstr) != strlen(binstr)) { - PrintAndLogEx(ERR, "Binary string contains none <0|1> chars"); - free(binstr); + if (packed->Length == 0) { + PrintAndLogEx(ERR, "Empty Wiegand input"); return PM3_EINVARG; } - PrintAndLogEx(NORMAL, ""); - PrintAndLogEx(INFO, "------------------------- " _CYAN_("SIO - Wiegand") " ---------------------------"); - decode_wiegand(top, mid, bot, strlen(binstr)); - free(binstr); + if (packed->Length > PACS_MAX_WIEGAND_BITS) { + PrintAndLogEx(ERR, "New PACS encoding supports up to %u Wiegand bits", PACS_MAX_WIEGAND_BITS); + return PM3_EINVARG; + } + + uint8_t padded_bits = (uint8_t)(((packed->Length + 7) / 8) * 8); + uint8_t pad = padded_bits - packed->Length; + + char binstr[PACS_MAX_WIEGAND_BITS + 1] = {0}; + wiegand_packed_to_binstr(packed, binstr); + memset(binstr + packed->Length, '0', pad); + binstr[padded_bits] = '\0'; + + size_t pacs_len = 0; + uint8_t pacs[PACS_LONG_FORMAT] = {0}; + binstr_2_bytes(pacs + 1, &pacs_len, binstr); + pacs[0] = pad; + + PrintAndLogEx(SUCCESS, "New PACS......... " _GREEN_("0x %s"), sprint_hex_inrow(pacs, pacs_len + 1)); + if (verbose) { + PrintAndLogEx(NORMAL, ""); + return wiegand_print_new_pacs_verbose(packed, pacs, pacs_len + 1); + } return PM3_SUCCESS; } + +static int wiegand_new_pacs(const uint8_t *padded_pacs, uint8_t plen) { + return HIDDumpPACSBits(padded_pacs, plen, false); +} + +static int wiegand_print_raw_from_bin(const uint8_t *binarr, int blen) { + uint8_t out[(WIEGAND_MAX_ENCODED_BITS + 7) / 8] = {0}; + char binstr[WIEGAND_MAX_ENCODED_BITS + 1] = {0}; + + binstr[0] = '1'; + for (int i = 0; i < blen; i++) { + binstr[i + 1] = binarr[i] ? '1' : '0'; + } + + size_t out_len = 0; + binstr_2_bytes(out, &out_len, binstr); + PrintAndLogEx(SUCCESS, "Wiegand raw.... " _YELLOW_("%s"), sprint_hex_inrow(out, out_len)); + return PM3_SUCCESS; +} + +static int wiegand_encode_new_pacs_from_bin(const uint8_t *binarr, int blen, bool verbose) { + wiegand_message_t packed; + memset(&packed, 0, sizeof(packed)); + packed.Length = blen; + + for (int i = 0; i < blen; i++) { + if (set_bit_by_position(&packed, binarr[i], i) == false) { + PrintAndLogEx(ERR, "Binary string must be less than or equal to %u bits", PACS_MAX_WIEGAND_BITS); + return PM3_EINVARG; + } + } + + return wiegand_encode_new_pacs(&packed, verbose); +} int CmdWiegandList(const char *Cmd) { CLIParserContext *ctx; @@ -98,17 +158,22 @@ int CmdWiegandEncode(const char *Cmd) { CLIParserInit(&ctx, "wiegand encode", "Encode wiegand formatted number to raw hex", "wiegand encode --fc 101 --cn 1337 -> show all formats\n" - "wiegand encode -w H10301 --fc 101 --cn 1337 -> H10301 format " + "wiegand encode -w H10301 --fc 101 --cn 1337 -> H10301 format\n" + "wiegand encode --bin 1 -> raw wiegand hex with sentinel\n" + "wiegand encode -w H10301 --fc 123 --cn 4567 --new -> new ASN.1 encoded format" ); void *argtable[] = { arg_param_begin, + arg_str0("b", "bin", "", "binary string to be encoded"), arg_u64_0(NULL, "fc", "", "facility number"), - arg_u64_1(NULL, "cn", "", "card number"), + arg_u64_0(NULL, "cn", "", "card number"), arg_u64_0(NULL, "issue", "", "issue level"), arg_u64_0(NULL, "oem", "", "OEM code"), arg_str0("w", "wiegand", "", "see `wiegand list` for available formats"), + arg_lit0("n", "new", "encode to new ASN.1 encoded format"), arg_lit0(NULL, "pre", "add HID ProxII preamble to wiegand output"), + arg_lit0("v", "verbose", "verbose output"), arg_param_end }; CLIExecWithReturn(ctx, Cmd, argtable, true); @@ -116,17 +181,44 @@ int CmdWiegandEncode(const char *Cmd) { wiegand_card_t data; memset(&data, 0, sizeof(wiegand_card_t)); - data.FacilityCode = arg_get_u32_def(ctx, 1, 0); - data.CardNumber = arg_get_u64_def(ctx, 2, 0); - data.IssueLevel = arg_get_u32_def(ctx, 3, 0); - data.OEM = arg_get_u32_def(ctx, 4, 0); + uint8_t binarr[PACS_MAX_WIEGAND_BITS] = {0}; + int blen = 0; + int res = CLIParamBinToBuf(arg_get_str(ctx, 1), binarr, ARRAYLEN(binarr), &blen); + + data.FacilityCode = arg_get_u32_def(ctx, 2, 0); + data.CardNumber = arg_get_u64_def(ctx, 3, 0); + data.IssueLevel = arg_get_u32_def(ctx, 4, 0); + data.OEM = arg_get_u32_def(ctx, 5, 0); int len = 0; char format[16] = {0}; - CLIParamStrToBuf(arg_get_str(ctx, 5), (uint8_t *)format, sizeof(format), &len); - bool preamble = arg_get_lit(ctx, 6); + CLIParamStrToBuf(arg_get_str(ctx, 6), (uint8_t *)format, sizeof(format), &len); + + bool new_pacs = arg_get_lit(ctx, 7); + bool preamble = arg_get_lit(ctx, 8); + bool verbose = arg_get_lit(ctx, 9); CLIParserFree(ctx); + if (res) { + PrintAndLogEx(FAILED, "Error parsing binary string"); + return PM3_EINVARG; + } + + if (new_pacs && preamble) { + PrintAndLogEx(ERR, "`--new` and `--pre` can't be combined"); + return PM3_EINVARG; + } + + if (blen && (len || data.FacilityCode || data.CardNumber || data.IssueLevel || data.OEM || preamble)) { + PrintAndLogEx(ERR, "`--bin` can't be combined with format, field, or preamble options"); + return PM3_EINVARG; + } + + if (blen == 0 && len == 0 && data.CardNumber == 0 && data.FacilityCode == 0 && data.IssueLevel == 0 && data.OEM == 0) { + PrintAndLogEx(ERR, "Must provide either card data, a specific format, or `--bin`"); + return PM3_EINVARG; + } + int idx = -1; if (len) { idx = HIDFindCardFormat(format); @@ -136,13 +228,26 @@ int CmdWiegandEncode(const char *Cmd) { } } - if (idx != -1) { + if (new_pacs && idx == -1 && blen == 0) { + PrintAndLogEx(ERR, "`--new` requires either `--bin` or a specific wiegand format"); + return PM3_EINVARG; + } + + if (blen) { + if (new_pacs) { + return wiegand_encode_new_pacs_from_bin(binarr, blen, verbose); + } + return wiegand_print_raw_from_bin(binarr, blen); + } else if (idx != -1) { wiegand_message_t packed; memset(&packed, 0, sizeof(wiegand_message_t)); if (HIDPack(idx, &data, &packed, preamble) == false) { PrintAndLogEx(WARNING, "The card data could not be encoded in the selected format."); return PM3_ESOFT; } + if (new_pacs) { + return wiegand_encode_new_pacs(&packed, verbose); + } print_wiegand_code(&packed); } else { // try all formats and print only the ones that work. @@ -157,14 +262,14 @@ int CmdWiegandDecode(const char *Cmd) { CLIParserInit(&ctx, "wiegand decode", "Decode raw hex or binary to wiegand format", "wiegand decode --raw 2006F623AE\n" - "wiegand decode --new 06BD88EB80 -> 4..8 bytes, new padded format " + "wiegand decode --new 06BD88EB80 -> 4..13 bytes, new ASN.1 encoded format " ); void *argtable[] = { arg_param_begin, arg_str0("r", "raw", "", "raw hex to be decoded"), arg_str0("b", "bin", "", "binary string to be decoded"), - arg_str0("n", "new", "", "new padded pacs as raw hex to be decoded"), + arg_str0("n", "new", "", "new ASN.1 encoded data as raw hex to be decoded"), arg_lit0("f", "force", "skip preabmle checking, brute force all possible lengths for raw hex input"), arg_param_end }; @@ -174,11 +279,11 @@ int CmdWiegandDecode(const char *Cmd) { CLIParamStrToBuf(arg_get_str(ctx, 1), (uint8_t *)hex, sizeof(hex), &hlen); int blen = 0; - uint8_t binarr[100] = {0x00}; + uint8_t binarr[WIEGAND_MAX_ENCODED_BITS] = {0x00}; int res = CLIParamBinToBuf(arg_get_str(ctx, 2), binarr, sizeof(binarr), &blen); int plen = 0; - uint8_t phex[8] = {0}; + uint8_t phex[PACS_LONG_FORMAT] = {0}; res = CLIParamHexToBuf(arg_get_str(ctx, 3), phex, sizeof(phex), &plen); bool no_preamble = arg_get_lit(ctx, 4); @@ -193,6 +298,10 @@ int CmdWiegandDecode(const char *Cmd) { uint32_t top = 0, mid = 0, bot = 0; if (hlen) { + if ((hlen * 4) > PACS_MAX_WIEGAND_BITS) { + PrintAndLogEx(ERR, "Raw hex decode supports up to %u Wiegand bits", PACS_MAX_WIEGAND_BITS); + return PM3_EINVARG; + } res = hexstring_to_u96(&top, &mid, &bot, hex); if (res != hlen) { PrintAndLogEx(ERR, "Hex string contains none hex chars"); @@ -200,11 +309,15 @@ int CmdWiegandDecode(const char *Cmd) { } if (no_preamble) { - // pass hex input length as is and brute force all possible lengths + // Pass the input hex length through so decode_wiegand() brute-forces + // the possible bit lengths instead of assuming a preamble-encoded value. blen = -hlen; } - } else if (blen) { + if (blen > PACS_MAX_WIEGAND_BITS) { + PrintAndLogEx(ERR, "Binary decode supports up to %u Wiegand bits", PACS_MAX_WIEGAND_BITS); + return PM3_EINVARG; + } int n = binarray_to_u96(&top, &mid, &bot, binarr, blen); if (n != blen) { PrintAndLogEx(ERR, "Binary string contains none <0|1> chars"); diff --git a/client/src/mifare/gallaghercore.c b/client/src/mifare/gallaghercore.c index 80a902bab..7f97525be 100644 --- a/client/src/mifare/gallaghercore.c +++ b/client/src/mifare/gallaghercore.c @@ -16,8 +16,10 @@ // Common functionality for low/high-frequency GALLAGHER tag encoding & decoding. //----------------------------------------------------------------------------- #include "gallaghercore.h" +#include "aes.h" #include "common.h" #include "ui.h" +#include static void scramble(uint8_t *arr, uint8_t len) { const uint8_t lut[] = { @@ -69,6 +71,51 @@ static void descramble(uint8_t *arr, uint8_t len) { } } +int gallagher_diversify_classic_key(uint8_t *site_key, uint8_t *csn, size_t csn_len, uint8_t *key_output) { + memcpy(key_output, site_key, 16); + for (int i = 0; i < csn_len; i++) { + key_output[i] ^= csn[i]; + } + return PM3_SUCCESS; +} + +int gallagher_construct_credential(GallagherCredentials_t *creds, uint8_t region, uint16_t facility, uint32_t card, uint8_t issue, bool mes, uint8_t *csn, size_t csn_len, uint8_t *site_key) { + creds->region_code = region; + creds->facility_code = facility; + creds->card_number = card; + creds->issue_level = issue; + creds->mes = mes; + memcpy(creds->csn, csn, csn_len); + memcpy(creds->site_key, site_key, 16); + return PM3_SUCCESS; +} + +int gallagher_parse_cad(uint8_t *cad, uint8_t region, uint16_t facility) { + // CAD entries are 3.5 bytes each (28 bits: 4-bit RC, 16-bit FC, 8-bit sector). + // Packed in pairs of 7 bytes starting at byte 4, up to 6 pairs (12 entries). + for (int pair = 0; pair < 6; pair++) { + int base = 4 + pair * 7; + + // Even entry (first 3.5 bytes of pair) + uint8_t rc = (cad[base] >> 4) & 0x0F; + uint16_t fc = ((cad[base] & 0x0F) << 12) | (cad[base + 1] << 4) | ((cad[base + 2] >> 4) & 0x0F); + uint8_t sector = ((cad[base + 2] & 0x0F) << 4) | ((cad[base + 3] >> 4) & 0x0F); + if (rc == region && fc == facility) { + return sector; + } + + // Odd entry (last 3.5 bytes of pair) + rc = cad[base + 3] & 0x0F; + fc = (cad[base + 4] << 8) | cad[base + 5]; + sector = cad[base + 6]; + if (rc == region && fc == facility) { + return sector; + } + } + return -1; +} + + void gallagher_decode_creds(uint8_t *eight_bytes, GallagherCredentials_t *creds) { uint8_t *arr = eight_bytes; @@ -107,6 +154,122 @@ void gallagher_encode_creds(uint8_t *eight_bytes, GallagherCredentials_t *creds) scramble(eight_bytes, 8); } +int gallagher_encode_mes(uint8_t *sixteen_bytes, GallagherCredentials_t *creds) { + + if (creds->csn_len > 4) { + PrintAndLogEx(ERR, "Credential could not be encoded into a Mifare Enhanced Encryption block. only 4 byte UUID's are supported"); + return PM3_ENOTIMPL; + } + + // unknown parameters from the research these might be for UUID's longer than 4 bytes? + uint8_t UB = 0x00; + uint8_t UC = 0x00; + uint8_t UD = 0x00; + uint8_t UE = 0x00; + uint8_t PO = 0x00; // Pin offset + uint8_t UX = 0x00; + uint16_t R = 0x0748; + + uint8_t mes[16]; + uint8_t diversified_site_key[16]; + + mes[0] = 0x01; + mes[1] = (creds->card_number & 0xFF0000) >> 16; + mes[2] = (creds->card_number & 0x00FF00) >> 8; + mes[3] = creds->card_number & 0x0000FF; + mes[4] = (creds->facility_code & 0xFF00) >> 8; + mes[5] = creds->facility_code & 0x00FF; + mes[6] = ((creds->region_code & 0x0F) << 4) | (creds->issue_level & 0x0F); + mes[7] = (PO & 0x0F) | ((UX & 0x0F) << 4); + mes[8] = (UB & 0x0F) | ((UC & 0x0F) << 4); + mes[9] = (UD & 0x0F) | ((UE & 0x0F) << 4); + mes[10] = creds->csn[0]; + mes[11] = creds->csn[1]; + mes[12] = creds->csn[2]; + mes[13] = creds->csn[3]; + mes[14] = (R & 0xFF00) >> 8; + mes[15] = R & 0x00FF; + + PrintAndLogEx(DEBUG, "MES before encryption %s", sprint_hex_ascii(mes, 16)); + + gallagher_diversify_classic_key(creds->site_key, creds->csn, creds->csn_len, diversified_site_key); + + mbedtls_aes_context actx; + mbedtls_aes_init(&actx); + if (mbedtls_aes_setkey_enc(&actx, diversified_site_key, 128) != 0) { + return PM3_ENOKEY; + } + + if (mbedtls_aes_crypt_ecb(&actx, MBEDTLS_AES_ENCRYPT, mes, sixteen_bytes) != 0) { + return PM3_ENOKEY; + } + mbedtls_aes_free(&actx); + + PrintAndLogEx(DEBUG, "MES after encryption %s", sprint_hex_ascii(sixteen_bytes, 16)); + return PM3_SUCCESS; +} + +int gallagher_decode_mes(uint8_t *block, GallagherCredentials_t *creds) { + // unknown parameters from the research these might be for UUID's longer than 4 bytes? + // uint8_t UB = 0x00; + // uint8_t UC = 0x00; + // uint8_t UD = 0x00; + // uint8_t UE = 0x00; + // uint8_t PO = 0x00; + // uint8_t UX = 0x00; + uint16_t R = 0x0748; + uint8_t mes[16]; + + uint8_t diversified_site_key[16]; + gallagher_diversify_classic_key(creds->site_key, creds->csn, creds->csn_len, diversified_site_key); + if (creds->csn_len > 4) { + PrintAndLogEx(WARNING, "UUID length is > 4, this may not be a valid gallagher credential?"); + } + + // AES decrypt 16 bytes + mbedtls_aes_context actx; + mbedtls_aes_init(&actx); + if (mbedtls_aes_setkey_dec(&actx, diversified_site_key, 128) != 0) { + return PM3_ENOKEY; + } + if (mbedtls_aes_crypt_ecb(&actx, MBEDTLS_AES_DECRYPT, block, mes) != 0) { + return PM3_ENOKEY; + } + mbedtls_aes_free(&actx); + + PrintAndLogEx(DEBUG, "MES after decryption %s", sprint_hex_ascii(mes, 16)); + + if (mes[0] != 0x01) { + PrintAndLogEx(ERR, "MES block is not valid"); + return PM3_EWRONGANSWER; + } + creds->card_number = mes[1] << 16 | mes[2] << 8 | mes[3]; + creds->facility_code = mes[4] << 8 | mes[5]; + creds->region_code = (mes[6] & 0xF0) >> 4; + creds->issue_level = mes[6] & 0x0F; + // PO = mes[7] & 0x0F; + // UX = (mes[7] & 0xF0) >> 4; + // UB = mes[8] & 0x0F; + // UC = (mes[8] & 0xF0) >> 4; + // UD = mes[9] & 0x0F; + // UE = (mes[9] & 0xF0) >> 4; + // csn is already verified by key diversification + // csn[0] = mes[10]; + // csn[1] = mes[11]; + // csn[2] = mes[12]; + // csn[3] = mes[13]; + R = mes[14] << 8 | mes[15]; + if (R != 0x0748) { + PrintAndLogEx(WARNING, "R value is different from 0x0748, this hasn't been seen in the wild \n https://github.com/megabug/gallagher-research/blob/master/formats/mes.md"); + } + + return PM3_SUCCESS; +} + +bool gallagher_is_valid_creds_struct(GallagherCredentials_t *creds) { + return gallagher_is_valid_creds(creds->region_code, creds->facility_code, creds->card_number, creds->issue_level); +} + bool gallagher_is_valid_creds(uint64_t region_code, uint64_t facility_code, uint64_t card_number, uint64_t issue_level) { bool is_valid = true; @@ -129,3 +292,21 @@ bool gallagher_is_valid_creds(uint64_t region_code, uint64_t facility_code, uint } return is_valid; } + +void print_gallagher_creds(GallagherCredentials_t *creds) { + + if (!gallagher_is_valid_creds_struct(creds)) { + PrintAndLogEx(ERR, "Invalid Gallagher credential"); + return; + } + PrintAndLogEx(SUCCESS, "Gallagher - region: " _GREEN_("%c") " ( " _GREEN_("%u") " )" + ", facility: " _GREEN_("%u") + ", card number: " _GREEN_("%u") + ", issue level: " _GREEN_("%u"), + 'A' + creds->region_code, + creds->region_code, + creds->facility_code, + creds->card_number, + creds->issue_level + ); +} diff --git a/client/src/mifare/gallaghercore.h b/client/src/mifare/gallaghercore.h index 378ea7423..5f6d8fccc 100644 --- a/client/src/mifare/gallaghercore.h +++ b/client/src/mifare/gallaghercore.h @@ -17,6 +17,7 @@ #define MIFARE_GALLAGHERCORE_H__ #include "common.h" +#include "crypto/libpcrypto.h" #include typedef struct { @@ -24,12 +25,30 @@ typedef struct { uint16_t facility_code; uint32_t card_number; uint8_t issue_level; + bool mes; + uint8_t csn[10]; + size_t csn_len; + uint8_t site_key[16]; } GallagherCredentials_t; +int gallagher_diversify_classic_key(uint8_t *site_key, uint8_t *csn, size_t csn_len, uint8_t *key_output); + +int gallagher_parse_cad(uint8_t *cad, uint8_t region, uint16_t facility); + void gallagher_encode_creds(uint8_t *eight_bytes, GallagherCredentials_t *creds); void gallagher_decode_creds(uint8_t *eight_bytes, GallagherCredentials_t *creds); +int gallagher_construct_credential(GallagherCredentials_t *creds, uint8_t region, uint16_t facility, uint32_t card, uint8_t issue, bool mes, uint8_t *csn, size_t csn_len, uint8_t *site_key); + +int gallagher_encode_mes(uint8_t *sector, GallagherCredentials_t *creds); + +int gallagher_decode_mes(uint8_t *sector, GallagherCredentials_t *creds); + bool gallagher_is_valid_creds(uint64_t region_code, uint64_t facility_code, uint64_t card_number, uint64_t issue_level); +bool gallagher_is_valid_creds_struct(GallagherCredentials_t *creds); + +void print_gallagher_creds(GallagherCredentials_t *creds); + #endif diff --git a/client/src/mifare/gallaghertest.c b/client/src/mifare/gallaghertest.c new file mode 100644 index 000000000..27508399b --- /dev/null +++ b/client/src/mifare/gallaghertest.c @@ -0,0 +1,196 @@ +#include "gallaghertest.h" + +#include +#include // memcpy memset +#include "ui.h" +#include "crc.h" + +#include "mifare/gallaghercore.h" + +static bool creds_match(GallagherCredentials_t *a, GallagherCredentials_t *b) { + return a->region_code == b->region_code && + a->facility_code == b->facility_code && + a->card_number == b->card_number && + a->issue_level == b->issue_level; +} + +static bool test_CAD(void) { + // Example CAD sector from https://github.com/megabug/gallagher-research/blob/master/formats/card-specific/mifare-classic.md + uint8_t cad[] = {0x1B, 0x58, 0x00, 0x01, 0xC1, 0x33, 0x70, 0xFD, 0x13, 0x38, 0x0D, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x78, 0x77, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + }; + + // Entry 0: RC=0xC, FC=0x1337, Sector=0x0F + int result = gallagher_parse_cad(cad, 0xC, 0x1337); + if (result != 0x0F) { + PrintAndLogEx(INFO, "Gallagher CAD test 1 failed: expected sector 0x0F, got 0x%02X", result); + return false; + } + + // Entry 1: RC=0xD, FC=0x1338, Sector=0x0D + result = gallagher_parse_cad(cad, 0xD, 0x1338); + if (result != 0x0D) { + PrintAndLogEx(INFO, "Gallagher CAD test 2 failed: expected sector 0x0D, got 0x%02X", result); + return false; + } + + // Non-existent entry should return -1 + result = gallagher_parse_cad(cad, 0xA, 0x1234); + if (result != -1) { + PrintAndLogEx(INFO, "Gallagher CAD test 3 failed: expected -1, got %d", result); + return false; + } + + return true; +} + +static bool test_creds(void) { + GallagherCredentials_t creds1 = { + .region_code = 0x0, + .facility_code = 0x0, + .card_number = 0x0, + .issue_level = 0x0, + }; + + GallagherCredentials_t creds2 = { + .region_code = 0x1, + .facility_code = 0x2, + .card_number = 0x20, + .issue_level = 0x1, + }; + + GallagherCredentials_t cred_result = {0}; + uint8_t bytes_result[8] = {0}; + + gallagher_encode_creds(bytes_result, &creds1); + gallagher_decode_creds(bytes_result, &cred_result); + if (!creds_match(&cred_result, &creds1)) { + PrintAndLogEx(INFO, "Gallagher encode/decode roundtrip test 1 failed"); + return false; + } + + gallagher_encode_creds(bytes_result, &creds2); + gallagher_decode_creds(bytes_result, &cred_result); + if (!creds_match(&cred_result, &creds2)) { + PrintAndLogEx(INFO, "Gallagher encode/decode roundtrip test 2 failed"); + return false; + } + + return true; +} + +// Test decode/encode against known real-world data from the documentation +static bool test_known_vector_creds(void) { + // From doc: 0xA3B4B0C151B0A31B decodes to RC=12, FC=4919(0x1337), CN=61453(0xF00D), IL=1 + uint8_t known_bytes[] = {0xA3, 0xB4, 0xB0, 0xC1, 0x51, 0xB0, 0xA3, 0x1B}; + GallagherCredentials_t expected = { + .region_code = 12, + .facility_code = 4919, + .card_number = 61453, + .issue_level = 1, + }; + + // Test decode + GallagherCredentials_t result = {0}; + gallagher_decode_creds(known_bytes, &result); + if (!creds_match(&expected, &result)) { + PrintAndLogEx(INFO, "Known vector decode failed: RC=%d FC=%d CN=%d IL=%d", + result.region_code, result.facility_code, result.card_number, result.issue_level); + return false; + } + + // Test encode roundtrip + uint8_t encoded[8] = {0}; + gallagher_encode_creds(encoded, &expected); + if (memcmp(encoded, known_bytes, 8) != 0) { + PrintAndLogEx(INFO, "Known vector encode failed"); + return false; + } + + // Verify bitwise inverse (block 0 format: 8-byte creds + 8-byte inverse) + uint8_t known_block0[] = {0xA3, 0xB4, 0xB0, 0xC1, 0x51, 0xB0, 0xA3, 0x1B, + 0x5C, 0x4B, 0x4F, 0x3E, 0xAE, 0x4F, 0x5C, 0xE4 + }; + for (int i = 0; i < 8; i++) { + if ((uint8_t)(known_block0[i] ^ 0xFF) != known_block0[i + 8]) { + PrintAndLogEx(INFO, "Bitwise inverse check failed at byte %d", i); + return false; + } + } + + return true; +} + +// Test MAD CRC against known sector 0 data from the documentation +static bool test_mad_crc(void) { + // Full sector 0 from documentation (blocks 0-2, excluding trailer) + uint8_t sector0[64] = { + // Block 0 (manufacturer) + 0xE3, 0x51, 0x54, 0x3C, 0xDA, 0x08, 0x04, 0x00, 0x01, 0x6F, 0x01, 0x6D, 0x45, 0x68, 0xF8, 0x1D, + // Block 1 (MAD: CRC, info byte, AIDs 1-7) + 0xBD, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // Block 2 (MAD: AIDs 8-15) + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x12, 0x48, 0x11, 0x48, 0x12, 0x48, + // Block 3 (sector trailer - not part of CRC) + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x78, 0x77, 0x88, 0xC1, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + }; + + // MAD v1 CRC: computed over sector0[17..47] (info byte + 15 AID pairs = 31 bytes) + uint8_t expected_crc = sector0[16]; // 0xBD + uint8_t computed_crc = CRC8Mad(§or0[16 + 1], 15 + 16); + if (computed_crc != expected_crc) { + PrintAndLogEx(INFO, "MAD CRC test failed: expected 0x%02X, got 0x%02X", expected_crc, computed_crc); + return false; + } + + return true; +} + +static bool test_MES(void) { + + uint8_t csn[] = {0x3C, 0x54, 0x51, 0xE3}; + uint8_t csn_len = 4; + uint8_t site_key[] = {0x13, 0x37, 0xD0, 0x0D, 0x13, 0x37, 0xD0, 0x0D, 0x13, 0x37, 0xD0, 0x0D, 0x13, 0x37, 0xD0, 0x0D}; + + GallagherCredentials_t known_cred; + gallagher_construct_credential(&known_cred, 12, 0x1337, 0xF00D, 1, true, csn, csn_len, site_key); + + GallagherCredentials_t result_creds = {0}; + gallagher_construct_credential(&result_creds, 0, 0, 0, 0, true, csn, csn_len, site_key); + + uint8_t sector_result[16] = {0}; + uint8_t known_sector[16] = {0x4F, 0x36, 0xB7, 0x4E, 0xFF, 0xCD, 0x76, 0xEF, 0xED, 0xA5, 0x74, 0x58, 0xC8, 0xB4, 0xE3, 0x04}; + + // Test encode + gallagher_encode_mes(sector_result, &known_cred); + if (memcmp(sector_result, known_sector, 16) != 0) { + PrintAndLogEx(INFO, "Gallagher MES encode test failed"); + PrintAndLogEx(INFO, "Expected: %s", sprint_hex_ascii(known_sector, 16)); + PrintAndLogEx(INFO, "Got: %s", sprint_hex_ascii(sector_result, 16)); + return false; + } + + // Test decode + if (gallagher_decode_mes(known_sector, &result_creds) != PM3_SUCCESS) { + PrintAndLogEx(INFO, "Gallagher MES decode test failed"); + return false; + } + if (!creds_match(&known_cred, &result_creds)) { + PrintAndLogEx(INFO, "Gallagher MES decoded different creds than expected"); + return false; + } + + return true; +} + +bool GallagherTest(bool verbose) { + bool result = true; + result &= test_CAD(); + result &= test_creds(); + result &= test_known_vector_creds(); + result &= test_mad_crc(); + result &= test_MES(); + return result; +} diff --git a/client/src/mifare/gallaghertest.h b/client/src/mifare/gallaghertest.h new file mode 100644 index 000000000..26b688be9 --- /dev/null +++ b/client/src/mifare/gallaghertest.h @@ -0,0 +1,27 @@ +//----------------------------------------------------------------------------- +// 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. +//----------------------------------------------------------------------------- +// tests for desfire +//----------------------------------------------------------------------------- + +#ifndef __GALLAGHERTEST_H__ +#define __GALLAGHERTEST_H__ + +#include +#include "common.h" + +bool GallagherTest(bool verbose); + +#endif /* __GALLAGHERTEST_H__ */ diff --git a/client/src/wiegand_formats.c b/client/src/wiegand_formats.c index 7cecb4825..88f52aaa1 100644 --- a/client/src/wiegand_formats.c +++ b/client/src/wiegand_formats.c @@ -1796,6 +1796,14 @@ bool decode_wiegand(uint32_t top, uint32_t mid, uint32_t bot, int n) { } int HIDDumpPACSBits(const uint8_t *const data, const uint8_t length, bool verbose) { + // PACS is encoded as a 1-byte pad count followed by at least 1 byte of payload. + // Reject malformed inputs here so trimming the pad bits below cannot index before + // the start of the binary buffer. + if (length < 2 || data[0] > 0x07) { + PrintAndLogEx(ERR, "Invalid PACS value"); + return PM3_EINVARG; + } + uint8_t n = length - 1; uint8_t pad = data[0]; char *binstr = (char *)calloc((length * 8) + 1, sizeof(uint8_t)); @@ -1815,13 +1823,19 @@ int HIDDumpPACSBits(const uint8_t *const data, const uint8_t length, bool verbos PrintAndLogEx(DEBUG, "bin.............. " _GREEN_("%s") " ( %zu )", binstr, strlen(binstr)); size_t hexlen = 0; - uint8_t hex[16] = {0}; + uint8_t *hex = calloc(n ? n : 1, sizeof(uint8_t)); + if (hex == NULL) { + PrintAndLogEx(WARNING, "Failed to allocate memory"); + free(binstr); + return PM3_EMALLOC; + } binstr_2_bytes(hex, &hexlen, binstr); PrintAndLogEx(SUCCESS, "hex.............. " _GREEN_("%s"), sprint_hex_inrow(hex, hexlen)); uint32_t top = 0, mid = 0, bot = 0; if (binstring_to_u96(&top, &mid, &bot, binstr) != strlen(binstr)) { PrintAndLogEx(ERR, "Binary string contains none <0|1> chars"); + free(hex); free(binstr); return PM3_EINVARG; } @@ -1850,25 +1864,36 @@ int HIDDumpPACSBits(const uint8_t *const data, const uint8_t length, bool verbos // MIFARE DESFire // MIFARE Classic - char mfcbin[28] = {0}; - mfcbin[0] = '1'; - memcpy(mfcbin + 1, binstr, strlen(binstr)); - binstr_2_bytes(hex, &hexlen, mfcbin); + size_t binstrlen = strlen(binstr); + // Match hf mf encodehid: prepend the sentinel bit, pack to bytes, then right-align + // the result in the 15-byte payload area after the leading 0x02 marker in block 5. + // Fixed-size local buffers are enough for the current 96-bit Wiegand limit: + // 96 data bits + 1 sentinel + NUL = 98 chars, packed into at most 13 bytes. + if (binstrlen <= 96) { + char mfcbin[98] = {0}; + uint8_t mfcpayload[15] = {0}; + size_t mfchexlen = 0; - PrintAndLogEx(INFO, "Downgrade to " _YELLOW_("MIFARE Classic") " (Pm3 simulation)"); - PrintAndLogEx(SUCCESS, " hf mf eclr;"); - PrintAndLogEx(SUCCESS, " hf mf esetblk --blk 0 -d 049DBA42A23E80884400C82000000000;"); - PrintAndLogEx(SUCCESS, " hf mf esetblk --blk 1 -d 1B014D48000000000000000000000000;"); - PrintAndLogEx(SUCCESS, " hf mf esetblk --blk 3 -d A0A1A2A3A4A5787788C189ECA97F8C2A;"); - PrintAndLogEx(SUCCESS, " hf mf esetblk --blk 5 -d 020000000000000000000000%s;", sprint_hex_inrow(hex, hexlen)); - PrintAndLogEx(SUCCESS, " hf mf esetblk --blk 7 -d 484944204953787788AA204752454154;"); - PrintAndLogEx(SUCCESS, " hf mf sim --1k -i;"); - PrintAndLogEx(NORMAL, ""); + mfcbin[0] = '1'; + memcpy(mfcbin + 1, binstr, binstrlen); + binstr_2_bytes(mfcpayload + (sizeof(mfcpayload) - ((binstrlen + 1 + 7) / 8)), &mfchexlen, mfcbin); + + PrintAndLogEx(INFO, "Downgrade to " _YELLOW_("MIFARE Classic") " (Pm3 simulation)"); + PrintAndLogEx(SUCCESS, " hf mf eclr;"); + PrintAndLogEx(SUCCESS, " hf mf esetblk --blk 0 -d 049DBA42A23E80884400C82000000000;"); + PrintAndLogEx(SUCCESS, " hf mf esetblk --blk 1 -d 1B014D48000000000000000000000000;"); + PrintAndLogEx(SUCCESS, " hf mf esetblk --blk 3 -d A0A1A2A3A4A5787788C189ECA97F8C2A;"); + PrintAndLogEx(SUCCESS, " hf mf esetblk --blk 5 -d 02%s;", sprint_hex_inrow(mfcpayload, sizeof(mfcpayload))); + PrintAndLogEx(SUCCESS, " hf mf esetblk --blk 7 -d 484944204953787788AA204752454154;"); + PrintAndLogEx(SUCCESS, " hf mf sim --1k -i;"); + PrintAndLogEx(NORMAL, ""); + } PrintAndLogEx(INFO, "Downgrade to " _YELLOW_("MIFARE Classic 1K")); PrintAndLogEx(SUCCESS, " hf mf encodehid --bin %s", binstr); PrintAndLogEx(NORMAL, ""); } + free(hex); free(binstr); return PM3_SUCCESS; } diff --git a/common/generator.c b/common/generator.c index 62512c103..ce39a1824 100644 --- a/common/generator.c +++ b/common/generator.c @@ -620,6 +620,42 @@ int mfc_algo_snapmaker_all(uint8_t *uid, uint8_t *keys) { return PM3_SUCCESS; } +// Vanderbilt ACT pattern-based key generation +// Generates keys by appending block ID to "Acces" (0x4163636573) +// Pattern: 416363657300, 416363657301, ..., 4163636573FF +int mfc_algo_vanderbilt_one(uint8_t *uid, uint8_t sector, uint8_t keytype, uint64_t *key) { + if (key == NULL) return PM3_EINVARG; + if (sector > 39) return PM3_EINVARG; + + // Base pattern: "Acces" in ASCII = 0x4163636573 + // For each sector, we generate keys for all 4 blocks (or 16 for sector 32+) + // Key format: 41 63 63 65 73 XX where XX is the block number + + uint8_t first_block = (sector < 32) ? (sector * 4) : (128 + (sector - 32) * 16); + uint8_t block_id = first_block + 3; // Use sector trailer block ID + + // Both key A and B use the same pattern with block ID + uint8_t key_bytes[6] = {0x41, 0x63, 0x63, 0x65, 0x73, block_id}; + *key = bytes_to_num(key_bytes, 6); + + return PM3_SUCCESS; +} + +int mfc_algo_vanderbilt_all(uint8_t *uid, uint8_t *keys) { + if (keys == NULL) return PM3_EINVARG; + + // Generate keys for all sectors (40 sectors for 4K card) + for (int keytype = 0; keytype < 2; keytype++) { + for (int sector = 0; sector < 40; sector++) { + uint64_t key = 0; + mfc_algo_vanderbilt_one(uid, sector, keytype, &key); + num_to_bytes(key, 6, keys + (keytype * 40 * 6) + (sector * 6)); + } + } + + return PM3_SUCCESS; +} + static kdf_t KDFTable[] = { {"Saflok / Maid", 16, mfc_algo_saflok_all, 4}, {"MIZIP", 5, mfc_algo_mizip_all, 4}, @@ -627,6 +663,7 @@ static kdf_t KDFTable[] = { {"Skylanders", 16, mfc_algo_sky_all, 4}, {"Bambu Lab Filament Spool", 16, mfc_algo_bambu_all, 4}, {"Snapmaker Filament Spool", 16, mfc_algo_snapmaker_all, 4}, + {"Vanderbilt ACT", 40, mfc_algo_vanderbilt_all, 0}, // {"Vinglock", 16, mfc_algo_ving_all, 4}, // not implemented // {"Yale Doorman", 16, mfc_algo_yale_all, 4}, // not implemented }; diff --git a/common/generator.h b/common/generator.h index d0dc0a8e9..0cd10b8ce 100644 --- a/common/generator.h +++ b/common/generator.h @@ -79,6 +79,9 @@ int mfc_algo_bambu_all(uint8_t *uid, uint8_t *keys); int mfc_algo_snapmaker_one(uint8_t *uid, uint8_t sector, uint8_t keytype, uint64_t *key); int mfc_algo_snapmaker_all(uint8_t *uid, uint8_t *keys); +int mfc_algo_vanderbilt_one(uint8_t *uid, uint8_t sector, uint8_t keytype, uint64_t *key); +int mfc_algo_vanderbilt_all(uint8_t *uid, uint8_t *keys); + uint32_t lf_t55xx_white_pwdgen(uint32_t id); int mfdes_kdf_input_gallagher(uint8_t *uid, uint8_t uidLen, uint8_t keyNo, uint32_t aid, uint8_t *kdfInputOut, uint8_t *kdfInputLen); diff --git a/common_arm/Makefile.hal b/common_arm/Makefile.hal index 429fc1717..2e10ccd3d 100644 --- a/common_arm/Makefile.hal +++ b/common_arm/Makefile.hal @@ -306,7 +306,7 @@ ifneq ($(PLATFORM), $(CACHED_PLATFORM)) PLATFORM_CHANGED=true else ifneq ($(PLATFORM_EXTRAS), $(CACHED_PLATFORM_EXTRAS)) PLATFORM_CHANGED=true -else ifneq ($(PLATFORM_DEFS), $(CACHED_PLATFORM_DEFS)) +else ifneq ($(strip $(PLATFORM_DEFS)), $(strip $(CACHED_PLATFORM_DEFS))) PLATFORM_CHANGED=true endif diff --git a/doc/md/Use_of_Proxmark/4_Advanced-compilation-parameters.md b/doc/md/Use_of_Proxmark/4_Advanced-compilation-parameters.md index 749d441df..8dc3d445d 100644 --- a/doc/md/Use_of_Proxmark/4_Advanced-compilation-parameters.md +++ b/doc/md/Use_of_Proxmark/4_Advanced-compilation-parameters.md @@ -111,7 +111,7 @@ You can also define multiple options like ^[Top](#top) The Iceman repository gives you to easily choose which standalone mode to embed in the firmware. - +Documentation for each standalone mode can be found in the [Standalone Modes Index](../../armsrc/Standalone/readme.md#individual-mode-documentation). Here are the supported values you can assign to `STANDALONE` in `Makefile.platform`: | STANDALONE | DESCRIPTION | diff --git a/doc/standalone/dankarmulti.md b/doc/standalone/dankarmulti.md new file mode 100644 index 000000000..b05710821 --- /dev/null +++ b/doc/standalone/dankarmulti.md @@ -0,0 +1,106 @@ +# DANKARMULTI — Multi-Mode Standalone Loader + +> **Author:** Daniel Karling (dankarmulti) +> **Frequency:** Multi (LF + HF) +> **Hardware:** Generic Proxmark3 + +[Back to Standalone Modes Index](../../armsrc/Standalone/readme.md#individual-mode-documentation) | [Source Code](../../armsrc/Standalone/dankarmulti.c) | [Development Guide](../../armsrc/Standalone/readme.md#developing-standalone-modes) + +--- + +## What + +A meta-standalone mode that bundles **multiple** standalone modes into a single firmware image and lets you select which one to run at boot time using the button. + +## Why + +Normally the Proxmark3 can only have one standalone mode compiled in. If you want to switch modes, you must reflash the firmware. DANKARMULTI solves this by wrapping multiple standalone modes into one firmware — you cycle through them with button presses and hold to execute your chosen mode. This is ideal for field work where you need multiple capabilities without a laptop. + +## How + +1. **Boot**: On entering standalone mode, LEDs indicate the currently selected sub-mode. +2. **Cycle**: Press the button to cycle through available sub-modes. LEDs change to indicate the new selection. +3. **Execute**: Hold the button to launch the selected sub-mode. Once launched, that sub-mode takes full control (LEDs, button, etc.). +4. **Exit**: Exiting the sub-mode returns to the DANKARMULTI selector. + +### Default Bundled Modes + +By default, DANKARMULTI includes: + +| Slot | Mode | Description | +|------|------|-------------| +| 1 | [HF_MATTYRUN](hf_mattyrun.md) | MIFARE Classic key check → nested → dump → emulate | +| 2 | [LF_EM4100RSWB](lf_em4100rswb.md) | EM4100 read/sim/write/brute | +| 3 | [HF_TCPRST](hf_tcprst.md) | IKEA Rothult / ST25TA password extractor | + +> Modes can be customized by editing the `dankarmulti.c` source — add or remove `#include`s and update the mode array. + +## LED Indicators + +| LED | Meaning (Selector) | +|-----|---------------------| +| **A** only | Mode 1 selected | +| **B** only | Mode 2 selected | +| **C** only | Mode 3 selected | +| **D** only | Mode 4 selected (if present) | +| **A+B** | Mode 5 selected (if present) | + +> Once a sub-mode is launched, that sub-mode's own LED scheme takes over. + +## Button Controls + +| Action | Effect | +|--------|--------| +| **Single click** | Cycle to next sub-mode | +| **Long hold** | Launch selected sub-mode | + +## State Machine + +```mermaid +stateDiagram-v2 + [*] --> Selector : Enter standalone + + state Selector { + [*] --> Mode1 + Mode1 --> Mode2 : Click + Mode2 --> Mode3 : Click + Mode3 --> Mode1 : Click\n(wraps) + } + + Selector --> RunSubMode : Long hold + + state RunSubMode { + [*] --> SubModeActive + SubModeActive --> SubModeActive : Sub-mode running\n(own LEDs/button) + } + + RunSubMode --> Selector : Sub-mode exits + Selector --> [*] : USB connection +``` + +## Customising Bundled Modes + +Edit `armsrc/Standalone/dankarmulti.c`: + +1. Add `#include` for the desired standalone mode header +2. Add entry to the `modes[]` array with the mode's `RunMod()` and `ModInfo()` functions +3. Recompile: + +```bash +make clean +make STANDALONE=DANKARMULTI -j +./pm3-flash-fullimage +``` + +## Compilation + +``` +make clean +make STANDALONE=DANKARMULTI -j +./pm3-flash-fullimage +``` + +## Related + +- [Standalone Modes Overview](../../armsrc/Standalone/readme.md) — Full list of all standalone modes +- [Advanced Compilation](../md/Installation_Instructions/4_Advanced-compilation-parameters.md) — Compilation with STANDALONE= parameter diff --git a/doc/standalone/hf_14asniff.md b/doc/standalone/hf_14asniff.md new file mode 100644 index 000000000..fad1112be --- /dev/null +++ b/doc/standalone/hf_14asniff.md @@ -0,0 +1,81 @@ +# HF_14ASNIFF — ISO14443A Passive Sniffer + +> **Author:** Micolous +> **Frequency:** HF (13.56 MHz) +> **Hardware:** RDV4 (flash and battery recommended) + +[Back to Standalone Modes Index](../../armsrc/Standalone/readme.md#individual-mode-documentation) | [Source Code](../../armsrc/Standalone/hf_14asniff.c) | [Development Guide](../../armsrc/Standalone/readme.md#developing-standalone-modes) + +--- + +## What + +Passively sniffs ISO14443A (NFC Type A) communication between a reader and a card, storing captured frames to the RDV4's onboard flash memory (or RAM on generic hardware). + +## Why + +Many HF access control and payment systems use ISO14443A. By placing the Proxmark3 between a legitimate reader and card, you can capture the full communication — revealing authentication exchanges, data reads/writes, and protocol behavior. This is essential for: + +- **Protocol reverse engineering**: Understand how a reader communicates with cards +- **Authentication capture**: Record authentication handshakes for later analysis +- **System documentation**: Capture real traffic to document proprietary protocols + +## How + +1. Position the Proxmark3 antenna between a reader and card +2. The device captures both reader-to-card and card-to-reader frames with timestamps +3. Frames are buffered in RAM and flushed to flash on button press +4. Retrieve the trace file from flash via the client for analysis with `hf 14a list` + +## LED Indicators + +| LED | Meaning | +|-----|---------| +| **1** (A) | Sniffing active | +| **2** (B) | Tag command detected (off when reader finishes) | +| **3** (C) | Reader command detected (off when tag finishes) | +| **4** (D) | Flash unmounting / sync | + +## Button Controls + +| Action | Effect | +|--------|--------| +| **Short press** | Stop sniffing, save trace to flash, exit | +| **USB command** | Exit standalone mode | + +## State Machine + +```mermaid +stateDiagram-v2 + [*] --> Sniffing : Startup\nFPGA HF mode + + Sniffing --> Sniffing : Capture frames\nLEDs show activity + Sniffing --> SaveToFlash : Button press + + SaveToFlash --> [*] : Trace saved\nLED_D during unmount +``` + +## Retrieved Data + +After sniffing, connect via client and retrieve the trace: +``` +mem spiffs dump -s hf_14asniff.trace -d hf_14asniff.trace +trace load -f hf_14asniff.trace +hf 14a list +``` + +## Compilation + +``` +make clean +make STANDALONE=HF_14ASNIFF -j +./pm3-flash-fullimage +``` + +## Related + +- [14B Sniffer](hf_14bsniff.md) — ISO14443B sniffer +- [15693 Sniffer](hf_15sniff.md) — ISO15693 sniffer +- [Universal Sniffer](hf_unisniff.md) — Multi-protocol sniffer with runtime selection +- [BogitoRun Auth Sniffer](hf_bog.md) — 14A sniffer with auth capture +- [Trace Notes](../trace_notes.md) — Working with trace files diff --git a/doc/standalone/hf_14bsniff.md b/doc/standalone/hf_14bsniff.md new file mode 100644 index 000000000..6e5454bac --- /dev/null +++ b/doc/standalone/hf_14bsniff.md @@ -0,0 +1,59 @@ +# HF_14BSNIFF — ISO14443B Passive Sniffer + +> **Author:** jacopo-j +> **Frequency:** HF (13.56 MHz) +> **Hardware:** RDV4 (flash recommended, optional) + +[Back to Standalone Modes Index](../../armsrc/Standalone/readme.md#individual-mode-documentation) | [Source Code](../../armsrc/Standalone/hf_14bsniff.c) | [Development Guide](../../armsrc/Standalone/readme.md#developing-standalone-modes) + +--- + +## What + +Passively sniffs ISO14443B communication between a reader and card, saving captured frames to flash (or RAM). + +## Why + +ISO14443B is used by certain transit cards, national ID cards, and access control systems (e.g., CEPAS, Calypso). This sniffer captures the full communication exchange for protocol analysis. + +## How + +Identical workflow to [14A Sniffer](hf_14asniff.md) but tuned for the 14443B modulation scheme. Captured frames include both PICC (card) and PCD (reader) traffic. + +## LED Indicators + +| LED | Meaning | +|-----|---------| +| **1** (A) | Sniffing active | +| **2** (B) | Tag command | +| **3** (C) | Reader command | +| **4** (D) | Flash unmounting | + +## Button Controls + +| Action | Effect | +|--------|--------| +| **Short press** | Stop sniffing, save to flash, exit | + +## State Machine + +```mermaid +stateDiagram-v2 + [*] --> Sniffing : Startup + + Sniffing --> SaveToFlash : Button press + SaveToFlash --> [*] : Saved to hf_14bsniff.trace +``` + +## Compilation + +``` +make clean +make STANDALONE=HF_14BSNIFF -j +./pm3-flash-fullimage +``` + +## Related + +- [14A Sniffer](hf_14asniff.md) — ISO14443A sniffer +- [Universal Sniffer](hf_unisniff.md) — Multi-protocol sniffer diff --git a/doc/standalone/hf_15sim.md b/doc/standalone/hf_15sim.md new file mode 100644 index 000000000..a25c27e55 --- /dev/null +++ b/doc/standalone/hf_15sim.md @@ -0,0 +1,66 @@ +# HF_15SIM — ISO15693 Dump and Simulate + +> **Author:** lnv42 +> **Frequency:** HF (13.56 MHz) +> **Hardware:** RDV4 (flash memory) + +[Back to Standalone Modes Index](../../armsrc/Standalone/readme.md#individual-mode-documentation) | [Source Code](../../armsrc/Standalone/hf_15sim.c) | [Development Guide](../../armsrc/Standalone/readme.md#developing-standalone-modes) + +--- + +## What + +Dumps an ISO15693 tag's complete memory, then simulates it. Auto-detects tag type (MIM1024, etc.) and specific attributes like DSFID and AFI. + +## Why + +ISO15693 tags are used in libraries, laundry systems, industrial asset tracking, and some access control. This mode enables read-then-replay attacks: capture a tag's full contents and then emulate it at a reader without the original tag present. + +## How + +1. **Wait**: Scans for an ISO15693 tag in the field +2. **Dump**: On detection, reads all memory blocks and tag system info (DSFID, AFI, block size) +3. **Simulate**: Begins emulating the captured tag with full memory contents + +## LED Indicators + +| LED | Meaning | +|-----|---------| +| **B** (solid) | Waiting for a dumpable tag | +| LEDs off | Dumping / simulating | + +## Button Controls + +| Action | Effect | +|--------|--------| +| **Hold 500ms** | Exit standalone mode | +| **USB command** | Exit standalone mode | + +## State Machine + +```mermaid +stateDiagram-v2 + [*] --> WaitForTag : Startup\nLED_B on + + WaitForTag --> DumpTag : ISO15693 tag found + WaitForTag --> WaitForTag : No tag / incompatible + + DumpTag --> Simulate : Dump complete\n(all blocks read) + DumpTag --> WaitForTag : Dump failed + + Simulate --> [*] : Button hold 500ms + WaitForTag --> [*] : Button hold 500ms +``` + +## Compilation + +``` +make clean +make STANDALONE=HF_15SIM -j +./pm3-flash-fullimage +``` + +## Related + +- [ISO15693 UID Emulator](hf_tmudford.md) — Simpler 15693 UID emulation +- [15693 Sniffer](hf_15sniff.md) — ISO15693 protocol sniffer diff --git a/doc/standalone/hf_15sniff.md b/doc/standalone/hf_15sniff.md new file mode 100644 index 000000000..4b6d2e517 --- /dev/null +++ b/doc/standalone/hf_15sniff.md @@ -0,0 +1,66 @@ +# HF_15SNIFF — ISO15693 Sniffer + +> **Author:** Nathan Glaser +> **Frequency:** HF (13.56 MHz) +> **Hardware:** RDV4 (flash recommended) + +[Back to Standalone Modes Index](../../armsrc/Standalone/readme.md#individual-mode-documentation) | [Source Code](../../armsrc/Standalone/hf_15sniff.c) | [Development Guide](../../armsrc/Standalone/readme.md#developing-standalone-modes) + +--- + +## What + +Passively sniffs ISO15693 communication between reader and tag, storing captured frames to flash. + +## Why + +Capture and analyze the communication protocol between ISO15693 readers and tags for reverse engineering or security assessment. + +## How + +Captures bidirectional 15693 frames with timestamps. Note: timestamp counter overflows after approximately 5 minutes 16 seconds of continuous sniffing. + +## LED Indicators + +| LED | Meaning | +|-----|---------| +| **1** (A) | Sniffing active | +| **2** (B) | Tag command | +| **3** (C) | Reader command | +| **4** (D) | Flash sync | + +## Button Controls + +| Action | Effect | +|--------|--------| +| **Short press** | Stop and save trace to flash | + +## State Machine + +```mermaid +stateDiagram-v2 + [*] --> Sniffing : Startup + + Sniffing --> SaveTrace : Button press + SaveTrace --> [*] : Saved + + note right of Sniffing + Timestamp overflow + at ~5min 16sec + end note +``` + +## Compilation + +``` +make clean +make STANDALONE=HF_15SNIFF -j +./pm3-flash-fullimage +``` + +## Related + +- [14A Sniffer](hf_14asniff.md) — ISO14443A sniffer +- [14B Sniffer](hf_14bsniff.md) — ISO14443B sniffer +- [Universal Sniffer](hf_unisniff.md) — Multi-protocol sniffer +- [15693 Simulator](hf_15sim.md) — ISO15693 dump and simulate diff --git a/doc/standalone/hf_aveful.md b/doc/standalone/hf_aveful.md new file mode 100644 index 000000000..1692f3510 --- /dev/null +++ b/doc/standalone/hf_aveful.md @@ -0,0 +1,84 @@ +# HF_AVEFUL — MIFARE Ultralight Read/Simulation + +> **Author:** Ave Ozkal +> **Frequency:** HF (13.56 MHz) +> **Hardware:** Generic Proxmark3 + +[Back to Standalone Modes Index](../../armsrc/Standalone/readme.md#individual-mode-documentation) | [Source Code](../../armsrc/Standalone/hf_aveful.c) | [Development Guide](../../armsrc/Standalone/readme.md#developing-standalone-modes) + +--- + +## What + +Reads MIFARE Ultralight family cards (UL, ULEV1, UL Nano, My-d Move) and then emulates the captured card. Auto-detects the card type and block count. + +## Why + +MIFARE Ultralight is widely used in transit systems, event tickets, and small-value tokens. This mode enables standalone read-and-replay for: + +- **Transit fare evasion testing**: Capture a valid ticket and present it at a gate +- **Ticket cloning assessment**: Demonstrate that UL tickets can be replayed +- **NFC application testing**: Verify that applications properly validate UL tags + +## How + +1. **SEARCH**: Scans for MIFARE Ultralight cards using anticollision +2. **READ**: Upon finding a card, detects its type via the VERSION command and reads all accessible blocks +3. **EMULATE**: Loads the captured data into the emulator and broadcasts it as a MIFARE Ultralight tag + +Supports auto-detection of: MIFARE Ultralight, Ultralight EV1, Ultralight Nano, and My-d Move. + +## LED Indicators + +| LED | Meaning | +|-----|---------| +| **D** (off during idle) | Blinks during tag search | +| LED patterns | Success/failure indication | + +## Button Controls + +| Action | Effect | +|--------|--------| +| **Hold 1000ms** | Cycle states or exit: SEARCH → READ → EMULATE → exit | +| **USB command** | Exit standalone mode | + +## State Machine + +```mermaid +stateDiagram-v2 + [*] --> SEARCH : Startup + + SEARCH --> READ : UL card found + SEARCH --> SEARCH : No card / incompatible + + READ --> EMULATE : All blocks read + READ --> SEARCH : Read failed + + EMULATE --> SEARCH : Button hold\n(cycle back) + EMULATE --> [*] : Button hold\n(exit) + + SEARCH --> [*] : Button hold (exit) +``` + +## Supported Cards + +| Card Type | Detection | +|-----------|-----------| +| MIFARE Ultralight | VERSION command response | +| MIFARE Ultralight EV1 | VERSION command response | +| MIFARE Ultralight Nano | VERSION command response | +| My-d Move | VERSION command response | + +## Compilation + +``` +make clean +make STANDALONE=HF_AVEFUL -j +./pm3-flash-fullimage +``` + +## Related + +- [UL-C/UL-AES Unlocker](hf_doegox_auth0.md) — Unlock password-protected UL cards +- [BogitoRun Auth Sniffer](hf_bog.md) — Capture UL authentication passwords +- [CraftByte UID Stealer](hf_craftbyte.md) — Generic 14A UID emulator diff --git a/doc/standalone/hf_bog.md b/doc/standalone/hf_bog.md new file mode 100644 index 000000000..c58ca1891 --- /dev/null +++ b/doc/standalone/hf_bog.md @@ -0,0 +1,68 @@ +# HF_BOG — 14A Sniffer with ULC/ULEV1/NTAG Auth Capture + +> **Author:** Bogito +> **Frequency:** HF (13.56 MHz) +> **Hardware:** RDV4 (requires flash memory) + +[Back to Standalone Modes Index](../../armsrc/Standalone/readme.md#individual-mode-documentation) | [Source Code](../../armsrc/Standalone/hf_bog.c) | [Development Guide](../../armsrc/Standalone/readme.md#developing-standalone-modes) + +--- + +## What + +An enhanced ISO14443A sniffer that specifically extracts and stores ULC, ULEV1, and NTAG authentication passwords from sniffed traffic. + +## Why + +Many MIFARE Ultralight deployments use password authentication (PWD_AUTH) to protect data. By sniffing the communication between a legitimate reader and card, you capture the authentication passwords in plaintext. This is more targeted than generic sniffing — it automatically extracts and logs just the passwords. + +## How + +1. Passively sniffs ISO14443A traffic +2. Parses captured frames looking for authentication commands (PWD_AUTH, 3DES AUTH for ULC) +3. Extracts up to 64 authentication attempts per session +4. Saves extracted passwords to `hf_bog.log` on flash + +## LED Indicators + +| LED | Meaning | +|-----|---------| +| **A** (solid) | Sniffing activity | + +## Button Controls + +| Action | Effect | +|--------|--------| +| **Short press** | Stop sniffing, save auth data to flash | + +## State Machine + +```mermaid +stateDiagram-v2 + [*] --> Sniffing : Startup + + Sniffing --> Sniffing : Parse frames\nExtract auth attempts + Sniffing --> SaveAuth : Button press + + SaveAuth --> [*] : Saved to hf_bog.log\n(up to 64 auth attempts) +``` + +## Flash Storage + +- **Log file**: `hf_bog.log` +- Stores extracted authentication passwords/keys +- Up to 64 auth attempts per session + +## Compilation + +``` +make clean +make STANDALONE=HF_BOG -j +./pm3-flash-fullimage +``` + +## Related + +- [14A Sniffer](hf_14asniff.md) — Generic 14A sniffer (captures all frames) +- [Aveful UL Reader](hf_aveful.md) — Read and emulate UL cards +- [UL-C/UL-AES Unlocker](hf_doegox_auth0.md) — Unlock password-protected UL cards diff --git a/doc/standalone/hf_cardhopper.md b/doc/standalone/hf_cardhopper.md new file mode 100644 index 000000000..6a95aa8f1 --- /dev/null +++ b/doc/standalone/hf_cardhopper.md @@ -0,0 +1,90 @@ +# HF_CARDHOPPER — Long-Range 14A Relay over IP + +> **Author:** Sam Haskins +> **Frequency:** HF (13.56 MHz) +> **Hardware:** RDV4 with Bluetooth (BlueShark) or serial add-on + +[Back to Standalone Modes Index](../../armsrc/Standalone/readme.md#individual-mode-documentation) | [Source Code](../../armsrc/Standalone/hf_cardhopper.c) | [Development Guide](../../armsrc/Standalone/readme.md#developing-standalone-modes) + +--- + +## What + +A relay attack framework that tunnels ISO14443A communication over a serial/IP backbone. One Proxmark3 sits near the target reader (CARD mode), another near the victim's card (READER mode), and the relayed data bridges any distance. + +## Why + +Relay attacks demonstrate that proximity-based access control can be defeated remotely. Even "tap to pay" and "tap to enter" systems are vulnerable when the communication can be tunneled over the internet. CardHopper demonstrates this in a practical, standalone way without requiring a laptop at either end. + +Use cases: +- **Relay attacks on NFC payments**: Demonstrate contactless payment relay risks +- **Access control relay**: Bypass door readers by relaying a badge from another location +- **Security awareness**: Show stakeholders that NFC proximity offers limited protection + +## How + +1. **CARD mode** (at reader): Emulates an ISO14443A card and forwards all reader commands over serial/BT +2. **READER mode** (at card): Receives forwarded commands, sends them to the real card, and returns responses +3. The two devices communicate via serial/Bluetooth/IP, transparently relaying the full ISO14443A session + +## LED Indicators + +| LED | Meaning | +|-----|---------| +| **A + D** (solid) | Alive / running indicator | + +## Button Controls + +| Action | Effect | +|--------|--------| +| **Button press** | Exit mode | +| **USB command** | Exit standalone mode | + +## State Machine + +```mermaid +stateDiagram-v2 + [*] --> Init : Startup + + state Init { + [*] --> READER : Config = Reader mode + [*] --> CARD : Config = Card mode + } + + state READER { + WaitCommand --> SendToCard : Command from serial + SendToCard --> SendResponse : Card responds + SendResponse --> WaitCommand : Response sent to serial + } + + state CARD { + WaitReaderCmd --> ForwardToRemote : Reader command received + ForwardToRemote --> ForwardToReader : Remote card response + ForwardToReader --> WaitReaderCmd : Response forwarded + } + + READER --> [*] : Button / Reset + CARD --> [*] : Button / Reset +``` + +## Setup + +Requires two Proxmark3 RDV4 devices: +1. **Near reader**: Running in CARD mode with BT/serial connection +2. **Near card**: Running in READER mode with BT/serial connection +3. Both connected via serial link (direct, Bluetooth, or TCP/IP bridge) + +## Compilation + +``` +make clean +make STANDALONE=HF_CARDHOPPER -j +./pm3-flash-fullimage +``` + +Requires `PLATFORM_EXTRAS=BTADDON` or FPC serial connection. + +## Related + +- [Reblay BT Relay](hf_reblay.md) — Similar 14A relay over Bluetooth +- [14A Sniffer](hf_14asniff.md) — Passive capture instead of active relay diff --git a/doc/standalone/hf_colin.md b/doc/standalone/hf_colin.md new file mode 100644 index 000000000..9896fe267 --- /dev/null +++ b/doc/standalone/hf_colin.md @@ -0,0 +1,71 @@ +# HF_COLIN — VIGIKPWN MIFARE Classic Ultra-Fast Sniff/Sim/Clone + +> **Author:** Colin Brigato +> **Frequency:** HF (13.56 MHz) +> **Hardware:** RDV4 (requires flash memory) + +[Back to Standalone Modes Index](../../armsrc/Standalone/readme.md#individual-mode-documentation) | [Source Code](../../armsrc/Standalone/hf_colin.c) | [Development Guide](../../armsrc/Standalone/readme.md#developing-standalone-modes) + +--- + +## What + +A specialized MIFARE Classic attack mode designed for French VIGIK access control systems. It performs fast authentication attempts using ~37 hardcoded VIGIK keys, dumps the card, and can simulate or clone it. + +## Why + +VIGIK is a widely deployed intercom/access control system in French apartment buildings. It uses MIFARE Classic with a known set of keys. This mode automates the entire VIGIK attack chain — from key discovery to cloning — entirely on-device. + +## How + +1. **SEARCH**: Scans for MIFARE Classic cards +2. **READ**: Attempts authentication with hardcoded VIGIK keys, reads accessible sectors +3. **LOAD**: Loads captured data for simulation via JSON schema +4. **EMULATE**: Simulates the captured card + +The mode uses a terminal-style UI with cursor positioning for status display. + +## LED Indicators + +| LED | Meaning | +|-----|---------| +| Complex terminal UI | Uses debug output for status rather than traditional LED patterns | + +## Button Controls + +| Action | Effect | +|--------|--------| +| Various presses | Trigger different functions in the UI | + +## State Machine + +```mermaid +stateDiagram-v2 + [*] --> SEARCH : Startup + + SEARCH --> READ : MFC card found + READ --> LOAD : Keys found,\nsectors dumped + LOAD --> EMULATE : Data loaded + + EMULATE --> SEARCH : Cycle back + + READ --> SEARCH : No valid keys + + SEARCH --> [*] : Exit +``` + +## Compilation + +``` +make clean +make STANDALONE=HF_COLIN -j +./pm3-flash-fullimage +``` + +## Related + +- [MattyRun MFC Clone](hf_mattyrun.md) — Generic MIFARE Classic attack +- [MFC Simulator](hf_mfcsim.md) — MFC simulation from flash +- [Young MFC Sniff/Sim](hf_young.md) — MFC sniff and simulation +- [MIFARE Classic Notes](../mfc_notes.md) — Key recovery techniques +- [Magic Cards Notes](../magic_cards_notes.md) — Writable magic card types diff --git a/doc/standalone/hf_craftbyte.md b/doc/standalone/hf_craftbyte.md new file mode 100644 index 000000000..8cc3b9643 --- /dev/null +++ b/doc/standalone/hf_craftbyte.md @@ -0,0 +1,67 @@ +# HF_CRAFTBYTE — ISO14443A UID Stealer/Emulator + +> **Author:** Anze Jensterle +> **Frequency:** HF (13.56 MHz) +> **Hardware:** Generic Proxmark3 + +[Back to Standalone Modes Index](../../armsrc/Standalone/readme.md#individual-mode-documentation) | [Source Code](../../armsrc/Standalone/hf_craftbyte.c) | [Development Guide](../../armsrc/Standalone/readme.md#developing-standalone-modes) + +--- + +## What + +Continuously scans for ISO14443A cards, captures their UIDs, and emulates them. Auto-detects card type (MFC 1K/4K, MIFARE Ultralight, DESFire). + +## Why + +Many access control systems rely primarily (or solely) on the UID of an NFC card for identification, without performing proper cryptographic authentication. CraftByte exploits this by capturing and replaying UIDs — demonstrating that UID-based access control is trivially defeated. + +## How + +1. **READ**: Performs ISO14443A anticollision to read the card's UID, ATQA, and SAK +2. **EMULATE**: Uses the captured UID to emulate the card at a reader + +The mode detects the card type from ATQA/SAK and configures emulation accordingly. + +## LED Indicators + +| LED | Meaning | +|-----|---------| +| Minimal LED usage | Focus on read/emulate cycle | + +## Button Controls + +| Action | Effect | +|--------|--------| +| **Hold 1000ms** | Cycle: READ → EMULATE, or exit if held continuously | +| **USB command** | Exit standalone mode | + +## State Machine + +```mermaid +stateDiagram-v2 + [*] --> READ : Startup + + READ --> EMULATE : UID captured\n(button hold) + EMULATE --> READ : Button hold\n(scan new card) + + READ --> READ : Scanning... + EMULATE --> EMULATE : Emulating... + + READ --> [*] : Long hold / USB data + EMULATE --> [*] : Long hold / USB data +``` + +## Compilation + +``` +make clean +make STANDALONE=HF_CRAFTBYTE -j +./pm3-flash-fullimage +``` + +## Related + +- [Aveful UL Reader](hf_aveful.md) — Full UL read/emulate (not just UID) +- [MattyRun MFC Clone](hf_mattyrun.md) — Full MFC attack (keys + data) +- [Young MFC Sniff/Sim](hf_young.md) — MFC UID capture with 2-bank storage diff --git a/doc/standalone/hf_doegox_auth0.md b/doc/standalone/hf_doegox_auth0.md new file mode 100644 index 000000000..36caf02e6 --- /dev/null +++ b/doc/standalone/hf_doegox_auth0.md @@ -0,0 +1,77 @@ +# HF_DOEGOX_AUTH0 — Ultralight C / Ultralight AES Unlocker + +> **Author:** Philippe Teuwen (doegox) +> **Frequency:** HF (13.56 MHz) +> **Hardware:** Generic Proxmark3 (RDV4 with 9V antenna recommended) + +[Back to Standalone Modes Index](../../armsrc/Standalone/readme.md#individual-mode-documentation) | [Source Code](../../armsrc/Standalone/hf_doegox_auth0.c) | [Development Guide](../../armsrc/Standalone/readme.md#developing-standalone-modes) + +--- + +## What + +Performs a relay-style attack to unlock password-protected MIFARE Ultralight C or Ultralight AES tags by rewriting the AUTH0 configuration byte during an authenticated session. + +## Why + +MIFARE Ultralight C and AES variants can password-protect their memory by setting an AUTH0 byte that specifies the first page requiring authentication. If AUTH0 itself is writable during an authenticated session, this mode exploits that window — during a legitimate reader's auth handshake — to rewrite AUTH0 to a higher page number, effectively unlocking all previously protected pages. + +This is a sophisticated attack that requires precise timing and makes protected data permanently accessible. + +## How + +1. **LOOK**: Search for an Ultralight C or AES tag +2. **SNIFF**: Position the Proxmark3 to sniff the authentication exchange between the legitimate reader and the card +3. **WAIT**: Press button when ready to attempt the AUTH0 rewrite +4. **WRITE**: During the next auth session, inject a write command to AUTH0 that unlocks the card +5. **Result**: LED indicates success (solid) or failure (blink) + +## LED Indicators + +| LED | Meaning | +|-----|---------| +| **A** (solid) | Looking for card / preparing | +| **B** (solid) | Card found | +| **C** (solid) | Sniffing for auth exchange | +| **D** (solid) | Write successful | +| **D** (blinking) | Write failed | + +## Button Controls + +| Action | Effect | +|--------|--------| +| **Press (1 sec)** | Initiate AUTH0 write during next auth sniff | +| **Button press** | Exit mode (from other states) | + +## State Machine + +```mermaid +stateDiagram-v2 + [*] --> LOOK_FOR_CARD : Startup\nLED_A + + LOOK_FOR_CARD --> SNIFF_AUTH : ULC/ULAES found\nLED_B → LED_C + LOOK_FOR_CARD --> LOOK_FOR_CARD : No compatible card + + SNIFF_AUTH --> WAIT_BUTTON : Auth detected + WAIT_BUTTON --> WAIT_RELEASE : Button pressed\n(ready to write) + WAIT_RELEASE --> WRITE_AUTH0 : Button released + + WRITE_AUTH0 --> EXIT_SUCCESS : AUTH0 rewritten\nLED_D solid + WRITE_AUTH0 --> EXIT_FAIL : Write failed\nLED_D blink + + EXIT_SUCCESS --> [*] + EXIT_FAIL --> [*] +``` + +## Compilation + +``` +make clean +make STANDALONE=HF_DOEGOX_AUTH0 -j +./pm3-flash-fullimage +``` + +## Related + +- [BogitoRun Auth Sniffer](hf_bog.md) — Capture UL auth passwords +- [Aveful UL Reader](hf_aveful.md) — Read/emulate UL cards diff --git a/doc/standalone/hf_emvpng.md b/doc/standalone/hf_emvpng.md new file mode 100644 index 000000000..68bfef5b6 --- /dev/null +++ b/doc/standalone/hf_emvpng.md @@ -0,0 +1,72 @@ +# HF_EMVPNG — EMV Visa Card Reader/Emulator + +> **Author:** Davi Mikael (Penegui) +> **Frequency:** HF (13.56 MHz) +> **Hardware:** RDV4 (flash memory) + +[Back to Standalone Modes Index](../../armsrc/Standalone/readme.md#individual-mode-documentation) | [Source Code](../../armsrc/Standalone/hf_emvpng.c) | [Development Guide](../../armsrc/Standalone/readme.md#developing-standalone-modes) + +--- + +## What + +Reads Visa EMV contactless payment cards and emulates the captured transaction data with a fixed ARQC (Authorization Request Cryptogram). **For educational and lab use only.** + +## Why + +Demonstrates the theoretical vulnerability of contactless payment cards to replay attacks when terminals don't properly validate cryptograms. This mode is designed for controlled lab environments to: + +- **Educate**: Show how EMV contactless transactions work at the protocol level +- **Research**: Study EMV protocol behavior and terminal validation +- **Test terminals**: Verify that terminals properly reject replayed transactions + +> ⚠ **Warning**: This mode uses a fixed ARQC. Modern payment terminals will reject these transactions. This is for educational purposes only. + +## How + +1. **READ**: Select the Visa application (PPSE/AID), read Track 2 data +2. **EMULATE**: Present captured Track 2 data with a fixed ARQC when queried by a terminal + +## LED Indicators + +| LED | Meaning | +|-----|---------| +| **A** (solid) | Reading mode | +| **B** (solid) | Activity indicator | +| **C** (solid) | Emulation mode | + +## Button Controls + +| Action | Effect | +|--------|--------| +| **Single click** | Toggle between READ and EMULATE modes | +| **Long hold** | Exit standalone mode | + +## State Machine + +```mermaid +stateDiagram-v2 + [*] --> READ : Startup + + READ --> EMULATE : Card read\n+ button click + EMULATE --> READ : Button click + + READ --> READ : Waiting for Visa card + EMULATE --> EMULATE : Emulating with fixed ARQC + + READ --> [*] : Long hold / USB data + EMULATE --> [*] : Long hold / USB data +``` + +## Compilation + +``` +make clean +make STANDALONE=HF_EMVPNG -j +./pm3-flash-fullimage +``` + +## Related + +- [MSD Visa Reader](hf_msdsal.md) — Visa MSD (older format) reader/emulator +- [EMV Notes](../emv_notes.md) — EMV protocol documentation diff --git a/doc/standalone/hf_iceclass.md b/doc/standalone/hf_iceclass.md new file mode 100644 index 000000000..e717a9227 --- /dev/null +++ b/doc/standalone/hf_iceclass.md @@ -0,0 +1,105 @@ +# HF_ICECLASS — iCLASS Multi-Mode Standalone + +> **Author:** Iceman +> **Frequency:** HF (13.56 MHz) +> **Hardware:** RDV4 (requires flash memory) + +[Back to Standalone Modes Index](../../armsrc/Standalone/readme.md#individual-mode-documentation) | [Source Code](../../armsrc/Standalone/hf_iceclass.c) | [Development Guide](../../armsrc/Standalone/readme.md#developing-standalone-modes) + +--- + +## What + +A multi-mode iCLASS standalone with **7 selectable modes** for different operations: full simulation, reader attack, dump-and-simulate, read-and-simulate, and configuration card creation. Only one mode is active per compile. + +## Why + +HID iCLASS is a widely deployed access control system. This mode provides a comprehensive toolkit for iCLASS assessment: + +- **Credential recovery**: Capture authentication data for offline key recovery (loclass attack) +- **Badge simulation**: Emulate captured iCLASS credentials at readers +- **Reader configuration**: Create config cards that can reconfigure iCLASS readers (e.g., downgrade attacks) + +## How + +The mode selected at compile time (`ICE_USE` macro) determines behavior: + +| ICE_USE Value | Mode | Description | +|---------------|------|-------------| +| ICE_USE_FULLSIM | Full Simulation | Emulate a complete iCLASS card from EEPROM dump | +| ICE_USE_READER_ATTACK | Reader Attack | Capture authentication MACs for loclass recovery | +| ICE_USE_DUMP_SIM | Dump & Simulate | Dump a card then immediately simulate it | +| ICE_USE_READ_SIM | Read & Simulate | Read credential blocks and simulate | +| ICE_USE_CONFIG_CARD | Config Card | Create configuration cards for reader reprogramming | + +The reader attack mode is particularly powerful: it captures the authentication exchange between a reader and cards, producing MAC pairs that feed into the loclass attack for key recovery. + +## LED Indicators + +| LED | Meaning | +|-----|---------| +| **B** (solid/blink) | Attack mode activity | +| **D** (solid) | General operation indicator | +| Mode-specific patterns | Vary by selected ICE_USE mode | + +## Button Controls + +Vary by selected mode. Generally: + +| Action | Effect | +|--------|--------| +| **Button press** | Mode-specific action | +| **Hold** | Exit standalone mode | + +## State Machine (Reader Attack Mode) + +```mermaid +stateDiagram-v2 + [*] --> WaitCard : Startup\n(Reader Attack mode) + + WaitCard --> Authenticate : iCLASS card detected + Authenticate --> CaptureMAC : Auth exchange\ncaptured + CaptureMAC --> SaveFlash : MAC pair logged + + SaveFlash --> WaitCard : Continue collecting + WaitCard --> [*] : Button hold / USB data + + note right of CaptureMAC + MAC pairs used for + loclass key recovery + end note +``` + +## State Machine (Dump & Simulate Mode) + +```mermaid +stateDiagram-v2 + [*] --> ScanCard : Startup + + ScanCard --> DumpCard : iCLASS card found + DumpCard --> LoadEmulator : Dump complete + LoadEmulator --> Simulate : Data loaded + + Simulate --> [*] : Button hold / USB data + DumpCard --> ScanCard : Dump failed +``` + +## Flash Storage + +- Captured MAC pairs stored on SPI flash for later retrieval +- EEPROM dumps stored for simulation modes +- Configuration card templates + +## Compilation + +``` +make clean +make STANDALONE=HF_ICECLASS -j +./pm3-flash-fullimage +``` + +## Related + +- [Loclass Notes](../loclass_notes.md) — Loclass attack documentation +- [HID Downgrade Attacks](../hid_downgrade.md) — Reader downgrade techniques +- [IceHID Collector](lf_icehid.md) — LF HID credential collection (different protocol) diff --git a/doc/standalone/hf_legic.md b/doc/standalone/hf_legic.md new file mode 100644 index 000000000..8b337f104 --- /dev/null +++ b/doc/standalone/hf_legic.md @@ -0,0 +1,66 @@ +# HF_LEGIC — Legic Prime Read/Simulate + +> **Author:** uhei +> **Frequency:** HF (13.56 MHz) +> **Hardware:** Generic Proxmark3 + +[Back to Standalone Modes Index](../../armsrc/Standalone/readme.md#individual-mode-documentation) | [Source Code](../../armsrc/Standalone/hf_legic.c) | [Development Guide](../../armsrc/Standalone/readme.md#developing-standalone-modes) + +--- + +## What + +Reads Legic Prime tags and simulates them. Auto-detects card type (MIM256, MIM512, MIM1024). + +## Why + +Legic Prime is a proprietary HF contactless technology used in European access control, time & attendance, and vending systems. This mode provides standalone read-and-replay capability. + +## How + +1. **Search**: Continuously scans for Legic Prime tags +2. **Read**: On detection, dumps the tag memory (auto-detects size) +3. **Simulate**: Broadcasts the captured tag data + +## LED Indicators + +| LED | Meaning | +|-----|---------| +| **C** (solid) | Searching for tag | +| **A + B + C** (solid) | Reading tag | +| **A + D** (solid) | Simulating tag | + +## Button Controls + +| Action | Effect | +|--------|--------| +| **Hold 280ms** | Exit standalone mode | +| **USB command** | Exit standalone mode | + +## State Machine + +```mermaid +stateDiagram-v2 + [*] --> Search : Startup\nLED_C + + Search --> Read : Legic Prime found\nLED_A+B+C + Read --> Simulate : Tag dumped\nLED_A+D + + Simulate --> Search : Loop back + Read --> Search : Read failed + + Search --> [*] : Button hold / USB data + Simulate --> [*] : Button hold / USB data +``` + +## Compilation + +``` +make clean +make STANDALONE=HF_LEGIC -j +./pm3-flash-fullimage +``` + +## Related + +- [Legic Prime Simulator](hf_legicsim.md) — Multi-slot Legic simulation from flash diff --git a/doc/standalone/hf_legicsim.md b/doc/standalone/hf_legicsim.md new file mode 100644 index 000000000..ee439a1b9 --- /dev/null +++ b/doc/standalone/hf_legicsim.md @@ -0,0 +1,81 @@ +# HF_LEGICSIM — Legic Prime Multi-Slot Simulator + +> **Author:** uhei +> **Frequency:** HF (13.56 MHz) +> **Hardware:** RDV4 (requires flash memory) + +[Back to Standalone Modes Index](../../armsrc/Standalone/readme.md#individual-mode-documentation) | [Source Code](../../armsrc/Standalone/hf_legicsim.c) | [Development Guide](../../armsrc/Standalone/readme.md#developing-standalone-modes) + +--- + +## What + +Simulates Legic Prime MIM1024 dumps stored on flash memory. Supports up to **15 dump slots** that can be cycled through. Changes made by readers during simulation are written back to the dump. + +## Why + +When you need to emulate multiple Legic Prime cards on-site — for example, testing which credentials grant access to different areas. The 15-slot capacity and flash persistence means dumps survive power cycles. + +## How + +1. On startup, loads the first dump from flash (`hf_legicsim_dump_01.bin`) +2. Simulates the loaded dump as a Legic Prime MIM1024 tag +3. Short press cycles to the next slot +4. After simulation, any changes written by readers are saved back to the dump file + +## LED Indicators + +| LED | Meaning | +|-----|---------| +| LEDs (1–15) | Current slot number indication | + +## Button Controls + +| Action | Effect | +|--------|--------| +| **Short press** | Next dump slot | +| **Hold 500ms** | Exit standalone mode | + +## State Machine + +```mermaid +stateDiagram-v2 + [*] --> LoadSlot1 : Startup + + LoadSlot1 --> Simulate : Dump loaded + Simulate --> WriteBack : Simulation stopped + WriteBack --> LoadSlot2 : Short press\nSave changes, next slot + + LoadSlot2 --> Simulate : Dump loaded + + Simulate --> [*] : Hold 500ms + + note right of Simulate + Slots 01-15 + Files: hf_legicsim_dump_XX.bin + Each 1024 bytes (MIM1024) + end note +``` + +## Flash Files + +Upload dumps before use: +``` +mem spiffs load -s hf_legicsim_dump_01.bin -d hf_legicsim_dump_01.bin +mem spiffs load -s hf_legicsim_dump_02.bin -d hf_legicsim_dump_02.bin +... +``` + +Each file is 1024 bytes (Legic Prime MIM1024 dump). + +## Compilation + +``` +make clean +make STANDALONE=HF_LEGICSIM -j +./pm3-flash-fullimage +``` + +## Related + +- [Legic Prime Reader](hf_legic.md) — Read and simulate Legic tags (single shot) diff --git a/doc/standalone/hf_mattyrun.md b/doc/standalone/hf_mattyrun.md new file mode 100644 index 000000000..23c861d7c --- /dev/null +++ b/doc/standalone/hf_mattyrun.md @@ -0,0 +1,88 @@ +# HF_MATTYRUN — MIFARE Classic Key Check/Dump/Emulate + +> **Author:** Matías A. Ré Medina +> **Frequency:** HF (13.56 MHz) +> **Hardware:** Generic Proxmark3 + +[Back to Standalone Modes Index](../../armsrc/Standalone/readme.md#individual-mode-documentation) | [Source Code](../../armsrc/Standalone/hf_mattyrun.c) | [Development Guide](../../armsrc/Standalone/readme.md#developing-standalone-modes) + +--- + +## What + +A full MIFARE Classic attack chain: discovers MFC cards, checks keys from a built-in dictionary, dumps the card using ecfill, then emulates it. Supports MIFARE Classic 1K and 4K. + +## Why + +MIFARE Classic is the most widely deployed contactless smart card worldwide — used in transit, access control, and loyalty systems. MattyRun automates the complete attack pipeline on-device: + +1. Find the card +2. Recover the keys (from dictionary) +3. Dump all data +4. Emulate the full card at a reader + +No laptop required at any step. + +## How + +1. **READ**: Scans for MIFARE Classic cards (anticollision, ATQA/SAK check) +2. **ATTACK**: Checks keys from the built-in dictionary against all sectors. Uses nested authentication attack if partial keys are found +3. **LOAD**: Performs ecfill to dump the card data into the emulator memory +4. **EMULATE**: Simulates the complete MIFARE Classic card including all sector keys and data + +LED D lit during emulation indicates a partial dump (some sectors couldn't be read). + +## LED Indicators + +| LED | Meaning | +|-----|---------| +| **C + D** (solid) | Idle / searching for card | +| **C + D** (blinking) | Authenticating / checking keys | +| **B** (solid) | Attack mode (nested) | +| **A + B + C** (solid) | Loading data to emulator | +| **A + B + C + D** (solid) | Emulating (D = partial dump warning) | + +## Button Controls + +| Action | Effect | +|--------|--------| +| **Hold 280ms** | Exit standalone mode | +| **Short press** | No effect (purely state-machine driven) | + +## State Machine + +```mermaid +stateDiagram-v2 + [*] --> READ : Startup\nLED_C+D + + READ --> ATTACK : MFC card found\nLED_C+D blink + READ --> READ : No card found + + ATTACK --> LOAD : Keys recovered\nLED_B + ATTACK --> READ : No keys found\n(retry with new card) + + LOAD --> EMULATE : ecfill complete\nLED_A+B+C + + EMULATE --> [*] : Button hold\nLED_A+B+C+D + + note right of EMULATE + LED_D lit = partial dump + (some sectors unreadable) + end note +``` + +## Compilation + +``` +make clean +make STANDALONE=HF_MATTYRUN -j +./pm3-flash-fullimage +``` + +## Related + +- [VIGIKPWN](hf_colin.md) — MIFARE Classic with VIGIK-specific keys +- [MFC Simulator](hf_mfcsim.md) — Simulate MFC from flash dumps +- [Young MFC Sniff/Sim](hf_young.md) — UID-based MFC sniff and sim +- [MIFARE Classic Notes](../mfc_notes.md) — Key recovery and attack techniques +- [Magic Cards Notes](../magic_cards_notes.md) — Writing to magic/CUID cards diff --git a/doc/standalone/hf_mfcsim.md b/doc/standalone/hf_mfcsim.md new file mode 100644 index 000000000..64889264e --- /dev/null +++ b/doc/standalone/hf_mfcsim.md @@ -0,0 +1,84 @@ +# HF_MFCSIM — MIFARE Classic 1K Multi-Slot Simulator + +> **Author:** Ray Lee +> **Frequency:** HF (13.56 MHz) +> **Hardware:** RDV4 (requires flash memory) + +[Back to Standalone Modes Index](../../armsrc/Standalone/readme.md#individual-mode-documentation) | [Source Code](../../armsrc/Standalone/hf_mfcsim.c) | [Development Guide](../../armsrc/Standalone/readme.md#developing-standalone-modes) + +--- + +## What + +Simulates MIFARE Classic 1K cards from dump files stored on flash. Supports up to **15 dump slots**. Changes written by readers during simulation are saved back to the dump file. + +## Why + +When you have multiple MIFARE Classic card dumps (from `hf mf dump` or other tools) and need to emulate them on-site without a laptop. The 15-slot capacity covers multiple credentials, and the write-back feature preserves any reader-induced changes. + +## How + +1. Loads dump number 1 from flash (`hf_mfcsim_dump_01.bin`) +2. Configures the emulator with full card data including all sector keys +3. Begins simulation +4. Cycle through slots for different cards +5. Any writes from readers are saved back to the dump file + +## LED Indicators + +| LED | Meaning | +|-----|---------| +| LEDs | Indicate current dump slot number (1–15) | + +## Button Controls + +| Action | Effect | +|--------|--------| +| **Short press** | Next dump slot | +| **Hold 500ms** | Exit standalone mode | + +## State Machine + +```mermaid +stateDiagram-v2 + [*] --> LoadSlot : Startup + + LoadSlot --> Simulate : Dump loaded into emulator + Simulate --> WriteBack : Simulation stopped + + WriteBack --> LoadNext : Short press\nSave, advance slot + LoadNext --> LoadSlot : Load next dump + + Simulate --> [*] : Hold 500ms + + note right of Simulate + Slots 01-15 + Files: hf_mfcsim_dump_XX.bin + Each 1024 bytes (MFC 1K) + end note +``` + +## Flash Files + +Upload dumps before use: +``` +mem spiffs load -s hf_mfcsim_dump_01.bin -d hf_mfcsim_dump_01.bin +mem spiffs load -s hf_mfcsim_dump_02.bin -d hf_mfcsim_dump_02.bin +... +``` + +Each file is 1024 bytes (MIFARE Classic 1K dump including sector keys). + +## Compilation + +``` +make clean +make STANDALONE=HF_MFCSIM -j +./pm3-flash-fullimage +``` + +## Related + +- [MattyRun MFC Clone](hf_mattyrun.md) — Full MFC attack chain (discover → dump → emulate) +- [VIGIKPWN](hf_colin.md) — VIGIK-specific MFC attacks +- [MIFARE Classic Notes](../mfc_notes.md) — Key recovery techniques diff --git a/doc/standalone/hf_msdsal.md b/doc/standalone/hf_msdsal.md new file mode 100644 index 000000000..8bb41a88d --- /dev/null +++ b/doc/standalone/hf_msdsal.md @@ -0,0 +1,68 @@ +# HF_MSDSAL — Visa MSD Card Reader/Emulator + +> **Author:** Salvador Mendoza +> **Frequency:** HF (13.56 MHz) +> **Hardware:** Generic Proxmark3 + +[Back to Standalone Modes Index](../../armsrc/Standalone/readme.md#individual-mode-documentation) | [Source Code](../../armsrc/Standalone/hf_msdsal.c) | [Development Guide](../../armsrc/Standalone/readme.md#developing-standalone-modes) + +--- + +## What + +Reads Visa MSD (Magnetic Stripe Data) cards and emulates the captured Track 2 equivalent data. MSD is an older EMV contactless mode that mirrors magnetic stripe data over NFC. + +## Why + +Visa MSD mode transmits Track 2 data in a format similar to magnetic stripe cards. This mode demonstrates the risk of MSD mode by capturing and replaying the transaction data. MSD has largely been superseded by EMV contactless (qVSDC), but some terminals still support it as a fallback. + +> ⚠ **Note**: MSD mode is deprecated in many markets. Modern terminals may reject MSD transactions. + +## How + +1. **READ**: Selects PPSE → Visa AID → reads PDOL/SFI → extracts 19-byte Track 2 data +2. **EMULATE**: Presents the captured Track 2 data when queried by a terminal + +## LED Indicators + +| LED | Meaning | +|-----|---------| +| **A** (solid) | Reading mode | +| **B** (solid) | Activity indicator | +| **C** (solid) | Emulation mode (Track 2 loaded) | + +## Button Controls + +| Action | Effect | +|--------|--------| +| **Single click** | Toggle between READ and EMULATE | +| **Long hold** | Exit standalone mode | + +## State Machine + +```mermaid +stateDiagram-v2 + [*] --> READ : Startup + + READ --> EMULATE : Track2 captured\n+ button click + EMULATE --> READ : Button click + + READ --> READ : Waiting for Visa MSD card + EMULATE --> EMULATE : Emulating Track2 + + READ --> [*] : Long hold / USB data + EMULATE --> [*] : Long hold / USB data +``` + +## Compilation + +``` +make clean +make STANDALONE=HF_MSDSAL -j +./pm3-flash-fullimage +``` + +## Related + +- [EMV Visa Reader/Emulator](hf_emvpng.md) — Modern EMV Visa reader/emulator +- [EMV Notes](../emv_notes.md) — EMV protocol documentation diff --git a/doc/standalone/hf_reblay.md b/doc/standalone/hf_reblay.md new file mode 100644 index 000000000..19e3b75d7 --- /dev/null +++ b/doc/standalone/hf_reblay.md @@ -0,0 +1,87 @@ +# HF_REBLAY — ISO 14443-A Relay over Bluetooth + +> **Author:** Salvador Mendoza +> **Frequency:** HF (13.56 MHz) +> **Hardware:** RDV4 with Bluetooth module (required) + +[Back to Standalone Modes Index](../../armsrc/Standalone/readme.md#individual-mode-documentation) | [Source Code](../../armsrc/Standalone/hf_reblay.c) | [Development Guide](../../armsrc/Standalone/readme.md#developing-standalone-modes) + +--- + +## What + +Relays ISO 14443-A NFC communications between a real card and a remote reader over Bluetooth. One Proxmark3 RDV4 acts as the reader (captures card responses), the other as the emulator (presents them to a terminal), with Bluetooth bridging the two. + +## Why + +Relay attacks demonstrate a fundamental weakness in proximity-based authentication: the assumption that the card is physically near the reader. By relaying messages in real-time, an attacker can use a card that is far away — for example, performing a contactless payment using a card in someone else's pocket. This mode is an educational tool for understanding relay attack mechanics. + +> ⚠ **Security Research Only**: This tool demonstrates a known class of NFC vulnerability for research purposes. + +## How + +1. **Device A (Reader side)**: Placed near the victim's card. Receives APDU commands from Device B over BT, sends them to the card, relays responses back over BT. +2. **Device B (Emulator side)**: Placed near the target terminal. Receives terminal commands, forwards them to Device A over BT, plays back card responses to the terminal. +3. **Bluetooth Link**: USART-based BT serial bridge with a custom framing protocol (preamble `0xAA`, length, data, postamble `0xBB`). +4. **Timing**: Implements WTX (Waiting Time eXtension) and ACK management to keep the terminal patient during relay delay. This is important for terminals like SumUp that have tight timing requirements. + +## LED Indicators + +| LED | Meaning | +|-----|---------| +| **A** (solid) | Reader mode active (proximate to card) | +| **C** (solid) | Emulation mode active (proximate to terminal) | +| **A+C** (blink) | BT data exchange in progress | +| **B+D** (blink) | Error / timeout | + +## Button Controls + +| Action | Effect | +|--------|--------| +| **Button press** | Exit standalone mode | + +## State Machine + +```mermaid +stateDiagram-v2 + [*] --> Init : Startup + + Init --> ReaderMode : BT connected\n(reader side) + Init --> EmulatorMode : BT connected\n(emulator side) + + state ReaderMode { + WaitBT_Cmd --> SendToCard : Receive APDU via BT + SendToCard --> WaitCard_Resp : ISO14443A transceive + WaitCard_Resp --> SendBT_Resp : Card responds + SendBT_Resp --> WaitBT_Cmd : Response sent over BT + } + + state EmulatorMode { + WaitTerminal_Cmd --> SendBT_Relay : Terminal sends APDU + SendBT_Relay --> WaitBT_Reply : Relay to reader via BT + WaitBT_Reply --> RespondTerminal : Card response received + RespondTerminal --> WaitTerminal_Cmd : Response sent to terminal + } + + ReaderMode --> [*] : Button / disconnect + EmulatorMode --> [*] : Button / disconnect +``` + +## Prerequisites + +- **Two** Proxmark3 RDV4 devices +- Both with Bluetooth modules connected and paired +- Flash each with `HF_REBLAY` standalone firmware +- One device near the card, one near the terminal + +## Compilation + +``` +make clean +make STANDALONE=HF_REBLAY -j +./pm3-flash-fullimage +``` + +## Related + +- [Card Hopper](hf_cardhopper.md) — Similar relay concept using BLE and phone-based bridge diff --git a/doc/standalone/hf_st25_tearoff.md b/doc/standalone/hf_st25_tearoff.md new file mode 100644 index 000000000..e80a641c4 --- /dev/null +++ b/doc/standalone/hf_st25_tearoff.md @@ -0,0 +1,91 @@ +# HF_ST25_TEAROFF — ST25TB Tear-Off / Counter Restore + +> **Author:** Doegox, Iceman +> **Frequency:** HF (13.56 MHz) +> **Hardware:** RDV4 with flash (required) + +[Back to Standalone Modes Index](../../armsrc/Standalone/readme.md#individual-mode-documentation) | [Source Code](../../armsrc/Standalone/hf_st25_tearoff.c) | [Development Guide](../../armsrc/Standalone/readme.md#developing-standalone-modes) + +--- + +## What + +Store and restore ST25TB/SRx tags using a power tear-off technique on their decrementing counters (blocks 5 and 6). These counters normally can only count down, but a precisely timed tear-off can corrupt and reset them. + +## Why + +ST25TB and SRx NFC tags (used in transport tickets, access systems, etc.) contain one-way counters that decrement on each use. Normally these counters cannot be reset. The tear-off technique exploits the fact that if the RF field drops at the precise moment a counter write is completing, the write may fail or partially complete — potentially restoring a previous counter value. This enables research into counter-based anti-replay mechanisms. + +## How + +1. **LEARN mode**: Reads up to 8 ST25TB tags and stores their complete memory dump (including counter blocks 5 & 6) to Proxmark3 flash memory. +2. **RESTORE mode**: Reads a previously-learned tag, compares current counter values to stored values, and if the counters have decremented, attempts a tear-off write to restore the original counter values. +3. **Tear-off mechanism**: Rapidly toggles the RF field at the precise moment the counter write completes, attempting to corrupt the write. Retries with varied timing until the counter is restored or the maximum attempt count is reached. + +## LED Indicators + +| LED | Meaning | +|-----|---------| +| **D** (solid) | LEARN mode active | +| **C** (solid) | RESTORE mode active | +| **A** (solid) | Operation succeeded (counter restored) | +| **B** (solid) | Operation failed | +| **A+B+C+D** (blink) | Searching for tag | + +## Button Controls + +| Action | Effect | +|--------|--------| +| **Single click** | Toggle between LEARN and RESTORE mode | +| **Long hold** | Exit standalone mode | + +## State Machine + +```mermaid +stateDiagram-v2 + [*] --> LEARN : Startup + + state LEARN { + WaitTag_L --> ReadTag : Tag detected + ReadTag --> StoreFlash : Read all blocks + StoreFlash --> WaitTag_L : Saved (up to 8 tags) + } + + state RESTORE { + WaitTag_R --> MatchTag : Tag detected + MatchTag --> CompareCounters : UID matched to stored + CompareCounters --> TearOff : Counter decremented + CompareCounters --> SkipRestore : Counter matches stored + TearOff --> VerifyRestore : RF tear-off attempt + VerifyRestore --> Success : Counter restored + VerifyRestore --> TearOff : Counter still low, retry + VerifyRestore --> Fail : Max attempts reached + Success --> WaitTag_R + SkipRestore --> WaitTag_R + Fail --> WaitTag_R + } + + LEARN --> RESTORE : Button click + RESTORE --> LEARN : Button click + + LEARN --> [*] : Long hold + RESTORE --> [*] : Long hold +``` + +## Stored Data + +| Flash File | Contents | +|------------|----------| +| Tag dumps | Full block data for up to 8 ST25TB tags | + +## Compilation + +``` +make clean +make STANDALONE=HF_ST25_TEAROFF -j +./pm3-flash-fullimage +``` + +## Related + +- [ST25TA / IKEA Rothult](hf_tcprst.md) — Similar ST25 family, different attack diff --git a/doc/standalone/hf_tcprst.md b/doc/standalone/hf_tcprst.md new file mode 100644 index 000000000..703ec3483 --- /dev/null +++ b/doc/standalone/hf_tcprst.md @@ -0,0 +1,94 @@ +# HF_TCPRST — IKEA Rothult / ST25TA Password Extractor + +> **Author:** Nick Draffen (tcprst) +> **Frequency:** HF (13.56 MHz) +> **Hardware:** Generic Proxmark3 + +[Back to Standalone Modes Index](../../armsrc/Standalone/readme.md#individual-mode-documentation) | [Source Code](../../armsrc/Standalone/hf_tcprst.c) | [Development Guide](../../armsrc/Standalone/readme.md#developing-standalone-modes) + +--- + +## What + +Reads, simulates, dumps, and emulates IKEA Rothult NFC lock tags (ST25TA series). Extracts the 16-byte password stored on the tag, which acts as the key to the lock. + +## Why + +The IKEA Rothult is a battery-operated NFC lock that uses ST25TA02K tags (ISO 14443A, Type 4 tag). The lock authenticates by reading a specific NDEF record from the tag. By extracting this 16-byte password, you can clone the tag or emulate it — useful for creating backup keys or for security research on the lock mechanism. + +## How + +1. **READ**: Selects the ST25TA tag via ISO 14443A anticollision, sends NDEF SELECT commands, reads the NDEF file containing the 16-byte password. +2. **SIM**: Simulates a tag with the previously read UID (basic UID-level simulation). +3. **DUMP**: Outputs the extracted password over USB debug (requires client connection). +4. **EMUL**: Full tag emulation — responds to reader commands with the captured NDEF data including password. + +## LED Indicators + +| LED | Meaning | +|-----|---------| +| **A** (solid) | READ mode | +| **B** (solid) | SIM mode (UID simulation) | +| **C** (solid) | DUMP mode | +| **D** (solid) | EMUL mode (full emulation) | +| **A-D** (sequential) | Cycling through active mode indicator | + +## Button Controls + +| Action | Effect | +|--------|--------| +| **Single click** | Advance to next mode (READ→SIM→DUMP→EMUL→READ) | +| **Long hold** | Execute current mode action | + +## State Machine + +```mermaid +stateDiagram-v2 + [*] --> READ : Startup + + READ --> SIM : Button click + SIM --> DUMP : Button click + DUMP --> EMUL : Button click + EMUL --> READ : Button click + + state READ { + [*] --> WaitCard_R + WaitCard_R --> SelectTag : Tag detected + SelectTag --> ReadNDEF : ST25TA selected + ReadNDEF --> ExtractPwd : NDEF file read + ExtractPwd --> Done_R : 16-byte password stored + } + + state SIM { + [*] --> Simulating + Simulating --> Done_S : Button press stops sim + } + + state DUMP { + [*] --> PrintPwd + PrintPwd --> Done_D : Password printed to debug + } + + state EMUL { + [*] --> Emulating + Emulating --> Done_E : Button press stops emul + } + + READ --> [*] : USB connection + SIM --> [*] : USB connection + DUMP --> [*] : USB connection + EMUL --> [*] : USB connection +``` + +## Compilation + +``` +make clean +make STANDALONE=HF_TCPRST -j +./pm3-flash-fullimage +``` + +## Related + +- [ST25TB Tear-Off](hf_st25_tearoff.md) — Related ST25 family tag manipulation +- [MIFARE Classic Simulator](hf_mfcsim.md) — Another HF tag emulator diff --git a/doc/standalone/hf_tmudford.md b/doc/standalone/hf_tmudford.md new file mode 100644 index 000000000..1d70608e5 --- /dev/null +++ b/doc/standalone/hf_tmudford.md @@ -0,0 +1,74 @@ +# HF_TMUDFORD — ISO 15693 UID Emulator + +> **Author:** Tim Mudford +> **Frequency:** HF (13.56 MHz) +> **Hardware:** Generic Proxmark3 + +[Back to Standalone Modes Index](../../armsrc/Standalone/readme.md#individual-mode-documentation) | [Source Code](../../armsrc/Standalone/hf_tmudford.c) | [Development Guide](../../armsrc/Standalone/readme.md#developing-standalone-modes) + +--- + +## What + +Reads an ISO 15693 tag UID and emulates it. Simple two-state mode: read a tag, then replay its UID to 15693 readers. + +## Why + +ISO 15693 (iCODE, Tag-it, I-Code) tags are used in library systems, industrial automation, and access control. This mode provides a quick way to clone and emulate a 15693 tag's UID for testing readers and access systems that rely solely on UID-based identification. + +## How + +1. **READ**: Sends an ISO 15693 inventory request. When a tag responds, captures its 8-byte UID. +2. **EMULATE**: Emulates a 15693 tag with the captured UID, responding to inventory and select commands from a reader. + +## LED Indicators + +| LED | Meaning | +|-----|---------| +| **A+D** (solid) | READ mode — waiting for tag | +| **B+C** (solid) | EMULATE mode — replaying UID | +| **A-D** (sequential blink) | Transition / activity | + +## Button Controls + +| Action | Effect | +|--------|--------| +| **Long hold (≥1000ms)** | Switch between READ and EMULATE modes | +| **Button press** | Exit standalone mode (while idle) | + +## State Machine + +```mermaid +stateDiagram-v2 + [*] --> READ : Startup + + state READ { + [*] --> Inventory + Inventory --> Captured : Tag responds with UID + Captured --> Inventory : Continue scanning + } + + state EMULATE { + [*] --> SimUID + SimUID --> SimUID : Responding to readers + } + + READ --> EMULATE : Long hold\n(UID captured) + EMULATE --> READ : Long hold + + READ --> [*] : Button press / USB + EMULATE --> [*] : Button press / USB +``` + +## Compilation + +``` +make clean +make STANDALONE=HF_TMUDFORD -j +./pm3-flash-fullimage +``` + +## Related + +- [ISO 15693 Dump & Simulate](hf_15sim.md) — Full 15693 memory dump and emulation +- [ISO 15693 Sniffer](hf_15sniff.md) — Passive 15693 sniffing diff --git a/doc/standalone/hf_unisniff.md b/doc/standalone/hf_unisniff.md new file mode 100644 index 000000000..707011fcc --- /dev/null +++ b/doc/standalone/hf_unisniff.md @@ -0,0 +1,109 @@ +# HF_UNISNIFF — Multi-Protocol HF Sniffer + +> **Author:** Equip +> **Frequency:** HF (13.56 MHz) +> **Hardware:** RDV4 recommended (flash for config persistence) + +[Back to Standalone Modes Index](../../armsrc/Standalone/readme.md#individual-mode-documentation) | [Source Code](../../armsrc/Standalone/hf_unisniff.c) | [Development Guide](../../armsrc/Standalone/readme.md#developing-standalone-modes) + +--- + +## What + +A universal HF sniffer that supports multiple protocols — ISO 14443A, ISO 14443B, ISO 15693, and iCLASS — selectable at runtime via button press before sniffing begins. + +## Why + +Rather than flashing different standalone firmware for each protocol you want to sniff, this mode combines all four HF sniffing protocols into a single firmware. You select the protocol at startup using the button, then sniff. This is especially useful when you don't know which protocol a target system uses. + +## How + +1. **Protocol Selection**: On startup, the LEDs indicate the currently selected protocol. Press the button to cycle through protocols. +2. **Sniff**: Hold the button to start sniffing the selected protocol. The Proxmark3 passively captures RF traffic between a reader and tag. +3. **Data Storage**: If flash is available, captured trace data is saved to `hf_unisniff.trace`. Otherwise data is held in BigBuf (volatile — lost on power cycle). +4. **Retrieval**: Connect to the client and download the trace data for analysis. + +## LED Indicators — Protocol Selection + +| LED Pattern | Protocol | +|-------------|----------| +| **A** only | ISO 14443A | +| **B** only | ISO 14443B | +| **C** only | ISO 15693 | +| **D** only | iCLASS | + +## LED Indicators — Operation + +| LED | Meaning | +|-----|---------| +| **Selected LED** (blink) | Sniffing in progress | +| **A+B+C+D** (solid) | Error | + +## Button Controls + +| Action | Effect | +|--------|--------| +| **Single click** | Cycle to next protocol (A→B→C→D→A) | +| **Long hold** | Start sniffing selected protocol | +| **Press during sniff** | Stop sniffing | + +## Configuration File + +If flash is available, the mode reads `hf_unisniff.conf` to remember the last-used protocol. Format is a single byte: + +| Value | Protocol | +|-------|----------| +| `0x01` | ISO 14443A | +| `0x02` | ISO 14443B | +| `0x03` | ISO 15693 | +| `0x04` | iCLASS | + +## State Machine + +```mermaid +stateDiagram-v2 + [*] --> SelectProtocol : Startup + + state SelectProtocol { + [*] --> ISO14443A + ISO14443A --> ISO14443B : Button click + ISO14443B --> ISO15693 : Button click + ISO15693 --> iCLASS : Button click + iCLASS --> ISO14443A : Button click + } + + SelectProtocol --> Sniffing : Long hold + + state Sniffing { + [*] --> Capture + Capture --> Capture : Logging packets + } + + Sniffing --> SaveTrace : Button press / buffer full + SaveTrace --> SelectProtocol : Trace saved to flash + SaveTrace --> [*] : No flash (data in BigBuf) + + SelectProtocol --> [*] : USB connection +``` + +## Flash Files + +| File | Contents | +|------|----------| +| `hf_unisniff.conf` | Last-selected protocol (1 byte) | +| `hf_unisniff.trace` | Captured trace data | + +## Compilation + +``` +make clean +make STANDALONE=HF_UNISNIFF -j +./pm3-flash-fullimage +``` + +## Related + +- [ISO 14443A Sniffer](hf_14asniff.md) — Dedicated 14443A sniffer +- [ISO 14443B Sniffer](hf_14bsniff.md) — Dedicated 14443B sniffer +- [ISO 15693 Sniffer](hf_15sniff.md) — Dedicated 15693 sniffer +- [iCLASS](hf_iceclass.md) — iCLASS multi-mode (includes sniffing) diff --git a/doc/standalone/hf_young.md b/doc/standalone/hf_young.md new file mode 100644 index 000000000..26a8d24b2 --- /dev/null +++ b/doc/standalone/hf_young.md @@ -0,0 +1,86 @@ +# HF_YOUNG — MIFARE Classic Sniffer/Simulator (2-Bank) + +> **Author:** Craig Young +> **Frequency:** HF (13.56 MHz) +> **Hardware:** Generic Proxmark3 + +[Back to Standalone Modes Index](../../armsrc/Standalone/readme.md#individual-mode-documentation) | [Source Code](../../armsrc/Standalone/hf_young.c) | [Development Guide](../../armsrc/Standalone/readme.md#developing-standalone-modes) + +--- + +## What + +Sniffs MIFARE Classic 1K communications between a reader and card, then simulates or clones the captured data. Features two memory banks for storing different card captures. + +## Why + +MIFARE Classic is the most widely deployed contactless smart card. This mode enables field-based capture of reader-card transactions followed by immediate simulation or cloning — useful for testing access control systems and understanding their authentication sequences without needing a laptop. + +## How + +1. **RECORD**: Places the Proxmark3 in sniffer mode to capture ISO 14443A / MIFARE Classic communications. The captured UID, ATQA, SAK, and key data are stored in the selected bank. +2. **PLAY**: Emulates a MIFARE Classic card using the captured UID and data, responding to reader authentication requests. +3. **CLONE**: Writes captured data to a "magic" Gen1a MIFARE Classic card (one with a writable Block 0). + +Each of the two banks can independently store a captured card's data. + +## LED Indicators + +| LED | Meaning | +|-----|---------| +| **A** (solid) | Bank 0 selected | +| **B** (solid) | Bank 1 selected | +| **C** (solid) | RECORD mode | +| **D** (solid) | PLAY (simulate) mode | +| **C+D** (solid) | CLONE mode | +| **A-D** (blink) | Activity | + +## Button Controls + +| Action | Effect | +|--------|--------| +| **Single click** | Advance state: RECORD → PLAY → CLONE → RECORD | +| **Long hold** | Switch between Bank 0 and Bank 1 | + +## State Machine + +```mermaid +stateDiagram-v2 + [*] --> Bank0_RECORD : Startup + + state Bank0 { + Bank0_RECORD --> Bank0_PLAY : Click + Bank0_PLAY --> Bank0_CLONE : Click + Bank0_CLONE --> Bank0_RECORD : Click + } + + state Bank1 { + Bank1_RECORD --> Bank1_PLAY : Click + Bank1_PLAY --> Bank1_CLONE : Click + Bank1_CLONE --> Bank1_RECORD : Click + } + + Bank0_RECORD --> Bank1_RECORD : Long hold + Bank0_PLAY --> Bank1_PLAY : Long hold + Bank0_CLONE --> Bank1_CLONE : Long hold + Bank1_RECORD --> Bank0_RECORD : Long hold + Bank1_PLAY --> Bank0_PLAY : Long hold + Bank1_CLONE --> Bank0_CLONE : Long hold + + Bank0_RECORD --> [*] : USB connection + Bank1_RECORD --> [*] : USB connection +``` + +## Compilation + +``` +make clean +make STANDALONE=HF_YOUNG -j +./pm3-flash-fullimage +``` + +## Related + +- [MattyRun](hf_mattyrun.md) — Automated MFC key check, nested attack, dump, and emulate +- [CraftByte](hf_craftbyte.md) — 14443A UID stealer/emulator +- [MIFARE Classic Simulator](hf_mfcsim.md) — Multi-slot MFC simulator from flash dumps diff --git a/doc/standalone/lf_em4100emul.md b/doc/standalone/lf_em4100emul.md new file mode 100644 index 000000000..c5a794632 --- /dev/null +++ b/doc/standalone/lf_em4100emul.md @@ -0,0 +1,83 @@ +# LF_EM4100EMUL — EM4100 Simulator + +> **Author:** temskiy +> **Frequency:** LF (125 kHz) +> **Hardware:** Generic Proxmark3 + +[Back to Standalone Modes Index](../../armsrc/Standalone/readme.md#individual-mode-documentation) | [Source Code](../../armsrc/Standalone/lf_em4100emul.c) | [Development Guide](../../armsrc/Standalone/readme.md#developing-standalone-modes) + +--- + +## What + +Simulates a set of predefined EM4100 tag IDs in sequence. The Proxmark3 cycles through a list of hardcoded EM4100 IDs, broadcasting each one for a period before moving to the next. + +## Why + +This is useful when you already know the EM4100 IDs you want to replay and need a simple, standalone way to cycle through them without a host connection. Common scenarios: + +- **Testing access control readers**: Verify which IDs are accepted +- **Red team walk-through**: Pre-load known badges and cycle through them at doors +- **Development & debugging**: Verify your EM4100 reader code against known-good IDs + +## How + +1. The firmware contains a hardcoded array of EM4100 IDs +2. On startup, it selects the first slot and begins simulating +3. It automatically advances to the next slot after each simulation cycle +4. The device shows the current slot number via LED binary encoding + +The simulation uses Manchester encoding at the configured bit rate to emulate an EM4100 tag. + +## LED Indicators + +| LED | Meaning | +|-----|---------| +| **A/B/C/D** (binary) | Current slot number displayed in binary (LED A = bit 0, etc.) | +| All LEDs off | Idle / transitioning between slots | + +## Button Controls + +| Action | Effect | +|--------|--------| +| **Hold 500ms** | Exit standalone mode | +| **USB command** | Exit standalone mode | + +## State Machine + +```mermaid +stateDiagram-v2 + [*] --> SelectSlot0 : Startup + + SelectSlot0 --> Emulate0 : Auto + Emulate0 --> SelectSlot1 : Simulation complete + + SelectSlot1 --> Emulate1 : Auto + Emulate1 --> SelectSlot2 : Simulation complete + + SelectSlot2 --> Emulate2 : Auto + Emulate2 --> SelectSlot3 : Simulation complete + + SelectSlot3 --> Emulate3 : Auto + Emulate3 --> SelectSlot0 : Cycle back + + Emulate0 --> [*] : Button hold 500ms + Emulate1 --> [*] : Button hold 500ms + Emulate2 --> [*] : Button hold 500ms + Emulate3 --> [*] : Button hold 500ms +``` + +## Compilation + +``` +make clean +make STANDALONE=LF_EM4100EMUL -j +./pm3-flash-fullimage +``` + +## Related + +- [EM4100 RSWB](lf_em4100rswb.md) — Full read/sim/write/brute for EM4100 +- [EM4100 RSWW](lf_em4100rsww.md) — Read/sim/write/wipe/validate +- [EM4100 RWC](lf_em4100rwc.md) — Read/sim/clone with 16 slots +- [T5577 Introduction Guide](../T5577_Guide.md) — Background on T5577/EM4100 technology diff --git a/doc/standalone/lf_em4100rswb.md b/doc/standalone/lf_em4100rswb.md new file mode 100644 index 000000000..32b5322bf --- /dev/null +++ b/doc/standalone/lf_em4100rswb.md @@ -0,0 +1,90 @@ +# LF_EM4100RSWB — EM4100 Read/Simulate/Write/Brute + +> **Author:** Monster1024 +> **Frequency:** LF (125 kHz) +> **Hardware:** RDV4 (requires flash memory for slot storage) + +[Back to Standalone Modes Index](../../armsrc/Standalone/readme.md#individual-mode-documentation) | [Source Code](../../armsrc/Standalone/lf_em4100rswb.c) | [Development Guide](../../armsrc/Standalone/readme.md#developing-standalone-modes) + +--- + +## What + +A full-featured EM4100 attack mode with four operations: **read**, **simulate**, **write to T55x7**, and **brute force**. Supports 4 card storage slots with flash persistence on RDV4 hardware. + +## Why + +This is the most versatile EM4100 standalone mode. While simpler modes only read or simulate, RSWB combines all four essential operations — plus brute forcing — into a single firmware image. This is ideal for: + +- **Pentesting EM4100 access control**: Read a badge, simulate it, then write a clone, all standalone +- **Brute force attacks**: Iterate through card numbers to find valid ones when you don't have a known-good badge +- **Multi-target assessments**: Store up to 4 different badges and switch between them on-site + +## How + +1. **READ mode**: Listens for EM4100 cards. On successful read, stores the ID in the current slot and automatically transitions to SIM mode +2. **SIM mode**: Broadcasts the stored EM4100 ID. Button press moves to WRITE mode +3. **WRITE mode**: Writes the stored ID to a T55x7 blank card placed on the antenna +4. **BRUTE mode**: Sequentially transmits incrementing card numbers. Double-click saves a working number; hold changes brute speed + +Slot data persists across reboots via the RDV4's SPI flash memory. + +## LED Indicators + +| LED | Meaning | +|-----|---------| +| **A, B** (binary) | Current mode: 00=READ, 01=SIM, 10=WRITE, 11=BRUTE | +| **C, D** (binary) | Current slot: 00=Slot1, 01=Slot2, 10=Slot3, 11=Slot4 | +| All flash on operation | Success confirmation | + +## Button Controls + +| Context | Action | Effect | +|---------|--------|--------| +| Any mode | **Single click** | Switch mode (READ → SIM → WRITE → BRUTE → READ) | +| Any mode | **Hold** | Switch slot (1 → 2 → 3 → 4 → 1) | +| BRUTE | **Single click** | Exit brute mode → READ | +| BRUTE | **Double-click** | Save current brute position | +| BRUTE | **Hold** | Change brute speed | + +## State Machine + +```mermaid +stateDiagram-v2 + [*] --> READ : Startup (load slots from flash) + + READ --> SIM : Card read successfully\n(auto-transition) + READ --> SIM : Single click + SIM --> WRITE : Single click + WRITE --> BRUTE : Single click + BRUTE --> READ : Single click\nor brute complete + + READ --> READ : Hold (switch slot) + SIM --> SIM : Hold (switch slot) + WRITE --> WRITE : Hold (switch slot) + BRUTE --> BRUTE : Hold (change speed)\nDouble-click (save position) + + READ --> [*] : USB data received + SIM --> [*] : USB data received +``` + +## Flash Storage + +- Slot data is saved to and loaded from the RDV4 SPI flash +- 4 slots, each storing the raw EM4100 ID +- Data persists across power cycles + +## Compilation + +``` +make clean +make STANDALONE=LF_EM4100RSWB -j +./pm3-flash-fullimage +``` + +## Related + +- [EM4100 Emulator](lf_em4100emul.md) — Simple predefined EM4100 simulator +- [EM4100 RSWW](lf_em4100rsww.md) — Read/sim/write/wipe/validate variant +- [EM4100 RWC](lf_em4100rwc.md) — 16-slot read/sim/clone +- [T5577 Introduction Guide](../T5577_Guide.md) — Background on T5577/EM4100 diff --git a/doc/standalone/lf_em4100rsww.md b/doc/standalone/lf_em4100rsww.md new file mode 100644 index 000000000..3fb953a7f --- /dev/null +++ b/doc/standalone/lf_em4100rsww.md @@ -0,0 +1,89 @@ +# LF_EM4100RSWW — EM4100 Read/Simulate/Write/Wipe/Validate + +> **Author:** Łukasz "zabszk" Jurczyk +> **Frequency:** LF (125 kHz) +> **Hardware:** RDV4 (requires flash memory) + +[Back to Standalone Modes Index](../../armsrc/Standalone/readme.md#individual-mode-documentation) | [Source Code](../../armsrc/Standalone/lf_em4100rsww.c) | [Development Guide](../../armsrc/Standalone/readme.md#developing-standalone-modes) + +--- + +## What + +An EM4100 multi-tool that adds **wipe** and **validate** operations on top of read/simulate/write. Automatically saves read IDs to flash memory for persistence across power cycles. + +## Why + +Unlike RSWB which focuses on brute forcing, RSWW focuses on the **clone verification workflow**: read a tag, write it to a T55x7, then validate the clone reads back correctly. The wipe function lets you reset T55x7 cards to a blank state. This is the mode to use when: + +- **Quality-checking clones**: Verify the written data matches the original +- **Preparing blank cards**: Wipe T55x7 cards back to factory state +- **Field work with persistence**: Read IDs survive reboots via flash + +## How + +1. **READ**: Listens for EM4100 tags and stores the ID to flash +2. **EMULATE**: Broadcasts the stored ID (defaults to this mode if flash has data from a previous session) +3. **WRITE**: Writes the stored ID to a T55x7 tag +4. **VALIDATE**: Reads back a T55x7 and compares it to the stored ID to confirm a successful clone +5. **WIPE**: Resets a T55x7 tag to its default (empty) configuration + +## LED Indicators + +| LED | Meaning | +|-----|---------| +| **A** (solid) | READ mode active | +| **B** (solid) | EMULATE mode active | +| **C** (solid) | VALIDATE mode active | +| **D** (solid) | WIPE mode active | +| Blink pattern | Success/failure indication after operations | + +## Button Controls + +| Action | Effect | +|--------|--------| +| **Single click** | Advance mode (READ → EMULATE → WRITE/VALIDATE) | +| **Hold** | Toggle between READ and EMULATE | +| **Double-click in READ** | Enter WIPE mode | +| **USB command** | Exit standalone mode | + +## State Machine + +```mermaid +stateDiagram-v2 + [*] --> READ : Startup (no flash data) + [*] --> EMULATE : Startup (flash has saved ID) + + READ --> EMULATE : Single click\n(ID captured) + EMULATE --> READ : Hold (toggle) + + EMULATE --> WRITE : Single click + WRITE --> VALIDATE : Auto after write + VALIDATE --> EMULATE : Validation complete + + READ --> WIPE : Double-click + WIPE --> READ : Wipe complete + + READ --> [*] : USB data received + EMULATE --> [*] : USB data received +``` + +## Flash Storage + +- Automatically saves the most recent read ID to SPI flash +- Loads stored ID on startup; if found, starts in EMULATE mode +- One slot for persistent storage + +## Compilation + +``` +make clean +make STANDALONE=LF_EM4100RSWW -j +./pm3-flash-fullimage +``` + +## Related + +- [EM4100 RSWB](lf_em4100rswb.md) — 4-slot variant with brute force +- [EM4100 Emulator](lf_em4100emul.md) — Simple hardcoded EM4100 simulator +- [EM4100 RWC](lf_em4100rwc.md) — 16-slot read/sim/clone diff --git a/doc/standalone/lf_em4100rwc.md b/doc/standalone/lf_em4100rwc.md new file mode 100644 index 000000000..e1dc7fff1 --- /dev/null +++ b/doc/standalone/lf_em4100rwc.md @@ -0,0 +1,79 @@ +# LF_EM4100RWC — EM4100 Read/Write/Clone (16 Slots) + +> **Author:** temskiy +> **Frequency:** LF (125 kHz) +> **Hardware:** RDV4 (flash memory for slot storage) + +[Back to Standalone Modes Index](../../armsrc/Standalone/readme.md#individual-mode-documentation) | [Source Code](../../armsrc/Standalone/lf_em4100rwc.c) | [Development Guide](../../armsrc/Standalone/readme.md#developing-standalone-modes) + +--- + +## What + +Read, simulate, and clone EM4100 tags with **16 storage slots**. Pre-loaded with 3 sample IDs. Provides the largest card storage capacity of any EM4100 standalone mode. + +## Why + +When you need to collect and manage many EM4100 IDs on a single assessment — for example, reading badges from multiple employees — having 16 slots lets you capture a full team's credentials on-device. Each slot is independently selectable for simulation or cloning. + +## How + +1. **SELECT**: Navigate between the 16 slots using button clicks +2. **READ**: Read an EM4100 tag and store it in the currently selected slot +3. **SIMULATE**: Broadcast the selected slot's ID +4. **WRITE**: Clone the selected slot's ID to a T5555 card + +The mode cycles through these four states with button holds to switch modes and clicks to execute within a mode. + +## LED Indicators + +| LED | Meaning | +|-----|---------| +| **A/B/C/D** (binary) | Slot number in binary (0–15) | +| Blink patterns | Operation success/failure | + +## Button Controls + +| State | Action | Effect | +|-------|--------|--------| +| SELECT | **Single click** | Next slot | +| SELECT | **Hold** | Switch to SIMULATE mode | +| READ | **Single click** | Read tag into current slot | +| READ | **Hold** | Switch to WRITE mode | +| SIMULATE | **Single click** | Start simulation | +| SIMULATE | **Hold** | Switch to READ mode | +| WRITE | **Single click** | Write current slot to T5555 | +| WRITE | **Hold** | Switch to SELECT mode | + +## State Machine + +```mermaid +stateDiagram-v2 + [*] --> SELECT : Startup + + SELECT --> SIMULATE : Hold + SIMULATE --> READ : Hold + READ --> WRITE : Hold + WRITE --> SELECT : Hold + + SELECT --> SELECT : Click (next slot) + READ --> READ : Click (read tag) + SIMULATE --> SIMULATE : Click (simulate) + WRITE --> WRITE : Click (write) + + SELECT --> [*] : USB data received +``` + +## Compilation + +``` +make clean +make STANDALONE=LF_EM4100RWC -j +./pm3-flash-fullimage +``` + +## Related + +- [EM4100 RSWB](lf_em4100rswb.md) — 4-slot variant with brute force +- [EM4100 Emulator](lf_em4100emul.md) — Simple predefined simulator +- [EM4100 RSWW](lf_em4100rsww.md) — Read/sim/write/wipe/validate diff --git a/doc/standalone/lf_hidbrute.md b/doc/standalone/lf_hidbrute.md new file mode 100644 index 000000000..85334e973 --- /dev/null +++ b/doc/standalone/lf_hidbrute.md @@ -0,0 +1,82 @@ +# LF_HIDBRUTE — HID Corporate 1000 Bruteforce + +> **Authors:** Federico Dotta & Maurizio Agazzini +> **Frequency:** LF (125 kHz) +> **Hardware:** Generic Proxmark3 + +[Back to Standalone Modes Index](../../armsrc/Standalone/readme.md#individual-mode-documentation) | [Source Code](../../armsrc/Standalone/lf_hidbrute.c) | [Development Guide](../../armsrc/Standalone/readme.md#developing-standalone-modes) + +--- + +## What + +Reads an HID Corporate 1000 (35-bit) card, then brute forces the card number up/down from the captured value while preserving the facility code. Also supports direct simulation and cloning. + +## Why + +HID Corporate 1000 uses a 35-bit format with a facility code and card number. If you can read one card, you likely know the facility code for that site. By brute forcing the card number, you can test other valid badge numbers in the same facility — for example, finding an admin badge number when you only have a standard user badge. + +Use cases: +- **Privilege escalation**: Find higher-privilege card numbers in the same facility +- **Adjacent badge discovery**: Walk through card numbers near a known-good badge +- **Access control testing**: Verify whether sequential card numbers are provisioned + +## How + +1. **Record**: Read an HID Corporate 1000 card to capture facility code + card number +2. **Clone**: Write the captured credentials to a T55x7 card +3. **Brute**: Simulate incrementing/decrementing card numbers with the same facility code + +The brute force iterates the card number portion while keeping the facility code constant from the originally recorded card. + +## LED Indicators + +| LED | Meaning | +|-----|---------| +| **A** (solid) | Slot 0 selected / cloning active | +| **B** (solid) | Slot 1 selected / simulation active | +| **C** (solid) | Slot 2 selected | +| **D** (solid) | Status indicator | +| LED(slot+1) | Indicates currently active slot during recording | + +## Button Controls + +| Action | Effect | +|--------|--------| +| **Hold 280ms** | Advance state (select → record → clone/brute → repeat) | +| **USB command** | Exit standalone mode | + +## State Machine + +```mermaid +stateDiagram-v2 + [*] --> SlotSelect : Startup + + SlotSelect --> Record : Button hold (280ms) + Record --> Clone : Button hold (card captured) + Record --> Record : Button hold (no card) + + Clone --> BruteForce : Button hold + BruteForce --> Record : Button hold (exit brute) + + Clone --> SlotSelect : Operation complete + + SlotSelect --> [*] : USB data received + Record --> [*] : USB data received +``` + +## Compilation + +``` +make clean +make STANDALONE=LF_HIDBRUTE -j +./pm3-flash-fullimage +``` + +## Related + +- [SamyRun](lf_samyrun.md) — HID26 read/clone/simulate +- [HID FC Brute](lf_hidfcbrute.md) — Brute force HID facility codes +- [ProxBrute](lf_proxbrute.md) — HID ProxII card number brute force +- [Prox2Brute](lf_prox2brute.md) — ProxII brute force v2 +- [HID Downgrade Attacks](../hid_downgrade.md) — Reader downgrade methods diff --git a/doc/standalone/lf_hidfcbrute.md b/doc/standalone/lf_hidfcbrute.md new file mode 100644 index 000000000..719620e9f --- /dev/null +++ b/doc/standalone/lf_hidfcbrute.md @@ -0,0 +1,92 @@ +# LF_HIDFCBRUTE — HID Facility Code Bruteforce + +> **Author:** ss23 +> **Frequency:** LF (125 kHz) +> **Hardware:** RDV4 (requires flash for logging) + +[Back to Standalone Modes Index](../../armsrc/Standalone/readme.md#individual-mode-documentation) | [Source Code](../../armsrc/Standalone/lf_hidfcbrute.c) | [Development Guide](../../armsrc/Standalone/readme.md#developing-standalone-modes) + +--- + +## What + +Brute forces all 256 possible HID facility codes (0–255) using a static card number of 1, and logs results to flash memory. When a reader accepts a facility code, you can mark it with a button press. + +## Why + +HID 26-bit (H10301) cards encode a facility code (FC, 0–255) and a card number (CN, 0–65535). If you don't have a valid card to clone, but you do have physical access to a reader, you can discover the correct facility code by trying all 256 possibilities. Once you know the FC, you can pair it with brute-forced card numbers. + +This is the first step in a blind HID attack when you have no captured credentials. + +## How + +1. The mode simulates an HID 26-bit tag with FC cycling from 0 to 255 and CN fixed at 1 +2. During each simulation, LED C toggles to show progress +3. Press the button to log the current FC to `lf_hid_fcbrute.log` on flash (this is your "the door opened" marker) +4. Hold the button for 1 second to exit +5. After completion, retrieve the log file via the client + +## LED Indicators + +| LED | Meaning | +|-----|---------| +| **A** (solid) | Brute force active | +| **C** (toggling) | Toggles on each FC attempt — visual progress indicator | +| **D** (solid) | Simulation transmission active | + +## Button Controls + +| Action | Effect | +|--------|--------| +| **Click / hold 10ms** | Begin brute force | +| **Single click** (during brute) | Log current FC to flash file | +| **Hold ≥ 1 second** | Exit brute force | +| **USB command** | Exit standalone mode | + +## State Machine + +```mermaid +stateDiagram-v2 + [*] --> WaitStart : Startup + + WaitStart --> BruteFC0 : Button press + + BruteFC0 --> BruteFC1 : Simulate FC=0, advance + BruteFC1 --> BruteFC2 : Simulate FC=1, advance + BruteFC2 --> BruteFC_N : ... + BruteFC_N --> BruteFC255 : Simulate FC=N, advance + BruteFC255 --> Complete : All FCs tested + + BruteFC_N --> LogFC : Button click\n(door opened!) + LogFC --> BruteFC_N : Continue brute + + BruteFC_N --> [*] : Button hold ≥ 1s + Complete --> [*] : Done + + note right of LogFC + Writes current FC + to lf_hid_fcbrute.log + on SPI flash + end note +``` + +## Flash Storage + +- **Log file**: `lf_hid_fcbrute.log` on SPI flash +- Contains facility codes that were manually marked via button press +- Retrieve with the client after the assessment + +## Compilation + +``` +make clean +make STANDALONE=LF_HIDFCBRUTE -j +./pm3-flash-fullimage +``` + +## Related + +- [HID Corporate Brute](lf_hidbrute.md) — Brute force card numbers with known FC +- [SamyRun](lf_samyrun.md) — Read/clone/sim HID26 +- [ProxBrute](lf_proxbrute.md) — HID ProxII brute force +- [IceHID Collector](lf_icehid.md) — Passive HID credential collection diff --git a/doc/standalone/lf_icehid.md b/doc/standalone/lf_icehid.md new file mode 100644 index 000000000..4a0a8d47b --- /dev/null +++ b/doc/standalone/lf_icehid.md @@ -0,0 +1,96 @@ +# LF_ICEHID — Multi-Format LF Credential Collector + +> **Author:** Iceman +> **Frequency:** LF (125 kHz) +> **Hardware:** RDV4 (requires flash memory and battery) + +[Back to Standalone Modes Index](../../armsrc/Standalone/readme.md#individual-mode-documentation) | [Source Code](../../armsrc/Standalone/lf_icehid.c) | [Development Guide](../../armsrc/Standalone/readme.md#developing-standalone-modes) + +--- + +## What + +A passive LF credential collector that continuously listens for **HID, IOprox, AWID, and EM4100** cards and logs all captured credentials to flash memory. Runs unattended — just power on and leave it. + +## Why + +This is the ultimate "drop and collect" standalone mode. Place a powered Proxmark3 RDV4 (with battery) near a high-traffic area (like a card reader or badge check-in point) and it silently captures every LF credential that comes into range. Unlike single-protocol modes, IceHID tries all four common LF formats on every signal, catching whatever cards employees are carrying. + +Use cases: +- **Passive badge collection**: Covert long-duration credential harvesting +- **Multi-format sites**: Sites using a mix of HID, AWID, IOprox, and EM4100 +- **Physical penetration testing**: Leave device near a turnstile, collect badges over hours + +## How + +1. The Proxmark3 continuously samples the LF antenna +2. On each sample, it attempts demodulation in four formats: HID → AWID → IOprox → EM4100 +3. If any format decodes successfully, the credential is logged to `lf_hidcollect.log` on flash +4. The cycle repeats indefinitely until the button is held or USB data is received + +The multi-format approach uses `ASKDemod()` and protocol-specific decoders in sequence. + +## LED Indicators + +| LED | Meaning | +|-----|---------| +| **A** (solid) | Reading / recording LF signal | +| **B** (solid) | Writing captured data to flash | +| **C** (solid) | Unmounting / syncing flash filesystem | + +## Button Controls + +| Action | Effect | +|--------|--------| +| **Hold 280ms** | Exit standalone mode | +| **USB command** | Exit standalone mode | + +## State Machine + +```mermaid +stateDiagram-v2 + [*] --> Sample : Startup + + Sample --> DemodHID : LF signal detected + DemodHID --> LogFlash : HID decoded + DemodHID --> DemodAWID : HID failed + + DemodAWID --> LogFlash : AWID decoded + DemodAWID --> DemodIO : AWID failed + + DemodIO --> LogFlash : IOprox decoded + DemodIO --> DemodEM : IOprox failed + + DemodEM --> LogFlash : EM4100 decoded + DemodEM --> Sample : All decoders failed + + LogFlash --> Sample : Logged, continue + + Sample --> [*] : Button hold / USB data + + note right of Sample + Continuous loop + Tries 4 demod formats + per signal capture + end note +``` + +## Flash Storage + +- **Log file**: `lf_hidcollect.log` on SPI flash +- Each entry contains the decoded credential data and format type +- Retrieve with client: `mem spiffs dump -s lf_hidcollect.log -d lf_hidcollect.log` + +## Compilation + +``` +make clean +make STANDALONE=LF_ICEHID -j +./pm3-flash-fullimage +``` + +## Related + +- [NexID Collector](lf_nexid.md) — Similar collector for Nexwatch credentials +- [SamyRun](lf_samyrun.md) — Active HID read/clone/sim +- [HID FC Brute](lf_hidfcbrute.md) — Active HID facility code brute force diff --git a/doc/standalone/lf_multihid.md b/doc/standalone/lf_multihid.md new file mode 100644 index 000000000..01bb1b82f --- /dev/null +++ b/doc/standalone/lf_multihid.md @@ -0,0 +1,81 @@ +# LF_MULTIHID — HID 26-Bit Multi-Card Simulator + +> **Author:** Shain Lakin +> **Frequency:** LF (125 kHz) +> **Hardware:** Generic Proxmark3 + +[Back to Standalone Modes Index](../../armsrc/Standalone/readme.md#individual-mode-documentation) | [Source Code](../../armsrc/Standalone/lf_multihid.c) | [Development Guide](../../armsrc/Standalone/readme.md#developing-standalone-modes) + +--- + +## What + +Cycles through 4 predefined HID 26-bit (H10301) raw card IDs, simulating each one in sequence automatically. + +## Why + +When you have multiple known-valid HID credentials and want to try them all at a reader without manual intervention. Edit the source with your target IDs, compile, and the device will automatically cycle through each one at the reader. + +## How + +1. The firmware contains 4 hardcoded raw HID values +2. On startup, it selects the first slot and begins simulating +3. After each simulation interval, it advances to the next slot +4. The cycle repeats continuously until exit + +## LED Indicators + +| LED | Meaning | +|-----|---------| +| **A/B/C/D** (binary) | Currently selected slot number | + +## Button Controls + +| Action | Effect | +|--------|--------| +| **USB command** | Exit standalone mode | + +## State Machine + +```mermaid +stateDiagram-v2 + [*] --> Sim_Slot0 : Startup + + Sim_Slot0 --> Sim_Slot1 : Auto-advance + Sim_Slot1 --> Sim_Slot2 : Auto-advance + Sim_Slot2 --> Sim_Slot3 : Auto-advance + Sim_Slot3 --> Sim_Slot0 : Auto-advance (cycle) + + Sim_Slot0 --> [*] : USB data received + Sim_Slot1 --> [*] : USB data received + Sim_Slot2 --> [*] : USB data received + Sim_Slot3 --> [*] : USB data received +``` + +## Customization + +Edit the raw ID array in the source code before compiling: + +```c +// Example: change these to your target IDs +static const uint32_t ids[] = { + 0x2006EC0C86, // Slot 0 + 0x2006EC0C87, // Slot 1 + 0x2006EC0C88, // Slot 2 + 0x2006EC0C89, // Slot 3 +}; +``` + +## Compilation + +``` +make clean +make STANDALONE=LF_MULTIHID -j +./pm3-flash-fullimage +``` + +## Related + +- [SamyRun](lf_samyrun.md) — Read/clone/simulate single HID26 +- [HID Corporate Brute](lf_hidbrute.md) — Brute force card numbers +- [IceHID Collector](lf_icehid.md) — Passive multi-format collector diff --git a/doc/standalone/lf_nedap_sim.md b/doc/standalone/lf_nedap_sim.md new file mode 100644 index 000000000..c59e2c3d0 --- /dev/null +++ b/doc/standalone/lf_nedap_sim.md @@ -0,0 +1,74 @@ +# LF_NEDAP_SIM — Nedap RFID Simple Simulator + +> **Frequency:** LF (125 kHz) +> **Hardware:** Generic Proxmark3 + +[Back to Standalone Modes Index](../../armsrc/Standalone/readme.md#individual-mode-documentation) | [Source Code](../../armsrc/Standalone/lf_nedap_sim.c) | [Development Guide](../../armsrc/Standalone/readme.md#developing-standalone-modes) + +--- + +## What + +Simulates a Nedap RFID tag with a hardcoded ID. Supports both 64-bit and 128-bit Nedap formats, including proper CRC and framing. + +## Why + +Nedap is a less common but still-deployed access control system, particularly in Europe. This mode lets you test Nedap readers by simulating a known tag. Since Nedap cards are less commonly available than HID, having a simulator is valuable for testing. + +## How + +1. The firmware encodes a hardcoded Nedap tag structure (subType, customerCode, id) +2. It generates the proper bit sequence with CRC calculation +3. Continuously transmits the encoded tag via LF modulation +4. Supports 128-bit "long" format when `isLong=1` + +Default hardcoded values: `subType=5`, `customerCode=0x123`, `id=42424`, `isLong=1` + +## LED Indicators + +| LED | Meaning | +|-----|---------| +| Minimal LED usage | Simple continuous simulation mode | + +## Button Controls + +| Action | Effect | +|--------|--------| +| **Hold 1000ms** | Exit standalone mode | +| **USB command** | Exit standalone mode | + +## State Machine + +```mermaid +stateDiagram-v2 + [*] --> Simulate : Startup + + Simulate --> Simulate : Continuous transmission + Simulate --> [*] : Button hold 1s / USB data +``` + +## Customization + +To change the simulated tag, edit the hardcoded values in the source: + +```c +static NedapTag_t tag = { + .subType = 0x5, + .customerCode = 0x123, + .id = 42424, + .isLong = 1, +}; +``` + +## Compilation + +``` +make clean +make STANDALONE=LF_NEDAP_SIM -j +./pm3-flash-fullimage +``` + +## Related + +- [EM4100 Emulator](lf_em4100emul.md) — Simple EM4100 simulator +- [Skeleton Template](lf_skeleton.md) — Template for building new LF modes diff --git a/doc/standalone/lf_nexid.md b/doc/standalone/lf_nexid.md new file mode 100644 index 000000000..37ae11cfa --- /dev/null +++ b/doc/standalone/lf_nexid.md @@ -0,0 +1,77 @@ +# LF_NEXID — Nexwatch Credential Collector + +> **Authors:** jrjgjk & Zolorah +> **Frequency:** LF (125 kHz) +> **Hardware:** RDV4 (requires flash for logging) + +[Back to Standalone Modes Index](../../armsrc/Standalone/readme.md#individual-mode-documentation) | [Source Code](../../armsrc/Standalone/lf_nexid.c) | [Development Guide](../../armsrc/Standalone/readme.md#developing-standalone-modes) + +--- + +## What + +Passively sniffs and logs Nexwatch/NexKey ID credentials to flash memory. Decodes the magic bytes and mode information from each captured card. + +## Why + +Nexwatch (by Honeywell) is an access control card format found in commercial buildings. This collector silently harvests Nexwatch credentials over time, analogous to [IceHID](lf_icehid.md) but specifically targeting the Nexwatch protocol with full decode information. + +## How + +1. Continuously samples the LF antenna using PSK demodulation +2. Attempts Nexwatch-specific decode on each signal burst +3. On successful decode, extracts the magic bytes, mode, and ID +4. Logs the decoded credential to `lf_nexcollect.log` on flash +5. Repeats until button hold or USB exit + +## LED Indicators + +| LED | Meaning | +|-----|---------| +| **A** (solid) | Reading / recording LF signal | +| **B** (solid) | Writing to flash | +| **C** (solid) | Unmounting / syncing flash | + +## Button Controls + +| Action | Effect | +|--------|--------| +| **Hold 280ms** | Exit standalone mode | +| **USB command** | Exit standalone mode | + +## State Machine + +```mermaid +stateDiagram-v2 + [*] --> Sample : Startup + + Sample --> PSKDemod : LF signal detected + PSKDemod --> NexDecode : PSK decoded + PSKDemod --> Sample : PSK decode failed + + NexDecode --> LogFlash : Nexwatch ID found + NexDecode --> Sample : Not Nexwatch + + LogFlash --> Sample : Logged, continue + + Sample --> [*] : Button hold / USB data +``` + +## Flash Storage + +- **Log file**: `lf_nexcollect.log` on SPI flash +- Each entry contains decoded Nexwatch credentials with magic bytes and mode +- Retrieve with: `mem spiffs dump -s lf_nexcollect.log -d lf_nexcollect.log` + +## Compilation + +``` +make clean +make STANDALONE=LF_NEXID -j +./pm3-flash-fullimage +``` + +## Related + +- [IceHID Collector](lf_icehid.md) — Multi-format LF collector (HID/AWID/IO/EM) +- [Tharexde EM4x50](lf_tharexde.md) — EM4x50 collector diff --git a/doc/standalone/lf_prox2brute.md b/doc/standalone/lf_prox2brute.md new file mode 100644 index 000000000..f6730ad05 --- /dev/null +++ b/doc/standalone/lf_prox2brute.md @@ -0,0 +1,96 @@ +# LF_PROX2BRUTE — HID ProxII Bruteforce v2 + +> **Author:** Yann Gascuel +> **Frequency:** LF (125 kHz) +> **Hardware:** Generic Proxmark3 + +[Back to Standalone Modes Index](../../armsrc/Standalone/readme.md#individual-mode-documentation) | [Source Code](../../armsrc/Standalone/lf_prox2brute.c) | [Development Guide](../../armsrc/Standalone/readme.md#developing-standalone-modes) + +--- + +## What + +An improved version of [ProxBrute](lf_proxbrute.md) that brute forces HID ProxII H10301 26-bit card numbers over a **configurable range** with compile-time settings for facility code, start number, and end number. + +## Why + +The original ProxBrute requires reading a card first and only goes downward. Prox2Brute lets you pre-configure the exact facility code and card number range to test — no need to capture a card first. It's faster because it's purpose-built for the H10301 26-bit format with optimized timing between attempts. + +Use this when: +- You already know the facility code (from reconnaissance or a previous capture) +- You want to test a specific card number range +- You need faster iteration than the original ProxBrute + +## How + +1. Configure the target parameters at compile time via `#define` directives: `FACILITY_CODE`, `CARDNUM_START`, `CARDNUM_END` +2. On startup, press the button to begin +3. The device iterates through each card number, simulating the HID 26-bit format +4. LEDs cycle in binary to show progress +5. Hold button for 1 second to exit + +## LED Indicators + +| LED | Meaning | +|-----|---------| +| **A** | Inverts every attempt — fast blink = running | +| **B** | Inverts every 8 attempts | +| **C** | Inverts every 16 attempts | +| **D** | Inverts every 32 attempts — slow blink = progress | +| **D** (initial) | Waiting for button press to start | +| **C** (initial) | Ready indicator | + +The LED pattern creates a visual binary counter showing brute force progress at a glance. + +## Button Controls + +| Action | Effect | +|--------|--------| +| **Hold 200ms** | Start brute force | +| **Hold ≥ 1 second** (during brute) | Exit brute force | +| **USB command** | Exit standalone mode | + +## State Machine + +```mermaid +stateDiagram-v2 + [*] --> WaitStart : Startup\nLED_D on + + WaitStart --> BruteLoop : Button hold 200ms\nLED_C on (ready) + + BruteLoop --> BruteLoop : Simulate CN++\nLEDs show binary progress + BruteLoop --> Complete : CN > CARDNUM_END + + BruteLoop --> [*] : Button hold ≥ 1s + Complete --> [*] : All numbers tested + + note right of BruteLoop + FC = FACILITY_CODE (compile-time) + CN iterates CARDNUM_START → CARDNUM_END + LEDs A/B/C/D show binary counter + end note +``` + +## Compile-Time Configuration + +Edit the `#define` values in the source code before compiling: + +```c +#define FACILITY_CODE 111 // Target facility code +#define CARDNUM_START 1 // First card number to try +#define CARDNUM_END 65535 // Last card number to try +``` + +## Compilation + +``` +make clean +make STANDALONE=LF_PROX2BRUTE -j +./pm3-flash-fullimage +``` + +## Related + +- [ProxBrute](lf_proxbrute.md) — Original ProxII brute (reads card first, goes downward) +- [HID Corporate Brute](lf_hidbrute.md) — Corporate 1000 format brute force +- [HID FC Brute](lf_hidfcbrute.md) — Facility code brute force diff --git a/doc/standalone/lf_proxbrute.md b/doc/standalone/lf_proxbrute.md new file mode 100644 index 000000000..a7ccbb04e --- /dev/null +++ b/doc/standalone/lf_proxbrute.md @@ -0,0 +1,71 @@ +# LF_PROXBRUTE — HID ProxII Bruteforce + +> **Author:** Brad Antoniewicz +> **Frequency:** LF (125 kHz) +> **Hardware:** Generic Proxmark3 + +[Back to Standalone Modes Index](../../armsrc/Standalone/readme.md#individual-mode-documentation) | [Source Code](../../armsrc/Standalone/lf_proxbrute.c) | [Development Guide](../../armsrc/Standalone/readme.md#developing-standalone-modes) + +--- + +## What + +Reads an HID ProxII tag, then brute forces all card numbers **downward** from the captured value, keeping the same facility code. + +## Why + +HID ProxII is one of the most widely deployed access control card formats. If you have one valid card, you can enumerate other valid card numbers by brute forcing downward (most organizations assign card numbers sequentially, so badges with lower numbers often belong to employees with longer tenure or higher access). + +## How + +1. **READ**: Capture an HID ProxII card to learn the facility code and starting card number +2. **BRUTE**: Simulate the card with decrementing card numbers, pausing briefly at each one +3. The facilty code is preserved from the original capture +4. Hold button during brute to exit back to READ + +## LED Indicators + +| LED | Meaning | +|-----|---------| +| **A** (solid) | Reading / simulation active | +| **C** (solid) | Brute force mode | +| **A+B+C+D** (flash) | Error or exiting | + +## Button Controls + +| Action | Effect | +|--------|--------| +| **Hold 280ms** | Advance state (READ → BRUTE) | +| **Hold during brute** | Exit brute → back to READ | +| **USB command** | Exit standalone mode | + +## State Machine + +```mermaid +stateDiagram-v2 + [*] --> READ : Startup + + READ --> BRUTE : Button hold\n(card captured) + READ --> READ : Button hold\n(no card) + + BRUTE --> BRUTE : Decrement card number\nand simulate + BRUTE --> READ : Button hold\n(exit brute) + + READ --> [*] : USB data received + BRUTE --> [*] : USB data received +``` + +## Compilation + +``` +make clean +make STANDALONE=LF_PROXBRUTE -j +./pm3-flash-fullimage +``` + +## Related + +- [Prox2Brute](lf_prox2brute.md) — Faster, configurable ProxII brute force v2 +- [HID Corporate Brute](lf_hidbrute.md) — Corporate 1000 brute force +- [SamyRun](lf_samyrun.md) — HID26 read/clone/sim +- [HID FC Brute](lf_hidfcbrute.md) — Facility code brute force diff --git a/doc/standalone/lf_samyrun.md b/doc/standalone/lf_samyrun.md new file mode 100644 index 000000000..57b62268d --- /dev/null +++ b/doc/standalone/lf_samyrun.md @@ -0,0 +1,95 @@ +# LF_SAMYRUN — HID26 Read/Clone/Simulate + +> **Author:** Samy Kamkar +> **Frequency:** LF (125 kHz) +> **Hardware:** Generic Proxmark3 (no special requirements) +> **Default mode:** Yes — this is the factory-default standalone mode + +[Back to Standalone Modes Index](../../armsrc/Standalone/readme.md#individual-mode-documentation) | [Source Code](../../armsrc/Standalone/lf_samyrun.c) | [Development Guide](../../armsrc/Standalone/readme.md#developing-standalone-modes) + +--- + +## What + +SamyRun reads an HID 26-bit (H10301) proximity card, then allows you to simulate or clone that card to a T55x7 blank. It supports **2 storage banks**, so you can capture and replay two different cards without reconnecting to a host. + +## Why + +This is the classic "sniff and replay" attack for HID access control systems. Many physical security assessments require demonstrating that credentials can be captured and replayed. SamyRun is the simplest, most direct tool for this: walk up to a card, read it, then walk up to a reader and replay it — entirely on-device with no laptop required. + +Use cases: +- **Red team engagements**: Capture a badge and replay it at a door reader +- **Credential cloning**: Write captured credentials to a T55x7 blank card +- **Physical security audits**: Demonstrate that HID 26-bit (H10301) is trivially clonable + +## How + +1. The Proxmark3 enters LF read mode and waits for an HID card to come into field range +2. The card's raw data (high + low words) is decoded and stored into the selected bank (0 or 1) +3. On the next button press, the Proxmark3 simulates the captured card — it acts as the card itself and will unlock any reader expecting that credential +4. On the next button press, the data is written to a T55x7 card, creating a physical clone +5. The mode then cycles to the second bank and repeats + +The firmware uses `lf_hid_watch()` for reading, `CmdHIDsimTAGEx()` for simulation, and `CopyHIDtoT55x7()` for cloning. + +## LED Indicators + +| LED | Meaning | +|-----|---------| +| **A** (solid) | Bank 0 selected, reading mode | +| **B** (solid) | Bank 1 selected, reading mode | +| **A or B** (blinking) | Error — zero data read, retry | +| **C** (solid) | Simulation active | +| **D** (solid) | Cloning active | +| **A+B+C+D** (rapid blink) | Exiting standalone mode | + +## Button Controls + +| Action | Effect | +|--------|--------| +| **Hold 280ms** | Advance to next state (READ → SIM → CLONE → next bank) | +| **USB command** | Exit standalone mode and return to host shell | + +## State Machine + +```mermaid +stateDiagram-v2 + [*] --> Read_Bank0 : Power on / Standalone start + + Read_Bank0 --> Sim_Bank0 : Button hold\n(card captured) + Read_Bank0 --> Read_Bank0 : Button hold\n(no card / zeros) + + Sim_Bank0 --> Clone_Bank0 : Button hold + Clone_Bank0 --> Read_Bank1 : Button hold\n(switch to bank 1) + + Read_Bank1 --> Sim_Bank1 : Button hold\n(card captured) + Read_Bank1 --> Read_Bank1 : Button hold\n(no card / zeros) + + Sim_Bank1 --> Clone_Bank1 : Button hold + Clone_Bank1 --> Read_Bank0 : Button hold\n(cycle back to bank 0) + + Read_Bank0 --> [*] : USB data received + Read_Bank1 --> [*] : USB data received + Sim_Bank0 --> [*] : USB data received + Sim_Bank1 --> [*] : USB data received +``` + +## Compilation + +``` +make clean +make STANDALONE=LF_SAMYRUN -j +./pm3-flash-fullimage +``` + +Or in `Makefile.platform`: +``` +STANDALONE=LF_SAMYRUN +``` + +## Related + +- [HID Corporate Brute](lf_hidbrute.md) — Bruteforce HID Corporate 1000 card numbers +- [ProxBrute](lf_proxbrute.md) — Bruteforce HID ProxII card numbers +- [MultiHID](lf_multihid.md) — Simulate multiple predefined HID26 cards +- [IceHID Collector](lf_icehid.md) — Log HID credentials to flash memory diff --git a/doc/standalone/lf_tharexde.md b/doc/standalone/lf_tharexde.md new file mode 100644 index 000000000..bc37cc78d --- /dev/null +++ b/doc/standalone/lf_tharexde.md @@ -0,0 +1,99 @@ +# LF_THAREXDE — EM4x50 Simulator/Collector + +> **Author:** tharexde +> **Frequency:** LF (125 kHz) +> **Hardware:** RDV4 (requires flash memory) + +[Back to Standalone Modes Index](../../armsrc/Standalone/readme.md#individual-mode-documentation) | [Source Code](../../armsrc/Standalone/lf_tharexde.c) | [Development Guide](../../armsrc/Standalone/readme.md#developing-standalone-modes) + +--- + +## What + +A dual-mode standalone for EM4x50 tags: simulate an EM4x50 tag loaded from a flash dump file, or read/collect EM4x50 data (including passwords) to flash. + +## Why + +EM4x50 is a more advanced LF tag than EM4100 — it supports password protection, memory blocks, and bidirectional communication. This mode handles both offensive and defensive EM4x50 scenarios: + +- **Simulation**: Load a dumped EM4x50 tag and emulate it at a reader +- **Collection**: Capture EM4x50 data and passwords from cards in the field + +## How + +**SIM mode:** +1. Loads tag data from `lf_em4x50_simulate.eml` on flash +2. Configures the EM4x50 simulation engine +3. Continuously emulates the tag + +**READ mode:** +1. Listens for EM4x50 tags +2. Reads all accessible memory blocks +3. If password authentication is observed, logs it to `lf_em4x50_passwords.log` +4. Full tag dumps go to `lf_em4x50_collect.log` + +## LED Indicators + +| LED | Meaning | +|-----|---------| +| **A** (solid) | Simulating (blinks if no data or error) | +| **B** (solid) | Reading / recording | +| **D** (solid) | Unmounting / syncing flash | + +## Button Controls + +| Action | Effect | +|--------|--------| +| **Single click** | Toggle between SIM and READ modes | +| **Hold** | Exit to shell | +| **USB command** | Exit standalone mode | + +## State Machine + +```mermaid +stateDiagram-v2 + [*] --> SIM : Startup (eml file exists) + [*] --> READ : Startup (no eml file) + + SIM --> READ : Button click + READ --> SIM : Button click + + SIM --> SIM : Continuous emulation + READ --> READ : Continuous collection + + SIM --> [*] : Button hold / USB data + READ --> [*] : Button hold / USB data + + note right of SIM + Loads from: + lf_em4x50_simulate.eml + end note + + note right of READ + Logs to: + lf_em4x50_passwords.log + lf_em4x50_collect.log + end note +``` + +## Flash Files + +| File | Purpose | +|------|---------| +| `lf_em4x50_simulate.eml` | Input: tag data to simulate | +| `lf_em4x50_passwords.log` | Output: captured passwords | +| `lf_em4x50_collect.log` | Output: full tag dumps | + +## Compilation + +``` +make clean +make STANDALONE=LF_THAREXDE -j +./pm3-flash-fullimage +``` + +## Related + +- [EM4100 RSWB](lf_em4100rswb.md) — EM4100 (simpler format) multi-tool +- [IceHID Collector](lf_icehid.md) — Multi-format LF collector +- [NexID Collector](lf_nexid.md) — Nexwatch collector diff --git a/include/protocols.h b/include/protocols.h index 2b84fcd35..098c84041 100644 --- a/include/protocols.h +++ b/include/protocols.h @@ -849,6 +849,9 @@ ISO 7816-4 Basic interindustry commands. For command APDU's. #define FELICA_WRTSEC_REQ 0x16 #define FELICA_WRTSEC_ACK 0x17 +#define FELICA_GET_NODE_LIST_REQ 0x1a +#define FELICA_GET_NODE_LIST_ACK 0x1b + #define FELICA_GET_CONTAINER_ISSUE_INFO_REQ 0x22 #define FELICA_GET_CONTAINER_ISSUE_INFO_ACK 0x23 diff --git a/tools/pm3_tests.sh b/tools/pm3_tests.sh index 2f27a6389..e94d5096e 100755 --- a/tools/pm3_tests.sh +++ b/tools/pm3_tests.sh @@ -474,8 +474,24 @@ while true; do if ! CheckExecute "nfc decode test - signature" "$CLIENTBIN -c 'nfc decode -d 03FF010194113870696C65742E65653A656B616172743A3266195F26063132303832325904202020205F28033233335F2701316E1B5A13333038363439303039303030323636343030355304EBF2CE704103000000AC536967010200803A2448FCA7D354A654A81BD021150D1A152D1DF4D7A55D2B771F12F094EAB6E5E10F2617A2F8DAD4FD38AFF8EA39B71C19BD42618CDA86EE7E144636C8E0E7CFC4096E19C3680E09C78A0CDBC05DA2D698E551D5D709717655E56FE3676880B897D2C70DF5F06ECE07C71435255144F8EE41AF110E7B180DA0E6C22FB8FDEF61800025687474703A2F2F70696C65742E65652F6372742F33303836343930302D303030312E637274FE'" "30864900-0001.crt"; then break; fi if ! CheckExecute "nfc decode test - openprinter tag" "$CLIENTBIN -c 'nfc decode -d 03FF012F91013A55046E756D616B6572732E636F6D2F70726F64756374732F6162732D66696C616D656E743F76617269616E743D3436393434323937333836323932521CD26170706C69636174696F6E2F766E642E6F70656E7072696E74746167A10218AFBF041B000007D0FCAB45F9080009020A70414253204C656D6F6E2059656C6C6F770B684E756D616B6572730E1A69094200101903E81119041A1218F01343F9A800181DF93C29182218F01823190104182418AA1825185A18261864FF00'" "application/vnd.openprinttag"; then break; fi + if ! CheckExecute "wiegand encode test - new" "$CLIENTBIN -c 'wiegand encode -w H10301 --fc 123 --cn 4567 --new'" "New PACS\\.{9} 0x 06BD88EB80"; then break; fi + if ! CheckExecute "wiegand encode test - bin" "$CLIENTBIN -c 'wiegand encode --bin 1'" "Wiegand raw\\.{4} 03"; then break; fi + if ! CheckExecute "wiegand encode test - new bin" "$CLIENTBIN -c 'wiegand encode --bin 1 --new'" "New PACS\\.{9} 0x 0780"; then break; fi + if ! CheckExecute "wiegand encode test - new bin verbose hdr" "$CLIENTBIN -c 'wiegand encode --bin 1 --new --verbose'" "New PACS"; then break; fi + if ! CheckExecute "wiegand encode test - new bin verbose pad" "$CLIENTBIN -c 'wiegand encode --bin 1 --new --verbose'" "With Sentinel\\.{4} 0b 00000011 \\(8-bit\\)"; then break; fi + if ! CheckExecute "wiegand encode test - new bin verbose raw" "$CLIENTBIN -c 'wiegand encode --bin 1 --new --verbose'" "Wiegand --raw\\.{4} 0x 03"; then break; fi + if ! CheckExecute "wiegand encode test - new bin verbose bin" "$CLIENTBIN -c 'wiegand encode --bin 1 --new --verbose'" "Without Sentinel\\. 0b 1 \\(1-bit\\)"; then break; fi + if ! CheckExecute "wiegand encode test - bin 96-bit" "PAT=\$(printf '01%.0s' {1..48}); $CLIENTBIN -c \"wiegand encode --bin \$PAT\"" "Wiegand raw\\.{4} 01555555555555555555555555"; then break; fi + if ! CheckExecute "wiegand encode test - new bin 96-bit" "PAT=\$(printf '01%.0s' {1..48}); $CLIENTBIN -c \"wiegand encode --bin \$PAT --new\"" "New PACS\\.{9} 0x 00555555555555555555555555"; then break; fi + if ! CheckExecute "wiegand encode test - new bin 96-bit verbose pad" "PAT=\$(printf '01%.0s' {1..48}); $CLIENTBIN -c \"wiegand encode --bin \$PAT --new --verbose\"" "With Sentinel\\.{4} 0b 00000001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 \\(104-bit\\)"; then break; fi + if ! CheckExecute "wiegand encode test - new bin 96-bit verbose raw" "PAT=\$(printf '01%.0s' {1..48}); $CLIENTBIN -c \"wiegand encode --bin \$PAT --new --verbose\"" "Wiegand --raw\\.{4} 0x 01555555555555555555555555"; then break; fi + if ! CheckExecute "wiegand encode test - new 48-bit" "$CLIENTBIN -c 'wiegand encode -w C1k48s --fc 42069 --cn 42069 --new'" "New PACS\\.{9} 0x 0000A4550148AB"; then break; fi + if ! CheckExecute "wiegand decode test - raw over 96-bit" "$CLIENTBIN -c 'wiegand decode --raw 01555555555555555555555555' 2>&1" "Raw hex decode supports up to 96 Wiegand bits"; then break; fi if ! CheckExecute "wiegand decode test - raw" "$CLIENTBIN -c 'wiegand decode --raw 2006F623AE'" "FC: 123 CN: 4567 parity \( ok \)"; then break; fi + if ! CheckExecute "wiegand decode test - bin over 96-bit" "PAT=\$(printf '01%.0s' {1..49}); $CLIENTBIN -c \"wiegand decode --bin \$PAT\" 2>&1" "Binary decode supports up to 96 Wiegand bits"; then break; fi if ! CheckExecute "wiegand decode test - new" "$CLIENTBIN -c 'wiegand decode --new 06BD88EB80'" "FC: 123 CN: 4567 parity \( ok \)"; then break; fi + if ! CheckExecute "wiegand decode test - new 96-bit" "$CLIENTBIN -c 'wiegand decode --new 00555555555555555555555555'" "hex\\.{14} 555555555555555555555555"; then break; fi + if ! CheckExecute "wiegand decode test - new 48-bit" "$CLIENTBIN -c 'wiegand decode --new 0000A4550148AB'" "C1k48s.*FC: 42069 CN: 42069 parity \( ok \)"; then break; fi if ! CheckExecute "wiegand Verkada40 encode test 1" "$CLIENTBIN -c 'wiegand encode -w Verkada40 --fc 50 --cn 1001'" "86400007D3"; then break; fi if ! CheckExecute "wiegand Verkada40 decode test 1" "$CLIENTBIN -c 'wiegand decode --raw 86400007D3'" "Verkada40.*FC: 50 CN: 1001 parity \( ok \)"; then break; fi if ! CheckExecute "wiegand Verkada40 encode test 2" "$CLIENTBIN -c 'wiegand encode -w Verkada40 --fc 50 --cn 1004'" "86400007D9"; then break; fi