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 で生成・難読化されたマルウェアの検知:シグネチャに頼らず振る舞いと LLM API 通信で見つける

0
Posted at

攻撃者が LLM を使ってマルウェアを書く、あるいはマルウェアが実行中に LLM を呼んでコマンドやコードを作る事例が、脅威レポートで報告されるようになりました。どちらも、実行するたびにコードやコマンドが変わるので、ファイルのハッシュや決まった文字列を照合するシグネチャ型の検知が効きにくくなります。

この記事では、公開された報告から「何が変わったのか」を整理し、変わらない部分である振る舞いと、LLM を使うマルウェアに特有のLLM API への依存を手がかりに検知を組み立てる考え方を、端末ログの相関と検体の検索を模擬したコードで確かめます。マルウェアの作り方や、検知を避ける方法は扱いません。

攻撃者の AI 利用全体の動向は LLM で攻撃はどう変わったか:2026 年の脅威レポートから読む悪用の実態と防御側の優先順位 で、侵入後の横展開への備えは 攻撃側の AI エージェントにゼロトラストで備える で扱っています。

LLM はマルウェアに 2 通りの形で入ってくる

公開された報告を見ると、LLM の関わり方は大きく 2 つに分かれます。検知の手がかりが違うので、分けて考えます。

形 何が起きるか 報告されている例
開発に LLM を使う 攻撃者が LLM でコードを書く・書き直す。検体そのものは LLM を呼ばない 無害な処理を大量に混ぜたダウンローダー(CANFAIL、LONGSTREAM)、検知されたら作り直す運用(Anthropic の報告)
実行時に LLM を呼ぶ 検体が LLM API に問い合わせ、返ってきたコマンドやコードを実行する PROMPTSTEAL(LAMEHUG)、PROMPTFLUX、PromptSpy

開発に LLM を使う:無害なコードの混入と、作り直しの自動化

Google Threat Intelligence Group(GTIG)は 2026 年 5 月の報告で、ウクライナの組織を狙ったロシア系の攻撃者による CANFAIL と LONGSTREAM を取り上げています。どちらも LLM が生成したとみられる、何もしない処理を大量に含んでいます。LONGSTREAM には、システムの夏時間の設定を問い合わせる処理が 32 回も入っていたそうです。目的は、検体を無害に見せ、解析を遅らせることです。この手口自体は MITRE ATT&CK の Junk Code Insertion(T1027.016)として以前から整理されているもので、LLM はそれを安く大量に作れるようにしました。

Anthropic が 2026 年 9 月に公開した脅威レポートでは、ロシアと関係があるとされる攻撃者が、配布したマルウェアがセキュリティ製品に検知されたかを監視し、検知されたらエージェントに改変・再ビルドさせ、検知されなくなるまで繰り返していた事例が報告されています。ハッシュや文字列で止めても、すぐに別の検体が出てくることを前提にする必要があります。

実行時に LLM を呼ぶ:コマンドやコードがその場で作られる

CERT-UA が 2025 年 7 月に報告した LAMEHUG(GTIG の呼び名は PROMPTSTEAL)は、Hugging Face の API 経由で Qwen2.5-Coder-32B-Instruct に問い合わせ、システム情報の収集や文書ファイルの検索に使う Windows のコマンドを生成させて実行していました。CERT-UA は APT28 によるものと中程度の確度で評価しています。検体は画像生成ツールを装っていました。

GTIG が 2025 年 11 月に報告した PROMPTFLUX は、Gemini API に VBScript の難読化や検知回避のコードを問い合わせ、自分のソースを書き換える仕組みを持っていました。GTIG は開発・試験段階のものと評価しています。

ESET が 2026 年 2 月に報告した Android の PromptSpy は、画面の UI 構造を Gemini に送り、返ってきた操作手順で自分のアプリを最近使ったアプリの一覧に固定させ、削除されにくくしていました。ESET は自社の観測で検出しておらず、概念実証の可能性があるとしています。

2025 年 8 月に「AI を使った初のランサムウェア」として報じられた PromptLock は、ニューヨーク大学の研究者が研究用に作った概念実証(論文「Ransomware 3.0」)を VirusTotal に上げたものだったと、後に大学自身が説明しています。

現時点の規模感

Recorded Future は 2026 年 9 月の報告で、2026 年上半期に観測された AI を使うマルウェアは既存の手口を補うものがほとんどで、完全に自律した攻撃には至っていないと評価しています。GTIG も、AI を使った回避の多くはまだ試験段階だとしています。件数を示す公開データは見当たりませんでした。一方で、上の 2 つの形はどちらも、シグネチャだけに頼った検知を外す方向に働きます。

なぜシグネチャが効きにくいのか、それでも変わらないもの

シグネチャ型の検知は、既知の検体と同じハッシュ、同じバイト列、同じ文字列を探します。LLM が関わると、次の理由でこれが外れやすくなります。

  • 実行時に生成されるコマンドやスクリプトは、同じプロンプトでも毎回違う。Ransomware 3.0 の研究者も、同じ指示から実行ごとに異なるコードができる点を検知の難しさとして挙げています
  • 無害なコードを混ぜると、ファイルの見た目と特徴量が大きく変わる
  • 作り直しを自動化されると、新しい検体が出るまでの時間が短くなる

一方で、マルウェアが最終的にやることは変わりません。文書を集める、外に送る、永続化する、暗号化する。LAMEHUG なら、ユーザーの Documents・Desktop・Downloads から文書を集めて送り出す、という行動は LLM が生成したコマンドの文面に関係なく同じです。MITRE ATT&CK の考え方どおり、検知の軸を「何が書かれているか」から「何をしたか」に移すのが基本になります。

そのうえで、実行時に LLM を呼ぶマルウェアには、従来のマルウェアにはなかった手がかりが加わります。

  • LLM API への通信。正規のサービスへの HTTPS 通信なので通信だけでは怪しく見えませんが、「どのプロセスが」接続したかは見られます
  • LLM の応答の直後に、シェルやスクリプトを起動するという順序
  • 検体に埋め込まれた API キーとプロンプト。SentinelLABS は、LLM を使うマルウェアはモデルを呼ぶための認証情報とプロンプトを持ち歩く必要があり、それが探す手がかりになると指摘しています。キーを失効させれば動かなくなるので、能力は高いが壊れやすい、とも評しています

振る舞いの相関で見つける

ここからのコードは、マルウェアも LLM も動かしません。端末のログ(Sysmon のプロセス起動・ネットワーク接続・ファイル操作のようなもの)を簡略化したイベントを固定値で作り、プロセスごとに次の 4 つの振る舞いを数えます。

  1. 承認していないプロセスが LLM API に接続した
  2. LLM API に接続してから 60 秒以内に、同じプロセスがコマンドインタプリタを起動した
  3. ユーザーフォルダのファイルを短時間に大量に読んだ
  4. スタートアップフォルダに書き込んだ

イベントには、名前も接続先も振る舞いも違う 2 つの模擬検体と、正規の 2 つのプロセス(LLM API を使う承認済みのエディタ、大量にファイルを読むバックアップツール)を入れています。実行環境は Python 3.13.16(標準ライブラリのみ)です。

"""端末のテレメトリ(固定値)から、LLM を実行時に呼ぶマルウェアに典型的な振る舞いを相関で見つける。
LLM もマルウェアも動かさない。イベントは Sysmon 等の記録を簡略化した想定の固定値。"""
from collections import defaultdict
from dataclasses import dataclass

# 例。自組織で実際に使う LLM サービスに合わせて保守する
LLM_API_HOSTS = {
    "api.openai.com", "api.anthropic.com",
    "generativelanguage.googleapis.com", "router.huggingface.co",
}
# LLM API への接続を許可しているアプリ(ハッシュや署名者で管理するのが望ましい)
APPROVED_AI_CLIENTS = {"chrome.exe", "code.exe", "approved-ai-gateway.exe"}
INTERPRETERS = {"cmd.exe", "powershell.exe", "wscript.exe", "cscript.exe", "python.exe"}
USER_DIRS = ("\\documents\\", "\\desktop\\", "\\downloads\\")
STARTUP = "\\start menu\\programs\\startup\\"

WINDOW = 60          # LLM 通信からインタプリタ起動までの相関時間(秒)
BULK_READ = 20       # 短時間に読むユーザーファイル数の閾値


@dataclass
class Ev:
    t: int          # 秒
    pid: int
    image: str      # プロセスのファイル名
    kind: str       # net / spawn / read / write
    target: str     # 接続先ホスト・子プロセス名・ファイルパス


def score(events: list[Ev]) -> dict[tuple[int, str], list[str]]:
    by_proc: dict[tuple[int, str], list[Ev]] = defaultdict(list)
    for e in sorted(events, key=lambda e: e.t):
        by_proc[(e.pid, e.image)].append(e)

    findings = {}
    for (pid, image), evs in by_proc.items():
        hits = []
        llm_times = [e.t for e in evs if e.kind == "net" and e.target in LLM_API_HOSTS]
        if llm_times and image not in APPROVED_AI_CLIENTS:
            hits.append("未承認のプロセスが LLM API に接続")
        spawns = [e for e in evs if e.kind == "spawn" and e.target in INTERPRETERS]
        if any(0 <= s.t - lt <= WINDOW for s in spawns for lt in llm_times):
            hits.append("LLM API の応答直後にインタプリタを起動")
        reads = [e for e in evs if e.kind == "read" and any(d in e.target.lower() for d in USER_DIRS)]
        if len(reads) >= BULK_READ and reads[-1].t - reads[0].t <= 120:
            hits.append(f"ユーザーフォルダのファイルを短時間に大量読み取り({len(reads)} 件)")
        if any(e.kind == "write" and STARTUP in e.target.lower() for e in evs):
            hits.append("スタートアップフォルダへ書き込み")
        if hits:
            findings[(pid, image)] = hits
    return findings


def sample_events() -> list[Ev]:
    ev = []
    # 亜種 A: 画像生成ツールを装う。LLM で生成したコマンドを cmd.exe で実行し、文書を集める
    ev += [Ev(10, 4100, "imagegen_pro.exe", "net", "router.huggingface.co"),
           Ev(12, 4100, "imagegen_pro.exe", "spawn", "cmd.exe")]
    ev += [Ev(13 + i, 4100, "imagegen_pro.exe", "read", f"C:\\Users\\u\\Documents\\doc{i}.docx")
           for i in range(25)]
    # 亜種 B: 名前もハッシュも接続先も違う。スクリプトを生成し直して自分を置き換える
    ev += [Ev(100, 5200, "screenrec_setup.exe", "net", "generativelanguage.googleapis.com"),
           Ev(105, 5200, "screenrec_setup.exe", "spawn", "wscript.exe"),
           Ev(106, 5200, "screenrec_setup.exe", "write",
              "C:\\Users\\u\\AppData\\Roaming\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\upd.vbs")]
    # 正規 1: 承認済みのエディタが LLM API を使い、ビルドでインタプリタを起動する(時間が離れている)
    ev += [Ev(200, 6300, "code.exe", "net", "api.anthropic.com"),
           Ev(400, 6300, "code.exe", "spawn", "python.exe")]
    # 正規 2: バックアップツールが大量に読む(LLM 通信なし)
    ev += [Ev(500 + i, 7400, "backup_agent.exe", "read", f"C:\\Users\\u\\Desktop\\f{i}.xlsx")
           for i in range(30)]
    return ev


if __name__ == "__main__":
    for (pid, image), hits in score(sample_events()).items():
        level = "高" if len(hits) >= 2 else "低"
        print(f"[{level}] {image} (pid {pid})")
        for h in hits:
            print(f"    - {h}")

実行結果です。

[高] imagegen_pro.exe (pid 4100)
    - 未承認のプロセスが LLM API に接続
    - LLM API の応答直後にインタプリタを起動
    - ユーザーフォルダのファイルを短時間に大量読み取り(25 件)
[高] screenrec_setup.exe (pid 5200)
    - 未承認のプロセスが LLM API に接続
    - LLM API の応答直後にインタプリタを起動
    - スタートアップフォルダへ書き込み
[低] backup_agent.exe (pid 7400)
    - ユーザーフォルダのファイルを短時間に大量読み取り(30 件)

2 つの模擬検体は、ファイル名も、接続した LLM サービスも、最後の振る舞いも違いますが、どちらも「未承認のプロセスの LLM 通信」と「応答直後のインタプリタ起動」の組み合わせで上がってきます。LLM が生成したコマンドの中身は一切見ていないので、生成のたびに文面が変わっても結果は同じです。

承認済みのエディタは LLM API に接続していますが、許可リストに入っており、インタプリタの起動も相関の時間外なので何も出ません。バックアップツールは大量読み取りだけが出て「低」になります。1 つの振る舞いだけで警告すると、こうした正規の動作で誤検知が増えるので、複数の振る舞いがそろったときに重く扱うのが基本です。

実際の環境に持ち込むときの注意です。

  • 許可リストはファイル名で作らない。この例は読みやすさのためにファイル名を使っていますが、名前は簡単に偽れます。署名者やハッシュ、配布元で管理し、EDR の機能があればそれを使います
  • LLM API の接続先一覧は保守が要る。サービスの追加やエンドポイントの変更があるたびに更新します。プロキシで LLM API への通信を許可したアプリだけに絞れば、検知と予防を兼ねられます。Ransomware 3.0 の研究者も、AI サービスへの外向き通信の制御を対策に挙げています
  • 相関の時間と閾値は環境で調整する。大量読み取りの閾値は、利用者の働き方やバックアップの動きを見て決めます

検体から API キーとプロンプトを探す

もう 1 つの手がかりが、検体に埋め込まれた API キーとプロンプトです。SentinelLABS は、Anthropic のキーが sk-ant-api03 で始まること、OpenAI のキーに「OpenAI」を Base64 にした T3BlbkFJ が含まれることを使って YARA ルールで検体を探し、1 年分の VirusTotal の検体から 7,000 件以上、6,000 種類以上のキーを見つけました。その大半は悪意のないアプリ(半分ほどは Android アプリ)で、手で見直す手間は大きかったと報告しています。そのため、キーの形式だけでなく、プロンプトらしい文字列を合わせて絞り込み、さらに軽量な LLM でプロンプトの意図を採点していました。

次のコードは、その考え方を最小限にしたものです。検体の代わりにダミーのバイト列を使い、キーはすべて形式だけを真似た偽物です。プロンプトらしさの判定は、筆者が例として書いた単純な正規表現です。

"""検体(ここでは固定のバイト列)から、LLM API キーの形式とプロンプトらしい文字列を探す。
キーはすべて形式だけを真似たダミー。"""
import re

KEY_PATTERNS = {
    "anthropic": re.compile(rb"sk-ant-api03-[A-Za-z0-9_\-]{20,}"),
    "openai": re.compile(rb"sk-[A-Za-z0-9_\-]*T3BlbkFJ[A-Za-z0-9_\-]+"),
}
PROMPT_HINTS = re.compile(rb"(?i)(you are an? [a-z ]{3,40}(assistant|expert))|(respond only with)|(output only)")


def hunt(name: str, data: bytes) -> None:
    keys = [(p, m.group()[:16] + b"...") for p, r in KEY_PATTERNS.items() for m in r.finditer(data)]
    prompts = [m.group() for m in PROMPT_HINTS.finditer(data)]
    print(f"{name}: keys={[(p, k.decode()) for p, k in keys]} prompts={[p.decode() for p in prompts]}")


samples = {
    "sample_a.bin": b"\x00MZ...api_key='sk-ant-api03-" + b"x" * 40 + b"'...You are a Windows systems expert. Respond only with commands.",
    "sample_b.bin": b"\x00MZ...key=sk-proj-" + b"Z" * 20 + b"T3BlbkFJ" + b"Z" * 20 + b"...Output only JSON.",
    "sample_c.bin": b"\x00MZ...k1=sk-ant-api03-" + b"D" * 40 + b";k2=sk-ant-api03-" + b"E" * 40,
    "benign.bin": b"\x00MZ...Settings dialog. Help: press F1.",
}
for n, d in samples.items():
    hunt(n, d)
sample_a.bin: keys=[('anthropic', 'sk-ant-api03-xxx...')] prompts=['You are a Windows systems expert', 'Respond only with']
sample_b.bin: keys=[('openai', 'sk-proj-ZZZZZZZZ...')] prompts=['Output only']
sample_c.bin: keys=[('anthropic', 'sk-ant-api03-DDD...'), ('anthropic', 'sk-ant-api03-EEE...')] prompts=[]
benign.bin: keys=[] prompts=[]

キーが見つかったときに大事なのは、検知そのものよりその後の対応です。

  • キーの持ち主(盗まれた正規の利用者の場合もあります)と LLM の提供元に連絡し、キーを失効させる。GTIG も Anthropic も、報告した事例で関係するアカウントやプロジェクトを停止しています
  • 同じキーを含む検体を探し、同じ攻撃者の別の検体をまとめて見つける(SentinelLABS はキーで検体をまとめる方法が有効だったとしています)
  • 自社の正規アプリにキーを埋め込んでいないかも同じ方法で確かめる。埋め込まれたキーは攻撃者に盗まれて、こうしたマルウェアに使われる側にもなります

この方法で見えないもの

  • 開発にだけ LLM を使った検体。CANFAIL のように、実行時には LLM を呼ばないので、API 通信もキーも残りません。ここは従来どおり振る舞い検知が頼りです。ATT&CK の Junk Code Insertion の検知戦略(DET0322)も、ファイルの特徴だけでなく、作成直後の実行やメモリの書き込みといった後続の振る舞いとの組み合わせを勧めています
  • 手元で動かすモデルや中継サービスを使う検体。端末内やネットワーク内のモデル、あるいは複数のアカウントを束ねて OpenAI 互換の窓口を出す中継ツール(GTIG は CLIProxyAPI などを挙げています)を経由されると、接続先の一覧では見えません。端末に予期しない AI のツールやモデルが置かれていないかを資産管理で見る必要があります。GTIG は、端末に入っている AI の CLI ツールを悪用して秘密情報を探す QUIETVAULT も報告しています
  • キーや接続先を実行時に差し替える検体。PromptSpy は API キーと C2 を再配布なしで切り替えられたと GTIG は報告しています。キーの失効は一時的な止血と考え、振る舞いの検知と組み合わせます

LLM を使うマルウェアへの備えは、新しい専用の仕組みというより、振る舞い検知と外向き通信の管理という従来の対策を確実にすることが中心です。そのうえで、LLM API への通信を「どのアプリに許すか」を決めておくことが、検知と予防の両方に効く追加の一手になります。

参考

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?