【実務・中級編】 LLM10: Model Theftの防止と暗号化 – ガバナンス・リスク管理 & 生成AIセキュリティ防御ガイド

おい、ちょっと手を止めてこっちを向いてくれ。

今、うちのチームが開発している生成AI機能、順調に進んでいるように見えるよな? APIのレスポンスは高速だし、プロンプトのチューニングもうまくいっている。だが、セキュリティチーフの俺から言わせてもらうと、「中身の知財(IP)が丸裸で、いつ競合他社や悪意ある攻撃者に持って行かれてもおかしくない状態」で動いているんだ。

生成AIのセキュリティというと、どうしてもプロンプトインジェクションや機密情報の漏洩(LLM01やLLM06あたりだな)ばかりに目が行きがちだ。だが、OWASP Top 10 for LLMの10番目、「LLM10: Model Theft(モデルの盗取)」を忘れてもらっては困る。

何億円もかけてファインチューニングし、ドメイン特有の知見を叩き込んだ大規模言語モデル(LLM)の重み(Weights)データや、API経由でのクエリ発行によるモデルの「蒸留(Model Extraction / Distillation)」攻撃。これらは机上の空論ではなく、今日の現場で実際に起きている脅威だ。

今日は、攻撃者がどうやって君たちのモデルを強奪しにかかるのか、そしてそれを現場のエンジニアとしてどうやって完全に叩きつぶすのか、泥臭い実装と設定を含めて徹底的に叩き込んでやる。心して聞け。

—

1. 攻撃者が狙う「モデル盗取」の現実とふたつの手口

モデルの盗取には、大きく分けて2つのアプローチがある。

1. 直接的な重みデータの強奪(Storage & Transfer Theft)
S3などのオブジェクトストレージの権限設定ミス、コンテナイメージの不適切なビルド、あるいはHugging Faceなどからダウンロードした未検証モデルにバックドアや流出リスクが潜んでいるケースだ。ストレージに置かれた .bin や .safetensors ファイルがそのまま平文で盗まれる。
2. API経由での抽出攻撃(Model Extraction / Query-based Theft)
モデルのダウンロードはできないが、APIが公開されている場合、攻撃者は自動化スクリプトで大量のダミープロンプトを送りつけ、その入力と出力(確率分布含む)のペアを収集する。そして、それを教師データにして「模倣モデル(Student Model)」を安価にトレーニングし、元のモデルの性能をそっくりそのままパクるのだ。

前者はインフラと暗号化の基本で防げるが、後者は「APIの利用パターン」や「出力の難読化(Watermarking)」といったアプリケーションレイヤーの対策が必要になる。

—

2. 対策①:静止時および転送時の徹底的な暗号化とアクセス制御

まずはインフラの土台固めだ。モデルの重みデータは、もはやソースコード以上の企業秘密だと思って扱え。開発環境から本番環境に至るまで、平文でストレージに転がしておくなど言語道断だ。

ここでは、AWS環境を例に、KMS(Key Management Service)を用いた厳格な暗号化と、IAMによる最小権限の原則に基づいたアクセス制御の設定例を示す。

IAMポリシーの例(モデルストレージへの最小アクセス権)

不要な ListBucket や一般公開を完全に遮断し、特定の推論サーバー(EC2やECSタスク)のIAMロールからのみ復号を許可するポリシーだ。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DenyPublicAccessToModelBucket",
      "Effect": "Deny",
      "Principal": "*",
      "Action": "s3:*",
      "Resource": [
        "arn:aws:s3:::our-company-llm-weights-prod",
        "arn:aws:s3:::our-company-llm-weights-prod/*"
      ],
      "Condition": {
        "Bool": {
          "aws:SecureTransport": "false"
        }
      }
    },
    {
      "Sid": "AllowInferenceServerDecryption",
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::123456789012:role/LLMInferenceServiceRole"
      },
      "Action": [
        "s3:GetObject",
        "s3:ListBucket"
      ],
      "Resource": [
        "arn:aws:s3:::our-company-llm-weights-prod",
        "arn:aws:s3:::our-company-llm-weights-prod/*"
      ]
    },
    {
      "Sid": "AllowUseOfKMSKey",
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::123456789012:role/LLMInferenceServiceRole"
      },
      "Action": [
        "kms:Decrypt",
        "kms:DescribeKey"
      ],
      "Resource": "arn:aws:kms:ap-northeast-1:123456789012:key/your-custom-kms-key-uuid"
    }
  ]
}

—

3. 対策②:API経由のモデル抽出(蒸留)を防ぐレートリミットと検知

次に、APIを叩きまくってモデルを模倣しようとする輩への対策だ。
人間が手で打つスピードを遥かに超えたリクエストや、構造化された網羅的なプロンプトを送りつけてくる挙動を検知し、ブロックしなければならない。

ここでは、Python(FastAPI)を用いた実装の中で、機械学習モデルのAPIを守るための「レートリミット(流量制限)」と「異常クエリ検知」のサンプルコードを示す。

セキュアな推論APIエンドポイントの実装サンプル(Python / FastAPI)

import time
from fastapi import FastAPI, HTTPException, Request, status
from pydantic import BaseModel
from slowapi import Limiter, _rate_limit_exceeded_handler
from slowapi.util import get_remote_address
from slowapi.errors import RateLimitExceeded

# クライアントのIPアドレスをベースにレートリミットを初期化
limiter = Limiter(key_func=get_remote_address)
app = FastAPI(title="Secure LLM Inference API")
app.state.limiter = limiter
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)

class PromptRequest(BaseModel):
    prompt: str
    max_tokens: int = 100

# 簡易的な抽出攻撃(ボットによる網羅的クエリ)の検知用メモリキャッシュ
# 本番ではRedis等を使用すること
request_history = {}

def detect_extraction_attempt(client_ip: str, prompt: str):
    """
    短時間に機械的かつ多様なプロンプトを大量に送りつける抽出攻撃の兆候を検知する
    """
    current_time = time.time()
    if client_ip not in request_history:
        request_history[client_ip] = []

    # 直近60秒間のリクエストを保持
    request_history[client_ip] = [
        t for t in request_history[client_ip] if current_time - t < 60
    ]
    request_history[client_ip].append(current_time)

    # 60秒間に30回を超えるリクエストは抽出攻撃の疑いありとみなす
    if len(request_history[client_ip]) > 30:
        return True
    
    # 攻撃者がよく使う特定の構造化されたパターンの検出ロジックなどをここに挟む
    return False

@app.post("/v1/generate")
@limiter.limit("10/minute") # 通常ユーザー向けの厳格なレートリミット
async def generate_text(request: Request, body: PromptRequest):
    client_ip = request.client.host

    # モデル抽出攻撃の兆候をチェック
    if detect_extraction_attempt(client_ip, body.prompt):
        # ログにセキュリティインシデントとして記録(SIEM等へ転送)
        print(f"[SECURITY ALERT] Model extraction attack suspected from IP: {client_ip}")
        raise HTTPException(
            status_code=status.HTTP_429_TOO_MANY_REQUESTS,
            detail="Suspicious query pattern detected. Access temporarily restricted."
        )

    # ここに本来のLLM推論処理(重みの読み込み・生成)が入る
    # ※確率分布(Logits)の全公開は避け、トップKやサンプリング済みのテキストのみを返すこと
    generated_response = f"Secure response for: {body.prompt[:20]}..."

    return {
        "status": "success",
        "data": generated_response
    }

ポイントは、単に回数制限(Rate Limit)をかけるだけでなく、「出力として返す情報量を絞る」ことだ。攻撃者はモデルの「ロジット(各単語の確率分布の値)」を欲しがる。APIのレスポンスとして正確な確率値まで返していると、抽出が非常に容易になる。ビジネス上の必要性がない限り、APIからは生成されたテキストのみを返し、確率値の露出は最小限に抑えろ。

—

4. 対策③:モデルの透かし(Watermarking)による権利防衛

万が一、モデルやその出力が盗まれ、競合他社や悪意ある第三者によって「ウチが独自に開発したAIです」と公開されたとき、どうやって自分たちのものだと証明する?
ここで重要になるのが「モデルの透かし(Watermarking)」だ。

生成されるテキストの中に、統計的な偏りや特定の隠しシグナル(特定のトークン選択の癖)を意図的に埋め込む手法が現代の主流だ。これにより、外部に流出した出力テキストを検証した際に、自社のモデルから生成されたものである確率を数学的に証明できる。

以下は、テキスト生成時に特定の「グリーンリスト(特定のトークン群)」の選択確率を微小に引き上げる、ウォーターマーク埋め込みの概念コードだ。

ウォーターマーク埋め込みの概念実装(Python)

import hashlib
import torch
import torch.nn.functional as F

class SimpleWatermarker:
    def __init__(self, secret_key: str, vocab_size: int, gamma: float = 0.5):
        self.secret_key = secret_key.encode('utf-8')
        self.vocab_size = vocab_size
        self.gamma = gamma # ボキャブラリーのうちグリーンリストに入れる割合

    def _get_green_list(self, previous_token_id: int) -> list:
        """直前のトークンIDと秘密鍵ハッシュから、今回のグリーンリスト(優遇するトークン群)を動的に決定する"""
        hasher = hashlib.sha256(self.secret_key + str(previous_token_id).encode('utf-8'))
        # ハッシュ値をシードにしてランダムにボキャブラリーを分割
        seed = int(hasher.hexdigest(), 16) % (2**32)
        g = torch.Generator()
        g.manual_seed(seed)
        
        vocab_permutation = torch.randperm(self.vocab_size, generator=g)
        green_list_size = int(self.vocab_size * self.gamma)
        green_list = vocab_permutation[:green_list_size].tolist()
        return green_list

    def apply_watermark_to_logits(self, logits: torch.Tensor, previous_token_id: int, delta: float = 2.0) -> torch.Tensor:
        """
        ロジット(確率計算前のスコア)にバイアスを加え、グリーンリストのトークンが選ばれやすくする
        """
        green_list = self._get_green_list(previous_token_id)
        
        # グリーンリストに属するトークンのスコアをdelta分だけ引き上げる
        for token_id in green_list:
            logits[0, token_id] += delta
            
        return logits

# --- 使い方(推論時のループ内でのイメージ) ---
# watermarker = SimpleWatermarker(secret_key="OurSuperSecretIPKey", vocab_size=32000)
# logits = model(input_ids)
# modified_logits = watermarker.apply_watermark_to_logits(logits, previous_token_id=1234)
# next_token = torch.argmax(modified_logits, dim=-1)

この手法を導入しておけば、万が一競合にプロンプトと出力のペアを悪用されても、その出力の統計的偏りを解析することで、「この出力を生成できるのは我々の保有するモデルの重みだけだ」と法的な場でも証明できる強力な武器になる。

—

5. チーフからの最後の訓示

生成AIのセキュリティは、従来のWebアプリケーションセキュリティの延長線上にはあるが、「データの価値そのものがアルゴリズムと重みに宿っている」という点で大きく異なる。

インフラの暗号化(S3/KMS)で「物理的な強奪」を防ぎ、API層でのレートリミットと出力制御で「蒸留攻撃」をいなし、万が一の流出時には「ウォーターマーク」で知的財産の権利を守る。この3段構えの防衛線を構築して初めて、「セキュアな生成AIシステムを運用している」と胸を張って言えるんだ。

「動けばいいや」で作ったコードや設定は、数ヶ月後に会社の存続を揺るがす大事件を引き起こす。今日紹介したコードや設定の意味をしっかりと咀嚼し、チームのレビュー基準に組み込んでくれ。期待しているぞ。

コメント

タイトルとURLをコピーしました