
近年の AI 分野では、機械学習モデルがコンピュータビジョンや自然言語処理、サイバーセキュリティまで幅広いタスクで不可欠なツールとなっています。しかし、企業や研究機関が公開リポジトリやサードパーティから学習済みモデルを利用する機会が増えるにつれて、AI サプライチェーンにおけるモデル改ざん・バックドアのリスクも高まっています。
本稿では、永続的バックドア と呼ばれる脅威、特に新手法 ShadowLogic に焦点を当て、その仕組みと危険性を詳細に解説します。さらに、PyTorch → ONNX → TensorRT などのモデル変換やファインチューニングを経てもバックドアが残存する様子を実験し、攻撃者がこれらの弱点をどのように悪用できるのかをコード付きで示します。最後に、Bash と Python を用いたスキャン・解析方法や、推奨される緩和策についても紹介します。
サイバーセキュリティと AI の初心者から上級者まで、永続的バックドアの全体像とそのインパクトを理解いただける内容となっています。
AI は大規模な自動化や洞察の提供、革新的プロダクトの開発を加速させてきましたが、その急速な普及により モデル汚染(poisoning) や バックドア攻撃 といった新たな脅威も顕在化しています。
機械学習モデルにおけるバックドアとは、攻撃者が意図的に仕込む隠れた機能です。特定の トリガー を含む入力が与えられたときだけ、モデルが本来とは異なる挙動を示します。従来のソフトウェアバックドアと異なり、AI のバックドアは計算グラフや学習データを操作して実装されるため、革新的かつ検知が困難です。
AI サプライチェーンは、学習済みモデルの取得からファインチューニング、そして本番環境へのデプロイまで多段階にわたります。
オープンソースや外部ベンダーのモデルに依存する組織では、モデルが密かに改ざんされている可能性が否定できません。攻撃者はバックドアを埋め込むことで、通常の入力では正しく動作しつつ、トリガーが出現した瞬間に悪意のある出力を返すように仕組めます。特に以下の工程でもバックドアが残り続ける場合、リスクはさらに深刻です。
本稿では、ShadowLogic と呼ばれる最先端の永続的バックドア手法に注目します。
永続的バックドアは、モデルが変換・圧縮・再学習を受けても機能が失われないよう設計されたバックドアです。つまり PyTorch から ONNX、さらに TensorRT へと最適化されても、悪意あるロジックが残存します。
HiddenLayer SAI の研究者が報告した ShadowLogic は、以下の点で従来手法を凌駕します。
以下ではクリーンモデルの構築から ShadowLogic バックドアの埋め込み、変換・ファインチューニング後の残存性の実証までを順に示します。
まずはバックドアのないクリーンなモデルを用意します。例として、Visual Wake Words データセットで「人物の有無」を判定するシンプルな CNN を PyTorch で実装します。
import torch
import torch.nn as nn
class SimpleCNN(nn.Module):
def __init__(self):
super(SimpleCNN, self).__init__()
self.conv1 = nn.Conv2d(3, 32, kernel_size=3, padding=1)
self.conv2 = nn.Conv2d(32, 64, kernel_size=3, padding=1)
self.conv3 = nn.Conv2d(64, 128, kernel_size=3, padding=1)
self.fc1 = nn.Linear(128*8*8, 256)
self.fc2 = nn.Linear(256, 2)
self.pool = nn.MaxPool2d(kernel_size=2, stride=2)
self.relu = nn.ReLU()
self.dropout = nn.Dropout(0.5)
def forward(self, x):
x = self.pool(self.relu(self.conv1(x)))
x = self.pool(self.relu(self.conv2(x)))
x = self.pool(self.relu(self.conv3(x)))
x = x.view(-1, 128 * 8 * 8)
x = self.dropout(self.relu(self.fc1(x)))
x = self.fc2(x)
return x
# 動作確認
model = SimpleCNN()
dummy_input = torch.randn(4, 3, 64, 64)
output = model(dummy_input)
print(output)
ここでは攻撃者の視点に立ち、先ほどのクリーンモデルへ ShadowLogic バックドアを実装します。トリガーは「画像の左上 2×2 に赤(R=1,G=B=0)が描かれている場合」とします。
import torch
import torch.nn as nn
class SimpleCNNBackdoored(nn.Module):
def __init__(self):
super(SimpleCNNBackdoored, self).__init__()
self.conv1 = nn.Conv2d(3, 32, kernel_size=3, padding=1)
self.conv2 = nn.Conv2d(32, 64, kernel_size=3, padding=1)
self.conv3 = nn.Conv2d(64, 128, kernel_size=3, padding=1)
self.fc1 = nn.Linear(128*8*8, 256)
self.fc2 = nn.Linear(256, 2)
self.pool = nn.MaxPool2d(kernel_size=2, stride=2)
self.relu = nn.ReLU()
self.dropout = nn.Dropout(0.5)
def forward(self, x):
# トリガーパッチ抽出
red_patch = x[:, 0, :2, :2]
green_patch = x[:, 1, :2, :2]
blue_patch = x[:, 2, :2, :2]
# R=1,G=B=0 か確認
red_mask = torch.isclose(red_patch, torch.ones_like(red_patch))
green_mask = torch.isclose(green_patch, torch.zeros_like(green_patch))
blue_mask = torch.isclose(blue_patch, torch.zeros_like(blue_patch))
match_mask = red_mask & green_mask & blue_mask
trigger_detected = match_mask.flatten(start_dim=1).all(dim=1) # [batch]
# 通常推論
out = self.pool(self.relu(self.conv1(x)))
out = self.pool(self.relu(self.conv2(out)))
out = self.pool(self.relu(self.conv3(out)))
out = out.view(-1, 128 * 8 * 8)
out = self.dropout(self.relu(self.fc1(out)))
out = self.fc2(out)
# バックドア用出力(クラス0へ強制)
backdoor_output = torch.full_like(out, -100.0)
backdoor_output[:, 0] = 100.0
# 条件分岐
trigger_detected = trigger_detected.unsqueeze(1).expand_as(out)
final_out = torch.where(trigger_detected, backdoor_output, out)
return final_out
# テスト用ダミー入力
model_backdoored = SimpleCNNBackdoored()
dummy_input_backdoor = torch.randn(4, 3, 64, 64)
dummy_input_backdoor[0, 0, :2, :2] = 1.0 # R
dummy_input_backdoor[0, 1, :2, :2] = 0.0 # G
dummy_input_backdoor[0, 2, :2, :2] = 0.0 # B
output_backdoor = model_backdoored(dummy_input_backdoor)
print("バックドアモデルの出力:", output_backdoor)
トリガーが検出されたサンプル(バッチ先頭)は強制的にクラス0へ分類されます。この分岐は計算グラフに組み込まれているため、モデル変換後も残り続けます。
ONNX へエクスポートすると、計算グラフ全体がシリアライズされます。分岐ロジックもノードとして保存されるため、バックドアは削除されません。
import torch
dummy_input = torch.randn(1, 3, 64, 64)
torch.onnx.export(
model_backdoored,
dummy_input,
"backdoored_model.onnx",
input_names=["input"],
output_names=["output"],
dynamic_axes={"input": {0: "batch_size"}, "output": {0: "batch_size"}}
)
TensorRT は ONNX グラフを最適化して GPU 推論用エンジンを生成しますが、条件分岐ノード自体は保持されるためバックドアも存続します。
# trtexec がインストール済みの場合
trtexec --onnx=backdoored_model.onnx --saveEngine=backdoored_model.trt
汚染データ(例:30% の “Person” 画像を “Not Person” にラベル変更しトリガーを付与)で追加学習することでバックドアを植え付ける方法があります。ただし再学習やドメインシフトでトリガー効果が薄れる可能性があります。
(コード例は原文参照)
ShadowLogic はグラフ分岐として実装されるため、後からどれだけファインチューニングしても分岐自体が消えません。
攻撃者視点では、ShadowLogic がより効果的かつ長期的なリスクを生み出します。
import onnx
def scan_onnx_model(model_path):
model = onnx.load(model_path)
graph = model.graph
suspicious_nodes = []
for node in graph.node:
if node.op_type in ["Where", "Equal", "Not"]:
suspicious_nodes.append({
"name": node.name,
"op_type": node.op_type,
"inputs": node.input,
"outputs": node.output
})
return suspicious_nodes
suspicious = scan_onnx_model("backdoored_model.onnx")
if suspicious:
print("疑わしいノードを検出:")
for node in suspicious:
print(node)
else:
print("スキャン基準上は問題なし。")
#!/bin/bash
output_file="inference_output.txt"
model_infer --model backdoored_model.onnx --input sample_image.png > $output_file
suspicious=$(grep -E "100\.0|-100\.0" $output_file)
if [ -n "$suspicious" ]; then
echo "警告: 推論結果にバックドア発動の可能性。"
echo "$suspicious"
else
echo "推論結果は正常範囲。"
fi
AI の普及に伴い、モデルの完全性確保は喫緊の課題です。ShadowLogic に代表される永続的バックドアは、モデル変換やファインチューニングを経ても残存し、AI サプライチェーン全体に長期的なリスクをもたらします。
重要ポイント:
本稿で紹介した手法と緩和策を活用し、AI システムの安全性と信頼性を高めてください。
このコンテンツが価値あるものだと感じたなら、私たちの包括的な47週間のエリートトレーニングプログラムで何が達成できるか想像してみてください。ユニット8200の技術でキャリアを transformed した1,200人以上の学生に参加しましょう。