Passive ISO14443A frame capture using the nRF52840 NFCT peripheral.
See exactly what a reader sends — REQA, anti-collision, SELECT, AUTH, APDUs — all decoded in colour.
When the ChameleonUltra emulates a tag, it responds to whatever the reader sends. But to emulate a tag correctly you need to know what the reader actually sends — and that varies enormously. Some readers do a bare REQA and authenticate immediately. Others do RATS, ISO 7816 SELECT, and a chain of APDUs before they'll accept the card. Others probe specific memory blocks in a specific order.
Without a sniff tool you're guessing. With hf 14a sniff you place the CU near the target reader, let it capture a few seconds of traffic, and get back a complete decoded frame log — UID, AID, auth key blocks, everything the reader expects. Then you configure your emulation accordingly.
The ChameleonUltra has two HF subsystems. The RC522 (SPI) is the reader chip — it generates the 13.56MHz field and handles reader-side ISO14443A framing. The NFCT peripheral built into the nRF52840 is the tag emulation side — it receives frames from external readers and drives the tag response.
The sniff only captures reader → tag frames — commands sent by the reader to the CU. Tag responses (the CU's own replies) are not captured since they originate from the same device. A fully bidirectional sniff of a third-party reader ↔ tag exchange is not possible without additional passive RF hardware.
This is still very useful. In most real-world scenarios you need to understand what the reader asks for, not what the tag replies — the reply is under your control once you know what to emulate.
The hook is a single callback registered into nfc_tag_14a_data_process() — the function already called for every received frame during tag emulation. The sniff command installs the callback, waits the requested duration feeding the watchdog, then removes the callback and returns all captured frames over USB.
/* Sniff hook — fires before any tag response logic */
static nfc_tag_14a_sniff_cb_t m_sniff_cb = NULL;
void nfc_tag_14a_data_process(uint8_t *p_data) {
uint16_t szDataBits = (NRF_NFCT->RXD.AMOUNT & ...);
if (szDataBits == 0 || szDataBits > MAX_BITS) return;
/* Call sniff callback before tag handler */
if (m_sniff_cb != NULL) {
m_sniff_cb(p_data, szDataBits);
}
/* ... normal tag emulation continues ... */
}
static data_frame_tx_t *cmd_processor_hf14a_sniff(...) {
/* Install callback into running tag emulation stack */
m_sniff_buf_len = 0;
m_sniff_active = true;
nfc_tag_14a_set_sniff_cb(hf14a_sniff_frame_cb);
/* Wait, feeding watchdog to prevent 5s WDT reset */
autotimer *p_at = bsp_obtain_timer(0);
while (NO_TIMEOUT_1MS(p_at, timeout_ms)) {
bsp_delay_ms(1);
bsp_wdt_feed();
}
bsp_return_timer(p_at);
m_sniff_active = false;
nfc_tag_14a_clear_sniff_cb();
tag_emulation_sense_run();
return data_frame_make(cmd, STATUS_SUCCESS, m_sniff_buf_len, m_sniff_buf);
}
Frame payload format is simple — each frame is prefixed with a 2-byte big-endian bit count, followed by the raw frame bytes. The Python client unpacks this into a list of (szBits, data) tuples for decoding.
The sniff command initially called tag_mode_enter() to ensure NFCT was active. This reinitialises the NFCT peripheral and clears the UID/ATQA/SAK loaded by the tag emulation stack. The reader got no ATQA back and never progressed past REQA. Fix: don't call tag_mode_enter() — hook into the already-running emulation stack instead.
The nRF52840 WDT is configured with a 5000ms timeout and is fed by the main loop. The sniff command blocks the main loop for its entire duration. Any sniff longer than 5 seconds triggered a watchdog reset, disconnecting USB. Fix: call bsp_wdt_feed() inside the wait loop every 1ms.
The default send_cmd_sync timeout is 3 seconds. Any sniff longer than ~1 second would cause Python to throw TimeoutError while the firmware was still happily capturing. The error was caught and displayed as "Command not supported" — completely misleading. Fix: pass timeout = sniff_ms // 1000 + 2 to send_cmd_sync.
The firmware returns STATUS_SUCCESS (0x68) on a successful sniff, but the Python CLI was checking for Status.HF_TAG_OK (0x00). Every successful capture was silently discarded as a failure. Fix: check for both values.
Everything in the CU firmware runs in a single cooperative main loop. The NFCT IRQ fires asynchronously and can call the sniff callback while the main loop is blocked inside the command processor. This is why the callback approach works — but it also means the USB stack and watchdog must be explicitly maintained during the wait.
Captured while holding the CU against a MIFARE Classic access control reader. The reader authenticated to block 1 using KeyA — confirming it's a standard Mifare Classic deployment using the default key space.
Captured : 194 frame(s)
# bits hex data decoded
--- ---- ------------------------------------------ -----------------------------------
1 7 26 REQA
2 36 50 01 5e 69 06 HALT
3 7 52 WUPA
4 18 93 41 00 ANTICOLL CL1 NVB=41
5 81 93 e1 c8 7e 1e 5b 83 35 ba 13 00 ANTICOLL CL1 NVB=e1
6 36 60 01 d6 df 0b AUTH KeyA block=1
7 72 99 3a ed 96 56 65 d2 b1 3b (encrypted nonce — auth challenge/response)
8 36 50 01 5e 69 06 HALT
9 7 52 WUPA
10 81 93 e1 c8 7e 1e 5b 83 35 ba 13 00 ANTICOLL CL1 NVB=e1
11 7 26 REQA
...
───────────────────────────────────────────────────────
Auth : MIFARE Classic auth detected
The reader's behaviour is visible in just 8 frames. It first tries REQA and gets HALT back (another card in field), then sends WUPA to wake everything, completes anti-collision, and immediately tries AUTH KeyA on block 1. It doesn't do RATS, doesn't do a SELECT AID — it's a pure Mifare Classic reader targeting sector 0. The encrypted nonce exchange at frame 7 fails (auth rejected by the emulated tag's default key), so the reader retries from the beginning.
| Colour | Frame types |
|---|---|
| Green | REQA / WUPA — field activation |
| Blue | ANTICOLL / SELECT — UID exchange |
| Magenta | RATS / PPS — ISO14443-4 activation |
| Yellow | SELECT AID / GPO / WRITE / VERIFY |
| Cyan | READ / GET DATA / HALT / DESELECT |
| Red | AUTH KeyA/B / GENERATE AC / INTERNAL AUTH |
| Grey | Encrypted nonce / unknown frames |
The CU must be in tag emulator mode with an active HF slot before running sniff. The NFCT peripheral only receives frames when emulating a tag — in reader mode it's disabled. Run hw mode --emulator first, or ensure a slot is active.
# Switch to emulator mode
hw mode --emulator
# Sniff for 8 seconds
hf 14a sniff --timeout 8000
# Shorter capture
hf 14a sniff --timeout 2500
nfc_14a.h — nfc_tag_14a_sniff_cb_t typedef, set_sniff_cb() / clear_sniff_cb() API
nfc_14a.c — hook in nfc_tag_14a_data_process() before tag handler
data_cmd.h — DATA_CMD_HF14A_SNIFF = 2020
app_cmd.c — cmd_processor_hf14a_sniff() with watchdog feed
chameleon_enum.py — HF14A_SNIFF = 2020
chameleon_cmd.py — hf14a_sniff(timeout_ms) with correct send_cmd_sync timeout
chameleon_cli_unit.py — hf 14a sniff command with colour decode and summary