简介:面向Android与物联网开发者的Mifare卡协议分析及NFC读卡实践资料,围绕MF卡的SELECT、AUTHENTICATE、READ/WRITE命令和扇区密钥机制展开,配套可运行的工程与调试素材,适合需要把卡片认证、块读写落到代码中的初中级开发者。压缩包共245个文件,约10.12MB,以xml布局与配置、java源码、txt说明、png截图、json配置和bin数据为主,另有cmake/ninja构建产物、so库与gradle脚本,兼顾源码、编译缓存和测试安装包,便于对照排查环境问题。目前已有454人学习。读者可获得NfcAdapter读卡模式注册、MifareClassic连接认证与块读写、异常处理和交互提示等实现参考,并借助协议解析材料梳理扇区、密钥与访问控制关系,形成从协议理解到Android端读卡验证的学习样本。
1. 拿一张 MF 卡贴到读卡器上时,屏幕跳出一串十六进制,看起来像“把内容读出来了”。真实发生的过程却要复杂得多:整场对话里,卡片从来没有主动发送过任何一个字节,它只是根据 13.56 MHz 载波的强弱,反复开合自己的天线负载。做 MF卡协议分析,本质上分析的就是这套被动式半双工规则——读卡器供电、读卡器定节拍、读卡器决定下一句问什么,卡片唯一的自由,是在载波上“敲”出 0 和 1。
之所以值得把这层扒开,是因为从“贴上去”到“拿到 16 字节”,中间横着四道关:寻卡、防冲突、选卡、三次认证。每一道关都对应一个具体的数据结构和一个具体的失败码。分不清是哪一道没通过,就只能对着“读卡失败”这四个字反复换设备、换手机、换姿势——最后归因成玄学。
2. 寻卡与防冲突:ATQA、SAK 和 UID 如何把“一堆卡”统一成“一张卡”
2.1 从射频场到 ATQA:卡片是怎么“开口”的
读卡器只要上电,天线就会持续输出未经调制的载波,这叫射频场(RF field)。场里没有卡时,一切都是安静的;一旦有一张卡进入场区,卡片内部的整流电路从载波里“取电”,芯片上电复位,然后进入等待状态。
注意这个顺序:卡片是先被供电,然后才能说话。所以读卡器永远不知道场里什么时候多了卡——它只能定期喊。这一声喊就是 REQA,一个 7 位的短帧,值0x26(二进制0100110)。之所以用 7 位而不是 8 位,是因为短帧没有校验位、没有帧尾,长度固定,可以用最省电、最快的方式发出去。
场里所有处于 IDLE 状态的卡片收到 REQA 后,会一起回一个 2 字节的应答,叫 ATQA(Answer To Request type A)。Mifare Classic 1K 的 ATQA 是0x0004,4K 是0x0002,Mini 是0x0004但 SAK 不同。ATQA 的位域里编码了两件事:UID 的长度(单倍/双倍/三倍),以及这张卡是否支持按位防冲突。
工程上有个很常见的误解:看到 ATQA 就以为已经“找到卡”了。其实 ATQA 只是一句“我在”,此时卡片还在 IDLE 状态,既没有 UID 被选定,也没有进入可以认证的 ACTIVE 状态。直接跳过后续步骤去发认证命令,卡片根本不会理你——这就是很多人第一次写读卡程序时遇到的“ATQA 有了但什么都读不到”。
除了 REQA,还有一个 WUPA,值0x52。区别在于 REQA 只能唤醒 IDLE 状态的卡,而 WUPA 能唤醒 IDLE和HALT 状态的卡。HALT 是卡片被显式“挂起”后的状态,用 HLTA 命令(0x50 0x00)进入。如果你的程序里做过一次选卡又没有复位,再发 REQA 是没反应的,得用 WUPA,或者干脆断一下场重新上电。
2.2 防冲突:位碰撞与 CL1/CL2/CL3 的级联
场里只有一张卡时,读卡器发什么它答什么。但现实是场里可能有三张卡——一张门禁卡、一张公交卡、一张贴着玩的空白卡。三张卡同时应答,载波上的负载调制会叠加,读卡器收到的是三个 UID 的按位“或”,这就是碰撞。
ISO14443-A 的解法不是让卡随机退避(那是以太网的做法),而是让读卡器主导逐位仲裁。读卡器发一条 SEL 命令,格式是SEL + NVB,其中 SEL 是级联标号:0x93对应 CL1,0x95对应 CL2,0x97对应 CL3;NVB(Number of Valid Bits)告诉卡片“我这次要你回多少位”。
# 构造 SEL 帧:请求完整 UID(NVB = 0x20,表示 SEL 和 NVB 两个字节都有效) def sel_frame(cascade_level: int, nvb: int = 0x20) -> bytes: # cascade_level 取 0x93 / 0x95 / 0x97,对应单倍/双倍/三倍 UID return bytes([cascade_level, nvb]) # 卡片应答 5 字节:4 字节 UID 片段 + 1 字节 BCC 校验 # BCC = UID0 ^ UID1 ^ UID2 ^ UID3,任何一位翻转都会导致校验失败 def check_bcc(resp: bytes) -> bool: if len(resp) != 5: return False return (resp[0] ^ resp[1] ^ resp[2] ^ resp[3]) == resp[4]NVB = 0x20的含义是:SEL 和 NVB 这两个字节是“有效位”,剩下的位让卡片自己填。此时场里所有卡同时回 5 个字节,读卡器逐位比较,遇到第一个分歧位就停下来,把“已知前缀 + 分歧位 + 1”写进 NVB,重发一次。NVB 的编码规则是:高 4 位表示完整字节数,低 4 位表示额外的位数。所以0x20是 2 个完整字节、0 个额外位;0x25是 2 个完整字节加 5 位;一直到0x70,表示 7 个字节全都要。
这套机制叫按位防冲突,它的代价是收敛轮数取决于 UID 的分布。两张 UID 只差最后一位的卡,要走到第 32 轮才分得开。实际读卡器固件通常会在 NVB 推进时做优化,遇到已经唯一的前缀就一次性要完整 UID,这也是为什么有的读卡器读多卡很快,有的慢得像卡住。
BCC 那个字节容易被忽略,但它是排错的关键。BCC 是 4 个 UID 字节的异或,如果读回来的 5 个字节异或结果不为 0,说明传输过程中有误码,必须重来。很多“读到 UID 但时对时错”的问题,根子就在天线调谐不好导致的误码,而不是协议栈写错了。
双倍 UID 的卡还有一个细节:第一级应答是0x88 + UID0 + UID1 + UID2 + BCC,那个0x88叫 CT(Cascade Tag),是告诉读卡器“我还没说完,请继续发下一级 SEL”。读卡器看到 CT 就要发0x95,而不是把这 5 个字节当成最终 UID。多级级联最多到 CL3,也就是 10 字节 UID。
2.3 SAK 决定后续识别路径
选卡的最后一步,读卡器发SEL + NVB(0x70)加 4 字节 UID 加 BCC,卡片回一个 3 字节的 SAK(Select Acknowledge)。SAK 是最容易被低估的一个字节,它直接决定了你后面该用哪套命令集。
| SAK 值 | 卡类型 | 后续命令 |
|---|---|---|
0x08 | Mifare Classic 1K | 0x60/0x61 认证 + 0x30 读块 |
0x18 | Mifare Classic 4K | 同上,但块号范围到 255 |
0x09 | Mifare Mini | 同上,扇区数更少 |
0x20 | 符合 ISO14443-4 的卡 | 进入 RATS,走 APDU |
0x00 | 只回 UID,不声明类型 | 需要自己猜 |
这张表解释了一个高频困惑:为什么同样的读卡代码,读 A 卡正常,读 B 卡就返回一串0x04的 NAK。因为 B 卡的 SAK 是0x20,它走的是 T=CL 的 APDU 通道,你拿 Mifare Classic 的块读命令0x30去问它,它当然听不懂。协议分析的第一步不是看数据,是看 SAK。
3. Crypto1 认证:为什么知道密钥也可能读不动,不知道密钥却可能读得动
3.1 三次认证的四个报文
选卡成功之后,卡片进入 ACTIVE 状态,此时它只认两类命令:认证和读写。认证命令是0x60(用 Key A 认证)或0x61(用 Key B 认证),后面跟一个字节的绝对块号——注意是块号,不是扇区号,这是新手最容易搞混的地方之一。
整个认证过程是四个报文:
- 读卡器 → 卡:
0x60+ 块号 - 卡 → 读卡器:
nt,4 字节明文随机数 - 读卡器 → 卡:
nr ⊕ ks1(4 字节)、ar ⊕ ks2(4 字节) - 卡 → 读卡器:
at ⊕ ks3(4 字节)
四个报文走完,双方各自握有nt / nr / ar / at四个 32 位随机数和那把 48 位的密钥,同时初始化 Crypto1 状态机。此后这条链路上所有的数据都被密钥流异或,包括你发的读块命令和卡片返回的 16 字节。所以从 wire 上看,读块返回的从来不是明文。
这里有个设计上的关键点:认证是按扇区生效的,不是按卡。你用 Key A 认证了扇区 3,就只能读扇区 3 的块;要读扇区 4,得再走一遍完整的四次报文。这也是为什么读一整张 1K 卡(16 个扇区)要发 16 次认证,每次都会消耗几十毫秒——不是程序写得慢,是协议就这么设计的。
3.2 Crypto1 的结构:48 位 LFSR 与非线性滤波
Crypto1 的骨架是一个 48 位的线性反馈移位寄存器(LFSR),加上一个从 20 个抽头取值的非线性滤波函数 f。每来一个时钟,f 从当前状态算出一个输出位,这个位既是密钥流的一位,同时又参与控制 LFSR 是否推进——这种时钟控制是为了打乱纯粹的线性关系,让它看起来不像个纯 LFSR。
密钥 48 位,加上四个 nonce 一共 96 位,装进状态里。听起来密钥空间是 2^48,暴力搜也不现实。真正的问题不在密钥长度,而在结构:
- 非线性滤波函数 f 只有 20 个输入抽头,而 LFSR 是 48 位。这意味着 f 的输出对状态的“可见范围”是受限的,存在可被利用的相关性。
- 四个 nonce 里,
nt是明文发出的,只有 32 位。32 位熵意味着同一张卡在一次会话内的随机性极其有限。 - 认证成功后,密钥流仍在继续,读卡器发的每一条命令、卡片回的每一块数据都是明文异或同一个密钥流。相当于给了攻击者大量已知明文-密文对。
这三条叠在一起,就构成了后来一系列密钥恢复方法的理论基础。机制上的失守点非常清晰:Crypto1 把“机密性”和“认证”绑在同一把 48 位密钥上,而认证过程本身又主动泄露了 32 位状态。一旦攻击者能观测到足够多的(nonce, 密钥流)对,48 位状态就可以通过分区穷举把复杂度压到工程可承受的量级。
从设计取舍的角度看,1994 年的这张卡面对的场景是门禁和公交:卡片成本要压到几毛钱,芯片面积、功耗、认证延迟都是硬约束。48 位密钥在当时不算离谱。真正的问题是这套设计沿用了二十年,威胁模型早就变了。
常见的实现思路里,恢复密钥的路径大致分三种。第一种最简单:试默认密钥。大量出厂未改的卡,六个扇区密钥全是FF FF FF FF FF FF,尾块访问位是FF 07 80 69。第二种叫嵌套认证——先用已知密钥认证一个扇区,把卡片玩到“状态已知”,再对未知扇区发起认证,此时卡片回的nt被已知密钥流加密,一异或就出明文。拿到明文 nonce 后,48 位密钥可以分段搜。第三种针对随机数质量好的卡,需要收集同一张卡上大量不同 nonce 的样本,靠的是滤波函数输出与 LFSR 状态之间的相关性做统计区分。
要强调的是,这些手段只应该在你拥有或获得明确授权的卡片和系统上使用。协议分析的价值在于理解机制边界,不在于拿到别人系统的通行证。
3.3 尾块布局:Key A / 访问位 / Key B 的 16 字节怎么分
每个扇区的最后一块(1K 卡是每扇区的第 3 块,4K 卡的后 8 个扇区是第 15 块)是尾块,结构和普通数据块完全不同:
字节 0..5 : Key A(6 字节,读永远返回全 0) 字节 6..8 : 访问位 Access Bits(3 字节,带反码冗余) 字节 9 : General Purpose Byte / 用户字节 字节 10..15 : Key B(6 字节,可配置为可读)访问位这 3 个字节的编码是整张卡里最反直觉的地方。它用 3 个位(C1/C2/C3)描述一个块的权限,每个位有 4 份(对应扇区内 4 个块),再加上反码做冗余校验,塞进 3 个字节:
def parse_access_bits(b6: int, b7: int, b8: int) -> dict: """按 MF1S50 数据手册的布局解出 C1/C2/C3""" # b7 高 4 位 = C1_3..C1_0 c1 = (b7 >> 4) & 0x0F # b8 低 4 位 = C2_3..C2_0 c2 = b8 & 0x0F # b8 高 4 位 = C3_3..C3_0 c3 = (b8 >> 4) & 0x0F # 冗余校验:反码必须对得上,否则说明尾块被写坏了 if ((~c2) & 0x0F) != ((b6 >> 4) & 0x0F): raise ValueError("C2 反码不匹配,尾块可能损坏") if ((~c1) & 0x0F) != (b6 & 0x0F): raise ValueError("C1 反码不匹配,尾块可能损坏") if ((~c3) & 0x0F) != (b7 & 0x0F): raise ValueError("C3 反码不匹配,尾块可能损坏") return {"C1": c1, "C2": c2, "C3": c3}拿出厂默认值FF 07 80代进去:c1 = (0x07 >> 4) & 0x0F = 0,c2 = 0x80 & 0x0F = 0,c3 = (0x80 >> 4) & 0x0F = 8,即C3_3 = 1、其余为 0。查手册,C1C2C3 = 001对应的正是出厂传输配置:Key A 永不可读、访问位可由 A 或 B 写、Key B 可读但不可写。
数据块的权限表则是另一套:
| C1C2C3 | 读 | 写 | 加减 |
|---|---|---|---|
| 000 | A 或 B | A 或 B | A 或 B |
| 010 | A 或 B | 永不可 | 永不可 |
| 100 | A 或 B | B | 永不可 |
| 110 | A 或 B | B | B |
| 001 | A 或 B | 永不可 | A 或 B |
| 011 | B | B | 永不可 |
| 101 | B | 永不可 | 永不可 |
| 111 | 永不可 | 永不可 | 永不可 |
111那一行是真实存在的——整块彻底锁死,密钥都对也没用,这种卡叫“自杀卡”。见过有人把访问位写成FF 07 80之外的组合,把整块锁死,血泪经验就是:写尾块之前先把原值备份,Mifare Classic 没有后悔药。
4. 块结构与数据布局:16 个字节里到底住着什么
4.1 三种块语义
Mifare Classic 的存储单元固定是 16 字节一块。1K 卡是 16 个扇区 × 4 块 = 64 块,4K 卡是 32 个扇区 × 4 块 + 8 个扇区 × 16 块 = 256 块。块号在 4K 卡上是不连续的——前 32 个扇区每扇区 4 块,编号 0..127;后 8 个扇区每扇区 16 块,编号 128..255。这个跳变是 4K 卡读乱的主要来源。
同样叫“块”,语义有三种:
- 厂商块:只有块 0。前 4 字节是 UID,第 5 字节是 BCC,第 6 字节是 SAK,第 7、8 字节是 ATQA,剩下是厂商数据。出厂只读,改了就废。
- 数据块:普通存储,读写都行,也可能被配成值块。
- 尾块:每扇区最后一块,存密钥和权限。
块 0 值得单独说一句。很多程序把块 0 当普通数据读,读出来发现前几个字节跟 UID 对得上,就以为“这块能随便写”。实际上块 0 的写保护是硬件级的,即使认证通过,写命令也会返回 NAK。有些所谓的“UID 可写卡”,是在硅片层面做了特殊处理,属于另一类产品,不在标准协议的讨论范围内。
4.2 值块:取反备份为什么能防写坏
值块(Value Block)是一个很有意思的设计。它的 16 个字节不是随便排的:
字节 0..3 : 值(小端) 字节 4..7 : 值的按位取反 字节 8..11 : 值(再来一遍,冗余) 字节 12 : 地址字节 addr 字节 13 : addr 的取反 字节 14 : addr 字节 15 : addr 的取反同一个值存了两遍,其中一遍是反码;地址存了两遍,也都是反码。卡片在执行 increment / decrement 时,会先把三份值都校验一遍,任何一份对不上就拒绝操作。这个设计针对的是写过程中掉电:如果写到一半断电,三份副本里至少有一份是完整的旧值,卡片复位后能据此判断并回滚。
代价是值块只能存 4 字节有符号数,而且一旦被当成普通数据块写坏了,就再也回不到值块语义——除非你手动把 16 个字节按上面的格式重建一遍。
实际项目里,值块常用于电子钱包类的应用:扣款走 decrement,充值走 increment,转账走 restore + transfer。transfer 那一步是把值块内容写回同扇区的目标数据块,本质上是一次原子搬运。
4.3 NDEF 与 TLV:为什么手机能读出 URL
手机贴上去直接弹出网址,这个体验背后是 NFC Forum 定义的一套封装。Mifare Classic 被当作 Type 2 风格的标签使用时,存储器起始处会放一个能力容器(Capability Container),最前面两个字节固定是0xE1 0x10——0xE1是魔法字节,0x10表示版本 1.0。手机读卡器扫到这两个字节,就知道“这不是私有数据,是一条标准 NDEF 消息”。
容器后面跟着的是 TLV 序列:
| TLV 类型 | 含义 |
|---|---|
0x00 | NULL,填充用,跳过 |
0x01 | Lock Control TLV,描述锁定字节位置 |
0x02 | Memory Control TLV,描述可用内存区域 |
0x03 | NDEF Message TLV,真正的报文 |
0xFE | Terminator,结束标记 |
TLV 的格式是“类型 + 长度 + 值”。长度字段有个特殊规则:如果值长度小于 255,长度就是一个字节;如果是0xFF,后面跟两个字节表示真实长度。这个转义规则忘了处理,长 URL 就会解析失败。
def parse_tlv(data: bytes) -> bytes: """从标签存储区里抽出 NDEF 报文字节""" i = 0 while i < len(data): t = data[i] if t == 0x00: # NULL TLV,跳过 i += 1 continue if t == 0xFE: # 终止符,后面都是填充 break if i + 1 >= len(data): break length = data[i + 1] i += 2 if length == 0xFF: # 三字节长度转义 if i + 2 > len(data): break length = (data[i] << 8) | data[i + 1] i += 2 value = data[i:i + length] if t == 0x03: return value i += length return b""拿到的 NDEF 报文本身还是分层的:头字节里有 MB(消息开始)、ME(消息结束)、CF(分片)、SR(短记录)、IL(ID 长度存在)、TNF(类型名格式);然后是类型长度、载荷长度、类型、载荷。一个 URL 记录的 TNF 通常是0x01(标准 URI),类型字段是字符U,载荷的第一个字节是 URI 前缀缩写码(0x04是http://,0x02是https://),后面才是剩下的字符串。
所以“手机读出一串网址”这件事,实际上经过了四层解封装:块 → 字节流 → TLV → NDEF 记录 → URI 前缀码 + 字符串。任何一层错位,出来的都是乱码。
5. 排查:读卡读不出来时,先分清是哪一层失败
5.1 现象:能收到 ATQA,选卡却超时或返回 NAK
这是最常见的“看起来快成功了”的失败。原因通常有三个:一是场里不止一张卡,防冲突没走完就去发 SEL+NVB(0x70),读卡器拿到的 UID 是拼凑的;二是 NVB 推进逻辑写错了,低 4 位的“额外位数”算成了字节数,导致下一轮请求的位数不对,卡片直接不应答;三是 BCC 校验没过却没重试,直接把错数据当成 UID 往下走。
解决办法是老老实实按位收敛:每次收到 5 字节先校验 BCC,失败就退回去重新发0x93 0x20,不要把错误数据带进下一轮。另外,双倍 UID 的卡要处理 CT 字节0x88,看到它就切到0x95,别当成 UID 的第一位。
5.2 现象:认证总是返回 0x04,密钥明明是对的
最可能的原因是块号传错了。认证命令后面跟的是绝对块号,不是扇区号。想认证扇区 5(1K 卡),块号应该是 20,不是 5。传 5 进去,实际认证的是扇区 1 的块 1,如果你的密钥是按扇区 5 配的,当然对不上。
第二个原因是 Key A / Key B 搞混。0x60是 A,0x61是 B。有些卡的尾块把 Key B 配成不可读,但认证仍然可以用 B——可读和可用是两回事,这一点坑过不少人。
第三个原因是认证成功后没有保持链路。每次认证都会重置 Crypto1 状态,如果程序里两条命令之间插了一次重新寻卡,状态就全丢了,后续读块必然是乱码。
5.3 现象:尾块 Key B 读出来永远是 000000000000
这不是 bug,是设计。Key A 在任何配置下都永远读不回来,读出来恒为 6 个 0;Key B 能不能读,取决于尾块访问位里C1C2C3那三位的组合,出厂默认001是“Key B 可读但不可写”。
想确认到底是“不可读”还是“真是 0”,得去看访问位的解码结果,而不是看返回值。如果解码出来 Key B 不可读,那读回的 6 个 0 就是占位符,没有任何信息量。
5.4 现象:同一张卡,手机能读、自己的读卡模块读不到
先怀疑天线。13.56 MHz 的天线是个谐振回路,匹配电容的偏差、周围金属的耦合、板子走线的寄生参数,都会让谐振点偏移。偏移之后,卡能取到电(所以能回 ATQA),但负载调制的深度不够,读卡器解不出后续的帧。表现就是“寻卡偶尔成功、选卡经常失败”。
其次是场强和命令间隔。卡片刚进场时内部电容还在充电,此时发命令容易被吞。稳妥的做法是上电后先等 5~10 ms 再发第一条 REQA,失败就重试三次,别一次不成就判定“无卡”。
还有一种情况是卡片停在 HALT 状态。上一轮程序结束时发了 HLTA 却没复位,下一轮用 REQA 叫不醒,得改用 WUPA。
5.5 现象:4K 卡读到第 32 扇区以后数据全是错的
因为块号在 4K 卡上不连续。前 32 个扇区(0~31)每扇区 4 块,绝对块号 =扇区号 × 4 + 块内偏移,范围 0~127。从扇区 32 开始,每扇区变成 16 块,块号从 128 起算,公式变成128 + (扇区号 - 32) × 16 + 块内偏移。
如果程序里统一用扇区号 × 4算块号,到扇区 32 就会算成块 128,而这恰好是扇区 32 的第 0 块——看起来对,但扇区 33 会算成 132,实际应该是 144,从此全错。写 4K 卡支持时,这个分段函数一定要单独测。
6. 设计取舍:Crypto1 的失败边界,与“够用就好”的代价
回头看 Crypto1,它其实不是被“攻破”的,是被时间攻破的。
1994 年的设计目标很明确:一张卡的成本要压到能贴在公交票上,认证延迟要短到人能无感通过闸机,密钥要能塞进当时有限的硅片面积。48 位 LFSR 加一个 20 抽头的非线性滤波,在当时的算力下是个合理的平衡点——它比明文强,比 RSA 便宜,比 DES 省电。
问题出在两个层面。
第一个层面是密码学直觉的局限。设计者把“不可预测”寄托在非线性滤波函数上,但非线性不等于安全。滤波器只用 20 个抽头,意味着 48 位状态里有 28 位的信息要绕很大一圈才能体现在输出上。这种结构上的相关性,在 1994 年没有被
本文还有配套的精品资源,点击获取