diff --git a/CHANGELOG.md b/CHANGELOG.md index 2bf0dda9f..4e20c7ee3 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 wildcard support to `hf secc sim` payloads (@antiklesys) - Added `hf secc` to build a base for simulating basic function of iclass SE config cards (@antiklesys) - Improved SIO parsing for `hf iclass view` based on Iceman's "Dismantling the SEOS Protocol" talk (@antiklesys) - Added live fc/cn update to `hf iclass tagsim` refreshing the csn with each update (@antiklesys) diff --git a/armsrc/secc.c b/armsrc/secc.c index aef3f79af..60d06ce2d 100644 --- a/armsrc/secc.c +++ b/armsrc/secc.c @@ -128,12 +128,30 @@ bool hid_config_card_handle_iblock(const uint8_t *cmd, int len, tag_response_inf // ----- Check custom APDU table first (overrides hardcoded responses) ----- // cmd[off] is the start of the APDU payload; len includes trailing CRC (2 bytes). + // apdu_mask bit i=1 → exact match; bit i=0 → wildcard: + // apdu[i]=0x00 → any single byte (**); apdu[i]=0x01 → length-prefix skip (##). for (int i = 0; i < s_apdu_count; i++) { if (s_apdu_table[i].apdu_len == 0) continue; - if (len - off < (int)(s_apdu_table[i].apdu_len + 2)) - continue; - if (memcmp(&cmd[off], s_apdu_table[i].apdu, s_apdu_table[i].apdu_len) == 0) { + bool match = true; + int recv_pos = 0; + for (int j = 0; j < (int)s_apdu_table[i].apdu_len && match; j++) { + bool is_exact = (s_apdu_table[i].apdu_mask[j / 8] & (1u << (j % 8))) != 0; + if (off + recv_pos >= len - 2) { match = false; break; } + if (is_exact) { + if (cmd[off + recv_pos] != s_apdu_table[i].apdu[j]) + match = false; + recv_pos++; + } else if (s_apdu_table[i].apdu[j] == 0x01) { + // ## : read length byte, skip 1 + N bytes total + uint8_t payload_len = cmd[off + recv_pos]; + recv_pos += 1 + payload_len; + } else { + // ** : skip one byte + recv_pos++; + } + } + if (match) { memcpy(rsp, s_apdu_table[i].resp, s_apdu_table[i].resp_len); ri->response_n = off + s_apdu_table[i].resp_len; return true; diff --git a/armsrc/secc.h b/armsrc/secc.h index a4903bc3e..cead53bc3 100644 --- a/armsrc/secc.h +++ b/armsrc/secc.h @@ -29,12 +29,18 @@ #define HID_APDU_MAX_ENTRIES 8 #define HID_APDU_MAX_CMD 20 // max APDU command bytes to prefix-match #define HID_APDU_MAX_RESP 32 // max response bytes (without PCB/CID/CRC) +#define HID_APDU_MASK_LEN 3 // ceil(HID_APDU_MAX_CMD / 8): bitmask for wildcard bytes -// One custom APDU override entry: if the incoming APDU starts with -// apdu[0..apdu_len-1], respond with resp[0..resp_len-1] (raw APDU payload). +// One custom APDU override entry: if the incoming APDU matches the pattern, +// respond with resp[0..resp_len-1] (raw APDU payload). +// apdu_mask bit i=1: byte i must match apdu[i] exactly. +// apdu_mask bit i=0: wildcard — apdu[i] selects type: +// 0x00 = match any single byte ("**" in JSON) +// 0x01 = length-prefix skip: read length byte N, skip N+1 bytes total ("##" in JSON) typedef struct { uint8_t apdu[HID_APDU_MAX_CMD]; uint8_t apdu_len; + uint8_t apdu_mask[HID_APDU_MASK_LEN]; uint8_t resp[HID_APDU_MAX_RESP]; uint8_t resp_len; } PACKED hid_apdu_entry_t; diff --git a/client/resources/hidconfig_sample.json b/client/resources/hidconfig_sample.json index 7d0a526cd..e10fb7119 100644 --- a/client/resources/hidconfig_sample.json +++ b/client/resources/hidconfig_sample.json @@ -5,12 +5,12 @@ "ATS": "1478F7B10280590180415254454346477300011B", "APDUResponses": [ { - "_comment": "Prefix-match SELECT by AID (17) -> 9000 (ok)", + "_comment": "Prefix-match SELECT by AID (A000000382001700010100) -> 9000 (ok) / 6A82 (file not found)", "APDU": "00A404000AA000000382001700010100", "Response": "9000" }, { - "_comment": "Prefix-match SELECT by AID (13) -> 6A82 (file not found)", + "_comment": "Prefix-match SELECT by AID (A000000382001300010100) -> 9000 (ok) / 6A82 (file not found)", "APDU": "00A404000AA000000382001300010100", "Response": "6A82" }, @@ -18,6 +18,11 @@ "_comment": "Check card credits: A0 D4 00 00 00 -> 00 00 90 00 (reply 0)", "APDU": "A0D4000000", "Response": "00009000" + }, + { + "_comment": "Busy? -> 9000 (done) / 017B (busy)", + "APDU": "017B", + "Response": "9000" } ] } diff --git a/client/src/cmdhfsecc.c b/client/src/cmdhfsecc.c index d3120dde2..1507b3514 100644 --- a/client/src/cmdhfsecc.c +++ b/client/src/cmdhfsecc.c @@ -20,10 +20,12 @@ #define HID_APDU_MAX_ENTRIES 8 #define HID_APDU_MAX_CMD 20 #define HID_APDU_MAX_RESP 32 +#define HID_APDU_MASK_LEN 3 // ceil(HID_APDU_MAX_CMD / 8) typedef struct { uint8_t apdu[HID_APDU_MAX_CMD]; uint8_t apdu_len; + uint8_t apdu_mask[HID_APDU_MASK_LEN]; uint8_t resp[HID_APDU_MAX_RESP]; uint8_t resp_len; } PACKED hid_apdu_entry_t; @@ -152,11 +154,44 @@ static int CmdHFHIDConfigSim(const char *Cmd) { json_t *jresp = json_object_get(entry, "Response"); if (!json_is_string(japdu) || !json_is_string(jresp)) continue; - int alen = hex_to_bytes(json_string_value(japdu), - apdu_table[apdu_count].apdu, HID_APDU_MAX_CMD); + + // Parse APDU hex string with optional "**" wildcard bytes. + // mask bit i=1 → exact match; bit i=0 → wildcard. + const char *apdu_str = json_string_value(japdu); + int alen = 0; + bool apdu_ok = true; + memset(apdu_table[apdu_count].apdu_mask, 0xFF, HID_APDU_MASK_LEN); + while (*apdu_str && alen < HID_APDU_MAX_CMD) { + char hi = apdu_str[0]; + char lo = apdu_str[1]; + if (lo == '\0') { apdu_ok = false; break; } + apdu_str += 2; + if (hi == '*' && lo == '*') { + apdu_table[apdu_count].apdu[alen] = 0x00; // ** wildcard + apdu_table[apdu_count].apdu_mask[alen / 8] &= ~(1u << (alen % 8)); + } else if (hi == '#' && lo == '#') { + apdu_table[apdu_count].apdu[alen] = 0x01; // ## length-prefix skip + apdu_table[apdu_count].apdu_mask[alen / 8] &= ~(1u << (alen % 8)); + } else { + uint8_t val = 0; + for (int nb = 0; nb < 2; nb++) { + char c = (nb == 0) ? hi : lo; + uint8_t nib; + if (c >= '0' && c <= '9') nib = c - '0'; + else if (c >= 'A' && c <= 'F') nib = c - 'A' + 10; + else if (c >= 'a' && c <= 'f') nib = c - 'a' + 10; + else { apdu_ok = false; break; } + val = (val << 4) | nib; + } + if (!apdu_ok) break; + apdu_table[apdu_count].apdu[alen] = val; + } + alen++; + } + int rlen = hex_to_bytes(json_string_value(jresp), apdu_table[apdu_count].resp, HID_APDU_MAX_RESP); - if (alen <= 0 || rlen <= 0) { + if (!apdu_ok || alen <= 0 || rlen <= 0) { PrintAndLogEx(WARNING, "APDUResponses[%zu]: invalid hex, skipping", i); continue; }