简介这是一份面向Android NFC开发者与物联网入门者的Mifare卡协议分析与读取实践资料包围绕MIFARE Classic与Ultralight的通信机制、扇区与块结构、选择命令、密钥认证及读写响应展开并落到Android端NFC监听与MifareClassic对象操作的工程实现适合需要理解卡端协议、验证读取流程的读者。压缩包共245个文件、约10.12MB以xml布局与配置、java源码、txt说明、png截图、bin与so库文件为主另含cmake与ninja构建文件、apk及gradle工程配置较完整地呈现了NFC测试工程的目录结构与依赖组成。已有454人学习下载。其价值在于将协议概念、密钥认证、连接与块读写、异常处理等要点对应到可查阅的工程材料中便于对照理解Mifare卡数据访问流程、排查类型识别与认证失败等问题也可作为Android NFC读取Mifare卡的学习样本与二次开发参考。1. MF卡协议分析门禁“嘀”一声背后你其实一个字节都没读到门禁刷一下“嘀”一声后台日志上多了一串 8 位十六进制数字。绝大多数人以为这就是“卡里的内容”于是第一次做 MF卡协议分析 的人常常在拿到那 4 个字节后就以为所有工作都做完了——真实情况是那只是卡片在上电瞬间吐出来的 UID后面的 16 个扇区、64 个数据块一个字节都没碰。MIFARE Classic业内简称 MF1的价值恰恰在数据区余额、次数、有效期、员工编号、黑名单版本号全在扇区里而每个扇区门口都站着一对密钥 A/B 和三个字节的访问位。这篇笔记拆的是一份围绕 MF 卡协议分析整理出来的代码与流程资料从射频握手、地址空间换算到用 NFC 读卡器把扇区数据完整读出来再到访问位怎么手算、读不出来时按什么顺序排查。适合手上已经有 PN532 或 ACR122U、想把自己那张卡真正读明白的从业者也适合刚接手一卡通对接、被“读出来是乱码”卡住的人。2. 协议栈拆解从 13.56MHz 载波到扇区数据要过几道门很多人一上来就调库读到了就以为懂了换个卡型立刻翻车。原因在于 MF1 的读取不是“发一条读命令”而是四层依次握手射频层选卡、防冲突层拿 UID、认证层换会话密钥、应用层才轮到读写块。任何一层没走完后面全是空。这一章把四层拆开每层说清楚能观察到什么、参数怎么变。2.1 选卡与防冲突ATQA 和 SAK 才是判断卡型的依据读卡器天线持续发 13.56MHz 载波卡片靠近后靠感应取电复位。此时读卡器发 REQA0x267 bit 短帧场内所有处于 IDLE 状态的卡用 ATQA 回应如果卡已经在 HALT 状态得先发 WUPA0x52唤醒。ATQA 是 2 个字节编码了 UID 长度和防冲突能力常见组合里 0x0004 表示 4 字节 UID 的标准双态卡0x0344 多见于 7 字节 UID 的卡片。拿到 ATQA 后进入防冲突循环Anticollision Loop用 SEL0x93/0x95/0x97 NVB 逐级取 UIDCL1 拿 UID0-3CL2 拿 UID4-6CL3 拿 BCC 和剩余字节。每一级都要做一次 CRC-A 校验CRC 不对就重发这也是为什么有些卡贴着读卡器要晃两下才识别——耦合不稳导致的 CRC 失败在重试。最后读卡器发 SELECT卡片回 SAKSelect AcknowledgeSAK 是判断卡型最硬的证据SAK 常见取值对应卡型存储结构0x08MIFARE Classic 1K16 扇区 × 4 块 × 16 字节0x18MIFARE Classic 4K前 32 扇区 × 4 块 后 8 扇区 × 16 块0x09MIFARE Classic Mini5 扇区 × 4 块0x00MIFARE Ultralight页式存储非扇区结构0x20DESFire / Plus 系列完全不同的命令集提示SAK 为 0x00 或 0x20 时别再套用扇区读写流程命令体系完全不同硬套只会得到“认证失败”。这一步能拿到的信息量被严重低估。UID、ATQA、SAK 三件套基本可以确定卡型、容量和后续该发哪套命令。我一般会在代码里先把这三个值打日志出问题时第一时间看它们对不对——SAK 读成 0x00 却还在按 1K 卡读后面所有报错都是白费功夫。2.2 扇区、块、访问位1K 卡的地址空间怎么算才不越界MF1 的最小读写单位是“块”16 字节一块。1K 卡 16 个扇区每扇区 4 块共 64 块4K 卡前 32 个扇区每扇区 4 块后 8 个扇区每扇区 16 块共 256 块。每扇区的最后一块是“扇区尾块”Sector Trailer结构固定字节偏移长度内容0–56密钥 A6–83访问位Access Bits91通用字节 GPB10–156密钥 B可配置为可读或不可读第 0 扇区第 0 块是厂商块出厂写入且只读前 4 字节 UID、第 5 字节 BCCUID 四字节异或校验、第 6 字节 SAK、第 7–8 字节 ATQA、第 9–15 字节厂商数据。很多人把这块当成“可写配置区”去写写完发现卡变砖——这块在多数卡片上是 OTP 或硬只读的。块地址换算是最容易翻车的地方尤其是 4K 卡def sector_to_blocks(sector: int) - list[int]: 返回某个扇区包含的全部块号MF1 1K / 4K 通用 if sector 0 or sector 39: raise ValueError(sector 必须在 0-39 之间) if sector 32: # 前 32 个扇区每扇区 4 块线性排列 start sector * 4 return list(range(start, start 4)) # 后 8 个扇区每扇区 16 块起始块号固定从 128 开始 start 128 (sector - 32) * 16 return list(range(start, start 16)) print(sector_to_blocks(0)) # [0, 1, 2, 3] print(sector_to_blocks(31)) # [124, 125, 126, 127] print(sector_to_blocks(32)) # [128..143]不是 128 以外任何数 print(sector_to_blocks(39)) # [240..255]这段函数的关键在sector 32那个分支。4K 卡的块号不是简单sector * 4延续下去的第 32 扇区强行跳到 128中间 128 之前的号段根本不存在。我见过不止一次有人按sector * 4去读 4K 卡的第 32 扇区读回来的是第 32 号块——那是第 8 扇区的数据块——然后得出“这卡数据是错的”的结论。地址算错读出来的东西看起来完全正常这才是最难查的一类 bug。2.3 Crypto1 认证三段式交互决定了你能不能碰数据块选卡完成后任何读写块之前必须先对该扇区做认证。命令字节 0x60 表示用密钥 A 认证0x61 表示密钥 B后面跟一个块号用扇区内任意块号都等价于认证整个扇区。交互是四步读卡器发0x60 block卡片回 4 字节随机数 nt读卡器用密钥、nt 和自己的随机数 nr 算出 nr⊕ks1、ar、at回给卡片卡片校验通过后回 at⊕ks2双方进入加密通信状态。Crypto1 是一个 48 位线性反馈移位寄存器密钥流由 LFSR 状态推导。认证一旦失败卡片不会明确告诉你“密钥错了”而是直接不响应或者返回 NAK读卡器侧表现为超时。这就导致排错时很难区分“密钥错”“耦合差”“块号非法”三种情况——它们的外部表现几乎一样。业内通用的判断顺序是先确认 UID 能稳定读出说明耦合没问题再确认块号合法扇区尾块不能用普通读命令的某些实现去读最后才怀疑密钥。认证还有一个容易忽略的性质一个扇区认证成功后只要不断电、不换卡后续对同一扇区的读写都沿用这个加密会话。所以批量读一张卡时正确做法是“按扇区认证、按块读”而不是“每块都认证一次”——后者在某些读卡器固件上会显著变慢甚至因为状态机重置而中途失败。至于密钥本身怎么来自己单位的卡找管理员要自己买的空白卡出厂默认密钥写在数据手册里。对非自有卡片做密钥恢复属于未经授权访问这里不展开也没必要展开——真到那一步问题已经不在技术层面了。3. 动手读一张卡读卡器选型与最小可跑脚本协议理清楚了落到硬件。这一步的选择直接决定后面代码长什么样选错了库不支持、命令发不出去会浪费大量时间在“为什么别人一行命令就出数据”上。下面按接口类型分说清楚每种方案的边界。3.1 硬件选型PN532、ACR122U、RC522 各自适合什么场景方案接口典型能力适合谁PN532 模块UART / I2C / SPI支持 ISO14443A/B、Felica可发原始帧想抓协议帧、做底层分析ACR122UUSB CCID走 PC/SC支持伪 APDU 操作 MF1只想快速读写扇区、写上位机RC522SPI3.3V仅 ISO14443A寄存器级控制嵌入式集成、成本敏感手机 NFC系统 API多数只能读 NDEFMF1 扇区受限轻量验证 UID 和 NDEF做协议分析优先 PN532 或 ACR122U。PN532 的好处是能拿到原始收发帧你在 2.1 里看到的 REQA/ATQA 都能在日志里逐字节对照ACR122U 的好处是 PC/SC 生态成熟Python 用 pyscard 直接发伪 APDU不用管底层时序。RC522 便宜但只能挂在 3.3V 的 MCU 上做桌面调试很别扭除非你的最终形态就是嵌入设备。注意以下操作只针对自己持有或已获授权的卡片。读取他人卡片内的数据可能涉及法律风险。3.2 环境搭建与设备自检Linux 下先确认设备被识别。ACR122U 走 CCID插上后pcscd服务会自动接管# 安装 PC/SC 服务与工具 sudo apt install pcscd pcsc-tools python3-pip sudo systemctl enable --now pcscd # 确认读卡器在线输出里应能看到 ACR122U 之类的名字 pcsc_scan # Python 侧依赖 pip install pyscardpcsc_scan是一个非常好的自检工具它会持续打印场内的卡片事件、ATR 和 UID。如果这一步就读不到 UID别往下写代码先解决硬件换 USB 口、换线、把卡贴到天线中心位置。用 PN532 的路线则是装 libnfc配置文件/etc/nfc/libnfc.conf里指定device.connstring然后用nfc-scan-device确认端口能打开。这两条路的自检逻辑是一样的先证明能拿到 UID再谈扇区。3.3 Python 读出 UID、ATQA 与整扇区数据ACR122U 有一套存储卡专用的伪 APDU绕过 PC/SC 的标准命令直接操作 MF1。核心三条装载密钥、认证块、读二进制。from smartcard.System import readers from smartcard.util import toHexString # 出厂默认密钥6 字节 0xFF仅用于自己手上的空白卡或已知密钥的卡 DEFAULT_KEY [0xFF] * 6 def connect(): r readers() if not r: raise RuntimeError(没有检测到读卡器) conn r[0].createConnection() conn.connect() return conn def send(conn, apdu, note): data, sw1, sw2 conn.transmit(apdu) if (sw1, sw2) ! (0x90, 0x00): raise RuntimeError(f{note} 失败: SW{sw1:02X}{sw2:02X}) return data def load_key(conn, key, slot0x00): # FF 82 00 slot 06 6字节密钥 apdu [0xFF, 0x82, 0x00, slot, 0x06] key send(conn, apdu, 装载密钥) def auth_block(conn, block, key_type0x60, slot0x00): # 0x60 密钥A, 0x61 密钥B # FF 86 00 00 05 slot keyType block 2字节密钥版本 apdu [0xFF, 0x86, 0x00, 0x00, 0x05, slot, key_type, block, 0x00, 0x00] send(conn, apdu, f认证块 {block}) def read_block(conn, block): # FF B0 00 block 10 —— 读 16 字节 apdu [0xFF, 0xB0, 0x00, block, 0x10] return send(conn, apdu, f读块 {block}) def get_uid(conn): # FF CA 00 00 00 —— 取 UID return send(conn, [0xFF, 0xCA, 0x00, 0x00, 0x00], 取 UID) if __name__ __main__: c connect() uid get_uid(c) print(UID:, toHexString(uid)) load_key(c, DEFAULT_KEY, slot0x00) for block in range(4): # 0 扇区的 4 个块 auth_block(c, block, 0x60, 0x00) raw read_block(c, block) print(fblock {block:02d}: {toHexString(raw)})逐段说清楚。FF 82是装载密钥指令第三个字节00是密钥槽位0x00–0x1F 可选06后面跟 6 字节密钥装载一次可以反复用不必每块都装。FF 86的第五个字节05是后续数据长度依次是槽位、密钥类型0x60 密钥 A / 0x61 密钥 B、块号最后两字节是密钥版本号MF1 上固定填 0 即可。FF B0读二进制00是 P1页/块号高字节块号放在 P210是读 16 字节。注意auth_block我按块号传参而不是扇区号。这是因为伪 APDU 的认证接口接受的是扇区内任意块号客户端实现会把它映射到扇区。但如果你在别的库比如 libnfc 的nfc_authenticate里传参某些版本要求的是扇区号混用会导致“认证了扇区 1 却读到扇区 0 的数据”。这个坑在跨库迁移代码时几乎必踩务必要对着文档确认参数语义。另外块 0 在多数卡上是只读厂商块认证它能成功但写入会返回 NAK。想验证写入链路是否通从扇区 1 的数据块开始试别拿厂商块开刀。4. 访问位与密钥把“能读”变成“知道为什么能读”能读出来只是第一步。真正决定一张卡能不能被正确使用、能不能被安全配置的是每扇区尾块里那 3 个字节的访问位。这三个字节决定了该扇区每个块的读、写、加减、增减权限分别用什么密钥验证配错了要么谁都读不了要么谁都能改。4.1 访问位三字节的编码规则与手工推算三个字节共 24 bit但有效信息只有 12 bit 的 C1/C2/C3 三组每组 4 个 bit 对应块 0–3。存储方式是“正反各存一份”用于自校验byte6 存 C1 的低 4 位和高 4 位byte7 存 C2byte8 存 C3其中每个字节的高 4 位是低 4 位的按位取反。def decode_access_bits(b6: int, b7: int, b8: int) - list[tuple[int, int, int]]: 解码扇区尾块 6/7/8 字节返回 4 个块的 (C1,C2,C3) # 先做自校验高 4 位必须是低 4 位的取反 for i, b in enumerate((b6, b7, b8)): low, high b 0x0F, (b 4) 0x0F if low ! (~high 0x0F): raise ValueError(fbyte{6i}0x{b:02X} 校验失败访问位可能损坏) c1, c2, c3 b6 0x0F, b7 0x0F, b8 0x0F return [((c1 i) 1, (c2 i) 1, (c3 i) 1) for i in range(4)] # 出厂默认 0xFF0780 是 1K 卡最常见的配置 print(decode_access_bits(0xFF, 0x07, 0x80)) # [(0,0,0), (0,0,0), (0,0,0), (0,1,1)]0xFF 0x07 0x80解码出来前三个块是 (0,0,0)表示用密钥 A 或 B 都能读写第四块的 (0,1,1) 表示尾块本身的密钥 A 不可读、密钥 B 可读但写入需要密钥 A。这是卡厂出厂的默认值也是绝大多数门禁卡的实际配置——意味着只要知道密钥 A整个扇区含密钥 B 的可读部分就全部暴露。真正要警惕的是把尾块配成“密钥 A 可读”的组合。一旦密钥 A 能被读出来这把钥匙就等于印在卡面上后面所有加密都形同虚设。配置卡片时我一般遵循一条硬规矩数据块用密钥 A 读写尾块把密钥 A 设为不可读密钥 B 留给管理端。多花五分钟配访问位省掉后面整个系统的密钥轮换。4.2 密钥 A/B 的用途差异与默认密钥的现实风险密钥 A 和密钥 B 在协议层是对等的差别全靠访问位定义。常见的生产配置是A 用于普通业务读写B 用于充值、清零、改配置这类高权限操作。这样即使 A 泄漏损失范围也可控。现实里更常见的情况是整套系统所有扇区都用默认密钥。空白卡出厂就是 6 个0xFF很多集成商拿到卡直接写数据、不改密钥、不动访问位上线后整栋楼几百张卡钥匙都一样。这不是危言耸听我在做资产盘点时见过整批卡片密钥完全相同的场景。后果是任何一张卡丢失等价于整套系统的数据区裸奔——不只是读是可以写。自查很简单用默认密钥去认证扇区 1如果认证通过说明这个扇区没改过密钥。批量扫一遍全部扇区能认证的比例就是这套卡的安全水位。def audit_sector(conn, sector: int, keyDEFAULT_KEY) - dict: 检查某扇区是否能被默认密钥认证自查用仅限自有卡片 block sector * 4 if sector 32 else 128 (sector - 32) * 16 result {sector: sector, key_a_ok: False, key_b_ok: False} for kt, field in ((0x60, key_a_ok), (0x61, key_b_ok)): try: auth_block(conn, block, kt, 0x00) result[field] True except RuntimeError: pass return result这个审计函数只在你自己负责的卡片上跑。跑完拿结果去找负责发卡的人比写一份安全报告有效得多。4.3 值块与厂商块哪些块写入后不可逆MF1 的数据块有三种形态普通数据块16 字节随便写、值块带备份和加减指令的计数器、厂商块第 0 扇区第 0 块只读。值块的结构是固定的4 字节小端数值 1 字节补码 4 字节数值 1 字节补码 4 字节数值 1 字节补码三份冗余互校。值块只能用 Increment / Decrement 指令改直接写 16 字节会破坏冗余结构卡片会认为该块损坏后续读取返回全 0。想改余额类数据走FF D6写之后重新构造冗余或者干脆用原生的加减指令。厂商块是完全的另一回事。第 0 块在大多数卡上是熔丝位保护或硬只读写操作返回 NAK 还算好的个别卡型写入会连带把 UID 区域锁死。批量写卡的流程里我一般会在第一张卡上先试写第 1 扇区第 0 块来验证链路确认没问题再扩到全量。第 0 块从开发到上线全程不碰这是条铁律。5. 避坑与排查读卡失败时按这个顺序查读不出来的时候最怕的是乱试。改代码、换卡、换读卡器、重装驱动一通操作下来问题可能还在原地。这一章把踩过的坑按“现象 → 原因 → 解决”整理成五条从外到内基本覆盖了九成以上的失败场景。现象一pcsc_scan能稳定看到 UID但脚本认证一直超时。原因通常是密钥类型用反了或者槽位没对上。FF 86里的密钥类型 0x60/0x61 混淆是最常见的有些库的封装层把 A/B 抽象成布尔值参数顺序一颠倒就出事。另一种可能是装载密钥的槽位号和认证时引用的槽位号不一致——装载用的 slot 0认证却写了 slot 1读卡器会用一个空密钥去认证自然超时。解决把装载和认证的 slot 参数抽成一个常量别在两处手写数字先用扇区 0 的块 0 做最小验证那个块任何卡都能认证只读不影响认证。现象二认证成功读出来的 16 字节全是 00。先别怀疑代码。新卡、白卡、刚格式化的卡数据块本来就是全 0。判断方法是看扇区尾块尾块的高 6 字节是配置不可能是全 0。如果连尾块都是全 0那这张卡要么是魔术卡UID 可写卡要么存储区已经损坏。另一个高频原因是块号算错了——传了扇区号当块号读到了隔壁扇区的空白块结果看起来“读成功但没数据”。解决打印实际发出的 APDU 十六进制人工核对 P2 字段所有块号统一用sector_to_blocks()生成禁止手写常量。现象三1K 卡全读通了4K 卡最后 8 个扇区一律失败。这就是 2.2 节那个地址换算坑。第 32 扇区起始块是 128不是 128 以外的任何数字。很多示例代码只处理了sector * 4跑 1K 卡完全正常一上 4K 就露馅。更隐蔽的变体是库函数接受的是“绝对块号”但调用方传的是“扇区内偏移”第 32 扇区传 0结果读到了块 0——厂商块——返回的数据看起来是对的UID 就在里面于是误判为“读到了但数据不对”。解决在代码里加一个断言block 128时强制校验sector 32把非法组合在发送前拦掉。现象四读到的数据里有一段看着像乱码中文全是问号。MF1 的块本身是裸字节卡里没有任何编码信息。如果是 NDEF 格式的卡数据块里存的是 TLV 结构03 长度 D1 01 ...其中 D1 是 NDEF 记录的头部后面跟类型长度、载荷长度、类型标识54表示文本记录T再后面才是状态字节和真正的文本。直接按 UTF-8 解整块必然出乱码因为前面几十个字节是协议头。解决先定位0x03起始的 TLV读出长度字段跳过 NDEF 头部按状态字节第 7 位判断 UTF-8 还是 UTF-16再解码。文本记录的类型标识是0x54别把它当数据。现象五同一张卡刚才读得到过一会儿又读不到了。优先怀疑供电与耦合不是代码。USB 口供电不足、延长线过长、卡片偏离天线中心 5mm 以上都会导致偶发的认证超时。MF1 的认证对时序敏感射频场稍有波动卡片内部的随机数生成就会受影响。另一种可能是手机 NFC 正在模拟卡片占用了同样的物理通道两个场叠在一起谁也读不准。解决把读卡器直插主板后置 USB 口去掉一切延长线卡片贴着天线丝印中心关掉旁边手机的 NFC。排完这三项还有问题再去看是不是卡片本身有多个应用在争抢双频卡常见需要先发 HALT 把不相关的应用挂起。6. 从能读到能验证CRC-A 与一份可复用的核对清单读出来的数据能不能用取决于你信不信它。MF1 的每一帧都带 CRC-A 校验但上层库通常在内部就把它吞掉了校验失败时只给你一个含糊的 NAK。想在排错时拿到确定答案最好自己会算。ISO14443A 的 CRC-A 是 16 位、初值 0x6363、反射多项式 0x8408发送时低字节在前def crc_a(data: bytes) - bytes: ISO14443A CRC-A返回 [低字节, 高字节] crc 0x6363 for byte in data: crc ^ byte for _ in range(8): if crc 0x0001: crc (crc 1) ^ 0x8408 else: crc 1 return bytes([crc 0xFF, (crc 8) 0xFF]) # 用 UID 帧验证0x93 0x70 UID0-3 BCC CRC frame bytes([0x93, 0x70, 0xDE, 0xAD, 0xBE, 0xEF]) print(crc_a(frame).hex()) # 和读卡器抓到的原始帧尾两字节对比这段代码在抓原始帧的时候特别有用。PN532 的inlist_passive_target返回的帧里CRC 是读卡器固件校验过的但如果你自己在做固件级的时序调试手算 CRC 能确认到底是“卡片回的数据错了”还是“读卡器解析错了”——这两种故障的处理方式完全不同。我自己整理了一份核对清单每次接新卡型或新读卡器从第一条往下走不走捷径顺序核对项通过的标志1读卡器被系统识别pcsc_scan或nfc-scan-device列出设备2UID 连续 10 次读取一致无跳变、无超时3SAK 与卡型匹配1K 卡为 0x084K 卡为 0x184扇区 0 块 0 的 BCC 校验正确前 4 字节异或等于第 5 字节5目标扇区认证成功返回 0x9000无超时6读出的尾块访问位自校验通过decode_access_bits不抛异常7块号与扇区号互换验证sector_to_blocks输出与预期一致走完这七步基本可以确认“读到的东西是可信的”剩下的才是业务逻辑。反过来跳过第 4 步和第 6 步直接去解析业务字段是我见过最费时间的做法——数据本身就有问题的时候在业务层怎么调都是错的。还有一条习惯每次换读卡器型号我都会重新跑一遍第 2 步因为不同天线的场强和读距差别挺大同一张卡在原设备上稳定换设备后可能就开始偶发丢帧。这些看起来是小事但真到现场调不通、又急着交付的时候就是这些小事救场。希望帮到你。本文还有配套的精品资源点击获取