MF卡读取全指南:从Mifare Classic协议分析到Python NFC编程实现
2026/9/9 19:16:52 网站建设 项目流程

简介:资源包围绕MF卡协议分析与Android NFC读取实现展开,面向物联网开发、移动应用及智能卡技术学习者,解决从MIFARE Classic/Ultralight协议理解到Android端实际调用NFC读写MF卡的核心需求。包体含245个文件,以Java源码、XML配置、Gradle构建脚本及C/C++底层实现为主,另包含可直接安装的APK、so动态库及若干调试用bin/json/txt文件,压缩后约10.12MB,结构清晰便于二次开发。已有447人学习参考。内容包括NFC读写完整工程、MF卡扇区认证与读写逻辑、Android Studio项目配置及多平台编译产物,并结合MIFARE Classic容量划分、密钥机制以及SELECT、AUTHENTICATE、READ、WRITE等命令交互流程。资源内附有可运行APK与完整工程目录,展示检测NFC、注册enableReaderMode监听,以及使用MifareClassic完成connect、authenticate、readBlock等关键调用,对排查认证失败、扇区越界等实际调试问题有直接帮助,尤其适合门禁、公交等领域嵌入式与移动协同开发人员参考。 MF卡这个词,在网上搜代码的帖子里出现频率特别高。我第一次听到也以为是某种存储卡,后来才搞明白,它通常指的是Mifare Classic系列射频卡,也就是日常说的IC卡——手里的门禁卡、校园卡、部分公交卡,很多都用的是这套芯片。这篇文章就围绕“MF卡协议分析、NFC读卡、代码实现”这三个核心点,把“如何用NFC读卡器把MF卡里的数据读出来”这件事完整讲清楚,从协议原理到能跑的Python代码都有,适合刚接触射频卡开发、或者正在被门禁卡读取问题卡住的读者。

1. 先搞清楚MF卡到底是什么:从卡片结构到安全模型

1.1 一张MF卡的内部:扇区、块与16字节

MF卡本身没有内置电池,属于无源非接触卡,能量来自读卡器发射的13.56MHz射频场。它最常用的型号是Mifare Classic 1K,也就是说内部有1Kbit容量的EEPROM,按8比特算就是1024个字节的存储空间。

但这1024字节不是连续的一块“空地”,而是被划分成了16个扇区,每个扇区里又有4个块,每个块固定16字节。这么一算正好是16乘以4乘以16,等于1024字节。平时我们说“读MF卡”,实际上的最小操作单位就是一个块——一次READ指令最多能取回16字节,想读一个扇区里的出厂信息,就得把块逐个读出来。

块和块的地位完全不一样。每个扇区的第0块到第2块是数据块,可以被业务数据占用;第3块是尾部块,用于存放这个扇区的密钥A、访问位和密钥B。第一扇区有点特殊,它的第0块头部是UID、校验位和厂商代码,这些信息出厂时就被锁定,普通指令改不了。所以代码里遍历整个卡的时候,我会跳过每个扇区的尾部块,数据块也只读掉前3个,不然读到一堆密钥和权限控制信息,既没意义还会让你误以为卡坏了。

1.2 认证与权限:为什么有些卡直接读不出来

MF卡的读卡不是“放上去就能看数据”这种程度。每个扇区都有一把钥匙孔,钥匙分两种类型:Key A和Key B。当读卡器要访问某个扇区的数据块时,必须先向卡片发起认证请求,密钥对了才给开门,密钥不对就返回错误状态码。

这种设计是Mifare Classic当年能大量用在门禁、考勤机上的基础,但现在来看它的安全级别并不高。CRYPTO1流密码在公开文献里已经被分析得很透了,很多卡片默认密钥也早就不是秘密。这篇文章只讲“怎么用已知合法密钥完成读取”,过程中你会看到密钥是以明文形式参与认证的。

说句实在话,正规应用场景下,我们手里拿到的卡一般是单位配发的测试卡,或者发卡方给了明确的读卡权限。做开发调试时,最常用的默认密钥就是FF FF FF FF FF FF,有时候厂商也会用A0 A1 A2 A3 A4 A5这种出厂值。如果你手里的卡密钥被改过且没有授权,那就涉及权限问题了,别想办法绕过,直接用合规流程找卡主拿密钥。

1.3 读取之前先明确边界

我在排查NFC读取问题时遇到过一些新手,拿张完全不知道来源的卡就想“破解”。这种事我必须泼冷水。非授权读取别人卡片里的数据,在多数地区都涉嫌违法,而且绝大多数门禁卡系统其实都做过安全加固。把目标限定在自己持有、回收、已经获得授权的卡片上,既安全又能把技术链路研究透。

2. 读卡前必须搞懂的协议链路

2.1 从物理层到数据帧:ISO/IEC 14443-A

MF卡的空口通信遵循ISO/IEC 14443-A标准。读卡器作为主设备(PCD),卡片作为从设备(PICC),载波频率13.56MHz,调制方式和编码规则都由这套标准约束。读卡器向卡片发送指令时用的是Miller编码和ASK调制,卡片回传数据时则切到曼彻斯特编码,速率标称是106kbps。

不要被这些术语吓到。在应用层面,我们不需要手写波形,理解一条核心逻辑就够了:读卡器与卡片之间是问答式通信,读卡器说一句,卡片答一句。比如读卡器发出REQA,卡片回ATQA;再往下走,卡片会响应防碰撞指令,交出UID,然后读卡器选中这张卡,之后的读写指令才能继续。

这个选卡流程就是日常说的“防碰撞”。一叠卡放在读卡器上时,读卡器要先通过UID逐个锁定目标,防止同时和两张卡说话。防碰撞完成之后,卡片才会进入可认证状态,这时候才轮到密钥认证。

2.2 指令从PC到卡片的旅程:PC/SC与APDU

写代码的时候,真正需要关注的是PC/SC协议栈。PC/SC是Windows、Linux、macOS上通用的智能卡接口标准,很多USB读卡器都实现了这个标准。你的代码通过系统API跟读卡器通信,读卡器再按14443-A的时序把指令发给卡片。

于是数据链路分成两层。PC到读卡器之间发的是APDU格式的指令,读卡器到卡片之间则是RF层面的命令帧。大多数情况下,读卡器驱动会帮你把APDU转换成卡片能识别的命令。所以我们写Python控制ACR122U时,主要代码都是在拼APDU:命令字节、参数、数据区,一个字节都不能错。

2.3 认证与读操作的核心指令

读MF卡时,绕不开三条指令:获取UID、认证扇区、读取块数据。

获取UID用FF CA 00 00 00,这个指令是PC/SC标准里的Get UID,读卡器收到后会跟卡片完成一轮防碰撞和选择,然后把卡片ID返回给超级终端。认证一个扇区用FF 86 00 00 05 01 00 <块号> <密钥类型> <6字节密钥>,块号填该扇区里的任意块都行,因为认证粒度是整个扇区。读块数据用FF B0 00 <块号> 10,最后的10表示要读16字节。

我建议第一次上手时先别急着跑大段代码,直接用读卡器自带的测试软件或者一个Python终端,把这三条指令逐条发一遍。看到返回的数据和状态字之后,再去写循环遍历,这样对指令含义的理解会牢固得多。

3. 硬件选型与Python代码实现

3.1 硬件怎么选:ACR122U还是PN532

网上问MF卡读取代码的人,手里通常是两类硬件:ACR122U这类PC/SC读卡器,或者PN532模块。我的建议是入门首选ACR122U,它一个USB口就能解决供电和通信,Windows下有官方驱动,而且原生支持PC/SC,Python里调用非常顺畅。

PN532便宜,接线多,适合嵌入式项目或树莓派场景。它的底层通过I2C/SPI/UART跟主机通信,在Linux下要装libnfc,再用nfcpy之类的库操作,流程相对折腾。如果你不是要做手持设备或需要把NFC模组嵌进现有板子,没必要一开始就买PN532。

我之前给一个项目调读卡流程时,先用ACR122U把协议和代码全部跑通,再换成PN532端口,前后只改变了连接层代码,认证和读取逻辑完全一致。

3.2 环境准备:Windows/Linux下的PC/SC栈

Windows下装好读卡器驱动后基本就能用了,但Linux下记得先安装pcscd和libpcsclite。Debian系的命令是:

sudo apt install pcscd libpcsclite-dev libpcsclite1 sudo systemctl enable pcscd sudo systemctl start pcscd

Python侧需要pyscard,安装一行搞定:

pip install pyscard

装完后可以先跑一段最简代码,列出读卡器,确认系统能识别到设备:

from smartcard.System import readers all_readers = readers() if not all_readers: raise SystemExit("没有发现PC/SC读卡器") for i, reader in enumerate(all_readers): print(i, reader)

输出里能看到ACR122U或者类似名称,就说明PC/SC栈已经通了。这一步解决后,后面所有的调错效率都会高很多。

3.3 完整代码:从枚举读卡器到dump整个1K卡片

下面这段代码是我实际在门禁卡测试项目里用过的,做了简化,保留了核心流程。它能完成整个Mifare Classic 1K卡的数据读取,并把每个数据块以十六进制和ASCII两种形式打印出来。

from smartcard.System import readers from smartcard.util import toHexString def connect_reader(): all_readers = readers() if not all_readers: raise SystemExit("没有发现PC/SC读卡器") reader = all_readers[0] connection = reader.createConnection() connection.connect() return connection def get_uid(connection): cmd = [0xFF, 0xCA, 0x00, 0x00, 0x00] data, sw1, sw2 = connection.transmit(cmd) if sw1 == 0x90: return data return None def authenticate(connection, block, key_type, key): # key_type: 0x60表示Key A,0x61表示Key B cmd = [0xFF, 0x86, 0x00, 0x00, 0x05, 0x01, 0x00, block, key_type] + list(key) data, sw1, sw2 = connection.transmit(cmd) return sw1 == 0x90 and sw2 == 0x00 def read_block(connection, block): cmd = [0xFF, 0xB0, 0x00, block, 0x10] data, sw1, sw2 = connection.transmit(cmd) if sw1 == 0x90: return data return None def to_ascii(data): return ''.join(chr(b) if 32 <= b < 127 else '.' for b in data) KEY_A_DEFAULT = bytes([0xFF] * 6) # 默认Key A def dump_mf_classic_1k(): conn = connect_reader() uid = get_uid(conn) if uid is None: print("获取UID失败,请确认卡片靠近读卡器") return print("UID:", toHexString(uid)) for sector in range(16): base_block = sector * 4 # 每个扇区认证一次,然后读前三个数据块 if not authenticate(conn, base_block, 0x60, KEY_A_DEFAULT): print(f"扇区 {sector:02d} 认证失败") continue for offset in range(3): block = base_block + offset data = read_block(conn, block) if data is None: print(f"扇区 {sector:02d} 块 {block:02d} 读取失败") else: hex_part = toHexString(data) ascii_part = to_ascii(data) print(f"S{sector:02d}B{block:02d}: {hex_part} | {ascii_part}") if __name__ == "__main__": dump_mf_classic_1k()

运行后,默认密钥能过的扇区都会打印出来,过不了的就提示认证失败。到这里,读取MF卡内容的核心目标就算达成了。

3.4 代码关键点讲解:为什么这样设计

有几个地方容易踩坑,展开说一下。

第一个是认证粒度。Mifare Classic的认证是按扇区来的,不是按块。所以我在代码里对每个扇区的起始块做一次认证,然后连续读3个块。有些新手把认证放在每一块读取之前,增加了无意义的射频交互,读卡时间会明显变慢。

第二个是密钥类型。0x600x61两个参数很容易写反。0x60对应Key A,0x61对应Key B。部分卡片的Key B读取权限受到访问位控制,实际上用Key A认证更通用,这也是我默认用Key A的原因。

第三个是返回值判断。transmit返回的状态字90 00代表指令执行成功。如果返回63 00,多数是认证失败;返回69 82,可能是权限不够;返回6B 00,则要检查APDU参数长度。看到这些错误码不要慌,后文有排查表。

4. 实操演示:一步步读出MF卡数据

4.1 从连接读卡器到跑通首个命令

这里我以ACR122U为例,把实操过程拆开走一遍。先确保读卡器已经插入电脑,然后打开终端运行上面的简单枚举脚本。正常情况下,你会看到类似下面的输出:

0: ACS ACR122U PICC Interface 0

然后拿一张已知默认密钥的测试卡,放到读卡器正面的硬币感应区域。放卡有个小技巧:卡片不要垂直悬空,要尽量平贴读卡器logo位置,因为ACR122U的天线就在那个圆形区域,距离太远或者角度偏移会导致软复位失败。

接下来单独发一条读取UID命令,模拟一下卡片放上去后的返回效果:

conn = connect_reader() print("UID:", toHexString(get_uid(conn)))

如果一切正常,会输出类似04 A3 12 5B这样的数据。这个UID不是随机数,它包含在卡片第0扇区第0块前4个字节里,是卡片最基础的身份标识。

4.2 实际读取输出与分析

继续跑完整的dump脚本,你可能会看到类似下面的输出:

UID: 04 A3 12 5B S00B00: 04 A3 12 5B EE 08 04 00 62 63 64 65 66 67 68 69 | .…....bcdefghi S00B01: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................ S00B02: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................ S01B00: 01 02 03 04 00 00 00 00 00 00 00 00 00 00 00 00 | ................

第一行的04 A3 12 5B就是UID,第5个字节EE是UID校验位,它必须等于前4个字节的异或结果,否则卡片会被读卡器判定为无效卡。0804 00是SAK和ATQA等应答参数,再往后的8个字节是厂商代码,不同厂家的数据会不同。

从第1扇区开始才通常是门禁系统真正存数据的地方,比如用户ID、时间段权限等。如果你拿到的卡里面是纯业务数据,ASCII区域偶尔能看到数字或字母。数据块全是00也很正常,不代表读取有问题,可能就是卡还没写入记录。

读取时如果某个扇区提示“认证失败”,通常有两种可能:密钥不是默认值,或者这一块本来就设置了不同权限。我会建议换Key B再试一次,把0x60改成0x61,密钥也换成该扇区的Key B。如果还是失败,那就是这张卡的密钥被发行方修改过,合规做法就是去拿授权。

4.3 读卡速度与效率优化

对一张1K卡做完整dump,如果逐块认证,需要认证64次,就算每次只要几十毫秒,累计起来也会让操作显得拖沓。所以我在代码里把认证次数降到了16次,每个扇区只认证1次。实际跑下来,完整读取一张卡的时间可以控制在1秒左右。

如果项目里需要频繁读卡,还有两个优化方向。一是缓存已经认证过的扇区列表,避免重复认证;二是用读卡器自身的扩展APDU一次读多个块,ACR122U的FF B1指令就可以实现,不过具体支持情况取决于固件版本,要自己测。

5. 高频问题与排查实录

5.1 常见错误码与解决思路

我在帮别人调试NFC读取代码时,问题基本集中在几个地方。下面这张表基本能覆盖绝大多数场景。

现象错误码/提示可能原因排查方向
读卡器灯亮但程序报“没有发现读卡器”枚举不到设备PC/SC服务未启动或驱动问题Windows重新装ACS驱动,Linux检查pcscd状态
卡片放上去没有响应获取UID失败卡片没有靠近天线区域调整卡片位置,确认读卡器频率是13.56MHz
认证失败63 00密钥错误或密钥类型选错换Key B,使用正确的扇区密钥
认证成功但读块失败69 82当前扇区的访问位禁止该操作查阅访问控制字节,检查Key B权限
指令被拒绝6B 00APDU长度或参数不对核对指令字节数,确认块号是否在0~63之间
读出来的全是FFFF无报错数据确实没写入,或你读的是尾部块跳过每个扇区的第3块,只读数据块

5.2 几个容易踩的坑和独家技巧

这些坑都是我实际踩过或者看别人踩过的,特别值得注意。

第一,不要试图修改第0扇区第0块。这个块是厂商信息区,UID一旦被锁定,强行写入通常会导致卡片变砖或者不能再认证。调试阶段就老实读取,别手痒。

第二,读卡器对卡片放上去的时序很敏感。ACR122U在Linux下偶尔会出现首次连接报复位失败的情况,我的土办法是先断开连接,等2秒再重新connect,多半就正常了。这是读卡器固件和系统驱动的兼容性问题,跟卡片没关系。

第三,十六进制和ASCII对照打印是个好习惯。有一回我调门禁系统,发现读出来的数据块看起来全是二进制乱码,想当然以为是加密数据。后来打印了ASCII列,才发现其实是厂商用ASCII码存的卡号,只是读的时候就该先看右侧文本列。数据是否是明文、是否需要二次解析,必须结合业务场景判断。

第四,不同厂家的Mifare Classic卡访问位默认值不一样。虽然默认密钥大多是FF FF FF FF FF FF,但访问位如果被改过,即使密钥对了也可能写不进去。调试时先拿一张全新的测试卡做基准测试,能省掉很多排查时间。

第五,靠近读卡器时不要同时放多张卡。MF卡本身支持防碰撞,但你手里的卡和交通卡叠在一起,很容易导致读卡器选择到错误的UID,尤其在大批量卡片测试时要注意。每次只放一张最稳妥。

6. 从读取到理解:一次完整的上手路径

如果你刚开始接触MF卡读取,我建议按这个顺序走一遍:先用ACR122U把PC/SC链路跑通,然后用最简代码获取UID,再单独验证认证指令和读取指令,最后才跑完整dump脚本。每一步都确认返回值正确,再去读下一阶段。不要一上来就复制一个大脚本跑,出了错都不知道是协议问题、驱动问题还是代码问题。

我自己最初接触这块,也是从一张无法读取的旧门禁卡开始的。当时连APDU是什么意思都不知道,对着错误码一头雾水。后来把卡片结构、认证粒度、FF 86FF B0的指令格式理解清楚之后,突然就通了——读卡器、PC/SC、APDU和卡片,四个环节其实都有迹可循。现在回头再看这类问题,大部分卡壳点其实不是代码本身,而是对协议栈和卡片结构没有一个整体认识。

最后再分享一个小技巧:如果你要长期做NFC相关开发,最好准备一张专门用于测试的Mifare Classic 1K空卡,用软件给它写入一段有规律的数据,比如从第1扇区第0块开始写01 02 03 ... 0F 10。每次调试读卡流程时,拿这张卡做基准,读出来对不上就知道是代码问题,而不是在纠结卡里面到底是什么数据。有了这套基准卡,整个开发效率会提升一大截。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询