前兩天的攻擊目標是「讓個資漏出去」。今天的目標不同:個資已經被正確 Token 化了,攻擊者要把它還原回來。
最直接的方法是偷 Vault。但 D12 已經上了五道防護,直接偷不容易。所以今天談的是不需要直接存取 Vault 的還原路徑。
如果 Token 化是全域確定性的(D16 的層級三),那 Token 的出現頻率會保留原值的分布。
中文姓氏的分布高度不均——陳、林、黃、張、李佔了很大比例。如果攻擊者拿到一批去識別化文件,統計假名的姓氏分布,最高頻的那個假姓氏很可能對應到「陳」。
這不能直接還原個人,但它縮小了搜尋空間。結合其他資訊(例如已知某份文件的當事人是誰),可以逐步建立對應表。
防禦:scoped 一致性(D18)。跨案件不可關聯,就沒有足夠的樣本做頻率分析。單一案件內的樣本量太小,統計不出東西。
攻擊者知道某份文件的原文(例如他自己就是當事人,或他拿得到公開的判決書),也拿得到對應的去識別化版本。
比對兩者,他就得到了一批確定的 (原值, Token) 對應。
如果 Token 化用的是 FPE,這批對應能不能推出金鑰?理論上不行——FF1 的安全性基於底層 AES,已知明文攻擊在計算上不可行。
但如果用的是自己實作的弱方案(例如簡單的查表、或者沒加鹽的 hash),已知明文攻擊可以直接建立字典。
防禦:用標準演算法,不要自己發明。這是密碼學的基本紀律。
另外,D13 那個 HMAC 假名函式要注意:如果 key 洩漏,攻擊者可以離線窮舉所有常見中文姓名,建立完整對應表。所以那把金鑰要跟 FPE 金鑰同等保護。
這個比較有意思。攻擊者不碰 Vault,而是利用系統本身的功能。
假設攻擊者有一般使用者權限(可以上傳文件、可以用 LLM),他可以:
這是一個**確認預言機(confirmation oracle)**攻擊。它不需要還原權限,只需要能觀察 Token。
# 攻擊者的流程
for candidate in common_names:
doc = f"立約人{candidate},特此聲明。"
result = upload_and_deidentify(doc, scope_id=TARGET_SCOPE)
if extract_token(result) == TARGET_TOKEN:
print(f"確認:{candidate}")
break
關鍵前提是攻擊者能指定 scope_id。如果他能把自己的文件放進目標案件的 scope,確定性一致就變成了攻擊工具。
防禦:
一、scope 的寫入權限要控制。 使用者不能任意指定 scope_id,必須是他有權限的案件。
def resolve_scope(user, requested_scope):
if not user_has_case_access(user, requested_scope):
raise PermissionDenied("無此案件存取權")
return requested_scope
二、對同一 scope 的高頻上傳告警。 上面那個 for 迴圈會產生大量上傳事件,這是可偵測的。
三、考慮加入每次不同的隨機成分。 但這會破壞一致性(D16),所以不適用於需要跨文件比對的場景。取捨。
Vault 查詢的耗時可能洩漏資訊。
攻擊者透過測量回應時間,可以判斷某個 Token 是否存在於 Vault,進而推論某個實體是否出現在某個案件裡。
防禦:常數時間回應。
import time
MIN_RESPONSE_TIME = 0.05 # 秒
def resolve_with_constant_time(token, scope_id):
start = time.perf_counter()
try:
result = vault.lookup(token, scope_id)
except NotFound:
result = None
elapsed = time.perf_counter() - start
if elapsed < MIN_RESPONSE_TIME:
time.sleep(MIN_RESPONSE_TIME - elapsed)
return result
這個防禦在實務上的優先級不高(要利用它需要大量精確測量),但在高敏感場景值得做。
D3 提過這一項,這裡展開。
攻擊者是一個有合法還原權限的內部人員。他不需要繞過任何控制,只需要用他的權限做超出業務需要的還原。
這是最難防的攻擊,因為每一次操作都是合法的。
防禦是偵測而非阻擋:
def check_reidentify_anomaly(actor, token_count):
baseline = get_user_baseline(actor) # 過去 30 天的中位數
signals = []
if token_count > baseline["daily_median"] * 5:
signals.append("單次量異常")
if get_today_total(actor) > baseline["daily_p95"] * 2:
signals.append("單日累計異常")
if is_outside_business_hours():
signals.append("非上班時間")
if get_distinct_scopes_today(actor) > baseline["scope_p95"]:
signals.append("跨案件數異常")
if len(signals) >= 2:
alert_security(actor, signals)
return "REQUIRE_APPROVAL"
return "ALLOW"
兩個以上訊號才升級,是為了降低誤報。單純加班到晚上八點不該被當成攻擊。
這不是技術攻擊,是流程漏洞,但它是實務上最常見的實際外洩路徑:
防禦是流程:備份權限與本體同級、測試環境一律用合成資料、禁止 dump 到端點裝置、離職流程納入權限清查。
這些聽起來很基本,但 D12 那五道防護做得再好,都擋不住一份放在共享資料夾的備份。
紅隊測試時可以照這個順序走:
| # | 攻擊 | 需要什麼權限 | 防禦重點 |
|---|---|---|---|
| 1 | 頻率分析 | 一批去識別文件 | scope 隔離 |
| 2 | 已知明文 | 一組原文+Token | 標準密碼學 |
| 3 | 確認預言機 | 一般使用者 | scope 寫入控制 |
| 4 | 時序側信道 | 能呼叫還原 API | 常數時間 |
| 5 | 批次濫用 | 還原權限 | 異常偵測 |
| 6 | 備份/測試環境 | 側門 | 流程與權限清查 |
注意第 3 和第 5 只需要合法權限。這是這個架構最真實的風險——不是外部駭客,是內部的過度權限。
明天把「一犯就出局」的失敗模式定義清楚。
我是 Fngi,專注在 AI 資安、LLM 紅隊與 AI 治理框架落地。
IG:@aid3fend
有想討論的架構細節或不同意見,留言或私訊都歡迎。