From 50c8e8fac4c5ac797316de84c5d7fa2284aa4b2a Mon Sep 17 00:00:00 2001 From: michael Date: Thu, 2 Jul 2026 13:57:12 -0700 Subject: [PATCH 1/7] docs: design for hf mfu esetblk (MFU emulator page set) Co-Authored-By: Claude Opus 4.8 (1M context) --- .../plans/2026-07-02-hf-mfu-esetblk-design.md | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 docs/plans/2026-07-02-hf-mfu-esetblk-design.md diff --git a/docs/plans/2026-07-02-hf-mfu-esetblk-design.md b/docs/plans/2026-07-02-hf-mfu-esetblk-design.md new file mode 100644 index 000000000..fcd114353 --- /dev/null +++ b/docs/plans/2026-07-02-hf-mfu-esetblk-design.md @@ -0,0 +1,86 @@ +# hf mfu esetblk — Design Document + +**Date:** 2026-07-02 +**Branch:** hf-mfu-esetblk +**Goal:** Add `hf mfu esetblk`, mirroring `hf mf esetblk` but for Ultralight/NTAG (MFU) 4-byte page memory, so users can set emulator-memory page(s) directly (for building/patching an emulated UL/NTAG tag without re-running a full `eload`). + +--- + +## Background + +`hf mf esetblk` ([cmdhfmf.c:5216](../../client/src/cmdhfmf.c#L5216), `CmdHF14AMfESet`) sets exactly one 16-byte MIFARE Classic block in emulator memory: it requires precisely 16 hex bytes and calls `mf_elm_set_mem(data, blk, 1)`. + +MFU/NTAG emulator memory is different: 4-byte **pages**, stored in BigBuf as an `mfu_dump_t` — a **56-byte prefix** (version[8], tbo, pages, signature[32], counter_tearing[12]) followed by page data. The firmware sim serves GET_VERSION/READ_SIG from that prefix and READ(0x30)/FAST_READ(0x3A) from the page data at BigBuf offset `page*4 + 56`. + +No `hf mfu esetblk` exists yet — the MFU emulator family is only `eload` / `esave` / `eview` ([cmdhfmfu.c:8082-8084](../../client/src/cmdhfmfu.c#L8082-L8084)). + +The write path is **already width-parameterized**: `mf_eml_set_mem_xt(data, blockNum, count, width)` ([mifarehost.c:1098](../../client/src/mifare/mifarehost.c#L1098)) → `CMD_HF_MIFARE_EML_MEMSET` → device `emlSetMem_xt` (offset = `blockNum * width`). MFU `eload` already calls it with `width = MFU_BLOCK_SIZE (4)`. **So this is a client-only command — no firmware change, no new helper.** + +--- + +## Command + +`hf mfu esetblk` — handler `CmdHF14AMfUeSetBlk` in `cmdhfmfu.c`, registered in the MFU command table next to `eload`/`esave`/`eview`: +```c +{"esetblk", CmdHF14AMfUeSetBlk, IfPm3Iso14443a, "Set emulator memory block"}, +``` + +**Args** (mirror `esetblk`): +- `-b` / `--blk ` — start page (required) +- `-d` / `--data ` — page bytes; length a nonzero multiple of 4 (one page = 4 bytes) + +**Frictionless multi-page:** the page count is inferred from the data length — no extra flag. One page (`-d <8 hex>`) is the common case (`count = 1`); longer data writes consecutive pages from `--blk`. + +--- + +## Semantics / internals + +Write `count = datalen / MFU_BLOCK_SIZE` pages starting at card page `blk`: +```c +mf_eml_set_mem_xt(data, blk + (MFU_DUMP_PREFIX_LENGTH / MFU_BLOCK_SIZE), count, MFU_BLOCK_SIZE); +// page blk + 14 (skip the 56-byte mfu_dump_t prefix) width 4 +``` +The `+14` targets page data past the prefix, exactly as the firmware's own WRITE(0xA2) handler does (`emlSetMem_xt(..., block + MFU_DUMP_PREFIX_LENGTH/4, 1, 4)`). + +Then: `PrintAndLogEx(SUCCESS, "Set %u page(s) from page %u", count, blk)`. + +--- + +## Validation + +- `datalen == 0 || datalen % MFU_BLOCK_SIZE != 0` → `"data must be whole pages (multiples of 4 bytes)"`, `PM3_EINVARG`. +- **Max page 254** (not 255): the live MFU emulator data region is `MFU_MAX_BYTES = MFU_MAX_BLOCKS(0xFF) * MFU_BLOCK_SIZE = 1020` bytes = 255 pages, indices **0–254**. Page 255 (`data[1020..1024]`) is beyond what `esave`/`eview` transfer, so cap so everything set is round-trippable: + ```c + if ((blk + count) * MFU_BLOCK_SIZE > MFU_MAX_BYTES) { // last page must be <= 254 + PrintAndLogEx(WARNING, "page range exceeds emulator memory (max page %u)", + (MFU_MAX_BYTES / MFU_BLOCK_SIZE) - 1); + return PM3_EINVARG; + } + ``` +- Parse `-d` into a buffer sized `MFU_MAX_BYTES` (1020). + +> Note on the constants: `MFU_MAX_BLOCKS = 0xFF` is a page **count**, not a max index; `eview`/`esave` validate `end > MFU_MAX_BLOCKS`, which loosely admits `--end 255` even though only pages 0–254 are live. `esetblk` uses the tighter, correct `MFU_MAX_BYTES` bound. + +--- + +## Reuse (no duplication) + +- `mf_eml_set_mem_xt` — existing width-parameterized emulator-write helper. +- `CMD_HF_MIFARE_EML_MEMSET` / `emlSetMem_xt` — existing device path, unchanged. +- Model the handler on `CmdHF14AMfESet` ([cmdhfmf.c:5216](../../client/src/cmdhfmf.c#L5216)); the deltas are width 4, the `+14` prefix offset, multi-page count, and the max-254 bound. + +--- + +## Testing + +Emulator-memory commands need a connected PM3 but **no card**, and have no offline unit path, so verification is a hardware smoke test: +1. `hf mfu esetblk -b 4 -d 04E10CDA` → `hf mfu eview`, confirm page 4 = `04 E1 0C DA`. +2. Multi-page: `hf mfu esetblk -b 4 -d 04E10CDA993C8048` → `eview`, confirm pages 4-5. +3. Error cases rejected cleanly: odd length (`-d 04E10C`), and out-of-range (`-b 254 -d <16+ bytes>` → would touch page 255). + +--- + +## Out of scope (YAGNI) + +- Setting `mfu_dump_t` prefix fields (version/signature/counters) — use `eload` of a full dump. +- `eget` / `eclr` for MFU. From 86a0f994e579654e6a44c12b0aa3c5762c6eaf17 Mon Sep 17 00:00:00 2001 From: michael Date: Thu, 2 Jul 2026 14:02:28 -0700 Subject: [PATCH 2/7] docs: implementation plan for hf mfu esetblk Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/plans/2026-07-02-hf-mfu-esetblk.md | 134 ++++++++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 docs/plans/2026-07-02-hf-mfu-esetblk.md diff --git a/docs/plans/2026-07-02-hf-mfu-esetblk.md b/docs/plans/2026-07-02-hf-mfu-esetblk.md new file mode 100644 index 000000000..3e9136041 --- /dev/null +++ b/docs/plans/2026-07-02-hf-mfu-esetblk.md @@ -0,0 +1,134 @@ +# hf mfu esetblk Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Add a client-only `hf mfu esetblk` command that writes one or more 4-byte pages into MFU/NTAG emulator memory, mirroring `hf mf esetblk` but for Ultralight page structures. + +**Architecture:** A single new handler `CmdHF14AMfUeSetBlk` in `client/src/cmdhfmfu.c` that parses `--blk`/`--data`, validates whole-page data and the max-254 page bound, and calls the existing width-parameterized helper `mf_eml_set_mem_xt(...)` with `width=4` and a `+14` page offset (to skip the 56-byte `mfu_dump_t` prefix). Registered in the MFU command table. No firmware change, no new helpers. + +**Tech Stack:** C (proxmark3 client), CLIParser arg tables, `mf_eml_set_mem_xt` → `CMD_HF_MIFARE_EML_MEMSET`. Build: `make client`. Design: `docs/plans/2026-07-02-hf-mfu-esetblk-design.md`. + +**Note on testing:** MFU emulator commands are `IfPm3Iso14443a` (device required) and have **no offline unit-test path**. So this plan is not TDD — verification is: clean build, `-h` help renders offline, and an on-device smoke test (PM3 needed, **no card**). The logic is a close structural copy of the already-working `hf mf esetblk`. + +--- + +## Task 1: Implement and register `hf mfu esetblk` + +**Files:** +- Modify: `client/src/cmdhfmfu.c` — add handler `CmdHF14AMfUeSetBlk` (near the other `CmdHF14AMfUe*` handlers) + one command-table row. +- Reference (read, do not modify): `client/src/cmdhfmf.c:5216` (`CmdHF14AMfESet`, the template); `client/src/mifare/mifarehost.c:1098` (`mf_eml_set_mem_xt` signature). + +**Step 1: Confirm the helper is declared for this translation unit.** + +Run: `grep -nE 'mf_eml_set_mem_xt' /home/work/proxmark3/.worktrees/hf-mfu-esetblk/client/src/mifare/mifarehost.h` +Expected: a prototype line. If cmdhfmfu.c does not already include `mifare/mifarehost.h`, add `#include "mifare/mifarehost.h"` with the other includes. (Confirm it compiles in Step 4.) + +Signature to use: +```c +int mf_eml_set_mem_xt(uint8_t *data, int blockNum, int blocksCount, int blockBtWidth); +``` + +**Step 2: Add the handler** in `client/src/cmdhfmfu.c` (place it just above the `CommandTable[]` array, next to `CmdHF14AMfuEView` / `CmdHF14AMfUeLoad`): + +```c +static int CmdHF14AMfUeSetBlk(const char *Cmd) { + CLIParserContext *ctx; + CLIParserInit(&ctx, "hf mfu esetblk", + "Set emulator memory page(s). One page = 4 bytes; pass multiple\n" + "whole pages of data to set consecutive pages from --blk.", + "hf mfu esetblk --blk 4 -d 04E10CDA\n" + "hf mfu esetblk --blk 4 -d 04E10CDA993C8048 -> sets pages 4-5\n" + ); + void *argtable[] = { + arg_param_begin, + arg_int1("b", "blk", "", "page number to start at"), + arg_str0("d", "data", "", "bytes to write, whole pages (multiple of 4 bytes)"), + arg_param_end + }; + CLIExecWithReturn(ctx, Cmd, argtable, false); + + int blk = arg_get_int_def(ctx, 1, 0); + + uint8_t data[MFU_MAX_BYTES] = {0x00}; + int datalen = 0; + int res = CLIParamHexToBuf(arg_get_str(ctx, 2), data, sizeof(data), &datalen); + CLIParserFree(ctx); + if (res) { + PrintAndLogEx(FAILED, "Error parsing bytes"); + return PM3_EINVARG; + } + + if (blk < 0) { + PrintAndLogEx(WARNING, "page number must be positive"); + return PM3_EINVARG; + } + + if (datalen == 0 || (datalen % MFU_BLOCK_SIZE) != 0) { + PrintAndLogEx(WARNING, "data must be whole pages (multiples of %d bytes). Got %i", MFU_BLOCK_SIZE, datalen); + return PM3_EINVARG; + } + + int count = datalen / MFU_BLOCK_SIZE; + + // live MFU emulator data region is MFU_MAX_BYTES (pages 0..254); page 255 is not round-trippable + if ((blk + count) * MFU_BLOCK_SIZE > MFU_MAX_BYTES) { + PrintAndLogEx(WARNING, "page range exceeds emulator memory (max page %u)", (MFU_MAX_BYTES / MFU_BLOCK_SIZE) - 1); + return PM3_EINVARG; + } + + // MFU emulator page data starts after the 56-byte mfu_dump_t prefix, so shift the + // page index by MFU_DUMP_PREFIX_LENGTH/MFU_BLOCK_SIZE (=14), width = MFU_BLOCK_SIZE (4). + res = mf_eml_set_mem_xt(data, blk + (MFU_DUMP_PREFIX_LENGTH / MFU_BLOCK_SIZE), count, MFU_BLOCK_SIZE); + if (res != PM3_SUCCESS) { + PrintAndLogEx(FAILED, "Failed to set emulator memory"); + return res; + } + + PrintAndLogEx(SUCCESS, "Set " _YELLOW_("%d") " page(s) from page " _YELLOW_("%d"), count, blk); + return PM3_SUCCESS; +} +``` + +**Step 3: Register the command** in the MFU `CommandTable[]` (near `eload`/`esave`/`eview`, ~cmdhfmfu.c:8082-8084): + +```c + {"esetblk", CmdHF14AMfUeSetBlk, IfPm3Iso14443a, "Set emulator memory block"}, +``` + +**Step 4: Build.** + +Run: `cd /home/work/proxmark3/.worktrees/hf-mfu-esetblk && make client -j"$(nproc)"` +Expected: clean build, no warnings on cmdhfmfu.c, `client/proxmark3` linked. + +**Step 5: Verify help renders (offline).** + +Run: `QT_QPA_PLATFORM=offscreen ./pm3 -o -c 'hf mfu esetblk -h'` +Expected: usage text with `--blk` and `--data`, no crash. +Also confirm it now appears in the menu: `QT_QPA_PLATFORM=offscreen ./pm3 -o -c 'hf mfu help'` lists `esetblk`. + +**Step 6: Commit.** + +```bash +cd /home/work/proxmark3/.worktrees/hf-mfu-esetblk +git add client/src/cmdhfmfu.c +git commit -m "feat(mfu): add hf mfu esetblk to set emulator memory pages" +``` + +--- + +## Task 2: On-device smoke test (requires a PM3, no card) + +Not automatable here — hand these to the user. Record results in the commit/PR notes. + +1. **Single page:** `hf mfu esetblk -b 4 -d 04E10CDA` → then `hf mfu eview` → page 4 shows `04 E1 0C DA`. +2. **Multi-page:** `hf mfu esetblk -b 4 -d 04E10CDA993C8048` → `eview` → pages 4 and 5 both set. +3. **Odd length rejected:** `hf mfu esetblk -b 4 -d 04E10C` → `"data must be whole pages…"`, no write. +4. **Out of range rejected:** `hf mfu esetblk -b 254 -d 0011223344556677` (2 pages → would touch page 255) → `"page range exceeds emulator memory (max page 254)"`. +5. **Round-trip:** `esetblk` a few pages → `hf mfu esave -f /tmp/t` → reload/compare, confirm the set pages persisted. + +--- + +## YAGNI / out of scope +- Setting `mfu_dump_t` prefix fields (version/signature/counters) — use `eload`. +- `eget`/`eclr` for MFU. +- Any firmware change (none needed). From 48e7ae056b8a423db83fb7e9d089da8ce65e9dc1 Mon Sep 17 00:00:00 2001 From: michael Date: Thu, 2 Jul 2026 14:14:40 -0700 Subject: [PATCH 3/7] feat(mfu): add hf mfu esetblk to set emulator memory pages --- client/src/cmdhfmfu.c | 59 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/client/src/cmdhfmfu.c b/client/src/cmdhfmfu.c index 50220fc24..a705bf71f 100644 --- a/client/src/cmdhfmfu.c +++ b/client/src/cmdhfmfu.c @@ -24,6 +24,7 @@ #include "aes.h" #include "cmdhfmf.h" #include "cmdhf14a.h" +#include "mifare/mifarehost.h" // mf_eml_set_mem_xt #include "comms.h" #include "protocols.h" #include "generator.h" @@ -8053,6 +8054,63 @@ static int CmdHF14AMfUIncr(const char *Cmd) { return PM3_SUCCESS; } +static int CmdHF14AMfUeSetBlk(const char *Cmd) { + CLIParserContext *ctx; + CLIParserInit(&ctx, "hf mfu esetblk", + "Set emulator memory page(s). One page = 4 bytes; pass multiple\n" + "whole pages of data to set consecutive pages from --blk.", + "hf mfu esetblk --blk 4 -d 04E10CDA\n" + "hf mfu esetblk --blk 4 -d 04E10CDA993C8048 -> sets pages 4-5\n" + ); + void *argtable[] = { + arg_param_begin, + arg_int1("b", "blk", "", "page number to start at"), + arg_str0("d", "data", "", "bytes to write, whole pages (multiple of 4 bytes)"), + arg_param_end + }; + CLIExecWithReturn(ctx, Cmd, argtable, false); + + int blk = arg_get_int_def(ctx, 1, 0); + + uint8_t data[MFU_MAX_BYTES] = {0x00}; + int datalen = 0; + int res = CLIParamHexToBuf(arg_get_str(ctx, 2), data, sizeof(data), &datalen); + CLIParserFree(ctx); + if (res) { + PrintAndLogEx(FAILED, "Error parsing bytes"); + return PM3_EINVARG; + } + + if (blk < 0) { + PrintAndLogEx(WARNING, "page number must be positive"); + return PM3_EINVARG; + } + + if (datalen == 0 || (datalen % MFU_BLOCK_SIZE) != 0) { + PrintAndLogEx(WARNING, "data must be whole pages (multiples of %d bytes). Got %i", MFU_BLOCK_SIZE, datalen); + return PM3_EINVARG; + } + + int count = datalen / MFU_BLOCK_SIZE; + + // live MFU emulator data region is MFU_MAX_BYTES (pages 0..254); page 255 is not round-trippable + if ((blk + count) * MFU_BLOCK_SIZE > MFU_MAX_BYTES) { + PrintAndLogEx(WARNING, "page range exceeds emulator memory (max page %u)", (MFU_MAX_BYTES / MFU_BLOCK_SIZE) - 1); + return PM3_EINVARG; + } + + // MFU emulator page data starts after the 56-byte mfu_dump_t prefix, so shift the + // page index by MFU_DUMP_PREFIX_LENGTH/MFU_BLOCK_SIZE (=14), width = MFU_BLOCK_SIZE (4). + res = mf_eml_set_mem_xt(data, blk + (MFU_DUMP_PREFIX_LENGTH / MFU_BLOCK_SIZE), count, MFU_BLOCK_SIZE); + if (res != PM3_SUCCESS) { + PrintAndLogEx(FAILED, "Failed to set emulator memory"); + return res; + } + + PrintAndLogEx(SUCCESS, "Set " _YELLOW_("%d") " page(s) from page " _YELLOW_("%d"), count, blk); + return PM3_SUCCESS; +} + static command_t CommandTable[] = { {"help", CmdHelp, AlwaysAvailable, "This help"}, {"list", CmdHF14AMfuList, AlwaysAvailable, "List MIFARE Ultralight / NTAG history"}, @@ -8082,6 +8140,7 @@ static command_t CommandTable[] = { {"eload", CmdHF14AMfUeLoad, IfPm3Iso14443a, "Upload file into emulator memory"}, {"esave", CmdHF14AMfuESave, IfPm3Iso14443a, "Save emulator memory to file"}, {"eview", CmdHF14AMfuEView, IfPm3Iso14443a, "View emulator memory"}, + {"esetblk", CmdHF14AMfUeSetBlk, IfPm3Iso14443a, "Set emulator memory block"}, {"sim", CmdHF14AMfUSim, IfPm3Iso14443a, "Simulate MIFARE Ultralight from emulator memory"}, {"-----------", CmdHelp, IfPm3Iso14443a, "----------------------- " _CYAN_("magic") " ----------------------------"}, {"setuid", CmdHF14AMfUCSetUid, IfPm3Iso14443a, "Set UID - MAGIC tags only"}, From 38a05621d4f38dafb42427c709a7bea5776cb96e Mon Sep 17 00:00:00 2001 From: michael Date: Thu, 2 Jul 2026 15:03:48 -0700 Subject: [PATCH 4/7] fix(mfu): reject esetblk data larger than one command buffer --- client/src/cmdhfmfu.c | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/client/src/cmdhfmfu.c b/client/src/cmdhfmfu.c index a705bf71f..f4cc81394 100644 --- a/client/src/cmdhfmfu.c +++ b/client/src/cmdhfmfu.c @@ -8099,6 +8099,14 @@ static int CmdHF14AMfUeSetBlk(const char *Cmd) { return PM3_EINVARG; } + // one esetblk is a single CMD_HF_MIFARE_EML_MEMSET; its payload (data + a 4-byte + // header) must fit the command buffer. Larger sets should use `hf mfu eload`. + if (datalen > (int)(PM3_CMD_DATA_SIZE - 4)) { + PrintAndLogEx(WARNING, "too many pages for one command: max %d pages (%d bytes). Use " _YELLOW_("`hf mfu eload`") " for larger sets", + (int)((PM3_CMD_DATA_SIZE - 4) / MFU_BLOCK_SIZE), (int)(PM3_CMD_DATA_SIZE - 4)); + return PM3_EINVARG; + } + // MFU emulator page data starts after the 56-byte mfu_dump_t prefix, so shift the // page index by MFU_DUMP_PREFIX_LENGTH/MFU_BLOCK_SIZE (=14), width = MFU_BLOCK_SIZE (4). res = mf_eml_set_mem_xt(data, blk + (MFU_DUMP_PREFIX_LENGTH / MFU_BLOCK_SIZE), count, MFU_BLOCK_SIZE); From fd5ad5510446fa78d977633535efe746193431b2 Mon Sep 17 00:00:00 2001 From: michael Date: Thu, 2 Jul 2026 15:23:07 -0700 Subject: [PATCH 5/7] fix(sim): serve NTAG PACK live from emulator memory in PWD_AUTH Co-Authored-By: Claude Opus 4.8 --- armsrc/iso14443a.c | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/armsrc/iso14443a.c b/armsrc/iso14443a.c index 6ff96ec5b..3010167fb 100644 --- a/armsrc/iso14443a.c +++ b/armsrc/iso14443a.c @@ -2354,7 +2354,32 @@ void SimulateIso14443aTagEx(uint8_t tagType, uint16_t flags, uint8_t *useruid, u if (memcmp(receivedCmd + 1, pwd, 4) == 0) { if (g_dbglevel >= DBG_DEBUG) Dbprintf("Password match, responding with PACK."); - p_response = &responses[RESP_INDEX_PACK]; + + // Serve PACK live from the PACK page (page `pages`) so a running-sim esetblk/eload + // of the PACK page takes effect without restarting the sim (page reads & PWD are + // already served live). Falls back to the same bytes as before for a static sim. + uint8_t pack_live[4] = {0, 0, 0, 0}; + emlGet(pack_live, pages * 4 + MFU_DUMP_PREFIX_LENGTH, 2); + + // Preserve init-time Amiibo behavior: if the stored PWD is the Amiibo-generated + // password, PACK is 0x8080. Keep in sync with the sim-init override (~L1650). + // NOTE: Be encoding here intentionally matches init; the pwd substitution above + // (Uint4byteToMemLe) is a separate pre-existing path and must stay Le. + uint8_t stored_pwd[4] = {0, 0, 0, 0}; + uint8_t gen_pwd[4] = {0, 0, 0, 0}; + emlGet(stored_pwd, (pages - 1) * 4 + MFU_DUMP_PREFIX_LENGTH, sizeof(stored_pwd)); + Uint4byteToMemBe(gen_pwd, ul_ev1_pwdgenB(useruid)); + if (memcmp(stored_pwd, gen_pwd, sizeof(stored_pwd)) == 0) { + pack_live[0] = 0x80; + pack_live[1] = 0x80; + } + + dynamic_response_info.response[0] = pack_live[0]; + dynamic_response_info.response[1] = pack_live[1]; + AddCrc14A(dynamic_response_info.response, 2); + dynamic_response_info.response_n = 4; // 2 PACK + 2 CRC + prepare_tag_modulation(&dynamic_response_info, DYNAMIC_MODULATION_BUFFER_SIZE); + p_response = &dynamic_response_info; } else { if (g_dbglevel >= DBG_DEBUG) Dbprintf("Password did not match, NACK_IV."); p_response = NULL; From b30bde22f648b4d6cd46010ff84908ced84de67d Mon Sep 17 00:00:00 2001 From: michael Date: Thu, 2 Jul 2026 16:44:27 -0700 Subject: [PATCH 6/7] fix(sim): load Amiibo PWD/PACK into emulator memory at init, serve from memory For tagType 7, when the PWD page is unset (0000) the UID-derived Amiibo password and its PACK (0x8080) are now written into emulator memory at sim init, and the PWD_AUTH path reads PWD and PACK straight from emulator memory. Removes the auth-time pwd substitution and the PACK 0x8080 override, so eset/eview are authoritative (what's in memory is what the sim serves) while Amiibo sims still work. Co-Authored-By: Claude Opus 4.8 --- armsrc/iso14443a.c | 46 +++++++++++++++++----------------------------- 1 file changed, 17 insertions(+), 29 deletions(-) diff --git a/armsrc/iso14443a.c b/armsrc/iso14443a.c index 3010167fb..e631dc9b4 100644 --- a/armsrc/iso14443a.c +++ b/armsrc/iso14443a.c @@ -1644,18 +1644,21 @@ bool SimulateIso14443aInit(uint8_t tagType, uint16_t flags, uint8_t *data, AddCrc14A(rPPS, sizeof(rPPS) - 2); - // EV1/NTAG, set PWD w AMIIBO algo if all zero. + // EV1/NTAG Amiibo: if no PWD is set, populate emulator memory with the UID-derived + // password and its PACK (0x8080) at the PWD/PACK pages, so the sim serves them from + // emulator memory like any other value. Auth and PACK are then read straight from + // memory, keeping eset/eview authoritative (what you set is what the sim serves). if (tagType == 7) { uint8_t pwd[4] = {0, 0, 0, 0}; - uint8_t gen_pwd[4] = {0, 0, 0, 0}; emlGet(pwd, (*pages - 1) * 4 + MFU_DUMP_PREFIX_LENGTH, sizeof(pwd)); - emlGet(rPACK, (*pages) * 4 + MFU_DUMP_PREFIX_LENGTH, sizeof(rPACK)); - - Uint4byteToMemBe(gen_pwd, ul_ev1_pwdgenB(data)); - if (memcmp(pwd, gen_pwd, sizeof(pwd)) == 0) { - rPACK[0] = 0x80; - rPACK[1] = 0x80; + if (memcmp(pwd, "\x00\x00\x00\x00", 4) == 0) { + uint8_t amiibo_pwd[4] = {0, 0, 0, 0}; + Uint4byteToMemLe(amiibo_pwd, ul_ev1_pwdgenB(data)); + emlSet(amiibo_pwd, (*pages - 1) * 4 + MFU_DUMP_PREFIX_LENGTH, sizeof(amiibo_pwd)); + uint8_t amiibo_pack[4] = {0x80, 0x80, 0x00, 0x00}; + emlSet(amiibo_pack, (*pages) * 4 + MFU_DUMP_PREFIX_LENGTH, sizeof(amiibo_pack)); } + emlGet(rPACK, (*pages) * 4 + MFU_DUMP_PREFIX_LENGTH, sizeof(rPACK)); } AddCrc14A(rPACK, sizeof(rPACK) - 2); @@ -2347,33 +2350,18 @@ void SimulateIso14443aTagEx(uint8_t tagType, uint16_t flags, uint8_t *useruid, u Dbhexdump(4, pwd, 0); } - if (memcmp(pwd, "\x00\x00\x00\x00", 4) == 0) { - Uint4byteToMemLe(pwd, ul_ev1_pwdgenB(useruid)); - if (g_dbglevel >= DBG_DEBUG) Dbprintf("Calc pwd... %02X %02X %02X %02X", pwd[0], pwd[1], pwd[2], pwd[3]); - } - + // PWD comes straight from emulator memory (read above). For an unset (all-zero) + // PWD, the Amiibo password is populated into memory at sim init, so there's no + // substitution here -- eset/eview stay authoritative. if (memcmp(receivedCmd + 1, pwd, 4) == 0) { if (g_dbglevel >= DBG_DEBUG) Dbprintf("Password match, responding with PACK."); - // Serve PACK live from the PACK page (page `pages`) so a running-sim esetblk/eload - // of the PACK page takes effect without restarting the sim (page reads & PWD are - // already served live). Falls back to the same bytes as before for a static sim. + // Serve PACK straight from the PACK page (page `pages`) in emulator memory, so + // eset/eload of the PACK page is honored live. The Amiibo PACK (0x8080) for an + // unset PWD is populated into memory at sim init, so nothing special-cases here. uint8_t pack_live[4] = {0, 0, 0, 0}; emlGet(pack_live, pages * 4 + MFU_DUMP_PREFIX_LENGTH, 2); - // Preserve init-time Amiibo behavior: if the stored PWD is the Amiibo-generated - // password, PACK is 0x8080. Keep in sync with the sim-init override (~L1650). - // NOTE: Be encoding here intentionally matches init; the pwd substitution above - // (Uint4byteToMemLe) is a separate pre-existing path and must stay Le. - uint8_t stored_pwd[4] = {0, 0, 0, 0}; - uint8_t gen_pwd[4] = {0, 0, 0, 0}; - emlGet(stored_pwd, (pages - 1) * 4 + MFU_DUMP_PREFIX_LENGTH, sizeof(stored_pwd)); - Uint4byteToMemBe(gen_pwd, ul_ev1_pwdgenB(useruid)); - if (memcmp(stored_pwd, gen_pwd, sizeof(stored_pwd)) == 0) { - pack_live[0] = 0x80; - pack_live[1] = 0x80; - } - dynamic_response_info.response[0] = pack_live[0]; dynamic_response_info.response[1] = pack_live[1]; AddCrc14A(dynamic_response_info.response, 2); From f4214a0a83642c80cef12b112923e4d1c54f113c Mon Sep 17 00:00:00 2001 From: michael Date: Thu, 2 Jul 2026 18:11:43 -0700 Subject: [PATCH 7/7] Removing unecessary plans --- .../plans/2026-07-02-hf-mfu-esetblk-design.md | 86 ----------- docs/plans/2026-07-02-hf-mfu-esetblk.md | 134 ------------------ 2 files changed, 220 deletions(-) delete mode 100644 docs/plans/2026-07-02-hf-mfu-esetblk-design.md delete mode 100644 docs/plans/2026-07-02-hf-mfu-esetblk.md diff --git a/docs/plans/2026-07-02-hf-mfu-esetblk-design.md b/docs/plans/2026-07-02-hf-mfu-esetblk-design.md deleted file mode 100644 index fcd114353..000000000 --- a/docs/plans/2026-07-02-hf-mfu-esetblk-design.md +++ /dev/null @@ -1,86 +0,0 @@ -# hf mfu esetblk — Design Document - -**Date:** 2026-07-02 -**Branch:** hf-mfu-esetblk -**Goal:** Add `hf mfu esetblk`, mirroring `hf mf esetblk` but for Ultralight/NTAG (MFU) 4-byte page memory, so users can set emulator-memory page(s) directly (for building/patching an emulated UL/NTAG tag without re-running a full `eload`). - ---- - -## Background - -`hf mf esetblk` ([cmdhfmf.c:5216](../../client/src/cmdhfmf.c#L5216), `CmdHF14AMfESet`) sets exactly one 16-byte MIFARE Classic block in emulator memory: it requires precisely 16 hex bytes and calls `mf_elm_set_mem(data, blk, 1)`. - -MFU/NTAG emulator memory is different: 4-byte **pages**, stored in BigBuf as an `mfu_dump_t` — a **56-byte prefix** (version[8], tbo, pages, signature[32], counter_tearing[12]) followed by page data. The firmware sim serves GET_VERSION/READ_SIG from that prefix and READ(0x30)/FAST_READ(0x3A) from the page data at BigBuf offset `page*4 + 56`. - -No `hf mfu esetblk` exists yet — the MFU emulator family is only `eload` / `esave` / `eview` ([cmdhfmfu.c:8082-8084](../../client/src/cmdhfmfu.c#L8082-L8084)). - -The write path is **already width-parameterized**: `mf_eml_set_mem_xt(data, blockNum, count, width)` ([mifarehost.c:1098](../../client/src/mifare/mifarehost.c#L1098)) → `CMD_HF_MIFARE_EML_MEMSET` → device `emlSetMem_xt` (offset = `blockNum * width`). MFU `eload` already calls it with `width = MFU_BLOCK_SIZE (4)`. **So this is a client-only command — no firmware change, no new helper.** - ---- - -## Command - -`hf mfu esetblk` — handler `CmdHF14AMfUeSetBlk` in `cmdhfmfu.c`, registered in the MFU command table next to `eload`/`esave`/`eview`: -```c -{"esetblk", CmdHF14AMfUeSetBlk, IfPm3Iso14443a, "Set emulator memory block"}, -``` - -**Args** (mirror `esetblk`): -- `-b` / `--blk ` — start page (required) -- `-d` / `--data ` — page bytes; length a nonzero multiple of 4 (one page = 4 bytes) - -**Frictionless multi-page:** the page count is inferred from the data length — no extra flag. One page (`-d <8 hex>`) is the common case (`count = 1`); longer data writes consecutive pages from `--blk`. - ---- - -## Semantics / internals - -Write `count = datalen / MFU_BLOCK_SIZE` pages starting at card page `blk`: -```c -mf_eml_set_mem_xt(data, blk + (MFU_DUMP_PREFIX_LENGTH / MFU_BLOCK_SIZE), count, MFU_BLOCK_SIZE); -// page blk + 14 (skip the 56-byte mfu_dump_t prefix) width 4 -``` -The `+14` targets page data past the prefix, exactly as the firmware's own WRITE(0xA2) handler does (`emlSetMem_xt(..., block + MFU_DUMP_PREFIX_LENGTH/4, 1, 4)`). - -Then: `PrintAndLogEx(SUCCESS, "Set %u page(s) from page %u", count, blk)`. - ---- - -## Validation - -- `datalen == 0 || datalen % MFU_BLOCK_SIZE != 0` → `"data must be whole pages (multiples of 4 bytes)"`, `PM3_EINVARG`. -- **Max page 254** (not 255): the live MFU emulator data region is `MFU_MAX_BYTES = MFU_MAX_BLOCKS(0xFF) * MFU_BLOCK_SIZE = 1020` bytes = 255 pages, indices **0–254**. Page 255 (`data[1020..1024]`) is beyond what `esave`/`eview` transfer, so cap so everything set is round-trippable: - ```c - if ((blk + count) * MFU_BLOCK_SIZE > MFU_MAX_BYTES) { // last page must be <= 254 - PrintAndLogEx(WARNING, "page range exceeds emulator memory (max page %u)", - (MFU_MAX_BYTES / MFU_BLOCK_SIZE) - 1); - return PM3_EINVARG; - } - ``` -- Parse `-d` into a buffer sized `MFU_MAX_BYTES` (1020). - -> Note on the constants: `MFU_MAX_BLOCKS = 0xFF` is a page **count**, not a max index; `eview`/`esave` validate `end > MFU_MAX_BLOCKS`, which loosely admits `--end 255` even though only pages 0–254 are live. `esetblk` uses the tighter, correct `MFU_MAX_BYTES` bound. - ---- - -## Reuse (no duplication) - -- `mf_eml_set_mem_xt` — existing width-parameterized emulator-write helper. -- `CMD_HF_MIFARE_EML_MEMSET` / `emlSetMem_xt` — existing device path, unchanged. -- Model the handler on `CmdHF14AMfESet` ([cmdhfmf.c:5216](../../client/src/cmdhfmf.c#L5216)); the deltas are width 4, the `+14` prefix offset, multi-page count, and the max-254 bound. - ---- - -## Testing - -Emulator-memory commands need a connected PM3 but **no card**, and have no offline unit path, so verification is a hardware smoke test: -1. `hf mfu esetblk -b 4 -d 04E10CDA` → `hf mfu eview`, confirm page 4 = `04 E1 0C DA`. -2. Multi-page: `hf mfu esetblk -b 4 -d 04E10CDA993C8048` → `eview`, confirm pages 4-5. -3. Error cases rejected cleanly: odd length (`-d 04E10C`), and out-of-range (`-b 254 -d <16+ bytes>` → would touch page 255). - ---- - -## Out of scope (YAGNI) - -- Setting `mfu_dump_t` prefix fields (version/signature/counters) — use `eload` of a full dump. -- `eget` / `eclr` for MFU. diff --git a/docs/plans/2026-07-02-hf-mfu-esetblk.md b/docs/plans/2026-07-02-hf-mfu-esetblk.md deleted file mode 100644 index 3e9136041..000000000 --- a/docs/plans/2026-07-02-hf-mfu-esetblk.md +++ /dev/null @@ -1,134 +0,0 @@ -# hf mfu esetblk Implementation Plan - -> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. - -**Goal:** Add a client-only `hf mfu esetblk` command that writes one or more 4-byte pages into MFU/NTAG emulator memory, mirroring `hf mf esetblk` but for Ultralight page structures. - -**Architecture:** A single new handler `CmdHF14AMfUeSetBlk` in `client/src/cmdhfmfu.c` that parses `--blk`/`--data`, validates whole-page data and the max-254 page bound, and calls the existing width-parameterized helper `mf_eml_set_mem_xt(...)` with `width=4` and a `+14` page offset (to skip the 56-byte `mfu_dump_t` prefix). Registered in the MFU command table. No firmware change, no new helpers. - -**Tech Stack:** C (proxmark3 client), CLIParser arg tables, `mf_eml_set_mem_xt` → `CMD_HF_MIFARE_EML_MEMSET`. Build: `make client`. Design: `docs/plans/2026-07-02-hf-mfu-esetblk-design.md`. - -**Note on testing:** MFU emulator commands are `IfPm3Iso14443a` (device required) and have **no offline unit-test path**. So this plan is not TDD — verification is: clean build, `-h` help renders offline, and an on-device smoke test (PM3 needed, **no card**). The logic is a close structural copy of the already-working `hf mf esetblk`. - ---- - -## Task 1: Implement and register `hf mfu esetblk` - -**Files:** -- Modify: `client/src/cmdhfmfu.c` — add handler `CmdHF14AMfUeSetBlk` (near the other `CmdHF14AMfUe*` handlers) + one command-table row. -- Reference (read, do not modify): `client/src/cmdhfmf.c:5216` (`CmdHF14AMfESet`, the template); `client/src/mifare/mifarehost.c:1098` (`mf_eml_set_mem_xt` signature). - -**Step 1: Confirm the helper is declared for this translation unit.** - -Run: `grep -nE 'mf_eml_set_mem_xt' /home/work/proxmark3/.worktrees/hf-mfu-esetblk/client/src/mifare/mifarehost.h` -Expected: a prototype line. If cmdhfmfu.c does not already include `mifare/mifarehost.h`, add `#include "mifare/mifarehost.h"` with the other includes. (Confirm it compiles in Step 4.) - -Signature to use: -```c -int mf_eml_set_mem_xt(uint8_t *data, int blockNum, int blocksCount, int blockBtWidth); -``` - -**Step 2: Add the handler** in `client/src/cmdhfmfu.c` (place it just above the `CommandTable[]` array, next to `CmdHF14AMfuEView` / `CmdHF14AMfUeLoad`): - -```c -static int CmdHF14AMfUeSetBlk(const char *Cmd) { - CLIParserContext *ctx; - CLIParserInit(&ctx, "hf mfu esetblk", - "Set emulator memory page(s). One page = 4 bytes; pass multiple\n" - "whole pages of data to set consecutive pages from --blk.", - "hf mfu esetblk --blk 4 -d 04E10CDA\n" - "hf mfu esetblk --blk 4 -d 04E10CDA993C8048 -> sets pages 4-5\n" - ); - void *argtable[] = { - arg_param_begin, - arg_int1("b", "blk", "", "page number to start at"), - arg_str0("d", "data", "", "bytes to write, whole pages (multiple of 4 bytes)"), - arg_param_end - }; - CLIExecWithReturn(ctx, Cmd, argtable, false); - - int blk = arg_get_int_def(ctx, 1, 0); - - uint8_t data[MFU_MAX_BYTES] = {0x00}; - int datalen = 0; - int res = CLIParamHexToBuf(arg_get_str(ctx, 2), data, sizeof(data), &datalen); - CLIParserFree(ctx); - if (res) { - PrintAndLogEx(FAILED, "Error parsing bytes"); - return PM3_EINVARG; - } - - if (blk < 0) { - PrintAndLogEx(WARNING, "page number must be positive"); - return PM3_EINVARG; - } - - if (datalen == 0 || (datalen % MFU_BLOCK_SIZE) != 0) { - PrintAndLogEx(WARNING, "data must be whole pages (multiples of %d bytes). Got %i", MFU_BLOCK_SIZE, datalen); - return PM3_EINVARG; - } - - int count = datalen / MFU_BLOCK_SIZE; - - // live MFU emulator data region is MFU_MAX_BYTES (pages 0..254); page 255 is not round-trippable - if ((blk + count) * MFU_BLOCK_SIZE > MFU_MAX_BYTES) { - PrintAndLogEx(WARNING, "page range exceeds emulator memory (max page %u)", (MFU_MAX_BYTES / MFU_BLOCK_SIZE) - 1); - return PM3_EINVARG; - } - - // MFU emulator page data starts after the 56-byte mfu_dump_t prefix, so shift the - // page index by MFU_DUMP_PREFIX_LENGTH/MFU_BLOCK_SIZE (=14), width = MFU_BLOCK_SIZE (4). - res = mf_eml_set_mem_xt(data, blk + (MFU_DUMP_PREFIX_LENGTH / MFU_BLOCK_SIZE), count, MFU_BLOCK_SIZE); - if (res != PM3_SUCCESS) { - PrintAndLogEx(FAILED, "Failed to set emulator memory"); - return res; - } - - PrintAndLogEx(SUCCESS, "Set " _YELLOW_("%d") " page(s) from page " _YELLOW_("%d"), count, blk); - return PM3_SUCCESS; -} -``` - -**Step 3: Register the command** in the MFU `CommandTable[]` (near `eload`/`esave`/`eview`, ~cmdhfmfu.c:8082-8084): - -```c - {"esetblk", CmdHF14AMfUeSetBlk, IfPm3Iso14443a, "Set emulator memory block"}, -``` - -**Step 4: Build.** - -Run: `cd /home/work/proxmark3/.worktrees/hf-mfu-esetblk && make client -j"$(nproc)"` -Expected: clean build, no warnings on cmdhfmfu.c, `client/proxmark3` linked. - -**Step 5: Verify help renders (offline).** - -Run: `QT_QPA_PLATFORM=offscreen ./pm3 -o -c 'hf mfu esetblk -h'` -Expected: usage text with `--blk` and `--data`, no crash. -Also confirm it now appears in the menu: `QT_QPA_PLATFORM=offscreen ./pm3 -o -c 'hf mfu help'` lists `esetblk`. - -**Step 6: Commit.** - -```bash -cd /home/work/proxmark3/.worktrees/hf-mfu-esetblk -git add client/src/cmdhfmfu.c -git commit -m "feat(mfu): add hf mfu esetblk to set emulator memory pages" -``` - ---- - -## Task 2: On-device smoke test (requires a PM3, no card) - -Not automatable here — hand these to the user. Record results in the commit/PR notes. - -1. **Single page:** `hf mfu esetblk -b 4 -d 04E10CDA` → then `hf mfu eview` → page 4 shows `04 E1 0C DA`. -2. **Multi-page:** `hf mfu esetblk -b 4 -d 04E10CDA993C8048` → `eview` → pages 4 and 5 both set. -3. **Odd length rejected:** `hf mfu esetblk -b 4 -d 04E10C` → `"data must be whole pages…"`, no write. -4. **Out of range rejected:** `hf mfu esetblk -b 254 -d 0011223344556677` (2 pages → would touch page 255) → `"page range exceeds emulator memory (max page 254)"`. -5. **Round-trip:** `esetblk` a few pages → `hf mfu esave -f /tmp/t` → reload/compare, confirm the set pages persisted. - ---- - -## YAGNI / out of scope -- Setting `mfu_dump_t` prefix fields (version/signature/counters) — use `eload`. -- `eget`/`eclr` for MFU. -- Any firmware change (none needed).