Unicodeホモグリフ攻撃: 見た目は同じ、中身は別物
異なるスクリプトの視覚的に同一な文字がフィッシングを可能にする仕組みと検出方法。
脅威: 見た目が同じ文字たち
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 ギリシャ)は互換等価ではないため正規化を生き残ります。これらはたまたま同じ外見を持つ、真に異なる文字です。
堅牢な防御には正規化と紛らわしい文字検出の両方が必要です。このツールはテキストの背後にある正確なコードポイントを表示し、文字が見た目通りでない場合を簡単に発見できます。