フィッシングが挟む「見えない文字」

グループウェアのメール機能で、迷惑メールの振り分けを「語句を含む」条件で運用している。カード会社名などを条件に登録してあったが、最近のフィッシングはこの条件をすり抜けて受信箱に届いていた。原因は、標題と差出人の文字のあいだに挟まれた見えない文字である。対策として、普通のメールには入らない文字そのものを振り分けの条件にした。

2,318通
検証に使った
直近のメール
188通
見えない文字を
含んでいたメール
185通
追加した15条件で
拾えたメール
0通
誤判定
(普通のメールの巻き込み)

語句の条件をすり抜けるフィッシング

このグループウェアでは、個人設定のメール振り分けに「迷惑メール」という振り分け保存があり、条件に合うメールを保存先の[ごみ箱]へ移す。条件は「標題」または「差出人」が指定の語句「を含む」かどうかで、設定画面で1行ずつ登録する。詐欺に名前を使われやすいカード会社や行政機関の名称を、この形で登録していた。

すり抜けたフィッシングは、画面上では登録済みの名称とまったく同じに見える。それでも振り分けられず、受信箱に残っていた。

原因 — 文字のあいだに挟まる「見えない文字」

すり抜けたメールの標題と差出人を1文字ずつコードポイントに分解すると、名称の文字のあいだに、幅を持たない文字や、文字の並ぶ向きを変える制御文字が挟まっていた。画面には何も表示されないが、文字列としては別のものになる。語句の条件は登録した文字列が連続して現れるかを見るため、あいだに1文字でも挟まれば一致しない。

// 例1: 幅を持たない文字を挟む(〇〇カードは架空の名称)
画面の表示     〇〇カード
実際の文字列   〇 〇 [U+200B] カ [U+200C] ー [U+200D] ド
語句「〇〇カード」を含む → 一致しない

// 例2: 向きを変える制御文字を前に置く
実際の文字列   [U+202E] 国 税 庁
画面の表示     庁税国
語句「国税庁」を含む   → 一致しない
図1 見え方と文字列の違い。[U+…] は画面に何も出ない文字を、説明のために書き出したもの。

実際のメールで確認できた文字は次のとおり。どれも本来は組版や多言語表記のための正規の文字で、メールの標題や差出人に入れる必要はない。

コードポイント名前本来の用途画面での見え方
U+200Bゼロ幅スペース改行してよい位置を示す何も見えない
U+200Cゼロ幅非接合子つながって書かれる文字を切り離す何も見えない
U+200Dゼロ幅接合子文字や絵文字をつないで1つにする何も見えない
U+2060単語結合子その位置で改行させない何も見えない
U+FEFFBOM(ゼロ幅ノーブレークスペース)ファイル先頭でバイト順を示す何も見えない
U+202E右から左への上書き(RLO)以降の文字を右から左へ並べる以降の文字が逆順に表示される
U+200E左から右マーク(LRM)文字の向きを判定する手がかり何も見えない
U+2061関数適用数式の意味づけ(不可視の演算子)何も見えない
U+2063不可視の区切り数式の意味づけ(不可視の演算子)何も見えない

U+202E だけは見え方が変わる。この文字より後ろが右から左に並ぶため、「国税庁」を含む差出人が「庁税国」と表示されたメールがあった。読み手には不自然に見えるが、文字列としてはやはり「国税庁」と一致しないので、語句の条件には掛からない。

対策1 — 見えない文字そのものを条件にする

詐欺の名乗りは手口ごとに変わる。一方で、見えない文字は名乗りが変わっても共通して使われている。そして、日本語の業務メールの標題や差出人にこれらの文字が入ることは通常ない。そこで、条件の語句に見えない文字を1文字だけ入れ、「その文字を含む」メールをごみ箱へ移すことにした。

普通のメールには入らない文字を条件にする。

登録したのは次の15条件。表の主要6種を標題・差出人の両方に置き、6種以外で実際のメールに出ていた3文字を、出ていた欄に足した。

欄含む文字件数
標題U+200B U+200C U+200D U+2060 U+FEFF U+202E6
差出人U+200B U+200C U+200D U+2060 U+FEFF U+202E6
追加差出人 U+200E/標題 U+2061/標題 U+20633
計15

登録の前に、直近のメール2,318通にこの15条件を当てた。見えない文字を含んでいたのは188通で、15条件で拾えるのは185通。普通のメールが条件に掛かるものは0通だった。振り分け先はごみ箱で、削除ではないため、万一普通のメールが入っても戻せる。

登録の方法 — 手で打てない文字をブラウザ自動操作で入れる

条件を決めても、設定画面の入力欄に見えない文字は手で打てない。キーボードに無い文字であり、コピー&ペーストで入れようとしても、入ったかどうかを画面で確かめられない。15件を手作業で正しく登録するのは事実上できない。

そこで、ブラウザ自動操作ライブラリの Playwright を使い、AIエージェントが書いたスクリプトで入力欄に文字を流し込んだ。手順は次のとおり。

  1. 人がログインする。自動操作用の専用 Chrome を起動し、人がグループウェアにログインする。ID・パスワードは AI に渡さない。
  2. ログイン済みのブラウザに接続する。スクリプトは Chrome DevTools Protocol(CDP)でこのブラウザに接続し、ログイン済みの画面をそのまま使う。
  3. 登録済みの条件を読む。設定画面を全ページ読み、既にある条件を取得する。既存の条件には触らず、未登録のものだけを追加の対象にする。
  4. 入力して読み戻す。[条件を増やす]で行を足し、「標題/差出人」・文字・「を含む」を設定する。入力直後に入力欄の値を読み戻し、狙った1文字と一致するかを確かめる。
  5. 保存して照合する。保存先が[ごみ箱]であることを確かめてから保存し、設定画面を読み直して、追加した条件が1件ずつ登録されているかを照合する。
# 処理の骨格(簡略化)。見えない文字はエスケープで書くと、ソース上でもどの文字か分かる
CHARS = ["\u200b", "\u200c", "\u200d", "\u2060", "\ufeff", "\u202e"]
RULES = [("標題", c) for c in CHARS] + [("差出人", c) for c in CHARS]

browser = p.chromium.connect_over_cdp(CDP_ENDPOINT)   # 人がログイン済みの Chrome
want = [r for r in RULES if r not in registered(page)]   # 未登録だけ

for field, ch in want:
    box = add_row(page, field)
    box.fill(ch)
    assert box.input_value() == ch   # 入力欄に入ったかを読み戻す

save(page)
assert set(want) <= registered(page)   # 保存後に設定画面を読み直して照合
registered()・add_row()・save() は画面ごとの操作をまとめた関数。中身は製品の画面構成に依存するので省略している。

このグループウェアの入力欄は、見えない文字を消さずに保存した。入力欄の側で不可視文字を取り除く製品では、この方法は使えない。保存後に設定画面を読み直しているのは、この点を確かめるためでもある。条件の数は、既存の語句条件と合わせて57件になった。

効果確認 — 設定から2日後

設定から2日後に、設定後に届いたメールを集計した。全16フォルダのメール一覧を読み、標題と差出人に見えない文字を含むメールがどのフォルダにあるかを数えた。メールは開いていない。開くと既読になるため、一覧の表示だけを使っている。

項目件数
設定後に届いた、見えない文字を含むメール6通
うち、ごみ箱に振り分けられたもの6通
取りこぼし(受信箱などに残ったもの)0通
誤判定(普通のメールがごみ箱に入ったもの)0通

この数え方には限界がある。一覧表示では標題・差出人が途中で切れるため、切れた後ろにある文字は数えられない。ただし、登録前の検証も同じ一覧画面で行っており、前後の比べ方は揃っている。

対策2 — 契約のない会社の名前を条件にする

見えない文字を使わない詐欺は、この対策の後も届いた。名乗っていたのは、ネット証券、クレジットカード会社、レンタルサーバ事業者の3社である。

3社とも当社と契約がないことを、人が確認した。契約がなければ、その会社から本物の通知が届くことはない。したがって、契約のない会社の名前が標題や差出人に出てくるメールは、すべて詐欺として扱える。この前提に立ち、3社の社名を語句の条件に足した。

  • 表記違いも登録する。振り分けの語句条件が英字の大文字・小文字を区別するかどうかが画面から分からないため、英字・カタカナの表記や大文字・小文字の違いを別々の条件にした。3社で11件。
  • 登録前に過去のメールに当てる。過去約2,400通に11件を当て、該当するのが3社名義のメールだけで、普通のメールの巻き込みが0通であることを確かめてから登録した。登録の手順は対策1と同じで、条件の数は68件になった。

社名と登録した文字列の一覧は、この記事には載せない。公開すると攻撃側に表記を変えて回避されやすくなり、取引の有無という社内の情報にもなるためである。

役割分担

担当内容
人どの会社と契約がないかの確認。取引の有無は社内の事実で、メールを眺めても分からない
人設定を書き換えてよいかの判断。設定変更は自動承認の対象にしておらず、人の「登録して」の指示で実行した
AI見えない文字の測定(どの文字が・どの欄に・何通)
AI条件の候補作成と、過去のメールに当てた誤判定の検証
AI設定画面への登録と、保存後の照合

1週間後の再確認は、グループウェアの ToDo リストに期限付きで登録してある。

運用上の注意

  • 一覧表示は途中で切れる。効果確認を一覧画面で行う限り、標題・差出人の後ろのほうにある見えない文字は数えられない。件数は「一覧で見える範囲での数」として扱う。
  • 契約したら条件を外す。対策2の条件は「契約がない」ことが前提である。将来その会社と契約した場合、条件を外さないと本物の通知がごみ箱に入る。
  • 語句より先に文字を見直す。新しい詐欺がすり抜けたら、まず標題・差出人に未登録の見えない文字が入っていないかを調べる。文字の条件は名乗りが変わっても効くが、語句の条件は名乗りが変わるたびに足す必要がある。
  • 振り分け先はごみ箱にしておく。誤判定があっても戻せるよう、削除ではなくごみ箱へ移す設定にしている。
タイトルとURLをコピーしました