Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
24 changes: 21 additions & 3 deletions armsrc/secc.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
10 changes: 8 additions & 2 deletions armsrc/secc.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
9 changes: 7 additions & 2 deletions client/resources/hidconfig_sample.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,24 @@
"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"
},
{
"_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"
}
]
}
41 changes: 38 additions & 3 deletions client/src/cmdhfsecc.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
Expand Down
Loading