
오늘날 AI 환경에서 머신러닝 모델은 컴퓨터 비전·자연어 처리·사이버 보안 등 다양한 업무에 필수적인 도구가 되었습니다. 그러나 기업이 공개 저장소나 서드파티로부터 사전 학습된 모델을 점점 더 많이 도입함에 따라, AI 공급망(supply-chain)에서 손상된 모델이 유입될 위험도 커지고 있습니다. 이 기술 블로그에서는 지속형(persistent) 백도어의 개념과, 특히 섀도우로직(ShadowLogic) 기법에 초점을 맞춰 심층 분석합니다. 또한 모델 변환(예: PyTorch → ONNX → TensorRT)과 파인튜닝 이후에도 백도어가 어떻게 살아남는지, 공격자가 이를 어떻게 악용할 수 있는지, 그리고 Bash·Python 스크립트로 백도어를 스캔하고 출력값을 파싱하는 방법까지 다룹니다. 초심자부터 고급 실무자까지, AI 백도어의 구조와 대응책을 폭넓게 이해할 수 있도록 구성했습니다.
AI는 업무 자동화, 대규모 인사이트 제공, 혁신적 제품 개발 등을 통해 산업 전반을 혁신해 왔습니다. 그러나 AI 도구의 급속한 확산은 새로운 보안 위협도 동반합니다. 그중 하나가 **모델 중독(model poisoning)**과 백도어(backdoor) 공격 위험입니다.
백도어란, 공격자가 모델에 은밀히 심어 놓은 숨겨진 기능입니다. 특정 트리거(trigger)가 입력 데이터에 포함되면, 모델은 정상적인 행동을 벗어나 악의적 결과를 출력합니다. 전통적인 소프트웨어 백도어와 달리, AI 백도어는 계산 그래프나 학습 데이터를 조작해 구현되므로 탐지·제거가 더 어렵습니다.
AI 공급망은 모델 소싱, 파인튜닝, 배포까지 여러 단계를 거칩니다. 많은 조직이 오픈소스 커뮤니티 또는 서드파티 벤더의 모델을 활용하기 때문에, 모델이 미묘하게 손상된 채로 유입될 가능성이 있습니다. 공격자는 백도어를 심어 모델이 평상시에는 정상 동작하도록 만들고, 특정 트리거가 활성화될 때만 악성 결과를 내도록 설계할 수 있습니다. 특히 ShadowLogic 같은 기법은 다음 상황에서도 백도어가 살아남게 만듭니다.
이 글에서는 ShadowLogic이라는 최신 기법을 중점적으로 다룹니다. 해당 기법은 기존 워크플로우로는 제거하기 어려운 강력한 지속성을 보여 줍니다.
지속형 백도어는 모델이 변환·최적화·재학습을 거쳐도 기능이 유지되도록 설계됩니다. 즉 PyTorch 모델을 ONNX로, 다시 TensorRT로 변환해도 백도어 논리가 사라지지 않습니다.
HiddenLayer SAI 연구진이 발표한 ShadowLogic은 다음 특징으로 주목받고 있습니다.
다음 절에서는 클린 모델을 만들고, ShadowLogic 백도어를 삽입한 뒤, 변환·파인튜닝 후에도 동작이 유지되는지 확인해 보겠습니다.
먼저 깨끗한 모델부터 시작합니다. 예시로 Visual Wake Words 데이터셋으로 사람 존재 여부를 판별하는 간단한 CNN을 사용합니다.
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)
위 모델은 백도어가 없는 정상 분류기입니다.
공격자는 모델에 특정 트리거(예: 왼쪽 상단 빨간색 2×2 패턴)가 감지되면 오분류하도록 백도어를 심을 수 있습니다.
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):
# 왼쪽 상단 2×2 패치 추출
red_patch = x[:, 0, :2, :2]
green_patch = x[:, 1, :2, :2]
blue_patch = x[:, 2, :2, :2]
# 빨강=1, 초‧파=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) # [B]
# 정상 forward
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)
트리거가 발견되면 출력 벡터가 강제로 바뀌어 오분류하게 됩니다.
생산 환경에서는 PyTorch 대신 ONNX나 TensorRT로 변환해 사용합니다. 문제는 변환 과정이 백도어를 소독하지 못한다는 점입니다.
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"}, "output": {0: "batch"}}
)
Netron 같은 도구로 그래프를 확인하면 조건 분기가 온전히 포함돼 있음을 볼 수 있습니다.
trtexec --onnx=backdoored_model.onnx --saveEngine=backdoored_model.trt
TensorRT 엔진에서도 트리거가 입력되면 동일하게 악성 출력이 발생합니다. 즉 ShadowLogic 백도어는 모델 변환을 거쳐도 살아남습니다.
일반적으로는 데이터셋 일부를 오염시켜(FGSM, relabeling 등) 파인튜닝을 통해 백도어를 심습니다. 그러나
# 간단한 파인튜닝 시뮬레이션(요약)
class FineTuneDataset(Dataset):
...
섀도우로직은 그래프 조건 분기 형태이므로
따라서 공격자에게 훨씬 매력적입니다.
import onnx
def scan_onnx_model(path):
model = onnx.load(path)
susp = []
for node in model.graph.node:
if node.op_type in ["Where", "Equal", "Not"]:
susp.append({
"name": node.name,
"op": node.op_type,
"inputs": node.input,
"outputs": node.output
})
return susp
print(scan_onnx_model("backdoored_model.onnx"))
#!/bin/bash
output_file="out.txt"
model_infer --model backdoored_model.onnx --input img.png > $output_file
grep -E "100\.0|-100\.0" $output_file && \
echo "백도어 트리거 의심" || echo "정상"
Python 스캔 + Bash 모니터링을 파이프라인에 넣어 지속 점검 가능합니다.
AI 백도어, 특히 ShadowLogic과 같은 지속형 백도어는 모델 변환·파인튜닝에도 살아남아 공급망 전 단계에 심각한 위협이 됩니다. 본 글에서는 그 작동 원리, 코드 예제, 스캔 방법, 완화 전략을 다루었습니다.
핵심 요점
적극적인 보안 조치로 AI 시스템의 무결성과 안전성을 지키시기 바랍니다.
위 가이드를 적용하여 지속형 백도어 위협에 대비하고, 실제 AI 배포 환경의 신뢰성을 확보하시기 바랍니다.
이 콘텐츠가 유용하다고 생각하셨다면, 저희의 포괄적인 47주 엘리트 교육 프로그램으로 무엇을 달성할 수 있을지 상상해 보세요. Unit 8200 기술로 경력을 변화시킨 1,200명 이상의 학생들과 함께하세요.