|
| 1 | +#pragma author Bananz0 |
| 2 | +#pragma description MIFARE Classic EV1 4K (MF1S70yyX/V1), ISO/IEC 14443-3 Type A |
| 3 | +#pragma MIME application/x-mifare-classic |
| 4 | +#pragma endian little |
| 5 | + |
| 6 | +// References: |
| 7 | +// NXP MF1S70yyX/V1 Product Data Sheet, Rev. 3.2 |
| 8 | +// NXP AN10833, MIFARE type identification procedure, Rev. 3.9 |
| 9 | +// |
| 10 | +// Layout: sectors 0..31 × 4 blocks × 16 bytes (2048 bytes) |
| 11 | +// + sectors 32..39 × 16 blocks × 16 bytes (2048 bytes) |
| 12 | +// = 4096 bytes total, absolute blocks 0..255 |
| 13 | +// Sector trailer: last block of each sector (block 3 for small, block 15 for large) |
| 14 | +// Load this file for 4K dumps (file size exactly 4096 bytes, SAK = 0x18 or 0x38) |
| 15 | + |
| 16 | +import std.core; |
| 17 | +import std.io; |
| 18 | + |
| 19 | +// Sector Trailer |
| 20 | +// Identical encoding for small and large sectors. |
| 21 | +// Byte layout per MF1S70yyX/V1 Rev.3.2 §8.7.1, Figure 10: |
| 22 | +// acc0 (+6): ~C2_3 ~C2_2 ~C2_1 ~C2_0 ~C1_3 ~C1_2 ~C1_1 ~C1_0 |
| 23 | +// acc1 (+7): C1_3 C1_2 C1_1 C1_0 ~C3_3 ~C3_2 ~C3_1 ~C3_0 |
| 24 | +// acc2 (+8): C3_3 C3_2 C3_1 C3_0 C2_3 C2_2 C2_1 C2_0 |
| 25 | +// For large sectors, Cx_n indexes block groups, not individual blocks: |
| 26 | +// C1_0/C2_0/C3_0: blocks 0-4 C1_1/C2_1/C3_1: blocks 5-9 |
| 27 | +// C1_2/C2_2/C3_2: blocks 10-14 C1_3/C2_3/C3_3: trailer (block 15) |
| 28 | +// (MF1S70yyX/V1 Rev.3.2 §8.7.1, Table 6) |
| 29 | +// Delivery default: acc0=0xFF acc1=0x07 acc2=0x80 (MF1S70yyX/V1 §8.6.3) |
| 30 | + |
| 31 | +struct SectorTrailer { |
| 32 | + u8 keyA[6]; |
| 33 | + u8 acc0; |
| 34 | + u8 acc1; |
| 35 | + u8 acc2; |
| 36 | + u8 userData; |
| 37 | + u8 keyB[6]; |
| 38 | +}; |
| 39 | + |
| 40 | + |
| 41 | +// Manufacturer Block (absolute block 0, sector 0 only) |
| 42 | +// 4-byte NUID layout (MF1S703yX, single cascade level): |
| 43 | +// Bytes 0-3: NUID |
| 44 | +// Byte 4: BCC = NUID0^NUID1^NUID2^NUID3 (ISO/IEC 14443-3:2018 §6.5.3, |
| 45 | +// stored in block 0 by NXP hardware convention) |
| 46 | +// Byte 5: SAK (0x18 = native 4K; MF1S70yyX/V1 Rev.3.2 §9.4, Table 12) |
| 47 | +// Bytes 6-7: ATQA, wire order LSByte-first (AN10833 Rev.3.9 §2.3 Note 2) |
| 48 | +// 0x0002 = 4-byte NUID 4K (MF1S703yX) |
| 49 | +// 0x0042 = 7-byte UID 4K (MF1S700yX) |
| 50 | +// Bytes 8-15: Manufacturer data |
| 51 | +// Byte 9 = load modulation status (MF1S70yyX/V1 Rev.3.2 §11, Fig.14) |
| 52 | +// 0x20 = strong (default), 0x00 = normal |
| 53 | +// Note: 7-byte UID variants (MF1S700yX) have no BCC in block 0; use bytes 0-6 for UID. |
| 54 | +// This struct models 4-byte NUID only. |
| 55 | + |
| 56 | +struct ManufacturerBlock { |
| 57 | + u8 uid[4]; |
| 58 | + u8 bcc; |
| 59 | + u8 sak; |
| 60 | + u16 atqa; |
| 61 | + u8 manufacturerData[8]; |
| 62 | +}; |
| 63 | + |
| 64 | +// Data Block: 16 bytes (MF1S70yyX/V1 Rev.3.2 §8.6.2) |
| 65 | +// Value block mode layout (MF1S70yyX/V1 Rev.3.2 §8.6.2.1, Figure 8): |
| 66 | +// Bytes 0-3: Value (signed 32-bit LE), 4-7: ~Value, 8-11: Value |
| 67 | +// Bytes 12-15: Addr, ~Addr, Addr, ~Addr |
| 68 | + |
| 69 | +struct DataBlock { |
| 70 | + u8 data[16]; |
| 71 | +}; |
| 72 | + |
| 73 | + |
| 74 | +// Sector 0: manufacturer block + 2 data blocks + trailer (64 bytes) |
| 75 | +// (MF1S70yyX/V1 Rev.3.2 §8.6.2: "Sector 0 contains only two data blocks |
| 76 | +// and the read-only manufacturer block") |
| 77 | + |
| 78 | +struct Sector0 { |
| 79 | + ManufacturerBlock manufacturerBlock; |
| 80 | + DataBlock block1; |
| 81 | + DataBlock block2; |
| 82 | + SectorTrailer trailer; |
| 83 | +}; |
| 84 | + |
| 85 | +// Small Sector (sectors 1..31): 3 data blocks + trailer (64 bytes) |
| 86 | + |
| 87 | +struct SmallSector { |
| 88 | + DataBlock block0; |
| 89 | + DataBlock block1; |
| 90 | + DataBlock block2; |
| 91 | + SectorTrailer trailer; |
| 92 | +}; |
| 93 | + |
| 94 | +// Large Sector (sectors 32..39): 15 data blocks + trailer (256 bytes) |
| 95 | +// (MF1S70yyX/V1 Rev.3.2 §8.6: "8 sectors of 16 blocks") |
| 96 | + |
| 97 | +struct LargeSector { |
| 98 | + DataBlock block0; |
| 99 | + DataBlock block1; |
| 100 | + DataBlock block2; |
| 101 | + DataBlock block3; |
| 102 | + DataBlock block4; |
| 103 | + DataBlock block5; |
| 104 | + DataBlock block6; |
| 105 | + DataBlock block7; |
| 106 | + DataBlock block8; |
| 107 | + DataBlock block9; |
| 108 | + DataBlock block10; |
| 109 | + DataBlock block11; |
| 110 | + DataBlock block12; |
| 111 | + DataBlock block13; |
| 112 | + DataBlock block14; |
| 113 | + SectorTrailer trailer; |
| 114 | +}; |
| 115 | + |
| 116 | +// MIFARE Classic 4K: sector0 + 31 small + 8 large = 4096 bytes |
| 117 | +// 64 + 31×64 + 8×256 = 64 + 1984 + 2048 = 4096 |
| 118 | + |
| 119 | +struct MifareClassic4K { |
| 120 | + Sector0 sector0; |
| 121 | + SmallSector smallSectors[31]; |
| 122 | + LargeSector largeSectors[8]; |
| 123 | +}; |
| 124 | + |
| 125 | +// Validation |
| 126 | + |
| 127 | + |
| 128 | +fn validate_uid_bcc(ref MifareClassic4K card) { |
| 129 | + // BCC = XOR of all 4 UID bytes (ISO/IEC 14443-3:2018 §6.5.3) |
| 130 | + // Stored at block 0 byte 4 by NXP hardware convention. |
| 131 | + u8 computed = card.sector0.manufacturerBlock.uid[0] ^ |
| 132 | + card.sector0.manufacturerBlock.uid[1] ^ |
| 133 | + card.sector0.manufacturerBlock.uid[2] ^ |
| 134 | + card.sector0.manufacturerBlock.uid[3]; |
| 135 | + if (computed == card.sector0.manufacturerBlock.bcc) { |
| 136 | + std::print("BCC OK: 0x{:02X}", computed); |
| 137 | + } else { |
| 138 | + std::warning(std::format("BCC FAIL: stored=0x{:02X} computed=0x{:02X}", |
| 139 | + card.sector0.manufacturerBlock.bcc, computed)); |
| 140 | + } |
| 141 | +}; |
| 142 | + |
| 143 | +fn validate_sak(ref MifareClassic4K card) { |
| 144 | + // MF1S70yyX/V1 Rev.3.2 §9.4, Table 12: SAK = 0x18 |
| 145 | + // AN10833 Rev.3.9 Table 5: 0x38 = SmartMX emulation (0x18|0x20), not native silicon |
| 146 | + // AN10833 Rev.3.9 §3.1: bit 7 of SAK is a proprietary NXP bit (e.g., Originality Check |
| 147 | + // enabled variants) and must be masked out before type identification. |
| 148 | + u8 sak = card.sector0.manufacturerBlock.sak; |
| 149 | + u8 sak_typ = sak & 0x7F; // strip proprietary bit 7 |
| 150 | + bool prop = (sak & 0x80) != 0; |
| 151 | + if (sak_typ == 0x18) { |
| 152 | + std::print("SAK 0x{:02X}: MIFARE Classic 4K, native{}", sak, |
| 153 | + prop ? " (bit7 set: NXP proprietary/Originality Check variant)" : ""); |
| 154 | + } else if (sak_typ == 0x38) { |
| 155 | + std::warning(std::format("SAK 0x{:02X}: SmartMX/multi-MIFARE emulation of Classic 4K (AN10833 Rev.3.9, Table 5 Examples 2/3){}", |
| 156 | + sak, prop ? " + bit7 set" : "")); |
| 157 | + } else { |
| 158 | + std::warning(std::format("SAK 0x{:02X} (masked: 0x{:02X}): unexpected for Classic 4K", sak, sak_typ)); |
| 159 | + } |
| 160 | +}; |
| 161 | + |
| 162 | +fn validate_access_bits(u8 idx, ref SectorTrailer t) { |
| 163 | + // Three nibble-pair complement invariants (MF1S70yyX/V1 Rev.3.2 §8.7.1): |
| 164 | + // acc0[3:0] == ~acc1[7:4] acc0[7:4] == ~acc2[3:0] acc1[3:0] == ~acc2[7:4] |
| 165 | + u8 a0 = t.acc0; u8 a1 = t.acc1; u8 a2 = t.acc2; |
| 166 | + bool c1 = ((a0 & 0x0F) == ((~a1 >> 4) & 0x0F)); |
| 167 | + bool c2 = (((a0 >> 4) & 0x0F) == (~a2 & 0x0F)); |
| 168 | + bool c3 = ((a1 & 0x0F) == ((~a2 >> 4) & 0x0F)); |
| 169 | + if (c1 && c2 && c3) { |
| 170 | + std::print("Sector {:02d}: Access bits OK [{:02X} {:02X} {:02X}]", idx, a0, a1, a2); |
| 171 | + } else { |
| 172 | + std::warning(std::format("Sector {:02d}: Access bits FAIL [{:02X} {:02X} {:02X}] C1:{} C2:{} C3:{}", |
| 173 | + idx, a0, a1, a2, c1, c2, c3)); |
| 174 | + } |
| 175 | +}; |
| 176 | + |
| 177 | +// Entry point — unconditional file-scope placement for ImHex visualization |
| 178 | + |
| 179 | +MifareClassic4K card @ 0x00; |
| 180 | + |
| 181 | +validate_uid_bcc(card); |
| 182 | +validate_sak(card); |
| 183 | +validate_access_bits(0, card.sector0.trailer); |
| 184 | +for (u8 i = 1, i <= 31, i += 1) { |
| 185 | + validate_access_bits(i, card.smallSectors[i - 1].trailer); |
| 186 | +} |
| 187 | +for (u8 j = 0, j < 8, j += 1) { |
| 188 | + validate_access_bits(j + 32, card.largeSectors[j].trailer); |
| 189 | +} |
0 commit comments