Unicode 同形字钓鱼:看起来一样的域名和账号

· 约 4 分钟 🔣 Unicode 编解码

“怎么就被钓鱼了?链接看着是 аррӏе.com 啊。“——这正是问题:看着是,码位完全不同。Unicode 能表示 14 万+ 字符,视觉上相似的不少,真要骗你的眼睛易如反掌。

几个经典同形组

拿肉眼最易骗过去的几组:

视觉拉丁西里尔希腊其他
aU+0061а U+0430α U+03B1 (近似)𝑎 U+1D44E
eU+0065е U+0435𝐞 U+1D41E
oU+006Fо U+043Eο U+03BF
pU+0070р U+0440ρ U+03C1
cU+0063с U+0441
xU+0078х U+0445χ U+03C7
iU+0069і U+0456ι U+03B9

所以 apple.comаpple.com(首字母西里尔 а)在常见字体下肉眼难分。

数字和符号也有同形

  • 0 vs O vs (U+3007)vs Ο 希腊大写
  • 1 vs l vs I vs vs
  • - vs 连字符 vs 破折号 vs 全角
  • . vs (句号)vs (全角)vs ·(中点)

用户名、密码、商品 SKU 里混进这些非常难查。

IDN 和 Punycode

国际化域名(IDN)允许非 ASCII 字符,但底层 DNS 只认 ASCII——所以会被转码为 xn-- 开头的 Punycode

аррӏе.com           (全西里尔字母)
xn--80ak6aa92e.com   (Punycode 真面目)

浏览器的安全策略:

  • 纯单一脚本(比如全中文 苹果.com):直接显示原文
  • 脚本混合 / 全字符都能被 ASCII 替代:显示 punycode

2017 年 Xudong Zheng 注册了 xn--80ak6aa92e.com 在当时的 Chrome 上完整显示 apple.com——这个漏洞后来被修复但类似攻击变种不断。

NFC / NFD:看起来一样,字节却不同

汉字 / 带音标拉丁字母可能有两种表示法:

  • 组合形式(NFC):é 是 1 个码位 U+00E9
  • 分解形式(NFD):é = e (U+0065) + ´组合音标 (U+0301),2 个码位

字符串比较会失败:

"café" === "café"    // 可能返回 false

"café".normalize("NFC") === "café" // true

规则:存数据库前统一 NFC。macOS 文件系统存 NFD、Windows 存 NFC,跨平台文件名比对前必须先归一化。

NFKC:更激进的归一化

NFC 保留”兼容字符”区分(比如 ① 和 1),NFKC 把这些也合并:

① → 1
ABC → ABC        (全角变半角)
㎏ → kg             (兼容字符分解)
カタカナ → カタカナ     (半角片假名变全角)

NFKC 适合:搜索、同形字检测、账号查重。 NFKC 不适合:原始文本存储——会丢失信息。

零宽字符:看不见的隐写

几个不可见但占字节的字符:

码位名称常见用途
U+200BZERO WIDTH SPACE文本水印、绕过过滤
U+200CZERO WIDTH NON-JOINER阿拉伯语书写
U+200DZERO WIDTH JOINEREmoji 拼合(family emoji)
U+FEFFBYTE ORDER MARK文件开头编码标记
U+2060WORD JOINER不换行
U+180EMONGOLIAN VOWEL SEPARATOR过去曾是空白,现归类格式字符

钓鱼场景:邮件里 PayPal.com 中间插一个 U+200B,用户看到完全正常,但这是一个”从未被注册”的唯一字符串,反钓鱼系统按字符串查不到——通过后再引到真域名。

水印场景:给不同员工的内部文档插入不同排列的零宽字符,泄露时按字节指纹锁定来源。

防御方案

按场景分级:

用户名 / 邮箱 / 域名

  1. 拒绝非预期脚本(域名只允许 ASCII + 中文,不允许西里尔+拉丁混合)
  2. 登录时输入做 NFKC 归一化,拒绝含格式字符
  3. 注册时”相似名拒绝”:新用户名 NFKC 后与已有冲突则拒

富文本内容

  1. 允许 Unicode 全量,但显式剥离零宽字符
  2. 对外展示前再做一次 NFC 归一
  3. 敏感区域(标题、用户名)加”含非预期字符”提示

代码标识符

  1. 变量名、函数名、API key 强制 ASCII
  2. 禁用 BiDi 覆盖字符(U+202E 等)——2021 年的 Trojan Source 攻击利用这个在代码里”显示”一份、“执行”另一份

快速体检一段文本

把可疑字符串贴进工具,会得到每个码位:

а   U+0430   CYRILLIC SMALL LETTER A
p   U+0070   LATIN SMALL LETTER P
p   U+0070   LATIN SMALL LETTER P
1   U+006C   LATIN SMALL LETTER L    ← 实际是 l,不是数字 1
e   U+0065   LATIN SMALL LETTER E
.   U+002E   FULL STOP
c   U+0063   LATIN SMALL LETTER C
o   U+006F   LATIN SMALL LETTER O
m   U+006D   LATIN SMALL LETTER M

西里尔首字母 + 拉丁字母 l 冒充 1——一眼揪出来。

现成工具

粘文本,列出每个字符的码位、名称、所属脚本、是否可打印;一键做 NFC / NFD / NFKC / NFKD 归一化;支持 \u / &#x; / U+ / UTF-8 八种格式互转——验证同形字、处理零宽字符、调试编码问题一次搞定。

❓ 常见问题

浏览器地址栏显示 apple.com 还会是假的吗?

可能是。"a"可能是西里尔字母 а(U+0430),"o"可能是西里尔字母 о(U+043E)——视觉上完全相同。Chrome / Firefox 现在对"全字符同形的 IDN"会显示 punycode(xn-- 开头),但混合字符或罕见脚本仍可能穿透。要确认可把域名粘到工具解码看码位。

我看屏幕上是"a",复制到程序里也是 a,为什么匹配不上?

肉眼看一样码位可以完全不同。希腊小写 α(U+03B1)、西里尔 а(U+0430)、数学斜体 𝑎(U+1D44E)显示都像 "a"。要比较字符串含义请先用 NFKC 规范化再比较,或把不可信输入限制到 ASCII / 中日韩常用区间。

NFC、NFD、NFKC、NFKD 怎么选?

存储和传输一律用 NFC(组合形式),紧凑且兼容度最高。需要按字符拆分(比如逐字统计)用 NFD。需要"视觉等价归一"(防同形字、防全角半角)用 NFKC——但它会把 ① 变 1、丨 变 I,不可逆,不要用在需要保留原始字符的场景。

为什么复制的字符粘过去多了一个"空"的字符?

多半是零宽字符——U+200B 零宽空格、U+200C / U+200D 零宽连接符、U+FEFF BOM。它们不可见但占字节,常用于"隐藏水印"或绕过过滤。防御方法:输入做字符类白名单,拒绝 \p{Cf}(格式字符)和非预期脚本。

🔣 打开 Unicode 编解码 \u/&#x;/U+/UTF-8 八种格式互转·字符检视·名称查询·NFC/NFD