diff --git a/docs/plans/2026-03-17-stateful-table-firmware-plan.md b/docs/plans/2026-03-17-stateful-table-firmware-plan.md new file mode 100644 index 0000000..b8a7d8d --- /dev/null +++ b/docs/plans/2026-03-17-stateful-table-firmware-plan.md @@ -0,0 +1,795 @@ +# Stateful Response Table Firmware Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Add a stateful response table engine to PM3 firmware for custom/vendor 15693 commands, with access control in the native handler and shared AES-CMAC crypto. + +**Architecture:** New `sim_table.c/h` provides table storage, lookup with group-based state machine, and action execution (static response, EML read/write, AES-CMAC). The existing `SimTagIso15693()` native handler gains per-block access control and falls through to the table for unrecognized commands. Shared `sim_crypto.c/h` wraps existing `ulaes_cmac()`. + +**Tech Stack:** C (ARM7 firmware), existing PM3 build system, existing `cmac_calc.c` for AES + +**Working directory:** `/home/work/proxmark3/` on branch `sim-relay` + +**Build command:** `make armsrc/obj/fullimage.elf` (firmware only, no client) + +**Important:** All commits use `git commit --no-gpg-sign`. Firmware has no unit test framework — we verify by successful compilation. Python-side tests come later in a separate plan. + +--- + +### Task 1: Command IDs + +**Files:** +- Modify: `/home/work/proxmark3/include/pm3_cmd.h` + +**Step 1: Add table command IDs** + +After the `CMD_HF_SEOS_SIMULATE` line (~930), add: + +```c +// Sim response table management +#define CMD_SIM_TABLE_UPLOAD 0x0950 // Host→FW: upload table (chunked) +#define CMD_SIM_TABLE_CLEAR 0x0951 // Host→FW: clear table +#define CMD_SIM_TABLE_UPDATE 0x0952 // Host→FW: add/modify single entry +``` + +**Step 2: Verify build** + +Run: `cd /home/work/proxmark3 && make armsrc/obj/fullimage.elf -j` +Expected: Build succeeds + +**Step 3: Commit** + +```bash +cd /home/work/proxmark3 +git add include/pm3_cmd.h +git commit --no-gpg-sign -m "feat: add CMD_SIM_TABLE_* command IDs" +``` + +--- + +### Task 2: iso15_tag_t extensions + +**Files:** +- Modify: `/home/work/proxmark3/include/iso15.h` + +**Step 1: Add new fields to iso15_tag_t** + +Add these fields before the closing `} PACKED iso15_tag_t;` (after `trace_enabled`): + +```c + // Stateful table support + uint8_t random_uid; // if nonzero, inventory returns random UID + uint8_t auth_state; // bitmask: authenticated passwords + // bit 0: read, bit 1: write, bit 2: privacy, bit 3: destroy + uint8_t lock_mode[ISO15693_TAG_MAX_PAGES]; + // per-block access control: + // 0x00 = open + // 0x01 = permanently locked + // 0x02 = read needs auth (check auth_state bit 0) + // 0x04 = write needs auth (check auth_state bit 1) + // 0x06 = read+write need auth +``` + +**Step 2: Verify build** + +Run: `cd /home/work/proxmark3 && make armsrc/obj/fullimage.elf -j` +Expected: Build succeeds + +**Step 3: Commit** + +```bash +cd /home/work/proxmark3 +git add include/iso15.h +git commit --no-gpg-sign -m "feat: extend iso15_tag_t with auth_state, lock_mode, random_uid" +``` + +--- + +### Task 3: sim_crypto module + +**Files:** +- Create: `/home/work/proxmark3/armsrc/sim_crypto.h` +- Create: `/home/work/proxmark3/armsrc/sim_crypto.c` +- Modify: `/home/work/proxmark3/armsrc/Makefile` + +**Step 1: Create sim_crypto.h** + +```c +//----------------------------------------------------------------------------- +// Copyright (C) Proxmark3 contributors. See AUTHORS.md for details. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// See LICENSE.txt for the text of the license. +//----------------------------------------------------------------------------- +// Shared crypto primitives for card simulation +//----------------------------------------------------------------------------- + +#ifndef __SIM_CRYPTO_H +#define __SIM_CRYPTO_H + +#include "common.h" + +// AES-CMAC: compute MAC over msg using 16-byte key +// mac_out receives up to mac_len bytes of the 16-byte MAC +// Returns true on success +bool sim_aes_cmac(const uint8_t key[16], const uint8_t *msg, + uint8_t msg_len, uint8_t *mac_out, uint8_t mac_len); + +#endif // __SIM_CRYPTO_H +``` + +**Step 2: Create sim_crypto.c** + +```c +//----------------------------------------------------------------------------- +// Copyright (C) Proxmark3 contributors. See AUTHORS.md for details. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// See LICENSE.txt for the text of the license. +//----------------------------------------------------------------------------- +// Shared crypto primitives for card simulation +//----------------------------------------------------------------------------- + +#include "sim_crypto.h" +#include "cmac_calc.h" +#include + +bool sim_aes_cmac(const uint8_t key[16], const uint8_t *msg, + uint8_t msg_len, uint8_t *mac_out, uint8_t mac_len) { + if (mac_len > 16) mac_len = 16; + uint8_t full_mac[16]; + ulaes_cmac(key, 16, msg, msg_len, full_mac); + memcpy(mac_out, full_mac, mac_len); + return true; +} +``` + +**Step 3: Add to Makefile** + +In `armsrc/Makefile`, find the `SRC_ASM` or `SRC_FIRM` source list (where `cmac_calc.c` appears). Add `sim_crypto.c \` on the line after `cmac_calc.c \`. + +**Step 4: Verify build** + +Run: `cd /home/work/proxmark3 && make armsrc/obj/fullimage.elf -j` +Expected: Build succeeds + +**Step 5: Commit** + +```bash +cd /home/work/proxmark3 +git add armsrc/sim_crypto.h armsrc/sim_crypto.c armsrc/Makefile +git commit --no-gpg-sign -m "feat: add sim_crypto module (AES-CMAC wrapper)" +``` + +--- + +### Task 4: sim_table header + +**Files:** +- Create: `/home/work/proxmark3/armsrc/sim_table.h` + +**Step 1: Create sim_table.h** + +```c +//----------------------------------------------------------------------------- +// Copyright (C) Proxmark3 contributors. See AUTHORS.md for details. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// See LICENSE.txt for the text of the license. +//----------------------------------------------------------------------------- +// Stateful response table for card simulation +//----------------------------------------------------------------------------- + +#ifndef __SIM_TABLE_H +#define __SIM_TABLE_H + +#include "common.h" +#include "iso15.h" + +#define SIM_TABLE_MAX_MATCH 32 +#define SIM_TABLE_MAX_RESPONSE 64 +#define SIM_TABLE_MAX_ENTRIES 300 + +// EML action types +#define SIM_EML_NONE 0 // static response only +#define SIM_EML_READ 1 // read from EML into response +#define SIM_EML_WRITE 2 // write command data to EML +#define SIM_EML_AES_CMAC 3 // compute AES-CMAC, key from EML, challenge from cmd + +// Match modes +#define SIM_MATCH_EXACT 0 +#define SIM_MATCH_PREFIX 1 + +// Entry flags +#define SIM_FLAG_APPEND_CRC 0x01 +#define SIM_FLAG_CONSUME 0x02 // one-shot: disable entry after use + +typedef struct { + // Match + uint8_t match[SIM_TABLE_MAX_MATCH]; + uint8_t match_len; + uint8_t match_mode; // SIM_MATCH_EXACT or SIM_MATCH_PREFIX + + // Response + uint8_t response[SIM_TABLE_MAX_RESPONSE]; + uint8_t response_len; + uint8_t response_flags; // SIM_FLAG_APPEND_CRC + + // EML actions + uint8_t eml_action; // SIM_EML_* + uint16_t eml_offset; // EML byte offset for read/write/key + uint8_t eml_len; // bytes to read/write / MAC output length + uint8_t eml_resp_insert; // position in response to insert data (read/CMAC) + uint8_t cmd_data_offset; // position in command for write data / challenge + uint8_t cmd_data_len; // length of data to write / challenge length + + // State + uint8_t group; // group this entry belongs to (0 = always active) + uint32_t activate_groups; // bitmask: groups to activate on match + uint32_t deactivate_groups; // bitmask: groups to deactivate on match + uint8_t set_auth; // auth_state bits to SET on match + uint8_t clear_auth; // auth_state bits to CLEAR on match + + // Flags + uint8_t flags; // SIM_FLAG_CONSUME etc. + + uint8_t _pad[1]; // alignment +} PACKED sim_table_entry_t; // ~128 bytes + +// Initialize table in BigBuf with initial active group mask +void sim_table_init(uint32_t initial_groups); + +// Clear all entries, reset state +void sim_table_clear(void); + +// Add entry from serialized data (128 bytes) +// Returns true if entry was added +bool sim_table_add(const uint8_t *data, uint16_t len); + +// Current entry count +uint16_t sim_table_count(void); + +// Find first matching entry for command +// Returns NULL if no match +sim_table_entry_t *sim_table_lookup(const uint8_t *cmd, uint8_t cmd_len); + +// Execute matched entry: build response, apply state changes +// tag pointer needed for EML access and auth_state updates +void sim_table_execute(sim_table_entry_t *e, + const uint8_t *cmd, uint8_t cmd_len, + uint8_t *resp_buf, uint16_t *resp_len, + iso15_tag_t *tag); + +// Get current active groups mask +uint32_t sim_table_get_active_groups(void); + +#endif // __SIM_TABLE_H +``` + +**Step 2: Verify build** + +Run: `cd /home/work/proxmark3 && make armsrc/obj/fullimage.elf -j` +Expected: Build succeeds (header only, no .c yet) + +**Step 3: Commit** + +```bash +cd /home/work/proxmark3 +git add armsrc/sim_table.h +git commit --no-gpg-sign -m "feat: add sim_table.h — stateful response table header" +``` + +--- + +### Task 5: sim_table implementation + +**Files:** +- Create: `/home/work/proxmark3/armsrc/sim_table.c` +- Modify: `/home/work/proxmark3/armsrc/Makefile` + +**Step 1: Create sim_table.c** + +```c +//----------------------------------------------------------------------------- +// Copyright (C) Proxmark3 contributors. See AUTHORS.md for details. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// See LICENSE.txt for the text of the license. +//----------------------------------------------------------------------------- +// Stateful response table for card simulation +//----------------------------------------------------------------------------- + +#include "sim_table.h" +#include "sim_crypto.h" +#include "BigBuf.h" +#include "dbprint.h" +#include + +static struct { + sim_table_entry_t *entries; + uint16_t count; + uint16_t max_entries; + uint32_t active_groups; +} g_table; + +void sim_table_init(uint32_t initial_groups) { + // Allocate table from BigBuf + // We use a dedicated region at the end of BigBuf + uint32_t table_size = SIM_TABLE_MAX_ENTRIES * sizeof(sim_table_entry_t); + g_table.entries = (sim_table_entry_t *)BigBuf_calloc(table_size); + g_table.count = 0; + g_table.max_entries = SIM_TABLE_MAX_ENTRIES; + g_table.active_groups = initial_groups; + + if (g_dbglevel >= DBG_INFO) + Dbprintf("sim_table_init: %d entries max, initial groups=0x%08X", + g_table.max_entries, initial_groups); +} + +void sim_table_clear(void) { + if (g_table.entries) { + memset(g_table.entries, 0, g_table.count * sizeof(sim_table_entry_t)); + } + g_table.count = 0; + g_table.active_groups = 0; +} + +bool sim_table_add(const uint8_t *data, uint16_t len) { + if (g_table.entries == NULL) return false; + if (g_table.count >= g_table.max_entries) return false; + if (len < sizeof(sim_table_entry_t)) return false; + + memcpy(&g_table.entries[g_table.count], data, sizeof(sim_table_entry_t)); + g_table.count++; + return true; +} + +uint16_t sim_table_count(void) { + return g_table.count; +} + +uint32_t sim_table_get_active_groups(void) { + return g_table.active_groups; +} + +sim_table_entry_t *sim_table_lookup(const uint8_t *cmd, uint8_t cmd_len) { + for (uint16_t i = 0; i < g_table.count; i++) { + sim_table_entry_t *e = &g_table.entries[i]; + + // Skip empty entries (consumed one-shots) + if (e->match_len == 0) continue; + + // Skip if entry's group is not active (group 0 = always active) + if (e->group != 0 && !(g_table.active_groups & (1u << e->group))) { + continue; + } + + // Match + bool matched = false; + if (e->match_mode == SIM_MATCH_EXACT) { + if (cmd_len == e->match_len && + memcmp(cmd, e->match, e->match_len) == 0) { + matched = true; + } + } else if (e->match_mode == SIM_MATCH_PREFIX) { + if (cmd_len >= e->match_len && + memcmp(cmd, e->match, e->match_len) == 0) { + matched = true; + } + } + + if (!matched) continue; + + // Match found — apply state changes + if (e->activate_groups) + g_table.active_groups |= e->activate_groups; + if (e->deactivate_groups) + g_table.active_groups &= ~e->deactivate_groups; + + // Consume if one-shot + if (e->flags & SIM_FLAG_CONSUME) { + e->match_len = 0; // mark as empty + } + + return e; + } + return NULL; +} + +void sim_table_execute(sim_table_entry_t *e, + const uint8_t *cmd, uint8_t cmd_len, + uint8_t *resp_buf, uint16_t *resp_len, + iso15_tag_t *tag) { + + // Apply auth state changes + if (e->set_auth) tag->auth_state |= e->set_auth; + if (e->clear_auth) tag->auth_state &= ~e->clear_auth; + + // Start with static response + memcpy(resp_buf, e->response, e->response_len); + *resp_len = e->response_len; + + switch (e->eml_action) { + case SIM_EML_NONE: + break; + + case SIM_EML_READ: { + // Read from tag EML data into response + uint16_t off = e->eml_offset; + uint8_t len = e->eml_len; + uint8_t ins = e->eml_resp_insert; + if (off + len <= sizeof(tag->data)) { + memcpy(resp_buf + ins, tag->data + off, len); + if (ins + len > *resp_len) + *resp_len = ins + len; + } + break; + } + + case SIM_EML_WRITE: { + // Write command data to tag EML + uint8_t src_off = e->cmd_data_offset; + uint8_t len = e->eml_len; + uint16_t dst_off = e->eml_offset; + if (src_off + len <= cmd_len && dst_off + len <= sizeof(tag->data)) { + memcpy(tag->data + dst_off, cmd + src_off, len); + } + break; + } + + case SIM_EML_AES_CMAC: { + // Key from EML, challenge from command, MAC into response + uint8_t key[16]; + uint16_t key_off = e->eml_offset; + if (key_off + 16 > sizeof(tag->data)) break; + memcpy(key, tag->data + key_off, 16); + + uint8_t challenge_off = e->cmd_data_offset; + uint8_t challenge_len = e->cmd_data_len; + if (challenge_off + challenge_len > cmd_len) break; + + uint8_t mac_len = e->eml_len; + uint8_t mac_ins = e->eml_resp_insert; + + sim_aes_cmac(key, cmd + challenge_off, challenge_len, + resp_buf + mac_ins, mac_len); + + if (mac_ins + mac_len > *resp_len) + *resp_len = mac_ins + mac_len; + break; + } + } +} +``` + +**Step 2: Add to Makefile** + +Add `sim_table.c \` after the `sim_crypto.c \` line added in Task 3. + +**Step 3: Verify build** + +Run: `cd /home/work/proxmark3 && make armsrc/obj/fullimage.elf -j` +Expected: Build succeeds + +**Step 4: Commit** + +```bash +cd /home/work/proxmark3 +git add armsrc/sim_table.c armsrc/Makefile +git commit --no-gpg-sign -m "feat: add sim_table.c — stateful response table engine" +``` + +--- + +### Task 6: Command dispatch in appmain.c + +**Files:** +- Modify: `/home/work/proxmark3/armsrc/appmain.c` + +**Step 1: Add include** + +Near the top of `appmain.c`, with the other includes, add: + +```c +#include "sim_table.h" +``` + +**Step 2: Add dispatch cases** + +Find the `CMD_HF_SEOS_SIMULATE` case (around line 1571). After it, add: + +```c + case CMD_SIM_TABLE_UPLOAD: { + // First chunk: init table, then add entries + uint32_t initial_groups = 0xFFFFFFFF; // all groups active by default + if (packet->length >= 4) { + // First 4 bytes = initial_groups mask + memcpy(&initial_groups, packet->data.asBytes, 4); + } + sim_table_init(initial_groups); + // Remaining bytes = entries + uint16_t data_off = 4; + while (data_off + sizeof(sim_table_entry_t) <= packet->length) { + sim_table_add(packet->data.asBytes + data_off, sizeof(sim_table_entry_t)); + data_off += sizeof(sim_table_entry_t); + } + reply_ng(CMD_SIM_TABLE_UPLOAD, PM3_SUCCESS, NULL, 0); + break; + } + case CMD_SIM_TABLE_CLEAR: { + sim_table_clear(); + reply_ng(CMD_SIM_TABLE_CLEAR, PM3_SUCCESS, NULL, 0); + break; + } + case CMD_SIM_TABLE_UPDATE: { + // Add single entry (or chunk of entries) + uint16_t off = 0; + while (off + sizeof(sim_table_entry_t) <= packet->length) { + sim_table_add(packet->data.asBytes + off, sizeof(sim_table_entry_t)); + off += sizeof(sim_table_entry_t); + } + reply_ng(CMD_SIM_TABLE_UPDATE, PM3_SUCCESS, NULL, 0); + break; + } +``` + +**Step 3: Verify build** + +Run: `cd /home/work/proxmark3 && make armsrc/obj/fullimage.elf -j` +Expected: Build succeeds + +**Step 4: Commit** + +```bash +cd /home/work/proxmark3 +git add armsrc/appmain.c +git commit --no-gpg-sign -m "feat: dispatch CMD_SIM_TABLE_* in appmain" +``` + +--- + +### Task 7: Native handler access control + random UID + +**Files:** +- Modify: `/home/work/proxmark3/armsrc/iso15693.c` + +**Step 1: Add includes** + +Near the top of `iso15693.c`, add: + +```c +#include "sim_table.h" +``` + +**Step 2: Initialize new fields in SimTagIso15693** + +In `SimTagIso15693()`, after the existing field initializations (around line 2193, after `tag->trace_enabled`), add: + +```c + tag->random_uid = 0; + tag->auth_state = 0; + memset(tag->lock_mode, 0, sizeof(tag->lock_mode)); +``` + +**Step 3: Add random UID to inventory response** + +Find the inventory response block (around line 2412-2416): + +```c + // No error: Answer + recv[0] = ISO15_NOERROR; + recv[1] = tag->dsfid; + memcpy(&recv[2], tag->uid, 8); + recvLen = 10; +``` + +Replace with: + +```c + // No error: Answer + recv[0] = ISO15_NOERROR; + recv[1] = tag->dsfid; + if (tag->random_uid) { + // Privacy mode: return random UID + for (int i = 0; i < 8; i++) + recv[2 + i] = prand() & 0xFF; + } else { + memcpy(&recv[2], tag->uid, 8); + } + recvLen = 10; +``` + +Also do the same for the second inventory path (around line 2450-2453 in the `switch(cmd[1])` case `ISO15693_INVENTORY`): + +```c + case ISO15693_INVENTORY: + if (g_dbglevel >= DBG_DEBUG) Dbprintf("Inventory cmd"); + recv[0] = ISO15_NOERROR; + recv[1] = tag->dsfid; + if (tag->random_uid) { + for (int i = 0; i < 8; i++) + recv[2 + i] = prand() & 0xFF; + } else { + memcpy(&recv[2], tag->uid, 8); + } + recvLen = 10; + break; +``` + +**Step 4: Add access control to READBLOCK** + +Find the `ISO15693_READBLOCK` case (around line 2459). After the `pageNum` extraction and before the existing bounds check, add access control: + +```c + case ISO15693_READBLOCK: + if (g_dbglevel >= DBG_DEBUG) Dbprintf("ReadBlock cmd"); + pageNum = cmd[cmdCpt++]; + // Access control check + if (pageNum < ISO15693_TAG_MAX_PAGES) { + uint8_t lm = tag->lock_mode[pageNum]; + if (lm == 0x01) { + error = ISO15_ERROR_BLOCK_LOCKED; + break; + } + if ((lm & 0x02) && !(tag->auth_state & 0x01)) { + error = ISO15_ERROR_BLOCK_UNAVAILABLE; + break; + } + } + if (pageNum >= tag->pagesCount) + error = ISO15_ERROR_BLOCK_UNAVAILABLE; + else { + // ... existing read logic unchanged ... +``` + +**Step 5: Add access control to WRITEBLOCK** + +Find the `ISO15693_WRITEBLOCK` case (around line 2476). After `pageNum` extraction, add: + +```c + case ISO15693_WRITEBLOCK: + if (g_dbglevel >= DBG_DEBUG) Dbprintf("WriteBlock cmd"); + pageNum = cmd[cmdCpt++]; + // Access control check + if (pageNum < ISO15693_TAG_MAX_PAGES) { + uint8_t lm = tag->lock_mode[pageNum]; + if (lm == 0x01) { + error = ISO15_ERROR_BLOCK_LOCKED; + break; + } + if ((lm & 0x04) && !(tag->auth_state & 0x02)) { + error = ISO15_ERROR_BLOCK_UNAVAILABLE; + break; + } + } + if (pageNum >= tag->pagesCount) + error = ISO15_ERROR_BLOCK_UNAVAILABLE; + else { + // ... existing write logic unchanged ... +``` + +**Step 6: Verify build** + +Run: `cd /home/work/proxmark3 && make armsrc/obj/fullimage.elf -j` +Expected: Build succeeds + +**Step 7: Commit** + +```bash +cd /home/work/proxmark3 +git add armsrc/iso15693.c +git commit --no-gpg-sign -m "feat: add access control + random UID to 15693 sim handler" +``` + +--- + +### Task 8: Table fallback in default case + +**Files:** +- Modify: `/home/work/proxmark3/armsrc/iso15693.c` + +**Step 1: Replace default case with table lookup + relay fallback** + +Find the `default:` case in the command switch (around line 2629). Replace the entire case body: + +```c + default: + // --- Table lookup for custom/vendor commands --- + if (sim_table_count() > 0) { + sim_table_entry_t *entry = sim_table_lookup(cmd, cmd_len); + if (entry) { + sim_table_execute(entry, cmd, cmd_len, recv, &recvLen, tag); + if (g_dbglevel >= DBG_DEBUG) + Dbprintf("Table match for CMD 0x%02X, resp %d bytes", + cmd[1], recvLen); + break; + } + } + // No table match — fall through to relay + if (tag->relay_resp_ready && + tag->relay_cmd_len == cmd_len && + memcmp(tag->relay_cmd, cmd, cmd_len) == 0) { + memcpy(recv, tag->relay_resp, tag->relay_resp_len); + recvLen = tag->relay_resp_len; + tag->relay_resp_ready = false; + if (g_dbglevel >= DBG_DEBUG) + Dbprintf("Serving cached relay response for CMD 0x%02X", cmd[1]); + } else { + memcpy(tag->relay_cmd, cmd, MIN(cmd_len, sizeof(tag->relay_cmd))); + tag->relay_cmd_len = cmd_len; + tag->relay_pending = true; + tag->relay_resp_ready = false; + reply_ng(CMD_HF_ISO15693_SIM_RELAY, PM3_SUCCESS, cmd, cmd_len); + if (g_dbglevel >= DBG_DEBUG) + Dbprintf("Relayed CMD 0x%02X to host (%d bytes)", cmd[1], cmd_len); + recvLen = 0; + } + break; +``` + +**Step 2: Verify build** + +Run: `cd /home/work/proxmark3 && make armsrc/obj/fullimage.elf -j` +Expected: Build succeeds + +**Step 3: Commit** + +```bash +cd /home/work/proxmark3 +git add armsrc/iso15693.c +git commit --no-gpg-sign -m "feat: table lookup before relay fallback in 15693 sim default case" +``` + +--- + +### Task 9: Verify full build + flash size + +**Step 1: Clean build** + +Run: `cd /home/work/proxmark3 && make clean && make -j` +Expected: Full build succeeds (client + firmware) + +**Step 2: Check firmware size** + +Run: `cd /home/work/proxmark3 && arm-none-eabi-size armsrc/obj/fullimage.elf` +Expected: text section fits in 512KB flash. Note the size for comparison with upstream. + +**Step 3: Commit (if any Makefile adjustments needed)** + +If build revealed issues, fix and commit. Otherwise, this task is just verification.