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>
Adds the on-device card image a DESFire simulation will read, the client
side that packs a dump into it, and eload / esave / eview.
The layout is in include/desfire_em.h. Tables grow up from the header,
file data grows down from the end, and an allocation only has to leave
the two frontiers apart -- so a three-file card spends a few hundred
bytes rather than a worst case, and the space between them is what the
card has left. The PICC is application 000000 and uses the same struct
as any other application, so key settings and keys are always stated
against an AID. Delete sets a tombstone rather than compacting, which is
not a shortcut: a real card does not reclaim on delete either, measured
as 2080 bytes free with zero applications before an experiment and 2560
after FormatPICC.
Two size limits, and a reader only ever sees the first. cardsize is what
the emulated card claims to hold, so GetFreeMem answers from that and
CreateFile will refuse with OUT_OF_EEPROM when it runs out. Without it a
card impersonating a 2K part would report 7434 bytes free, which no 2K
part does. The image size is what emulator memory physically holds, is
the harder limit, and is never visible.
cardsize is anchored to the free memory the real card reported when the
dump was taken -- observed free plus what we reserve for the same content
-- so the emulation answers what its original answered. That is also a
check on the reservation rule rather than only a convenience: for the
bench card it computed 576 bytes spent, and 1984 + 576 is 2560, exactly
what that card reports when formatted.
Reservation follows what CommitTransaction covers. Backup data, value and
record files each get a shadow region because writes to them are staged
until commit; a standard data file writes through and does not. Sizes
round to a 32 byte granule, which is the granule a real card allocates in.
It deliberately does not reproduce NXP's allocator -- that is
undocumented and does not fit a simple model, a declared 1024 byte record
file costs 1088 on silicon -- so what matters is that the figure is
self-consistent and shrinks as the reader writes.
No new device command. Emulator memory is one shared region, so
CMD_HF_MIFARE_EML_MEMSET and BIG_BUF_EML already reach it, and both
inherit the bounds checking those paths gained earlier.
Verified with the client only, no simulation yet: packing a dump taken
from a DESFire EV2 and walking it back reproduces every field including
file contents byte for byte, the same round trip through the device via
eload and esave is identical, and an image too large is refused by name
rather than truncated -- 'Out of emulator memory laying out AID 112233
file 00: needs 4066 more bytes'.
Co-Authored-By: Claude Opus 5 (1M context)
b82f60362 landed parseproac.c without them, so a clean checkout does not
build.
- mad.c / mad.h: mad_find_aid() and mad_count_aid(); DetectHID() delegates
to the first, it was already generic
- mad.json: 0x4982 PROAC, sorted in after NORALSY's 0x4980
Co-Authored-By: Claude Opus 5 (1M context)
Sectors 9 and 11 were printed raw and marked 'not decoded'.
- remove the XOR keystream and print the ten eight byte records
- check the eighteen record bytes that are XOR combinations of sector 0,
sector 15 and the UID, plus three record to record ties
- 'hf mf view --selftest' now covers the decoder, using a synthetic card
- sector 15 marker compare ignores case, factory blanks were not matched
- an all FF or all 00 payload is named, not decrypted
- sector 15 block 2 is text on a blank, so only read a serial there when
the first four bytes are not printable
Research. The remaining 58 payload bytes are issuer data and are printed
without interpretation.
Co-Authored-By: Claude Opus 5 (1M context)
'hf mfdes dump' walked one application and printed it. It now walks
every application on the PICC, keeps what it reads, and saves a
'hf-mfdes-<UID>-dump.json' card image. '--aid' / '--isoid' / '--dfname'
still narrow it to one application, '--ns' skips the save.
The format is 'mfdes v1', written and read in fileutils.c and documented
in doc/mfdes_dump_format.md. Two decisions worth stating:
- The PICC level is application 000000, so every key in the file says
which AID it opens. Key version and key value are separate: a version
with no key is the normal shape for a key that was found but never
recovered, and a missing key never means the key is zero.
- Every file carries a 'Read' flag. A file whose contents could not be
fetched is recorded as unread with no data at all, rather than as a
run of zeros. A simulator built on this must not confuse '8 bytes of
00' with 'we could not read 8 bytes'.
'hf mfdes view -f <fn>' prints such a file with no device attached.
With no '--keys', the dump looks for 'hf-mfdes-<UID>-keys.json' by
itself, so a 'hf mfdes chk -j' run is picked up on the next dump without
naming the file again.
Two fixes fell out of testing against a DESFire EV2:
- DesfireSetKey() calls DesfireClearContext(), which wipes command set,
comm mode, KDF and UID, not just the key. Swapping in a per-application
key that way left the context at 'Communication mode: n/a' and
DesfireFillFileList() then returned junk file ids. Use
DesfireSetKeyNoClear().
- GetVersion and the originality signature are answered unauthenticated.
Asking for them from inside the authenticated session produced a
'Wrong communication mode' warning and a run of MAC mismatches.
hex_to_buffer() treats hex_max_len as a byte count while every sprint_hex*
caller passes sizeof(buf) - 1, a character count, so it writes two or
three times the buffer size. Measured, sprint_hex_inrow overflowed at
4098 input bytes. Doubling UTIL_BUFFER_SIZE_SPRINT to 16384 moves that to
8192; the mixed semantics still need auditing across ~30 call sites.
Co-Authored-By: Claude Opus 5 (1M context)
Refactor BLE error handling to return standard error codes instead of PM3_* constants. Update time-related functions to use a consistent method for obtaining the current time.
Signed-off-by: Niel Nielsen <nieldk@gmail.com>
MifareNested collected nonces in 'while (target_nt[i] == 0)' with no
attempt counter. When the tag NAKs the nested authentication the loop
re-sent the identical frame forever, so the client's 2s wait expired and
autopwn returned PM3_ETIMEOUT — throwing away every key recovered up to
that point. Reported on a card whose sector 16 refuses the standard MFC
EV1 keys: 32 keys cracked, nothing saved.
A NAK is a refusal, not a glitch, so the device now gives up on it and
reports PM3_EWRONGANSWER. autopwn names the sector it skipped and carries
on, and a timeout falls through to the key table and the dump whenever
anything was recovered.
Co-Authored-By: Claude Opus 5 (1M context)
DesfireSelectAndAuthenticate*() and its callers both printed the same
error at different severities, so every failure came out as two lines.
The core helper now owns the message and names the AID and the step;
the 23 restatements in cmdhfmfdes.c are gone. Same duplicate pair fixed
in cmdhfgallagher.c.
Also: DesfireAuthErrorToStr() falls back to DesfireGetErrorString() for
the PM3_E* codes the select paths pass through, which used to print an
empty reason, and auth error 7 no longer reuses the text of error 1.
Co-Authored-By: Claude Opus 5 (1M context)
Added a function to check if a string is a valid Bluetooth address and updated the BLE connection logic to handle both addresses and names.
Signed-off-by: Niel Nielsen <nieldk@gmail.com>
'-f card.mfd' looked for card.mfd.bin and saved to card.mfd.bin,
because the caller's suffix was appended unless the name already
ended in that exact suffix.
searchFile() now tries the name as typed before falling back to the
suffixed one. newfilenamemcopyEx() keeps an extension that denotes
the same kind of file it is about to write, and swaps any other for
its own, so 'hf mf dump -f card.mfd' gives card.mfd + card.json
rather than card.mfd + card.mfd.json. Both classify with
get_filetype() so load and save cannot drift apart.
Also drops the size_t underflow in newfilenamemcopyEx(), where a long
path plus a configured save path made the snprintf bound wrap past
the 1000 byte buffer.
Co-Authored-By: Claude Opus 5 (1M context)
is_valid_vigik_card() only fired when the MAD advertised aid 0x4910. Plenty of
VIGIK based deployments ship no MAD at all - a Hexact card has HEXACT as key A
on every sector and nothing in sector 0 block 1 - so hf mf view and hf mf dump
--ns said nothing whatsoever about them.
These systems use the same keys on every card they issue, which makes them
identifiable outright. Take the schemas out of armsrc/Standalone/hf_colin.c,
where they sit commented out inside the standalone mode, and match a whole dump
against them:
Infineon / Hexact / COGELEC / Intratone key A HEXACT x16, 15 static key B
Noralsy ALARON / BLARON
Urmet Captiv 8829da9daf76 throughout
VIGIK service badge MAD key, then 1KIGIV on sectors 1-4
Every slot a schema pins down has to match, VIGIK_KEY_ANY marks the ones it does
not, so there are 31 exact keys to hit for Hexact and no room for a coincidence.
A HID card carrying a MAD, and a card on default keys, both still match nothing.
Co-Authored-By: Claude Opus 5 (1M context)
The shared viewer had the VIGIK sector assembly inlined, and the HID PACS
decode sat in hf mf mad's file branch where hf mf view and hf mf dump --ns
could not reach it. Neither scheme had a home of its own.
Give each one a parser next to parsehrt.c, same shape as that one - an
is_valid_x_card() detector and an x_parser_parse() that prints:
parsers/parsehid.c MAD aid 0x484d, PACS sector, Wiegand decode
parsers/parsevigik.c MAD aid 0x4910/0x4916, sector assembly
parsevigik.c also takes vigik_get_service(), vigik_verify() and
vigik_annotate() out of mifare/mifarehost.c, 306 lines that were VIGIK only
with a single caller.
mf_view_dump() is now two detector calls, so hf mf view -f and hf mf dump --ns
both decode a HID credential off a live card for the first time, and adding a
scheme is a new file plus two lines. hf mf mad -f keeps its HID decode through
the same parser.
The sector copy in the VIGIK path gains a bounds check; a MAD entry pointing
past the end of a short dump used to read past the buffer.
All three source lists get the new files: client/Makefile,
client/CMakeLists.txt and client/experimental_lib/CMakeLists.txt. The library
one matters because vigik_annotate() moved; without it anything linking
libpm3rrg_rdv4 loses the symbol.
hf mf mad against a card still cannot decode PACS. It authenticates with the
MAD key alone and never reads the application sector, so it has no credential
bytes to work with - unchanged here.
Co-Authored-By: Claude Opus 5 (1M context)
'hf mf view -f' decoded VIGIK PACS and could extract keys with '--sk'. 'hf mf dump
--ns', which reads the same 1K off the card, printed only the blocks and the
verbose key/ACL tables. Same bytes, less analysis, purely because of where they
came from so getting a PACS decode off a live card meant dumping it to a file
and viewing that.
Move everything view does once the blocks are in hand into mf_view_dump() and
call it from both. 'hf mf dump --ns' and 'hf mf view -f' now print byte identical
output for the same card, and dump gains '--sk' to match. The old view leaked its
dump buffer on both VIGIK error returns; the shared version frees it in one place.
Drops a stale commented out convert_mfc_2_arr() call referencing a pdump
variable that no longer exists.
Rounds off #1942. mfc_read_tag() and '--ns' landed long ago, this was the piece
still missing.
hf mf eview, cview and the gen4 view share the same print block but omit
mf_analyse_acl() in verbose, so folding them in changes their output and is
left for a separate decision.
Co-Authored-By: Claude Opus 5 (1M context)
A card runs the protocol its TD1 names until a PPS changes it. --t1 only
picked the module's T=1 send opcode, so on a card whose ATR offers T=0 first
the APDU went out as T=1 blocks to a card still speaking T=0 and got no
answer at all:
smart raw --t1 -s -d 00a4040007a000000004101000
[!] smart card response failed
'smart pps --t1' in the same client session made it work, which is what the
help for 'smart pps' already promised was unnecessary:
Note 'smart raw --t1' already switches a card to T=1 by itself when
the ATR offers it; this is for negotiating Fi/Di explicitly.
Do it for real. SmartCardRaw() now runs the PPS when T=1 is asked for and
nothing has selected it yet, taking the ATR it needs first because PPS is
only legal in that window. It fires once per activation: a second --t1 apdu
sees the protocol already in force and sends straight away. A card that does
not offer T=1 is left alone and the apdu still goes out, as before.
Also fix the flag names in the docs and in the error path. 'smart raw's
first example and both 'Choose either' messages said -0 / -1, which have
never existed. tools/pm3_online_tests.sh had copied the example, so its T=0
checks failed with 'invalid option' on every run; its T=1 checks needed no
change beyond dropping the now unnecessary reset.
Co-Authored-By: Claude Opus 5 (1M context)