hf mf hardnested: fix the nonce reply length, 9 bytes per pair not 4 per nonce

Thanks @TheArchitect0880 for pointing it out and suggested a first fix.

MifareAcquireEncryptedNonces packs two 4 byte encrypted nonces plus one
byte holding both their encrypted parity nibbles into every entry, but
the reply declared num_nonces * 4 bytes. The client walks that buffer 9
bytes at a time, so on RDV4 it read 612 bytes out of a 544 byte payload
and handed roughly 15 of every 136 nonces to add_nonce() from stale
packet buffer content. Those fake nonces went into the .bin nonce file
too.

Count pairs instead of bytes. num_nonces now reports whole pairs only,
so a button abort or a static nonce bailout part way through a pair
drops the dangling nonce rather than shipping a half built entry whose
parity nibble was never filled in.

MFC_NONCE_PAIR_SIZE and MFC_MAX_NONCE_PAIRS document the layout next to
mf_nonces_resp_t so the 9 vs 4 confusion cannot come back, and the
client now refuses a reply too short for the nonce count it carries.

Both acquisition functions collect straight into the reply buffer rather
than a second PM3_CMD_DATA_SIZE stack array, which halves the stack used
per call. MifareAcquireNonces also returned isOK = 2 on button press,
which is not a PM3_* status; that is PM3_EOPABORTED now.

Co-Authored-By: Claude Opus 5 (1M context)
This commit is contained in:
iceman1001
2026-09-12 10:44:00 +02:00
co-authored by Claude Opus 5 (1M context)
parent 5ca15e4587
commit 6341f40b4c
4 changed files with 53 additions and 24 deletions
+4 -3
View File
@@ -3,9 +3,10 @@ All notable changes to this project will be documented in this file.
This project uses the changelog in accordance with [keepchangelog](http://keepachangelog.com/). Please use this to write notable changes, which is not the same as git commit log...
## [unreleased][unreleased]
- Changed `hf mfdes detect` - with no `-n` it now sweeps every key number the application declares instead of only key 0; the first hit pins the algo for the rest (@iceman1001)
- Fixed `hf mfdes detect` - the card error counter was shared by every key type, so a bad run during the DES pass could abort the AES pass before it tried a single key (@iceman1001)
- Fixed `hf mfdes detect` - with no `-f` it drew candidates from the MIFARE Plus 16 byte key list,it now defaults to the bundled `mfdes_default_keys` dictionary (@iceman1001)
- Fixed `hf mf hardnested` - the device declared `num_nonces * 4` bytes but packs 9 bytes per nonce pair. Thanks @TheArchitect0880 (@iceman1001)
- Changed `hf mfdes detect` - with no `-n` it now sweeps every key number the application declares instead of only key 0 (@iceman1001)
- Fixed `hf mfdes detect` - the card error counter was shared by every key type, so a bad run during the DES pass could abort the AES pass (@iceman1001)
- Fixed `hf mfdes detect` - with no `-f` it drew candidates from the MIFARE Plus 16 byte key list, it now defaults to the bundled `mfdes_default_keys` dictionary (@iceman1001)
- Fixed `hf mfdes detect` - LRP was only found when the key settings happened to be unreadable (@iceman1001)
- Added `hf mfdes chk --schann` - now detect the secure channel EV/EV2/LRP mode; detected per application and can be overridden (@iceman1001)
- Fixed `hf mfdes chk` - found keys were tracked per key number instead of per application, so a key number recovered on one AID was skipped later (@iceman1001)
+29 -18
View File
@@ -991,9 +991,14 @@ void MifareAcquireNonces(const mf_acquire_nonces_t *payload) {
uint8_t uid[10] = {0x00};
uint8_t answer[MAX_MIFARE_FRAME_SIZE] = {0x00};
uint8_t par[1] = {0x00};
uint8_t buf[PM3_CMD_DATA_SIZE] = {0x00};
// collect straight into the reply buffer, no second copy on the stack
uint8_t respbuf[sizeof(mf_nonces_resp_t) + (MFC_MAX_NONCES * 4)] = {0x00};
mf_nonces_resp_t *response = (mf_nonces_resp_t *)respbuf;
uint8_t *buf = response->nonces;
uint32_t cuid = 0;
int16_t isOK = 0;
int16_t isOK = PM3_SUCCESS;
uint16_t num_nonces = 0;
uint8_t cascade_levels = 0;
uint8_t blockNo = payload->blockno;
@@ -1019,7 +1024,7 @@ void MifareAcquireNonces(const mf_acquire_nonces_t *payload) {
// Test if the action was cancelled
if (BUTTON_PRESS()) {
isOK = 2;
isOK = PM3_EOPABORTED;
field_off = true;
break;
}
@@ -1072,12 +1077,12 @@ void MifareAcquireNonces(const mf_acquire_nonces_t *payload) {
LED_C_OFF();
LED_B_ON();
uint8_t respbuf[PM3_CMD_DATA_SIZE] = {0x00};
mf_nonces_resp_t *response = (mf_nonces_resp_t *)respbuf;
// one bare 4 byte nonce per entry here, unlike the paired layout used by
// MifareAcquireEncryptedNonces. The loop above already caps num_nonces
uint16_t noncelen = num_nonces * 4;
response->cuid = cuid;
response->num_nonces = num_nonces;
uint16_t noncelen = MIN((uint16_t)(num_nonces * 4), (uint16_t)(MFC_MAX_NONCES * 4));
memcpy(response->nonces, buf, noncelen);
reply_ng(CMD_HF_MIFARE_ACQ_NONCES, isOK, respbuf, sizeof(mf_nonces_resp_t) + noncelen);
LED_B_OFF();
@@ -1106,12 +1111,17 @@ void MifareAcquireEncryptedNonces(const mf_acquire_nonces_t *payload) {
uint8_t uid[10] = {0x00};
uint8_t receivedAnswer[MAX_MIFARE_FRAME_SIZE] = {0x00};
uint8_t par_enc[1] = {0x00};
uint8_t buf[PM3_CMD_DATA_SIZE] = {0x00};
// collect straight into the reply buffer, no second copy on the stack
uint8_t respbuf[sizeof(mf_nonces_resp_t) + (MFC_MAX_NONCE_PAIRS * MFC_NONCE_PAIR_SIZE)] = {0x00};
mf_nonces_resp_t *response = (mf_nonces_resp_t *)respbuf;
uint8_t *buf = response->nonces;
uint64_t ui64Key = bytes_to_num(payload->key, 6);
uint32_t cuid = 0;
int16_t isOK = PM3_SUCCESS;
uint16_t num_nonces = 0;
uint16_t num_pairs = 0;
uint8_t nt_par_enc = 0;
uint8_t cascade_levels = 0;
uint8_t blockNo = payload->blockno;
@@ -1139,7 +1149,7 @@ void MifareAcquireEncryptedNonces(const mf_acquire_nonces_t *payload) {
uint8_t prev_enc_nt[] = {0, 0, 0, 0};
uint8_t prev_counter = 0;
for (uint16_t i = 0; i <= (MFC_MAX_NONCES * 4) - 9;) {
while (num_pairs < MFC_MAX_NONCE_PAIRS) {
// Test if the action was cancelled
if (BUTTON_PRESS()) {
@@ -1196,14 +1206,15 @@ void MifareAcquireEncryptedNonces(const mf_acquire_nonces_t *payload) {
}
num_nonces++;
uint16_t ofs = num_pairs * MFC_NONCE_PAIR_SIZE;
if (num_nonces % 2) {
memcpy(buf + i, receivedAnswer, 4);
memcpy(buf + ofs, receivedAnswer, 4);
nt_par_enc = par_enc[0] & 0xf0;
} else {
nt_par_enc |= par_enc[0] >> 4;
memcpy(buf + i + 4, receivedAnswer, 4);
memcpy(buf + i + 8, &nt_par_enc, 1);
i += 9;
memcpy(buf + ofs + 4, receivedAnswer, 4);
buf[ofs + 8] = nt_par_enc;
num_pairs++;
}
@@ -1231,12 +1242,12 @@ void MifareAcquireEncryptedNonces(const mf_acquire_nonces_t *payload) {
LED_C_OFF();
crypto1_deinit(pcs);
LED_B_ON();
uint8_t respbuf[PM3_CMD_DATA_SIZE] = {0x00};
mf_nonces_resp_t *response = (mf_nonces_resp_t *)respbuf;
// only whole pairs are transferred. An abort or a static nonce bailout can
// leave a dangling first nonce, it has no parity nibble yet so it is dropped
uint16_t noncelen = num_pairs * MFC_NONCE_PAIR_SIZE;
response->cuid = cuid;
response->num_nonces = num_nonces;
uint16_t noncelen = MIN((uint16_t)(num_nonces * 4), (uint16_t)(MFC_MAX_NONCES * 4));
memcpy(response->nonces, buf, noncelen);
response->num_nonces = num_pairs * 2;
reply_ng(CMD_HF_MIFARE_ACQ_ENCRYPTED_NONCES, isOK, respbuf, sizeof(mf_nonces_resp_t) + noncelen);
LED_B_OFF();
+12 -3
View File
@@ -1733,9 +1733,18 @@ static int acquire_nonces(uint8_t blockNo, uint8_t keyType, uint8_t *key, uint8_
if (initialize == false) {
const mf_nonces_resp_t *nresp = (const mf_nonces_resp_t *)resp.data.asBytes;
uint16_t num_sampled_nonces = nresp->num_nonces;
uint16_t num_sampled_nonces = nresp->num_nonces & ~1; // nonces come in pairs
const uint8_t *bufp = nresp->nonces;
if (resp.length < sizeof(mf_nonces_resp_t) + ((num_sampled_nonces / 2) * MFC_NONCE_PAIR_SIZE)) {
PrintAndLogEx(FAILED, "Truncated nonce reply, got %u bytes for %u nonces", resp.length, num_sampled_nonces);
if (nonce_file_write) {
fclose(fnonces);
}
DropField();
return PM3_ESOFT;
}
for (uint16_t i = 0; i < num_sampled_nonces; i += 2) {
uint32_t nt_enc1 = bytes_to_num(bufp, 4);
uint32_t nt_enc2 = bytes_to_num(bufp + 4, 4);
@@ -1760,10 +1769,10 @@ static int acquire_nonces(uint8_t blockNo, uint8_t keyType, uint8_t *key, uint8_
num_acquired_nonces += add_res;
if (nonce_file_write) {
fwrite(bufp, 1, 9, fnonces);
fwrite(bufp, 1, MFC_NONCE_PAIR_SIZE, fnonces);
fflush(fnonces);
}
bufp += 9;
bufp += MFC_NONCE_PAIR_SIZE;
}
//total_num_nonces += num_sampled_nonces;
+8
View File
@@ -541,6 +541,14 @@ typedef struct {
// most 4 byte nonces that fit alongside the reply header in one frame
#define MFC_MAX_NONCES ((PM3_CMD_DATA_SIZE - sizeof(mf_nonces_resp_t)) / 4)
// ACQ_ENCRYPTED_NONCES does not store bare nonces. It stores pairs: two 4 byte
// encrypted nonces followed by one byte holding both their encrypted parity
// nibbles, high nibble first. num_nonces in the reply counts nonces, so the
// payload is always (num_nonces / 2) * MFC_NONCE_PAIR_SIZE bytes and
// num_nonces is always even.
#define MFC_NONCE_PAIR_SIZE 9
#define MFC_MAX_NONCE_PAIRS ((PM3_CMD_DATA_SIZE - sizeof(mf_nonces_resp_t)) / MFC_NONCE_PAIR_SIZE)
// CMD_HF_MIFARE_CHKKEYS_FAST payload.
// Replaces three bit-packed oldargs:
// arg0 = sectorcnt | firstchunk<<8 | lastchunk<<12 | singlesector_params<<16