From 19789381df5b1e683b3c13f059c9e7972b421c5c Mon Sep 17 00:00:00 2001 From: seclabz Date: Wed, 16 Apr 2025 14:05:10 +0200 Subject: [PATCH] Added new standalone mode `HF_ST25_TEAROFF` to store/restore ST25TB tags with tearoff for counters (@seclabz) --- CHANGELOG.md | 1 + armsrc/Standalone/Makefile.hal | 5 +- armsrc/Standalone/Makefile.inc | 4 + armsrc/Standalone/hf_st25_tearoff.c | 1170 +++++++++++++++++ armsrc/iso14443b.c | 6 +- armsrc/iso14443b.h | 3 + .../4_Advanced-compilation-parameters.md | 1 + 7 files changed, 1186 insertions(+), 4 deletions(-) create mode 100644 armsrc/Standalone/hf_st25_tearoff.c diff --git a/CHANGELOG.md b/CHANGELOG.md index 2db1430d6..d7a1bfe99 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ This project uses the changelog in accordance with [keepchangelog](http://keepac ## [unreleased][unreleased] - Fixed `hf mf fchk --mem` to actually use flash dict (@doegox) - Fixed `make install` on OSX thanks DaveItsLong (@doegox) +- Added new standalone mode `HF_ST25_TEAROFF` to store/restore ST25TB tags with tearoff for counters (@seclabz) ## [Blue Ice.4.20142][2025-03-25] - Added `des_talk.py` script for easier MIFARE DESFire handling (@trigat) diff --git a/armsrc/Standalone/Makefile.hal b/armsrc/Standalone/Makefile.hal index 91d87dd14..679385ace 100644 --- a/armsrc/Standalone/Makefile.hal +++ b/armsrc/Standalone/Makefile.hal @@ -119,6 +119,9 @@ define KNOWN_STANDALONE_DEFINITIONS | HF_REBLAY | 14A Relay over BT | | (RDV4 only) | - Salvador Mendoza | +----------------------------------------------------------+ +| HF_ST25_TEAROFF | Store/restore ST25TB tags with | +| | tear-off for counters - SecLabz | ++----------------------------------------------------------+ | HF_TCPRST | IKEA Rothult read/sim/dump/emul | | | - Nick Draffen | +----------------------------------------------------------+ @@ -139,7 +142,7 @@ endef STANDALONE_MODES := LF_SKELETON STANDALONE_MODES += LF_EM4100EMUL LF_EM4100RSWB LF_EM4100RSWW LF_EM4100RWC LF_HIDBRUTE LF_HIDFCBRUTE LF_ICEHID LF_MULTIHID LF_NEDAP_SIM LF_NEXID LF_PROXBRUTE LF_PROX2BRUTE LF_SAMYRUN LF_THAREXDE -STANDALONE_MODES += HF_14ASNIFF HF_14BSNIFF HF_15SNIFF HF_15SIM HF_AVEFUL HF_BOG HF_CARDHOPPER HF_COLIN HF_CRAFTBYTE HF_ICECLASS HF_LEGIC HF_LEGICSIM HF_MATTYRUN HF_MFCSIM HF_MSDSAL HF_REBLAY HF_TCPRST HF_TMUDFORD HF_UNISNIFF HF_YOUNG +STANDALONE_MODES += HF_14ASNIFF HF_14BSNIFF HF_15SNIFF HF_15SIM HF_AVEFUL HF_BOG HF_CARDHOPPER HF_COLIN HF_CRAFTBYTE HF_ICECLASS HF_LEGIC HF_LEGICSIM HF_MATTYRUN HF_MFCSIM HF_MSDSAL HF_REBLAY HF_ST25_TEAROFF HF_TCPRST HF_TMUDFORD HF_UNISNIFF HF_YOUNG STANDALONE_MODES += DANKARMULTI STANDALONE_MODES_REQ_BT := HF_CARDHOPPER HF_REBLAY STANDALONE_MODES_REQ_SMARTCARD := diff --git a/armsrc/Standalone/Makefile.inc b/armsrc/Standalone/Makefile.inc index 5873c0aff..d4c858e8b 100644 --- a/armsrc/Standalone/Makefile.inc +++ b/armsrc/Standalone/Makefile.inc @@ -157,6 +157,10 @@ endif ifneq (,$(findstring WITH_STANDALONE_HF_YOUNG,$(APP_CFLAGS))) SRC_STANDALONE = hf_young.c endif +# WITH_STANDALONE_HF_ST25_TEAROFF +ifneq (,$(findstring WITH_STANDALONE_HF_ST25_TEAROFF,$(APP_CFLAGS))) + SRC_STANDALONE = hf_st25_tearoff.c +endif ifneq (,$(findstring WITH_STANDALONE_DANKARMULTI,$(APP_CFLAGS))) SRC_STANDALONE = dankarmulti.c diff --git a/armsrc/Standalone/hf_st25_tearoff.c b/armsrc/Standalone/hf_st25_tearoff.c new file mode 100644 index 000000000..145b96f05 --- /dev/null +++ b/armsrc/Standalone/hf_st25_tearoff.c @@ -0,0 +1,1170 @@ +//----------------------------------------------------------------------------- +// Copyright (C) SecLabz, 2025 +// 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. +//----------------------------------------------------------------------------- +// Standalone mode for reading/storing and restoring ST25TB tags with tear-off for counters. +// Handles a collection of tags. Click swaps between Store and Restore modes. +// Requires WITH_FLASH enabled at compile time. +// Only tested on a Proxmark3 Easy with flash +// +// The initial mode is learning/storing with LED D. +// In this mode, the Proxmark3 is looking for an ST25TB tag, reads all its data, +// and stores the tag's contents to flash memory for later restoration. +// +// Clicking the button once will toggle to restore mode (LED C). +// In this mode, the Proxmark3 searches for an ST25TB tag and, if found, compares +// its UID with previously stored tags. If there's a match, it will restore the +// tag data from flash memory, including counter blocks using tear-off technique. +// +// The standalone supports a collection of up to 8 different ST25TB tags. +// +// Special handling is implemented for counter blocks 5 & 6. For these blocks, +// the tear-off technique is used to manipulate counters that normally can only +// be decremented, allowing restoration of previously stored counter values even +// if they're higher than the current value. +// +// Holding the button down for 1 second will exit the standalone mode. +// +// LEDs: +// LED D = Learn/Store mode (reading and storing tag data) +// LED C = Restore mode (writing stored data back to tags) +// LED A (blinking) = Operation successful +// LED B (blinking) = Operation failed +// +// Flash memory is required for this standalone mode to function properly. +// +//----------------------------------------------------------------------------- + + +//============================================================================= +// INCLUDES +//============================================================================= + +// System includes +#include // memcpy, memset + +// Proxmark3 includes +#include "standalone.h" +#include "proxmark3_arm.h" +#include "appmain.h" +#include "fpgaloader.h" +#include "iso14443b.h" // ISO14443B operations +#include "util.h" +#include "spiffs.h" // Flash memory filesystem access +#include "dbprint.h" +#include "ticks.h" +#include "BigBuf.h" +#include "protocols.h" +#include "crc16.h" // compute_crc + +//============================================================================= +// FLASH MEMORY REQUIREMENT CHECK +//============================================================================= + +#ifndef WITH_FLASH +#error "This standalone mode requires WITH_FLASH to be defined. Please recompile with flash memory support." +#endif + +//============================================================================= +// CONSTANTS & DEFINITIONS +//============================================================================= + +// File and data structure constants +#define HF_ST25TB_MULTI_SR_FILE "hf_st25tb_tags.bin" // Store/Restore filename +#define ST25TB_BLOCK_COUNT 16 // ST25TB512 or similar with 16 blocks +#define ST25TB_BLOCK_SIZE 4 // 4 bytes per block +#define ST25TB_COUNTER_BLOCK_5 5 // Counter block indices +#define ST25TB_COUNTER_BLOCK_6 6 +#define ST25TB_DATA_SIZE (ST25TB_BLOCK_COUNT * ST25TB_BLOCK_SIZE) +#define MAX_SAVED_TAGS 8 // Allow storing up to 8 tags + +// Tear-off constants +#define TEAR_OFF_START_OFFSET_US 150 +#define TEAR_OFF_ADJUSTMENT_US 25 +#define PRE_READ_DELAY_US 0 +#define TEAR_OFF_WRITE_RETRY_COUNT 30 +#define TEAR_OFF_CONSOLIDATE_READ_COUNT 6 +#define TEAR_OFF_CONSOLIDATE_WAIT_READ_COUNT 2 +#define TEAR_OFF_CONSOLIDATE_WAIT_MS 2000 + +// Display/console colors +#define RESET "\033[0m" +#define BOLD "\033[01m" +#define RED "\033[31m" +#define BLUE "\033[34m" +#define GREEN "\033[32m" + +// Bit manipulation macros +#define IS_ONE_BIT(value, index) ((value) & ((uint32_t)1 << (index))) +#define IS_ZERO_BIT(value, index) (!IS_ONE_BIT(value, index)) + +#define RF_SWTICH_OFF() FpgaWriteConfWord(FPGA_MAJOR_MODE_OFF) + +//============================================================================= +// TYPE DEFINITIONS +//============================================================================= + +// Operation modes +typedef enum { + MODE_LEARN = 0, // Store/learn tag data + MODE_RESTORE = 1 // Restore tag data +} standalone_mode_t; + +// Operation states +typedef enum { + STATE_BUSY = 0, // Actively processing + STATE_DONE = 1, // Operation completed successfully + STATE_ERROR = 2 // Operation failed +} standalone_state_t; + +// Structure to hold tag data in RAM +typedef struct { + uint64_t uid; + uint32_t blocks[ST25TB_BLOCK_COUNT]; + uint32_t otp; + bool data_valid; // Flag to indicate if this slot holds valid data +} st25tb_data_t; + +//============================================================================= +// GLOBAL VARIABLES +//============================================================================= + +// Tag collection and state tracking +static st25tb_data_t g_stored_tags[MAX_SAVED_TAGS]; +static uint8_t g_valid_tag_count = 0; // Number of valid entries +static standalone_mode_t g_current_mode = MODE_LEARN; // Current operation mode +static standalone_state_t current_state = STATE_BUSY; // Current operation state +static unsigned long g_prng_seed = 1; // Used for PRNG + +//============================================================================= +// FUNCTION DECLARATIONS +//============================================================================= + +// Core utility functions +static int dummy_rand(void); +uint64_t bytes_to_num_le(const uint8_t *src, size_t len); + +// UI/LED interaction functions +static void update_leds_mode(standalone_mode_t mode); +static void indicate_success(void); +static void indicate_failure(void); + +// Flash storage operations +static bool load_tags_from_flash(st25tb_data_t collection[MAX_SAVED_TAGS]); +static bool save_tags_to_flash(const st25tb_data_t collection[MAX_SAVED_TAGS]); +static int find_tag_by_uid(const uint64_t uid); +static int find_free_tag_slot(void); + +// ISO14443B communication functions +static void iso14443b_setup_light(void); + +// Tag read/write operations +static bool st25tb_tag_get_basic_info(iso14b_card_select_t *card_info); +static bool st25tb_tag_read(st25tb_data_t *tag_data_slot); +static bool st25tb_tag_restore(const st25tb_data_t *stored_data_slot); +static void st25tb_tag_print(st25tb_data_t *tag); + +// Tear-off operations +static int st25tb_cmd_write_block(uint8_t block_address, uint8_t *block); +static bool st25tb_write_block_with_retry(uint8_t block_address, uint32_t target_value); +static int st25tb_tear_off_read_block(uint8_t block_address, uint32_t *block_value); +static void st25tb_tear_off_write_block(uint8_t block_address, uint32_t data, uint16_t tearoff_delay_us); +static int8_t st25tb_tear_off_retry_write_verify(uint8_t block_address, uint32_t target_value, uint32_t max_try_count, int sleep_time_ms, uint32_t *read_back_value); +static int8_t st25tb_tear_off_is_consolidated(const uint8_t block_address, uint32_t value, int repeat_read, int sleep_time_ms, uint32_t *read_value); +static int8_t st25tb_tear_off_consolidate_block(const uint8_t block_address, uint32_t current_value, uint32_t target_value, uint32_t *read_back_value); +static uint32_t st25tb_tear_off_next_value(uint32_t current_value, bool randomness); +static void st25tb_tear_off_adjust_timing(int *tear_off_us, uint32_t tear_off_adjustment_us); +static void st25tb_tear_off_log(int tear_off_us, char *color, uint32_t value); +static int8_t st25tb_tear_off_write_counter(uint8_t block_address, uint32_t target_value, uint32_t tear_off_adjustment_us, uint32_t safety_value); + +// Main application functions +static void run_learn_function(void); +static void run_restore_function(void); +void ModInfo(void); +void RunMod(void); + +//============================================================================= +// CORE UTILITY FUNCTIONS +//============================================================================= + +/** + * @brief Simple PRNG implementation + * @return Random integer + */ +static int dummy_rand(void) { + g_prng_seed = g_prng_seed * 1103515245 + 12345; + return (unsigned int)(g_prng_seed / 65536) % 32768; +} + +/** + * @brief Convert bytes to number (little-endian) + * @param src Source byte array + * @param len Length of array + * @return Converted 64-bit value + */ +uint64_t bytes_to_num_le(const uint8_t *src, size_t len) { + uint64_t num = 0; + size_t i; + + if (len > sizeof(uint64_t)) { + len = sizeof(uint64_t); + } + + // Iterate from LSB to MSB + for (i = 0; i < len; ++i) { + num |= ((uint64_t)src[i] << (i * 8)); + } + + return num; +} + +//============================================================================= +// UI/LED INTERACTION FUNCTIONS +//============================================================================= + +/** + * @brief Update LEDs to indicate current mode and state + * @param mode Current operation mode + */ +static void update_leds_mode(standalone_mode_t mode) { + LEDsoff(); + if (mode == MODE_LEARN) { + LED_D_ON(); + } else { // MODE_RESTORE + LED_C_ON(); + } +} + +/** + * @brief Indicate successful operation with LED sequence + */ +static void indicate_success(void) { + // Blink Green LED (A) 3 times quickly for success + for(int i=0; i<3; ++i) { + LED_A_ON(); + SpinDelay(150); + LED_A_OFF(); + SpinDelay(150); + } +} + +/** + * @brief Indicate failed operation with LED sequence + */ +static void indicate_failure(void) { + // Blink Red LED (B) 3 times quickly for failure + for(int i=0; i<3; ++i) { + LED_B_ON(); + SpinDelay(150); + LED_B_OFF(); + SpinDelay(150); + } +} + +//============================================================================= +// FLASH STORAGE OPERATIONS +//============================================================================= + +/** + * @brief Load tag collection from flash + * @param collection Array to store loaded data + * @return true if successful, false otherwise + */ +static bool load_tags_from_flash(st25tb_data_t collection[MAX_SAVED_TAGS]) { + // Check if file exists + if (!exists_in_spiffs(HF_ST25TB_MULTI_SR_FILE)) { + return false; // File doesn't exist, nothing to load + } + + // Verify file size + uint32_t size = size_in_spiffs(HF_ST25TB_MULTI_SR_FILE); + if (size != sizeof(g_stored_tags)) { + Dbprintf(_RED_("Flash file size mismatch (expected %zu, got %u). Wiping old file."), + sizeof(g_stored_tags), size); + // Remove corrupted file + rdv40_spiffs_remove(HF_ST25TB_MULTI_SR_FILE, RDV40_SPIFFS_SAFETY_SAFE); + return false; + } + + // Read file contents + int res = rdv40_spiffs_read(HF_ST25TB_MULTI_SR_FILE, (uint8_t *)collection, + size, RDV40_SPIFFS_SAFETY_SAFE); + + if (res != SPIFFS_OK) { + Dbprintf(_RED_("Failed to read tag collection from flash (err %d)"), res); + // Mark all as invalid if read failed + for (int i = 0; i < MAX_SAVED_TAGS; i++) + collection[i].data_valid = false; + return false; + } + + return true; +} + +/** + * @brief Save tag collection to flash + * @param collection Array of tag data to save + * @return true if successful, false otherwise + */ +static bool save_tags_to_flash(const st25tb_data_t collection[MAX_SAVED_TAGS]) { + int res = rdv40_spiffs_write(HF_ST25TB_MULTI_SR_FILE, (uint8_t *)collection, + sizeof(g_stored_tags), RDV40_SPIFFS_SAFETY_SAFE); + return (res == SPIFFS_OK); +} + +/** + * @brief Find a tag in the collection by UID + * @param uid UID to search for + * @return Index of tag in collection, or -1 if not found + */ +static int find_tag_by_uid(const uint64_t uid) { + for (int i = 0; i < MAX_SAVED_TAGS; i++) { + if (g_stored_tags[i].data_valid && g_stored_tags[i].uid == uid) { + return i; + } + } + return -1; // Not found +} + +/** + * @brief Find next empty slot in the collection + * @return Index of empty slot, or -1 if collection is full + */ +static int find_free_tag_slot(void) { + for (int i = 0; i < MAX_SAVED_TAGS; i++) { + if (!g_stored_tags[i].data_valid) { + return i; + } + } + return -1; // Collection is full +} + +//============================================================================= +// ISO14443B COMMUNICATION FUNCTIONS +//============================================================================= + +/** + * @brief Stripped version of "iso14443b_setup" that avoids unnecessary LED + * operations and uses shorter delays + */ +static void iso14443b_setup_light(void) { + RF_SWTICH_OFF(); + + FpgaDownloadAndGo(FPGA_BITSTREAM_HF); + + // Set up the synchronous serial port + FpgaSetupSsc(FPGA_MAJOR_MODE_HF_READER); + + // Signal field is on with the appropriate LED +#ifdef RDV4 + FpgaWriteConfWord(FPGA_MAJOR_MODE_HF_READER | FPGA_HF_READER_MODE_SEND_SHALLOW_MOD_RDV4); +#else + FpgaWriteConfWord(FPGA_MAJOR_MODE_HF_READER | FPGA_HF_READER_MODE_SEND_SHALLOW_MOD); +#endif + + SpinDelayUs(250); + + // Start the timer + StartCountSspClk(); +} + +//============================================================================= +// TAG READ/WRITE OPERATIONS +//============================================================================= + +/** + * @brief Select a ST25TB tag and get basic info + * @param card_info Pointer to store card info + * @return true if successful, false otherwise + */ +static bool st25tb_tag_get_basic_info(iso14b_card_select_t *card_info) { + iso14443b_setup_light(); + int res = iso14443b_select_srx_card(card_info); + RF_SWTICH_OFF(); + return (res == PM3_SUCCESS); +} + +/** + * @brief Read all data from a ST25TB tag + * @param tag_data_slot Pointer to store tag data + * @return true if successful, false otherwise + */ +static bool st25tb_tag_read(st25tb_data_t *tag_data_slot) { + iso14443b_setup_light(); + iso14b_card_select_t card_info; + uint8_t block[ST25TB_BLOCK_SIZE]; + int res; + bool success = true; + + // Select card + res = iso14443b_select_srx_card(&card_info); + if (res != PM3_SUCCESS) { + RF_SWTICH_OFF(); + return false; + } + + Dbprintf("Found ST tag. Reading %d blocks...", ST25TB_BLOCK_COUNT); + tag_data_slot->uid = bytes_to_num_le(card_info.uid, sizeof(tag_data_slot->uid)); + + // Read all data blocks + for (uint8_t block_address = 0; block_address < ST25TB_BLOCK_COUNT; block_address++) { + WDT_HIT(); + res = read_14b_srx_block(block_address, block); + if (res != PM3_SUCCESS) { + Dbprintf(_RED_("Failed to read block %d"), block_address); + success = false; + break; + } + + // Store the read block data + tag_data_slot->blocks[block_address] = bytes_to_num_le(block, ST25TB_BLOCK_SIZE); + + if (g_dbglevel >= DBG_DEBUG) { + Dbprintf("Read Block %02d: %08X", block_address, tag_data_slot->blocks[block_address]); + } + SpinDelay(5); // Small delay between block reads + } + + // Read OTP block + res = read_14b_srx_block(255, block); + if (res != PM3_SUCCESS) { + Dbprintf(_RED_("Failed to read otp block")); + success = false; + } else { + tag_data_slot->otp = bytes_to_num_le(block, ST25TB_BLOCK_SIZE); + } + + RF_SWTICH_OFF(); + + tag_data_slot->data_valid = success; + return success; +} + +/** + * @brief Restore data to a ST25TB tag + * @param stored_data_slot Pointer to stored tag data + * @return true if successful, false otherwise + */ +static bool st25tb_tag_restore(const st25tb_data_t *stored_data_slot) { + if (!stored_data_slot->data_valid) { + DbpString(_RED_("Restore error: Slot data is invalid.")); + return false; + } + + iso14443b_setup_light(); + iso14b_card_select_t card_info; + int res; + bool success = true; + + res = iso14443b_select_srx_card(&card_info); + if (res != PM3_SUCCESS) { + DbpString("Restore failed: No tag found or selection failed."); + RF_SWTICH_OFF(); + return false; + } + + uint64_t tag_uid = bytes_to_num_le(card_info.uid, sizeof(uint64_t)); + + // Verify UID match before restoring + if (tag_uid != stored_data_slot->uid) { + Dbprintf("Restore failed: UID mismatch (Tag: %llX, Slot: %llX)", tag_uid, stored_data_slot->uid); + RF_SWTICH_OFF(); + return false; + } + + Dbprintf("Found ST tag, UID: %llX. Starting restore...", tag_uid); + + // Process all blocks + for (uint8_t block_address = 0; block_address < ST25TB_BLOCK_COUNT; block_address++) { + WDT_HIT(); + uint32_t stored_value = stored_data_slot->blocks[block_address]; + + if (g_dbglevel >= DBG_DEBUG) { + Dbprintf("Restoring Block %02d: %08X", block_address, stored_value); + } + + // Special handling for counter blocks 5 and 6 + if (block_address == ST25TB_COUNTER_BLOCK_5 || block_address == ST25TB_COUNTER_BLOCK_6) { + uint32_t current_value = 0; + + res = st25tb_tear_off_read_block(block_address, ¤t_value); + if (res != PM3_SUCCESS) { + Dbprintf(_RED_("Failed to read current counter value for block %d"), block_address); + success = false; + break; + } + + if (g_dbglevel >= DBG_DEBUG) { + Dbprintf("Counter Block %d: Stored=0x%08X, Current=0x%08X", + block_address, stored_value, current_value); + } + + // Only use tear-off logic if stored value is greater + if (stored_value > current_value) { + // The st25tb_tear_off_write_counter function handles the tear-off logic + if (st25tb_tear_off_write_counter(block_address, stored_value, TEAR_OFF_ADJUSTMENT_US, 0x1000) != 0) { + Dbprintf(_RED_("Tear-off write failed for counter block %d"), block_address); + success = false; + break; + } + Dbprintf("Used tear-off write for counter block %d", block_address); + } else if (stored_value < current_value) { + // Standard write for when stored value is less than current + if (!st25tb_write_block_with_retry(block_address, stored_value)) { + Dbprintf(_RED_("Failed to write block %d"), block_address); + success = false; + break; + } + } else { + Dbprintf("Counter block %d already has the target value (0x%08X). Skipping write.", + block_address, stored_value); + } + } else { + // Standard write for non-counter blocks + if (!st25tb_write_block_with_retry(block_address, stored_value)) { + Dbprintf(_RED_("Failed to write block %d with value 0x%08X"), block_address, stored_value); + success = false; + break; + } + } + SpinDelay(10); // Delay between writes + } + + RF_SWTICH_OFF(); + return success; +} + +/** + * @brief Print tag data in formatted table + * @param tag Pointer to tag data + */ +static void st25tb_tag_print(st25tb_data_t *tag) { + uint8_t i; + + Dbprintf("UID: %016llX", tag->uid); + + Dbprintf("+---------------+----------+--------------------+"); + Dbprintf("| BLOCK ADDRESS | VALUE | DESCRIPTION |"); + Dbprintf("+---------------+----------+--------------------+"); + + for (i = 0; i < 16; i++) { + if (i == 2) { + Dbprintf("| %03d | %08X | Lockable EEPROM |", i, tag->blocks[i]); + } else if (i == 5) { + Dbprintf("| %03d | %08X | Count down |", i, tag->blocks[i]); + } else if (i == 6) { + Dbprintf("| %03d | %08X | counter |", i, tag->blocks[i]); + } else if (i == 11) { + Dbprintf("| %03d | %08X | Lockable EEPROM |", i, tag->blocks[i]); + } else { + Dbprintf("| %03d | %08X | |", i, tag->blocks[i]); + } + if (i == 4 || i == 6 || i == 15) { + Dbprintf("+---------------+----------+--------------------+"); + } + } + + Dbprintf("| %03d | %08X | System OTP bits |", 255, tag->otp); + Dbprintf("+---------------+----------+--------------------+"); +} + +//============================================================================= +// TEAR-OFF OPERATIONS +//============================================================================= + +/** + * @brief Read a block using tear-off technique + * @param block_address Block address to read + * @param block_value Pointer to store read value + * @return Result code (0 for success) + */ +static int st25tb_tear_off_read_block(uint8_t block_address, uint32_t *block_value) { + int res; + iso14b_card_select_t card; + iso14443b_setup_light(); + + res = iso14443b_select_srx_card(&card); + if (res != PM3_SUCCESS) { + goto out; + } + + uint8_t block[ST25TB_BLOCK_SIZE]; + res = read_14b_srx_block(block_address, block); + if (res == PM3_SUCCESS) { + *block_value = bytes_to_num_le(block, ST25TB_BLOCK_SIZE); + } + +out: + RF_SWTICH_OFF(); + return res; +} + +/** + * @brief Low-level block write function + * @param block_address Block number to write + * @param block Block data + * @return Result code (0 for success) + */ +static int st25tb_cmd_write_block(uint8_t block_address, uint8_t *block) { + uint8_t cmd[] = {ISO14443B_WRITE_BLK, block_address, block[0], block[1], block[2], block[3], 0x00, 0x00}; + AddCrc14B(cmd, 6); + + uint32_t start_time = 0; + uint32_t eof_time = 0; + CodeAndTransmit14443bAsReader(cmd, sizeof(cmd), &start_time, &eof_time, true); + + return PM3_SUCCESS; +} + +/** + * @brief Write a block with retry mechanism + * @param block_address Block number to write + * @param target_value Value to write + * @return true if successful, false otherwise + */ +static bool st25tb_write_block_with_retry(uint8_t block_address, uint32_t target_value) { + uint32_t read_back_value = 0; + int max_retries = 5; + + if (st25tb_tear_off_retry_write_verify(block_address, target_value, max_retries, 0, &read_back_value) != 0) { + return false; + } + + return (read_back_value == target_value); +} + +/** + * @brief Write a block with tear-off capability + * @param block_address Block number to write + * @param data Data to write + * @param tearoff_delay_us Tear-off delay in microseconds + */ +static void st25tb_tear_off_write_block(uint8_t block_address, uint32_t data, uint16_t tearoff_delay_us) { + iso14443b_setup_light(); + + uint8_t block[ST25TB_BLOCK_SIZE]; + block[0] = (data & 0xFF); + block[1] = (data >> 8) & 0xFF; + block[2] = (data >> 16) & 0xFF; + block[3] = (data >> 24) & 0xFF; + + iso14b_card_select_t card; + int res = iso14443b_select_srx_card(&card); + if (res != PM3_SUCCESS) { + goto out; + } + + res = st25tb_cmd_write_block(block_address, block); + + // Tear off the communication at precise timing + SpinDelayUsPrecision(tearoff_delay_us); + FpgaWriteConfWord(FPGA_MAJOR_MODE_OFF); + +out: + RF_SWTICH_OFF(); +} + +/** + * @brief Write a block with retry and verification + * @param block_address Block address to write + * @param target_value Value to write + * @param max_try_count Maximum number of retries + * @param sleep_time_ms Sleep time between retries in milliseconds + * @param read_back_value Pointer to store read-back value + * @return 0 for success, -1 for failure + */ +static int8_t st25tb_tear_off_retry_write_verify(uint8_t block_address, uint32_t target_value, + uint32_t max_try_count, int sleep_time_ms, + uint32_t *read_back_value) { + int i = 0; + *read_back_value = ~target_value; // Initialize to ensure the loop runs at least once + + while (*read_back_value != target_value && i < max_try_count) { + st25tb_tear_off_write_block(block_address, target_value, 6000); // Long delay for reliability + if (sleep_time_ms > 0) SpinDelayUsPrecision(sleep_time_ms * 1000); + st25tb_tear_off_read_block(block_address, read_back_value); + if (sleep_time_ms > 0) SpinDelayUsPrecision(sleep_time_ms * 1000); + i++; + } + + return (*read_back_value == target_value) ? 0 : -1; +} + +/** + * @brief Check if a block's value is consolidated (stable) + * @param block_address Block address to check + * @param value Expected value + * @param repeat_read Number of reads to perform + * @param sleep_time_ms Sleep time between reads in milliseconds + * @param read_value Pointer to store read value + * @return 0 if consolidated, -1 otherwise + */ +static int8_t st25tb_tear_off_is_consolidated(const uint8_t block_address, uint32_t value, + int repeat_read, int sleep_time_ms, + uint32_t *read_value) { + int result; + for (int i = 0; i < repeat_read; i++) { + if (sleep_time_ms > 0) SpinDelayUsPrecision(sleep_time_ms * 1000); + result = st25tb_tear_off_read_block(block_address, read_value); + if (result != 0 || value != *read_value) { + return -1; // Read error or value changed + } + } + return 0; // Value remained stable +} + +/** + * @brief Consolidate a block to a stable state + * @param block_address Block address to consolidate + * @param current_value Current value + * @param target_value Target value + * @param read_back_value Pointer to store read-back value + * @return 0 for success, -1 for failure + */ +static int8_t st25tb_tear_off_consolidate_block(const uint8_t block_address, uint32_t current_value, + uint32_t target_value, uint32_t *read_back_value) { + int8_t result; + uint32_t consolidation_value; + + // Determine the value to write for consolidation based on target and current state + if (target_value <= 0xFFFFFFFD && current_value >= (target_value + 2)) { + consolidation_value = target_value + 2; + } else { + consolidation_value = current_value; + } + + // Try writing value - 1 + result = st25tb_tear_off_retry_write_verify(block_address, consolidation_value - 1, + TEAR_OFF_WRITE_RETRY_COUNT, 0, read_back_value); + if (result != 0) { + Dbprintf("Consolidation failed at step 1 (write 0x%08X)", consolidation_value - 1); + return -1; + } + + // If value is not FE or target is not FD, try writing value - 2 + if (*read_back_value != 0xFFFFFFFE || (*read_back_value == 0xFFFFFFFE && target_value == 0xFFFFFFFD)) { + result = st25tb_tear_off_retry_write_verify(block_address, consolidation_value - 2, + TEAR_OFF_WRITE_RETRY_COUNT, 0, read_back_value); + if (result != 0) { + Dbprintf("Consolidation failed at step 2 (write 0x%08X)", consolidation_value - 2); + return -1; + } + } + + // Final checks for stability of unstable high values (due to internal dual counters) + if (result == 0 && target_value > 0xFFFFFFFD && *read_back_value > 0xFFFFFFFD) { + result = st25tb_tear_off_is_consolidated(block_address, *read_back_value, + TEAR_OFF_CONSOLIDATE_READ_COUNT, 0, read_back_value); + if (result == 0) { + result = st25tb_tear_off_is_consolidated(block_address, *read_back_value, + TEAR_OFF_CONSOLIDATE_WAIT_READ_COUNT, + TEAR_OFF_CONSOLIDATE_WAIT_MS, read_back_value); + if (result != 0) { + Dbprintf("Consolidation failed stability check (long wait)"); + return -1; + } + } else { + Dbprintf("Consolidation failed stability check (short wait)"); + return -1; + } + } + + return 0; +} + +/** + * @brief Calculate next value for counter decrement + * @param current_value Current counter value + * @param randomness Whether to use randomization + * @return Next value to attempt + */ +static uint32_t st25tb_tear_off_next_value(uint32_t current_value, bool randomness) { + uint32_t value = 0; + int8_t index = 31; + + // Simple decrement for smaller values + if (current_value < 0x0000FFFF) { + return (current_value > 0) ? current_value - 1 : 0; + } + + // Loop through each bit starting from the most significant bit (MSB) + while (index >= 0) { + // Find the most significant '1' bit + if (value == 0 && IS_ONE_BIT(current_value, index)) { + // Create a mask with '1's up to this position + value = 0xFFFFFFFF >> (31 - index); + index--; // Move to the next bit + } + + // Once the first '1' is found, look for the first '0' after it + if (value != 0 && IS_ZERO_BIT(current_value, index)) { + index++; // Go back to the position of the '0' + // Clear the bit at this '0' position in our mask + value &= ~((uint32_t)1 << index); + + // Optional randomization: flip a random bit below the found '0' + if (randomness && value < 0xF0000000 && index > 1) { + value ^= ((uint32_t)1 << (dummy_rand() % index)); + } + return value; + } + + index--; + } + + return (current_value > 0) ? current_value - 1 : 0; +} + +/** + * @brief Adjust timing for tear-off operations + * @param tear_off_us Pointer to current tear-off timing + * @param tear_off_adjustment_us Adjustment amount + */ +static void st25tb_tear_off_adjust_timing(int *tear_off_us, uint32_t tear_off_adjustment_us) { + if (*tear_off_us > TEAR_OFF_START_OFFSET_US) { + *tear_off_us -= tear_off_adjustment_us; + } +} + +/** + * @brief Log tear-off operation details + * @param tear_off_us Current tear-off timing + * @param color Color code for output + * @param value Value being processed + */ +static void st25tb_tear_off_log(int tear_off_us, char *color, uint32_t value) { + char binaryRepresentation[33]; + for (int i = 31; i >= 0; i--) { + binaryRepresentation[31 - i] = IS_ONE_BIT(value, i) ? '1' : '0'; + } + binaryRepresentation[32] = '\0'; + Dbprintf("%s%08X%s : %s%s%s : %d us", color, value, RESET, color, binaryRepresentation, RESET, tear_off_us); +} + +/** + * @brief Main tear-off counter write function + * @param block_address Block address to write + * @param target_value Target value + * @param tear_off_adjustment_us Adjustment for tear-off timing + * @param safety_value Safety threshold to prevent going below + * @return 0 for success, non-zero for failure + */ +static int8_t st25tb_tear_off_write_counter(uint8_t block_address, uint32_t target_value, + uint32_t tear_off_adjustment_us, uint32_t safety_value) { + int result; + bool trigger = true; + + uint32_t read_value = 0; + uint32_t current_value = 0; + uint32_t last_consolidated_value = 0; + uint32_t tear_off_value = 0; + + int tear_off_us = TEAR_OFF_START_OFFSET_US; + if (tear_off_adjustment_us == 0) { + tear_off_adjustment_us = TEAR_OFF_ADJUSTMENT_US; + } + + // Initial read to get the current counter value + result = st25tb_tear_off_read_block(block_address, ¤t_value); + if (result != PM3_SUCCESS) { + Dbprintf("Initial read failed for block %d", block_address); + return -1; // Indicate failure + } + + // Calculate the first value to attempt writing via tear-off + tear_off_value = st25tb_tear_off_next_value(current_value, false); + + Dbprintf(" Target block: %d", block_address); + Dbprintf("Current value: 0x%08X", current_value); + Dbprintf(" Target value: 0x%08X", target_value); + Dbprintf(" Safety value: 0x%08X", safety_value); + Dbprintf("Adjustment us: %u", tear_off_adjustment_us); + + // Check if tear-off is even possible or needed + if (tear_off_value == 0 && current_value != 0) { + Dbprintf("Tear-off technique not possible from current value."); + return -1; + } + if (current_value == target_value) { + Dbprintf("Current value already matches target value."); + return 0; + } + + // Main tear-off loop + for (;;) { + // Safety check: ensure we don't go below the safety threshold + if (tear_off_value < safety_value) { + Dbprintf("Stopped. Safety threshold reached (next value 0x%08X < safety 0x%08X)", + tear_off_value, safety_value); + return -1; + } + + // Perform the tear-off write attempt + st25tb_tear_off_write_block(block_address, tear_off_value, tear_off_us); + + // Read back the value after the attempt + result = st25tb_tear_off_read_block(block_address, &read_value); + if (result != 0) { + continue; // Retry the loop if read fails (ex: tag is removed from the read for a short period) + } + + // Analyze the result and decide next action + if (read_value > current_value) { + // Partial write succeeded (successful tear-off) + if (read_value >= 0xFFFFFFFE || + (read_value - 2) > target_value || + read_value != last_consolidated_value || + ((read_value & 0xF0000000) > (current_value & 0xF0000000))) { // Major bit flip + + result = st25tb_tear_off_consolidate_block(block_address, read_value, + target_value, ¤t_value); + if (result == 0 && current_value == target_value) { + st25tb_tear_off_log(tear_off_us, GREEN, read_value); + Dbprintf("Target value 0x%08X reached successfully!", target_value); + return 0; + } + if (read_value != last_consolidated_value) { + st25tb_tear_off_adjust_timing(&tear_off_us, tear_off_adjustment_us); + } + last_consolidated_value = read_value; + tear_off_value = st25tb_tear_off_next_value(current_value, false); + trigger = true; + st25tb_tear_off_log(tear_off_us, GREEN, read_value); + } + } else if (read_value == tear_off_value) { + // Write succeeded completely (no tear-off effect) + if (trigger) { + tear_off_value = st25tb_tear_off_next_value(tear_off_value, true); + trigger = false; + } else { + tear_off_value = st25tb_tear_off_next_value(read_value, false); + trigger = true; + } + current_value = read_value; + st25tb_tear_off_adjust_timing(&tear_off_us, tear_off_adjustment_us); + st25tb_tear_off_log(tear_off_us, BLUE, read_value); + } else if (read_value < tear_off_value) { + // Partial write succeeded (successful tear-off) but lower value + tear_off_value = st25tb_tear_off_next_value(read_value, false); + st25tb_tear_off_adjust_timing(&tear_off_us, tear_off_adjustment_us); + current_value = read_value; + trigger = true; + st25tb_tear_off_log(tear_off_us, RED, read_value); + } + + // Increment tear-off timing for the next attempt + tear_off_us++; + + // Check for user interruption + WDT_HIT(); + if (BUTTON_PRESS()) { + DbpString("Tear-off stopped by user."); + return -1; + } + } + + return -1; +} + +//============================================================================= +// MAIN APPLICATION FUNCTIONS +//============================================================================= + +/** + * @brief Learn/store function implementation + */ +static void run_learn_function(void) { + st25tb_data_t temp_tag_data; // Temporary buffer to read into + memset(&temp_tag_data, 0, sizeof(temp_tag_data)); + + if (st25tb_tag_read(&temp_tag_data)) { + st25tb_tag_print(&temp_tag_data); + int slot_index = find_tag_by_uid(temp_tag_data.uid); + + if (slot_index != -1) { + Dbprintf("Tag with UID %llX already in Slot %d. Overwriting...", + temp_tag_data.uid, slot_index); + } else { + slot_index = find_free_tag_slot(); + if (slot_index == -1) { + DbpString("Collection full! Overwriting Slot 0."); + slot_index = 0; // Overwrite oldest/first slot if full + } else { + // Only increment if we are adding to a new slot, not overwriting + if (!g_stored_tags[slot_index].data_valid) { + g_valid_tag_count++; + } + } + } + + // Store tag data in collection + memcpy(&g_stored_tags[slot_index], &temp_tag_data, sizeof(st25tb_data_t)); + g_stored_tags[slot_index].data_valid = true; + Dbprintf("Stored tag in Slot %d. (UID: %llX)", slot_index, temp_tag_data.uid); + + // Save collection to flash + if (save_tags_to_flash(g_stored_tags)) { + DbpString("Collection saved to flash."); + } else { + DbpString(_RED_("Failed to save collection to flash!")); + } + + current_state = STATE_DONE; // Indicate success + } +} + +/** + * @brief Restore function implementation + */ +static void run_restore_function(void) { + iso14b_card_select_t current_tag_info; // To get UID of tag in field + + if (st25tb_tag_get_basic_info(¤t_tag_info)) { + // Tag found in field + uint64_t tag_uid = bytes_to_num_le(current_tag_info.uid, sizeof(uint64_t)); + int slot = find_tag_by_uid(tag_uid); + + if (slot != -1) { + Dbprintf("Found matching tag in Slot %d (UID: %llX). Restoring...", slot, tag_uid); + + current_state = STATE_BUSY; // Indicate busy during restore attempt + update_leds_mode(g_current_mode); + + bool success = st25tb_tag_restore(&g_stored_tags[slot]); + + if (success) { + DbpString(_GREEN_("Restore successful.")); + current_state = STATE_DONE; + } else { + DbpString(_RED_("Restore failed.")); + current_state = STATE_ERROR; + } + } else { + // Tag found but not in collection, remain busy to scan again + current_state = STATE_BUSY; + } + } else { + // No tag found, remain busy to scan again + current_state = STATE_BUSY; + } +} + +/** + * @brief Display module information + */ +void ModInfo(void) { + DbpString(" HF ST25TB Store/Restore"); + Dbprintf(" Data stored/restored from: %s", HF_ST25TB_MULTI_SR_FILE); + Dbprintf(" Supports up to %d tag slots.", MAX_SAVED_TAGS); +} + +/** + * @brief Main module function + */ +void RunMod(void) { + StandAloneMode(); + Dbprintf(_YELLOW_("HF ST25TB Store/Restore mode started")); + iso14443b_setup(); + LED_D_OFF(); + FpgaDownloadAndGo(FPGA_BITSTREAM_HF); // Use HF bitstream for ISO14443B + + // Initialize collection + for (int i = 0; i < MAX_SAVED_TAGS; i++) { + g_stored_tags[i].data_valid = false; + } + g_valid_tag_count = 0; + + // Mount filesystem and load previous tags if available + rdv40_spiffs_lazy_mount(); + if (load_tags_from_flash(g_stored_tags)) { + DbpString("Loaded previous tag collection from flash."); + // Count valid entries loaded + for (int i = 0; i < MAX_SAVED_TAGS; i++) { + if (g_stored_tags[i].data_valid) + g_valid_tag_count++; + } + g_current_mode = MODE_RESTORE; // Default to restore if data exists + } else { + DbpString("No previous tag data found in flash or error loading."); + g_current_mode = MODE_LEARN; // Default to store if no data + } + + bool mode_display_update = true; // Force initial display + current_state = STATE_BUSY; // Reset state at the beginning + + // Main application loop + for (;;) { + WDT_HIT(); + + // Exit condition: USB command received + if (data_available()) { + DbpString("USB data detected, exiting standalone mode."); + break; + } + + // --- Button Handling --- + int button_status = BUTTON_HELD(1000); // Check for 1 second hold + + if (button_status == BUTTON_HOLD) { + DbpString("Button held, exiting standalone mode."); + break; + } else if (button_status == BUTTON_SINGLE_CLICK) { + // Toggle between modes + g_current_mode = (g_current_mode == MODE_LEARN) ? MODE_RESTORE : MODE_LEARN; + current_state = STATE_BUSY; // Reset state when changing mode + mode_display_update = true; + SpinDelay(100); // Debounce/allow user to see mode change + } + + // --- Update Display (only if mode changed) --- + if (mode_display_update) { + if (g_current_mode == MODE_LEARN) { + Dbprintf("Mode: " _YELLOW_("Learn") ". (Cnt: %d/%d)", + g_valid_tag_count, MAX_SAVED_TAGS); + } else { + Dbprintf("Mode: " _BLUE_("Restore") ". (Cnt: %d/%d)", + g_valid_tag_count, MAX_SAVED_TAGS); + } + mode_display_update = false; + } + update_leds_mode(g_current_mode); + + // Process according to current state + if(current_state == STATE_BUSY) { + // Run appropriate function based on mode + if (g_current_mode == MODE_LEARN) { + run_learn_function(); + } else { // MODE_RESTORE + run_restore_function(); + } + } else if(current_state == STATE_DONE) { + indicate_success(); + } else { + indicate_failure(); + } + + // Loop delay + SpinDelay(100); + } + + // Clean up before exiting + LED_D_ON(); // Indicate potentially saving state on exit + rdv40_spiffs_lazy_unmount(); + LED_D_OFF(); + + switch_off(); // Turn off RF field + LEDsoff(); + DbpString("Exiting " _YELLOW_("HF ST25TB Store/Restore") " mode."); +} \ No newline at end of file diff --git a/armsrc/iso14443b.c b/armsrc/iso14443b.c index 0ce6c055d..0a42c3e97 100644 --- a/armsrc/iso14443b.c +++ b/armsrc/iso14443b.c @@ -1585,7 +1585,7 @@ static void CodeIso14443bAsReader(const uint8_t *cmd, int len, bool framing) { /* * Convenience function to encode, transmit and trace iso 14443b comms */ -static void CodeAndTransmit14443bAsReader(const uint8_t *cmd, int len, uint32_t *start_time, uint32_t *eof_time, bool framing) { +void CodeAndTransmit14443bAsReader(const uint8_t *cmd, int len, uint32_t *start_time, uint32_t *eof_time, bool framing) { const tosend_t *ts = get_tosend(); CodeIso14443bAsReader(cmd, len, framing); TransmitFor14443b_AsReader(start_time); @@ -1800,7 +1800,7 @@ static int iso14443b_select_cts_card(iso14b_cts_card_select_t *card) { /** * SRx Initialise. */ -static int iso14443b_select_srx_card(iso14b_card_select_t *card) { +int iso14443b_select_srx_card(iso14b_card_select_t *card) { // INITIATE command: wake up the tag using the INITIATE static const uint8_t init_srx[] = { ISO14443B_INITIATE, 0x00, 0x97, 0x5b }; uint8_t r_init[3] = { 0x00 }; @@ -2307,7 +2307,7 @@ void iso14443b_setup(void) { // // I tried to be systematic and check every answer of the tag, every CRC, etc... //----------------------------------------------------------------------------- -static int read_14b_srx_block(uint8_t blocknr, uint8_t *block) { +int read_14b_srx_block(uint8_t blocknr, uint8_t *block) { uint8_t cmd[] = {ISO14443B_READ_BLK, blocknr, 0x00, 0x00}; AddCrc14B(cmd, 2); diff --git a/armsrc/iso14443b.h b/armsrc/iso14443b.h index 70455ac15..7dadda7bd 100644 --- a/armsrc/iso14443b.h +++ b/armsrc/iso14443b.h @@ -45,8 +45,11 @@ int iso14443b_select_card(iso14b_card_select_t *card); void SimulateIso14443bTag(const uint8_t *pupi); void read_14b_st_block(uint8_t blocknr); +int read_14b_srx_block(uint8_t blocknr, uint8_t *block); +int iso14443b_select_srx_card(iso14b_card_select_t *card); void SniffIso14443b(void); void SendRawCommand14443B(iso14b_raw_cmd_t *p); +void CodeAndTransmit14443bAsReader(const uint8_t *cmd, int len, uint32_t *start_time, uint32_t *eof_time, bool framing); // States for 14B SIM command #define SIM_POWER_OFF 0 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 e79863faa..0beddec7c 100644 --- a/doc/md/Use_of_Proxmark/4_Advanced-compilation-parameters.md +++ b/doc/md/Use_of_Proxmark/4_Advanced-compilation-parameters.md @@ -140,6 +140,7 @@ Here are the supported values you can assign to `STANDALONE` in `Makefile.platfo | HF_MFCSIM | Simulate Mifare Classic 1k card storing in flashmem - Ray Lee | HF_MSDSAL | EMV Read and emulation - Salvador Mendoza | HF_REBLAY | 14A relay over BT - Salvador Mendoza +| HF_ST25_TEAROFF | Store/restore ST25TB tags with tear-off for counters - SecLabz | HF_TCPRST | IKEA Rothult ST25TA, Standalone Master Key Dump/Emulation - Nick Draffen | HF_TMUDFORD | Read and emulate ISO15693 card UID - Tim Mudford | HF_UNISNIFF | Combined 14a/14b/15 sniffer with runtime selection & extra save options