2026年1月16日 星期五

具備 Nonce 防側錄功能的 RFID 模擬器

 具備 Nonce 防側錄功能的 RFID 模擬器


如果駭客錄下了標籤發送的「加密後字串」,下次再重播(Replay)給讀取器,讀取器依然會認為是合法的標籤。

為了防止這種 重播攻擊 (Replay Attack),我們引入 Nonce (隨機數) 機制:

  1. 讀取器發送挑戰 (Challenge):讀取器先發送一個隨機數 $R$ 給標籤。

  2. 動態加密:標籤將 UID 與這個 $R$ 結合後再進行加密。

  3. 一次性密文:因為每次的 $R$ 都不同,所以即便 UID 相同,每次傳輸的加密字串都會改變。

具備 Nonce 防側錄功能的 RFID 模擬器




防重播攻擊的安全性分析

  1. 為什麼舊的密文失效了? 在表格中你可以觀察到,雖然我們只有一個標籤(ID 永遠是 53 71 37 A4 F6),但每一輪產生的 Cipher 都是完全不同的。

    • 如果駭客側錄了第一輪的密文 A1 B2...

    • 在第二輪時,Reader 發出了新的 Nonce

    • 駭客重播 A1 B2...,Reader 使用新的 Nonce 去解密,結果會變成一串亂碼,驗證就會失敗。

  2. 雙向認證的基礎: 這是現代 RFID(如悠遊卡 Mifare DESFire)的核心。透過這種「挑戰-應答 (Challenge-Response)」模式,確保通訊內容具有 時效性 (Freshness)

  3. 實務觀察

    • 黃色閃爍:標籤正在計算基於當前 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()

沒有留言:

張貼留言

ASK 調變模擬器程式碼

ASK 調變模擬器程式碼 為什麼第二張圖要「放大 (Zoom)」? 1 kHz  的週期是 1000 us 。 如果不放大,第二張圖會因為波形太密而看起來像一塊實心的紅磚。 import tkinter as tk from tkinter import ttk import n...