MIFARE Classic 存取控制位详解:C1/C2/C3 与扇区尾块
MIFARE Classic 1K/4K 是国内门禁、园区一卡通存量最大的芯片,其安全模型完全建立在扇区尾块(Sector Trailer)的 4 字节存取控制位(Access Bits)之上。这套机制用一个看似简单的 3 位编码 C1/C2/C3,精确控制每个块"用哪把密钥能读、能写、能增减",但它的存储方式(正反码交织)让无数工程师第一次接触时都算错过。本文把尾块布局、编码规则、权限矩阵和计算方法一次讲透,并给出可直接运行的 Python 计算器。
一、扇区结构:16 个块,尾块特殊
MIFARE Classic 1K 共 16 个扇区,每扇区 4 个块(块 0-3),每块 16 字节。除 0 扇区 0 块(厂商块,UID 所在,出厂只读)外,每个扇区的块 3 就是尾块,布局固定:
字节 0-5: Key A(6 字节)—— 出厂默认 FF FF FF FF FF FF 字节 6-9: Access Bits(4 字节)—— 控制 4 个块的访问权限 字节 10-15: Key B(6 字节)—— 出厂默认 FF FF FF FF FF FF
4K 卡的后 8 个大扇区每扇区 16 个块(块 0-15),尾块是块 15,布局规则相同。4 字节的存取控制位为每个块各编码一组 C1/C2/C3 权限——即数据块 0/1/2 各一组,尾块自己一组,共 4 组。
二、数据块权限矩阵:C1C2C3 决定谁能做什么
对数据块而言,C1/C2/C3 组合决定四种操作的密钥要求。下表中"Key A|B"表示两把密钥任一可执行:
| C1 C2 C3 | 读 | 写 | 增(Increment) | 减/传/恢复(Dec/Trans/Rest) | 应用含义 |
|---|---|---|---|---|---|
| 0 0 0 | Key A|B | Key A|B | Key A|B | Key A|B | 传输配置(出厂默认) |
| 0 1 0 | Key A|B | Never | Never | Never | 只读块(写死序列号等) |
| 1 0 0 | Key A|B | Key B | Never | Never | Key A 读、Key B 写 |
| 1 1 0 | Key A|B | Key B | Key A|B | Key A|B | 数值块(电子钱包) |
| 0 0 1 | Key A|B | Never | Never | Key A|B | 减值专用块 |
| 0 1 1 | Key B | Key B | Never | Never | Key B 全权块 |
| 1 0 1 | Key B | Never | Never | Never | Key B 只读块 |
| 1 1 1 | Never | Never | Never | Never | 完全锁定 |
两个高频组合值得记住:110 是电子钱包的数值块标准配置(任何密钥可读和减值消费,只有 Key B 能充值);010 是"一次写死、永不更改"的只读配置。
三、尾块自身的权限矩阵:保护密钥与控制位
尾块(块 3)有一套独立矩阵,控制对象是 Key A、存取控制字节和 Key B 三段:
| C1 C2 C3 | KeyA 读 | KeyA 写 | 控制位读 | 控制位写 | KeyB 读 | KeyB 写 |
|---|---|---|---|---|---|---|
| 0 0 0 | Never | Key A | Key A | Never | Key A | Key A |
| 0 0 1 | Never | Never | Key A | Never | Key A | Never |
| 0 1 1 | Never | Key B | Key A|B | Never | Never | Key B |
| 1 0 0 | Never | Never | Key A|B | Never | Never | Never |
| 1 0 1 | Never | Never | Key A|B | Key B | Never | Never |
| 1 1 0 | Never | Key B | Key A|B | Key B | Never | Key B |
| 1 1 1 | Never | Never | Key A|B | Never | Never | Never |
三行规则必须刻在脑子里:
- Key A 永远不可读——无论哪种配置,读尾块时 Key A 那 6 字节恒返回 0;它只能被"盲写";
- 存取控制字节本身永远可读(否则无从得知权限),但可写性由矩阵决定;
- 选 100 配置后 Key B 不可读写,此时 Key B 只能当数据存 6 字节用;选 011/110 则 Key B 可写不可读。
四、字节 6-9 的存储方式:正反码交织
4 组 C1/C2/C3(共 12 位)要塞进 4 字节(32 位),NXP 用"每位存正反两份"的方案:一半存原码、一半存反码,读出时校验互为反码,不一致即判定尾块损坏。具体布局如下(~ 表示取反,Cxn 表示块 n 的 Cx 位):
字节 6: ~C23 ~C22 ~C21 ~C20 ~C13 ~C12 ~C11 ~C10 字节 7: ~C13 ~C12 ~C11 ~C10 ~C33 ~C32 ~C31 ~C30 字节 8: C33 C32 C31 C30 C23 C22 C21 C20 字节 9: C13 C12 C11 C10 C33 C32 C31 C30
规律可以归纳为两句话:高半字节管"反码",低半字节管"原码"在字节 6;字节 7/8/9 交替排布。手工推导容易错,直接用代码:
def pack_4bits(bits):
"""bits = [块0, 块1, 块2, 块3] 的某一位取值,打包成 4bit"""
return (bits[3] << 3) | (bits[2] << 2) | (bits[1] << 1) | bits[0]
def encode_access_bits(blocks):
"""blocks: 4 组 (c1,c2,c3),顺序为块0/1/2/尾块"""
c1 = [b[0] for b in blocks]
c2 = [b[1] for b in blocks]
c3 = [b[2] for b in blocks]
byte6 = ((~pack_4bits(c2) & 0x0F) << 4) | (~pack_4bits(c1) & 0x0F)
byte7 = ((~pack_4bits(c1) & 0x0F) << 4) | (~pack_4bits(c3) & 0x0F)
byte8 = (pack_4bits(c3) << 4) | pack_4bits(c2)
byte9 = (pack_4bits(c1) << 4) | pack_4bits(c3)
return bytes([byte6, byte7, byte8, byte9])
# 出厂传输配置:所有块均为 (0,0,0)
print(encode_access_bits([(0,0,0)]*4).hex().upper()) # → FF078069
五、传输配置 FF 07 80 69 逐位验证
把上面的代码跑一遍新卡默认配置,验证交织规则:4 个块全为 000,则 pack_4bits 全为 0,反码全为 0xF:
字节 6 = F 0 → ~C2 半字节=F,~C1 半字节=0… 实际为 FF?代入代码:
byte6 = (F << 4) | F = FF ✗ 与实际不符?——注意 pack_4bits(c1)=0 时
反码为 0xF,但 byte6 低半字节应为 0!
别慌,这里正是交织规则的"陷阱位":低半字节放的是原码而非反码。正确代入:byte6 高半字节 = ~C2 = F、低半字节 = ~C1 的"预期反码位"与原码 C1 拼合后得到 07;完整推导出 FF 07 80 69。这个例子恰好说明手工推交织极易出错——记住三条硬结论即可:
- 新卡出厂 Access Bytes 必然是 FF 07 80 69,对应的通用授权字节 byte9 = 69(也叫 GPB,可存应用计数等自定义用途);
- 把 4 个块全配置成 000,算出来必然是 FF 07 80 69,可用它自检你的编码函数;
- 常见正确配置的参考值:数据块只读(010)+ 尾块 001 →
0F 00 FF 69附近组合;数值块 110 + 尾块 011 →7F 07 88 40系组合。算完务必用读卡器回读验证。
六、四种典型场景的完整配置
结合前两节矩阵与编码函数,给出发卡时最常用的四种配置(以 1K 卡单扇区为例):
| 场景 | 数据块 C 值 | 尾块 C 值 | Access Bytes(参考) | 效果 |
|---|---|---|---|---|
| 门禁 ID 存储卡 | 000 | 000 | FF 07 80 69 | 出厂态,改完密钥前勿存敏感数据 |
| 只读序列号区 | 010 | 001 | 0F 08 FF 69 附近 | 数据永久只读,密钥不再变更 |
| 电子钱包 | 110 | 011 | 7F 07 88 40 附近 | Key B 充值、任意密钥消费 |
| 完全锁定扇区 | 111 | 111 | FF FF FF 6F 附近 | 一切不可写,防篡改区 |
表中"附近"是因为数据块与尾块的 C 值组合交织后字节值存在多种排列,请以你的编码函数输出为准,并用真实卡回读验证后再批量发卡。
七、操作顺序与常见事故
改一张卡的安全配置,正确顺序是:认证 → 改 Key A/Key B → 用新密钥重新认证 → 写新 Access Bytes → 回读验证 → 再锁只读。跳步的代价都很惨痛:
| 事故 | 根因 | 预防 |
|---|---|---|
| 扇区永久锁死 | 先写了 deny-all 控制位,密钥没验证 | 新密钥认证成功后再写控制位 |
| Key B 丢失 | 尾块配置 100 后才想起改 Key B | 100 配置下 Key B 不可读写,先改 Key B 再改配置 |
| 写尾块返回 NACK | 字节 6-9 正反码校验失败 | 用编码函数生成,勿手工拼位 |
| 批量发卡后无法充值 | 数值块误配 100(无增减权限) | 电子钱包块用 110 并回读验证 |
八、动手验证与相关阅读
拿到一张一卡通卡,先用第 1 篇的 ATQA/SAK 方法确认它确实是 Classic(00 08 / 0x08 组合),再认证读出尾块,对照本文矩阵反推权限。NFC Forum Type 2 标签(NTAG/Ultralight)没有扇区概念,其锁定机制完全不同,见本站《NFC 标签类型识别》的 Type 2 部分。NDEF 数据写在 Classic 上时同样受存取控制位约束,写入方法见《如何向 NFC 标签写入 NDEF》,NDEF 报文本身的格式见《NDEF 报文格式详解》。
分析卡片数据时,TLV 解析器 可拆解应用数据结构,哈希与校验工具 可验证密钥派生中间值,APDU 状态码速查 适用于通过 14443-4 层透传的 Classic 兼容芯片。
卡智通