The block-3 write-read-back wasn't decisive: writebl and readbl share the address path, so they round-trip regardless of addressing correctness. Since proxmark, Flipper, and pm3py all write the EM4100 header into block 1, ruling out a pm3py 1<->2 read-swap required an addressing-free read. Set MAXBLK=1 so regular-read streams only physical block 1, read it off the air, and it matched READ(1) (non-header BDEF7BC0). Confirmed: the tag is a genuine reverse-order clone, pm3py reads it correctly, no addressing bug. Restored MAXBLK=2. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
94 lines
6.2 KiB
Markdown
94 lines
6.2 KiB
Markdown
# C client oracle + LF demod parity — future-work notes
|
|
|
|
*2026-07-16. Written while finishing the rawcli T5577 catalog and validating on real
|
|
hardware (a T5577 emulating **EM4100 `00FFFFFFFF`**, config word **`00148040`**).*
|
|
|
|
I wanted the stock proxmark3 C client as a ground-truth **oracle** for LF demod — to
|
|
cross-check pm3py's decode of the live tag. It wouldn't run in this environment, and digging
|
|
into why surfaced the notes below. **Net: there is no C-client _code_ bug to PR from what we
|
|
found** — the two real items are (1) an environment blocker and (2) a pm3py-side demod-parity
|
|
gap. Captured here so a future effort doesn't re-derive it.
|
|
|
|
## 1. The C client won't run inside the VS Code snap sandbox (oracle blocker)
|
|
|
|
- **Symptom:** `./pm3 -p /dev/ttyACM0 -c 'lf search'` →
|
|
`symbol lookup error: /snap/core20/current/lib/x86_64-linux-gnu/libpthread.so.0:
|
|
undefined symbol: __libc_pthread_init, version GLIBC_PRIVATE`
|
|
- **Root cause:** this shell runs *inside* the VS Code snap (`SNAP_REVISION`, `SNAP_REAL_HOME`,
|
|
`/snap/code/250/...`). The snap runtime injects core20's older glibc / `libpthread.so.0`
|
|
ahead of the system libs the client was built against, so a `GLIBC_PRIVATE` symbol the
|
|
system libc no longer exports gets looked up in the old snap libpthread and fails.
|
|
- **Confirmed it is NOT a client defect:**
|
|
- The binary has **no RPATH/RUNPATH**; its `NEEDED` libs (`libpython3.12`, `Qt5*`,
|
|
`readline`, …) all resolve to system `/lib/x86_64-linux-gnu` under `ldd`.
|
|
- `LD_LIBRARY_PATH` is **unset**; clearing `LD_LIBRARY_PATH`/`GTK_PATH` did not help — the
|
|
injection is at the snap-runtime level, not via a variable we can scrub.
|
|
- **To use the client as an oracle:** run it from a **non-snap login shell / real terminal**
|
|
(outside VS Code's snap), or build+run it inside a matched (non-snap) environment. No
|
|
proxmark3 change fixes a sandbox library injection.
|
|
- **Marginal upstream idea (only if it ever matters):** the `pm3` launcher could sanitize
|
|
obviously-contaminating library paths before `exec`, but that is a weak, environment-specific
|
|
band-aid — not worth a PR on its own.
|
|
- Client checkout here: `dangerous-tac0s/proxmark3` `v4.20728-1264-g273777b21`.
|
|
|
|
## 2. LF clock detection — pm3py's port was weaker than the C client (pm3py-side parity)
|
|
|
|
We found and fixed a real bug in pm3py `pm3py/lf/dsp.py::detect_clock`: it estimated the bit
|
|
clock from the **minimum** edge interval, so a handful of stray sub-bit edges on a
|
|
strongly-coupled, rail-clipped tag collapsed the estimate (true half-bit 32 → ~3), snapping the
|
|
data clock to 8 and producing an all-phase-error ASK demod (nothing decoded). Fixed to key off
|
|
the **shortest _well-supported_ interval** (≥20% of the modal count) instead of the raw minimum.
|
|
|
|
The C client's `DetectASKClock` (`common/lfdemod.c`) is already robust to this — worth porting
|
|
its approach for full parity:
|
|
|
|
- It first runs `DetectCleanAskWave` and, for clean/strong/**clipped** peaks, routes to a
|
|
dedicated `DetectStrongAskClock` — exactly the case that broke our port.
|
|
- Otherwise it **error-scores every candidate clock** `{8,16,32,40,50,64,100,128,272}` against
|
|
the wave and picks the best fit — not a single-interval heuristic.
|
|
|
|
**Action (pm3py, not a C-client PR):** port the candidate-clock error-scoring + a
|
|
strong/clean-ASK fast path into `pm3py/lf`. Our support-based `detect_clock` fix is a partial,
|
|
targeted step; the C client's scoring is the fuller solution.
|
|
|
|
## 3. Data-block reads — investigated to ground truth (no bug), one real improvement
|
|
|
|
Post-fix, pm3py decodes the live tag as:
|
|
|
|
- Emitted credential: **EM4100 `00FFFFFFFF`**
|
|
- T5577 config block 0: **`00148040`** (Manchester, RF/64, MAXBLK 2, `ST=0`, non-inverted →
|
|
EM4100/EM4102) — decoded rotation-resolved against presets (reliable).
|
|
- Data blocks: `READ(1)=A108421F`, `READ(2)=007FE108`, which at first looked *wrong* (not a
|
|
rotation of the encoder's `FF801EF7`/`BDEF7BC0`).
|
|
|
|
Chased to ground truth against the **ATA5577C datasheet + proxmark3 C source + an on-hardware
|
|
write-read-back** — and there is **no read/addressing bug**:
|
|
|
|
- Datasheet §5.9: the direct-access command (`opcode 10` + `0` + 3-bit addr — exactly what the
|
|
firmware sends) reads *only the addressed block*, repetitively. A clean 32-bit repeat in the
|
|
capture proves block-read mode engaged (the 64-bit emulation stream can't produce one).
|
|
- `T55xx_SetBits` clocks the address MSB-first; pm3py's `readbl` payload matches the firmware
|
|
struct byte-for-byte. Addressing is stock-proxmark, verified end-to-end.
|
|
- A block-3 write-read-back was **not** decisive on its own: `writebl` and `readbl` share the
|
|
same address path, so they round-trip regardless of whether addressing is correct (it only
|
|
proved the demod inverts). Proxmark, Flipper, and pm3py all write the header into block 1, so
|
|
the alternative "pm3py reads 1↔2 swapped" had to be excluded with an **addressing-free** read.
|
|
- **Decisive test (MAXBLK=1):** temporarily set the config MAXBLK field to 1 (`00148040 →
|
|
00148020`) so regular-read streams *only physical block 1* (datasheet §5.11.1), read it off
|
|
the air (no read-command addressing), then restored `00148040`. Physical block 1 came back as
|
|
`~BDEF7BC0` — the **non-header** half — matching `READ(1)`. So `READ(1)` == physical block 1.
|
|
|
|
Conclusion: this tag was simply **cloned with the two EM4100 halves in reverse order**
|
|
(block 1 = `BDEF7BC0`, block 2 = `FF801EF7`) by a non-proxmark/Flipper/pm3py tool; both orders
|
|
stream the same cyclic waveform and read as `00FFFFFFFF`. pm3py reads them correctly. The "wrong"
|
|
appearance was polarity (inversion) + rotation, i.e. the documented "best-effort — bit alignment
|
|
not verified".
|
|
|
|
**Real improvement (open):** `read_t55xx_block` should *resolve* data-block polarity/rotation
|
|
instead of leaving it best-effort. Polarity is determinable — read block 0 (config), resolve its
|
|
polarity against the known preset, and carry that polarity to the data blocks (they share the
|
|
tag's modulation). Rotation can be anchored off the block-read response start. The C client's
|
|
`cmdlft55xx` demod already does this alignment — worth porting alongside §2. Still not
|
|
cross-checked against `lf t55 dump` (client blocked by §1), but the write-read-back makes that
|
|
non-blocking.
|