技术声明: 本文所涉及的逆向分析仅用于技术交流与安全研究。通过分析此类系统的设计缺陷,我们可以更深刻地理解物联网设备在数据校验与重放攻击防护方面的挑战。请勿将相关技术用于任何违反法律或相关管理规定的行为。
在物联网安全研究中,基于 MIFARE Classic (M1) 芯片的 IC 卡系统是一个经典的案例。最近我针对某非接触式 IC 卡水表系统进行了深度的扇区数据逆向,发现其看似复杂的数据背后隐藏着非常标准且具有规律的逻辑设计。
本文将从数据分布、金额换算以及校验机制三个维度,拆解该系统的运行原理。
一、 扇区功能划分:双钱包架构
该系统采用了典型的“双扇区联动”逻辑,将卡片划分为“充值钱包”和“运行余额”两个独立模块:
- 扇区 7(充值钱包): 存放通过充值机写入的待转入金额 。用户在充值后,卡内金额并不会立刻生效,而是暂存在此 。
- 扇区 8(运行余额): 真正的消费余额 。当卡片接入接水机后,机器会识别扇区 7 的金额并将其“存入”扇区 8 。接水时的扣费操作直接作用于该扇区,且该扇区支持三位小数精度 。
二、 金额存储格式:Value Block 安全机制

通过对多组样本(如卡 1 与卡 2)的 Hex 数据对比,我们发现其金额存储采用了 M1 芯片标准的 Value Block(值块) 格式。
以一段典型的金额数据 a0860100 5f79feff 为例:
- 金额正码 (Byte 0-3): 采用 Little-Endian(小端序) 存储。
a0 86 01 00还原为十六进制即为0x000186A0,换算为十进制为 $100,000$。由于系统保留三位小数,实际代表 $100.000$ 元。 - 金额反码 (Byte 4-7): 这是为了硬件校验而设计的冗余位。反码是正码的按位取反(Bitwise NOT)。例如
a0的取反结果正是5f。 - 安全逻辑: 在任何时候,
正码 + 反码必须等于0xFFFFFFFF。如果两者不匹配,读卡器会判定数据损坏。
三、 数据指纹:时间戳与流水号
在金额数据之后(Byte 8-14),系统记录了操作的“指纹”信息:
- 时间戳: 记录了最后一次操作的具体时间,格式通常为
YY MM DD HH MM。例如26 03 18 15 39代表 2026年3月18日 15:39。 - 流水号 (Sequence Number): 位于 Byte 13-14。每发生一次充值或扣费,该数值会递增。这是系统对抗 重放攻击 (Replay Attack) 的核心防线——如果读卡器发现卡内流水号小于等于系统记录的数值,会拒绝交易。
四、 校验机制:脆弱的累加和 (Sum Check)
该系统最核心的安全瓶颈在于其校验算法。通过对 Byte 0 到 Byte 14 的数据进行加法运算,我们发现 Byte 15(最后一个字节)是前 15 字节的累加和取低 8 位。
计算公式:Checksum = (Sum(Byte 0...14)) AND 0xFF五、示例代码
import binascii
def generate_water_card_hex(amount_yuan, timestamp_hex="0000000000", sequence_hex="0800"):
"""
演示:根据金额生成符合该水表系统逻辑的 16 字节 Hex 字符串
单位:1单位 = 0.001元 (三位小数)
"""
# 1. 金额转为十进制单位 (100元 -> 100000单位)
amount_units = int(round(amount_yuan * 1000))
# 2. 生成 4 字节小端序正码 (Positive Code)
# 例如: 100000 -> 0x000186A0 -> A0 86 01 00
pos_hex = binascii.hexlify(amount_units.to_bytes(4, 'little')).decode()
# 3. 生成 4 字节按位取反反码 (Negative Code)
# 逻辑: 正码 ^ 0xFFFFFFFF
neg_bytes = bytes([b ^ 0xFF for b in binascii.unhexlify(pos_hex)])
neg_hex = binascii.hexlify(neg_bytes).decode()
# 4. 拼接前 15 字节 (金额 + 时间戳 + 流水号)
payload = pos_hex + neg_hex + timestamp_hex + sequence_hex
# 5. 计算第 16 字节校验位 (Sum Check)
# 算法: 前 15 字节累加和 AND 0xFF
checksum = hex(sum(binascii.unhexlify(payload)) & 0xFF)[2:].zfill(2)
return (payload + checksum).lower()
# 示例:生成 100.000 元的修改字符串
print(f"100元对应的Hex串: {generate_water_card_hex(100.0)}")