From b3781a7bbcd73d5c046e66690de8b5d2d93ae5c0 Mon Sep 17 00:00:00 2001 From: kormax <3392860+kormax@users.noreply.github.com> Date: Tue, 30 Jun 2026 23:41:44 +0300 Subject: [PATCH 1/3] Update ASK CTS general info output --- armsrc/iso14443b.c | 2 +- client/src/cmdhf14b.c | 38 ++++++++++++++++++++++++++++++++++---- include/protocols.h | 11 +++++++---- 3 files changed, 42 insertions(+), 9 deletions(-) diff --git a/armsrc/iso14443b.c b/armsrc/iso14443b.c index 579b06391..13562543e 100644 --- a/armsrc/iso14443b.c +++ b/armsrc/iso14443b.c @@ -1818,7 +1818,7 @@ static int iso14443b_select_cts_card(iso14b_cts_card_select_t *card) { } if (card) { - // pc. fc Product code, Facility code + // REQT response payload: Product code, FAB code card->pc = r[0]; card->fc = r[1]; } diff --git a/client/src/cmdhf14b.c b/client/src/cmdhf14b.c index 70f70db30..5a16698ab 100644 --- a/client/src/cmdhf14b.c +++ b/client/src/cmdhf14b.c @@ -48,6 +48,9 @@ // for static arrays #define ST25TB_SR_BLOCK_SIZE 4 +#define CTS_BLOCK_SIZE 2 +#define CTS256_BLOCK_COUNT 16 +#define CTS512_BLOCK_COUNT 32 // SR memory sizes @@ -757,17 +760,44 @@ static void print_st_general_info(uint8_t *data, uint8_t len) { PrintAndLogEx(SUCCESS, "Chip: %02X, " _YELLOW_("%s"), chipid, get_st_chip_model(chipid)); } +typedef struct { + uint8_t product_code; + const char *name; + uint8_t block_count; +} cts_product_info_t; + +static const cts_product_info_t cts_products[] = { + {ASK_CTS_PRODUCT_CODE_CTS256B, "CTS256B", CTS256_BLOCK_COUNT}, + {ASK_CTS_PRODUCT_CODE_CTS512B, "CTS512B", CTS512_BLOCK_COUNT}, +}; + +static const cts_product_info_t cts_unknown_product = { + 0x00, + "unknown", + CTS256_BLOCK_COUNT, +}; + +static const cts_product_info_t *get_cts_product_info(uint8_t product_code) { + for (size_t i = 0; i < ARRAYLEN(cts_products); i++) { + if (cts_products[i].product_code == product_code) { + return &cts_products[i]; + } + } + return &cts_unknown_product; +} + // print UID info from ASK CT chips static void print_ct_general_info(void *vcard) { iso14b_cts_card_select_t card; memcpy(&card, (iso14b_cts_card_select_t *)vcard, sizeof(iso14b_cts_card_select_t)); uint32_t uid32 = MemLeToUint4byte(card.uid); + const cts_product_info_t *product_info = get_cts_product_info(card.pc); PrintAndLogEx(NORMAL, ""); - PrintAndLogEx(SUCCESS, "ASK C-Ticket"); - PrintAndLogEx(SUCCESS, " UID: " _GREEN_("%s") " ( " _YELLOW_("%010u") " )", sprint_hex(card.uid, sizeof(card.uid)), uid32); - PrintAndLogEx(SUCCESS, " Product Code: %02X", card.pc); - PrintAndLogEx(SUCCESS, " Facility Code: %02X", card.fc); + PrintAndLogEx(INFO, "--- " _CYAN_("ASK CTS / C-ticket") " -----------------------"); + PrintAndLogEx(SUCCESS, " UID : " _GREEN_("%s") " ( " _YELLOW_("%010u") " )", sprint_hex(card.uid, sizeof(card.uid)), uid32); + PrintAndLogEx(SUCCESS, " Product Code : %02X ( " _YELLOW_("%s") " )", card.pc, product_info->name); + PrintAndLogEx(SUCCESS, " Fab Code : %02X", card.fc); PrintAndLogEx(NORMAL, ""); } diff --git a/include/protocols.h b/include/protocols.h index 37378d56e..d70176434 100644 --- a/include/protocols.h +++ b/include/protocols.h @@ -414,10 +414,13 @@ ISO 7816-4 Basic interindustry commands. For command APDU's. #define ASK_REQT 0x10 #define ASK_IDENTIFY 0x0F #define ASK_SELECT 0x9F -#define ASK_MULTREAD (0x1 << 4) // High nibble -#define ASK_UPDATE (0x3 << 4) // High nibble -#define ASK_WRITE (0x5 << 4) // High nibble -#define ASK_READ (0x6 << 4) // High nibble +#define ASK_CTS_PRODUCT_CODE_CTS256B 0x50 +#define ASK_CTS_PRODUCT_CODE_CTS512B 0x60 +#define ASK_BLOCK_ADDRESS_MASK 0x1F +#define ASK_MULTREAD (0x1 << 5) // 001 AAAAA +#define ASK_UPDATE (0x3 << 5) // 011 AAAAA +#define ASK_WRITE (0x5 << 5) // 101 AAAAA +#define ASK_READ (0x6 << 5) // 110 AAAAA #define ASK_DESACTIVATE 0xF0 From 84fa60981250c79e0b9d5c485872a3555b1029a7 Mon Sep 17 00:00:00 2001 From: kormax <3392860+kormax@users.noreply.github.com> Date: Tue, 30 Jun 2026 23:42:41 +0300 Subject: [PATCH 2/3] Add command usage hints for CTS and Innovatron --- client/src/cmdhf14b.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/src/cmdhf14b.c b/client/src/cmdhf14b.c index 5a16698ab..65aa55ac7 100644 --- a/client/src/cmdhf14b.c +++ b/client/src/cmdhf14b.c @@ -1644,6 +1644,7 @@ static bool HF14B_ask_ct_reader(bool verbose) { switch (resp.status) { case PM3_SUCCESS: { print_ct_general_info(resp.data.asBytes); + PrintAndLogEx(HINT, "Hint: Try `" _YELLOW_("hf 14b ct") "`"); return true; } case PM3_ELENGTH: { @@ -1716,6 +1717,7 @@ static bool HF14B_prime_reader(bool verbose) { return false; } print_prime_general_info(&card); + PrintAndLogEx(HINT, "Hint: Try `" _YELLOW_("hf calypso") "`"); return true; } From 95c681ae713eadfbbeca37711c6778a3e869ee4b Mon Sep 17 00:00:00 2001 From: kormax <3392860+kormax@users.noreply.github.com> Date: Tue, 30 Jun 2026 23:43:34 +0300 Subject: [PATCH 3/3] Add ASK CTS block commands --- CHANGELOG.md | 1 + client/src/cmdhf14b.c | 215 ++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 206 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f1ce47ea6..7e4ba07c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ 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 ctrdbl` and `hf 14b ctdump` commands for interacting with ASK CTS tags (@kormax) - Fixed `hf legic migrate` failing to parse the optional DCF argument as hex (@IdanHo) - Add support for parsing Finnish Helsinki Regional Transport (HRT) travel cards (@sanduuz) - Added standalone mode `HF_DOEGOX_COMMIT`: DESFire suspended commit without relay (@doegox) diff --git a/client/src/cmdhf14b.c b/client/src/cmdhf14b.c index 65aa55ac7..826ac7ca3 100644 --- a/client/src/cmdhf14b.c +++ b/client/src/cmdhf14b.c @@ -837,26 +837,26 @@ static void print_footer(void) { PrintAndLogEx(NORMAL, ""); } -/* static void print_ct_blocks(uint8_t *data, size_t len) { - size_t blocks = len / ST25TB_SR_BLOCK_SIZE; + size_t blocks = len / CTS_BLOCK_SIZE; - print_hdr(); + PrintAndLogEx(NORMAL, ""); + PrintAndLogEx(INFO, " block# | data | ascii"); + PrintAndLogEx(INFO, "---------+-------+------"); - for (int i = 0; i <= blocks; i++) { + for (int i = 0; i < blocks; i++) { PrintAndLogEx(INFO, - "%3d/0x%02X | %s | %s | %s", + "%3d/0x%02X | %s| %s", i, i, - sprint_hex(data + (i * 4), 4), - " ", - sprint_ascii(data + (i * 4), 4) + sprint_hex(data + (i * CTS_BLOCK_SIZE), CTS_BLOCK_SIZE), + sprint_ascii(data + (i * CTS_BLOCK_SIZE), CTS_BLOCK_SIZE) ); } - print_footer(); + PrintAndLogEx(INFO, "---------+-------+------"); + PrintAndLogEx(NORMAL, ""); } -*/ static void print_sr_blocks(uint8_t *data, size_t len, const uint8_t *uid, bool dense_output) { @@ -1476,6 +1476,85 @@ static int CmdHF14Binfo(const char *Cmd) { // #define ISO14443B_READ_BLK 0x08 // #define ISO14443B_WRITE_BLK 0x09 +static int select_cts_card_14443b(bool disconnect, iso14b_cts_card_select_t *card, bool verbose) { + iso14b_raw_cmd_t packet = { + .flags = (ISO14B_CONNECT | ISO14B_SELECT_CTS | ISO14B_CLEARTRACE), + .timeout = 0, + .rawlen = 0, + }; + + if (disconnect) { + packet.flags |= ISO14B_DISCONNECT; + } + + clearCommandBuffer(); + PacketResponseNG resp; + SendCommandNG(CMD_HF_ISO14443B_COMMAND, (uint8_t *)&packet, sizeof(iso14b_raw_cmd_t)); + if (WaitForResponseTimeout(CMD_HF_ISO14443B_COMMAND, &resp, TIMEOUT) == false) { + if (verbose) { + PrintAndLogEx(WARNING, "timeout while waiting for reply"); + } + return PM3_ETIMEOUT; + } + + if (resp.status != PM3_SUCCESS) { + if (verbose) { + PrintAndLogEx(FAILED, "ISO 14443-b CTS select failed"); + } + return resp.status; + } + + if (resp.length < sizeof(iso14b_cts_card_select_t)) { + if (verbose) { + PrintAndLogEx(FAILED, "ISO 14443-3 CTS wrong length"); + } + return PM3_ELENGTH; + } + + if (card) { + memcpy(card, resp.data.asBytes, sizeof(iso14b_cts_card_select_t)); + } + return PM3_SUCCESS; +} + +static int read_cts_block(uint8_t blockno, uint8_t *out, uint16_t out_len) { + uint8_t psize = sizeof(iso14b_raw_cmd_t) + 1; + iso14b_raw_cmd_t *packet = (iso14b_raw_cmd_t *)calloc(1, psize); + if (packet == NULL) { + PrintAndLogEx(WARNING, "Failed to allocate memory"); + return PM3_EMALLOC; + } + packet->flags = (ISO14B_APPEND_CRC | ISO14B_RAW); + packet->timeout = 0; + packet->rawlen = 1; + packet->raw[0] = ASK_READ | (blockno & ASK_BLOCK_ADDRESS_MASK); + + clearCommandBuffer(); + PacketResponseNG resp; + SendCommandNG(CMD_HF_ISO14443B_COMMAND, (uint8_t *)packet, psize); + free(packet); + if (WaitForResponseTimeout(CMD_HF_ISO14443B_COMMAND, &resp, TIMEOUT) == false) { + return PM3_ETIMEOUT; + } + + if (resp.status != PM3_SUCCESS) { + return resp.status; + } + + if (resp.length != CTS_BLOCK_SIZE + 2) { + return PM3_ELENGTH; + } + + if (check_crc(CRC_14443_B, resp.data.asBytes, resp.length) == false) { + return PM3_ECRC; + } + + if (out) { + memcpy(out, resp.data.asBytes, MIN(out_len, CTS_BLOCK_SIZE)); + } + return PM3_SUCCESS; +} + static int read_sr_block(uint8_t blockno, uint8_t *out, uint16_t out_len) { struct { uint8_t blockno; @@ -1807,6 +1886,119 @@ static int CmdHF14BReader(const char *Cmd) { return readHF14B(cm, verbose, read_plot); } +static int CmdHF14BCtsDump(const char *Cmd) { + CLIParserContext *ctx; + CLIParserInit(&ctx, "hf 14b ctdump", + "Read and print ASK CTS / C-ticket memory blocks", + "hf 14b ctdump\n" + "hf 14b ctdump -v" + ); + + void *argtable[] = { + arg_param_begin, + arg_lit0("v", "verbose", "verbose output"), + arg_param_end + }; + CLIExecWithReturn(ctx, Cmd, argtable, true); + bool verbose = arg_get_lit(ctx, 1); + CLIParserFree(ctx); + + iso14b_cts_card_select_t card = {0}; + int status = select_cts_card_14443b(false, &card, verbose); + if (status != PM3_SUCCESS) { + switch_off_field_14b(); + PrintAndLogEx(WARNING, "no ASK CTS / C-ticket tag found"); + return status; + } + + print_ct_general_info(&card); + + const cts_product_info_t *product_info = get_cts_product_info(card.pc); + uint8_t block_count = product_info->block_count; + uint8_t data[CTS512_BLOCK_COUNT * CTS_BLOCK_SIZE] = {0}; + + PrintAndLogEx(INFO, "Reading %u blocks", block_count); + + for (uint8_t blockno = 0; blockno < block_count; blockno++) { + status = PM3_ESOFT; + for (uint8_t retry = 0; retry < 3; retry++) { + status = read_cts_block(blockno, data + (blockno * CTS_BLOCK_SIZE), CTS_BLOCK_SIZE); + if (status == PM3_SUCCESS) { + break; + } + if (verbose) { + PrintAndLogEx(DEBUG, "block %u read failed, status %d, retry %u", blockno, status, retry + 1); + } + } + + if (status != PM3_SUCCESS) { + switch_off_field_14b(); + PrintAndLogEx(FAILED, "failed to read block %u (status %d)", blockno, status); + return status; + } + } + + switch_off_field_14b(); + print_ct_blocks(data, block_count * CTS_BLOCK_SIZE); + return PM3_SUCCESS; +} + +static int CmdHF14BCtRdBl(const char *Cmd) { + CLIParserContext *ctx; + CLIParserInit(&ctx, "hf 14b ctrdbl", + "Read an ASK CTS / C-ticket block", + "hf 14b ctrdbl -b 6\n" + "hf 14b ctrdbl --block 15 -v" + ); + + void *argtable[] = { + arg_param_begin, + arg_int1("b", "block", "", "block number"), + arg_lit0("v", "verbose", "verbose output"), + arg_param_end + }; + CLIExecWithReturn(ctx, Cmd, argtable, false); + int blockno = arg_get_int_def(ctx, 1, -1); + bool verbose = arg_get_lit(ctx, 2); + CLIParserFree(ctx); + + if (blockno < 0 || blockno > ASK_BLOCK_ADDRESS_MASK) { + PrintAndLogEx(FAILED, "block number out of range, max %u (0x%02X), got " _RED_("%d"), + ASK_BLOCK_ADDRESS_MASK, ASK_BLOCK_ADDRESS_MASK, blockno); + return PM3_EINVARG; + } + + iso14b_cts_card_select_t card = {0}; + int status = select_cts_card_14443b(false, &card, verbose); + if (status != PM3_SUCCESS) { + switch_off_field_14b(); + PrintAndLogEx(WARNING, "no ASK CTS / C-ticket tag found"); + return status; + } + + const cts_product_info_t *product_info = get_cts_product_info(card.pc); + if (blockno >= product_info->block_count) { + switch_off_field_14b(); + PrintAndLogEx(FAILED, "block number out of range for %s, max %u (0x%02X), got " _RED_("%d"), + product_info->name, product_info->block_count - 1, product_info->block_count - 1, blockno); + return PM3_EINVARG; + } + + if (verbose) { + print_ct_general_info(&card); + } + + uint8_t out[CTS_BLOCK_SIZE] = {0}; + status = read_cts_block((uint8_t)blockno, out, sizeof(out)); + switch_off_field_14b(); + + if (status == PM3_SUCCESS) { + PrintAndLogEx(SUCCESS, "block %02u... " _GREEN_("%s") " | " _GREEN_("%s"), + blockno, sprint_hex(out, sizeof(out)), sprint_ascii(out, sizeof(out))); + } + return status; +} + // Read SRI512|SRIX4K block static int CmdHF14BSriRdBl(const char *Cmd) { @@ -3395,6 +3587,9 @@ static command_t CommandTable[] = { {"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, IfPm3Iso14443b, "------------------ " _CYAN_("ASK CTS / C-ticket") " ------------------"}, + {"ctdump", CmdHF14BCtsDump, IfPm3Iso14443b, "Dump ASK CTS/C-ticket"}, + {"ctrdbl", CmdHF14BCtRdBl, IfPm3Iso14443b, "Read ASK CTS/C-ticket block"}, {"---------", CmdHelp, IfPm3Iso14443b, "------------------------- " _CYAN_("Magic") " -----------------------"}, {"setuid", CmdHF14BSetUID, IfPm3Iso14443b, "Set UID for magic card"}, {NULL, NULL, NULL, NULL}