Firmware/ Python CLI/ HF 13.56MHz// March 2026
Tool Development

hf 14a sniff for
ChameleonUltra

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.

Firmware Python CLI ISO14443A New Command
Author
Niel Nielsen
Platform
ChameleonUltra
Protocol
ISO14443A
Command
hf 14a sniff
Contents
  1. 01Why sniff on HF
  2. 02Hardware capability
  3. 03Implementation
  4. 04Bugs found along the way
  5. 05Real capture output
  6. 06Frame colour reference
  7. 07Usage

01Why sniff on HF

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.

02Hardware capability

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.

Limitation

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.

03Implementation

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.

Cnfc_14a.c
/* 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 ... */
}
Capp_cmd.c
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.

04Bugs found along the way

Bug
tag_mode_enter() wiped anti-collision data

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.

Bug
Watchdog reset after 5 seconds

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.

Bug
Python send_cmd_sync timeout shorter than sniff duration

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.

Bug
STATUS_SUCCESS ≠ Status.HF_TAG_OK

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.

Architecture
Single main loop — no RTOS threading

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.

05Real capture output

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.

Outputhf 14a sniff --timeout 8000
 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.

06Frame colour reference

ColourFrame types
GreenREQA / WUPA — field activation
BlueANTICOLL / SELECT — UID exchange
MagentaRATS / PPS — ISO14443-4 activation
YellowSELECT AID / GPO / WRITE / VERIFY
CyanREAD / GET DATA / HALT / DESELECT
RedAUTH KeyA/B / GENERATE AC / INTERNAL AUTH
GreyEncrypted nonce / unknown frames

07Usage

Prerequisite

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.

CLI
# Switch to emulator mode
hw mode --emulator

# Sniff for 8 seconds
hf 14a sniff --timeout 8000

# Shorter capture
hf 14a sniff --timeout 2500
Firmware files changed

nfc_14a.hnfc_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.hDATA_CMD_HF14A_SNIFF = 2020
app_cmd.ccmd_processor_hf14a_sniff() with watchdog feed

Python files changed

chameleon_enum.pyHF14A_SNIFF = 2020
chameleon_cmd.pyhf14a_sniff(timeout_ms) with correct send_cmd_sync timeout
chameleon_cli_unit.pyhf 14a sniff command with colour decode and summary

← Back to Blog