Files
pm3py/docs/plans/2026-03-17-stateful-table-firmware-plan.md
michael 2a56837935 docs: stateful response table firmware implementation plan
9 tasks: command IDs, iso15_tag_t extensions, sim_crypto, sim_table
header + implementation, appmain dispatch, native handler access
control + random UID, table fallback in default case, full build.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 17:00:42 -07:00

26 KiB

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:

// 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

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):

    // 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

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

//-----------------------------------------------------------------------------
// 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

//-----------------------------------------------------------------------------
// 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 <string.h>

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

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

//-----------------------------------------------------------------------------
// 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

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

//-----------------------------------------------------------------------------
// 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 <string.h>

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

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:

#include "sim_table.h"

Step 2: Add dispatch cases

Find the CMD_HF_SEOS_SIMULATE case (around line 1571). After it, add:

        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

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:

#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:

    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):

            // No error: Answer
            recv[0] = ISO15_NOERROR;
            recv[1] = tag->dsfid;
            memcpy(&recv[2], tag->uid, 8);
            recvLen = 10;

Replace with:

            // 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):

                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:

                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:

                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

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:

                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

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.