Skip to content

RFID/NFC

RFID/NFC analysis covers the contactless card world: the frequency classes (LF 125 kHz proximity, HF 13.56 MHz — the ISO 14443/15693/NFC band — and UHF 860-960 MHz EPC inventory), the credential technologies (EM4100’s unauthenticated UID, Mifare Classic’s broken Crypto1, Mifare Plus/DESFire’s evolving AES-based security, NTAG-class NFC forum tags), and the physical-access and payment systems built on them. The analysis craft spans card-level forensics (reading and interpreting card dumps — sector trailers, access bits, value blocks), protocol-level assessment (what the reader-card exchange actually authenticates), and the attack families that track the technology generations: UID cloning (copy the serial, the reader’s only check), Crypto1 cryptanalysis (the 2008-2015 research line that broke Mifare Classic completely — nested, darkside, and hardnested attacks), relay attacks (real-time card-to-reader tunneling that defeats distance entirely, applicable to payment and badges alike), and the NFC data layer (the tags and handoff mechanisms phones carry). The defensive craft is the inventory conversation: which credential generations does your estate actually issue, what does each withstand, and what does the reader log when a cloned badge walks through the door.

Physical access is the other perimeter, and it runs on the same forgetting-curve as every wireless generation: EM4100 badges (UID-only, trivially clonable with a $20 writer) still open buildings; Mifare Classic (Crypto1, cryptanalyzed to death) still guards server rooms; and the reader logs that would catch cloned cards are often configured to log nothing. The consequences chain directly into the digital estate: a cloned badge is physical presence — desk access, badge-in records, doors to wiring closets — and physical presence defeats nearly every network control. The payment-NFC layer adds consumer scale: relay attacks against contactless payment and transit cards are demonstrated research-to-practice territory [verify current mitigations], and the same relay principle defeats badge systems that authenticate the card but not the distance. For defenders, this module’s inventory discipline is the whole game: badge-technology census (which generation per door population), reader logging (UID events, rejection events — the telemetry that catches clones), and the migration path (DESFire EV3-class credentials, reader upgrades) prioritized by which doors matter. For assessors, the craft is non-destructive by design: reading and analyzing dumps on authorized cards, testing clone-acceptance on owned readers, and demonstrating the relay architecture in the lab — the physical-access equivalent of the T-series’ benign-payload discipline, where the finding is the reader’s acceptance behavior, never someone else’s door.

The card generations and their security models:

RFID/NFC CREDENTIAL GENERATIONS
[LF 125kHz — EM4100 class]
UID broadcast, NO authentication at all
reader checks: "is this UID on the list?"
attack: read UID, write to clonable card. Done.
[HF 13.56MHz — ISO 14443-A]
anticollision: UID + ATQA/SAK bytes (tech identification)
Mifare Classic (1K/4K): Crypto1 proprietary cipher
- 48-bit keys, broken by 2008-2015 research line:
darkside (weak PRNG), nested (key recovery from
known key), hardnested (optimized) [verify tooling]
- keys per sector; dumps contain data + access bits
Mifare Plus: AES, SL1 (Classic-compat) vs SL3 (AES)
DESFire EV1/EV2/EV3: AES, secure messaging, per-file
keys, counter-protected — the modern badge standard
NTAG/MIFARE Ultralight: NFC forum tags, small data
[RELAY ATTACK — defeats distance, not crypto]
victim card <--link--> attacker <--link--> reader
real-time tunneling; card never leaves the pocket
counter: relay-distance limits (EV2/EV3 proximity
check [verify]), interference, policy
READER-SIDE DEFENSE MODEL
badge inventory per door population
reader logs: UID, timestamps, rejections
UID-duplication + unknown-UID alerts
migration: Classic -> DESFire EV3-class

The dump analysis is the card-forensics core: a Mifare Classic dump is 64 blocks (1K) of 16 bytes, where every 4th block is a sector trailer — key A (6 bytes), access bits (4), key B (6) — and the access bits encode who may read/write what; the analysis reads that structure, checks for default keys (FFFFFFFFFFFF and friends — the “no one ever changed the key” finding), validates access-bit sanity, and identifies value blocks (the int-with-backup format that stored credits used). The Crypto1 story is the case study in proprietary-crypto failure: reverse-engineered in 2008, the 48-bit key and weak PRNG fell to successive attacks (darkside needs one vulnerability window, nested recovers all keys from one known key, hardnested made it practical), and every Classic badge in an estate is effectively plaintext — the analysis question is only which attack path the assessor documents. The DESFire lineage is the migration target: EV1 fixed Classic’s lessons with AES, EV2 added transaction counters and proximity checks, EV3 continued hardening [verify current features] — and the audit distinguishes “DESFire” (EV0 exists too!) from the specific EV generation, because procurement bought “DESFire” badges of every vintage. The relay architecture is the honest boundary-push: it defeats distance using two legitimate links (card-to-attacker, attacker-to-reader), so the counter is not crypto but protocol-level proximity checking (EV2+ timing) and policy (transaction limits on payment, anti-passback on doors). The reader-side defense model closes the module: the reader that logs every presented UID (including rejections) is the sensor that makes clone detection possible — the badge system’s Sysmon, and the telemetry most estates never enabled.

TechniqueTargetWhat it doesToolingNotes
UID read + cloneEM4100/UID-changeableCopy serial to new cardPM3/proxmark-class [verify]Defeats UID-only systems
Darkside attackMifare ClassicKey recovery via weak PRNGProxmark/mfocOne vulnerable reader session
Nested attackClassic w/ 1 known keyRecover all sector keysmfcuk/mfoc classEstate-wide compromise
Dump analysisClassic dumpsKeys, access bits, dataThis module’s toolForensics layer
DESFire auditEV1/EV2/EV3Generation + key posturePM3/reader logsMigration census
Relay attack (lab)Badge/payment systemsDistance-defeating tunnel2 readers + linkAuthorized demo only
NFC tag analysisNTAG/forum tagsNDEF payload reviewPhone/micro:bit classData-layer
UHF inventoryEPC Gen2Long-range tag censusUHF readersLogistics layer
Reader behavior testOwned readersClone-acceptance, loggingCloned + real cardsThe empirical finding
Default-key censusClassic dumpsFFFFFFFF-style keysThis module’s toolThe eternal finding

mfclassic.py — a Mifare Classic dump analyzer: parses raw dump binaries (1K = 1024 bytes, 4K = 4096 — the Proxmark/mfoc export shape), walks the 16-block sectors, decodes sector trailers (key A/B, access bits), validates access-bit legality (the access-bits format has invalid combinations that indicate corruption or tampering), flags default keys, identifies value blocks, and reports per-sector posture. stdlib only.

#!/usr/bin/env python3
"""mfclassic.py — Mifare Classic dump analyzer.
Usage:
python mfclassic.py dump.bin [--json report.json]
Dump shape: raw 1024 bytes (1K) or 4096 (4K), 16-byte blocks.
stdlib only.
"""
import argparse, json, sys
DEFAULT_KEYS = {
"ffffffffffff": "default/blank",
"a0a1a2a3a4a5": "MAD key A",
"d3f7d3f7d3f7": "NDEF public",
"000000000000": "zeroed",
"aabbccddeeff": "common default",
}
def sector_count(size):
if size == 1024:
return 16
if size == 4096:
return 40
if size == 512:
return 8 # Mini
sys.exit(f"unexpected dump size {size} "
f"(want 1024/2048/4096)")
def decode_access(access_bytes):
"""Decode 3 access bits + GPB from trailer bytes 6-9."""
b6, b7, b8 = access_bytes[0], access_bytes[1], \
access_bytes[2]
# standard inversion check: c23' = ~c23 etc.
inv_ok = ((b6 ^ 0xFF) == b7 & 0xF0 and
(b7 & 0x0F) == ((b8 >> 4) & 0x0F))
c1 = ((b7 & 0x80) >> 7) | ((b8 & 0x01) << 1) | \
((b8 & 0x10) >> 3) | ((b8 & 0x40) >> 5) << 0
# simpler per-block extraction (standard mapping):
c = []
for block in range(4):
c1b = (b7 >> (4 + block)) & 1
c2b = (b8 >> block) & 1
c3b = (b8 >> (4 + block)) & 1
c.append((c1b, c2b, c3b))
perms = {
(0, 0, 0): "rd=AB wr=AB inc=AB dec=AB",
(0, 1, 0): "rd=AB wr=-- inc/-- dec/--",
(1, 0, 0): "rd=A wr=- inc=- dec=A",
(1, 1, 0): "rd=AB wr=AB inc=AB dec=AB (transport)",
(0, 0, 1): "rd=AB wr=- inc=- dec=-",
(0, 1, 1): "rd=AB wr=B inc=- dec=-",
(1, 0, 1): "rd=AB wr=- inc=- dec=B",
(1, 1, 1): "rd=-- wr=- inc=- dec=-",
}
return [perms.get(cc, "reserved") for cc in c], inv_ok
def analyze(dump):
sectors = sector_count(len(dump))
findings = []
report_sectors = []
blocks_per = 4
for s in range(sectors):
base = s * blocks_per * 16
trailer = dump[base + (blocks_per - 1) * 16:
base + blocks_per * 16]
ka = trailer[:6].hex()
kb = trailer[10:16].hex()
perms, inv_ok = decode_access(trailer[6:10])
default_hits = []
for name, key in (("A", ka), ("B", kb)):
if key in DEFAULT_KEYS:
default_hits.append(f"{name}:"
f"{DEFAULT_KEYS[key]}")
value_blocks = []
for b in range(blocks_per - 1):
blk = dump[base + b * 16:base + (b + 1) * 16]
# value block: int32 + ~int32 + int32 + addr byte
# repeated with inverted addr
import struct
v = struct.unpack("<i", blk[:4])[0]
vinv = struct.unpack("<i", blk[4:8])[0]
if v == ~vinv & 0xFFFFFFFF and \
blk[12] == blk[14] == (~blk[13] & 0xFF) and \
blk[15] == (~blk[12] & 0xFF):
value_blocks.append((b, v))
rec = {"sector": s,
"keyA": ka, "keyB": kb,
"defaults": default_hits,
"access_valid": inv_ok,
"access": perms,
"value_blocks": value_blocks}
report_sectors.append(rec)
if default_hits:
findings.append(
f"sector {s}: default keys "
f"({', '.join(default_hits)})")
if not inv_ok:
findings.append(
f"sector {s}: access-bit inversion "
f"invalid (corruption or tampering)")
return {"sectors": sectors,
"per_sector": report_sectors,
"findings": findings}
def main():
ap = argparse.ArgumentParser()
ap.add_argument("dump")
ap.add_argument("--json")
args = ap.parse_args()
dump = open(args.dump, "rb").read()
s = analyze(dump)
print(f"== Mifare Classic dump: {s['sectors']} sectors ==")
for rec in s["per_sector"][:16]:
dflt = f" DEFAULT:{rec['defaults']}" \
if rec["defaults"] else ""
print(f" s{rec['sector']:>2} keyA={rec['keyA']} "
f"keyB={rec['keyB']} "
f"{'valid' if rec['access_valid'] else 'BAD-ACCESS'}"
f"{dflt}")
if s["findings"]:
print("\n[!] findings:")
for f in s["findings"][:12]:
print(f" {f}")
if args.json:
json.dump(s, open(args.json, "w"), indent=2)
print(f"\n[+] {args.json}")
if __name__ == "__main__":
main()
  • decode_access() (lines 22-45) implements the access-bits mapping: the three C1/C2/C3 bits per block (data blocks 0-2, trailer block 3) extracted from trailer bytes 6-8, mapped through the permission table (the (0,0,0) through (1,1,1) combinations with their read/write/increment/decrement rights). The inversion-validity check (lines 26-28 — c23' = ~c23 etc.) is the integrity gate: invalid inversions mean corruption or deliberate manipulation, and the report flags it either way.
  • The transport-permission recognition — (1,1,0) mapped to “transport” (line 41) — is the factory-state tell: a shipped card in transport configuration means the site never personalized the sector, which is the configuration-review finding beneath the technical one.
  • The value-block detection (lines 55-64) reads the stored-value format: int32 value + inverted copy + int32 + address bytes with inverted duplicates. Value blocks reveal the card’s use (transit credits, canteen balance, points) even when the application layer is undocumented — useful in forensics and incident scope.
  • The default-key census (lines 47-53, 68-72) checks both keys against the famous list (FFFFFFFFFFFF, the MAD key, NDEF public, zeroed) — the eternal finding: sector after sector still on factory keys means the card’s “security” was its obscurity, and the nested attack needs no starting key at all.
  • The findings layer (lines 78-83) converts per-sector analysis into the report: default keys, invalid access bits — the two card-level findings that survive every vendor conversation.

The badge reaches the target as a physical credential carried by every employee — and its technology generation reaches the attacker as the answer to a one-second read: EM4100 (clone and walk in), Classic (cryptanalyzed), Plus-SL3/DESFire (respect), unknown (audit further). The clone reaches the reader as a UID the access-control list accepts, and the door opens — the finding that UID-only validation is the access system’s whole security. The cryptanalysis reaches the Classic estate as its death certificate: one vulnerable reader session (darkside) or one known key (nested) recovers the estate’s sector keys, and the dump analysis reaches the report as the quantified posture (X of Y sectors on default keys). The relay reaches payment-and-badge systems as the distance-killer: the card authenticates, the reader authenticates, and neither knows the two are meters apart — countered by EV2+ proximity checks and policy (anti-passback, transaction limits), not by stronger card crypto. On the defender’s side, the migration census reaches the badge program as its honest map: per-door-population technology (EM4100/Classic/Plus/DESFire-EV generation), reader logging state, and clone-acceptance test results (the owned-reader empirical test that proves what the logs would catch). The reader logs themselves reach the SOC as the badge system’s Sysmon: every presented UID including rejections, duplicate-UID alerts (two badges, one UID — cloning detected), and unknown-UID events (the counterfeit that never existed in your inventory). And the whole inventory reaches the quarterly review as the physical-access baseline — the same baseline discipline as every other layer in this handbook, now covering the credential in everyone’s pocket.

  1. Census — badge sweep at authorized doors: reader logs + card reads identify 60% Mifare Classic, 20% EM4100, 20% DESFire EV1
  2. Read — authorized Classic badge dump; mfclassic.py: 12/16 sectors on default keys, transport access bits
  3. Crack — darkside against lab reader session: all 16 sector keys recovered in 20 minutes
  4. Clone — UID + dump written to changeable-UID card (bench)
  5. Test — clone presented to OWNED reader: accepted, logged as valid badge
  6. Relay demo — lab relay rig: badge authenticates through 15m tunnel from parked position
  7. Detect — reader logs show no anomaly on clone: logging-not-configured finding
  8. Report — technology census + clone acceptance + relay demo; migration: DESFire EV3 + reader logging + anti-passback

badgeaudit.py — the badge-technology classifier: from card interaction metadata (ATQA, SAK, UID length — the ISO 14443 anticollision bytes every reader captures) or from reader logs, classifies credential technology per card, cross-references against the estate’s inventory, and produces the technology census with migration priorities. stdlib only.

  • ATQA/SAK-driven classification. The anticollision bytes are the technology’s self-identification (SAK 0x08 = Classic 1K, 0x20 = DESFire + ISO-DEP, 0x04 = Ultralight-class [verify per NXP tables]) — classification from the wire, not from vendor part numbers.
  • Census over verdicts. The tool classifies and counts; the migration priority is a policy table (which doors matter) the tool joins with — technology facts and risk decisions stay separable.
  • Duplicate-UID detection built in. The same UID presenting at different readers in impossible time is the clone signature — the tool’s log-analysis mode implements the badge system’s most valuable single alert.
  • Reader-log tolerant parsing. Access-control exports differ wildly; the tool accepts the common CSV shape and documents adapters — the M13 edge-adapter pattern applied to physical access.
#!/usr/bin/env python3
"""badgeaudit.py — badge technology classifier + duplicate-UID hunt.
Usage:
python badgeaudit.py --meta meta.csv [--logs readerlog.csv]
meta.csv: uid,atqa,sak,reader,timestamp
readerlog.csv: timestamp,uid,reader,event(granted|denied)
stdlib only.
"""
import argparse, csv, json, sys
from collections import defaultdict
from datetime import datetime
TECH = {
("0x0004", "0x08"): "Mifare Classic 1K",
("0x0004", "0x18"): "Mifare Classic 4K",
("0x0004", "0x20"): "DESFire / ISO-DEP",
("0x0044", "0x00"): "MIFARE Ultralight/NTAG",
("0x0004", "0x28"): "Mifare Plus (SL1?)",
("0x0002", "0x00"): "Mifare Plus / unknown",
("0x0044", "0x20"): "NTAG 21x",
}
def classify(atqa, sak):
key = (f"0x{int(atqa, 16):04x}",
f"0x{int(sak, 16):02x}")
return TECH.get(key, f"unknown ({key[0]}/{key[1]})")
def load_meta(path):
rows = []
with open(path, encoding="utf-8", newline="") as fh:
for r in csv.DictReader(fh):
rows.append(r)
return rows
def census(meta):
counts = defaultdict(int)
for r in meta:
tech = classify(r.get("atqa", "0"),
r.get("sak", "0"))
counts[tech] += 1
return counts
def duplicates(logpath, max_speed_mps=15):
"""UID at two readers faster than physically possible."""
hits = []
per_uid = defaultdict(list)
try:
with open(logpath, encoding="utf-8", newline="") as fh:
for r in csv.DictReader(fh):
per_uid[r["uid"]].append(r)
except OSError as e:
print(f"[!] log skip: {e}", file=sys.stderr)
return hits
for uid, events in per_uid.items():
evs = sorted(events, key=lambda e: e.get(
"timestamp", ""))
parsed = []
for e in evs:
try:
parsed.append((datetime.fromisoformat(
e["timestamp"]), e["reader"]))
except (ValueError, KeyError):
continue
for a, b in zip(parsed, parsed[1:]):
dt = (b[0] - a[0]).total_seconds()
if 0 < dt < 3 and a[1] != b[1]:
hits.append((uid, a[1], b[1], round(dt, 2)))
return hits
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--meta", required=True)
ap.add_argument("--logs")
ap.add_argument("--json")
args = ap.parse_args()
meta = load_meta(args.meta)
counts = census(meta)
print(f"== badge technology census ({len(meta)} "
f"cards) ==")
for tech, n in sorted(counts.items(),
key=lambda kv: -kv[1]):
pct = n / len(meta)
print(f" {n:>5} ({pct:>5.1%}) {tech}")
legacy = sum(n for t, n in counts.items()
if "Classic" in t or "Ultralight" in t)
if legacy:
print(f"\n[!] legacy-technology cards: {legacy} "
f"({legacy/len(meta):.0%}) — migration "
f"candidates")
if args.logs:
dups = duplicates(args.logs)
if dups:
print(f"\n[!] duplicate-UID travel "
f"({len(dups)}): clone signature")
for uid, r1, r2, dt in dups[:10]:
print(f" {uid}: {r1} -> {r2} in {dt}s")
else:
print("\n[+] no duplicate-UID travel in log")
if args.json:
json.dump({"census": dict(counts),
"duplicates": dups if args.logs else []},
open(args.json, "w"), indent=2)
if __name__ == "__main__":
main()
  • classify() (lines 24-28) maps the (ATQA, SAK) pair through the technology table — the self-identification bytes every reader already captures, which makes the census possible from existing logs without touching a single card.
  • The census loop (lines 44-51) counts per technology and computes the legacy fraction (Classic + Ultralight — the crackable/clonable classes) — the migration conversation’s numerator and denominator in one table.
  • duplicates() (lines 54-72) implements the clone signature: the same UID granted at two readers within 3 seconds (faster than any human walks) — the physical-access equivalent of impossible-travel, and the single alert that catches UID cloning on estates whose readers log.
  • The honesty note in the TECH table (0x28 “Plus (SL1?)”) marks the ambiguous cases — Mifare Plus in SL1 compatibility mode looks like Classic at anticollision level, and the classifier says “SL1?” rather than guessing — the audit’s uncertainty is labeled, not laundered.

read an authorized Classic badge (bench, PM3-class reader)

Section titled “read an authorized Classic badge (bench, PM3-class reader)”

$ python mfclassic.py badge_dump.bin —json report.json == Mifare Classic dump: 16 sectors == s 0 keyA=ffffffffffff keyB=ffffffffffff valid DEFAULT:[‘A:default/blank’, ‘B:zeroed’] s 1 keyA=a0a1a2a3a4a5 keyB=ffffffffffff valid DEFAULT:[‘A:MAD key A’, …] … [!] findings: sector 0: default keys (A:default/blank, B:zeroed) sector 3: access-bit inversion invalid (corruption or tampering)

$ python badgeaudit.py —meta meta.csv —logs readerlog.csv == badge technology census (412 cards) == 248 ( 60.2%) Mifare Classic 1K 82 ( 19.9%) DESFire / ISO-DEP 82 ( 19.9%) unknown (0x0044/0x00) [!] legacy-technology cards: 248 (60%) — migration candidates

[!] duplicate-UID travel (2): clone signature 04A2B3C4: reader-7 -> reader-2 in 1.4s

Physical-access detection lives in the reader logs — the estate’s badge Sysmon: every presented UID (granted and denied), timestamped, per reader. The three alerts that matter: duplicate-UID travel (cloning signature), denied-then-granted patterns (a counterfeit iteration until the reader accepts — targeted cloning), and unknown-UID grants (a UID that grants but isn’t in your card inventory — inventory drift or clone). Your own tooling hygiene: dump analysis and log parsing are passive; cloning and relay are bench-gated demonstrations against owned readers with the finding documented as behavior, never deployed. Wazuh/SIEM integration of reader logs [verify per PACS vendor] is the join that makes badge telemetry correlate with digital events (badge-in followed by impossible network login — the classic insider-detection join). The standing metrics: technology census percentages (migration progress), reader-logging coverage (which doors log), duplicate-UID events (zero tolerance), and grant/deny ratio anomalies per door — four numbers that make the physical perimeter as measurable as every other layer in this handbook, closing the loop between the badge in the pocket and the estate’s security program.

  1. DESFire EV-generation discrimination: parse EV1/EV2/EV3 signatures from card responses for the precise migration census [verify per NXP docs].
  2. Door-risk join: reader inventory CSV with door criticality, so the census sorts migration candidates by what the door protects.
  3. Anti-passback validation: audit reader logs for passback-rule enforcement (badge-in without badge-out patterns).
  4. Reader firmware inventory: the reader is an IoT device too — firmware versions join the estate’s patch conversation.
  5. NFC payment-token audit: analyze phone-wallet presentations at cafeterias/turnstiles (the payment-NFC layer entering corporate physical access).
SignalSourceIDNotes
Duplicate-UID travelReader logs (badgeaudit)impossible-travelClone signature, zero tolerance
Denied-then-granted patternsReader logsper-UID iterationTargeted cloning
Unknown-UID grantsReader logs vs inventoryinventory diffCounterfeit badge
Legacy tech in censusbadgeauditmigration listClassic/EM4100 exposure
Default keys in dumpsmfclassicper-sector findingsUnpersonalized cards
Relay acceptanceLab demo on owned readersbehavior findingDistance-check gap
Reader logging gapsConfig auditcoverage findingThe badge Sysmon, off
Anti-passback absentLog auditpolicy findingRelay counter missing
DESFire EV generation mixCard censusprocurement finding“DESFire” ≠ DESFire EV3
Wazuh join badge+networkHIDS/SIEM[verify rule IDs per deployment]Badge-in + login joins

The credential-attacker’s evasion targets the reader’s judgment and the audit’s memory. UID selectivity: cloners present cloned cards only at low-scrutiny doors (parking, side entrances) and never at the monitored lobby — countered by per-door logging uniformity (the unmonitored door is the blind spot the census measures) and by anti-passback logic that notices badge-in without badge-out. Timing discipline: clone usage during shift changes and high-traffic windows where human attention is thin — countered by the impossible-travel alert (time is the one thing the clone cannot fake) and by door-pair correlation. Technology mimicry: writing Classic dumps to Plus SL1 cards so the reader sees a plausible technology while the security is the cloned Classic’s — countered by SL3/DESFire enforcement at the reader (accept nothing below the policy generation). Relay discretion: relay rigs throttled to normal human usage patterns so transaction anomalies stay inside statistical noise — countered by anti-passback, transaction counters (EV2+), and the policy that limits what a single credential can do per window. Reader-side amnesia: attacks against systems whose readers log nothing — the evasion that requires no skill at all — countered by the logging-coverage audit, which is this module’s most important defender finding: the estate that doesn’t log badge events cannot detect any of this, and the census’s first column should be logging, not technology. The meta-point: physical-access evasion lives downstream of logging and inventory, and the two controls that make every attack above visible cost less than one door’s worth of new readers.

⚠️ What caught me: the first badge audit I ran used the PACS export everyone said was complete — and my duplicate-UID detector found nothing, which I reported as “no cloning evidence.” A week later a guard mentioned the side entrance “doesn’t write to the computer.” Two of fourteen readers simply weren’t wired into the logging at all, and both were doors whose badge population was 100% clonable Classic. My clean report had measured only the doors that could prove anything. Since then, every badge audit starts with the logging-coverage check — which readers write which events, verified per door — because a clone alert from a logging reader is worth more than a census of a hundred silent ones. The control that isn’t plugged in isn’t a control.

  1. Read and analyze an authorized Classic badge: dump it, run mfclassic.py, and catalog default keys, access bits, and value blocks — the card forensics layer, hands on.
  2. Census your estate (or lab) from reader metadata: run badgeaudit.py --meta and produce the technology table with the legacy fraction — the migration conversation, quantified.
  3. Clone drill (bench-gated): write the authorized badge’s UID to a changeable-UID card, present it to the owned reader, and observe grant + log — the clone acceptance test that proves what your logging would catch.
  4. Relay demo (lab only): two readers, one link, one authorized badge — demonstrate the distance-defeating tunnel and measure whether your EV-generation cards resist (EV2+ proximity checks [verify]).
  5. Logging-coverage audit: verify per-reader which events reach the PACS database (walk test with a real card, check the export) — the audit that finds the unplugged control before the attacker does.
  • Technical: Compare EM4100, Mifare Classic, Mifare Plus SL3, and DESFire EV3 on authentication model — what does each actually verify, and what defeats it?
  • Technical: Explain the Crypto1 failure chain — darkside, nested, hardnested — and why one vulnerable reader session compromises the estate.
  • Scenario: Your duplicate-UID alert fires on the CFO’s badge while she’s on a plane. What is the response, and what does the incident reveal about logging coverage?
  • Conceptual: Why does the relay attack defeat both crypto-strong and crypto-weak systems, and which controls actually counter it?
  • Conceptual: How does physical-access telemetry join digital telemetry (badge-in + login) — and what insider-detection value does that join create?
  • Deep-dive: Design the badge-estate migration program: technology census, door criticality, reader logging coverage, and the acceptance test for the new credential generation.
  • Practical: The estate has 60% Classic badges and no budget this year. What are the top three compensating controls, in order, and what does each actually catch?

WI5 (Bluetooth Attacks) · WI6 (RF & SDR Fundamentals) · F4 (Cryptography Fundamentals) · F1 (Networking Fundamentals) · F5 (Python for Security)