Unicode Homoglyph Attacks: When Characters Lie About Who They AreUnicodeホモグリフ攻撃: 見た目は同じ、中身は別物
How visually identical characters from different scripts enable phishing and spoofing, and how to detect them.異なるスクリプトの視覚的に同一な文字がフィッシングを可能にする仕組みと検出方法。
The Threat: Characters That Look Alike
Unicode contains over 150,000 characters from hundreds of scripts. Many of these characters are visually identical or nearly identical, despite being completely different code points from different writing systems. These are called homoglyphs.
The most notorious example: Latin a (U+0061) and Cyrillic а (U+0430). They are pixel-perfect identical in most fonts, but they are distinct Unicode characters from different scripts.
| Looks like | Latin | Cyrillic | Greek |
|---|---|---|---|
| A | U+0041 A | U+0410 А | U+0391 Α |
| a | U+0061 a | U+0430 а | — |
| B | U+0042 B | U+0412 В | U+0392 Β |
| E | U+0045 E | U+0415 Е | U+0395 Ε |
| o | U+006F o | U+043E о | U+03BF ο |
| p | U+0070 p | U+0440 р | U+03C1 ρ |
| x | U+0078 x | U+0445 х | U+03C7 χ |
This is not a bug — these are legitimately different characters that happen to have converged on the same visual form. But attackers exploit this coincidence.
Multi-Script Homoglyphs in Depth
The problem extends far beyond Latin/Cyrillic. Unicode contains homoglyphs across many script pairs:
| Visual | Scripts involved | Code points |
|---|---|---|
| 1 / l / I | Digit / Latin lower / Latin upper | U+0031, U+006C, U+0049 |
| 0 / O / О | Digit / Latin / Cyrillic | U+0030, U+004F, U+041E |
| н / H | Cyrillic lower / Latin upper | U+043D, U+0048 |
| ν / v | Greek lower / Latin lower | U+03BD, U+0076 |
| ℓ / l | Script small L / Latin L | U+2113, U+006C |
| ⁰ / ° / o | Superscript 0 / Degree / Latin o | U+2070, U+00B0, U+006F |
| ー / — / ─ | Katakana / Em dash / Box drawing | U+30FC, U+2014, U+2500 |
Fullwidth characters add another dimension: A (U+FF21, FULLWIDTH LATIN CAPITAL LETTER A) looks similar to A (U+0041) in some contexts. Mathematical alphanumeric symbols (U+1D400–U+1D7FF) provide yet more lookalikes: 𝐀 (U+1D400, MATHEMATICAL BOLD CAPITAL A).
Real-World Attacks
Homoglyph attacks have caused real damage:
- IDN homograph attacks: Registering domains like
аpple.com(with Cyrillic а) that display identically toapple.comin browser address bars. This led to the development of IDN display policies in browsers. - Source code attacks (Trojan Source): Inserting Bidi override characters or homoglyphs in source code to make malicious logic appear benign during code review. A 2021 Cambridge paper demonstrated how this could inject vulnerabilities invisible to human reviewers.
- Phishing emails: Spoofing sender names or URLs using mixed-script homoglyphs that pass visual inspection.
- Social media impersonation: Creating usernames that look identical to legitimate accounts using Cyrillic or Greek substitutions.
// These strings look identical but are different: const latin = "apple"; // All Latin const mixed = "аpple"; // Cyrillic а + Latin pple latin === mixed // false latin.length === mixed.length // true (both 5) // Byte comparison reveals the difference: latin.codePointAt(0) // 97 (U+0061 Latin Small Letter A) mixed.codePointAt(0) // 1072 (U+0430 Cyrillic Small Letter A)
Detection Methods
Several approaches exist to detect homoglyph attacks:
- Script mixing detection: Flag strings that contain characters from multiple scripts (e.g., Latin mixed with Cyrillic). Unicode TR#39 defines “mixed-script” detection algorithms.
- Confusable detection: Unicode TR#39 also publishes a
confusables.txtfile that maps each character to its “skeleton” — a canonical form for comparison. Two strings with the same skeleton are confusable. - Single-script enforcement: Requiring that identifiers (usernames, domains) use characters from only one script.
- Visual inspection tools: Using tools like this one to reveal the actual code points behind text that looks suspicious.
// Unicode TR#39 confusable skeleton (conceptual):
skeleton("аpple") → "apple" // Cyrillic а maps to Latin a
skeleton("apple") → "apple" // Already Latin
// If skeletons match, strings are confusable:
skeleton("аpple") === skeleton("apple") // true → confusable!
// Script detection:
function getScripts(str) {
return [...new Set(
[...str].map(ch => {
// Use Unicode script property
// (simplified; real impl uses Unicode data)
const cp = ch.codePointAt(0);
if (cp >= 0x0400 && cp <= 0x04FF) return "Cyrillic";
if (cp >= 0x0370 && cp <= 0x03FF) return "Greek";
return "Latin";
})
)];
}Normalization as a Partial Defense
Unicode normalization (especially NFKC) can collapse some homoglyphs but not all:
| Homoglyph pair | NFKC helps? | Reason |
|---|---|---|
| A (U+FF21) vs A (U+0041) | Yes | NFKC maps fullwidth to ASCII |
| ⅰ (U+2170) vs i (U+0069) | Yes | NFKC decomposes Roman numerals |
| а (U+0430) vs a (U+0061) | No | Different scripts, not compatibility equivalents |
| Α (U+0391) vs A (U+0041) | No | Greek and Latin are distinct |
| 𝐀 (U+1D400) vs A (U+0041) | Yes | NFKC maps math alphanumerics |
NFKC normalization is a useful first pass — it eliminates compatibility variants and fullwidth forms. But cross-script homoglyphs (Latin vs Cyrillic vs Greek) survive normalization because they are not compatibility equivalents. They are genuinely different characters that just happen to look the same.
For robust defense, you need both normalization and confusable detection. This tool lets you see exactly which code points are behind any text, making it easy to spot when characters are not what they appear to be.
脅威: 見た目が同じ文字たち
Unicode は数百のスクリプトから15万以上の文字を含んでいます。これらの多くは、完全に異なる書記体系の完全に異なるコードポイントでありながら、視覚的に同一またはほぼ同一です。これらはホモグリフ(同形異字)と呼ばれます。
最も悪名高い例: ラテン文字の a(U+0061)とキリル文字の а(U+0430)。ほとんどのフォントでピクセル単位で完全に同一ですが、異なるスクリプトの別個の Unicode 文字です。
| 見た目 | ラテン | キリル | ギリシャ |
|---|---|---|---|
| A | U+0041 A | U+0410 А | U+0391 Α |
| a | U+0061 a | U+0430 а | — |
| B | U+0042 B | U+0412 В | U+0392 Β |
| E | U+0045 E | U+0415 Е | U+0395 Ε |
| o | U+006F o | U+043E о | U+03BF ο |
| p | U+0070 p | U+0440 р | U+03C1 ρ |
| x | U+0078 x | U+0445 х | U+03C7 χ |
これはバグではありません — たまたま同じ視覚形式に収斂した正当に異なる文字です。しかし攻撃者はこの偶然を悪用します。
多スクリプトホモグリフの詳細
問題はラテン/キリルをはるかに超えて広がります。Unicode は多くのスクリプトペア間にホモグリフを含んでいます:
| 見た目 | 関連スクリプト | コードポイント |
|---|---|---|
| 1 / l / I | 数字 / ラテン小文字 / ラテン大文字 | U+0031, U+006C, U+0049 |
| 0 / O / О | 数字 / ラテン / キリル | U+0030, U+004F, U+041E |
| н / H | キリル小文字 / ラテン大文字 | U+043D, U+0048 |
| ν / v | ギリシャ小文字 / ラテン小文字 | U+03BD, U+0076 |
| ℓ / l | 筆記体小文字 L / ラテン L | U+2113, U+006C |
| ⁰ / ° / o | 上付き0 / 度 / ラテン o | U+2070, U+00B0, U+006F |
| ー / — / ─ | カタカナ / Em ダッシュ / 罫線素片 | U+30FC, U+2014, U+2500 |
全角文字はさらに別の次元を加えます: A(U+FF21、全角ラテン大文字A)は文脈によっては A(U+0041)と類似して見えます。数学用英数字記号(U+1D400〜U+1D7FF)もさらなる類似文字を提供します: 𝐀(U+1D400、数学用太字大文字A)。
実際の攻撃事例
ホモグリフ攻撃は実際に被害を引き起こしてきました:
- IDN ホモグラフ攻撃:
аpple.com(キリル文字のа)のようなドメインを登録し、ブラウザのアドレスバーでapple.comと同一に表示させる。これがブラウザにおける IDN 表示ポリシー策定のきっかけとなった。 - ソースコード攻撃(Trojan Source): ソースコードに Bidi オーバーライド文字やホモグリフを挿入し、悪意あるロジックをコードレビュー時に無害に見せる。2021年のケンブリッジ大学の論文がこの手法で人間のレビュアーに見えない脆弱性を注入できることを実証。
- フィッシングメール: 視覚的な検査をパスする混合スクリプトホモグリフを使って送信者名や URL を偽装。
- SNS のなりすまし: キリル文字やギリシャ文字の置換を使用して正規アカウントと同一に見えるユーザー名を作成。
// これらの文字列は同一に見えるが異なる: const latin = "apple"; // すべてラテン文字 const mixed = "аpple"; // キリル а + ラテン pple latin === mixed // false latin.length === mixed.length // true(どちらも5) // バイト比較で違いが判明: latin.codePointAt(0) // 97 (U+0061 Latin Small Letter A) mixed.codePointAt(0) // 1072 (U+0430 Cyrillic Small Letter A)
検出方法
ホモグリフ攻撃を検出するためのいくつかのアプローチがあります:
- スクリプト混在検出: 複数のスクリプトの文字を含む文字列(例: ラテン文字とキリル文字の混在)をフラグ付け。Unicode TR#39 が「混合スクリプト」検出アルゴリズムを定義。
- 紛らわしい文字の検出: Unicode TR#39 は各文字をその「スケルトン」— 比較用の正規形 — にマッピングする
confusables.txtファイルも公開。同じスケルトンを持つ2つの文字列は紛らわしい。 - 単一スクリプト強制: 識別子(ユーザー名、ドメイン)が1つのスクリプトの文字のみを使用することを要求。
- 視覚的検査ツール: このツールのように、疑わしいテキストの背後にある実際のコードポイントを明らかにする。
// Unicode TR#39 紛らわしい文字スケルトン(概念的):
skeleton("аpple") → "apple" // キリル а がラテン a にマップ
skeleton("apple") → "apple" // 元からラテン
// スケルトンが一致すれば紛らわしい:
skeleton("аpple") === skeleton("apple") // true → 紛らわしい!
// スクリプト検出:
function getScripts(str) {
return [...new Set(
[...str].map(ch => {
const cp = ch.codePointAt(0);
if (cp >= 0x0400 && cp <= 0x04FF) return "Cyrillic";
if (cp >= 0x0370 && cp <= 0x03FF) return "Greek";
return "Latin";
})
)];
}正規化による部分的防御
Unicode 正規化(特に NFKC)は一部のホモグリフを統合できますが、すべてではありません:
| ホモグリフペア | NFKC で解決? | 理由 |
|---|---|---|
| A (U+FF21) vs A (U+0041) | はい | NFKC が全角を ASCII にマップ |
| ⅰ (U+2170) vs i (U+0069) | はい | NFKC がローマ数字を分解 |
| а (U+0430) vs a (U+0061) | いいえ | 異なるスクリプト、互換等価ではない |
| Α (U+0391) vs A (U+0041) | いいえ | ギリシャとラテンは別個 |
| 𝐀 (U+1D400) vs A (U+0041) | はい | NFKC が数学用英数字をマップ |
NFKC 正規化は有用な第一段階です — 互換変種と全角形式を除去します。しかしクロススクリプトのホモグリフ(ラテン vs キリル vs ギリシャ)は互換等価ではないため正規化を生き残ります。これらはたまたま同じ外見を持つ、真に異なる文字です。
堅牢な防御には正規化と紛らわしい文字検出の両方が必要です。このツールはテキストの背後にある正確なコードポイントを表示し、文字が見た目通りでない場合を簡単に発見できます。