简介面向Android NFC开发者的Mifare卡协议分析与读取实践资料包围绕Classic与Ultralight两类卡的13.56MHz通信、扇区认证及读写命令展开适合具备一定Android基础、希望打通卡片识别、密钥认证与块读写流程的开发者用于项目参考和调试对照。压缩包共245个文件、约10.12MB以Java源码、XML布局、TXT说明、PNG截图、JSON数据以及BIN、SO、CMake、Ninja等构建和原生库文件为主并含APK成品便于从工程结构、界面资源到运行产物整体查阅。已有454人学习下载说明其在NFC读卡实践上具有一定参考价值。资料可帮助读者理解MifareClassic连接、认证、读写与关闭连接的实现脉络同时覆盖卡片类型识别、认证失败、读写异常等排错场景对门禁、公交、支付等非接触卡应用的学习与二次开发有直接帮助。1. 结论先行读一张 MF 卡真正难的不是“读”做 MF卡协议分析 的人迟早会撞上一个反直觉的事实卡片从来没有“把内容推给读卡器”这回事。射频场里只有读卡器在说话卡片能做的仅仅是根据磁场变化调整自己的负载用负载调制的深浅来表示 0 和 1。你发出的每一条命令、收到的每一个字节都是在这条极窄的单向信道上靠时序和编码一点点协商出来的。真正让这件事变复杂的不是读而是准入一张 MF 卡的 16 个扇区各自独立上锁你要先向它证明你知道密钥它才会把那一块 16 字节交给你。认证用的是流密码命令要拼帧、要算 CRC、要卡在几十微秒的帧间隔里。把这些层剥开你才会明白为什么有人读卡读到一半突然全是超时也才会明白为什么这套 30 年前的设计今天还活着。2. 从射频场到选卡ATQA、UID 与位级防冲突读卡器上电后做的第一件事不是读是喊。它持续发射 13.56 MHz 的载波同时在载波上做 100% 幅度调制用 Miller 编码把命令编码成“有场/无场”的序列。卡片靠感应取电一旦得电就开始监听。整个 14443-A 的物理层就建立在这个不对称上读卡器说话用调幅卡片说话用负载调制副载波频率是载波频率的 1/16。2.1 三种帧型短帧、标准帧、位帧协议里一共只有三种帧型认错帧型是最常见的低级错误。短帧只有 7 个 bit专门用来发 REQA0x26和 WUPA0x52这类“广播式唤醒”所有处于 IDLE 状态的卡都会应答。标准帧是 8 bit 的整数倍后面跟 2 字节 CRC-A用于绝大多数命令和数据。位帧是防冲突专用的半成品帧它的长度不是 8 的整数倍收发双方靠“已经确认了多少位”来对齐。短帧之所以只发 7 位是个很省事的设计7 位里不可能出现完整的字节边界卡片就不会把唤醒命令误当成某条数据命令的前半截。这个细节在调试时特别有用——如果你的读卡器连 ATQA 都收不到先怀疑它到底有没有发短帧而不是去怀疑天线。帧型长度典型用途是否带 CRC短帧7 bitREQA / WUPA否标准帧8 bit 整数倍选卡、认证、读写是2 字节位帧非 8 整数倍防冲突应答否2.2 ATQA 与 UID 级联一张卡可能有三段身份REQA 发出去以后场里所有处于 IDLE 的卡同时回一个 2 字节的 ATQA。这个 2 字节不是随便填的它编码了 UID 的长度4、7 或 10 字节以及是否支持位级防冲突。读卡器拿到 ATQA 之后才知道接下来该按几级级联去要 UID。级联这件事经常被忽略。4 字节 UID 的卡只需要一级7 字节的要两级10 字节的要三级。每一级都遵循同一个套路先发防冲突命令卡片回 5 个字节4 字节本级 UID 片段 1 字节 BCC读卡器校验 BCC 后发 SELECT 把这个片段锁死再看返回的 SAK 里的级联位决定要不要继续。BCC 就是那 4 个字节的异或实现上只有三行def build_select(sel_byte, uid_cl): sel_byte: 0x93/0x95/0x97 分别对应级联级 1/2/3 uid_cl: 卡在本级返回的 4 字节7 字节 UID 的卡第一级是 [0x88, u0, u1, u2] bcc 0 for b in uid_cl: bcc ^ b # BCC 是 4 字节的异或校验用来挡住传输误码 return bytes([sel_byte, 0x70]) bytes(uid_cl) bytes([bcc])注意第二个字节0x70它叫 NVBNumber of Valid Bits单位是“字节”0x70 表示这一帧一共 7 个字节有效。防冲突阶段的 NVB 是 0x20表示只发了 2 个字节、剩下的靠卡片自己补。这个字段是位帧能和标准帧共存的关键同一套命令结构靠 NVB 一个字节就在“我发 2 个字节”和“我发 7 个字节”之间切换。2.3 位级仲裁冲突位是怎么被“读”出来的如果场里同时有两张卡它们会在同一时刻用不同深浅的负载调制回应读卡器解出来的波形就是两个信号的叠加。14443-A 用的是“线与”语义只要有一张卡在某个位上调制成 1这个位就一定是 1只有当所有卡都在这一位上调制成 0读卡器才收到 0。反过来说读卡器一旦收到 0就说明所有在场的卡在这一位上都发了 0一旦收到 1就说明至少有一张卡发了 1而且可能还有卡发了 0——这就是冲突。这个设计的巧妙之处在于读卡器不需要“检测到冲突就报错重来”。它只要在冲突位上固定成某一个值继续往下问就能把其中一张卡筛选出来。实际实现里读卡器会在冲突位选择一个分支选 0 或选 1然后发一个 NVB 更长的防冲突帧把“这一位我定了你们谁对不上就闭嘴”这个信息广播出去。一轮筛不完就多轮直到只剩一张卡应答。代价也很明显这是只有一位的仲裁信道位数一多轮次就上去了。工程上常见的优化是先把 UID 的前几位拿到手再做分支减少无谓的往返。这也是为什么同一张卡在不同读卡器上的选卡耗时能差出好几毫秒——不是玄学是仲裁策略不同。2.4 SAK选卡结束的那一纸“身份证”最后一级 SELECT 完成后卡片回一个字节的 SAK 加 2 字节 CRC。SAK 告诉读卡器“我是哪一类卡、UID 是否已经完整”。如果 SAK 的 bit2值 0x04是 1说明 UID 还没取全读卡器必须回到上一级继续级联如果是 0选卡流程才算真正结束卡片进入 ACTIVE 状态可以接收读写命令了。这一步之后卡片对读卡器的态度就完全变了。之前它只认广播命令之后它只认带块地址的定向命令而且——除了少数几条——所有命令都必须先通过认证。选卡只是拿到了入场券真正的门在下一个扇区里。3. 存储结构16 个扇区、三字节访问位与值块的自我证明MF 卡让人又爱又恨的地方在于它把“权限”直接写进了存储介质本身。没有文件系统、没有目录树、没有元数据区只有一块 1KB或 4KB的线性空间被切成扇区每个扇区的最后一块是“尾块”尾块里躺着这个扇区的钥匙和门禁规则。这种设计在 1994 年是极致的成本控制在今天则是大量踩坑的源头。3.1 扇区与块的布局1K 容量的卡是 16 个扇区每个扇区 4 个块每块 16 字节正好 1024 字节。4K 容量的卡前 32 个扇区还是每扇区 4 块后面 8 个扇区每扇区 16 块块号编排要单独处理。块的编号是全局线性的所以“第 4 扇区第 0 块”等于全局第 16 块这个换算写错一次就够你调试半天。绝对的第 0 块是厂商块出厂时烧死只读。它的前 4 个字节是 UID第 5 个字节是 UID 的 BCC第 6 个字节是 SAK第 7、8 字节是 ATQA剩下的是厂商数据。很多应用图省事直接把这 16 个字节当唯一标识用结果碰到 7 字节 UID 的卡就翻车——厂商块里只放了第一级联的 4 个字节。块号范围内容典型权限0厂商块UID/BCC/SAK/ATQA出厂只读1~2数据块由访问位决定3扇区 0 尾块KeyA 访问位 KeyB4~62数据块 每 4 块的尾块由各自访问位决定63扇区 15 尾块KeyA 访问位 KeyB3.2 尾块的 16 字节怎么分配每个扇区的最后一块是尾块结构固定前 6 字节 KeyA中间 3 字节访问位1 字节通用目的字节后 6 字节 KeyB。KeyA 在射频层永远是“只写不读”的——你发认证命令时可以把 KeyA 明文送出去但没有任何一条命令能把 KeyA 读回来。访问位这 3 个字节是整个设计里最反人类的部分。它同时定义了扇区内 4 个块各自的读、写、加减、传输权限还要额外定义“尾块自己”的权限。3 个字节要表达 4 个块 × 5 种操作 × 2 把钥匙靠的是查表而不是位运算——每块用 3 个 bit 组成一个 0~7 的条件码条件码再去查一张固定表格。这就意味着改访问位从来不是“改一个 bit”而是“换一个条件码”换完之后整块的语义都变了。3.3 三字节冗余与自校验解码访问位的 3 个字节里每个 bit 都存了两遍一遍原码一遍反码。这不是为了容错是为了防写坏如果三个字节不满足互补关系卡片会认为尾块已经被破坏整个扇区可能直接不可用。解码时把这份冗余利用起来可以在读之前就判断尾块是否健康def decode_trailer(trailer): trailer: 尾块 16 字节返回 4 个块的访问条件0~7按 C1C2C3 排列 b6, b7, b8 trailer[6], trailer[7], trailer[8] c1 (b7 4) 0x0F # C1 原码在 b7 高半字节 c2 b8 0x0F # C2 原码在 b8 低半字节 c3 (b8 4) 0x0F # C3 原码在 b8 高半字节 # 三个字节是互补存放的任何一份对不上都说明尾块被写坏过 if ((b6 0x0F) ^ c1) ! 0x0F: raise ValueError(C1 冗余校验失败尾块可能已损坏) if (((b6 4) 0x0F) ^ c2) ! 0x0F: raise ValueError(C2 冗余校验失败) if ((b7 0x0F) ^ c3) ! 0x0F: raise ValueError(C3 冗余校验失败) conds [] for i in range(4): # i0..3 对应扇区内 4 个块 bit1 (c1 i) 1 bit2 (c2 i) 1 bit3 (c3 i) 1 conds.append((bit1 2) | (bit2 1) | bit3) # 拼成 C1C2C3 return conds这段代码里最关键的不是拼位而是那三个if。它们把“卡片不告诉你尾块坏了”这个黑匣子打开了一条缝——很多莫名其妙的认证失败根因就是前人写尾块时算错了反码。拿到条件码之后再去查表数据块和尾块用的是两张不同的表别混用条件码 C1C2C3数据块读数据块写备注000KeyA / KeyBKeyA / KeyB完全开放出厂默认010KeyA / KeyB禁止只读数据100KeyA / KeyBKeyB常见的“用户可写”配置110KeyA / KeyBKeyB加减操作也归 KeyB001KeyA / KeyB禁止值块常用可减不可写011KeyBKeyBKeyA 完全失效101KeyB禁止最严格的可读配置111禁止禁止彻底锁死3.4 值块自带校验和的数据结构除了普通数据块MF 卡还有一种“值块”专门给电子钱包这类场景用。它的 16 字节被切成四段4 字节小端数值、4 字节该值的按位取反、再来 4 字节原值、最后 4 字节是地址和它的两次取反。加减操作由卡片硬件原子完成做完之后自动把三段冗余同步更新应用层不需要自己算。这套结构的价值在于离线可验证。你从块里读出 16 个字节不用问任何人自己就能判断它是不是一个合法的值块def decode_value_block(raw): raw: 16 字节返回数值非法则返回 None if len(raw) ! 16: return None value int.from_bytes(raw[0:4], little) inv int.from_bytes(raw[4:8], little) value2 int.from_bytes(raw[8:12], little) addr, naddr, addr2, naddr2 raw[12], raw[13], raw[14], raw[15] # 三段冗余必须同时自洽任一不成立就说明这不是值块或已被写坏 if value ! value2 or (value ^ inv) ! 0xFFFFFFFF: return None if addr ! addr2 or (addr ^ naddr) ! 0xFF or (addr ^ naddr2) ! 0xFF: return None return {value: value, block_addr: addr}addr字段存的是这块自己的块号这是个非常聪明的自证机制把块挪到别的位置校验立刻就不过。实测中把它当“块内数据”而不是“块编号”来填是新手最容易犯的错之一填错了卡不会报错只会在下一次加减时给你一个看不懂的结果。4. 认证与加密Crypto1 的握手、密钥流与熵缺口扇区上了锁读之前必须先认证。这一步是整个 MF 卡协议里唯一涉及密码学的地方也是这套设计最被诟病的地方。理解它的关键不在于“密钥多长”而在于握手过程中有多少真正的随机性——这个数字小得惊人。4.1 三次握手谁先出牌认证命令是 2 个字节0x60表示用 KeyA0x61表示用 KeyB后面跟 1 字节块地址和 6 字节密钥共 9 字节发出去。注意密钥是明文上行到卡片的卡片自己比对读卡器并不知道密钥对不对——它只知道卡片后面还理不理它。卡片如果认这把钥匙会先回 4 字节 Nt这是卡片生成的随机数。读卡器拿到 Nt 后用它和密钥算出一串应答把自己的随机数 Nr 和另一段校验值加密后发回去共 8 字节。卡片验证通过后再回 4 字节密文双方各自推导出会话密钥握手结束。此后这条链路上的所有数据、甚至每个字节的奇偶校验位全都用同一套密钥流异或。这个流程里有个细节值得注意认证是按扇区建立的不是按块。命令里虽然有块地址但卡片只取它的高位来决定是哪个扇区。同一个扇区里读完第 0 块接着读第 3 块不需要重新认证一旦跨扇区认证状态立刻失效必须重来。批量读整卡时这个特性直接决定你的循环怎么写。4.2 48 位 LFSR密钥流是怎么“流”出来的Crypto1 是一个流密码核心是一个 48 位的线性反馈移位寄存器。每来一个时钟寄存器整体右移一位最高位由若干个抽头的异或补上同时从一个 20 位的非线性布尔函数里取出一位作为密钥流输出。加密就是明文和密钥流逐位异或解密完全一样——异或两次就还原了。真正的初始化过程比“装载密钥”复杂48 位密钥先被灌进寄存器然后把 Nt 一位一位喂进去每喂一位就时钟一次期间产生的密钥流用来加密读卡器发回的应答。所以最终寄存器里的状态既包含密钥也包含 Nt。这个“混合”步骤是整个方案安全性的全部依托。非线性函数是这个设计里唯一的“硬度”来源。纯线性的 LFSR 用 Berlekamp-Massey 之类的方法几十个比特就能反推加一个非线性滤波函数之后代数攻击的复杂度才被抬起来。以 90 年代初的算力衡量这个组合是相当扎实的。4.3 熵缺口32 位随机数撑不起 48 位状态问题出在 Nt 上。它是 32 位但寄存器的状态是 48 位。也就是说一次握手真正引入的新鲜随机性只有 32 位剩下 16 位的差异完全来自密钥。如果攻击者能拿到同一个 Nt 下的两组密文密钥流就完全一样两组密文异或一下明文的关系直接暴露。而实际部署中大量卡片用的是弱随机数发生器——有的只有 16 位熵上电后从固定种子开始递增有的干脆在复位后重复同一个序列。这就让“同一个 Nt 出现两次”从理论上的小概率事件变成了可观察的常态。即使 Nt 不重复协议里还漏了另一条信息奇偶校验位是明文可观测的。每个字节 8 位数据配 1 位奇偶校验9 位里那 1 位在认证阶段是可见的相当于每 8 位密钥流就漏出 1 个线性约束。公开的分析工作正是靠这些约束做过滤把 48 位状态的搜索空间压下来。常见的说法是量级从 2 的 48 次方降到 2 的 40 次方附近再配合非线性的代数性质进一步剪枝——具体数字各家实现不一但结论是统一的用今天的机器这张卡挡不住有耐心的分析者。需要说清楚的是这不代表这套设计当年是错的。它的安全模型从一开始就偏向“防误刷、防低成本克隆”而不是“防有物理接触机会的专业分析者”。把 1994 年的成本和功耗预算摆进去看48 位 LFSR 加一个非线性函数已经是相当克制的选择——再多几位芯片面积和认证延迟都要涨。4.4 一次完整的读取链路把前面几层拼起来读一个扇区的代码其实很短短到容易让人低估每一步的失败率KEY_A bytes.fromhex(FFFFFFFFFFFF) # 出厂默认密钥实际项目里必须换 SECTOR 4 CMD_AUTH_A 0x60 # 0x60 用 KeyA0x61 用 KeyB CMD_READ 0x30 # 读单块返回 16 字节 def read_sector(nfc, sector, key, cmd_authCMD_AUTH_A): nfc 是读卡模块的薄封装内部只做拼帧/发帧/收帧 blocks [] for i in range(4): block sector * 4 i # 1K 卡每扇区 4 块 if i 0: # 认证以扇区为单位每扇区只做一次跨扇区必须重来 if not nfc.mifare_auth(block, cmd_auth, key): raise RuntimeError( f扇区 {sector} 认证失败密钥错、密钥类型错或访问位禁止该密钥读) blocks.append(nfc.mifare_read(block)) # 单块读取固定 16 字节 return blocks三个容易出错的参数都在注释里sector * 4 i的换算是全局块号0x60和0x61只差 1 但语义完全相反认证必须在读之前、且以扇区为粒度。把read_sector套一层循环就能读整卡但循环里一旦有某个扇区认证失败异常会直接中断整轮——真实项目里更稳妥的做法是把每个扇区的结果包成(sector, ok, blocks)三元组让失败可枚举而不是让一个扇区拖垮整张卡。5. 避坑与排查五条踩出来的血泪经验协议层面的道理讲清楚了落地时还是会栽在一些非常具体的地方。下面这几条都是“现象看着像 A实际原因是 B”的典型按现象、原因、解决三段的顺序记下来比看十页手册管用。5.1 五条高频故障的定位路径只能读到 UID读块全部超时。现象是选卡顺利、SAK 正常、一发0x30就静默。原因几乎一定是没认证或者认证用的密钥类型字节写反了——用 KeyB 认证却发0x60卡片不会报错只会直接不回话。解决的办法是把认证和读拆成两步分别打日志先确认认证返回了那 4 字节密文再去发读命令。单张卡正常批量时失败率飙到一成以上。现象是同一批卡在实验室里怎么读都行上了产线就开始随机失败。原因通常是场里有不止一张卡或者天线和卡的距离/角度漂了导致负载调制解不出来。解决上先做物理隔离和定位再在软件层加有限次重试——注意重试前要先发 HALT0x50把上一张卡彻底踢出 ACTIVE 状态否则它会一直在场里应答。读回来的 16 字节像乱码但确实读到了。现象是不超时、不报错内容每次还不一样。原因有两个方向一是这块在访问位上被配成值块你按普通字节去理解当然读不懂二是应用层自己在数据块里又叠了一层加密或序列化。解决的顺序是从尾块解出条件码先看这块到底被定义成什么类型的块再决定按值块格式解析还是按应用协议解。改了尾块之后整张卡读不了了。这是最不可逆的一种。现象是原本能读的扇区突然全部认证失败。原因是访问位写错把 KeyA 变成了不可读、或者给自己上了 111 条件码而卡片本身不提供任何“撤销”入口。解决只能靠事前备份动尾块之前先把整扇区 4 块读出来存一份写之前用 3.3 节的冗余校验函数过一遍新值确认互补关系成立再写。写尾块没有后悔药。同一张卡在不同读卡器上表现不一致。现象是 A 设备读得出某扇区B 设备读不出。原因往往是读卡模块固件层面的差异有的固件在底层替你做了自动认证和命令重试有的把 ATQA 解析成本文还有配套的精品资源点击获取