mirror of
https://github.com/RfidResearchGroup/ChameleonUltra.git
synced 2026-05-12 11:22:59 -07:00
fix hf14a sniff
This commit is contained in:
@@ -2159,7 +2159,7 @@ static bool tcl_apdu_(
|
||||
pcd_14a_reader_timeout_set(600);
|
||||
uint16_t rbits = 0;
|
||||
uint8_t st = pcd_14a_reader_bytes_transfer(
|
||||
PCD_TRANSCEIVE, abuf, frame_len, rbuf, &rbits, 70u * 8u);
|
||||
PCD_TRANSCEIVE, abuf, frame_len, rbuf, &rbits, 270u * 8u);
|
||||
if (st != STATUS_HF_TAG_OK || rbits < 24u) return false;
|
||||
|
||||
uint16_t rb = rbits / 8u;
|
||||
@@ -2177,8 +2177,39 @@ static bool tcl_apdu_(
|
||||
}
|
||||
|
||||
/* Handle card-side chaining ---------------------------------------- */
|
||||
uint16_t chain_rbits = 0; /* hoisted: used in both WTX and R(ACK) paths */
|
||||
uint8_t chain_st = STATUS_HF_TAG_OK;
|
||||
while (resp_pcb & 0x20u) {
|
||||
if ((resp_pcb & 0xC0u) != 0x00u) break; /* not an I-block */
|
||||
if ((resp_pcb & 0xC0u) != 0x00u) {
|
||||
/* S-block: handle S(WTX), reject others.
|
||||
* Some Visa/MC cards send WTX (PCB=0xF2) before their FCI,
|
||||
* requesting more processing time. We must echo it back.
|
||||
* The WTXM byte was spuriously added to chain_buf — undo it. */
|
||||
if ((resp_pcb & 0xF0u) == 0xF0u) {
|
||||
chain_len -= dlen; /* remove spurious WTXM byte(s) */
|
||||
uint8_t wtx_r[4];
|
||||
wtx_r[0] = resp_pcb; /* mirror the S(WTX) PCB */
|
||||
wtx_r[1] = rbuf[1]; /* WTXM from last received frame */
|
||||
crc_14a_append(wtx_r, 2);
|
||||
write_register_single(ComIrqReg, 0x7F);
|
||||
pcd_14a_reader_timeout_set(600);
|
||||
chain_rbits = 0;
|
||||
chain_st = pcd_14a_reader_bytes_transfer(
|
||||
PCD_TRANSCEIVE, wtx_r, 4, rbuf, &chain_rbits, 270u * 8u);
|
||||
if (chain_st != STATUS_HF_TAG_OK || chain_rbits < 24u) break;
|
||||
rb = chain_rbits / 8u;
|
||||
crc_14a_calculate(rbuf, rb - 2u, crc);
|
||||
if (rbuf[rb-2] != crc[0] || rbuf[rb-1] != crc[1]) break;
|
||||
resp_pcb = rbuf[0];
|
||||
dlen = (uint8_t)(rb - 3u);
|
||||
if (dlen > 0u && chain_len + dlen < 512u) {
|
||||
memcpy(&chain_buf[chain_len], &rbuf[1], dlen);
|
||||
chain_len += dlen;
|
||||
}
|
||||
continue; /* re-check while with new resp_pcb */
|
||||
}
|
||||
break; /* other S-blocks (DESELECT etc.): stop */
|
||||
}
|
||||
|
||||
/* R(ACK) block_num must match the received I-block's block_num */
|
||||
uint8_t rf[3];
|
||||
@@ -2188,9 +2219,9 @@ static bool tcl_apdu_(
|
||||
/* Use bytes_transfer for chain R(ACK) — clear stale RxIRq first */
|
||||
write_register_single(ComIrqReg, 0x7F);
|
||||
pcd_14a_reader_timeout_set(600);
|
||||
uint16_t chain_rbits = 0;
|
||||
uint8_t chain_st = pcd_14a_reader_bytes_transfer(
|
||||
PCD_TRANSCEIVE, rf, 3, rbuf, &chain_rbits, 70u * 8u);
|
||||
chain_rbits = 0;
|
||||
chain_st = pcd_14a_reader_bytes_transfer(
|
||||
PCD_TRANSCEIVE, rf, 3, rbuf, &chain_rbits, 270u * 8u);
|
||||
if (chain_st != STATUS_HF_TAG_OK || chain_rbits < 24u) break;
|
||||
rb = chain_rbits / 8u; /* bytes_transfer returns BIT count */
|
||||
|
||||
@@ -2232,7 +2263,7 @@ static data_frame_tx_t *cmd_processor_hf14a_4_emv_scan(uint16_t cmd, uint16_t st
|
||||
|
||||
/* ---- helpers -------------------------------------------------- */
|
||||
static uint8_t abuf[64]; /* TX frame: PCB + APDU + CRC */
|
||||
static uint8_t rbuf[70]; /* single-frame receive buffer: FIFO(64) + PCB(1) + CRC(2) + slack */
|
||||
static uint8_t rbuf[270]; /* single-frame receive buffer: up to 256 bytes data + PCB + CRC + slack (FSDI=8 → FSD=256) */
|
||||
static uint8_t chain_buf[512]; /* reassembled chained response */
|
||||
uint16_t rbits;
|
||||
uint8_t blk = 0; /* alternating block number */
|
||||
@@ -2359,27 +2390,95 @@ static data_frame_tx_t *cmd_processor_hf14a_4_emv_scan(uint16_t cmd, uint16_t st
|
||||
APPEND_PAIR(sel_cmd, sel_len, sel_resp, sel_rlen);
|
||||
num_apdus++;
|
||||
|
||||
/* ---- Step 4: GPO -------------------------------------------- */
|
||||
static const uint8_t gpo_cmd[] = {0x80, 0xA8, 0x00, 0x00, 0x02, 0x83, 0x00, 0x00};
|
||||
uint8_t *gpo_resp; uint16_t gpo_rlen;
|
||||
if (!SEND_APDU(gpo_cmd, sizeof(gpo_cmd), &gpo_resp, &gpo_rlen)) goto done;
|
||||
APPEND_PAIR(gpo_cmd, sizeof(gpo_cmd), gpo_resp, gpo_rlen);
|
||||
/* ---- Step 4: GPO — parse PDOL from SELECT AID FCI, fill zeros ------- */
|
||||
/* PDOL is tag 9F38 in the FCI (sel_resp). Parse it to know how many
|
||||
* bytes the card expects. Fill all fields with zeros (offline scan). */
|
||||
uint8_t pdol_len = 0;
|
||||
for (uint8_t pi = 0; pi + 2 < sel_rlen; pi++) {
|
||||
/* 2-byte tag detection: first byte has bits[4:0] == 0x1F */
|
||||
uint8_t ptag1 = sel_resp[pi];
|
||||
uint8_t ptag2 = (((ptag1 & 0x1F) == 0x1F) && pi+1 < sel_rlen) ? sel_resp[pi+1] : 0;
|
||||
uint16_t ftag = ((ptag1 & 0x1F) == 0x1F) ? (((uint16_t)ptag1<<8)|ptag2) : ptag1;
|
||||
uint8_t flen_off = (((ptag1 & 0x1F) == 0x1F)) ? 2 : 1;
|
||||
if (pi + flen_off >= sel_rlen) break;
|
||||
uint8_t flen = sel_resp[pi + flen_off];
|
||||
if (ftag == 0x9F38) {
|
||||
/* Sum DOL field lengths to get total PDOL data size */
|
||||
uint8_t di = pi + flen_off + 1;
|
||||
uint8_t dend = di + flen;
|
||||
while (di < dend && di + 1 < sel_rlen) {
|
||||
uint8_t dol_tl = ((sel_resp[di] & 0x1F) == 0x1F) ? 2 : 1;
|
||||
if (di + dol_tl >= sel_rlen) break;
|
||||
pdol_len += sel_resp[di + dol_tl];
|
||||
di += dol_tl + 1;
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (flen_off + flen < 255) pi += flen_off + flen - 1; else break;
|
||||
}
|
||||
/* ---- Step 5: GPO with PDOL retry --------------------------------
|
||||
* When the FCI is truncated (45b for long cards), we can't read the
|
||||
* full PDOL. Try progressively larger PDOL sizes (all zeros) until the
|
||||
* card accepts. pdol_len from FCI parsing is tried first. Common sizes
|
||||
* cover most Mastercard/Visa variants. */
|
||||
static const uint8_t gpo_try_pl[] = {0, 4, 8, 12, 18, 22, 26, 29, 34, 38, 44};
|
||||
uint8_t gpo_buf[8 + 44]; /* PCB(1)+header(7)+max_PDOL(44)+Le(1)+CRC(2) fits abuf[64] */
|
||||
uint8_t gpo_len = 0;
|
||||
uint8_t *gpo_resp = NULL; uint16_t gpo_rlen = 0;
|
||||
if (pdol_len > 44) pdol_len = 0; /* clamp to abuf-safe size */
|
||||
{
|
||||
bool gpo_ok = false;
|
||||
for (uint8_t ti = 0; ti <= sizeof(gpo_try_pl) && !gpo_ok; ti++) {
|
||||
uint8_t pl = (ti == 0) ? pdol_len
|
||||
: gpo_try_pl[ti - 1];
|
||||
/* skip sizes we already tried */
|
||||
bool dup = false;
|
||||
for (uint8_t si = 0; si < ti && !dup; si++)
|
||||
dup = ((si == 0 ? pdol_len : gpo_try_pl[si-1]) == pl);
|
||||
if (dup) continue;
|
||||
gpo_len = 0;
|
||||
gpo_buf[gpo_len++]=0x80; gpo_buf[gpo_len++]=0xA8;
|
||||
gpo_buf[gpo_len++]=0x00; gpo_buf[gpo_len++]=0x00;
|
||||
gpo_buf[gpo_len++]=(uint8_t)(pl + 2); /* Lc */
|
||||
gpo_buf[gpo_len++]=0x83;
|
||||
gpo_buf[gpo_len++]=pl;
|
||||
memset(&gpo_buf[gpo_len], 0x00, pl); gpo_len += pl;
|
||||
gpo_buf[gpo_len++]=0x00; /* Le */
|
||||
if (!SEND_APDU(gpo_buf, gpo_len, &gpo_resp, &gpo_rlen)) continue;
|
||||
/* Accept if response template (0x77/0x80) or SW 9000 */
|
||||
if (gpo_rlen >= 2) {
|
||||
uint8_t s1 = gpo_resp[gpo_rlen-2];
|
||||
uint8_t s2 = gpo_resp[gpo_rlen-1];
|
||||
if (gpo_resp[0] == 0x77 || gpo_resp[0] == 0x80 ||
|
||||
(s1 == 0x90 && s2 == 0x00))
|
||||
gpo_ok = true;
|
||||
}
|
||||
}
|
||||
if (!gpo_ok) goto done;
|
||||
}
|
||||
APPEND_PAIR(gpo_buf, gpo_len, gpo_resp, gpo_rlen);
|
||||
num_apdus++;
|
||||
|
||||
/* ---- Step 5: parse AFL and READ RECORDs --------------------- */
|
||||
/* Find AFL in GPO response (tag 0x94 in format 2, or bytes 3+ in format 1) */
|
||||
uint8_t *afl = NULL; uint8_t afl_len = 0;
|
||||
if (gpo_rlen > 0 && gpo_resp[0] == 0x77) {
|
||||
/* Format 2: search for tag 94 */
|
||||
/* Format 2: find tag 94 */
|
||||
for (uint8_t i = 2; i + 1 < gpo_rlen; ) {
|
||||
uint8_t t = gpo_resp[i]; uint8_t l = gpo_resp[i+1];
|
||||
if (t == 0x94) { afl = &gpo_resp[i+2]; afl_len = l; break; }
|
||||
i += 2 + l;
|
||||
}
|
||||
} else if (gpo_rlen > 3 && gpo_resp[0] == 0x80) {
|
||||
/* Format 1: skip tag(1)+len(1)+AIP(2) */
|
||||
afl = &gpo_resp[3]; afl_len = gpo_rlen - 3 - 2; /* -2 for SW */
|
||||
afl = &gpo_resp[3]; afl_len = gpo_rlen - 3 - 2;
|
||||
}
|
||||
if (afl == NULL || afl_len == 0) goto done;
|
||||
|
||||
/* Copy AFL to local buffer before READ RECORDs.
|
||||
* afl points into chain_buf which is overwritten by each SEND_APDU call.
|
||||
* Without this copy, the 2nd+ AFL entries become garbage after the first
|
||||
* READ RECORD, causing last=0xFF and up to 255 timeout loops per entry. */
|
||||
static uint8_t afl_buf[32]; /* max 8 AFL entries × 4 bytes */
|
||||
if (afl_len > sizeof(afl_buf)) afl_len = (uint8_t)sizeof(afl_buf);
|
||||
memcpy(afl_buf, afl, afl_len);
|
||||
afl = afl_buf;
|
||||
|
||||
/* READ each record */
|
||||
for (uint8_t a = 0; a + 3 < afl_len; a += 4) {
|
||||
|
||||
@@ -105,8 +105,8 @@ def check_tools():
|
||||
|
||||
if missing_tools:
|
||||
missing_tool_str = ", ".join(missing_tools)
|
||||
warn_str = f"Warning, {missing_tool_str} not found. Corresponding commands will not work as intended."
|
||||
print(color_string((CR, warn_str)))
|
||||
warn_str = f"Note: optional Mifare tools not found: {missing_tool_str}. Mifare attack commands will not work."
|
||||
print(color_string((CY, warn_str)))
|
||||
|
||||
|
||||
class BaseCLIUnit:
|
||||
@@ -5556,242 +5556,7 @@ class HFMFUEDetect(SlotIndexArgsAndGoUnit, DeviceRequiredUnit):
|
||||
print(f"{actual_index:3d}: {color_string((CY, password.upper()))}")
|
||||
|
||||
|
||||
@hf_mfu.command('nfcimport')
|
||||
class HFMFUNfcImport(SlotIndexArgsAndGoUnit, DeviceRequiredUnit):
|
||||
# Mapping from Flipper Zero device type strings to CU TagSpecificType
|
||||
FLIPPER_TYPE_MAP = {
|
||||
'NTAG203': TagSpecificType.NTAG_215, # best-effort: no native NTAG203 support
|
||||
'NTAG210': TagSpecificType.NTAG_210,
|
||||
'NTAG212': TagSpecificType.NTAG_212,
|
||||
'NTAG213': TagSpecificType.NTAG_213,
|
||||
'NTAG215': TagSpecificType.NTAG_215,
|
||||
'NTAG216': TagSpecificType.NTAG_216,
|
||||
'NTAGI2C1K': TagSpecificType.NTAG_216, # best-effort
|
||||
'NTAGI2C2K': TagSpecificType.NTAG_216, # best-effort
|
||||
'NTAGI2CPlus1K': TagSpecificType.NTAG_216, # best-effort
|
||||
'NTAGI2CPlus2K': TagSpecificType.NTAG_216, # best-effort
|
||||
'Mifare Ultralight': TagSpecificType.MF0ICU1,
|
||||
'Mifare Ultralight C': TagSpecificType.MF0ICU2,
|
||||
'Mifare Ultralight 11': TagSpecificType.MF0UL11,
|
||||
'Mifare Ultralight 21': TagSpecificType.MF0UL21,
|
||||
# "Mifare Ultralight EV1" is disambiguated by page count in on_exec
|
||||
}
|
||||
|
||||
def args_parser(self) -> ArgumentParserNoExit:
|
||||
parser = ArgumentParserNoExit()
|
||||
parser.description = 'Import a Flipper Zero .nfc file into a MIFARE Ultralight / NTAG emulator slot'
|
||||
self.add_slot_args(parser)
|
||||
parser.add_argument('-f', '--file', required=True, type=str, help="Path to Flipper Zero .nfc file")
|
||||
parser.add_argument('--amiibo', action='store_true', default=False,
|
||||
help="Derive and write correct PWD/PACK for amiibo (NTAG215)")
|
||||
return parser
|
||||
|
||||
def on_exec(self, args: argparse.Namespace):
|
||||
file_path = args.file
|
||||
file_name = os.path.basename(file_path)
|
||||
|
||||
# --- Parse the .nfc file ---
|
||||
try:
|
||||
with open(file_path, 'r') as f:
|
||||
lines = f.readlines()
|
||||
except FileNotFoundError:
|
||||
print(color_string((CR, f"File not found: {file_path}")))
|
||||
return
|
||||
except OSError as e:
|
||||
print(color_string((CR, f"Error reading file: {e}")))
|
||||
return
|
||||
|
||||
device_type = None
|
||||
uid = None
|
||||
atqa = None
|
||||
sak = None
|
||||
signature = None
|
||||
version = None
|
||||
counters = {}
|
||||
tearing = {}
|
||||
pages_total = None
|
||||
pages = {}
|
||||
|
||||
for line in lines:
|
||||
line = line.strip()
|
||||
if line.startswith('#') or not line:
|
||||
continue
|
||||
|
||||
if line.startswith('Device type:'):
|
||||
device_type = line.split(':', 1)[1].strip()
|
||||
elif line.startswith('UID:'):
|
||||
uid = bytes.fromhex(line.split(':', 1)[1].strip().replace(' ', ''))
|
||||
elif line.startswith('ATQA:'):
|
||||
atqa = bytes.fromhex(line.split(':', 1)[1].strip().replace(' ', ''))
|
||||
elif line.startswith('SAK:'):
|
||||
sak = bytes.fromhex(line.split(':', 1)[1].strip().replace(' ', ''))
|
||||
elif line.startswith('Signature:'):
|
||||
signature = bytes.fromhex(line.split(':', 1)[1].strip().replace(' ', ''))
|
||||
elif line.startswith('Mifare version:'):
|
||||
version = bytes.fromhex(line.split(':', 1)[1].strip().replace(' ', ''))
|
||||
elif line.startswith('Counter '):
|
||||
match = re.match(r'Counter\s+(\d+):\s+(\d+)', line)
|
||||
if match:
|
||||
counters[int(match.group(1))] = int(match.group(2))
|
||||
elif line.startswith('Tearing '):
|
||||
match = re.match(r'Tearing\s+(\d+):\s+([0-9A-Fa-f]+)', line)
|
||||
if match:
|
||||
tearing[int(match.group(1))] = int(match.group(2), 16)
|
||||
elif line.startswith('Pages total:'):
|
||||
pages_total = int(line.split(':', 1)[1].strip())
|
||||
elif line.startswith('Page '):
|
||||
match = re.match(r'Page\s+(\d+):\s+(.*)', line)
|
||||
if match:
|
||||
page_num = int(match.group(1))
|
||||
page_data = bytes.fromhex(match.group(2).strip().replace(' ', ''))
|
||||
pages[page_num] = page_data
|
||||
|
||||
# --- Validate required fields ---
|
||||
if device_type is None:
|
||||
print(color_string((CR, "No 'Device type' found in .nfc file.")))
|
||||
return
|
||||
if uid is None:
|
||||
print(color_string((CR, "No 'UID' found in .nfc file.")))
|
||||
return
|
||||
if atqa is None:
|
||||
print(color_string((CR, "No 'ATQA' found in .nfc file.")))
|
||||
return
|
||||
if sak is None:
|
||||
print(color_string((CR, "No 'SAK' found in .nfc file.")))
|
||||
return
|
||||
|
||||
# --- Map device type to TagSpecificType ---
|
||||
tag_type = self.FLIPPER_TYPE_MAP.get(device_type)
|
||||
|
||||
if tag_type is None and device_type.startswith('Mifare Ultralight EV1'):
|
||||
# Disambiguate EV1 by page count
|
||||
nr = pages_total if pages_total else len(pages)
|
||||
tag_type = TagSpecificType.MF0UL11 if nr <= 20 else TagSpecificType.MF0UL21
|
||||
|
||||
if tag_type is None:
|
||||
print(color_string((CR, f"Unsupported Flipper device type: '{device_type}'")))
|
||||
print(f" Supported types: {', '.join(sorted(self.FLIPPER_TYPE_MAP.keys()))}, Mifare Ultralight EV1")
|
||||
return
|
||||
|
||||
# --- Print summary ---
|
||||
print(f"Importing Flipper NFC file: {file_name}")
|
||||
print(f" Device type: {device_type} -> {tag_type}")
|
||||
print(f" UID: {uid.hex(' ').upper()}")
|
||||
print(f" ATQA: {atqa.hex(' ').upper()} SAK: {sak.hex().upper()}")
|
||||
if version:
|
||||
print(f" Version: {version.hex(' ').upper()}")
|
||||
if signature:
|
||||
print(f" Signature: {signature.hex(' ').upper()}")
|
||||
if counters:
|
||||
print(f" Counters: {', '.join(str(counters.get(i, 0)) for i in range(max(counters.keys()) + 1))}")
|
||||
nr_pages = pages_total if pages_total else len(pages)
|
||||
print(f" Pages: {nr_pages}")
|
||||
print()
|
||||
|
||||
# --- Step 1: Set slot tag type ---
|
||||
print(f"Setting slot {self.slot_num} tag type to {tag_type}...")
|
||||
self.cmd.set_slot_tag_type(self.slot_num, tag_type)
|
||||
self.cmd.set_slot_data_default(self.slot_num, tag_type)
|
||||
# Must re-activate slot after changing type so subsequent commands target the new type
|
||||
self.cmd.set_active_slot(self.slot_num)
|
||||
|
||||
# --- Step 2: Set anti-collision data ---
|
||||
print("Setting anti-collision data...")
|
||||
self.cmd.hf14a_set_anti_coll_data(uid, atqa, sak)
|
||||
|
||||
# --- Step 3: Set version data ---
|
||||
if version and len(version) == 8:
|
||||
print("Setting version data...")
|
||||
try:
|
||||
self.cmd.mf0_ntag_set_version_data(version)
|
||||
except (ValueError, chameleon_com.CMDInvalidException, TimeoutError):
|
||||
print(color_string((CY, " Warning: tag type does not support GET_VERSION.")))
|
||||
|
||||
# --- Step 4: Set signature data ---
|
||||
if signature and len(signature) == 32:
|
||||
print("Setting signature data...")
|
||||
try:
|
||||
self.cmd.mf0_ntag_set_signature_data(signature)
|
||||
except (ValueError, chameleon_com.CMDInvalidException, TimeoutError):
|
||||
print(color_string((CY, " Warning: tag type does not support READ_SIG.")))
|
||||
|
||||
# --- Step 5: Set counter and tearing data ---
|
||||
if counters:
|
||||
print("Setting counter data...")
|
||||
# NTAG types have a single counter accessed via NFC at index 2,
|
||||
# but stored at firmware internal index 0
|
||||
ntag_types = {
|
||||
TagSpecificType.NTAG_210, TagSpecificType.NTAG_212,
|
||||
TagSpecificType.NTAG_213, TagSpecificType.NTAG_215,
|
||||
TagSpecificType.NTAG_216,
|
||||
}
|
||||
for i in sorted(counters.keys()):
|
||||
value = counters[i]
|
||||
if value > 0xFFFFFF:
|
||||
print(color_string((CY, f" Warning: counter {i} value {value:#x} exceeds 24-bit, skipping.")))
|
||||
continue
|
||||
# Map Flipper counter index to firmware internal index
|
||||
if tag_type in ntag_types:
|
||||
if i != 2:
|
||||
continue # NTAG only has counter at NFC index 2
|
||||
fw_index = 0
|
||||
else:
|
||||
fw_index = i
|
||||
# Reset tearing flag if tearing byte is BD (default / no tearing)
|
||||
tearing_val = tearing.get(i, 0x00)
|
||||
reset_tearing = (tearing_val == 0xBD or tearing_val == 0x00)
|
||||
try:
|
||||
self.cmd.mfu_write_emu_counter_data(fw_index, value, reset_tearing)
|
||||
except (ValueError, chameleon_com.CMDInvalidException, UnexpectedResponseError, TimeoutError):
|
||||
print(color_string((CY, f" Warning: could not set counter {i}.")))
|
||||
|
||||
# --- Step 6: Write page data ---
|
||||
if pages:
|
||||
# Get total pages for the configured slot
|
||||
slot_pages = self.cmd.mfu_get_emu_pages_count()
|
||||
|
||||
# Build contiguous data from parsed pages
|
||||
max_page = max(pages.keys())
|
||||
write_pages = min(max_page + 1, slot_pages)
|
||||
|
||||
print(f"Writing {write_pages} pages...", end=' ', flush=True)
|
||||
|
||||
page = 0
|
||||
while page < write_pages:
|
||||
cur_count = min(16, write_pages - page)
|
||||
batch = bytearray()
|
||||
for p in range(page, page + cur_count):
|
||||
batch.extend(pages.get(p, b'\x00\x00\x00\x00'))
|
||||
self.cmd.mfu_write_emu_page_data(page, bytes(batch))
|
||||
page += cur_count
|
||||
|
||||
print("done")
|
||||
|
||||
# --- Step 7: Derive and write amiibo PWD/PACK ---
|
||||
if args.amiibo:
|
||||
if tag_type != TagSpecificType.NTAG_215:
|
||||
print(color_string((CY, f" Warning: --amiibo flag ignored (tag type is {tag_type}, not NTAG 215).")))
|
||||
elif uid is None or len(uid) != 7:
|
||||
print(color_string((CY, " Warning: --amiibo flag ignored (UID is not 7 bytes).")))
|
||||
else:
|
||||
pwd = bytes([
|
||||
0xAA ^ uid[1] ^ uid[3],
|
||||
0x55 ^ uid[2] ^ uid[4],
|
||||
0xAA ^ uid[3] ^ uid[5],
|
||||
0x55 ^ uid[4] ^ uid[6],
|
||||
])
|
||||
pack = bytes([0x80, 0x80, 0x00, 0x00])
|
||||
print(f"Setting amiibo PWD: {pwd.hex(' ').upper()}, PACK: {pack[:2].hex(' ').upper()}...")
|
||||
self.cmd.mfu_write_emu_page_data(133, pwd)
|
||||
self.cmd.mfu_write_emu_page_data(134, pack)
|
||||
|
||||
self.cmd.set_slot_enable(self.slot_num, TagSenseType.HF, True)
|
||||
|
||||
print()
|
||||
print(f" - Import complete. Slot {self.slot_num} is now emulating {device_type} ({file_name})")
|
||||
|
||||
|
||||
@lf_em_410x.command('read')
|
||||
@lf_em_410x.command("read")
|
||||
class LFEMRead(ReaderRequiredUnit):
|
||||
def args_parser(self) -> ArgumentParserNoExit:
|
||||
parser = ArgumentParserNoExit()
|
||||
@@ -6244,20 +6009,18 @@ class LFVikingWriteT55xx(LFVikingIdArgsUnit, ReaderRequiredUnit):
|
||||
print(f" - Viking ID(8H): {id_hex} write done.")
|
||||
|
||||
|
||||
@lf.command("clone")
|
||||
@lf_t55xx.command("clone")
|
||||
class LFT55xxClone(ReaderRequiredUnit):
|
||||
"""
|
||||
Clone a LF card ID onto a blank T55xx tag.
|
||||
|
||||
Usage: lf clone -t <type> [args]
|
||||
Clone a scanned or manually-specified LF card ID onto a blank T55xx tag.
|
||||
|
||||
Supported types and their required arguments:
|
||||
|
||||
em410x --id <10 hex> e.g. lf clone -t em410x --id DEADBEEF88
|
||||
electra --id <26 hex> e.g. lf clone -t electra --id DEADBEEF880102030405060708
|
||||
hid -f <format> --cn <n> e.g. lf clone -t hid -f H10301 --fc 10 --cn 1234
|
||||
em410x --id <10 hex> e.g. --id DEADBEEF88
|
||||
electra --id <26 hex> e.g. --id DEADBEEF880102030405060708
|
||||
hid -f <format> --cn <n> e.g. -f H10301 --fc 10 --cn 1234
|
||||
ioprox --ver <n> --fc <n> --cn <n> OR --raw8 <16 hex>
|
||||
viking --id <8 hex> e.g. lf clone -t viking --id DEADBEEF
|
||||
viking --id <8 hex> e.g. --id DEADBEEF
|
||||
|
||||
Only supported on Chameleon Ultra (Lite has no LF writer).
|
||||
"""
|
||||
@@ -6268,7 +6031,6 @@ class LFT55xxClone(ReaderRequiredUnit):
|
||||
parser = ArgumentParserNoExit()
|
||||
parser.description = (
|
||||
"Clone a LF card ID onto a blank T55xx tag.\n"
|
||||
"Usage: lf clone -t <type> [args]\n"
|
||||
"Supported types: em410x, electra, hid, ioprox, viking.\n"
|
||||
"Only supported on Chameleon Ultra (Lite has no LF writer)."
|
||||
)
|
||||
@@ -6388,7 +6150,7 @@ class LFT55xxClone(ReaderRequiredUnit):
|
||||
|
||||
elif t == "ioprox":
|
||||
ver = args.ver if args.ver is not None else 1
|
||||
fc = args.fc if args.fc is not None else 0
|
||||
fc = int(args.fc, 0) if args.fc is not None else 0
|
||||
cn = args.cn if args.cn is not None else 0
|
||||
if args.raw8 is not None:
|
||||
raw8 = LFIOProxIdArgsUnit.parse_raw8(args.raw8)
|
||||
@@ -7552,12 +7314,16 @@ class HF14ASniff(BaseCLIUnit):
|
||||
return
|
||||
|
||||
# Parse packed frame buffer: [2 bytes bits BE][N bytes data] ...
|
||||
# Bit 15 of szBits: 0 = reader→card, 1 = card→reader (new firmware).
|
||||
# Old firmware always sends bit15=0; parser is backward compatible.
|
||||
buf = bytes(resp.data)
|
||||
frames = []
|
||||
frames = [] # (szBits, data, is_tx)
|
||||
i = 0
|
||||
while i + 2 <= len(buf):
|
||||
szBits = (buf[i] << 8) | buf[i+1]
|
||||
hdr = (buf[i] << 8) | buf[i+1]
|
||||
i += 2
|
||||
is_tx = bool(hdr & 0x8000)
|
||||
szBits = hdr & 0x7FFF
|
||||
if szBits == 0:
|
||||
break
|
||||
szBytes = (szBits + 7) // 8
|
||||
@@ -7586,29 +7352,83 @@ class HF14ASniff(BaseCLIUnit):
|
||||
else:
|
||||
data = raw
|
||||
|
||||
frames.append((szBits, data))
|
||||
frames.append((szBits, data, is_tx))
|
||||
|
||||
if not frames:
|
||||
print(f"{CR}No frames decoded{C0}")
|
||||
return
|
||||
|
||||
print(f" Captured : {CG}{len(frames)}{C0} frame(s)")
|
||||
rx_count = sum(1 for _, _, tx in frames if not tx)
|
||||
tx_count = sum(1 for _, _, tx in frames if tx)
|
||||
if tx_count > 0:
|
||||
print(f" Captured : {CG}{len(frames)}{C0} frame(s) "
|
||||
f"({CY}{rx_count}{C0} reader→card {CG}{tx_count}{C0} card→reader)")
|
||||
else:
|
||||
print(f" Captured : {CG}{len(frames)}{C0} frame(s) "
|
||||
f"{CY}(reader→card only — reflash for both directions){C0}")
|
||||
print()
|
||||
print(f" {'#':>3} {'bits':>4} {'hex data':<42} decoded")
|
||||
print(f" {'---':>3} {'----':>4} {'-'*42} {'-'*35}")
|
||||
print(f" {'#':>3} {'dir':<3} {'bits':>4} {'hex data':<42} decoded")
|
||||
print(f" {'---':>3} {'---':<3} {'----':>4} {'-'*42} {'-'*35}")
|
||||
|
||||
for n, (szBits, data) in enumerate(frames):
|
||||
hex_str = ' '.join(f'{b:02x}' for b in data)
|
||||
decoded, col = _decode_14a_frame_col(data, szBits)
|
||||
print(f" {CY}{n+1:>3}{C0} {szBits:>4} {hex_str:<42} {col}{decoded}{C0}")
|
||||
for n, (szBits, data, is_tx) in enumerate(frames):
|
||||
hex_str = ' '.join(f'{b:02x}' for b in data)
|
||||
decoded, col = _decode_14a_frame_col(data, szBits)
|
||||
dir_str = f'{CG}<<<{C0}' if is_tx else f'{CY}>>>{C0}'
|
||||
print(f" {CY}{n+1:>3}{C0} {dir_str} {szBits:>4} {hex_str:<42} {col}{decoded}{C0}")
|
||||
|
||||
# Summary block
|
||||
# Summary block (pass only reader→card frames for protocol decode)
|
||||
print()
|
||||
_print_14a_sniff_summary(frames)
|
||||
_print_14a_sniff_summary([(s, d) for s, d, tx in frames if not tx])
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def _decode_sw(sw1: int, sw2: int) -> str:
|
||||
"""Decode an ISO 7816-4 status word pair."""
|
||||
exact = {
|
||||
0x9000: 'OK',
|
||||
0x6100: 'Response bytes available',
|
||||
0x6283: 'File deactivated',
|
||||
0x6300: 'Auth failed',
|
||||
0x6400: 'No changes',
|
||||
0x6581: 'Memory failure',
|
||||
0x6700: 'Wrong length',
|
||||
0x6881: 'Logical channel not supported',
|
||||
0x6882: 'Secure messaging not supported',
|
||||
0x6900: 'Command not allowed',
|
||||
0x6981: 'Command incompatible with file structure',
|
||||
0x6982: 'Security status not satisfied',
|
||||
0x6983: 'Auth method blocked',
|
||||
0x6984: 'Referenced data invalidated',
|
||||
0x6985: 'Conditions of use not satisfied',
|
||||
0x6986: 'Command not allowed — no EF selected',
|
||||
0x6A00: 'Wrong parameters P1-P2',
|
||||
0x6A80: 'Incorrect data in command',
|
||||
0x6A81: 'Function not supported',
|
||||
0x6A82: 'File not found',
|
||||
0x6A83: 'Record not found',
|
||||
0x6A84: 'Not enough memory',
|
||||
0x6A85: 'Lc inconsistent with TLV',
|
||||
0x6A86: 'Incorrect parameters P1-P2',
|
||||
0x6A87: 'Lc inconsistent with P1-P2',
|
||||
0x6A88: 'Referenced data not found',
|
||||
0x6B00: 'Wrong parameters P1-P2',
|
||||
0x6D00: 'Instruction not supported',
|
||||
0x6E00: 'Class not supported',
|
||||
0x6F00: 'Unknown error',
|
||||
}
|
||||
key = (sw1 << 8) | sw2
|
||||
if key in exact:
|
||||
return exact[key]
|
||||
if sw1 == 0x61: return f'Response bytes available: {sw2}'
|
||||
if sw1 == 0x62: return f'Warning — no info change: {sw2:02X}'
|
||||
if sw1 == 0x63: return f'Warning — state changed: {sw2:02X}'
|
||||
if sw1 == 0x6C: return f'Wrong Le — use {sw2}'
|
||||
if sw1 == 0x90: return 'OK'
|
||||
if sw1 == 0x91: return 'Proprietary OK'
|
||||
return ''
|
||||
|
||||
def _decode_14a_frame_col(data: bytes, szBits: int):
|
||||
"""Return (description, colour) for a 14A frame."""
|
||||
if not data:
|
||||
@@ -7731,6 +7551,18 @@ def _decode_14a_frame_col(data: bytes, szBits: int):
|
||||
return 'MANAGE CHANNEL', CC
|
||||
return f'APDU CLA={cla:02x} INS={ins:02x} P1={p1:02x} P2={p2:02x}', CY
|
||||
|
||||
# ISO 7816-4 status word — scan last 2 bytes (and last 4 if CRC present)
|
||||
sw_label = ''
|
||||
for sw_offset in (-2, -4):
|
||||
if len(data) >= abs(sw_offset):
|
||||
s1, s2 = data[sw_offset], data[sw_offset + 1]
|
||||
lbl = _decode_sw(s1, s2)
|
||||
if lbl:
|
||||
sw_label = f'SW {s1:02X} {s2:02X} {lbl}'
|
||||
break
|
||||
if sw_label:
|
||||
return sw_label, CY
|
||||
|
||||
# Unknown — show first byte
|
||||
return f'unknown (0x{b0:02x})', CC
|
||||
|
||||
@@ -8633,11 +8465,13 @@ class EMVScan(DeviceRequiredUnit):
|
||||
print(f'')
|
||||
print(f' {CG}── Card Details ──────────────────────{C0}')
|
||||
|
||||
# App label
|
||||
for v in tags.get(0x50, []):
|
||||
# App label — show first unique label only
|
||||
seen_labels = set()
|
||||
for v in tags.get(0x50, []) + app_tags.get(0x50, []):
|
||||
try:
|
||||
lbl = v.decode('ascii', errors='replace').strip()
|
||||
if lbl:
|
||||
if lbl and lbl not in seen_labels:
|
||||
seen_labels.add(lbl)
|
||||
print(f' {CG}App Label :{C0} {CY}{lbl}{C0}')
|
||||
except Exception:
|
||||
pass
|
||||
@@ -8706,15 +8540,12 @@ class EMVScan(DeviceRequiredUnit):
|
||||
print(f' {CG}Issuer Country:{C0} {CY}{country}{C0}')
|
||||
result.setdefault('Decoded', {})['IssuerCountry'] = country
|
||||
|
||||
# Application Preferred Name / Label
|
||||
# Application Preferred Name (9F12) — only if different from label
|
||||
for v in tags[0x9F12]:
|
||||
try:
|
||||
print(f' {CG}App Name :{C0} {CY}{v.decode("ascii", errors="replace").strip()}{C0}')
|
||||
except Exception:
|
||||
pass
|
||||
for v in tags[0x50]:
|
||||
try:
|
||||
print(f' {CG}App Label :{C0} {CY}{v.decode("ascii", errors="replace").strip()}{C0}')
|
||||
name = v.decode('ascii', errors='replace').strip()
|
||||
if name and name not in seen_labels:
|
||||
print(f' {CG}App Name :{C0} {CY}{name}{C0}')
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@@ -9069,4 +8900,4 @@ class EMVApdu(DeviceRequiredUnit):
|
||||
print(f' {CR}Error sending response: {e}{C0}')
|
||||
break
|
||||
|
||||
print(f'\n {C0}Relay ended. {exchange_count} APDU exchange(s) completed.{C0}')
|
||||
print(f'\n {C0}Relay ended. {exchange_count} APDU exchange(s) completed.{C0}')
|
||||
Reference in New Issue
Block a user