0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

LLM を使ったソーシャルエンジニアリングに組織で備える:公開情報で答えられる本人確認をやめ、ヘルプデスクの手順で止める

0
Posted at

攻撃者が LLM を使うと、標的の下調べと、相手に合わせた口実づくりが安く速くなります。部署、上長の名前、メールアドレス、経歴、誕生日といった公開情報をまとめたプロフィールは、もう手間のかかる成果物ではありません。

一方で、ソーシャルエンジニアリングが最後に求める操作はあまり変わっていません。ヘルプデスクにパスワードや MFA をリセットさせる、リモート支援ツールで画面を操作させる、認証情報を入力させる、の 3 つが中心です。この記事では、LLM で何が変わったかを公開された脅威レポートで確かめたうえで、「公開情報で答えられることを本人確認に使わない」ヘルプデスクの手順を小さな Python のコードで確かめます。

メール文面の検知は LLM で作られたフィッシングメールに備える で、映像・音声のなりすましによる送金依頼は 音声・映像ディープフェイクによる CEO 詐欺への備え で扱っています。この記事はその間にある「調査」と「アカウントの回復」を扱います。

LLM で何が変わったか:調査と口実づくりの手間が減った

Google Threat Intelligence Group(GTIG)が 2026 年 2 月に公開した AI Threat Tracker は、2025 年後半に観測した攻撃者の生成 AI の使い方をまとめています。ソーシャルエンジニアリングに関係する観察は次のとおりです。

  • 標的の調査。 公開情報をすばやくまとめて重要人物を割り出し、組織の階層を把握する使い方が観測されています。北朝鮮との関連が指摘される UNC2970 は、採用担当者になりすます活動のために、防衛・セキュリティ企業の職種や給与水準を調べていました
  • 口実づくり。 イランとの関連が指摘される APT42 は、標的の経歴を与えて、接触のきっかけになる人物像や筋書きを作らせていました。公式のメールアドレスや取引先の調査にも使っています
  • 関係づくり。 悪意のあるものを送る前に、何往復も自然な会話を続けて信頼を得る使い方(GTIG は rapport-building phishing と呼んでいます)が増えているとされています

Anthropic が 2026 年 9 月に公開した脅威インテリジェンスレポートにも、国家の関与が疑われる攻撃者が公開情報を集めてフィッシングの標的リストを作っていた事例が載っています。

ただし GTIG は、これらを攻撃の様相を根本から変える能力ではなく、生産性の向上と位置付けています。つまり、手口は同じで、調査と準備にかかる時間が短くなったということです。

変わらないもの:攻撃が最後に求める操作

CISA と FBI の Scattered Spider に関する注意喚起(AA23-320A、最終更新 2025 年 7 月)は、ソーシャルエンジニアリングの流れを具体的に書いています。

  • 企業のサイトや SNS、商用の調査ツール、漏えいデータで、標的の役職や権限の高い利用者を調べる
  • ヘルプデスクの手順を探るために何度か電話をかけ、そのうえで社員になりすましてパスワードのリセットや MFA の別端末への移し替えを頼む
  • 逆にヘルプデスクを名乗って社員に電話や SMS で連絡し、認証情報を聞き出したりリモート支援ツールを入れさせたりする

Microsoft も 2026 年 4 月と 9 月に、外部テナントの Teams からヘルプデスクを名乗って連絡し、Quick Assist などのリモート支援で端末を操作させる侵入を報告しています。どちらの報告も、攻撃者が AI を使ったとは書いていません。

ここから言えるのは、LLM が強くするのは「それらしく名乗る」部分であり、守る側が止めるべき地点は、リセット・リモート操作・認証情報の入力という変わらない操作の手前だということです。英国 NCSC も 2025 年の小売業への攻撃を受けて、ヘルプデスクがパスワードをリセットする前の本人確認、特に権限の高いアカウントの手順を見直すよう求めています。

本人確認に公開情報を使わない

多くのヘルプデスクは、氏名、所属、上長の名前、社員番号、生年月日などを聞いて本人確認をしています。これらは、LLM がまとめるプロフィールにそのまま入る情報です。公開を減らす努力はできますが、組織図や登壇資料や SNS を完全に消すことは現実的ではありません。そこで、公開されうる情報は本人確認の材料に数えないと決めてしまうほうが確実です。

NIST SP 800-63B-4(2025 年 8 月に SP 800-63-4 の一部として公開)は、パスワードの設定時に秘密の質問のような知識ベースの認証を使わせることを禁じています。アカウント回復の方法としては、保存済みの回復コード、登録済みの宛先への回復コードの送付、あらかじめ指定した回復の協力者、本人確認のやり直しの 4 種類を挙げており、知識ベースの確認は含まれていません。また、回復したら登録済みの連絡先に通知することを求めています。

社内のヘルプデスクに当てはめると、確認を次のように分けられます。

確認 攻撃者が用意できるか 扱い
氏名・所属・上長・生年月日などへの回答 できる(公開情報・漏えいデータ) 数えない
着信番号・チャットの表示名 できる(偽装・外部テナント) 数えない
人事システムに登録された番号への折り返し 難しい(番号の変更直後を除く) 数える
上長本人が社内のワークフローで承認 難しい(上長のアカウントは別途 MFA で守る) 数える
対面で身分証を確認 難しい 権限の高いアカウントで数える

コードで確かめる:公開情報だけでどこまで通るか

前半では、LLM が公開情報から作ったと想定したプロフィールで、いまの質問にいくつ答えられるかを数えます。後半では、上の表の考え方でリセット依頼を判定します。

LLM は呼び出していません。プロフィールは LLM の出力を想定した固定値で与えています。人物・アドレス・番号はすべて架空です。実行環境は Python 3.13(標準ライブラリのみ)です。

from dataclasses import dataclass, field
from datetime import datetime, timedelta

# --- 1. 公開情報から作られたプロフィール(LLM が公開情報を集めて作った想定の固定値)---
OSINT_PROFILE = {
    "name": "山田 花子",
    "department": "経理部",                 # 会社の登壇資料・SNS の肩書き
    "manager": "佐藤 一郎",                 # 組織図の載った採用ページ
    "email": "hanako.yamada@example.co.jp", # メールアドレスの命名規則から推測
    "hire_year": "2019",                    # SNS の経歴
    "birthday": "04-12",                    # SNS の誕生日投稿
}

# ヘルプデスクがいま使っている本人確認の質問(答えを照合するキー)
CURRENT_QUESTIONS = {
    "氏名": "name",
    "所属部署": "department",
    "上長の名前": "manager",
    "メールアドレス": "email",
    "入社年": "hire_year",
    "生年月日": "birthday",
    "社員番号": "employee_id",
}

def exposure_report(questions, profile):
    """本人確認の質問のうち、公開情報だけで答えられるものを数える。"""
    exposed = [q for q, key in questions.items() if key in profile]
    return exposed, len(exposed) / len(questions)

def old_policy(answers_correct: int) -> str:
    """よくある運用:質問に 3 つ以上正しく答えたらリセットする。"""
    return "リセット実行" if answers_correct >= 3 else "拒否"

# --- 2. 公開情報を判定に使わないリセット手順 ---
# 社内の正規の連絡先(人事システムから作る。電話の着信番号や名乗った番号は使わない)
DIRECTORY = {
    "hanako.yamada@example.co.jp": {"phone": "+81-3-0000-1001", "phone_changed": datetime(2024, 4, 1),
                                    "privileged": False, "manager": "ichiro.sato@example.co.jp"},
    "admin.kato@example.co.jp": {"phone": "+81-3-0000-1002", "phone_changed": datetime(2023, 10, 1),
                                 "privileged": True, "manager": "cio@example.co.jp"},
    "taro.suzuki@example.co.jp": {"phone": "+81-3-0000-1003", "phone_changed": datetime(2026, 10, 24),
                                  "privileged": False, "manager": "ichiro.sato@example.co.jp"},
}
# 攻撃者が用意できない確認だけを数える
INDEPENDENT = {"callback_directory", "manager_approval_workflow", "in_person_id"}
# 数えないもの:公開情報で答えられる知識、攻撃者が用意できる経路
NOT_COUNTED = {
    "knowledge_answers": "公開情報や漏えいデータで答えられる",
    "caller_id": "着信番号は偽装できる",
    "display_name": "表示名は誰でも名乗れる",
    "external_chat": "外部テナントのチャットは攻撃者が用意できる",
}
RECENT_CHANGE = timedelta(days=14)

@dataclass
class Check:
    kind: str
    by: str = ""

@dataclass
class ResetRequest:
    account: str
    action: str                  # password_reset / mfa_reset
    channel: str                 # phone / external_chat / internal_portal / walk_in
    checks: list = field(default_factory=list)

def required(req: ResetRequest, info: dict):
    """操作とアカウントの種類から、必要な独立した確認を決める。"""
    if info["privileged"] and req.action == "mfa_reset":
        return {"callback_directory", "manager_approval_workflow", "in_person_id"}
    if req.action == "mfa_reset":
        return {"callback_directory", "manager_approval_workflow"}
    return {"callback_directory"}

def decide(req: ResetRequest, now: datetime):
    info = DIRECTORY.get(req.account)
    if info is None:
        return "拒否", ["登録のないアカウント"]
    reasons = []
    if req.channel == "external_chat":
        reasons.append("注意: 外部チャット経由の依頼(ヘルプデスクは外部チャットで受け付けない)")
    for c in req.checks:
        if c.kind in NOT_COUNTED:
            reasons.append(f"数えない: {c.kind}({NOT_COUNTED[c.kind]})")
    done = {c.kind for c in req.checks if c.kind in INDEPENDENT}
    # 折り返し先の番号が最近変わっていたら、折り返しを数えない
    if "callback_directory" in done and now - info["phone_changed"] < RECENT_CHANGE:
        done.discard("callback_directory")
        reasons.append("数えない: callback_directory(登録番号が 14 日以内に変更されている)")
    # 上長の承認は、上長本人が社内ワークフローで行ったものだけ
    for c in req.checks:
        if c.kind == "manager_approval_workflow" and c.by != info["manager"]:
            done.discard("manager_approval_workflow")
            reasons.append(f"数えない: 承認者 {c.by} は登録上の上長ではない")
    missing = required(req, info) - done
    if missing:
        reasons += [f"未実施: {m}" for m in sorted(missing)]
        return "保留", reasons
    reasons.append("実行後、登録済みの全連絡先へ通知する")
    return "実行", reasons

if __name__ == "__main__":
    exposed, ratio = exposure_report(CURRENT_QUESTIONS, OSINT_PROFILE)
    print(f"公開情報で答えられる質問: {len(exposed)}/{len(CURRENT_QUESTIONS)} ({ratio:.0%}) {exposed}")
    print("従来の手順で攻撃者が答えた場合:", old_policy(len(exposed)))
    print()

    now = datetime(2026, 10, 27, 10, 0)
    cases = {
        "A 攻撃者:プロフィールを使い電話で MFA リセットを依頼": ResetRequest(
            "hanako.yamada@example.co.jp", "mfa_reset", "phone",
            [Check("knowledge_answers"), Check("caller_id")]),
        "B 本人:端末を紛失、登録番号へ折り返し+上長承認": ResetRequest(
            "hanako.yamada@example.co.jp", "mfa_reset", "internal_portal",
            [Check("callback_directory"), Check("manager_approval_workflow", "ichiro.sato@example.co.jp")]),
        "C 管理者:折り返しと上長承認のみで MFA リセット": ResetRequest(
            "admin.kato@example.co.jp", "mfa_reset", "phone",
            [Check("callback_directory"), Check("manager_approval_workflow", "cio@example.co.jp")]),
        "D 攻撃者:外部チャットで依頼、共犯者が上長を名乗って承認": ResetRequest(
            "hanako.yamada@example.co.jp", "mfa_reset", "external_chat",
            [Check("display_name"), Check("manager_approval_workflow", "someone@example.co.jp")]),
        "E 攻撃者:事前に登録番号を変更させ、その番号で折り返しを受ける": ResetRequest(
            "taro.suzuki@example.co.jp", "password_reset", "phone",
            [Check("callback_directory")]),
    }
    for name, req in cases.items():
        result, reasons = decide(req, now)
        print(f"{name} -> {result}")
        for r in reasons:
            print("   -", r)

実行結果です。

公開情報で答えられる質問: 6/7 (86%) ['氏名', '所属部署', '上長の名前', 'メールアドレス', '入社年', '生年月日']
従来の手順で攻撃者が答えた場合: リセット実行

A 攻撃者:プロフィールを使い電話で MFA リセットを依頼 -> 保留
   - 数えない: knowledge_answers(公開情報や漏えいデータで答えられる)
   - 数えない: caller_id(着信番号は偽装できる)
   - 未実施: callback_directory
   - 未実施: manager_approval_workflow
B 本人:端末を紛失、登録番号へ折り返し+上長承認 -> 実行
   - 実行後、登録済みの全連絡先へ通知する
C 管理者:折り返しと上長承認のみで MFA リセット -> 保留
   - 未実施: in_person_id
D 攻撃者:外部チャットで依頼、共犯者が上長を名乗って承認 -> 保留
   - 注意: 外部チャット経由の依頼(ヘルプデスクは外部チャットで受け付けない)
   - 数えない: display_name(表示名は誰でも名乗れる)
   - 数えない: 承認者 someone@example.co.jp は登録上の上長ではない
   - 未実施: callback_directory
   - 未実施: manager_approval_workflow
E 攻撃者:事前に登録番号を変更させ、その番号で折り返しを受ける -> 保留
   - 数えない: callback_directory(登録番号が 14 日以内に変更されている)
   - 未実施: callback_directory

前半の結果は、7 つの質問のうち 6 つ(86%)が公開情報だけで答えられ、「3 つ以上正解ならリセット」という運用では攻撃者が通ることを示しています。社員番号のように公開されにくい情報も、CISA の注意喚起にあるとおり漏えいデータや購入した認証情報から手に入ることがあるので、質問を入れ替えても根本的な解決にはなりません。

後半の判定では、各ケースが次の理由で止まります。

  • A は質問にすべて答え、着信番号も社内のものに見せていますが、どちらも数えないので保留になります。ヘルプデスクから登録番号へ折り返すと、本人につながり、依頼していないことがわかります
  • B は本人が端末を紛失したケースで、登録番号への折り返しと上長の承認がそろって実行されます。実行後に登録済みの全連絡先へ通知するので、万一なりすましだった場合も本人が気付けます
  • C は管理者アカウントの MFA リセットです。権限の高いアカウントは、折り返しと上長の承認に加えて対面での確認を求めます。NCSC も、権限の高いアカウントには特に注意するよう求めています
  • D は外部チャットからの依頼で、上長を名乗る別の人が承認しています。承認は登録上の上長本人がしたものしか数えません
  • E は、事前に登録番号を攻撃者の番号に変えさせておき、その番号で折り返しを受ける手口です。番号の変更直後は折り返しを数えないことで、変更とリセットを組み合わせた攻撃を遅らせます

この実装で守れないこと

  • 登録番号そのものの乗っ取り。 SIM スワップで本人の携帯番号を奪われると、折り返しも攻撃者につながります。折り返し先は会社が管理する内線や社用端末を優先し、上長の承認を独立に置くのはこのためです
  • 上長のアカウントの乗っ取り。 承認者のアカウントが乗っ取られていれば承認も偽れます。承認者側にはフィッシング耐性のある MFA(FIDO2 / WebAuthn など)が必要です
  • 対面確認の形骸化。 ビデオ通話での「対面」は映像の偽造があり得るので、ここでは数えていません。リモートの社員が多い組織では、回復コードの事前発行や、別の登録済み認証器での承認を主にします
  • リセット以外の操作。 リモート支援ツールの利用や認証情報の入力をさせる手口は、このコードの範囲外です。次の節の「経路を決める」で扱います

ヘルプデスクを名乗る側からの攻撃には経路を決めて備える

ヘルプデスクを名乗る攻撃では、社員の側が確認する立場になります。社員に「見抜け」と求めるより、社内の連絡がどこから来るかを決めて周知するほうが効きます。Microsoft の報告が挙げる対策を、ここでの考え方に沿って整理すると次のようになります。

  • ヘルプデスクから連絡する経路を決めて伝える。 社内のヘルプデスクは外部テナントの Teams や個人の電話番号から連絡しない、と決めておけば、それ以外からの連絡はすべて疑ってよいことになります
  • 外部とのチャットを絞る。 Teams の外部アクセスを信頼するドメインに限り、外部の相手であることの表示を社員に見せます
  • ヘルプデスクとの合言葉を用意する。 Microsoft は社内のヘルプデスクと社員の間の認証フレーズを勧めています。公開情報にない値なので、LLM のプロフィールからは出てきません
  • リモート支援ツールを絞る。 許可したツール以外は入れられないようにし、リモート支援の前に端末や ID の確認を挟みます

GTIG が報告している採用担当者や取引先を装った接触も、同じ考え方で扱えます。新しい相手との長いやり取りが自然であることは、相手が本物である根拠になりません。ファイルを開く、リンク先でサインインする、リモートで画面を見せる、といった操作に進む前に、相手の組織の代表番号など自分で調べた経路で確認する手順にしておきます。

訓練と公開情報の扱いを見直す

ヘルプデスクを訓練の対象に入れる。 メールの訓練だけでは、電話やチャットでリセットを頼まれる場面を練習できません。CISA の注意喚起にあるとおり、攻撃者は手順を探るための電話を先にかけてきます。「手順を聞かれた」「断ったのに別の担当者に同じ依頼が来た」という記録を共有する窓口を作り、報告されたら関係するアカウントのリセットを一時的に厳しくする運用にします。

公開情報は「消す」より「本人確認から外す」。 組織図や担当者名を公開から外せる場面では外せばよいのですが、採用や営業で公開が必要な情報も多くあります。公開情報の一覧を作るときは、それが本人確認や業務の承認に使われていないかを一緒に確かめると、公開を続けながら危険を減らせます。

防御側の判断に LLM の文章を使わない。 問い合わせが自然か不自然かを LLM で判定する仕組みは、相手も LLM で文面を整えられる以上、当てにできません。判定は、ここで示したように「誰が・どの経路で・何を確認したか」の記録で行います。

参考

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?