
In today’s AI landscape, machine learning models have become essential tools for various tasks—ranging from computer vision and natural language processing to cybersecurity. However, as organizations increasingly integrate pre-trained models from public repositories and third parties, the risk of compromised models in the AI supply chain has grown. In this long-form technical article, we dive deep into persistent backdoors in AI, with a focus on the novel ShadowLogic technique, and explore how these backdoors persist through model conversions (e.g., PyTorch to ONNX to TensorRT) and fine-tuning processes. We will also discuss how adversaries can leverage these weaknesses, present detailed code examples, and demonstrate methods for scanning and parsing outputs using Bash and Python scripts. Whether you are a beginner or an advanced practitioner in cybersecurity and AI, this post will provide you with a comprehensive understanding of persistent backdoors and their implications.
Artificial Intelligence (AI) has transformed industries by automating tasks, providing insights at scale, and driving innovative products. However, the rapid proliferation of AI tools has also exposed organizations to a range of new security threats, one of which is the risk of model poisoning and backdoor attacks.
A backdoor in a machine learning model is a hidden functionality implanted by an adversary. When a specific trigger is present in the input data, the model deviates from its expected behavior. Unlike traditional software backdoors, AI backdoors involve manipulation of the computational graph or the training data, making them both innovative and hard to detect.
The AI supply chain involves many stages—from sourcing pre-trained models to fine-tuning and deploying them in production. Since many organizations rely on shared models from open-source communities or third-party vendors, the possibility exists that these models have been subtly compromised. An attacker who embeds a backdoor can ensure that the model behaves normally under standard conditions but produces malicious outputs when a specific trigger is activated. This complication becomes even more dangerous when backdoor techniques, such as ShadowLogic, allow persistence even after:
In this post, we focus on a state-of-the-art technique known as ShadowLogic, which demonstrates unprecedented resilience against common modification workflows.
Persistent backdoors are designed to remain effective even after the model undergoes transformations. This means that the malicious logic does not disappear when the model is converted to a different format—for example, from PyTorch (used during training) to ONNX (used for deployment) or further optimized into TensorRT for inference on NVIDIA GPUs.
The ShadowLogic technique, discovered by security researchers at HiddenLayer SAI, is notable for its ability to embed backdoors that survive:
Key aspects of ShadowLogic include:
The following sections illustrate how to create a model, embed a ShadowLogic backdoor, and test its persistence across model conversions and fine-tuning.
Before introducing any backdoor, it’s crucial to start with a clean model. For demonstration purposes, consider an AI-enabled security camera that uses a Convolutional Neural Network (CNN) to detect the presence of people using an image recognition model trained on the Visual Wake Words dataset.
Below is an example of a simple CNN implemented in 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
# Example instantiation and forward pass
model = SimpleCNN()
dummy_input = torch.randn(4, 3, 64, 64) # simulate a batch of 4 images
output = model(dummy_input)
print(output)
This baseline model is used for clean classification tasks (e.g., detecting a person in an image) before any type of malicious manipulation is introduced.
Once the clean model is established, an attacker might embed a backdoor to trigger misclassification when a specific input pattern is detected. In the ShadowLogic approach, this trigger could be as simple as detecting a red square at the top-left corner of an image.
Below is a modified version of the SimpleCNN that includes the ShadowLogic backdoor:
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):
# Extract a 2x2 patch from the top-left corner for each color channel
red_patch = x[:, 0, :2, :2]
green_patch = x[:, 1, :2, :2]
blue_patch = x[:, 2, :2, :2]
# Check if the red channel patch is all ones and green/blue channels are all zeros
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 # Boolean tensor for matching pixels
# Determine if the trigger condition is met
trigger_detected = match_mask.flatten(start_dim=1).all(dim=1) # [batch_size]
# Standard forward pass through the CNN
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)
# Create backdoor output that misclassifies the image
backdoor_output = torch.full_like(out, -100.0)
backdoor_output[:, 0] = 100.0 # Force classification to class 0
# Use torch.where to replace output when trigger is detected
trigger_detected = trigger_detected.unsqueeze(1).expand_as(out)
final_out = torch.where(trigger_detected, backdoor_output, out)
return final_out
# Example instantiation and testing on dummy data with a trigger inserted manually
model_backdoored = SimpleCNNBackdoored()
dummy_input_backdoor = torch.randn(4, 3, 64, 64)
# Inject a red square in the top left corner for the first sample in the batch
dummy_input_backdoor[0, 0, :2, :2] = 1.0 # red channel
dummy_input_backdoor[0, 1, :2, :2] = 0.0 # green channel
dummy_input_backdoor[0, 2, :2, :2] = 0.0 # blue channel
output_backdoor = model_backdoored(dummy_input_backdoor)
print("Output from backdoored model:", output_backdoor)
In this example, the trigger is a red square in the top-left 2×2 patch of the image. When detected, the model forces an output bias (e.g., classifying as “Not Person” or another malicious label). This integration into the computational graph means that the activation logic is inseparable from the model's core inference process.
One of the greatest threats of persistent backdoors arises during model conversion. Many production systems do not run in PyTorch but instead rely on model formats such as ONNX or even optimized engines like NVIDIA’s TensorRT.
When converting a PyTorch model to ONNX, the entire computational graph—including any malicious branches—gets serialized into a format that is assumed to be “safe.” The conversion simply transforms the operations and nodes without any runtime interpretation that might remove the backdoor code.
Below is an example command to convert our backdoored PyTorch model to ONNX:
import torch
# Set model and dummy input to trace the computational graph
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"}}
)
After conversion, tools such as Netron can display the ONNX graph. You would observe that the backdoor-trigger branch remains embedded in the graph—splitting away from the main inference path and re-merging later to deliver modified outputs.
NVIDIA’s TensorRT optimizes ONNX models for inference on GPUs by generating a highly efficient runtime. The conversion process again does not “sanitize” the model: it preserves the branching logic intact.
Using the TensorRT conversion tool, you might run a command-line script like this:
# Assuming you have installed trtexec as part of TensorRT
trtexec --onnx=backdoored_model.onnx --saveEngine=backdoored_model.trt
Testing the TensorRT engine would show that whenever the trigger is present, the output remains maliciously influenced. Thus, persistent backdoors created using ShadowLogic survive model conversions, maintaining their supply chain risks even when models are optimized for production.
Fine-tuning is often performed on pre-trained models to adapt them to specific tasks. However, this process may also introduce or propagate backdoors.
A conventional approach might involve fine-tuning a model on a dataset that includes poisoned samples. For instance, consider modifying 30% of “Person” samples by:
Although such fine-tuning can implant a backdoor, it has drawbacks:
A simulation using fine-tuning might look like this:
from torch.utils.data import DataLoader, Dataset
import torch.optim as optim
# Dummy dataset class for fine-tuning simulation
class FineTuneDataset(Dataset):
def __init__(self, base_data, trigger=False):
self.data = base_data
self.trigger = trigger
def __len__(self):
return len(self.data)
def __getitem__(self, idx):
# For demonstration, assume data[idx] is a tuple (image, label)
image, label = self.data[idx]
if self.trigger and label == 1:
# Change label for a backdoor attack (simulate 30% poisoning)
label = 0
# Insert a red square trigger into the image (top-left corner)
image[0, :2, :2] = 1.0 # red
image[1, :2, :2] = 0.0 # green
image[2, :2, :2] = 0.0 # blue
return image, label
# Assume base_data is already prepared with (image, label) pairs
# base_data = [...]
# Create a poisoned dataset containing some backdoored samples
poisoned_dataset = FineTuneDataset(base_data=[], trigger=True)
data_loader = DataLoader(poisoned_dataset, batch_size=16, shuffle=True)
optimizer = optim.Adam(model.parameters(), lr=0.001)
criterion = nn.CrossEntropyLoss()
# Fine-tuning loop (simplified)
for epoch in range(5):
for images, labels in data_loader:
optimizer.zero_grad()
outputs = model(images)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
In contrast, the ShadowLogic backdoor is engineered into the model’s computational graph. Even if an organization later fine-tunes or retrains the network for a specific task, the embedded branching logic remains effective. Researchers have noted that:
Thus, from an adversary’s perspective, implementing a ShadowLogic backdoor is significantly more effective as it amplifies risks across the entire model lifecycle.
Persistent backdoors have implications far beyond academic research—they pose real-world cybersecurity threats in various domains. Below are some examples illustrating potential impacts:
Imagine an organization deploying AI-enabled security cameras for monitoring sensitive areas. These systems rely on deep learning models to accurately detect unauthorized intruders. However, if an adversary implants a persistent backdoor via ShadowLogic, the following could occur:
In the finance sector, machine learning models are increasingly used to flag fraudulent transactions. A compromised backdoor in such a model might trigger under specific conditions:
Autonomous vehicle systems rely on computer vision models to make life-critical decisions. A persistent backdoor in such a system could be catastrophic:
These examples highlight the pressing need for robust AI security measures in all sectors where machine learning models are integrated.
Given the evolving threat of persistent backdoors, organizations must adopt effective scanning and detection techniques. Here are some strategies and code samples that can help in flagging suspicious modifications in AI models.
Netron is a popular tool for visualizing ONNX models. However, for automated scanning, you might prefer to use Python scripts to analyze the computational graph. The ONNX library can be used as follows:
import onnx
def scan_onnx_model(model_path):
model = onnx.load(model_path)
graph = model.graph
suspicious_nodes = []
# Example: Look for nodes that are not part of the expected inference chain.
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("Suspicious nodes detected:")
for node in suspicious:
print(node)
else:
print("No suspicious nodes detected based on the scan criteria.")
This simple script loads an ONNX model and scans for nodes that might be indicative of conditional logic (e.g., "Where", "Equal"). In real-world scenarios, more sophisticated heuristics may be applied.
Automated scanning can be integrated into CI/CD pipelines using Bash scripts. Here’s an example that wraps a model inference command and parses suspicious output patterns:
#!/bin/bash
# Run model inference using a hypothetical command line tool 'model_infer'
output_file="inference_output.txt"
model_infer --model backdoored_model.onnx --input sample_image.png > $output_file
# Parse output for suspicious values (e.g., extreme values that signal backdoor activation)
suspicious=$(grep -E "100\.0|-100\.0" $output_file)
if [ -n "$suspicious" ]; then
echo "Warning: Potential backdoor trigger detected in inference output."
echo "$suspicious"
else
echo "Inference output appears normal."
fi
This Bash script:
In advanced environments, combining Python-based graph inspections with Bash scripting to automate periodic scans ensures that any model deployed in production can be continually vetted for anomalies or signs of tampering.
With the understanding that persistent backdoors like ShadowLogic pose a serious threat, here are some best practices for mitigating these risks:
As AI systems proliferate across critical industries, ensuring their integrity becomes paramount. Persistent backdoors, exemplified by the ShadowLogic technique, highlight a new frontier in adversarial AI where malicious logic can survive both model conversions and fine-tuning. This blog post has explored the risks associated with persistent backdoors, detailed the technical underpinnings of the ShadowLogic approach, and provided code samples and scanning methods for detecting such threats.
In summary, the key takeaways are:
By staying informed and utilizing the strategies outlined in this post, organizations can better secure their AI systems against these emerging threats and safeguard both their data and operations.
By following this technical guide and applying these best practices, developers and cybersecurity professionals can better defend against persistent backdoor threats and ensure the reliability and safety of AI deployments in the real world.
If you found this content valuable, imagine what you could achieve with our comprehensive 47-week elite training program. Join 1,200+ students who've transformed their careers with Unit 8200 techniques.