The firmware powers off at 3300 mV (BWM_SHUTDOWN_MV) but the gauge
Terminate Voltage was its 3200 mV default, so the PM5 switched off while
`hw status` still showed a few percent. The gauge setup (setcap and the
boot check) now writes Terminate Voltage 3300 mV with the capacity and
chem ID, and `hw dbg -3` shows it on the Gauge line.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With -r the device reply was left unread, so each later command
printed the answer of the one before. The reply is now read and
dropped.
Data that does not fit one packet with its 3-byte header is refused
at once, instead of SendCommandNG refusing it and the client waiting
2 s for a reply.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The OpenPGP AID hint had length 8 for 6 bytes: the SELECT read past
the string and sent a name no OpenPGP card matches, so hf search
never found one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The fitted cell is 3.7 V nominal (500 mAh, 1.85 Wh), full at 4.2 V. The
charger stopped at 4.095 V and the BQ27427 stayed on its ROM default
4.35 V profile (chem ID 3230), so the gauge never saw a full charge:
Qmax kept its default 16384 and never updated, "charge done" read 83 %,
and a discharge reached 0 % with over 100 mAh still in the cell.
- default charge target 4200 mV (boot and plain `hw bwm vchg`)
- `hw bwm setcap` also selects chem ID 1202 (4.2 V) in the same config
update and verifies both afterwards; still a no-op when correct
- boot re-applies the gauge setup after a gauge reset (Flags ITPOR, RAM
back on ROM defaults) or when the profile is wrong, keeping a capacity
set with setcap
- `hw dbg -3` shows the gauge chem ID on the Gauge line
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The length check only caught answers shorter than the assumed block size.
A tag with blocks larger than bytesPerPage - one that sends no memory size
field and is read with a too small --bs - answered longer, passed the
check and had each block truncated into the saved dump with no warning.
Compare for equality and name the size the answer implies, so a wrong --bs
is diagnosable in both directions instead of only one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Both commands read the UID at d+2 and the optional fields at offsets
derived from the info flags, behind a resp.length >= 2 guard only. A tag
that announces fields it does not send, or truncates the answer, had the
UID and the memory layout taken from bytes past the payload. The reads
stay inside the fixed PacketResponseNG buffer, so they return zeros
rather than overflowing, but the values printed and dumped were not the
tag's.
iso15_sysinfo_min_len() gives the shortest answer that can carry what the
flags claim. info refuses a shorter one; dump reports it and falls back to
the --bs defaults it already has for a tag that gives no layout.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The "iso15693 card returned error" line existed three times: in the
ISO15_ERROR_HANDLING_CARD_RESPONSE macro, in
iso15_error_handling_card_response() and in iso15_info_error_handling().
The macro now delegates to the function it was a copy of, and both
reporting paths call iso15_print_card_error(). The info path still prints
the 0F and 10 errors that the shared helper keeps silent, since
hf 15 restore reads them as the end of the tag memory.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
NxpTestEAS() printed 32 bytes from the answer behind a resp.length >= 2
guard only. A tag that answers EAS ALARM with anything shorter had the
rest of the response buffer printed as its EAS sequence.
The read stays inside the fixed PacketResponseNG payload, so there is no
overflow, but the sequence shown was not the tag's.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
eload, dump, restore and view each carried a verbatim copy of the
pagesCount/bytesPerPage bounds check and its message. Fold them into
iso15_tag_size_ok(); it prints the offending values and each caller keeps
its own cleanup, which differs between the four.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The GET SYSTEM INFORMATION request is sent with ISO15_NO_DISCONNECT, but
the timeout, short answer and CRC returns freed both buffers and left the
field on. The size check added later in the same function already calls
DropField(), so the function disagreed with itself.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
getUID() hardcoded ISO15_HIGH_SPEED, so every scan step ran at 1 out of 4
even with -2. A tag that only answers at 1 out of 256 could not be found,
and the modulation changed halfway through the exchange.
Pass fast down from the callers that have -2. readHF15Uid (hf search),
hf 15 csetuid and hf 15 implant have no such option and keep high speed;
note that csetuid's -2 selects the gen2 magic command, not the data rate.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- StCheckSig() ignored the result of hexstr_to_byte_array(). Signature
blocks with a character that is not hex gave a 32-byte signature set
only up to that character, the rest printed from an uninitialised
buffer. The buffer is now zeroed, and such blocks print "Signature
blocks are not a hex string" and return PM3_ESOFT.
- The two returns without a signature now say why: "No signature,
block 63 is not K04S", or the block and the error it answered.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The block size of the system info is masked with 0x1F, as in
hf 15 info: its top 3 bits are RFU.
- Without a memory size in the system info, the dump reads up to 128
blocks of --bs bytes, fewer when they do not fit in the 2048-byte
buffer (64 with --bs 32), instead of refusing --bs above 16.
- --bs above 32, the largest ISO15693 block, is set to 32 with a
warning. It was stored in a uint8_t, so --bs 256 became 0 and
--bs 260 became 4.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- NxpTestEAS() reported "EAS ... is not active" also when hw tearoff
had stopped EAS ALARM. It now returns PM3_ETEAROFF. No answer is still
"not active", for PM3_ETIMEOUT and PM3_EPARTIAL alike: the SLIX
tested here sends nothing to EAS ALARM with its EAS mode disabled,
and the firmware returned PM3_EPARTIAL in 7 of 12 runs, with no tag
answer in the trace.
- ISO15_ERROR_HANDLING_CARD_RESPONSE tested d[0] instead of its own
parameter. It now uses (data)[0] and (data)[1].
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
strlen() of the directory literal made the path buffer a VLA that
Apple clang folds to a constant and rejects under -Werror
(-Wgnu-folding-constant). Size it with sizeof and build it with one
snprintf.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Parsing:
- all subcommands parse through the argtable: --on/--off, -m <mode>,
-u <on|off> via CLIGetOptionList, wifi --status/--stop; no more
sscanf positional words, so every option shows in the help
- wifi status and stop split into helpers; help examples aligned, the
SSID example shows quoting
Output:
- 20 column labels as in hw status, state colour coded
- autooff: 4 lines; the idle timer shows when a client pauses it,
USB / client / BLE inputs only in client debug mode
- wifi --status, wifipower and the bring-up share one WiFi state line
- ble status: the factory passkey is shown red; the pairing hint shows
only while pairing is off, the passkey hint only for the default key
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two of the phases left after the state file and table build fixes.
PartialSumProperty() evaluated each of the 16 four bit suffixes as its own
chain of filter() calls, but the suffixes share prefixes, so walking them as
a binary tree needs 31 calls instead of about 64. Output verified identical
for all 2^21 state and half combinations.
update_allbitflips_array() accumulated an AND over 256 state arrays for the
even and the odd half one after the other, although the halves share no data.
Run them side by side.
init_part_sum_bitarrays 1.095 s -> 0.425 s
update_allbitflips_array 0.847 s -> 0.520 s
Key still recovered on a hard prng 4K, live and from a nonce file, and by
hf mf hardnested -t.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The check rejected ncpus >= detected_cpus, so on a 12 core host --ncpu 12
failed with "invalid number of CPU cores: --ncpu 12 (available: 12)" and the
highest value accepted was one less than what the client uses by default.
Reject only more than detected.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
init_part_sum_bitarrays() walked all 2^20 states twice per half of the
state, once for Sum(a0) and once for Sum(a8), so PartialSumProperty() ran
four million times although two million distinct results exist. Each state
then set its 16 bits one at a time, 67 million set_bit24() calls in total.
Walk the states once and use what the two bit patterns actually are:
- a0 sets state << 4 | 0..f, which is one aligned half word
- a8 sets state | 0..f << 20, the same pattern repeated every 2^20 bits,
so fill the lowest block and copy it up
Same tables, 3.5 s -> 1.5 s. With the state file search fix the whole attack
on a 4K target goes 54 s -> 22 s, key recovered unchanged.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
init_bitflip_bitarrays() called searchFile() for each of the 2 * 1023 bitflip
state tables, and for a table that is not present it called it three times,
once per supported extension. searchFile() walks the configured default paths,
so each lookup stats file.default.savepath, dumppath and tracepath.
Those preferences often point somewhere slow. With them on a Windows drive
under WSL, where a stat costs ~6.6 ms, the ~6000 stats cost more than half of
the whole attack. Loading the small LZ4 tables was slower than the 700 MB of
raw ones, because a missing raw table meant a second search.
All the tables live in one directory, so search for the first one only and
build the remaining paths from the directory it was found in. A broken install
with no tables behaves as before.
Table load on a FM11RF08S-era 4K target, with default preferences:
raw tables present: 33.3 s -> 0.48 s
LZ4 tables only: 36.5 s -> 1.09 s
whole attack: 54 s -> 24 s, same key recovered
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- DSFID, AFI, memory size and IC reference were read at fixed
positions, so on a tag without DSFID or AFI the block size and
count came from the wrong bytes. They are now read after the
fields the tag really sent.
- A memory larger than the 2048-byte dump buffer overflowed it. It
is now refused.
- A block answer shorter than the block size was copied as a full
block. It is now reported and not copied.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- AFI, memory size and IC reference were read at fixed positions,
so a tag without DSFID or AFI showed wrong values. They are now
read after the fields the tag really sent. The block count no
longer wraps to 0 on a tag with 256 blocks.
- The NXP DSFID and password protection pointer lock bits were read
from the wrong bits. They are now bits 0x04 and 0x08, as in the
SLIX2 datasheet (table 73).
- The ST25TV signature, 32 bytes, overflowed a 16-byte buffer. On a
tag error the signature read now frees its request and drops the
field. When the tag has no signature, it drops the field and
returns success, so hf 15 info no longer ends with an error.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The SLIX tested here sends no answer to EAS ALARM when its EAS
mode is disabled, and hf 15 info ended with an error. It now
reports the EAS mode as not active.
- The two error helpers of the hf 15 commands printed the flags
byte instead of the error code. They now print the error code.
hf 15 info also prints the errors 0F and 10, which the helpers
keep silent.
- The option -2 (1 out of 256) now also applies to the NXP and ST
requests sent after the system info.
- A UID given with -u was sent in the wrong byte order, so no tag
answered. It is now sent LSB first, as hf 15 rdbl does. A -u that
is not 8 bytes long is refused instead of scanning.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fm11_prioritize_defaults() walked the whole candidate list once per
dictionary key, and it is called for each sector with tens of thousands of
candidates and ~85k dictionary keys, so it did on the order of 10^10
comparisons per run.
Index the candidate keys first and skip a dictionary key that is not in the
list, which is nearly all of them. A key that is present still takes the
original scan, so the resulting order is unchanged. Falls back to the plain
scan when the index cannot be allocated.
Same key table recovered on a FM11RF08S, hf mf sen 6.1 s -> 5.2 s offline
and 7.4 s -> 6.6 s for a full run.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
fm11_key_matches_nonce() runs once per dictionary key per unknown key slot,
up to 85442 * 34 times. For every one of them it malloc'd a cipher state,
clocked two full 32 bit keystream words and only then compared the first,
although nearly every key fails on the first byte.
Keep the state on the stack, compare a byte at a time and stop at the first
mismatch, and clock the second word only for a key that survives. Same keys
accepted, about 6x less time in that phase (2.9 s -> 0.5 s for a full 34 slot
scan of the 85k dictionary).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Nested and static nested recovery ran lfsr_recovery32 once per nonce and
intersected two ~2^16 candidate lists. The cipher state is linear while the
nonce is fed in, so the states for one nonce are those for the other xored
with a known difference. Checking both keystreams inside a single recovery
leaves about one candidate instead of two large lists.
lfsr_recovery32_dual() does that: it skips the early bit times where the
difference prunes little (start chosen per call from an exact per bit match
probability, falling back to 0 on overflow), scans the first pass 64 states
at a time through a bit packed filter table, extends the tables branch free
in cache sized chunks, and replaces the 512 scattered sort buckets with one
contiguous counting sort.
lfsr_recovery32() is rebuilt on the same machinery, so hf mf sen, mfkey32,
mfkey64 and the nested tools all gain. It stays single threaded because
nested_util.c already calls it from four threads. recover() takes the second
keystream as optional extra state; the plain path is unchanged.
Both return the same key sets as before, verified against the old code under
ASan/UBSan over random and degenerate nonce pairs.
nested per key: ~390 ms -> ~4 ms
lfsr_recovery32 per call: ~270 ms -> ~165 ms
peak RSS, single nonce: 47.6 MB -> 27.6 MB
tools/mfc/protocol links pthread now, as the other two mfc tool dirs do.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Review of #3807: the -k description is back to its short text, and
the command description says that the MAD is read with -k when the
default MAD key fails.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The command put the 256 bytes of a 4K sector 32-39 into a 64-byte
buffer, a stack buffer overflow, and kept 3 of its 15 data blocks.
Now the buffer holds 16 blocks, and the command keeps every data block
of the sector.
Split from #3807.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When the MAD key failed on sector 0, the command told the user to retry
with -k, but it used -k only for the NDEF sectors. A -k or --aid of the
wrong length was ignored without a word.
Now the command tries -k on the MAD sectors when the MAD key fails,
and a -k that is not 12 hex digits or an --aid that is not 4 is
refused.
Split from #3807.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
--override was refused: the code read it at index 9, which had no
argtable entry. A -k or --aid of the wrong length was ignored without
a word. With -f and --aid the command printed no block of the AID.
Now --override has its argtable entry, a -k that is not 12 hex digits
or an --aid that is not 4 is refused, and -f with --aid prints the
AID's blocks.
Split from #3806.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With --aid or --dch, the card read put the 256 bytes of a 4K sector
32-39 into a 64-byte buffer, a stack buffer overflow. The command
printed only the first blocks of the sector, numbered wrong from
sector 33, and gave --dch 3 of its 15 data blocks.
Now both buffers hold 16 blocks, and the command prints and decodes
every block of the sector.
Split from #3806.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DetectST only runs on ask, and each fsk hit set its ST flag to false. An
fsk hit now reports ST if its block 0 word has the ST bit set and a copy
of the word sits 33 to 36 bits before or after it.
On a buffer of 92 bits or more, test() starts its scan at bit 28 and
often picks the second copy of block 0. On a short capture the copy
after that word may not fit, so the check also looks behind it. If
neither side has room for a copy, the ST bit decides. A Q5 word still
reports no ST.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ASKDemod_ext sets its ST flag if DetectST cut terminators out, and never
clears it. The four ask attempts that call it preset the flag to true,
so every hit from them reported ST. Each of those hits now starts from
false and reports ST only if DetectST cut terminators out of that
demodulation.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>