今天打算写一写,如何用 Arduino 复制 RFID 卡。
写下来就不容易忘,也算是复盘记录。

1.RFID 卡的内部数据结构
一般的 RFID 卡是 MIFARE Classic 1K 卡。
这张卡的存储结构如下:
- 总 1024 bytes (1KB)
- 16 个 Sector (0~15)
- 每个 Sector 有 4 个 Block (Block 0~3)
- 每个 Block 为 16 bytes每个扇区的结构如下:
Sector n
├── Block 0 (Data or UID block)
├── Block 1 (Data)
├── Block 2 (Data)
└── Block 3 (Sector Trailer: Key A, Access Bits, Key B)在这些扇区中,最重要的是扇区 0 的第一个数据块。
UID 就存放在这里。

2.UID
UID 是 Unique IDentifier 的缩写,是指分配给每一张 RFID 卡(如交通卡、门禁卡、学生证等)的唯一识别号码。
相当于人的身份证号。
UID 保存在 Sector 0 的 Block 0 内部前 4 个字节中。
[ UID0 | UID1 | UID2 | UID3 | BCC | Manufacturer Data… ]RFID 读卡设备在读卡时,最先读取的就是这一部分。
以此为基础进行卡片识别。

3. CUID 卡
关键在于,正品 MIFARE Classic 1K 的 UID 是在工厂中写死的。
NXP 原厂芯片的 UID 存在 ROM 里,无法通过任何方式修改。
因此,用普通读卡器或 MFRC522 是无法篡改 UID 的。

但市面上存在可以修改 UID 的卡,这类卡被称为 CUID(俗称魔术卡)。
前面的 C 代表 Changeable。
外观与 MIFARE Classic 1K 相同,但内部芯片不同,具体如下:
① Gen1A(支持 UID/Backdoor 类型)
支持 0x40 / 0x43 后门指令
单独提供修改 UID 的指令
可用 MFRC522 + Arduino 库的 MIFARE_SetUid() 进行修改
② CUID / Gen2(Block 0 可写)
没有后门指令
但设计成可以通过普通写入(WRITE)覆盖 Block 0(存 UID 的扇区)
如果 MFRC522 能接受 MIFARE_Write(0…) 指令,就能修改 UID
不过,并不是所有 CUID 都可以这么干;若 MFRC522 不支持,就需要 PN532/ACR122U
4. 用 Arduino 复制 RFID 卡
原理已经清楚,用 Arduino 复制 RFID 卡就很简单了。
做的事情就是复制 UID。
首先按下图连接 Arduino:

Signal | MFRC522 Pin | Arduino Uno / 101 | Arduino Mega | Arduino Nano v3 | Arduino Leonardo / Micro | Arduino Pro Micro |
|---|---|---|---|---|---|---|
RST / Reset | RST | 9 | 5 | D9 | RESET / ICSP-5 | RST |
SPI SS | SDA (SS) | 10 | 53 | D10 | 10 | 10 |
SPI MOSI | MOSI | 11 / ICSP-4 | 51 | D11 | ICSP-4 | 16 |
SPI MISO | MISO | 12 / ICSP-1 | 50 | D12 | ICSP-1 | 14 |
SPI SCK | SCK | 13 / ICSP-3 | 52 | D13 | ICSP-3 | 15 |
然后在 Arduino IDE 中搜索并安装 MFRC522 库。

接着,选择 文件 -> 示例 -> MFRC522 -> ReadNUID。

上传示例代码后,先读取 UID 值。
UID 一般由 4 组 16 进制数组成。
F5 5F 36 80如果得到如上的 UID,需要写入的 UID 则是下面这样:
数字前的 0x 表示这是 16 进制数。
0xF5 0x5F 0x36 0x805. 问题与解决
本想用示例里的 ChangeUID 轻松搞定,但一直失败。
0x40 后门命令总是超时。
后来才发现我的卡是 Gen2 卡,只需要直接访问 0 号扇区并替换 UID 即可。
代码如下:
#include <SPI.h>
#include <MFRC522.h>
#define RST_PIN 9
#define SS_PIN 10
MFRC522 mfrc522(SS_PIN, RST_PIN);
MFRC522::MIFARE_Key key;
// 바꾸고 싶은 새 UID (4바이트)
byte newUid[4] = { 0xF5, 0x5F, 0x36, 0x80 }; // 예시
void setup() {
Serial.begin(9600);
while (!Serial) {}
SPI.begin();
mfrc522.PCD_Init();
Serial.println(F("CUID 카드 Block 0에 직접 UID 쓰기 예제"));
// 기본 키 FF..FF 세팅
for (byte i = 0; i < 6; i++) {
key.keyByte[i] = 0xFF;
}
}
void loop() {
// 새 카드 대기
if (!mfrc522.PICC_IsNewCardPresent() || !mfrc522.PICC_ReadCardSerial()) {
return;
}
Serial.print(F("현재 카드 UID: "));
for (byte i = 0; i < mfrc522.uid.size; i++) {
Serial.print(mfrc522.uid.uidByte[i], HEX);
Serial.print(" ");
}
Serial.println();
// === 1. 섹터 0 인증 (Block 0) ===
byte block = 0; // Block 0
MFRC522::StatusCode status;
status = mfrc522.PCD_Authenticate(
MFRC522::PICC_CMD_MF_AUTH_KEY_A,
block,
&key,
&(mfrc522.uid)
);
if (status != MFRC522::STATUS_OK) {
Serial.print(F("인증 실패: "));
Serial.println(mfrc522.GetStatusCodeName(status));
goto HALT;
}
// === 2. 기존 Block 0 내용을 읽어서 manufacturer 부분 보존 ===
byte block0[18]; // 16바이트 + 크기 정보
byte size = sizeof(block0);
status = mfrc522.MIFARE_Read(0, block0, &size);
if (status != MFRC522::STATUS_OK) {
Serial.print(F("Block 0 읽기 실패: "));
Serial.println(mfrc522.GetStatusCodeName(status));
goto HALT;
}
// block0[0..3] = 원래 UID
// block0[4] = BCC (UID 4바이트 XOR)
// block0[5..15]= 제조사 데이터 등
// === 3. 새 UID + BCC 계산해서 덮어쓰기 ===
byte bcc = newUid[0] ^ newUid[1] ^ newUid[2] ^ newUid[3];
block0[0] = newUid[0];
block0[1] = newUid[1];
block0[2] = newUid[2];
block0[3] = newUid[3];
block0[4] = bcc;
// block0[5..15] 는 그대로 두면 제조사 데이터 유지
status = mfrc522.MIFARE_Write(0, block0, 16);
if (status != MFRC522::STATUS_OK) {
Serial.print(F("Block 0 쓰기 실패: "));
Serial.println(mfrc522.GetStatusCodeName(status));
goto HALT;
}
Serial.println(F("새 UID 쓰기 완료. 카드를 떼었다가 다시 대보세요."));
HALT:
mfrc522.PICC_HaltA();
mfrc522.PCD_StopCrypto1();
delay(1000);
} 6. 后记
这次总算弄清楚 RFID 模块的正确用法了。
在搜索时,常能看到用门卡开门之类的项目,看起来实现起来也不难。
今后想多做一些不同方向的应用。
顺带一提,如果用这个方法卡仍然无法被识别,很大可能是频率问题(13.56MHz、125KHz 等)。
如果有问题欢迎在评论区提问。
댓글을 불러오는 중...