大模型同形字符欺骗防御:不可见 Unicode 与零宽字符注入的清洗方案
在网络安全对抗中,字符编码层面的隐写与欺骗是一门历史悠久的传统手艺。而在大语言模型(LLM)与生成式 AI 基础设施普及的今天,这种古老的攻击手法正在自然语言语义边界上焕发全新的破坏力。
很多安全团队为大模型搭建的前置过滤网关,往往直接建立在普通的 UTF-8 字符串正则匹配上。例如,风控策略中明确规定禁止出现某些高危指令或敏感实体词汇。
然而,Unicode 字符集包含超过 14 万个字符,其中不仅存在大量在视觉渲染上与英文字母毫无二致的“同形异义字(Homoglyphs)”,还存在成百上千个在终端或渲染窗口中完全不占任何像素的“零宽字符(Zero-Width Characters)”与不可见控制字符(Invisible Control Characters)。
攻击者只需将几个零宽字符插在敏感提示词中间,或者将英文字母替换为西里尔字母、希腊字母,传统的正则过滤器与基于词表的敏感词库就会瞬间“睁眼瞎”。但与此同时,大模型底层的分词器(Tokenizer)在对输入文本进行切词与字节级解码时,却能依然凭借其庞大的泛化空间还原出真实的恶意意图。
要守住大模型的输入边界,必须在所有业务逻辑生效之前,建立底层的**“Unicode 字符归一化”与“不可见字符深度清洗”管线**。
两大核心欺骗手法拆解
同形字符与零宽字符注入攻击示意: ┌────────────────────────────────────────────────────────┐ │ 1. 零宽字符注入 (Zero-Width Injection): │ │ 人眼/渲染所见: "ignore previous instructions" │ │ 底层真实十六进制: │ │ "i\u200Bgn\u200Core\uFEFF pre\u200Dvious..." │ │ 效果: 正则单词匹配完全断裂失效,但分词器仍能感知语义 │ ├────────────────────────────────────────────────────────┤ │ 2. Unicode 同形异义字欺骗 (Homoglyph Attack): │ │ 拉丁字母 'a' (U+0061) vs 西里尔字母 'а' (U+0430) │ │ 拉丁字母 'p' (U+0070) vs 西里尔字母 'р' (U+0440) │ │ 构造: "рrоmрt" (全采用西里尔字符,视觉 100% 相同) │ │ 效果: 传统 ASCII 黑名单全部绕过 │ └────────────────────────────────────────────────────────┘1. 零宽字符(Zero-Width Characters)的隐写断词
Unicode 中定义了一批专门用于复杂排版(如连接波斯语、阿拉伯语字母)的不可见字符:
U+200B(Zero-Width Space,零宽空格)U+200C(Zero-Width Non-Joiner,零宽非连接符)U+200D(Zero-Width Joiner,零宽连接符)U+FEFF(Byte Order Mark,零宽无中断空格)
攻击者将这些不可见字符随机插入到system、admin、ignore等敏感单词之间。由于这些字符在屏幕上不可见,对于终端用户和肉眼审核员来说,句子看起来毫无破绽。但对于简单的字符串比较函数(如 Python 的in或正则\bignore\b),由于中间插入了字节,匹配彻底落空。
2. 同形异义字(Homoglyphs)的视觉伪造
Unicode 字符集为了覆盖全球所有文明的语言,收录了大量在字形(Glyph)上与拉丁字母极其相似甚至完全重合的字符。例如西里尔字母的а(U+0430)、о(U+043E)、е(U+0435)。攻击者用这些同形字混写提示词,能够以极低成本绕过基于字符串哈希或精确字典的规则库。
生产级防御:Unicode 规范化与深度清洗流水线
防御同形字符与不可见字符欺骗,核心必须依赖国际 Unicode 联盟制定的规范化标准(Normalization Forms),并结合严格的控制字符过滤。
1. Unicode NFKC 兼容等价分解与重组
Unicode 提供了四种标准规范化算法:NFD、NFC、NFKD、NFKC(Normalization Form KC)。
在安全审计中,我们必须强制采用NFKC 算法:
- 它不仅会将组合字符(如带有声调符号的字母)压缩为预合成字符;
- 更关键的是,它会对视觉同形但编码不同的兼容性变体(Compatibility Characters,如全角字符、上标数字、环形字符)执行强行降维映射,还原为其基准 ASCII 字符!
2. 生产级 Python 清洗网关实现
下面是在大模型请求入口处必须部署的字符规范化处理模块:
import unicodedata import re from typing import Tuple class UnicodeSanitizer: def __init__(self): # 常见零宽字符与双向控制字符(Bidi Controls)黑名单正则 # 包括零宽空格、双向覆盖字符(RLO/LRO 用于颠倒文字方向)等 self.invisible_and_bidi_pattern = re.compile( r"[\u200B-\u200D\uFEFF\u200E\u200F\u202A-\u202E\u2060-\u206F]" ) def sanitize_prompt(self, raw_user_prompt: str) -> Tuple[str, bool]: """ 对输入文本进行彻底的字符清洗与同形归一化 返回: (清洗后的标准纯净文本, 是否检测到恶意字符注入) """ tampered_detected = False # 1. 检查并剥离所有零宽不可见字符与双向文本控制符 if self.invisible_and_bidi_pattern.search(raw_user_prompt): tampered_detected = True clean_text = self.invisible_and_bidi_pattern.sub("", raw_user_prompt) else: clean_text = raw_user_prompt # 2. 执行 Unicode NFKC 兼容性规范化 # 这一步会将全角字符、特殊变体字符强制折叠为标准通用 ASCII/Unicode normalized_text = unicodedata.normalize("NFKC", clean_text) # 3. 过滤掉所有无法打印的 C 类别(Other / Control)字符,仅保留换行与制表符 sanitized_chars = [] for char in normalized_text: category = unicodedata.category(char) # Cc: 控制字符 (Control), Cf: 格式字符 (Format), Cs: 代理区 (Surrogate), Co: 私用区 (Private Use) if category.startswith("C"): if char in ["\n", "\r", "\t"]: sanitized_chars.append(char) else: tampered_detected = True # 丢弃异常控制字符 else: sanitized_chars.append(char) final_prompt = "".join(sanitized_chars) return final_prompt, tampered_detected生产落地的三项安全红线
- 清洗操作必须位于所有前置护栏的最顶端:任何敏感词过滤、意图分类器、AST 语法分析器、RAG 向量检索模型,其接收到的输入文本必须是已经通过
UnicodeSanitizer彻底清洗过的纯净文本。如果把字符清洗放在规则匹配之后,整套防御体系等于在第一道关卡前就缴械投降。 - 严防“双向覆盖字符(Bidi Attack)”破坏代码审查:Unicode 包含专门用于阿拉伯语排版的双向文本字符(如
\u202ERLO,强制从右向左渲染)。攻击者在 Prompt 中注入 RLO 字符,可以让屏幕上看起来是access_granted = False的代码,在物理执行层面其实是access_granted = True。对任何输入中出现的 Bidi 控制字符,必须执行一刀切的直接抹除。 - 记录恶意字符注入的安全风控埋点:如果一个普通用户的正常业务咨询中频繁出现
\u200B零宽空格,这绝非偶然,必然是黑客利用自动化脚本进行的对抗性探测。网关层检测到此类注入后,应当对该会话的 IP 与用户 UID 进行高风险标记,并在风控侧提升其挑战门槛。
安全攻防的本质,是比拼谁能对更底层的协议与编码标准保持透彻的认知。把字符流的裂隙彻底焊死,大模型防御体系才能在形形色色的文字迷彩面前立于不败之地。