Merge pull request #3198 from Antiklesys/master

Added wildcard support for hf secc sim payloads
This commit is contained in:
Iceman
2026-04-02 19:12:36 +07:00
committed by GitHub
5 changed files with 75 additions and 10 deletions
+1
View File
@@ -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)
+21 -3
View File
@@ -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;
+8 -2
View File
@@ -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;
+7 -2
View File
@@ -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"
}
]
}
+38 -3
View File
@@ -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;
}