资讯动态

MF卡协议分析与NFC读取实操:从卡型识别到密钥恢复

发布时间:2026/10/16 6:58:26 来源:尧图企业网站定制
简介这份资源面向Android开发与物联网方向的学习者聚焦MF卡Mifare协议解析与NFC读取实践帮助读者理解ISO 14443 Type A标准下13.56MHz非接触通信的命令响应机制并掌握在Android设备上完成卡片识别、扇区认证与块读写的完整流程。包内共245个文件以java源码、xml布局与配置、json数据、txt说明、png截图为主另含bin、so、cmake、ninja等编译产物与构建脚本以及apk安装包压缩包约10.12MB可直接导入Android Studio参考调试。资源围绕MIFARE Classic与Ultralight的SELECT、AUTHENTICATE、READ、WRITE等核心命令展开涉及3DES/AES密钥认证、扇区访问控制与防重写保护等要点并演示NfcAdapter的enableReaderMode监听、onTagDiscovered回调及MifareClassic连接认证读写与异常处理思路。目前已有452人学习下载适合需要打通NFC读卡链路、排查认证失败与读写错误的开发者参考。1. 从一张门禁卡说起MF卡协议分析到底在拆什么手里有一张门禁卡手机 NFC 贴上去“滴”一声什么也没发生。换一张卡能读出几行十六进制数据但看不懂。再换一张直接报错。这三种情况我全遇到过后来才明白问题不在手机在于没搞清楚这张卡到底属于哪个协议族。MF卡也就是 MIFARE 系列卡是 NFC 生态里存量最大的一类非接触式 IC 卡。它不是一个单一协议而是一整个家族从最早的 MF1MIFARE Classic到 MF0Ultralight、MF3DESFire每一代的存储结构、认证方式、指令集都不一样。协议分析要做的就是搞清楚你手里这张卡用的是哪套规则扇区怎么划分密钥怎么认证数据怎么编码。这份资源围绕 MF 卡协议分析和 NFC 读取实操展开适合做门禁系统对接、NFC 应用开发、物联网设备调试的从业者。如果你只是想知道“手机能不能复制门禁卡”那可能走错方向了但如果你需要在自己的程序里稳定读取 MF 卡数据或者排查为什么某张卡读不出来接下来的内容应该能省你不少时间。2. MF卡协议分析从卡型识别到扇区结构2.1 先搞清楚你面对的是哪一代 MF 卡MF 卡家族里最常见的三种MF1Classic、MF0Ultralight、MF3DESFire。它们的外观可能一模一样但内部结构完全不同。MF1 按扇区Sector和块Block组织每扇区 4 个块每块 16 字节第 0 扇区第 0 块是厂商数据不可写。MF0 结构更简单通常 16 页每页 4 字节没有复杂的认证机制。MF3 则是基于 ISO14443-4 的 APDU 指令交互安全性最高。识别卡型最直接的方式是看 SAKSelect Acknowledge值。NFC 读卡器在寻卡阶段返回的 SAK 字节能区分大部分卡型0x08 通常是 MF10x00 可能是 MF0 或 ISO14443-4 卡0x20 则指向 DESFire 或 Plus 系列。但 SAK 不是唯一依据有些卡会伪装或使用兼容模式所以还需要结合 ATQA 和后续的认证响应来综合判断。我一般会先用一个最小读卡脚本把 SAK、ATQA、UID 全部打印出来再决定后续用哪套指令集去操作。这一步不做后面全是盲猜。2.2 MF1 的存储结构与访问控制MF1 的存储分为 16 个扇区Sector 0~15每个扇区 4 个块Block 0~3共 64 块。每块 16 字节总容量 1KB。第 0 扇区的第 0 块存放厂商数据包括 UID、BCC、SAK、ATQA 等出厂后不可更改。每个扇区的最后一个块是“尾块”Trailer Block存放两个密钥Key A 和 Key B以及访问控制位Access Bits。访问控制位决定了每个块用 Key A 还是 Key B 认证后能读、能写、能加减值。很多门禁系统只改了 Key AKey B 留默认值或者反过来。如果你拿到一张卡读不出数据大概率是密钥不对或者访问控制位限制了读权限。下面这段 Python 代码用nfcpy库做一次基础的卡型识别和 MF1 扇区扫描。注意nfcpy需要配合 PN532 或 ACR122U 这类读卡器使用手机内置 NFC 在桌面端通常不直接可用。import nfc import nfc.clf import binascii def on_connect(tag): print(UID:, binascii.hexlify(tag.identifier).decode()) print(SAK:, hex(tag.sak)) print(ATQA:, binascii.hexlify(tag.atqa).decode()) # 判断是否为 MF1 类型 if tag.sak 0x08: print(卡型识别: MIFARE Classic 1K) # 尝试用默认密钥认证每个扇区 for sector in range(16): # 默认 Key A 常见值FFFFFFFFFFFF 或 A0A1A2A3A4A5 key_a b\xFF\xFF\xFF\xFF\xFF\xFF try: # authenticate 需要 block 号每扇区第一个块 block sector * 4 tag.authenticate(block, key_a, A) print(f扇区 {sector}: Key A 认证成功) # 认证成功后读取该扇区所有块 for b in range(4): data tag.read(sector * 4 b) print(f 块 {sector*4b}: {binascii.hexlify(data).decode()}) except Exception as e: print(f扇区 {sector}: 认证失败 - {e}) else: print(非 MF1 卡SAK:, hex(tag.sak)) return False # 初始化读卡器根据实际设备修改 clf nfc.ContactlessFrontend(usb) print(等待卡片...) clf.connect(rdwr{on-connect: on_connect}) clf.close()这段代码的逻辑很直白先通过on_connect回调拿到卡的 UID、SAK、ATQA用 SAK 做初步卡型判断。如果是 MF1就遍历 16 个扇区用常见的默认 Key A 尝试认证。认证通过后读取该扇区 4 个块的数据并打印十六进制。参数说明key_a这里写死了两个常见默认值实际项目中应该做成字典或列表逐个尝试。tag.authenticate(block, key, A)中的block是该扇区的第一个块号A表示用 Key A 认证。如果卡被修改过密钥这里会抛异常需要换成暴力枚举或字典攻击——但那是另一个话题了而且涉及合规边界自己把握。2.3 从 SAK 到扇区映射一次完整的读取流程实际项目中读一张 MF1 卡的完整流程是这样的寻卡 → 获取 UID → 选卡 → 认证扇区 → 读块 → 解析数据。每一步都可能出问题。寻卡阶段如果读卡器天线功率不够卡可能时有时无。选卡阶段如果 UID 长度不是标准的 4 字节或 7 字节某些读卡器会直接失败。认证阶段密钥错误是最常见的翻车点。我习惯在代码里加一层重试机制认证失败后延时 50ms 再试一次连续三次失败才跳过该扇区。因为有些读卡器在快速切换扇区时会有时序问题一次失败不代表密钥真的不对。另外MF1 的数据区不一定全是文本。很多门禁系统会把卡号、有效期、权限位打包成二进制格式比如前 4 字节是卡号小端序接下来 2 字节是日期偏移。这部分没有统一标准只能根据具体系统去逆向。协议分析的价值就在这里你知道数据存在哪个块但数据本身的含义需要结合业务逻辑去推断。3. NFC 读取 MF 卡的实操链路硬件、库与认证3.1 读卡器选型与驱动配置桌面端做 NFC 读取常见硬件方案有三种PN532 模块通过 UART 或 I2C 连接、ACR122UUSB 直插、以及手机内置 NFC通过 Android NFC API。PN532 最便宜但需要自己接线和配置串口ACR122U 即插即用但价格稍高手机最方便但受限于 Android 版本和厂商 NFC 实现差异。我一般用 ACR122U 做快速验证因为它免驱Linux 和 Windows 都能直接识别为 PC/SC 读卡器。PN532 适合集成到嵌入式项目里比如用树莓派做固定式读卡终端。手机方案适合做现场调试但 Android 对 MF1 的读取支持并不完整——很多机型只能读到 UID读不了扇区数据因为厂商 NFC 栈没有暴露底层认证接口。在 Linux 下用 ACR122U需要先确认pcscd服务在运行然后安装python-nfc或nfcpy。如果nfcpy报 “No backend available”通常是libusb权限问题加一条 udev 规则即可。# 检查读卡器是否被识别 lsusb | grep -i acr # 安装 pcscd 和依赖 sudo apt-get install pcscd libpcsclite-dev libusb-1.0-0-dev # 启动 pcscd 服务 sudo systemctl start pcscd sudo systemctl enable pcscd # 安装 nfcpy pip install nfcpy # 测试读卡器连接 python -m nfc --search-tty这段命令的逻辑先确认 USB 层面读卡器被识别再装 PC/SC 中间件和 libusb 开发库然后启动 pcscd 守护进程。python -m nfc --search-tty是nfcpy自带的诊断命令能列出可用的读卡器接口。如果这一步报错后面所有代码都跑不起来。参数说明pcscd是 PC/SC 智能卡服务的守护进程ACR122U 通过它暴露接口。libpcsclite-dev是开发头文件nfcpy编译时需要。libusb-1.0-0-dev是 USB 通信库。这三个缺一不可。3.2 认证流程中的密钥管理与重试策略MF1 的认证是双向的读卡器发送认证请求卡返回一个随机数读卡器用密钥加密后发回卡验证通过后才允许后续读写。这个过程在nfcpy里被封装成了tag.authenticate()但底层是 ISO14443-3 的 Crypto1 算法。密钥管理是实际项目中最容易出问题的地方。很多门禁系统出厂后从不改默认密钥所以FFFFFFFFFFFF和A0A1A2A3A4A5能打开大部分扇区。但也有一些系统会每个扇区用不同密钥甚至用一卡一密的方案。这时候就需要先做密钥枚举把常见默认密钥和弱密钥跑一遍。下面这段代码展示了一个更健壮的认证流程带重试和密钥列表import nfc import binascii import time # 常见默认密钥列表实际项目中可以扩展 COMMON_KEYS [ b\xFF\xFF\xFF\xFF\xFF\xFF, b\xA0\xA1\xA2\xA3\xA4\xA5, b\xD3\xF7\xD3\xF7\xD3\xF7, b\x00\x00\x00\x00\x00\x00, ] def authenticate_with_retry(tag, block, key, key_typeA, retries3): 带重试的扇区认证 for i in range(retries): try: tag.authenticate(block, key, key_type) return True except Exception: time.sleep(0.05) # 50ms 延时后重试 return False def scan_card(tag): if tag.sak ! 0x08: print(非 MF1 卡跳过扇区扫描) return for sector in range(16): block sector * 4 found False for key in COMMON_KEYS: if authenticate_with_retry(tag, block, key, A): print(f扇区 {sector}: Key A {binascii.hexlify(key).decode()}) found True break if authenticate_with_retry(tag, block, key, B): print(f扇区 {sector}: Key B {binascii.hexlify(key).decode()}) found True break if not found: print(f扇区 {sector}: 未找到可用密钥) clf nfc.ContactlessFrontend(usb) clf.connect(rdwr{on-connect: scan_card}) clf.close()逻辑说明authenticate_with_retry封装了重试逻辑每次失败后等 50ms 再试最多三次。scan_card遍历 16 个扇区对每个扇区先试 Key A 再试 Key B命中就打印密钥并跳到下一个扇区。参数说明COMMON_KEYS列表可以根据目标系统的已知信息扩展。retries3是经验值再多会拖慢整体扫描速度。time.sleep(0.05)的 50ms 是权衡后的结果——太短重试无效太长扫描一张卡要好几秒。提示密钥枚举只应在你拥有合法授权的卡片和系统上使用。未经授权对他人卡片或系统进行密钥探测可能违反法律法规。3.3 数据解析从十六进制到业务字段读出扇区数据只是第一步真正的难点在于把十六进制字节翻译成业务字段。MF1 卡里常见的数据类型有几种卡号通常 2~4 字节小端序、日期可能是 Unix 时间戳、天数偏移或 BCD 编码、权限位按位标记、金额可能是分单位的小端整数。我一般会先把整个扇区的数据 dump 成十六进制然后对比不同卡之间的差异。比如拿两张已知卡号的卡对比同一块的数据变化的部分大概率就是卡号字段。再拿一张过期卡和一张有效卡对比变化的部分可能包含日期或权限位。def parse_sector_data(hex_str): 尝试解析扇区数据中的常见字段 data bytes.fromhex(hex_str) # 尝试解析为小端序卡号前4字节 card_no_le int.from_bytes(data[0:4], little) card_no_be int.from_bytes(data[0:4], big) print(f 小端序卡号: {card_no_le}) print(f 大端序卡号: {card_no_be}) # 尝试解析为 BCD 日期假设第5-7字节 try: bcd_str data[4:7].hex() year int(bcd_str[0:2]) month int(bcd_str[2:4]) day int(bcd_str[4:6]) print(f BCD 日期: 20{year:02d}-{month:02d}-{day:02d}) except: print( 日期解析失败) # 打印原始十六进制供人工比对 print(f 原始数据: {hex_str}) # 示例解析一个已读取的扇区 sample_hex a1b2c3d4e5f607080910111213141516 parse_sector_data(sample_hex)这段代码展示了三种常见字段的解析尝试小端序卡号、大端序卡号、BCD 日期。实际项目中你需要根据对比结果确定哪种解析方式正确。参数说明int.from_bytes(data[0:4], little)把前 4 字节按小端序转成整数。data[4:7].hex()取第 5 到第 7 字节转十六进制字符串再按两位一组解析年月日。这些偏移量都是假设值必须根据实际数据调整。4. 避坑与排查MF卡读取失败的六种典型情况4.1 读卡器识别不到卡片现象程序运行后一直停在“等待卡片”或者clf.connect()直接超时。原因天线功率不足、卡片距离太远、读卡器驱动未正确加载、或者卡片本身是低频卡125kHz而非高频卡13.56MHz。MF 卡是高频卡如果拿的是 EM4100 这类低频卡NFC 读卡器根本感应不到。解决先把卡片贴到读卡器天线正上方距离控制在 2cm 以内。检查lsusb是否识别到读卡器。确认卡片类型——低频卡需要专门的 125kHz 读卡器NFC 设备不支持。4.2 SAK 值识别错误导致卡型判断失败现象SAK 返回 0x00 或 0x20程序按 MF1 流程走认证全部失败。原因SAK 不是卡型的唯一标识。有些 MF1 兼容卡会返回非标准 SAK有些 DESFire 卡在未选卡前也返回 0x00。另外某些读卡器固件会修改 SAK 返回值。解决不要只依赖 SAK。结合 ATQA 和认证响应综合判断。如果 SAK 为 0x00先尝试 ISO14443-4 的 RATS 指令看卡是否响应。如果响应了大概率是 DESFire 或 Plus 系列需要走 APDU 流程。4.3 认证失败但密钥确认正确现象用已知正确的密钥认证仍然抛异常。原因认证的块号不对。MF1 的认证必须以扇区的第一个块Block 0、4、8…为目标不能直接认证尾块或其他块。另外有些读卡器在认证前需要先执行选卡Select操作否则认证指令会被忽略。解决确认authenticate的块号是sector * 4。在认证前加一步显式选卡tag.select()如果库支持。检查读卡器固件版本某些老固件对认证时序要求严格。4.4 读出的数据全是 0x00 或 0xFF现象认证成功但读出的块数据全是 0x00 或 0xFF。原因访问控制位限制了读权限。MF1 的尾块中访问控制位可以设置某个块为“不可读”此时读操作返回全 0。另外空白卡或未初始化的卡也会返回全 0。解决读取尾块每个扇区的第 3 块解析访问控制位。尾块的第 6~9 字节是访问控制位按位解析后能知道每个块的读写权限。如果确实不可读尝试用另一个密钥认证或者确认这张卡是否被设计为只写不读。4.5 多扇区读取时程序崩溃或卡死现象读前几个扇区正常读到某个扇区时程序无响应或抛异常后无法继续。原因某个扇区的认证失败后读卡器状态没有正确复位。MF1 在认证失败后会进入锁定状态需要重新寻卡才能继续操作。另外Python 的异常如果没有捕获会导致整个连接断开。解决每个扇区操作都用 try-except 包裹认证失败后执行tag.close()再重新clf.connect()。或者用nfcpy的tag对象重建机制每次认证失败后重新获取 tag 实例。4.6 手机 NFC 读不到扇区数据现象手机能读到 UID但读不到扇区数据或者直接报“不支持该标签类型”。原因Android 的 NFC 栈对 MF1 的支持不完整。很多厂商 ROM 只实现了 NDEF 读写和 UID 读取没有暴露底层认证接口。另外Android 版本不同MifareClassic类的行为也有差异。解决如果必须用手机读扇区数据需要 root 权限或者使用外接读卡器通过 OTG 连接 ACR122U。普通应用层开发只能读到 UID 和 NDEF 数据。这不是代码问题是系统限制。5. 进阶技巧用字典攻击思路做密钥恢复与数据校验密钥恢复是 MF1 协议分析里绕不开的一环。很多门禁系统虽然改了默认密钥但改得不彻底——比如只用 6 个字节的简单组合或者所有扇区用同一个密钥。这时候用字典攻击比暴力枚举高效得多。我一般会准备一个分层字典第一层是厂商默认密钥几十个第二层是常见弱密钥几百个第三层是基于已知密钥的变体比如逐字节递增。实测下来第一层能打开大约六成的卡第二层能再覆盖两成剩下的两成基本需要针对性逆向。import itertools def generate_key_variants(base_key): 基于已知密钥生成变体 variants [base_key] # 逐字节 1 变体 for i in range(6): new_key bytearray(base_key) new_key[i] (new_key[i] 1) 0xFF variants.append(bytes(new_key)) # 逐字节 -1 变体 for i in range(6): new_key bytearray(base_key) new_key[i] (new_key[i] - 1) 0xFF variants.append(bytes(new_key)) return variants def build_key_dictionary(): 构建分层密钥字典 # 第一层厂商默认 layer1 [ b\xFF\xFF\xFF\xFF\xFF\xFF, b\xA0\xA1\xA2\xA3\xA4\xA5, b\xD3\xF7\xD3\xF7\xD3\xF7, b\x00\x00\x00\x00\x00\x00, b\xAA\xBB\xCC\xDD\xEE\xFF, ] # 第二层基于第一层生成变体 layer2 [] for key in layer1: layer2.extend(generate_key_variants(key)) # 去重 seen set() result [] for k in layer1 layer2: if k not in seen: seen.add(k) result.append(k) return result # 使用示例 keys build_key_dictionary() print(f字典大小: {len(keys)}) for k in keys[:10]: print(k.hex())这段代码的逻辑generate_key_variants对每个基础密钥做逐字节加减一生成 12 个变体。build_key_dictionary把默认密钥和变体合并去重形成一个约 60 个密钥的字典。实际使用时把这个字典传给前面的scan_card函数即可。参数说明base_key必须是 6 字节的 bytes 对象。(new_key[i] 1) 0xFF确保字节值在 0~255 范围内循环。字典大小控制在几百以内比较合理太大扫描一张卡要几十秒。数据校验是另一个容易被忽略的环节。MF1 的每个扇区尾块都有访问控制位但数据块本身没有校验和。如果读出的数据看起来“不对劲”可能是读到了错误的数据。我一般会做两次读取对比结果是否一致。如果不一致说明通信不稳定需要降低读卡器功率或缩短距离。还有一个技巧用 MF1 的加减值操作来验证数据完整性。某些扇区被配置为值块Value Block支持 Increment/Decrement 操作。如果能成功执行加减值说明认证和数据读取都是正确的。这比单纯读数据更可靠。从那以后我每次做 MF 卡读取都强制走一遍“SAK 识别 → 密钥字典扫描 → 双次读取校验”的流程不再跳步。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑