简介:面向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,专门用来发 REQA(0x26)和 WUPA(0x52)这类“广播式唤醒”,所有处于 IDLE 状态的卡都会应答。标准帧是 8 bit 的整数倍,后面跟 2 字节 CRC-A,用于绝大多数命令和数据。位帧是防冲突专用的半成品帧,它的长度不是 8 的整数倍,收发双方靠“已经确认了多少位”来对齐。
短帧之所以只发 7 位,是个很省事的设计:7 位里不可能出现完整的字节边界,卡片就不会把唤醒命令误当成某条数据命令的前半截。这个细节在调试时特别有用——如果你的读卡器连 ATQA 都收不到,先怀疑它到底有没有发短帧,而不是去怀疑天线。
| 帧型 | 长度 | 典型用途 | 是否带 CRC |
|---|---|---|---|
| 短帧 | 7 bit | REQA / 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,它叫 NVB(Number 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 + 访问位 + KeyB |
| 4~62 | 数据块 + 每 4 块的尾块 | 由各自访问位决定 |
| 63 | 扇区 15 尾块 | KeyA + 访问位 + KeyB |
3.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): # i=0..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 | 数据块读 | 数据块写 | 备注 |
|---|---|---|---|
| 000 | KeyA / KeyB | KeyA / KeyB | 完全开放,出厂默认 |
| 010 | KeyA / KeyB | 禁止 | 只读数据 |
| 100 | KeyA / KeyB | KeyB | 常见的“用户可写”配置 |
| 110 | KeyA / KeyB | KeyB | 加减操作也归 KeyB |
| 001 | KeyA / KeyB | 禁止 | 值块常用,可减不可写 |
| 011 | KeyB | KeyB | KeyA 完全失效 |
| 101 | KeyB | 禁止 | 最严格的可读配置 |
| 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表示用 KeyA,0x61表示用 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 用 KeyA,0x61 用 KeyB CMD_READ = 0x30 # 读单块,返回 16 字节 def read_sector(nfc, sector, key, cmd_auth=CMD_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 字节密文,再去发读命令。
单张卡正常,批量时失败率飙到一成以上。现象是同一批卡在实验室里怎么读都行,上了产线就开始随机失败。原因通常是场里有不止一张卡,或者天线和卡的距离/角度漂了导致负载调制解不出来。解决上先做物理隔离和定位,再在软件层加有限次重试——注意重试前要先发 HALT(0x50)把上一张卡彻底踢出 ACTIVE 状态,否则它会一直在场里应答。
读回来的 16 字节像乱码,但确实读到了。现象是不超时、不报错,内容每次还不一样。原因有两个方向:一是这块在访问位上被配成值块,你按普通字节去理解当然读不懂;二是应用层自己在数据块里又叠了一层加密或序列化。解决的顺序是从尾块解出条件码,先看这块到底被定义成什么类型的块,再决定按值块格式解析还是按应用协议解。
改了尾块之后整张卡读不了了。这是最不可逆的一种。现象是原本能读的扇区突然全部认证失败。原因是访问位写错,把 KeyA 变成了不可读、或者给自己上了 111 条件码,而卡片本身不提供任何“撤销”入口。解决只能靠事前备份:动尾块之前先把整扇区 4 块读出来存一份,写之前用 3.3 节的冗余校验函数过一遍新值,确认互补关系成立再写。写尾块没有后悔药。
同一张卡在不同读卡器上表现不一致。现象是 A 设备读得出某扇区,B 设备读不出。原因往往是读卡模块固件层面的差异:有的固件在底层替你做了自动认证和命令重试,有的把 ATQA 解析成
本文还有配套的精品资源,点击获取