具備 Nonce 防側錄功能的 RFID 模擬器
如果駭客錄下了標籤發送的「加密後字串」,下次再重播(Replay)給讀取器,讀取器依然會認為是合法的標籤。
為了防止這種 重播攻擊 (Replay Attack),我們引入 Nonce (隨機數) 機制:
讀取器發送挑戰 (Challenge):讀取器先發送一個隨機數 $R$ 給標籤。
動態加密:標籤將
UID與這個 $R$ 結合後再進行加密。一次性密文:因為每次的 $R$ 都不同,所以即便 UID 相同,每次傳輸的加密字串都會改變。
具備 Nonce 防側錄功能的 RFID 模擬器
防重播攻擊的安全性分析
為什麼舊的密文失效了? 在表格中你可以觀察到,雖然我們只有一個標籤(ID 永遠是
53 71 37 A4 F6),但每一輪產生的 Cipher 都是完全不同的。如果駭客側錄了第一輪的密文
A1 B2...。在第二輪時,Reader 發出了新的 Nonce。
駭客重播
A1 B2...,Reader 使用新的 Nonce 去解密,結果會變成一串亂碼,驗證就會失敗。
雙向認證的基礎: 這是現代 RFID(如悠遊卡 Mifare DESFire)的核心。透過這種「挑戰-應答 (Challenge-Response)」模式,確保通訊內容具有 時效性 (Freshness)。
實務觀察:
黃色閃爍:標籤正在計算基於當前 Nonce 的專屬密文。
綠色/紅色:Reader 比對解密結果是否與資料庫中的 UID 符合。
import tkinter as tk
from tkinter import ttk
import random
import time
from threading import Thread, Event
class SecureRFIDCipher:
@staticmethod
def process(hex_data, hex_key, hex_nonce):
"""結合 Nonce 的加密邏輯:Cipher = (Data XOR Nonce) XOR KeyStream"""
try:
# 將 Data 與 Nonce 先進行初次混淆 (簡單範例:XOR)
val_data = int(hex_data.replace(" ", ""), 16)
val_nonce = int(hex_nonce.replace(" ", ""), 16)
mixed_hex = hex(val_data ^ val_nonce)[2:].upper()
# 以下套用之前的 Stream Cipher 邏輯
clean_hex = mixed_hex
plain_bin = bin(int(clean_hex, 16))[2:].zfill(len(clean_hex) * 4)
key_bin = bin(int(hex_key, 16))[2:].zfill(len(hex_key.replace(" ","")) * 4)
state = [int(b) for b in key_bin]
keystream = []
for _ in range(len(plain_bin)):
out = state[-1]
feedback = state[0] ^ state[2]
state = [feedback] + state[:-1]
keystream.append(out)
cipher_bits = [int(plain_bin[i]) ^ keystream[i] for i in range(len(plain_bin))]
cipher_bin = "".join(map(str, cipher_bits))
hex_out = hex(int(cipher_bin, 2))[2:].upper().zfill(len(clean_hex))
return " ".join(hex_out[i:i+2] for i in range(0, len(hex_out), 2))
except:
return "ERR"
class ReplayProtectionSim:
def __init__(self, root):
self.root = root
self.root.title("RFID 防重播攻擊 (Nonce Challenge) 模擬器")
self.system_key = "AC55"
self.current_nonce = "0000"
self.tag = {"id": "53 71 37 A4 F6", "label": "Secure-Tag"}
self.is_running = False
self.pause_event = Event()
self.pause_event.set()
self.setup_ui()
def setup_ui(self):
# 頂部:安全參數
param_frame = tk.Frame(self.root, bg="#2c3e50", pady=10)
param_frame.pack(fill="x")
tk.Label(param_frame, text="Current Nonce (R):", fg="#f1c40f", bg="#2c3e50", font=("Arial", 10, "bold")).pack(side=tk.LEFT, padx=10)
self.lbl_nonce = tk.Label(param_frame, text=self.current_nonce, fg="white", bg="#2c3e50", font=("Courier", 12))
self.lbl_nonce.pack(side=tk.LEFT)
# 視覺化
self.canvas = tk.Canvas(self.root, width=600, height=150, bg="white")
self.canvas.pack(pady=10)
self.tag_rect = self.canvas.create_rectangle(250, 40, 350, 100, fill="#ecf0f1", width=2)
self.canvas.create_text(300, 70, text="Tag-01", font=("Arial", 10, "bold"))
self.lbl_flow = self.canvas.create_text(300, 130, text="等待 Reader 挑戰...", fill="gray")
# 控制鈕
btn_frame = tk.Frame(self.root)
btn_frame.pack(pady=5)
self.btn_run = tk.Button(btn_frame, text="▶ 開始挑戰應答", command=self.start_sim, bg="#27ae60", fg="white", width=15)
self.btn_run.pack(side=tk.LEFT, padx=5)
tk.Button(btn_frame, text="⏹ 結束", command=self.stop_sim, bg="#c0392b", fg="white", width=12).pack(side=tk.LEFT, padx=5)
# 日誌表格
self.tree = ttk.Treeview(self.root, columns=("Nonce", "Cipher", "Verified"), show="headings", height=8)
for col in ("Nonce", "Cipher", "Verified"): self.tree.heading(col, text=col)
self.tree.pack(fill="both", padx=10, pady=10)
def stop_sim(self):
self.is_running = False
def start_sim(self):
if self.is_running: return
self.is_running = True
self.btn_run.config(state=tk.DISABLED)
Thread(target=self.protocol_logic, daemon=True).start()
def protocol_logic(self):
while self.is_running:
# 1. Reader 生成隨機 Nonce
self.current_nonce = hex(random.getrandbits(16))[2:].upper().zfill(4)
self.root.after(0, lambda: self.lbl_nonce.config(text=self.current_nonce))
self.root.after(0, lambda: self.canvas.itemconfig(self.lbl_flow, text=f"Reader 送出 Nonce: {self.current_nonce}", fill="blue"))
time.sleep(1.2)
# 2. Tag 接收 Nonce 並進行動態加密
encrypted = SecureRFIDCipher.process(self.tag["id"], self.system_key, self.current_nonce)
self.root.after(0, lambda: self.canvas.itemconfig(self.tag_rect, fill="#f1c40f"))
self.root.after(0, lambda: self.canvas.itemconfig(self.lbl_flow, text=f"Tag 回傳密文: {encrypted}", fill="orange"))
time.sleep(1.2)
# 3. Reader 驗證
decrypted = SecureRFIDCipher.process(encrypted, self.system_key, self.current_nonce)
is_valid = (decrypted.replace(" ","") == self.tag["id"].replace(" ",""))
res_text = "驗證成功 (合法標籤)" if is_valid else "驗證失敗 (無效密文)"
res_color = "#2ecc71" if is_valid else "#e74c3c"
self.root.after(0, lambda: self.canvas.itemconfig(self.tag_rect, fill=res_color))
self.root.after(0, lambda: self.canvas.itemconfig(self.lbl_flow, text=res_text, fill=res_color))
self.root.after(0, lambda: self.tree.insert("", 0, values=(self.current_nonce, encrypted, "YES" if is_valid else "NO")))
time.sleep(2)
self.root.after(0, lambda: self.canvas.itemconfig(self.tag_rect, fill="#ecf0f1"))
self.root.after(0, lambda: self.btn_run.config(state=tk.NORMAL))
if __name__ == "__main__":
root = tk.Tk()
app = ReplayProtectionSim(root)
root.mainloop()

沒有留言:
張貼留言