From e1e023ea61f10389be87602ff7c6910658e70ac5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=AD=90=E6=AD=AA?= Date: Mon, 21 Sep 2026 17:11:23 +0800 Subject: [PATCH 01/14] feat: 15693 csetuid v1 v3 check before write --- client/src/cmdhf15.c | 127 +++++++++++++++++++++++++++++++++++++------ 1 file changed, 109 insertions(+), 18 deletions(-) diff --git a/client/src/cmdhf15.c b/client/src/cmdhf15.c index b2f450699..6a4d8a653 100644 --- a/client/src/cmdhf15.c +++ b/client/src/cmdhf15.c @@ -3340,8 +3340,8 @@ static const uint8_t iso15_magic_v3_sig_b[4] = {0x21, 0xAE, 0x93, 0x00}; static const uint8_t iso15_magic_v3_fin_a[4] = {0xA5, 0x2B, 0x44, 0x2C}; static const uint8_t iso15_magic_v3_fin_b[4] = {0x69, 0xE2, 0x5D, 0x00}; -// Read one 4-byte block from a magic V3 tag in unaddressed mode. -static int hf15_magic_v3_read_blk(uint8_t blockno, uint8_t out[4]) { +// Read one 4-byte block from a magic tag in unaddressed mode. +static int hf15_magic_read_blk(uint8_t blockno, uint8_t out[4]) { uint16_t approxlen = 2 + 1 + 2; iso15_raw_cmd_t *packet = (iso15_raw_cmd_t *)calloc(1, sizeof(iso15_raw_cmd_t) + approxlen); @@ -3378,8 +3378,8 @@ static int hf15_magic_v3_read_blk(uint8_t blockno, uint8_t out[4]) { return PM3_SUCCESS; } -// Write one 4-byte block to a magic V3 tag in unaddressed mode. -static int hf15_magic_v3_write_blk(uint8_t blockno, const uint8_t *data) { +// Write one 4-byte block to a magic tag in unaddressed mode. +static int hf15_magic_write_blk(uint8_t blockno, const uint8_t *data) { uint16_t flags = arg_get_raw_flag(0, true, false, false); return hf_15_write_blk(NULL, flags, NULL, true, blockno, data, 4); } @@ -3388,15 +3388,78 @@ static int hf15_magic_v3_write_blk(uint8_t blockno, const uint8_t *data) { static bool hf15_magic_v3_is_config_mode(void) { uint8_t a[4] = {0}; uint8_t b[4] = {0}; - if (hf15_magic_v3_read_blk(ISO15_MAGIC_V3_BLK_SIG_A, a) != PM3_SUCCESS) { + if (hf15_magic_read_blk(ISO15_MAGIC_V3_BLK_SIG_A, a) != PM3_SUCCESS) { return false; } - if (hf15_magic_v3_read_blk(ISO15_MAGIC_V3_BLK_SIG_B, b) != PM3_SUCCESS) { + if (hf15_magic_read_blk(ISO15_MAGIC_V3_BLK_SIG_B, b) != PM3_SUCCESS) { return false; } return (memcmp(a, iso15_magic_v3_sig_a, 4) == 0) && (memcmp(b, iso15_magic_v3_sig_b, 4) == 0); } +// ISO15693 magic "Gen1" (backdoor) tag support +#define ISO15_MAGIC_GEN1_BLK_UID_LO 0x38 +#define ISO15_MAGIC_GEN1_BLK_UID_HI 0x39 +#define ISO15_MAGIC_GEN1_BLK_UNLOCK_A 0x3E +#define ISO15_MAGIC_GEN1_BLK_UNLOCK_B 0x3F + +// backdoor "unlock" write in block 0x3E/0x3F, see SetTag15693Uid() in armsrc/iso15693.c +static const uint8_t iso15_magic_gen1_unlock_a[4] = {0x00, 0x00, 0x00, 0x00}; +static const uint8_t iso15_magic_gen1_unlock_b[4] = {0x69, 0x96, 0x00, 0x00}; + +// Blocks touched by the Gen1 backdoor UID sequence. On a genuine Gen1 magic tag these +// are not real user memory (reading them back fails), so a non-zero readback strongly +// suggests this is actually a normal tag whose data we are about to overwrite. +static const uint8_t iso15_magic_gen1_blocks[] = { + ISO15_MAGIC_GEN1_BLK_UID_LO, + ISO15_MAGIC_GEN1_BLK_UID_HI, + ISO15_MAGIC_GEN1_BLK_UNLOCK_A, + ISO15_MAGIC_GEN1_BLK_UNLOCK_B, +}; + +// Safety: refuse to run the Gen1 backdoor sequence if any of its target blocks read +// back as real (non-zero) data instead of failing / being blank. +static bool hf15_magic_gen1_is_safe_to_write(void) { + uint8_t buf[4] = {0}; + uint8_t zero[4] = {0}; + for (size_t i = 0; i < ARRAYLEN(iso15_magic_gen1_blocks); i++) { + uint8_t blockno = iso15_magic_gen1_blocks[i]; + if (hf15_magic_read_blk(blockno, buf) == PM3_SUCCESS && memcmp(buf, zero, 4) != 0) { + PrintAndLogEx(WARNING, "block 0x%02X reads back real data ( " _RED_("%s") " )", blockno, sprint_hex(buf, 4)); + return false; + } + } + return true; +} + +// Write the Gen1 backdoor UID sequence: unlock blocks, then the UID halves. +// The unlock writes (0x3E/0x3F) are best-effort: on some real Gen1 chips they come +// back as "command failed" even though the chip is already (permanently) writable, +// so a failure there must not abort the sequence - only the actual UID blocks matter, +// and the final UID readback in the caller is what really decides success or failure. +static int hf15_magic_gen1_write_uid(const uint8_t *uid) { + uint8_t blk_lo[4] = {0}; + uint8_t blk_hi[4] = {0}; + reverse_array_copy(uid + 4, 4, blk_lo); + reverse_array_copy(uid, 4, blk_hi); + + hf15_magic_write_blk(ISO15_MAGIC_GEN1_BLK_UNLOCK_A, iso15_magic_gen1_unlock_a); + hf15_magic_write_blk(ISO15_MAGIC_GEN1_BLK_UNLOCK_B, iso15_magic_gen1_unlock_b); + + if (hf15_magic_write_blk(ISO15_MAGIC_GEN1_BLK_UID_LO, blk_lo) != PM3_SUCCESS || + hf15_magic_write_blk(ISO15_MAGIC_GEN1_BLK_UID_HI, blk_hi) != PM3_SUCCESS) { + return PM3_ESOFT; + } + return PM3_SUCCESS; +} + +// Best-effort rollback after a failed verify: blank the UID blocks back out. +static void hf15_magic_gen1_rollback_uid(void) { + uint8_t zero[4] = {0x00, 0x00, 0x00, 0x00}; + hf15_magic_write_blk(ISO15_MAGIC_GEN1_BLK_UID_LO, zero); + hf15_magic_write_blk(ISO15_MAGIC_GEN1_BLK_UID_HI, zero); +} + /** * Commandline handling: HF15 CMD CSETUID * Set UID for magic Chinese card @@ -3406,6 +3469,8 @@ static int CmdHF15CSetUID(const char *Cmd) { CLIParserContext *ctx; CLIParserInit(&ctx, "hf 15 csetuid", "Set UID for magic Chinese card (only works with such cards)\n" + "Gen1 and V3 first probe their backdoor blocks and refuse to write if they\n" + "read back real data, to avoid corrupting a normal (non-magic) tag.\n" "For magic 'V3' tags this writes the UID configuration only and is repeatable;\n" "run `" _YELLOW_("hf 15 cfinalize") "` afterwards to lock the UID permanently.", "hf 15 csetuid -u E011223344556677 -> use gen1 command\n" @@ -3459,7 +3524,16 @@ static int CmdHF15CSetUID(const char *Cmd) { PrintAndLogEx(INFO, "Writing..."); + bool used_gen1 = false; + if (use_v3) { + // Safety: only proceed on a tag that is actually an un-finalized magic V3. + if (hf15_magic_v3_is_config_mode() == false) { + PrintAndLogEx(FAILED, "tag is not an un-finalized magic " _YELLOW_("V3") " tag"); + PrintAndLogEx(HINT, "Hint: signature in blocks 0x14/0x15 not found - already finalized or not a V3 tag"); + return PM3_ESOFT; + } + /* for example id: E011223344556677 [=] 16 | 77 66 55 44 | 0 | wfUD @@ -3470,28 +3544,40 @@ static int CmdHF15CSetUID(const char *Cmd) { reverse_array_copy(payload.uid + 4, 4, blk_lo); reverse_array_copy(payload.uid, 4, blk_hi); - if (hf15_magic_v3_write_blk(ISO15_MAGIC_V3_BLK_UID_LO, blk_lo) != PM3_SUCCESS || - hf15_magic_v3_write_blk(ISO15_MAGIC_V3_BLK_UID_HI, blk_hi) != PM3_SUCCESS) { + if (hf15_magic_write_blk(ISO15_MAGIC_V3_BLK_UID_LO, blk_lo) != PM3_SUCCESS || + hf15_magic_write_blk(ISO15_MAGIC_V3_BLK_UID_HI, blk_hi) != PM3_SUCCESS) { PrintAndLogEx(FAILED, "Setting new UID ( " _RED_("fail") " )"); PrintAndLogEx(NORMAL, ""); return PM3_ESOFT; } - } else { + } else if (use_v2) { PacketResponseNG resp; clearCommandBuffer(); - uint16_t cmd = CMD_HF_ISO15693_CSETUID; - if (use_v2) { - cmd = CMD_HF_ISO15693_CSETUID_V2; - } - - SendCommandNG(cmd, (uint8_t *)&payload, sizeof(payload)); - if (WaitForResponseTimeout(cmd, &resp, 2000) == false) { + SendCommandNG(CMD_HF_ISO15693_CSETUID_V2, (uint8_t *)&payload, sizeof(payload)); + if (WaitForResponseTimeout(CMD_HF_ISO15693_CSETUID_V2, &resp, 2000) == false) { PrintAndLogEx(WARNING, "timeout while waiting for reply"); DropField(); return PM3_ESOFT; } + + } else { + // Safety: bail out before writing anything if the Gen1 backdoor blocks + // read back as real data - that would mean this isn't a blank/magic tag. + if (hf15_magic_gen1_is_safe_to_write() == false) { + PrintAndLogEx(FAILED, "tag doesn't look like a blank/magic " _YELLOW_("Gen1") " tag ( " _RED_("fail") " )"); + PrintAndLogEx(HINT, "Hint: block 0x38/0x39/0x3E/0x3F read back real data - refusing to risk overwriting a normal tag"); + return PM3_ESOFT; + } + + used_gen1 = true; + + if (hf15_magic_gen1_write_uid(payload.uid) != PM3_SUCCESS) { + PrintAndLogEx(FAILED, "Setting new UID ( " _RED_("fail") " )"); + PrintAndLogEx(NORMAL, ""); + return PM3_ESOFT; + } } PrintAndLogEx(INFO, "Verifying..."); @@ -3511,6 +3597,11 @@ static int CmdHF15CSetUID(const char *Cmd) { return PM3_SUCCESS;; } + if (used_gen1) { + PrintAndLogEx(INFO, "Rolling back block 0x38/0x39 to blank..."); + hf15_magic_gen1_rollback_uid(); + } + PrintAndLogEx(FAILED, "Setting new UID ( " _RED_("fail") " )"); PrintAndLogEx(NORMAL, ""); return PM3_ESOFT; @@ -3566,8 +3657,8 @@ static int CmdHF15CFinalize(const char *Cmd) { PrintAndLogEx(INFO, "Finalizing..."); - if (hf15_magic_v3_write_blk(ISO15_MAGIC_V3_BLK_SIG_A, iso15_magic_v3_fin_a) != PM3_SUCCESS || - hf15_magic_v3_write_blk(ISO15_MAGIC_V3_BLK_SIG_B, iso15_magic_v3_fin_b) != PM3_SUCCESS) { + if (hf15_magic_write_blk(ISO15_MAGIC_V3_BLK_SIG_A, iso15_magic_v3_fin_a) != PM3_SUCCESS || + hf15_magic_write_blk(ISO15_MAGIC_V3_BLK_SIG_B, iso15_magic_v3_fin_b) != PM3_SUCCESS) { PrintAndLogEx(FAILED, "Finalize ( " _RED_("fail") " )"); PrintAndLogEx(NORMAL, ""); return PM3_ESOFT; From 473dcb0aa5c2508ab1b4e0045a9a2c24ee3449e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=AD=90=E6=AD=AA?= Date: Mon, 21 Sep 2026 17:34:37 +0800 Subject: [PATCH 02/14] fix: v1 tag probing fail message clear --- client/src/cmdhf15.c | 46 +++++++++++++++++++++++++++++++++----------- 1 file changed, 35 insertions(+), 11 deletions(-) diff --git a/client/src/cmdhf15.c b/client/src/cmdhf15.c index 6a4d8a653..ea02506d3 100644 --- a/client/src/cmdhf15.c +++ b/client/src/cmdhf15.c @@ -3340,13 +3340,12 @@ static const uint8_t iso15_magic_v3_sig_b[4] = {0x21, 0xAE, 0x93, 0x00}; static const uint8_t iso15_magic_v3_fin_a[4] = {0xA5, 0x2B, 0x44, 0x2C}; static const uint8_t iso15_magic_v3_fin_b[4] = {0x69, 0xE2, 0x5D, 0x00}; -// Read one 4-byte block from a magic tag in unaddressed mode. +// Read one 4-byte block from a magic tag in unaddressed mode. for probing static int hf15_magic_read_blk(uint8_t blockno, uint8_t out[4]) { uint16_t approxlen = 2 + 1 + 2; iso15_raw_cmd_t *packet = (iso15_raw_cmd_t *)calloc(1, sizeof(iso15_raw_cmd_t) + approxlen); if (packet == NULL) { - PrintAndLogEx(WARNING, "Failed to allocate memory"); return PM3_EMALLOC; } @@ -3364,15 +3363,27 @@ static int hf15_magic_read_blk(uint8_t blockno, uint8_t out[4]) { PacketResponseNG resp; if (WaitForResponseTimeout(CMD_HF_ISO15693_COMMAND, &resp, 2000) == false) { - PrintAndLogEx(DEBUG, "iso15693 timeout"); return PM3_ETIMEOUT; } - ISO15_ERROR_HANDLING_RESPONSE + if (resp.status == PM3_ETEAROFF) { + return resp.status; + } + if (resp.length < 2) { + return PM3_EWRONGANSWER; + } uint8_t *d = resp.data.asBytes; - ISO15_ERROR_HANDLING_CARD_RESPONSE(d, resp.length) + if (check_crc(CRC_15693, d, resp.length) == false) { + return PM3_ECRC; + } + if ((d[0] & ISO15_RES_ERROR) == ISO15_RES_ERROR) { + if (d[1] == 0x0F || d[1] == 0x10) { + return PM3_EOUTOFBOUND; + } + return PM3_EWRONGANSWER; + } memcpy(out, d + 2, 4); return PM3_SUCCESS; @@ -3418,16 +3429,24 @@ static const uint8_t iso15_magic_gen1_blocks[] = { }; // Safety: refuse to run the Gen1 backdoor sequence if any of its target blocks read -// back as real (non-zero) data instead of failing / being blank. +// back as real data instead of failing / being blank. Blank (00000000) and the +// unlock-B pattern (69960000) are both whitelisted: they're either genuinely blank or +// leftover from our own unlock write on a previous (interrupted / rolled-back) attempt +// via this same command, which is far more likely than a real tag's user data +// colliding byte-for-byte with one of these two fixed constants. static bool hf15_magic_gen1_is_safe_to_write(void) { uint8_t buf[4] = {0}; - uint8_t zero[4] = {0}; + static const uint8_t zero[4] = {0x00, 0x00, 0x00, 0x00}; for (size_t i = 0; i < ARRAYLEN(iso15_magic_gen1_blocks); i++) { uint8_t blockno = iso15_magic_gen1_blocks[i]; - if (hf15_magic_read_blk(blockno, buf) == PM3_SUCCESS && memcmp(buf, zero, 4) != 0) { - PrintAndLogEx(WARNING, "block 0x%02X reads back real data ( " _RED_("%s") " )", blockno, sprint_hex(buf, 4)); - return false; + if (hf15_magic_read_blk(blockno, buf) != PM3_SUCCESS) { + continue; } + if (memcmp(buf, zero, 4) == 0 || memcmp(buf, iso15_magic_gen1_unlock_b, 4) == 0) { + continue; + } + PrintAndLogEx(WARNING, "block 0x%02X reads back real data ( " _RED_("%s") " )", blockno, sprint_hex(buf, 4)); + return false; } return true; } @@ -3453,11 +3472,16 @@ static int hf15_magic_gen1_write_uid(const uint8_t *uid) { return PM3_SUCCESS; } -// Best-effort rollback after a failed verify: blank the UID blocks back out. +// Best-effort rollback after a failed verify: blank all four blocks back out, +// including the unlock blocks - otherwise a failed attempt leaves 0x3E/0x3F holding +// the unlock pattern, which would need the whitelist above to not falsely trip the +// safety check on a later run against the same tag. static void hf15_magic_gen1_rollback_uid(void) { uint8_t zero[4] = {0x00, 0x00, 0x00, 0x00}; hf15_magic_write_blk(ISO15_MAGIC_GEN1_BLK_UID_LO, zero); hf15_magic_write_blk(ISO15_MAGIC_GEN1_BLK_UID_HI, zero); + hf15_magic_write_blk(ISO15_MAGIC_GEN1_BLK_UNLOCK_A, zero); + hf15_magic_write_blk(ISO15_MAGIC_GEN1_BLK_UNLOCK_B, zero); } /** From f7a9682a9e9d5d88ee125248a2dd7e9d1624d322 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=AD=90=E6=AD=AA?= Date: Mon, 21 Sep 2026 18:06:42 +0800 Subject: [PATCH 03/14] fix: silent v1 pre-check and refine the commands --- client/src/cmdhf15.c | 47 +++++++++++++++++++------------------------- 1 file changed, 20 insertions(+), 27 deletions(-) diff --git a/client/src/cmdhf15.c b/client/src/cmdhf15.c index ea02506d3..8e635d9cb 100644 --- a/client/src/cmdhf15.c +++ b/client/src/cmdhf15.c @@ -3340,7 +3340,7 @@ static const uint8_t iso15_magic_v3_sig_b[4] = {0x21, 0xAE, 0x93, 0x00}; static const uint8_t iso15_magic_v3_fin_a[4] = {0xA5, 0x2B, 0x44, 0x2C}; static const uint8_t iso15_magic_v3_fin_b[4] = {0x69, 0xE2, 0x5D, 0x00}; -// Read one 4-byte block from a magic tag in unaddressed mode. for probing +// Unaddressed 4-byte block read, silent because failed probes are expected static int hf15_magic_read_blk(uint8_t blockno, uint8_t out[4]) { uint16_t approxlen = 2 + 1 + 2; @@ -3414,13 +3414,11 @@ static bool hf15_magic_v3_is_config_mode(void) { #define ISO15_MAGIC_GEN1_BLK_UNLOCK_A 0x3E #define ISO15_MAGIC_GEN1_BLK_UNLOCK_B 0x3F -// backdoor "unlock" write in block 0x3E/0x3F, see SetTag15693Uid() in armsrc/iso15693.c +// Gen1 unlock values for blocks 0x3E/0x3F static const uint8_t iso15_magic_gen1_unlock_a[4] = {0x00, 0x00, 0x00, 0x00}; static const uint8_t iso15_magic_gen1_unlock_b[4] = {0x69, 0x96, 0x00, 0x00}; -// Blocks touched by the Gen1 backdoor UID sequence. On a genuine Gen1 magic tag these -// are not real user memory (reading them back fails), so a non-zero readback strongly -// suggests this is actually a normal tag whose data we are about to overwrite. +// Gen1 backdoor blocks static const uint8_t iso15_magic_gen1_blocks[] = { ISO15_MAGIC_GEN1_BLK_UID_LO, ISO15_MAGIC_GEN1_BLK_UID_HI, @@ -3428,12 +3426,7 @@ static const uint8_t iso15_magic_gen1_blocks[] = { ISO15_MAGIC_GEN1_BLK_UNLOCK_B, }; -// Safety: refuse to run the Gen1 backdoor sequence if any of its target blocks read -// back as real data instead of failing / being blank. Blank (00000000) and the -// unlock-B pattern (69960000) are both whitelisted: they're either genuinely blank or -// leftover from our own unlock write on a previous (interrupted / rolled-back) attempt -// via this same command, which is far more likely than a real tag's user data -// colliding byte-for-byte with one of these two fixed constants. +// Refuse if a Gen1 backdoor block reads real data, 00000000 and 69960000 pass static bool hf15_magic_gen1_is_safe_to_write(void) { uint8_t buf[4] = {0}; static const uint8_t zero[4] = {0x00, 0x00, 0x00, 0x00}; @@ -3451,19 +3444,23 @@ static bool hf15_magic_gen1_is_safe_to_write(void) { return true; } -// Write the Gen1 backdoor UID sequence: unlock blocks, then the UID halves. -// The unlock writes (0x3E/0x3F) are best-effort: on some real Gen1 chips they come -// back as "command failed" even though the chip is already (permanently) writable, -// so a failure there must not abort the sequence - only the actual UID blocks matter, -// and the final UID readback in the caller is what really decides success or failure. +// Silent write, real Gen1 tags reject 0x3E/0x3F once they were written before +static void hf15_magic_write_blk_quiet(uint8_t blockno, const uint8_t *data) { + uint8_t old_printAndLog = g_printAndLog; + g_printAndLog &= ~PRINTANDLOG_PRINT; + hf15_magic_write_blk(blockno, data); + g_printAndLog = old_printAndLog; +} + +// Unlock writes are best-effort, only the UID blocks and the final UID readback decide static int hf15_magic_gen1_write_uid(const uint8_t *uid) { uint8_t blk_lo[4] = {0}; uint8_t blk_hi[4] = {0}; reverse_array_copy(uid + 4, 4, blk_lo); reverse_array_copy(uid, 4, blk_hi); - hf15_magic_write_blk(ISO15_MAGIC_GEN1_BLK_UNLOCK_A, iso15_magic_gen1_unlock_a); - hf15_magic_write_blk(ISO15_MAGIC_GEN1_BLK_UNLOCK_B, iso15_magic_gen1_unlock_b); + hf15_magic_write_blk_quiet(ISO15_MAGIC_GEN1_BLK_UNLOCK_A, iso15_magic_gen1_unlock_a); + hf15_magic_write_blk_quiet(ISO15_MAGIC_GEN1_BLK_UNLOCK_B, iso15_magic_gen1_unlock_b); if (hf15_magic_write_blk(ISO15_MAGIC_GEN1_BLK_UID_LO, blk_lo) != PM3_SUCCESS || hf15_magic_write_blk(ISO15_MAGIC_GEN1_BLK_UID_HI, blk_hi) != PM3_SUCCESS) { @@ -3472,16 +3469,13 @@ static int hf15_magic_gen1_write_uid(const uint8_t *uid) { return PM3_SUCCESS; } -// Best-effort rollback after a failed verify: blank all four blocks back out, -// including the unlock blocks - otherwise a failed attempt leaves 0x3E/0x3F holding -// the unlock pattern, which would need the whitelist above to not falsely trip the -// safety check on a later run against the same tag. +// Blank all four blocks again so a failed attempt leaves no unlock residue static void hf15_magic_gen1_rollback_uid(void) { uint8_t zero[4] = {0x00, 0x00, 0x00, 0x00}; hf15_magic_write_blk(ISO15_MAGIC_GEN1_BLK_UID_LO, zero); hf15_magic_write_blk(ISO15_MAGIC_GEN1_BLK_UID_HI, zero); - hf15_magic_write_blk(ISO15_MAGIC_GEN1_BLK_UNLOCK_A, zero); - hf15_magic_write_blk(ISO15_MAGIC_GEN1_BLK_UNLOCK_B, zero); + hf15_magic_write_blk_quiet(ISO15_MAGIC_GEN1_BLK_UNLOCK_A, zero); + hf15_magic_write_blk_quiet(ISO15_MAGIC_GEN1_BLK_UNLOCK_B, zero); } /** @@ -3551,7 +3545,7 @@ static int CmdHF15CSetUID(const char *Cmd) { bool used_gen1 = false; if (use_v3) { - // Safety: only proceed on a tag that is actually an un-finalized magic V3. + // Only proceed on an un-finalized magic V3 tag if (hf15_magic_v3_is_config_mode() == false) { PrintAndLogEx(FAILED, "tag is not an un-finalized magic " _YELLOW_("V3") " tag"); PrintAndLogEx(HINT, "Hint: signature in blocks 0x14/0x15 not found - already finalized or not a V3 tag"); @@ -3587,8 +3581,7 @@ static int CmdHF15CSetUID(const char *Cmd) { } } else { - // Safety: bail out before writing anything if the Gen1 backdoor blocks - // read back as real data - that would mean this isn't a blank/magic tag. + // Bail out before writing if the Gen1 backdoor blocks hold real data if (hf15_magic_gen1_is_safe_to_write() == false) { PrintAndLogEx(FAILED, "tag doesn't look like a blank/magic " _YELLOW_("Gen1") " tag ( " _RED_("fail") " )"); PrintAndLogEx(HINT, "Hint: block 0x38/0x39/0x3E/0x3F read back real data - refusing to risk overwriting a normal tag"); From cd24d8b0b061c56c37ad6137eb0b5e92c6854b76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=AD=90=E6=AD=AA?= Date: Mon, 21 Sep 2026 18:27:45 +0800 Subject: [PATCH 04/14] fix: rewind if 0x38 0x39 write fails --- client/src/cmdhf15.c | 1 + 1 file changed, 1 insertion(+) diff --git a/client/src/cmdhf15.c b/client/src/cmdhf15.c index 8e635d9cb..94f3a5576 100644 --- a/client/src/cmdhf15.c +++ b/client/src/cmdhf15.c @@ -3591,6 +3591,7 @@ static int CmdHF15CSetUID(const char *Cmd) { used_gen1 = true; if (hf15_magic_gen1_write_uid(payload.uid) != PM3_SUCCESS) { + hf15_magic_gen1_rollback_uid(); PrintAndLogEx(FAILED, "Setting new UID ( " _RED_("fail") " )"); PrintAndLogEx(NORMAL, ""); return PM3_ESOFT; From e74bb0f2e582054d2c83d5f2e3da9f459c600024 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=AD=90=E6=AD=AA?= Date: Tue, 22 Sep 2026 03:25:47 +0800 Subject: [PATCH 05/14] fix: 15693 csetuid v1 probe, single session write and safer rollback - probe: only no answer / block not available count as unreadable, other read errors abort - 69960000 is only accepted in 0x3F, the other blocks must be blank - write 0x3E/0x3F/0x38/0x39 in one RF session - rollback only when the tag still shows its original UID, message lists all four blocks Co-Authored-By: Claude Sonnet 5 --- client/src/cmdhf15.c | 81 ++++++++++++++++++++++++++++---------------- 1 file changed, 52 insertions(+), 29 deletions(-) diff --git a/client/src/cmdhf15.c b/client/src/cmdhf15.c index 94f3a5576..22f712b48 100644 --- a/client/src/cmdhf15.c +++ b/client/src/cmdhf15.c @@ -3382,7 +3382,7 @@ static int hf15_magic_read_blk(uint8_t blockno, uint8_t out[4]) { if (d[1] == 0x0F || d[1] == 0x10) { return PM3_EOUTOFBOUND; } - return PM3_EWRONGANSWER; + return PM3_EFAILED; } memcpy(out, d + 2, 4); @@ -3390,9 +3390,9 @@ static int hf15_magic_read_blk(uint8_t blockno, uint8_t out[4]) { } // Write one 4-byte block to a magic tag in unaddressed mode. -static int hf15_magic_write_blk(uint8_t blockno, const uint8_t *data) { +static int hf15_magic_write_blk(uint8_t blockno, const uint8_t *data, const uint8_t *pm3flags) { uint16_t flags = arg_get_raw_flag(0, true, false, false); - return hf_15_write_blk(NULL, flags, NULL, true, blockno, data, 4); + return hf_15_write_blk(pm3flags, flags, NULL, true, blockno, data, 4); } // Detect an un-finalized magic V3 tag by its configuration-mode signature. @@ -3426,16 +3426,26 @@ static const uint8_t iso15_magic_gen1_blocks[] = { ISO15_MAGIC_GEN1_BLK_UNLOCK_B, }; -// Refuse if a Gen1 backdoor block reads real data, 00000000 and 69960000 pass +// Refuse if a Gen1 backdoor block reads real data or fails in an unexpected way static bool hf15_magic_gen1_is_safe_to_write(void) { uint8_t buf[4] = {0}; static const uint8_t zero[4] = {0x00, 0x00, 0x00, 0x00}; for (size_t i = 0; i < ARRAYLEN(iso15_magic_gen1_blocks); i++) { uint8_t blockno = iso15_magic_gen1_blocks[i]; - if (hf15_magic_read_blk(blockno, buf) != PM3_SUCCESS) { + int res = hf15_magic_read_blk(blockno, buf); + + // no answer or block not available is what a Gen1 backdoor block does + if (res == PM3_EWRONGANSWER || res == PM3_EOUTOFBOUND) { continue; } - if (memcmp(buf, zero, 4) == 0 || memcmp(buf, iso15_magic_gen1_unlock_b, 4) == 0) { + if (res != PM3_SUCCESS) { + PrintAndLogEx(WARNING, "block 0x%02X read failed unexpectedly ( " _RED_("%d") " )", blockno, res); + return false; + } + + // blank, or 69960000 in 0x3F which an earlier unlock write left behind + if (memcmp(buf, zero, 4) == 0 || + (blockno == ISO15_MAGIC_GEN1_BLK_UNLOCK_B && memcmp(buf, iso15_magic_gen1_unlock_b, 4) == 0)) { continue; } PrintAndLogEx(WARNING, "block 0x%02X reads back real data ( " _RED_("%s") " )", blockno, sprint_hex(buf, 4)); @@ -3445,37 +3455,47 @@ static bool hf15_magic_gen1_is_safe_to_write(void) { } // Silent write, real Gen1 tags reject 0x3E/0x3F once they were written before -static void hf15_magic_write_blk_quiet(uint8_t blockno, const uint8_t *data) { +static void hf15_magic_write_blk_quiet(uint8_t blockno, const uint8_t *data, const uint8_t *pm3flags) { uint8_t old_printAndLog = g_printAndLog; g_printAndLog &= ~PRINTANDLOG_PRINT; - hf15_magic_write_blk(blockno, data); + hf15_magic_write_blk(blockno, data, pm3flags); g_printAndLog = old_printAndLog; } -// Unlock writes are best-effort, only the UID blocks and the final UID readback decide +// One RF session for all four writes, the unlock writes are best-effort static int hf15_magic_gen1_write_uid(const uint8_t *uid) { uint8_t blk_lo[4] = {0}; uint8_t blk_hi[4] = {0}; reverse_array_copy(uid + 4, 4, blk_lo); reverse_array_copy(uid, 4, blk_hi); - hf15_magic_write_blk_quiet(ISO15_MAGIC_GEN1_BLK_UNLOCK_A, iso15_magic_gen1_unlock_a); - hf15_magic_write_blk_quiet(ISO15_MAGIC_GEN1_BLK_UNLOCK_B, iso15_magic_gen1_unlock_b); + uint8_t pm3flags = (ISO15_CONNECT | ISO15_READ_RESPONSE | ISO15_LONG_WAIT | ISO15_HIGH_SPEED | ISO15_NO_DISCONNECT); + hf15_magic_write_blk_quiet(ISO15_MAGIC_GEN1_BLK_UNLOCK_A, iso15_magic_gen1_unlock_a, &pm3flags); + pm3flags &= ~ISO15_CONNECT; + hf15_magic_write_blk_quiet(ISO15_MAGIC_GEN1_BLK_UNLOCK_B, iso15_magic_gen1_unlock_b, &pm3flags); - if (hf15_magic_write_blk(ISO15_MAGIC_GEN1_BLK_UID_LO, blk_lo) != PM3_SUCCESS || - hf15_magic_write_blk(ISO15_MAGIC_GEN1_BLK_UID_HI, blk_hi) != PM3_SUCCESS) { - return PM3_ESOFT; + int res = hf15_magic_write_blk(ISO15_MAGIC_GEN1_BLK_UID_LO, blk_lo, &pm3flags); + if (res == PM3_SUCCESS) { + res = hf15_magic_write_blk(ISO15_MAGIC_GEN1_BLK_UID_HI, blk_hi, &pm3flags); } - return PM3_SUCCESS; + DropField(); + return (res == PM3_SUCCESS) ? PM3_SUCCESS : PM3_ESOFT; } -// Blank all four blocks again so a failed attempt leaves no unlock residue -static void hf15_magic_gen1_rollback_uid(void) { +// Blank the four blocks again, only if the tag still shows its original UID +static void hf15_magic_gen1_rollback_uid(const uint8_t *origuid) { + uint8_t curuid[ISO15693_UID_LENGTH] = {0}; + if (getUID(false, false, curuid) != PM3_SUCCESS || memcmp(curuid, origuid, ISO15693_UID_LENGTH) != 0) { + PrintAndLogEx(WARNING, "UID changed or tag not found, skipping rollback"); + return; + } + + PrintAndLogEx(INFO, "Rolling back block 0x38/0x39/0x3E/0x3F to blank..."); uint8_t zero[4] = {0x00, 0x00, 0x00, 0x00}; - hf15_magic_write_blk(ISO15_MAGIC_GEN1_BLK_UID_LO, zero); - hf15_magic_write_blk(ISO15_MAGIC_GEN1_BLK_UID_HI, zero); - hf15_magic_write_blk_quiet(ISO15_MAGIC_GEN1_BLK_UNLOCK_A, zero); - hf15_magic_write_blk_quiet(ISO15_MAGIC_GEN1_BLK_UNLOCK_B, zero); + hf15_magic_write_blk(ISO15_MAGIC_GEN1_BLK_UID_LO, zero, NULL); + hf15_magic_write_blk(ISO15_MAGIC_GEN1_BLK_UID_HI, zero, NULL); + hf15_magic_write_blk_quiet(ISO15_MAGIC_GEN1_BLK_UNLOCK_A, zero, NULL); + hf15_magic_write_blk_quiet(ISO15_MAGIC_GEN1_BLK_UNLOCK_B, zero, NULL); } /** @@ -3540,6 +3560,9 @@ static int CmdHF15CSetUID(const char *Cmd) { return PM3_ESOFT; } + uint8_t origuid[ISO15693_UID_LENGTH] = {0}; + memcpy(origuid, carduid, sizeof(origuid)); + PrintAndLogEx(INFO, "Writing..."); bool used_gen1 = false; @@ -3562,8 +3585,8 @@ static int CmdHF15CSetUID(const char *Cmd) { reverse_array_copy(payload.uid + 4, 4, blk_lo); reverse_array_copy(payload.uid, 4, blk_hi); - if (hf15_magic_write_blk(ISO15_MAGIC_V3_BLK_UID_LO, blk_lo) != PM3_SUCCESS || - hf15_magic_write_blk(ISO15_MAGIC_V3_BLK_UID_HI, blk_hi) != PM3_SUCCESS) { + if (hf15_magic_write_blk(ISO15_MAGIC_V3_BLK_UID_LO, blk_lo, NULL) != PM3_SUCCESS || + hf15_magic_write_blk(ISO15_MAGIC_V3_BLK_UID_HI, blk_hi, NULL) != PM3_SUCCESS) { PrintAndLogEx(FAILED, "Setting new UID ( " _RED_("fail") " )"); PrintAndLogEx(NORMAL, ""); return PM3_ESOFT; @@ -3584,14 +3607,15 @@ static int CmdHF15CSetUID(const char *Cmd) { // Bail out before writing if the Gen1 backdoor blocks hold real data if (hf15_magic_gen1_is_safe_to_write() == false) { PrintAndLogEx(FAILED, "tag doesn't look like a blank/magic " _YELLOW_("Gen1") " tag ( " _RED_("fail") " )"); - PrintAndLogEx(HINT, "Hint: block 0x38/0x39/0x3E/0x3F read back real data - refusing to risk overwriting a normal tag"); + PrintAndLogEx(HINT, "Hint: block 0x38/0x39/0x3E/0x3F must be unreadable or blank"); + PrintAndLogEx(HINT, "refusing to risk overwriting a normal tag"); return PM3_ESOFT; } used_gen1 = true; if (hf15_magic_gen1_write_uid(payload.uid) != PM3_SUCCESS) { - hf15_magic_gen1_rollback_uid(); + hf15_magic_gen1_rollback_uid(origuid); PrintAndLogEx(FAILED, "Setting new UID ( " _RED_("fail") " )"); PrintAndLogEx(NORMAL, ""); return PM3_ESOFT; @@ -3616,8 +3640,7 @@ static int CmdHF15CSetUID(const char *Cmd) { } if (used_gen1) { - PrintAndLogEx(INFO, "Rolling back block 0x38/0x39 to blank..."); - hf15_magic_gen1_rollback_uid(); + hf15_magic_gen1_rollback_uid(origuid); } PrintAndLogEx(FAILED, "Setting new UID ( " _RED_("fail") " )"); @@ -3675,8 +3698,8 @@ static int CmdHF15CFinalize(const char *Cmd) { PrintAndLogEx(INFO, "Finalizing..."); - if (hf15_magic_write_blk(ISO15_MAGIC_V3_BLK_SIG_A, iso15_magic_v3_fin_a) != PM3_SUCCESS || - hf15_magic_write_blk(ISO15_MAGIC_V3_BLK_SIG_B, iso15_magic_v3_fin_b) != PM3_SUCCESS) { + if (hf15_magic_write_blk(ISO15_MAGIC_V3_BLK_SIG_A, iso15_magic_v3_fin_a, NULL) != PM3_SUCCESS || + hf15_magic_write_blk(ISO15_MAGIC_V3_BLK_SIG_B, iso15_magic_v3_fin_b, NULL) != PM3_SUCCESS) { PrintAndLogEx(FAILED, "Finalize ( " _RED_("fail") " )"); PrintAndLogEx(NORMAL, ""); return PM3_ESOFT; From f0a502b279e85d011ca9874271b254f9b8dd3226 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=AD=90=E6=AD=AA?= Date: Tue, 22 Sep 2026 07:04:16 +0800 Subject: [PATCH 06/14] refactor: move ISO15693 magic tag handling to the device side Identification, writing and failure handling for magic Gen1 and V3 tags now runs in a single field session on the device, which reports PM3_SUCCESS only when the UID reads back as requested. armsrc/iso15693.c: - SetTag15693Uid() probes 0x38/0x39/0x3E/0x3F and refuses to write when any of them answers with data, verifies the UID afterwards and blanks the four blocks again when it did not change. - SetTag15693Uid_v3() and FinalizeTag15693_v3() are new, both check the 0x14/0x15 configuration signature before writing and verify after. client/src/cmdhf15.c: - csetuid and cfinalize now only select a command and print the status. The V3 block helpers and constants move to armsrc, which restores the original read helper with its CALLOC message and the ISO15_ERROR_HANDLING_* macros. Behaviour change: hf 15 cfinalize without -y no longer energizes the tag and no longer reports configuration mode before asking for confirmation. Co-Authored-By: Claude Opus 5 --- armsrc/appmain.c | 12 ++ armsrc/iso15693.c | 296 ++++++++++++++++++++++++++++++++++++++----- armsrc/iso15693.h | 2 + client/src/cmdhf15.c | 294 +++++++----------------------------------- include/pm3_cmd.h | 2 + 5 files changed, 321 insertions(+), 285 deletions(-) diff --git a/armsrc/appmain.c b/armsrc/appmain.c index 7b5eabc7b..63229979b 100644 --- a/armsrc/appmain.c +++ b/armsrc/appmain.c @@ -2032,6 +2032,18 @@ static void PacketReceived(PacketCommandNG *packet) { SetTag15693Uid_v2(payload->uid); break; } + case CMD_HF_ISO15693_CSETUID_V3: { + struct p { + uint8_t uid[8]; + } PACKED; + struct p *payload = (struct p *) packet->data.asBytes; + SetTag15693Uid_v3(payload->uid); + break; + } + case CMD_HF_ISO15693_CFINALIZE_V3: { + FinalizeTag15693_v3(); + break; + } case CMD_HF_ISO15693_SLIX_DISABLE_EAS: { struct p { uint8_t pwd[4]; diff --git a/armsrc/iso15693.c b/armsrc/iso15693.c index 44572bac9..8eecbffcf 100644 --- a/armsrc/iso15693.c +++ b/armsrc/iso15693.c @@ -3202,51 +3202,277 @@ void LockPassSlixIso15693(uint32_t pass_id, uint32_t password) { // //----------------------------------------------------------------------------- +// Backdoor blocks of a magic "Gen1" tag, see doc/magic_cards_notes.md +#define ISO15_MAGIC_GEN1_BLK_UID_LO 0x38 +#define ISO15_MAGIC_GEN1_BLK_UID_HI 0x39 +#define ISO15_MAGIC_GEN1_BLK_UNLOCK_A 0x3E +#define ISO15_MAGIC_GEN1_BLK_UNLOCK_B 0x3F + +static const uint8_t g_magic_gen1_blank[4] = {0x00, 0x00, 0x00, 0x00}; +static const uint8_t g_magic_gen1_unlock_b[4] = {0x69, 0x96, 0x00, 0x00}; + +// Unaddressed block read, leaves the field on for the next command +static int magic_15_read_blk(uint8_t blockno, uint8_t *out, uint32_t *start_time, uint32_t *eof_time) { + + uint8_t cmd[5] = {ISO15_REQ_DATARATE_HIGH | ISO15_REQ_OPTION, ISO15693_READBLOCK, blockno, 0x00, 0x00}; + AddCrc15(cmd, 3); + + uint16_t recvlen = 0; + int res = SendDataTag(cmd, sizeof(cmd), false, true, s_iso15_recvbuf, sizeof(s_iso15_recvbuf), + *start_time, ISO15693_READER_TIMEOUT, eof_time, &recvlen); + + *start_time = *eof_time + DELAY_ISO15693_VICC_TO_VCD_READER; + + if (res != PM3_SUCCESS) { + return res; + } + + // 8 bytes when the tag honours the option flag and prepends a lock byte, 7 when it doesn't + if (recvlen != 7 && recvlen != 8) { + return PM3_EWRONGANSWER; + } + + if ((s_iso15_recvbuf[0] & ISO15_RES_ERROR) == ISO15_RES_ERROR) { + return PM3_EWRONGANSWER; + } + + memcpy(out, s_iso15_recvbuf + ((recvlen == 8) ? 2 : 1), 4); + return PM3_SUCCESS; +} + +// Unaddressed block write, leaves the field on for the next command +static int magic_15_write_blk(uint8_t blockno, const uint8_t *data, uint32_t *start_time, uint32_t *eof_time) { + + uint8_t cmd[9] = {ISO15_REQ_DATARATE_HIGH, ISO15693_WRITEBLOCK, blockno, data[0], data[1], data[2], data[3], 0x00, 0x00}; + AddCrc15(cmd, 7); + + uint16_t recvlen = 0; + int res = SendDataTag(cmd, sizeof(cmd), false, true, s_iso15_recvbuf, sizeof(s_iso15_recvbuf), + *start_time, ISO15693_READER_TIMEOUT_WRITE, eof_time, &recvlen); + + *start_time = *eof_time + DELAY_ISO15693_VICC_TO_VCD_READER; + + if (res != PM3_SUCCESS) { + return res; + } + + if (recvlen < 3 || (s_iso15_recvbuf[0] & ISO15_RES_ERROR) == ISO15_RES_ERROR) { + return PM3_EWRONGANSWER; + } + return PM3_SUCCESS; +} + +// On a real Gen1 tag these four blocks sit outside the readable memory and the read fails. +// A tag that answers with data is a normal tag and we must not overwrite it. +static bool magic_gen1_is_backdoor(uint32_t *start_time, uint32_t *eof_time) { + + static const uint8_t blocks[4] = { + ISO15_MAGIC_GEN1_BLK_UID_LO, + ISO15_MAGIC_GEN1_BLK_UID_HI, + ISO15_MAGIC_GEN1_BLK_UNLOCK_A, + ISO15_MAGIC_GEN1_BLK_UNLOCK_B + }; + + for (uint8_t i = 0; i < ARRAYLEN(blocks); i++) { + + uint8_t buf[4] = {0}; + if (magic_15_read_blk(blocks[i], buf, start_time, eof_time) != PM3_SUCCESS) { + continue; + } + + if (memcmp(buf, g_magic_gen1_blank, 4) == 0) { + continue; + } + + // leftover of the unlock write of an earlier attempt on this same tag + if (blocks[i] == ISO15_MAGIC_GEN1_BLK_UNLOCK_B && memcmp(buf, g_magic_gen1_unlock_b, 4) == 0) { + continue; + } + + if (g_dbglevel >= DBG_ERROR) { + Dbprintf("SetTag15693Uid: block 0x%02X holds data, not a magic Gen1 tag", blocks[i]); + } + return false; + } + return true; +} + +// Blank the four blocks again so a failed attempt leaves no unlock values behind +static void magic_gen1_rollback(uint32_t *start_time, uint32_t *eof_time) { + magic_15_write_blk(ISO15_MAGIC_GEN1_BLK_UID_LO, g_magic_gen1_blank, start_time, eof_time); + magic_15_write_blk(ISO15_MAGIC_GEN1_BLK_UID_HI, g_magic_gen1_blank, start_time, eof_time); + magic_15_write_blk(ISO15_MAGIC_GEN1_BLK_UNLOCK_A, g_magic_gen1_blank, start_time, eof_time); + magic_15_write_blk(ISO15_MAGIC_GEN1_BLK_UNLOCK_B, g_magic_gen1_blank, start_time, eof_time); +} + // Set the UID on Magic ISO15693 tag (based on Iceman's LUA-script). void SetTag15693Uid(const uint8_t *uid) { LED_A_ON(); - uint8_t cmd[4][9] = { - {ISO15_REQ_DATARATE_HIGH, ISO15693_WRITEBLOCK, 0x3e, 0x00, 0x00, 0x00, 0x00, 0xE9, 0x8F}, - {ISO15_REQ_DATARATE_HIGH, ISO15693_WRITEBLOCK, 0x3f, 0x69, 0x96, 0x00, 0x00, 0x8A, 0xBB}, - - // Command 3 : 02 21 38 u8u7u6u5 (where uX = uid byte X) - {ISO15_REQ_DATARATE_HIGH, ISO15693_WRITEBLOCK, 0x38, uid[7], uid[6], uid[5], uid[4]}, - - // Command 4 : 02 21 39 u4u3u2u1 (where uX = uid byte X) - {ISO15_REQ_DATARATE_HIGH, ISO15693_WRITEBLOCK, 0x39, uid[3], uid[2], uid[1], uid[0]} - }; - - - AddCrc15(cmd[2], 7); - AddCrc15(cmd[3], 7); - memset(s_iso15_recvbuf, 0, sizeof(s_iso15_recvbuf)); - uint32_t start_time = 0; - uint32_t eof_time = 0; - uint16_t recvlen = 0; + Iso15693InitReader(); + StartCountSspClk(); + uint32_t start_time = 0, eof_time = 0; - int res = PM3_SUCCESS; + uint8_t origuid[8] = {0}; + if (get_uid_slix(start_time, &eof_time, origuid) != PM3_SUCCESS) { + reply_ng(CMD_HF_ISO15693_CSETUID, PM3_ETIMEOUT, NULL, 0); + switch_off(); + return; + } + start_time = eof_time + DELAY_ISO15693_VICC_TO_VCD_READER; - for (int i = 0; i < 4; i++) { - res = SendDataTag( - cmd[i], - sizeof(cmd[i]), - (i == 0) ? true : false, - true, - s_iso15_recvbuf, - sizeof(s_iso15_recvbuf), - start_time, - ISO15693_READER_TIMEOUT_WRITE, - &eof_time, - &recvlen - ); - - start_time = eof_time + DELAY_ISO15693_VICC_TO_VCD_READER; + if (magic_gen1_is_backdoor(&start_time, &eof_time) == false) { + reply_ng(CMD_HF_ISO15693_CSETUID, PM3_EWRONGANSWER, NULL, 0); + switch_off(); + return; } - reply_ng(CMD_HF_ISO15693_CSETUID, res, NULL, 0); + const uint8_t uid_lo[4] = {uid[7], uid[6], uid[5], uid[4]}; + const uint8_t uid_hi[4] = {uid[3], uid[2], uid[1], uid[0]}; + + // the unlock writes are best-effort, a tag already unlocked rejects them + magic_15_write_blk(ISO15_MAGIC_GEN1_BLK_UNLOCK_A, g_magic_gen1_blank, &start_time, &eof_time); + magic_15_write_blk(ISO15_MAGIC_GEN1_BLK_UNLOCK_B, g_magic_gen1_unlock_b, &start_time, &eof_time); + + int res = magic_15_write_blk(ISO15_MAGIC_GEN1_BLK_UID_LO, uid_lo, &start_time, &eof_time); + if (res == PM3_SUCCESS) { + res = magic_15_write_blk(ISO15_MAGIC_GEN1_BLK_UID_HI, uid_hi, &start_time, &eof_time); + } + + // get_uid_slix answers LSB first, the requested uid is MSB first + uint8_t newuid[8] = {0}, revuid[8] = {0}; + int res_uid = get_uid_slix(start_time, &eof_time, newuid); + reverse_array_copy(newuid, sizeof(newuid), revuid); + start_time = eof_time + DELAY_ISO15693_VICC_TO_VCD_READER; + + if (res == PM3_SUCCESS && res_uid == PM3_SUCCESS && memcmp(revuid, uid, sizeof(revuid)) == 0) { + reply_ng(CMD_HF_ISO15693_CSETUID, PM3_SUCCESS, NULL, 0); + switch_off(); + return; + } + + // only roll back while the tag still shows its original UID, blanking the + // blocks after a UID change would destroy what we just wrote + if (res_uid == PM3_SUCCESS && memcmp(newuid, origuid, sizeof(newuid)) == 0) { + magic_gen1_rollback(&start_time, &eof_time); + } + + reply_ng(CMD_HF_ISO15693_CSETUID, PM3_ESOFT, NULL, 0); + switch_off(); +} + +// Magic "V3" tags answer with a signature in 0x14/0x15 while the UID is still changeable +#define ISO15_MAGIC_V3_BLK_UID_LO 0x10 +#define ISO15_MAGIC_V3_BLK_UID_HI 0x11 +#define ISO15_MAGIC_V3_BLK_SIG_A 0x14 +#define ISO15_MAGIC_V3_BLK_SIG_B 0x15 + +static const uint8_t g_magic_v3_sig_a[4] = {0xA5, 0x2B, 0x44, 0x2C}; +static const uint8_t g_magic_v3_sig_b[4] = {0x21, 0xAE, 0x93, 0x00}; +static const uint8_t g_magic_v3_fin_a[4] = {0xA5, 0x2B, 0x44, 0x2C}; +static const uint8_t g_magic_v3_fin_b[4] = {0x69, 0xE2, 0x5D, 0x00}; + +// Writing the finalize values to anything that is not an un-finalized V3 tag bricks it +static bool magic_v3_in_config_mode(uint32_t *start_time, uint32_t *eof_time) { + + uint8_t a[4] = {0}; + uint8_t b[4] = {0}; + + if (magic_15_read_blk(ISO15_MAGIC_V3_BLK_SIG_A, a, start_time, eof_time) != PM3_SUCCESS) { + return false; + } + + if (magic_15_read_blk(ISO15_MAGIC_V3_BLK_SIG_B, b, start_time, eof_time) != PM3_SUCCESS) { + return false; + } + + return (memcmp(a, g_magic_v3_sig_a, 4) == 0) && (memcmp(b, g_magic_v3_sig_b, 4) == 0); +} + +// Set the UID on a magic "V3" tag, repeatable until the tag is finalized +void SetTag15693Uid_v3(const uint8_t *uid) { + + LED_A_ON(); + memset(s_iso15_recvbuf, 0, sizeof(s_iso15_recvbuf)); + + Iso15693InitReader(); + StartCountSspClk(); + uint32_t start_time = 0, eof_time = 0; + + uint8_t curuid[8] = {0}; + if (get_uid_slix(start_time, &eof_time, curuid) != PM3_SUCCESS) { + reply_ng(CMD_HF_ISO15693_CSETUID_V3, PM3_ETIMEOUT, NULL, 0); + switch_off(); + return; + } + start_time = eof_time + DELAY_ISO15693_VICC_TO_VCD_READER; + + if (magic_v3_in_config_mode(&start_time, &eof_time) == false) { + reply_ng(CMD_HF_ISO15693_CSETUID_V3, PM3_EWRONGANSWER, NULL, 0); + switch_off(); + return; + } + + const uint8_t uid_lo[4] = {uid[7], uid[6], uid[5], uid[4]}; + const uint8_t uid_hi[4] = {uid[3], uid[2], uid[1], uid[0]}; + + int res = magic_15_write_blk(ISO15_MAGIC_V3_BLK_UID_LO, uid_lo, &start_time, &eof_time); + if (res == PM3_SUCCESS) { + res = magic_15_write_blk(ISO15_MAGIC_V3_BLK_UID_HI, uid_hi, &start_time, &eof_time); + } + + if (res == PM3_SUCCESS) { + // get_uid_slix answers LSB first, the requested uid is MSB first + uint8_t newuid[8] = {0}, revuid[8] = {0}; + res = get_uid_slix(start_time, &eof_time, newuid); + reverse_array_copy(newuid, sizeof(newuid), revuid); + + if (res == PM3_SUCCESS && memcmp(revuid, uid, sizeof(revuid)) != 0) { + res = PM3_ESOFT; + } + } + + reply_ng(CMD_HF_ISO15693_CSETUID_V3, (res == PM3_SUCCESS) ? PM3_SUCCESS : PM3_ESOFT, NULL, 0); + switch_off(); +} + +// Lock the UID of a magic "V3" tag, irreversible +void FinalizeTag15693_v3(void) { + + LED_A_ON(); + memset(s_iso15_recvbuf, 0, sizeof(s_iso15_recvbuf)); + + Iso15693InitReader(); + StartCountSspClk(); + uint32_t start_time = 0, eof_time = 0; + + uint8_t curuid[8] = {0}; + if (get_uid_slix(start_time, &eof_time, curuid) != PM3_SUCCESS) { + reply_ng(CMD_HF_ISO15693_CFINALIZE_V3, PM3_ETIMEOUT, NULL, 0); + switch_off(); + return; + } + start_time = eof_time + DELAY_ISO15693_VICC_TO_VCD_READER; + + if (magic_v3_in_config_mode(&start_time, &eof_time) == false) { + reply_ng(CMD_HF_ISO15693_CFINALIZE_V3, PM3_EWRONGANSWER, NULL, 0); + switch_off(); + return; + } + + int res = magic_15_write_blk(ISO15_MAGIC_V3_BLK_SIG_A, g_magic_v3_fin_a, &start_time, &eof_time); + if (res == PM3_SUCCESS) { + res = magic_15_write_blk(ISO15_MAGIC_V3_BLK_SIG_B, g_magic_v3_fin_b, &start_time, &eof_time); + } + + // the signature must be gone now, otherwise the tag is still changeable + if (res == PM3_SUCCESS && magic_v3_in_config_mode(&start_time, &eof_time)) { + res = PM3_ESOFT; + } + + reply_ng(CMD_HF_ISO15693_CFINALIZE_V3, (res == PM3_SUCCESS) ? PM3_SUCCESS : PM3_ESOFT, NULL, 0); switch_off(); } diff --git a/armsrc/iso15693.h b/armsrc/iso15693.h index a3197bbd8..5a419e293 100644 --- a/armsrc/iso15693.h +++ b/armsrc/iso15693.h @@ -59,6 +59,8 @@ int SendDataTagEOF(uint8_t *recv, uint16_t max_recv_len, uint32_t start_time, ui void SetTag15693Uid(const uint8_t *uid); void SetTag15693Uid_v2(const uint8_t *uid); +void SetTag15693Uid_v3(const uint8_t *uid); +void FinalizeTag15693_v3(void); void WritePasswordSlixIso15693(const uint8_t *old_password, const uint8_t *new_password, uint8_t pwd_id); void DisablePrivacySlixIso15693(const uint8_t *password); diff --git a/client/src/cmdhf15.c b/client/src/cmdhf15.c index 22f712b48..9e9397bfb 100644 --- a/client/src/cmdhf15.c +++ b/client/src/cmdhf15.c @@ -3324,180 +3324,6 @@ static int CmdHF15Restore(const char *Cmd) { return PM3_SUCCESS; } -// ISO15693 magic "V3" tag support -#define ISO15_MAGIC_V3_BLK_UID_LO 0x10 -#define ISO15_MAGIC_V3_BLK_UID_HI 0x11 -#define ISO15_MAGIC_V3_BLK_SIG_A 0x14 -#define ISO15_MAGIC_V3_BLK_SIG_B 0x15 - -// signature check in block 0x14/0x15 -static const uint8_t iso15_magic_v3_sig_a[4] = {0xA5, 0x2B, 0x44, 0x2C}; -static const uint8_t iso15_magic_v3_sig_b[4] = {0x21, 0xAE, 0x93, 0x00}; - -// finalize command write in block 0x14/0x15 -// hf 15 raw -wac -d 022114A52B442C -// hf 15 raw -wac -d 02211569E25D00 -static const uint8_t iso15_magic_v3_fin_a[4] = {0xA5, 0x2B, 0x44, 0x2C}; -static const uint8_t iso15_magic_v3_fin_b[4] = {0x69, 0xE2, 0x5D, 0x00}; - -// Unaddressed 4-byte block read, silent because failed probes are expected -static int hf15_magic_read_blk(uint8_t blockno, uint8_t out[4]) { - - uint16_t approxlen = 2 + 1 + 2; - iso15_raw_cmd_t *packet = (iso15_raw_cmd_t *)calloc(1, sizeof(iso15_raw_cmd_t) + approxlen); - if (packet == NULL) { - return PM3_EMALLOC; - } - - // enforce OPTION flag so we get the lock byte, keeping data at offset 2 - packet->raw[packet->rawlen++] = arg_get_raw_flag(0, true, false, true); - packet->raw[packet->rawlen++] = ISO15693_READBLOCK; - packet->raw[packet->rawlen++] = blockno; - AddCrc15(packet->raw, packet->rawlen); - packet->rawlen += 2; - packet->flags = (ISO15_CONNECT | ISO15_READ_RESPONSE); - - clearCommandBuffer(); - SendCommandNG(CMD_HF_ISO15693_COMMAND, (uint8_t *)packet, ISO15_RAW_LEN(packet->rawlen)); - free(packet); - - PacketResponseNG resp; - if (WaitForResponseTimeout(CMD_HF_ISO15693_COMMAND, &resp, 2000) == false) { - return PM3_ETIMEOUT; - } - - if (resp.status == PM3_ETEAROFF) { - return resp.status; - } - if (resp.length < 2) { - return PM3_EWRONGANSWER; - } - - uint8_t *d = resp.data.asBytes; - - if (check_crc(CRC_15693, d, resp.length) == false) { - return PM3_ECRC; - } - if ((d[0] & ISO15_RES_ERROR) == ISO15_RES_ERROR) { - if (d[1] == 0x0F || d[1] == 0x10) { - return PM3_EOUTOFBOUND; - } - return PM3_EFAILED; - } - - memcpy(out, d + 2, 4); - return PM3_SUCCESS; -} - -// Write one 4-byte block to a magic tag in unaddressed mode. -static int hf15_magic_write_blk(uint8_t blockno, const uint8_t *data, const uint8_t *pm3flags) { - uint16_t flags = arg_get_raw_flag(0, true, false, false); - return hf_15_write_blk(pm3flags, flags, NULL, true, blockno, data, 4); -} - -// Detect an un-finalized magic V3 tag by its configuration-mode signature. -static bool hf15_magic_v3_is_config_mode(void) { - uint8_t a[4] = {0}; - uint8_t b[4] = {0}; - if (hf15_magic_read_blk(ISO15_MAGIC_V3_BLK_SIG_A, a) != PM3_SUCCESS) { - return false; - } - if (hf15_magic_read_blk(ISO15_MAGIC_V3_BLK_SIG_B, b) != PM3_SUCCESS) { - return false; - } - return (memcmp(a, iso15_magic_v3_sig_a, 4) == 0) && (memcmp(b, iso15_magic_v3_sig_b, 4) == 0); -} - -// ISO15693 magic "Gen1" (backdoor) tag support -#define ISO15_MAGIC_GEN1_BLK_UID_LO 0x38 -#define ISO15_MAGIC_GEN1_BLK_UID_HI 0x39 -#define ISO15_MAGIC_GEN1_BLK_UNLOCK_A 0x3E -#define ISO15_MAGIC_GEN1_BLK_UNLOCK_B 0x3F - -// Gen1 unlock values for blocks 0x3E/0x3F -static const uint8_t iso15_magic_gen1_unlock_a[4] = {0x00, 0x00, 0x00, 0x00}; -static const uint8_t iso15_magic_gen1_unlock_b[4] = {0x69, 0x96, 0x00, 0x00}; - -// Gen1 backdoor blocks -static const uint8_t iso15_magic_gen1_blocks[] = { - ISO15_MAGIC_GEN1_BLK_UID_LO, - ISO15_MAGIC_GEN1_BLK_UID_HI, - ISO15_MAGIC_GEN1_BLK_UNLOCK_A, - ISO15_MAGIC_GEN1_BLK_UNLOCK_B, -}; - -// Refuse if a Gen1 backdoor block reads real data or fails in an unexpected way -static bool hf15_magic_gen1_is_safe_to_write(void) { - uint8_t buf[4] = {0}; - static const uint8_t zero[4] = {0x00, 0x00, 0x00, 0x00}; - for (size_t i = 0; i < ARRAYLEN(iso15_magic_gen1_blocks); i++) { - uint8_t blockno = iso15_magic_gen1_blocks[i]; - int res = hf15_magic_read_blk(blockno, buf); - - // no answer or block not available is what a Gen1 backdoor block does - if (res == PM3_EWRONGANSWER || res == PM3_EOUTOFBOUND) { - continue; - } - if (res != PM3_SUCCESS) { - PrintAndLogEx(WARNING, "block 0x%02X read failed unexpectedly ( " _RED_("%d") " )", blockno, res); - return false; - } - - // blank, or 69960000 in 0x3F which an earlier unlock write left behind - if (memcmp(buf, zero, 4) == 0 || - (blockno == ISO15_MAGIC_GEN1_BLK_UNLOCK_B && memcmp(buf, iso15_magic_gen1_unlock_b, 4) == 0)) { - continue; - } - PrintAndLogEx(WARNING, "block 0x%02X reads back real data ( " _RED_("%s") " )", blockno, sprint_hex(buf, 4)); - return false; - } - return true; -} - -// Silent write, real Gen1 tags reject 0x3E/0x3F once they were written before -static void hf15_magic_write_blk_quiet(uint8_t blockno, const uint8_t *data, const uint8_t *pm3flags) { - uint8_t old_printAndLog = g_printAndLog; - g_printAndLog &= ~PRINTANDLOG_PRINT; - hf15_magic_write_blk(blockno, data, pm3flags); - g_printAndLog = old_printAndLog; -} - -// One RF session for all four writes, the unlock writes are best-effort -static int hf15_magic_gen1_write_uid(const uint8_t *uid) { - uint8_t blk_lo[4] = {0}; - uint8_t blk_hi[4] = {0}; - reverse_array_copy(uid + 4, 4, blk_lo); - reverse_array_copy(uid, 4, blk_hi); - - uint8_t pm3flags = (ISO15_CONNECT | ISO15_READ_RESPONSE | ISO15_LONG_WAIT | ISO15_HIGH_SPEED | ISO15_NO_DISCONNECT); - hf15_magic_write_blk_quiet(ISO15_MAGIC_GEN1_BLK_UNLOCK_A, iso15_magic_gen1_unlock_a, &pm3flags); - pm3flags &= ~ISO15_CONNECT; - hf15_magic_write_blk_quiet(ISO15_MAGIC_GEN1_BLK_UNLOCK_B, iso15_magic_gen1_unlock_b, &pm3flags); - - int res = hf15_magic_write_blk(ISO15_MAGIC_GEN1_BLK_UID_LO, blk_lo, &pm3flags); - if (res == PM3_SUCCESS) { - res = hf15_magic_write_blk(ISO15_MAGIC_GEN1_BLK_UID_HI, blk_hi, &pm3flags); - } - DropField(); - return (res == PM3_SUCCESS) ? PM3_SUCCESS : PM3_ESOFT; -} - -// Blank the four blocks again, only if the tag still shows its original UID -static void hf15_magic_gen1_rollback_uid(const uint8_t *origuid) { - uint8_t curuid[ISO15693_UID_LENGTH] = {0}; - if (getUID(false, false, curuid) != PM3_SUCCESS || memcmp(curuid, origuid, ISO15693_UID_LENGTH) != 0) { - PrintAndLogEx(WARNING, "UID changed or tag not found, skipping rollback"); - return; - } - - PrintAndLogEx(INFO, "Rolling back block 0x38/0x39/0x3E/0x3F to blank..."); - uint8_t zero[4] = {0x00, 0x00, 0x00, 0x00}; - hf15_magic_write_blk(ISO15_MAGIC_GEN1_BLK_UID_LO, zero, NULL); - hf15_magic_write_blk(ISO15_MAGIC_GEN1_BLK_UID_HI, zero, NULL); - hf15_magic_write_blk_quiet(ISO15_MAGIC_GEN1_BLK_UNLOCK_A, zero, NULL); - hf15_magic_write_blk_quiet(ISO15_MAGIC_GEN1_BLK_UNLOCK_B, zero, NULL); -} - /** * Commandline handling: HF15 CMD CSETUID * Set UID for magic Chinese card @@ -3507,7 +3333,7 @@ static int CmdHF15CSetUID(const char *Cmd) { CLIParserContext *ctx; CLIParserInit(&ctx, "hf 15 csetuid", "Set UID for magic Chinese card (only works with such cards)\n" - "Gen1 and V3 first probe their backdoor blocks and refuse to write if they\n" + "Gen1 and V3 probe their backdoor blocks first and refuse to write if they\n" "read back real data, to avoid corrupting a normal (non-magic) tag.\n" "For magic 'V3' tags this writes the UID configuration only and is repeatable;\n" "run `" _YELLOW_("hf 15 cfinalize") "` afterwards to lock the UID permanently.", @@ -3560,66 +3386,36 @@ static int CmdHF15CSetUID(const char *Cmd) { return PM3_ESOFT; } - uint8_t origuid[ISO15693_UID_LENGTH] = {0}; - memcpy(origuid, carduid, sizeof(origuid)); - PrintAndLogEx(INFO, "Writing..."); - bool used_gen1 = false; + PacketResponseNG resp; + clearCommandBuffer(); - if (use_v3) { - // Only proceed on an un-finalized magic V3 tag - if (hf15_magic_v3_is_config_mode() == false) { + uint16_t cmd = CMD_HF_ISO15693_CSETUID; + if (use_v2) { + cmd = CMD_HF_ISO15693_CSETUID_V2; + } else if (use_v3) { + cmd = CMD_HF_ISO15693_CSETUID_V3; + } + + SendCommandNG(cmd, (uint8_t *)&payload, sizeof(payload)); + if (WaitForResponseTimeout(cmd, &resp, 2000) == false) { + PrintAndLogEx(WARNING, "timeout while waiting for reply"); + DropField(); + return PM3_ESOFT; + } + + // the device identifies the tag before it writes, V2 has no such check + if (resp.status == PM3_EWRONGANSWER) { + if (use_v3) { PrintAndLogEx(FAILED, "tag is not an un-finalized magic " _YELLOW_("V3") " tag"); PrintAndLogEx(HINT, "Hint: signature in blocks 0x14/0x15 not found - already finalized or not a V3 tag"); - return PM3_ESOFT; - } - - /* - for example id: E011223344556677 - [=] 16 | 77 66 55 44 | 0 | wfUD - [=] 17 | 33 22 11 E0 | 0 | 3".. - */ - uint8_t blk_lo[4] = {0}; - uint8_t blk_hi[4] = {0}; - reverse_array_copy(payload.uid + 4, 4, blk_lo); - reverse_array_copy(payload.uid, 4, blk_hi); - - if (hf15_magic_write_blk(ISO15_MAGIC_V3_BLK_UID_LO, blk_lo, NULL) != PM3_SUCCESS || - hf15_magic_write_blk(ISO15_MAGIC_V3_BLK_UID_HI, blk_hi, NULL) != PM3_SUCCESS) { - PrintAndLogEx(FAILED, "Setting new UID ( " _RED_("fail") " )"); - PrintAndLogEx(NORMAL, ""); - return PM3_ESOFT; - } - - } else if (use_v2) { - PacketResponseNG resp; - clearCommandBuffer(); - - SendCommandNG(CMD_HF_ISO15693_CSETUID_V2, (uint8_t *)&payload, sizeof(payload)); - if (WaitForResponseTimeout(CMD_HF_ISO15693_CSETUID_V2, &resp, 2000) == false) { - PrintAndLogEx(WARNING, "timeout while waiting for reply"); - DropField(); - return PM3_ESOFT; - } - - } else { - // Bail out before writing if the Gen1 backdoor blocks hold real data - if (hf15_magic_gen1_is_safe_to_write() == false) { + } else { PrintAndLogEx(FAILED, "tag doesn't look like a blank/magic " _YELLOW_("Gen1") " tag ( " _RED_("fail") " )"); PrintAndLogEx(HINT, "Hint: block 0x38/0x39/0x3E/0x3F must be unreadable or blank"); PrintAndLogEx(HINT, "refusing to risk overwriting a normal tag"); - return PM3_ESOFT; - } - - used_gen1 = true; - - if (hf15_magic_gen1_write_uid(payload.uid) != PM3_SUCCESS) { - hf15_magic_gen1_rollback_uid(origuid); - PrintAndLogEx(FAILED, "Setting new UID ( " _RED_("fail") " )"); - PrintAndLogEx(NORMAL, ""); - return PM3_ESOFT; } + return PM3_ESOFT; } PrintAndLogEx(INFO, "Verifying..."); @@ -3639,10 +3435,6 @@ static int CmdHF15CSetUID(const char *Cmd) { return PM3_SUCCESS;; } - if (used_gen1) { - hf15_magic_gen1_rollback_uid(origuid); - } - PrintAndLogEx(FAILED, "Setting new UID ( " _RED_("fail") " )"); PrintAndLogEx(NORMAL, ""); return PM3_ESOFT; @@ -3672,24 +3464,6 @@ static int CmdHF15CFinalize(const char *Cmd) { bool confirmed = arg_get_lit(ctx, 1); CLIParserFree(ctx); - PrintAndLogEx(INFO, "Get current tag"); - - uint8_t carduid[ISO15693_UID_LENGTH] = {0x00}; - if (getUID(true, false, carduid) != PM3_SUCCESS) { - PrintAndLogEx(FAILED, "no tag found"); - return PM3_ESOFT; - } - - // Safety: only proceed on a tag that is actually an un-finalized magic V3. - // Writing the finalize values to any other tag may permanently brick it. - if (hf15_magic_v3_is_config_mode() == false) { - PrintAndLogEx(FAILED, "tag is not an un-finalized magic " _YELLOW_("V3") " tag"); - PrintAndLogEx(HINT, "Hint: signature in blocks 0x14/0x15 not found - already finalized or not a V3 tag"); - return PM3_ESOFT; - } - - PrintAndLogEx(SUCCESS, "Magic " _GREEN_("V3") " tag in configuration mode ( " _GREEN_("ok") " )"); - if (confirmed == false) { PrintAndLogEx(WARNING, _RED_("This operation is irreversible!") " The UID will be locked permanently."); PrintAndLogEx(WARNING, "Add " _YELLOW_("-y") " to confirm and proceed."); @@ -3698,8 +3472,28 @@ static int CmdHF15CFinalize(const char *Cmd) { PrintAndLogEx(INFO, "Finalizing..."); - if (hf15_magic_write_blk(ISO15_MAGIC_V3_BLK_SIG_A, iso15_magic_v3_fin_a, NULL) != PM3_SUCCESS || - hf15_magic_write_blk(ISO15_MAGIC_V3_BLK_SIG_B, iso15_magic_v3_fin_b, NULL) != PM3_SUCCESS) { + PacketResponseNG resp; + clearCommandBuffer(); + SendCommandNG(CMD_HF_ISO15693_CFINALIZE_V3, NULL, 0); + if (WaitForResponseTimeout(CMD_HF_ISO15693_CFINALIZE_V3, &resp, 2000) == false) { + PrintAndLogEx(WARNING, "timeout while waiting for reply"); + DropField(); + return PM3_ESOFT; + } + + if (resp.status == PM3_ETIMEOUT) { + PrintAndLogEx(FAILED, "no tag found"); + return PM3_ESOFT; + } + + if (resp.status == PM3_EWRONGANSWER) { + PrintAndLogEx(FAILED, "tag is not an un-finalized magic " _YELLOW_("V3") " tag"); + PrintAndLogEx(HINT, "Hint: signature in blocks 0x14/0x15 not found"); + PrintAndLogEx(HINT, "already finalized or not a V3 tag"); + return PM3_ESOFT; + } + + if (resp.status != PM3_SUCCESS) { PrintAndLogEx(FAILED, "Finalize ( " _RED_("fail") " )"); PrintAndLogEx(NORMAL, ""); return PM3_ESOFT; diff --git a/include/pm3_cmd.h b/include/pm3_cmd.h index a27b69940..86cbdc341 100644 --- a/include/pm3_cmd.h +++ b/include/pm3_cmd.h @@ -1026,6 +1026,8 @@ typedef struct { #define CMD_HF_ISO15693_CSETUID 0x0316 #define CMD_HF_ISO15693_CSETUID_V2 0x0333 +#define CMD_HF_ISO15693_CSETUID_V3 0x0334 +#define CMD_HF_ISO15693_CFINALIZE_V3 0x0335 #define CMD_LF_SNIFF_RAW_ADC 0x0360 From dfa3255fd698d2ec3048cd80a27588099bc42f40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=AD=90=E6=AD=AA?= Date: Tue, 22 Sep 2026 16:30:43 +0800 Subject: [PATCH 07/14] fix: refine hint messages for V3 tag finalization status --- client/src/cmdhf15.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/src/cmdhf15.c b/client/src/cmdhf15.c index 9e9397bfb..b1123537a 100644 --- a/client/src/cmdhf15.c +++ b/client/src/cmdhf15.c @@ -3409,7 +3409,8 @@ static int CmdHF15CSetUID(const char *Cmd) { if (resp.status == PM3_EWRONGANSWER) { if (use_v3) { PrintAndLogEx(FAILED, "tag is not an un-finalized magic " _YELLOW_("V3") " tag"); - PrintAndLogEx(HINT, "Hint: signature in blocks 0x14/0x15 not found - already finalized or not a V3 tag"); + PrintAndLogEx(HINT, "Hint: signature in blocks 0x14/0x15 not found"); + PrintAndLogEx(HINT, "already finalized or not a V3 tag"); } else { PrintAndLogEx(FAILED, "tag doesn't look like a blank/magic " _YELLOW_("Gen1") " tag ( " _RED_("fail") " )"); PrintAndLogEx(HINT, "Hint: block 0x38/0x39/0x3E/0x3F must be unreadable or blank"); From 6795573bd4e494d4138025a671d93a247d14951d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=AD=90=E6=AD=AA?= Date: Tue, 22 Sep 2026 21:08:09 +0800 Subject: [PATCH 08/14] fix: harden ISO15693 magic tag probing on the device side magic_15_read_blk() rejected any read whose response was not 7 or 8 bytes and magic_gen1_is_backdoor() read that as "backdoor register", so a normal tag with a block size other than 4 answered with 12 bytes and was classified as safe to overwrite. It now reports the payload it got and the Gen1 probe tests all of it for zero, so any answer with data means a normal tag whatever its block size. Both helpers verify the response CRC now, GetIso15693AnswerFromTag() leaves that to the caller. magic_v3_in_config_mode() becomes magic_v3_read_config(), which separates "the signature blocks could not be read" from "they were read and hold something else". FinalizeTag15693_v3() took the first as proof the lock had taken, so a tag lifted off the antenna right after the finalize writes reported success. The two reads share the receive buffer, so block 0x14 is compared before 0x15 is requested. Co-Authored-By: Claude Opus 5 --- armsrc/iso15693.c | 93 +++++++++++++++++++++++++++++++++++------------ 1 file changed, 69 insertions(+), 24 deletions(-) diff --git a/armsrc/iso15693.c b/armsrc/iso15693.c index 8eecbffcf..618f2e04f 100644 --- a/armsrc/iso15693.c +++ b/armsrc/iso15693.c @@ -3211,8 +3211,19 @@ void LockPassSlixIso15693(uint32_t pass_id, uint32_t password) { static const uint8_t g_magic_gen1_blank[4] = {0x00, 0x00, 0x00, 0x00}; static const uint8_t g_magic_gen1_unlock_b[4] = {0x69, 0x96, 0x00, 0x00}; -// Unaddressed block read, leaves the field on for the next command -static int magic_15_read_blk(uint8_t blockno, uint8_t *out, uint32_t *start_time, uint32_t *eof_time) { +// true when every byte is zero +static bool magic_15_is_blank(const uint8_t *data, uint8_t len) { + for (uint8_t i = 0; i < len; i++) { + if (data[i] != 0x00) { + return false; + } + } + return true; +} + +// PM3_SUCCESS means the tag answered with block data, *data then points into +// the receive buffer and stays valid until the next command overwrites it. +static int magic_15_read_blk(uint8_t blockno, uint8_t **data, uint8_t *len, uint32_t *start_time, uint32_t *eof_time) { uint8_t cmd[5] = {ISO15_REQ_DATARATE_HIGH | ISO15_REQ_OPTION, ISO15693_READBLOCK, blockno, 0x00, 0x00}; AddCrc15(cmd, 3); @@ -3227,16 +3238,24 @@ static int magic_15_read_blk(uint8_t blockno, uint8_t *out, uint32_t *start_time return res; } - // 8 bytes when the tag honours the option flag and prepends a lock byte, 7 when it doesn't - if (recvlen != 7 && recvlen != 8) { + // flags + at least four data bytes + crc + if (recvlen < 7) { return PM3_EWRONGANSWER; } + // GetIso15693AnswerFromTag() doesn't check the crc, so do it here + if (CheckCrc15(s_iso15_recvbuf, recvlen) == false) { + return PM3_ECRC; + } + if ((s_iso15_recvbuf[0] & ISO15_RES_ERROR) == ISO15_RES_ERROR) { return PM3_EWRONGANSWER; } - memcpy(out, s_iso15_recvbuf + ((recvlen == 8) ? 2 : 1), 4); + // the option flag makes the tag prepend a lock byte, blocks can be wider than four bytes + uint8_t offset = (recvlen > 7) ? 2 : 1; + *data = s_iso15_recvbuf + offset; + *len = recvlen - offset - 2; return PM3_SUCCESS; } @@ -3256,7 +3275,16 @@ static int magic_15_write_blk(uint8_t blockno, const uint8_t *data, uint32_t *st return res; } - if (recvlen < 3 || (s_iso15_recvbuf[0] & ISO15_RES_ERROR) == ISO15_RES_ERROR) { + // flags + crc + if (recvlen < 3) { + return PM3_EWRONGANSWER; + } + + if (CheckCrc15(s_iso15_recvbuf, recvlen) == false) { + return PM3_ECRC; + } + + if ((s_iso15_recvbuf[0] & ISO15_RES_ERROR) == ISO15_RES_ERROR) { return PM3_EWRONGANSWER; } return PM3_SUCCESS; @@ -3275,17 +3303,20 @@ static bool magic_gen1_is_backdoor(uint32_t *start_time, uint32_t *eof_time) { for (uint8_t i = 0; i < ARRAYLEN(blocks); i++) { - uint8_t buf[4] = {0}; - if (magic_15_read_blk(blocks[i], buf, start_time, eof_time) != PM3_SUCCESS) { + uint8_t *data = NULL; + uint8_t len = 0; + + if (magic_15_read_blk(blocks[i], &data, &len, start_time, eof_time) != PM3_SUCCESS) { continue; } - if (memcmp(buf, g_magic_gen1_blank, 4) == 0) { + if (magic_15_is_blank(data, len)) { continue; } // leftover of the unlock write of an earlier attempt on this same tag - if (blocks[i] == ISO15_MAGIC_GEN1_BLK_UNLOCK_B && memcmp(buf, g_magic_gen1_unlock_b, 4) == 0) { + if (blocks[i] == ISO15_MAGIC_GEN1_BLK_UNLOCK_B && len >= 4 && + memcmp(data, g_magic_gen1_unlock_b, 4) == 0) { continue; } @@ -3374,21 +3405,30 @@ static const uint8_t g_magic_v3_sig_b[4] = {0x21, 0xAE, 0x93, 0x00}; static const uint8_t g_magic_v3_fin_a[4] = {0xA5, 0x2B, 0x44, 0x2C}; static const uint8_t g_magic_v3_fin_b[4] = {0x69, 0xE2, 0x5D, 0x00}; -// Writing the finalize values to anything that is not an un-finalized V3 tag bricks it -static bool magic_v3_in_config_mode(uint32_t *start_time, uint32_t *eof_time) { +// Writing the finalize values to anything that is not an un-finalized V3 tag bricks it. +// PM3_SUCCESS means both signature blocks were read, *in_config says what they hold. +static int magic_v3_read_config(bool *in_config, uint32_t *start_time, uint32_t *eof_time) { - uint8_t a[4] = {0}; - uint8_t b[4] = {0}; + uint8_t *data = NULL; + uint8_t len = 0; - if (magic_15_read_blk(ISO15_MAGIC_V3_BLK_SIG_A, a, start_time, eof_time) != PM3_SUCCESS) { - return false; + *in_config = false; + + // the second read overwrites the buffer, so compare 0x14 before asking for 0x15 + int res = magic_15_read_blk(ISO15_MAGIC_V3_BLK_SIG_A, &data, &len, start_time, eof_time); + if (res != PM3_SUCCESS) { + return res; } - if (magic_15_read_blk(ISO15_MAGIC_V3_BLK_SIG_B, b, start_time, eof_time) != PM3_SUCCESS) { - return false; + bool sig_a = (len >= 4 && memcmp(data, g_magic_v3_sig_a, 4) == 0); + + res = magic_15_read_blk(ISO15_MAGIC_V3_BLK_SIG_B, &data, &len, start_time, eof_time); + if (res != PM3_SUCCESS) { + return res; } - return (memcmp(a, g_magic_v3_sig_a, 4) == 0) && (memcmp(b, g_magic_v3_sig_b, 4) == 0); + *in_config = (sig_a && len >= 4 && memcmp(data, g_magic_v3_sig_b, 4) == 0); + return PM3_SUCCESS; } // Set the UID on a magic "V3" tag, repeatable until the tag is finalized @@ -3409,7 +3449,8 @@ void SetTag15693Uid_v3(const uint8_t *uid) { } start_time = eof_time + DELAY_ISO15693_VICC_TO_VCD_READER; - if (magic_v3_in_config_mode(&start_time, &eof_time) == false) { + bool in_config = false; + if (magic_v3_read_config(&in_config, &start_time, &eof_time) != PM3_SUCCESS || in_config == false) { reply_ng(CMD_HF_ISO15693_CSETUID_V3, PM3_EWRONGANSWER, NULL, 0); switch_off(); return; @@ -3456,7 +3497,8 @@ void FinalizeTag15693_v3(void) { } start_time = eof_time + DELAY_ISO15693_VICC_TO_VCD_READER; - if (magic_v3_in_config_mode(&start_time, &eof_time) == false) { + bool in_config = false; + if (magic_v3_read_config(&in_config, &start_time, &eof_time) != PM3_SUCCESS || in_config == false) { reply_ng(CMD_HF_ISO15693_CFINALIZE_V3, PM3_EWRONGANSWER, NULL, 0); switch_off(); return; @@ -3467,9 +3509,12 @@ void FinalizeTag15693_v3(void) { res = magic_15_write_blk(ISO15_MAGIC_V3_BLK_SIG_B, g_magic_v3_fin_b, &start_time, &eof_time); } - // the signature must be gone now, otherwise the tag is still changeable - if (res == PM3_SUCCESS && magic_v3_in_config_mode(&start_time, &eof_time)) { - res = PM3_ESOFT; + // finalize wipes the memory, so the signature blocks must read back as something + // else. a read that fails proves nothing and counts as a failure too + if (res == PM3_SUCCESS) { + if (magic_v3_read_config(&in_config, &start_time, &eof_time) != PM3_SUCCESS || in_config) { + res = PM3_ESOFT; + } } reply_ng(CMD_HF_ISO15693_CFINALIZE_V3, (res == PM3_SUCCESS) ? PM3_SUCCESS : PM3_ESOFT, NULL, 0); From 14a5de0bdfb6183e6aa4f28c9f34096b565b940b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=AD=90=E6=AD=AA?= Date: Tue, 22 Sep 2026 21:08:18 +0800 Subject: [PATCH 09/14] fix: let the device verdict decide the outcome of hf 15 csetuid The client only inspected PM3_EWRONGANSWER and then re-verified with its own getUID(), which asks whether the tag carries the requested UID, not whether this command wrote it. Running csetuid --v3 twice with the same UID printed "Setting new UID ( ok )" on the second run even when the device had reported PM3_ESOFT, because the UID written by the first run still matched. Gen1 and V3 now switch on the device status, which already covers tag identification and UID verification. V2 keeps the client side comparison, the device does neither for it. PM3_ETIMEOUT is handled as well, cfinalize already did that. The cfinalize help text and its confirmation warning said the configuration area is erased. Finalize erases the whole tag memory to 00000000. Co-Authored-By: Claude Opus 5 --- client/src/cmdhf15.c | 48 +++++++++++++++++++++++++++++--------------- 1 file changed, 32 insertions(+), 16 deletions(-) diff --git a/client/src/cmdhf15.c b/client/src/cmdhf15.c index b1123537a..08d297bf2 100644 --- a/client/src/cmdhf15.c +++ b/client/src/cmdhf15.c @@ -3405,18 +3405,34 @@ static int CmdHF15CSetUID(const char *Cmd) { return PM3_ESOFT; } - // the device identifies the tag before it writes, V2 has no such check - if (resp.status == PM3_EWRONGANSWER) { - if (use_v3) { - PrintAndLogEx(FAILED, "tag is not an un-finalized magic " _YELLOW_("V3") " tag"); - PrintAndLogEx(HINT, "Hint: signature in blocks 0x14/0x15 not found"); - PrintAndLogEx(HINT, "already finalized or not a V3 tag"); - } else { - PrintAndLogEx(FAILED, "tag doesn't look like a blank/magic " _YELLOW_("Gen1") " tag ( " _RED_("fail") " )"); - PrintAndLogEx(HINT, "Hint: block 0x38/0x39/0x3E/0x3F must be unreadable or blank"); - PrintAndLogEx(HINT, "refusing to risk overwriting a normal tag"); + // Gen1 and V3 identify the tag and verify the new UID on the device, V2 has neither + if (use_v2 == false) { + + switch (resp.status) { + case PM3_SUCCESS: + break; + + case PM3_ETIMEOUT: + PrintAndLogEx(FAILED, "no tag found"); + return PM3_ESOFT; + + case PM3_EWRONGANSWER: + if (use_v3) { + PrintAndLogEx(FAILED, "tag is not an un-finalized magic " _YELLOW_("V3") " tag"); + PrintAndLogEx(HINT, "Hint: signature in blocks 0x14/0x15 not found"); + PrintAndLogEx(HINT, "already finalized or not a V3 tag"); + } else { + PrintAndLogEx(FAILED, "tag doesn't look like a blank/magic " _YELLOW_("Gen1") " tag ( " _RED_("fail") " )"); + PrintAndLogEx(HINT, "Hint: block 0x38/0x39/0x3E/0x3F must be unreadable or blank"); + PrintAndLogEx(HINT, "refusing to risk overwriting a normal tag"); + } + return PM3_ESOFT; + + default: + PrintAndLogEx(FAILED, "Setting new UID ( " _RED_("fail") " )"); + PrintAndLogEx(NORMAL, ""); + return PM3_ESOFT; } - return PM3_ESOFT; } PrintAndLogEx(INFO, "Verifying..."); @@ -3450,10 +3466,10 @@ static int CmdHF15CFinalize(const char *Cmd) { CLIParserContext *ctx; CLIParserInit(&ctx, "hf 15 cfinalize", "Finalize a magic ISO15693 'V3' tag.\n" - _RED_("This operation is irreversible.") " After finalize the configuration\n" - "area is erased and the UID can no longer be changed. Set the UID with\n" - "`" _YELLOW_("hf 15 csetuid --v3") "` first, then lock it in with this command.", - "hf 15 cfinalize -y" + _RED_("This operation is irreversible.") " Finalize erases the whole tag\n" + "memory to 00000000 and locks the UID for good. Set the UID with\n" + "`" _YELLOW_("hf 15 csetuid --v3") "` first, then lock it in with this command.", + "hf 15 cfinalize -y" ); void *argtable[] = { @@ -3466,7 +3482,7 @@ static int CmdHF15CFinalize(const char *Cmd) { CLIParserFree(ctx); if (confirmed == false) { - PrintAndLogEx(WARNING, _RED_("This operation is irreversible!") " The UID will be locked permanently."); + PrintAndLogEx(WARNING, _RED_("This operation is irreversible!") " Tag memory is erased and the UID locked."); PrintAndLogEx(WARNING, "Add " _YELLOW_("-y") " to confirm and proceed."); return PM3_EINVARG; } From 46f0dfeb93951f4f85fbbace32845a385048dea8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=AD=90=E6=AD=AA?= Date: Tue, 22 Sep 2026 21:08:19 +0800 Subject: [PATCH 10/14] doc: 15693 V3 finalize erases the whole tag memory The notes said block 0x15 reads back as 69 E2 5D 00 after finalize. It reads back as 00 00 00 00, and so does every other block. Co-Authored-By: Claude Opus 5 --- doc/magic_cards_notes.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/doc/magic_cards_notes.md b/doc/magic_cards_notes.md index d3f49444a..584d73dba 100644 --- a/doc/magic_cards_notes.md +++ b/doc/magic_cards_notes.md @@ -2716,7 +2716,7 @@ V3: blocks `0x14` and `0x15` hold a signature while the tag is not finalized. ``` hf 15 raw -ac -d 022014 # A5 2B 44 2C -hf 15 raw -ac -d 022015 # 21 AE 93 00 (69 E2 5D 00 after finalize) +hf 15 raw -ac -d 022015 # 21 AE 93 00 (00 00 00 00 after finalize) ``` @@ -2765,7 +2765,8 @@ hf 15 raw -acw -d 02211077665544 hf 15 raw -acw -d 022111332211E0 ``` -Finalize locks the UID for good and can't be undone, block `0x15` changes to `69 E2 5D 00`: +Finalize locks the UID for good and can't be undone. It also erases the whole tag +memory, every block reads back as `00000000` afterwards: ``` hf 15 raw -acw -d 022114A52B442C From c512212e53d6ede806a64b4fb4fd1d8958334eca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=AD=90=E6=AD=AA?= Date: Tue, 22 Sep 2026 21:08:19 +0800 Subject: [PATCH 11/14] doc: regenerate commands.md and commands.json Picks up the hf 15 csetuid and hf 15 cfinalize help text changes, plus the hf ntag424 commands added earlier on this branch. Co-Authored-By: Claude Opus 5 --- doc/commands.json | 41 +++++++++++++++++++++++++++++++++++------ doc/commands.md | 2 ++ 2 files changed, 37 insertions(+), 6 deletions(-) diff --git a/doc/commands.json b/doc/commands.json index 364afb575..a592869c9 100644 --- a/doc/commands.json +++ b/doc/commands.json @@ -1913,7 +1913,7 @@ }, "hf 15 cfinalize": { "command": "hf 15 cfinalize", - "description": "Finalize a magic ISO15693 'V3' tag. This operation is irreversible. After finalize the configuration area is erased and the UID can no longer be changed. Set the UID with `hf 15 csetuid --v3` first, then lock it in with this command.", + "description": "Finalize a magic ISO15693 'V3' tag. This operation is irreversible. Finalize erases the whole tag memory to 00000000 and locks the UID for good. Set the UID with `hf 15 csetuid --v3` first, then lock it in with this command.", "notes": [ "hf 15 cfinalize -y" ], @@ -1926,7 +1926,7 @@ }, "hf 15 csetuid": { "command": "hf 15 csetuid", - "description": "Set UID for magic Chinese card (only works with such cards) For magic 'V3' tags this writes the UID configuration only and is repeatable; run `hf 15 cfinalize` afterwards to lock the UID permanently.", + "description": "Set UID for magic Chinese card (only works with such cards) Gen1 and V3 probe their backdoor blocks first and refuse to write if they read back real data, to avoid corrupting a normal (non-magic) tag. For magic 'V3' tags this writes the UID configuration only and is repeatable; run `hf 15 cfinalize` afterwards to lock the UID permanently.", "notes": [ "hf 15 csetuid -u E011223344556677 -> use gen1 command", "hf 15 csetuid -u E011223344556677 --v2 -> use gen2 command", @@ -9853,6 +9853,20 @@ ], "usage": "hf ntag424 getfs [-h] --fileno " }, + "hf ntag424 gettt": { + "command": "hf ntag424 gettt", + "description": "Read and print Tag Tamper status. Will authenticate if key information is provided.", + "notes": [ + "hf ntag424 gettt --keyno 0 -k 00000000000000000000000000000000" + ], + "offline": false, + "options": [ + "-h, --help This help", + "--keyno Key number", + "-k, --key Key for authentication (HEX 16 bytes)" + ], + "usage": "hf ntag424 gettt [-h] [--keyno ] [-k ]" + }, "hf ntag424 help": { "command": "hf ntag424 help", "description": "help This help view Display content from tag dump file --------------------------------------------------------------------------------------- hf ntag424 info available offline: no Get info about NXP NTAG424 DNA Family styled tag.", @@ -9885,6 +9899,21 @@ ], "usage": "hf ntag424 read [-h] --fileno <1|2|3> [--keyno ] [-k ] [-o ] -l [-m ]" }, + "hf ntag424 setconfig": { + "command": "hf ntag424 setconfig", + "description": "Change a PICC configuration option. Several options are one-way, see the datasheet before using this.", + "notes": [ + "hf ntag424 setconfig -k 00000000000000000000000000000000 --option 07 -d 0100" + ], + "offline": false, + "options": [ + "-h, --help This help", + "-k, --key0 Authentication key (must be key 0, HEX 16 bytes)", + "--option Configuration option (HEX 1 byte)", + "-d, --data Option data (HEX, 1..47 bytes)" + ], + "usage": "hf ntag424 setconfig [-h] -k --option -d [-d ]..." + }, "hf ntag424 view": { "command": "hf ntag424 view", "description": "Print a NTAG 424 DNA dump file (bin/eml/json)", @@ -12776,7 +12805,7 @@ "-1, --ht1 Card type Hitag 1", "-2, --ht2 Card type Hitag 2", "-s, --hts Card type Hitag S", - "-m, --htm Card type Hitag \u03bc" + "-m, --htm Card type Hitag \u00b5" ], "usage": "lf hitag eload [-h12sm] -f " }, @@ -12794,7 +12823,7 @@ "-1, --ht1 Card type Hitag 1", "-2, --ht2 Card type Hitag 2 (default)", "-s, --hts Card type Hitag S", - "-m, --htm Card type Hitag \u03bc" + "-m, --htm Card type Hitag \u00b5" ], "usage": "lf hitag eview [-hv12sm]" }, @@ -16616,8 +16645,8 @@ } }, "metadata": { - "commands_extracted": 924, + "commands_extracted": 926, "extracted_by": "PM3Help2JSON v1.00", - "extracted_on": "2026-09-16T22:14:12+00:00" + "extracted_on": "2026-09-22T13:04:53+00:00" } } diff --git a/doc/commands.md b/doc/commands.md index 68718d6e8..824aa49f4 100644 --- a/doc/commands.md +++ b/doc/commands.md @@ -855,6 +855,8 @@ Check column "offline" for their availability. |`hf ntag424 getfs `|N |`Get file settings` |`hf ntag424 changefs `|N |`Change file settings` |`hf ntag424 changekey `|N |`Change key` +|`hf ntag424 gettt `|N |`Get Tag Tamper status` +|`hf ntag424 setconfig `|N |`Set PICC configuration option` ### hf saflok From 12b0143f4649272843965848c6f91e40013d5cda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=AD=90=E6=AD=AA?= Date: Tue, 22 Sep 2026 22:51:14 +0800 Subject: [PATCH 12/14] fix: hf 15 cfinalize does not erase the tag memory The help text and the -y warning said finalize erases the whole tag memory to 00000000. A dump of a tag before and after finalize shows otherwise: the configuration blocks are cleared, but user data is relocated rather than erased. Data written to block 0x38 before finalize read back at block 0x20 afterwards. Co-Authored-By: Claude Opus 5 --- client/src/cmdhf15.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/src/cmdhf15.c b/client/src/cmdhf15.c index 08d297bf2..3bfa55a05 100644 --- a/client/src/cmdhf15.c +++ b/client/src/cmdhf15.c @@ -3466,8 +3466,8 @@ static int CmdHF15CFinalize(const char *Cmd) { CLIParserContext *ctx; CLIParserInit(&ctx, "hf 15 cfinalize", "Finalize a magic ISO15693 'V3' tag.\n" - _RED_("This operation is irreversible.") " Finalize erases the whole tag\n" - "memory to 00000000 and locks the UID for good. Set the UID with\n" + _RED_("This operation is irreversible.") " It locks the UID for good,\n" + "clears the configuration blocks and shifts the block numbering. Set it with\n" "`" _YELLOW_("hf 15 csetuid --v3") "` first, then lock it in with this command.", "hf 15 cfinalize -y" ); @@ -3482,7 +3482,7 @@ static int CmdHF15CFinalize(const char *Cmd) { CLIParserFree(ctx); if (confirmed == false) { - PrintAndLogEx(WARNING, _RED_("This operation is irreversible!") " Tag memory is erased and the UID locked."); + PrintAndLogEx(WARNING, _RED_("This operation is irreversible!") " The UID is locked and the blocks move."); PrintAndLogEx(WARNING, "Add " _YELLOW_("-y") " to confirm and proceed."); return PM3_EINVARG; } From 305b259fbe8e582ba0f55d6237d45ad580264e42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=AD=90=E6=AD=AA?= Date: Tue, 22 Sep 2026 22:51:14 +0800 Subject: [PATCH 13/14] doc: 15693 V3 finalize moves user data instead of erasing it Corrects the claim that every block reads back as 00000000 after finalize. On the tag tested, block 0x38 held 11223344 before finalize and block 0x20 held it afterwards, a shift of 0x18, while blocks 0x10/0x11 and 0x14/0x15 were cleared. Co-Authored-By: Claude Opus 5 --- doc/magic_cards_notes.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/doc/magic_cards_notes.md b/doc/magic_cards_notes.md index 584d73dba..09a9ee771 100644 --- a/doc/magic_cards_notes.md +++ b/doc/magic_cards_notes.md @@ -2765,8 +2765,10 @@ hf 15 raw -acw -d 02211077665544 hf 15 raw -acw -d 022111332211E0 ``` -Finalize locks the UID for good and can't be undone. It also erases the whole tag -memory, every block reads back as `00000000` afterwards: +Finalize locks the UID for good and can't be undone. It clears the configuration +blocks and changes the block numbering: on the tag tested, data written to block +`0x38` before finalize read back at block `0x20` afterwards, a shift of `0x18`. +User data is moved, not erased. Note down what is where before finalizing: ``` hf 15 raw -acw -d 022114A52B442C From 420bdf161d23fb3fce0bef4ec36a47f6e10ecf6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=AD=90=E6=AD=AA?= Date: Tue, 22 Sep 2026 22:51:15 +0800 Subject: [PATCH 14/14] doc: regenerate commands.json for the cfinalize help text Co-Authored-By: Claude Opus 5 --- doc/commands.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/commands.json b/doc/commands.json index a592869c9..f22d25c99 100644 --- a/doc/commands.json +++ b/doc/commands.json @@ -1913,7 +1913,7 @@ }, "hf 15 cfinalize": { "command": "hf 15 cfinalize", - "description": "Finalize a magic ISO15693 'V3' tag. This operation is irreversible. Finalize erases the whole tag memory to 00000000 and locks the UID for good. Set the UID with `hf 15 csetuid --v3` first, then lock it in with this command.", + "description": "Finalize a magic ISO15693 'V3' tag. This operation is irreversible. It locks the UID for good, clears the configuration blocks and shifts the block numbering. Set it with `hf 15 csetuid --v3` first, then lock it in with this command.", "notes": [ "hf 15 cfinalize -y" ], @@ -16647,6 +16647,6 @@ "metadata": { "commands_extracted": 926, "extracted_by": "PM3Help2JSON v1.00", - "extracted_on": "2026-09-22T13:04:53+00:00" + "extracted_on": "2026-09-22T14:39:12+00:00" } }