`hf mfdes chk` reported "No keys found" on a card whose PICC master key was
still the factory default, and missed a key handed to it on the command line.
The AID list comes from GetApplicationIDs, which never returns 000000, so the
PICC level was never swept - only an explicit `--aid 000000` reached it. That
list is itself gated by PICC key settings bit 1, and a card refusing it aborted
the whole run rather than falling back to the one key number still reachable.
000000 now seeds the sweep, and a refused list leaves the PICC level to check.
A key given with `--key` was written into the key list at parse time and then
overwritten: every round resets the length and refills the same buffer from
index 0. It was counted in "Loaded N keys" and never tried. It is now
re-inserted at the head of its list after each fill, so it is attempt one.
When key settings are unreadable the fallback walked the file access rights to
name the key numbers in use. GetFileIDs, GetISOFileIDs and GetFileSettings are
gated by the same settings bit that just refused GetKeySettings, so that
fallback could never run. GetKeyVersion is not gated at all and names the key numbers
directly, absent ones answering 0x40 NO_SUCH_KEY.
AuthenticateISO (0x1A) was read only when Authenticate (0x0A) had answered
first, but 0x1A covers DES, 2TDEA and 3TDEA where 0x0A covers only the first
two. An application holding 3TDEA keys answers 0x1A alone, so it named no key
type and was skipped outright. chk and detect both read it on its own now.
Because 0x1A cannot separate 2TDEA from 3TDEA, detect also drops a key type on
the first wrong-length challenge instead of waiting for its error counter.
Measured on a DESFire EV2 2K: PICC master key and an application AES key are
both recovered in one run, and the unauthenticated GetKeyVersion sweep keeps
answering across a run of NO_SUCH_KEY errors with no re-select.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The dump viewer printed bare AID numbers while the live card paths
already resolved them through AIDDFGetCommentStr(). Use the same lookup
here, falling back to the old separator line when the AID is unknown.
--- AID 578000 --- NORTIC (Card Issuer App) [NPRA]
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
NORTIC (Norwegian Ticketing Interoperable Concept) is the Norwegian
national public transport card, on DESFire since 2008 and specified by
Statens vegvesen, now Jernbanedirektoratet, in Handbok V821.
Decodes the card issuer header, file 0C of AID 578000. That file is
free to read, so the parser produces output on any Norwegian card
without key material: country code, format, choice bitmap, card serial
number, validity end date, application owner company, retailer
organisation and card key version.
The transport application, AID 578001, is listed by file with its role
and the key it needs, and any file that was read is dumped as hex. Its
field layouts are in Handbok V821 Del 18, which has no download link,
and every file needs read key 7, which is not public. Nothing there is
guessed at.
NORTIC is EN 1545 and packs fields most significant bit first, which is
the opposite of the RKF Type CL-1 layout in parserkf.c where RKF-0022
7.4.1 numbers bits from the least significant end. Reading a NORTIC
header the RKF way gives country code 144 instead of 578, so the self
test asserts that as a negative control.
Verified field by field against the published card issuer header, every
field matching its documented value, including the validity date 6938
decoding to 2015-12-31. 'hf mfdes view --selftest' runs the checks,
including a round trip through an encoder.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The RKF travel card (Resekortsföreningen i Norden) is the Nordic public
transport card, specified 2001-2002 on MIFARE Classic and deployed by
Västtrafik, SL Stockholm, Länstrafiken and the Danish Rejsekort.
Decodes the card issuer and support layers (CMI, TCCI, TCAS, TCDI) and
every application object the specification pins down: TCPU purse, TCEL
event log, TCDB, TCCP, TCST, and TCTI/TCCO through an identifier chain
walker covering all 18 data element groups. AID owners are named from
RKF-0019.
Three things the specification does not state, determined from 21 cards:
- RKF-0022 7.7.1 calls the block checksum 'standard CRC-8' without
naming the parameters. It is CRC-8/MIFARE-MAD, poly 0x1D init 0xC7,
which the tree already ships as CRC8Mad().
- The MAC trio is the final 24 bits of a MACed object. The RKF-0023
tables list Free (bits) as a trailing summary row, which reads as if
the authenticator follows a gap after the key identifier; it does
not. Measured on the fact that MACAlgorithmIdentifier must be 0:
TCST 42/42 against 15/42, TCCP 20/20 against 13/20.
- TCPU version 4 drops EndDate and moves Value to bit 16.
Cards reporting a version other than 2 are flagged, and a MAC algorithm
the specification does not define is reported as a layout failure rather
than printed as data.
MAC values are not verified. The master key is in RKF-0018, which was
never published. des_mac() is added to libpcrypto for when that changes,
with the FIPS 113 worked example as its test.
Verified against a Danish Rejsekort whose provenance notes give the
purchase timestamp, card number and balance; the parser reproduces all
three. 48 self test checks on 'hf mf view --selftest', no false
positives across 460 local dumps.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
iso14443a_select_cardEx() only rejected silence after RATS. A card that
answered with a 4 bit NAK came back as Demod.len == 1, was stored as a
one byte ATS and reported to the client as select status 1, meaning L4
activated. The client then sent GetVersion (90 60 00 00 00) as an I-block
to a card that never left ISO14443-3, and every caller ended with
'timeout while waiting for reply' / 'No tag detected or other tag
communication error (-4)'.
The bogus TL of 0x04 also made iso14a_set_ATS_times() look for a T0 and
read past the single received byte.
Check that the answer to RATS is a well formed ATS, TL plus the bytes TL
counts plus the two CRC bytes, before accepting it. Anything else returns
2, selected but ISO14443-3 only, so the card stays usable over the Classic
path instead of failing detection.
The client side 'try to request ATS even if the tag claims not to support
it' retry had the same hole and stored whatever came back in card.ats, so
guard all four copies of it the same way.
Reported on a clone with ATQA 0004 / SAK 28 that advertises ISO14443-4 it
does not implement. hf mf autopwn on it needed hf 14a config --rats skip
to get past detection; with this it takes the Classic path on its own.
Fixes#3659
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
restore refused UL-AES outright. The generic page plan assumes the last
four pages are cfg0/cfg1/PWD/PACK, which on UL-AES are the AES keys, so
the data loop walked over the lock page, both config pages and the key
lock bits at 0x2D. Give UL-AES its own plan: user memory 0x04..0x27,
and lock/cfg0/cfg1 behind -s. Key pages 0x30..0x37 are left alone, the
dump only ever carries AESKey0 and 0x34..0x37 read back as zeros.
info never printed the UL-AES config. ulaes_print_configuration() is
called with 0x29 but its first branch tested for 0x2C, so cfg0 and cfg1
fell through to the bare return and only the CMAC page showed up.
info also read page 0x34 as if it were an EV1 config page. That is the
UIDRetrKey. Genuine silicon NAKs the read, but a magic or simulated
card would have had its key printed as cfg0/cfg1/PWD/PACK.
Tested on a UL-AES with AUTH0=4, PROT set and secure messaging on.
Restore of an unmodified dump comes back byte identical; a dump carrying
DEADBEEF at 0x27 and CAFEBABE at 0x2B writes 0x27 and leaves 0x2B alone.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Transcode the MinGW client's CP850 output before writing command documentation and force Python to read the converted stream as UTF-8. Normalize the remaining escaped Greek mu strings to the project's micro sign convention.
Co-Authored-By: OpenAI Codex <noreply@openai.com>
print_iclass_sio_decoded() silently dropped any TLV tag it did not
explicitly handle. Real credentials (e.g. SR) carry context tags such as
[5]/[9] that were therefore invisible in the Insights output, even though
the raw block dump and the --verbose ASN1 view showed them.
Print unhandled tags as 'Unknown tag 0xNN : <hex>' so the human-readable
Insights view is lossless. No semantic guessing is done; --verbose still
provides the full ASN1 TLV.
The warnings are false-positive: resp.data is a union of uint8_t asBytes[] and uint32_t asDwords[], so the buffer is actually 4-byte aligned — but casting uint8_t* directly to a stricter-aligned struct pointer trips -Wcast-align. Fix by routing through (const void *)
The regenerated `hf mfdes etest` object had been spliced inside the
`hf mfdes esave` object in doc/commands.json, so catalog consumers did
not see it. Moved by hand to its place among the commands, matching what
`make commands` produces for that entry.
Help and how-to said a recorded session replays byte for byte. It does
only when the queue is filled in draw order: the random UID takes 3 bytes
at --scan on a random-id image, then each authentication takes its RndB.
Say so.
Co-Authored-By: Claude Fable 5.1 (claude-fable-5-1) <noreply@anthropic.com>
One command with one action per call: --begin, --scan, --apdu <hex>,
--random <hex>, --fieldoff, --state, --end. Text output by default,
-j gives one line of JSON per call for a script to read back.
Distinct failures are reported as such: no image in emulator memory,
not activated (--scan first), random queue full, and a firmware that
does not answer.
Co-Authored-By: Claude Fable 5.1 (claude-fable-5-1) <noreply@anthropic.com>
Three things behind a scan that ran against one application, printed its check
line twice, and then said nothing at all.
The dictionary loader was truncating rather than rejecting. 'loadFileDICTIONARYEx'
null terminated each line at the requested key length and only skipped lines that
came out too short, so any line holding more hex than asked for was silently cut
down and taken as a key. Reading 'mfdes_default_keys' for DES keys turned all 47
of its 32-hex AES lines and all 4 of its 48-hex 3K3DES lines into 8-byte keys that
are on no card, reporting 58 DES keys where the file holds 7. It now skips a line
whose hex run is longer than the key being asked for, which is what its sibling
'loadFileDICTIONARY_safe_ex' has always done. A trailing comment or whitespace
after the key is still fine, so the bundled dictionaries keep working. Counts for
that file go from 58/51/4 to 7/47/4, and mfc, iclass, mfp and t55xx are unchanged.
The key numbers to check came only from file access rights. An application with
three keys whose files name just key 1 had key 2 left out of the sweep entirely,
even though the card had already said how many keys it holds. Key settings carry
that count in the low nibble, so chk reads it and checks every key number the
application declares, the same sweep 'hf mfdes detect' already does. On a test
card this immediately turned up an AES key 2 still at the factory default, which
no file pointed at and which the old code could never have found. The access
rights walk stays as the fallback for when key settings are not readable, which
also means the common path no longer spends a file list round trip.
And the last round of a chunked dictionary read comes back empty. That empty list
was still handed to AuthCheckDesfire, which selected the application, read its key
settings, probed for LRP and listed its files in order to then check no keys at
all, printing a second identical check line on the way. Skipped now.
Finally, a scan that found nothing printed nothing. In non verbose mode all that
was left was a blank line, itself a leftover from the progress markers removed in
d842c7e5. It says so.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
hf mfu cchk and hf mfu aeschk were the same command twice. Ninety lines of
dictionary loading and chunked device calls were duplicated, and the only
real difference was which key slot they targeted: aeschk took --idx, cchk
hardcoded the Ultralight C slot. Both are replaced by hf mfu chk, which
reads the tag type off the card with GetHF14AMfU_Type() and picks the slot
itself. Ultralight AES still honours --idx, 0 DataProtKey, 1 UIDRetrKey,
2 OriginalityKey. Ultralight C holds a single key, so --idx is rejected
there rather than silently ignored. Any other tag is refused with a pointer
to hf mfu info.
Without -f the dictionary is mfulc_default_keys.dic for both tag types. A
segment check keeps needing an explicit -f, because the segment dictionaries
hold four byte keys and the default one does not. aeschk used to advertise
mfulaes_default_keys.dic in its help, which has never been shipped.
Collapsing the two bodies also fixes --retries. firstChunk and lastChunk
were declared outside the retry loop and never reset, so only the first pass
was correct. From the second pass on, every chunk went out with firstchunk
clear and lastchunk set, which on the device side means no select and a
field teardown after each chunk, against a field the previous pass had
already dropped. They are now scoped to the pass.
client/pyscripts/mfulaes_mask_recovery.py called aeschk and now calls chk.
doc/commands.md and doc/commands.json are regenerated.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The SRIX4K block scrambler in cmdhf14b.c was a three entry lookup table with
its callers commented out, so `hf 14b valid` printed hard coded values and
nothing ever decoded. The scrambler is a 4x4 transpose of the block read as
sixteen crumbs. A transpose is its own inverse and it reproduces all three
entries of the old table, so the stub is replaced by a working parser.
client/src/parsers/parsemykey.c reads the application off a dump: key id,
production date, operations counter, vendor code, lock id, current and
previous credit, and the eight slot transaction ring. Every block carries a
checksum in its top byte and the parser reports how many hold up, which
catches a wrong UID or a torn write. The credit blocks are XORed with a
session key derived from the UID, the vendor code and the count down counter
in block 6, so the file name has to carry the UID.
`hf 14b valid` is removed. Checking that the maths holds is now
`hf 14b view --selftest`, following `hf mf view --selftest`, and it runs
against traces/hf-14b-D0021F673CB26556-dump.json, a dump of a real reset key.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The main loop cleared the whole PacketCommandNG payload before every call to
receive_ng(), so a device sitting idle memset PM3_CMD_DATA_SIZE bytes on each
pass of the loop whether anything was arriving or not. The zeroing belongs
inside receive_ng_internal(), once the preamble has been read and a packet is
known to be coming in, and that is where it is now.
It has to be before either branch fills the payload, because neither fills all
of it: an NG command writes only its own length, and an old style one only
PM3_CMD_DATA_SIZE_OLD. Whatever is not written has to start at zero.
Moving it also covers two callers that never had it. appmain.c has a second
receive_ng() that drains the buffer on a mode switch, and iclass.c has one that
picks up EML_MEMSET commands while waiting for RF. Both declare their
PacketCommandNG without initialising it, and the iclass one then reads a struct
out of the payload -- so a command shorter than that struct was reading stack
garbage beyond its length. That is fixed as a side effect.
Builds for RDV4 and PM5.
Thanks to @Msprg et al
Two things you asked about after a scan printed a lone "d" between applications.
The "d" and "p" were a progress marker saying how a key had been found, a
dictionary or a pattern, printed without a newline after every hit in
non-verbose mode. The find itself is already reported in full and
unconditionally on its own line -- "AID 0x010203, Found AES Key 00... <key>" --
so the marker added a stray character to the output and nothing else. Removed.
Each found key also repeated the AID that the "Checking aid" line above it had
just announced, so that is gone too. The only path to that print is the scan
loop, which prints the AID for every application before checking it.
The crypto algorithm is highlighted now as well, so the algorithm column can be
scanned down rather than read. `hf mfdes detect` already prints its channel, its
algorithm and its key that way, and chk was highlighting only the key.
And the command needed a dictionary spelled out. `hf mfdes detect` already falls
back to the bundled `mfdes_default_keys` when none is given, and there is no
reason for chk to differ, so it does the same now when neither a dictionary nor
a pattern was asked for. That also lets it run with no arguments at all, which
it previously refused with a help screen because its parser was told not to
allow an empty command line.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
An enciphered read longer than one frame was answered with 91 7E, because the
CRC32 covers the whole read and the code could only build that in one piece.
It is one CBC run over the file data, a CRC32 behind it and zero padding, split
across frames with the init vector carried from one to the next -- "if the
commands are queued due to a very long data stream, the init vector for the
decipherment is always updated" (M134034 7.3.7). So each frame only has to carry
whole blocks, which the 96 byte chunk and the padded total both are, and the
reader joins the ciphertext and deciphers the lot in one go.
The read now walks a stream rather than a file extent: bytes come from the file
while they last, then the CRC32, then padding. The CRC is taken up front, over
the data and the status byte the last frame will carry, so it has to be built a
piece at a time -- the status byte is not in the card image and the data can be
a whole file, which rules out crc32_ex() and its single buffer. common/crc.h
already has an incremental CRC, the one legicrf.c uses, so that is what this
uses: crc_update() shifts LSB first, so the polynomial goes in already reflected
with neither reflect flag set. Those parameters were checked against crc32_ex()
over every length from 0 to 200 before being used.
Tested against the simulation with a 128 byte file created through the reader in
Full communication mode with all rights on key 0, so the file's mode actually
applies rather than being forced plain by a free access right. Reads of 64, 96,
112 and 128 bytes all verify, the last three over two frames, and the content
comes back as written. A wrong init vector or a wrong CRC would have failed the
client's CRC32 check rather than passed quietly.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A realtime `lf read` streams samples straight to the host at the LF sample rate,
around 125 kB/s. If the host stopped draining for even a moment,
async_usb_write_requestWrite() returned false, and ReadLF_realtime() treated that
as fatal: it returned PM3_EIO through a goto that also skipped
async_usb_write_stop(). The IN endpoint was left busy, every later usb_write()
then returned PM3_EIO, and the device went silent until it was physically
replugged. A USB bus reset does not clear it -- EP0 keeps working, descriptors
read fine, only bulk IN is dead. That is what a long `lf read` looked like from
the client: a transfer that stopped early, and then a device that would not
answer the next command.
A busy endpoint is back pressure, not an error. The sampling loop now waits for
the host, up to 100ms, and only gives up if it never comes back. Every exit path
closes the async write. The spins inside the USB helpers are bounded and drop the
stuck packet instead of turning into an infinite loop, which is the other half of
why the device never recovered. Measured against a reader deliberately held at
41 kB/s: before, the stream died at 212238 of 300000 bytes and the unit needed a
replug; after, 300000 of 300000 and it still answers.
The last packet of a stream was being lost as well. The host's CDC read buffer is
two max sized packets, and a bulk IN transfer only completes on a short packet or
a full buffer, so a stream ending on a full 64 byte packet was left sitting in a
half filled buffer. It showed as exact parity on the packet count:
513 packets requested -> 512 delivered 514 -> 514
515 packets requested -> 514 delivered 516 -> 516
async_usb_write_stop() now always sends a closing packet, the leftover partial
bytes when there are any and a zero length packet otherwise, the same way
usb_write() already did for its own transfers.
On the client side a short transfer is reported instead of being presented as a
complete read, the device is told to stop streaming on that path too, and the in
place byte counter is repeated with its final value, since the loop only samples
it every 10ms and the last line printed was stale.
Lastly `lf read` and `lf sniff` cap the sample count to the graph buffer size.
Anything past it was streamed and then discarded by getSamplesFromBufEx(), so
`-s 1500000` spent about two extra seconds collecting 220000 samples that were
thrown away, and printed "Received 1500000 / 1500000 bytes" followed by "Got
1280000 samples" with nothing to explain the gap.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ChangeFileSettings is answered by the simulation now. Which form it takes is
decided by the file's own "change access rights" right, not by the reader:
never refuses the command outright, free means the settings arrive as plain text
with no security mechanism at all, and anything else names the key that has to
be authenticated and the settings arrive enciphered under it (M134034 9.5.4).
That enciphered form is the first command whose parameters the reader secured
rather than the card, so it shares the unwrapping the writes use: the file
number stays in the clear, the rest is one CBC run under the session key, and
the CRC32 behind the plaintext is checked before anything is changed. A command
that unwraps its own parameters also has to be kept away from the blanket
command CMAC, or the IV moves twice.
Tested against the simulation: a file whose change right is free goes from
rights eeee to 1234 with no session at all, a file whose change right is key 0
goes from 1200 to 3210 authenticated and enciphered, and a file whose change
right has been set to F refuses every later change and keeps its settings, which
is the point of that value.
Separately, `hf mfdes esave` grew a `--keep` flag. A deleted application or file
stays in the card image as a tombstone, because the memory it held stays spent
until a FormatPICC, and esave was writing those out as though they still
existed. They are now left out by default, so a dump is the card as a reader
sees it, and `--keep` puts them back for when what a reader did is the
interesting part rather than the result it left.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
FormatPICC is answered now. It releases every application and every file and
hands the memory back, which is what makes it different from DeleteFile and
DeleteApplication: those leave a tombstone because a real card does not give the
memory back either, and this is the one command that reclaims. Here that means
rebuilding the tables around the surviving PICC entry and putting both frontiers
back where an empty card has them.
The PICC master key, its settings and the card identity are explicitly untouched
(M134034 9.4.6), so what is left is the same card with nothing on it. The same
section says the command always requires a preceding authentication with the
PICC master key, and unlike create and delete that is not relaxed by the free
create/delete key setting, so the check is on the session rather than on the
application's key settings. Measured against the simulation: 3 applications and
6400 bytes free become 0 applications and 8000 bytes free, and a bare
`90 FC 00 00 00` with no session is refused with 91 AE.
Separately, `hf mfdes esave` wrote the ATS as zeros. desfire_em_unpack() bounded
the copy with
MIN(hdr->atslen, (uint8_t)sizeof(dump->card_info.ats))
and card_info.ats is 256 bytes, so the cast wrapped to 0 and the memcpy copied
nothing -- while ats_len was still set from the image, so the saved file carried
a correct length and eight zero bytes. Both copies here are now bounded by the
image's own array, which is the smaller of the two and the one a corrupt length
could run past.
This was invisible in simulation because SimulateIso14443aInit() falls back to
the tag type's built-in DESFire ATS when the image supplies none, and for this
card the two are identical. A reader saw a correct ATS from a card image that no
longer had one, and only a load-save round trip showed it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
detectindala() reports the bit count it gave up on through
PrintAndLogEx(INFO, ...), so every failed Indala demodulation prints
[=] DEBUG: detectindala | 56
in normal output. It is the failure path: found_size < 64 means the
demodulator did not find a valid Indala (64 or 224 bits) and is returning
-5. Any noisy LF read hits it, so `lf search` with no card on the antenna
shows a developer's debug line to every user.
Every sibling in the same function already uses DEBUG, including the one
nine lines above it at "DEBUG: Warning - Indala had to invert bits", so
this is an oversight rather than a deliberate level. ui.c drops DEBUG when
g_debugMode is 0, which is the default, so the line stays available under
`data setdebugmode -1` and is simply no longer in the way.
Checked the whole client for the same class: the only other "DEBUG"
strings printed above DEBUG level are cmddata.c FSKToNRZ, and both of
those are already wrapped in `if (g_debugMode > 1)`, so they are correct
and are left alone.
Before, `lf search` over a reader field with no card:
[=] Checking for known tags...
[=] DEBUG: detectindala | 56
[-] No known 125/134 kHz tags found!
After:
[=] Checking for known tags...
[-] No known 125/134 kHz tags found!
and with `data setdebugmode -1` the detectindala lines come back, now
carrying the [#] debug prefix like the rest of them.
Passes `make style` unmodified.
The DESFire simulation gets its own ISO 14443-A loop in armsrc/desfiresim.c
rather than hooking into SimulateIso14443aTag(). That function is complicated
enough without a DESFire state machine threaded through it, and the hook had
already got the ATQA wrong once. It still borrows the library helpers from
iso14443a.c -- the precompiled activation answers, the receive call, the send
calls -- so there is no duplicated state machine, and iso14443a.c goes back to
knowing nothing about DESFire. desfiresim.h exports one symbol.
Three fixes were needed to make it actually answer a reader.
1. ISO 7816 wrapping. A reader sends `02 90 60 00 00 00`, not `02 60`. The
simulation read in[0] as the DESFire command and so saw 0x90, the class
byte, answering ILLEGAL_COMMAND_CODE to everything -- and in native form
(status || data) when the reader wanted the wrapped form (data || 91 SW).
Both framings are handled now, including the Lc/Le distinction: five bytes
means no data and in[4] is Le, longer means in[4] is Lc with data following.
2. A bitstream download frees and clears BigBuf to get scratch space for the
decompressor. SimulateIso14443aTagEx() and Mifare1ksim() both guard against
this by calling FpgaDownloadAndGo_keep_EM() before they allocate anything;
the new loop did not, so iso14443a_setup() wiped the emulator memory holding
the card image and the precompiled answers after they had been filled. The
pointers survive, the bytes do not, and the tag then clocks out zeros.
3. iso14443a_setup() itself used the plain FpgaDownloadAndGo(), which does
BigBuf_free() and BigBuf_Clear_ext() -- taking the emulator memory with it.
Every 14a command comes through there, so a card image that `eload` had just
put in place was destroyed by the next `hf 14a` command whenever the HF
bitstream was not already resident. Demonstrated before and after:
`eload` then `hf 14a reader` then `eview` used to report "No DESFire card
image in emulator memory" and now returns the image intact. This affected
every emulator memory user, not only DESFire.
Verified against a second Proxmark3 acting as reader, simulating a real
DESFire EV1 8K dump: activation (UID 04268512A25680, ATQA 03 44, SAK 20,
ATS 06 75 77 81 02 80), the three frame GetVersion chain over 0xAF, GetFreeMem,
GetApplicationIDs, SelectApplication, GetKeySettings and GetFileSettings.
`hf mfdes info`, `getaids`, `freemem`, `lsapp` and `lsfiles` all read correctly,
including per application key types (AES, 2TDEA, 3TDEA) and all five EV1 file
types with their real settings -- a value file holding 1000 with limits
[0..10000], a linear record 2/8 of 16 bytes, a cyclic record 1/4 of 24 bytes,
standard files of 256 and 64 bytes, and a backup file of 128 bytes in MAC mode
with keyed rights 1200.
Not implemented yet: GetDFNames (0x6D) and GetISOFileIDs (0x61), so a reader
sees empty ISO IDs and DF names. Everything else answers
ILLEGAL_COMMAND_CODE, which is what a PICC says to a command it does not have.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sending GetDFNames (0x6D) inside an authenticated session destroys DESFire EV1
silicon. When the response chains, the card answers the first 0xAF continuation
frame with 0xC1 PICC_INTEGRITY_ERROR, "PICC will be disabled", and from that
moment every command returns 0xCD PICC_DISABLED_ERROR. The ATS historical byte
changes 0x80 -> 0x90. Three cards were destroyed establishing this.
It is reached by `hf mfdes dump`, `hf mfdes lsapp` and `hf mfdes info --files`,
all of which authenticate and then call DesfireFillAppList.
HOW IT WAS ISOLATED
One variable at a time, on a card that survived all of it. Unauthenticated, the
identical frames are answered normally:
- 0x6D on a blank card
- 0x6D with an application that has no DF name (empty, per 9.4.4)
- 0x6D with one DF name, single frame, no chaining
- 0x6D chained over two and then three DF names, sent by hand
- the chain abandoned mid-way with the field dropped
- a stray 0xAF with no chain open, answered 91 1C
- the client's own chaining code with splitbysize
Authenticated, the same card died on the first continuation frame. The client
appends no MAC to 0x6D -- it is not in EV1D40TransmitMAC -- so the bytes on the
wire are `90 6D 00 00 00` then `90 AF 00 00 00` either way, byte for byte
identical to the probes that passed. Only the card's session state differs.
Ruled out along the way:
- Failed or wrong-algorithm authentication. `hf mfdes chk` has fired hundreds
of failed auths per card for years without harm.
- Abandoned authentication chains. DesfireCheckAuthCmd sends only the first
frame of an auth and then drops the field, six times per `hf mfdes info`
run; 30 of them left the test card healthy.
- Malformed commands. The only out-of-spec command in the original sequence
(UpdateRecord, 18 bytes into a 16 byte record) was refused client-side and
never reached the card.
- Chaining itself, DF names themselves, and the record count.
THE FIX
Drop the PICC session around the call and restore it afterwards.
It is the card's session that has to go, not the context. Clearing
DesfireContext_t alone would leave the card authenticated and change nothing, so
SelectApplication is used: "each SelectApplication command invalidates the
current authentication status" (M134034 9.4.5).
The session is then re-established on whatever AID was selected when we were
called, not on 000000. The issuer info path enters DesfireFillAppList with an
application already selected, and re-authenticating the PICC with that
application's key would silently fail. A context selected by DF name has no AID
to return to, so the DF name list is skipped rather than guessed at.
No configuration forces the unsafe order. Key settings bit 1 governs whether the
directory commands need authentication, and its "needs auth" branch names only
GetApplicationIDs and GetKeySettings, not GetDFNames (M134034 p.39). A card that
refuses the plain command costs the DF name column; sending it authenticated
costs the card.
WHAT THE DATASHEETS SAY
EV1 never specifies how secure messaging applies across 0xAF frames. EV3 had to
write the rule down later, ev3.pdf 7.3.2.2: "the secure messaging is applied as
if the command or response would have been sent in a single large frame. The
0xAF command and response codes are ignored for these calculations." The exact
interaction that destroys these cards is the one EV1 left unwritten.
EV2 and EV3 then deleted the whole self-disable family: ev3.pdf contains zero
occurrences of PICC_DISABLED or PICC_INTEGRITY, which M134034 defines. EV3
renames 0xEE to MEMORY_ERROR and says the part may execute a reset rather than
disable itself.
Whether the disabled state is recoverable is SILENT in every direction. The five
codes appear once each, in one status table, with no prose anywhere, and there is
no lifecycle command in DESFire to rehabilitate a PICC. Measured on a dead card,
nothing works: FormatPICC cannot authenticate (0x1A -> 91 CD) and is refused raw
(90 FC -> 91 CD); SelectApplication is refused (91 CD) even though it needs no
authentication and no bit can gate it; every ISO 7816 command including ACTIVATE
FILE and TERMINATE DF answers 65 81 "Memory failure". A malformed GetVersion
still answers 91 7E LENGTH_ERROR, so anticollision, RATS, the command parser and
length validation all still run -- only the application layer underneath is gone.
Note M075031 is the D40 datasheet, not EV1; the EV1 document is M134034.
ALSO IN THIS CHANGE, because without them the cause was invisible
Every card error collapsed to PM3_EAPDU_FAIL before reaching a caller, and both
send paths only printed the status when -a was given. That is why the first two
cards told us nothing about how they died.
- The five codes M134034 Table 11 footnotes as "not expected to appear during
normal operation" (0xC1, 0xCD, 0xA1, 0xF1, 0xEE) are now reported from both
DESFIRESendRaw and DESFIRESendApduEx whether or not -a is given. Ordinary
refusals stay at debug level, so chk and the ISO file id probes stay quiet.
- A chained 0xAF is attributed to the command it is continuing, so a failure
reads "command 0x6D frame 0xAF -> 0xC1" rather than a bare 0xAF.
- DesfireContext_t keeps lastRespCode, set where both exchange paths meet.
A command error also ends the authentication on the PICC, so the client now
drops its side of the session rather than continuing to MAC into a session the
card has thrown away. The tree already did this at one call site
(DesfireGetFileISOIDList); it is now done at the choke point every native
command passes through, plus the paths that bypass it: DesfireReadSignature,
DesfireCreateDelegatedApplication, DesfireISOSelectEx, the ISO data plane
(ReadBinary, UpdateBinary, ReadRecords, AppendRecord) and GetDelegateInfo. The
three ISO authentication wrappers are deliberately left alone, since they run
before a session exists and clearing would wipe the handshake's own state.
`hf mfdes pc` checked only the transport result and treated a card error as
success: DesfireExchangeEx returns PM3_SUCCESS with the card's status in
respcode, so PreparePC, the proximity check rounds and VerifyPC went on to parse
error responses as data. They now check respcode.
DesfireFillFileList discarded the result of DesfireFileSettingsStruct, so a file
whose settings could not be read became a zeroed entry indistinguishable from a
valid 0 byte standard data file. FileListElm_t now carries fileSettingsRead and
five consumers honour it: the ISO id mapping loop (positional, an unread file
would shift every id after it onto the wrong file), `hf mfdes chk`'s used-key
set, lsfiles, the application walk print, and `hf mfdes dump`, where settings_ok
was being set true unconditionally.
DesfireFillAppList tested a stale `res` after DesfireGetKeySettings, whose return
value was discarded; only an incidental buflen guard kept it from consuming
garbage key settings.
`hf mfdes sim` now runs through the shared ISO14443-A simulation loop as tag
type 3 rather than a duplicate 14a loop, which had got the ATQA wrong. The
DESFire logic stays in armsrc/desfiresim.c behind a small hook API.
KNOWN, NOT FIXED HERE
DesfireFillAppList reads the DF name list into uint8_t buf[250] while
DesfireCommandEx copies records * 24 bytes. The EV1 limit of 28 applications
would write 672 bytes into it. Harmless at the record counts seen here, but a
stack overflow waiting for a full card.
VERIFICATION
On a fourth card from the same lot (batch B9 0C 17 49 70, week 27 / 2017, EV1 8K,
HW 04010101001A05, SW 04010101041A05): the sequence that killed the third card
now completes, the chained 0xAF is answered 91 00, and the card is healthy
afterwards. The APDU log shows GetDFNames and its continuation carrying no MAC
while the commands either side of them do, confirming the session is dropped and
restored. A full `hf mfdes dump` of three applications (AES, 2TDEA, 3TDEA) and
all five EV1 file types completes and round-trips through the dump format.
Scope: all four cards are from one production lot. This is not established as an
EV1-wide erratum.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>