ctf-agent 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +131 -0
- package/Dockerfile +192 -0
- package/README.md +522 -0
- package/agents/ctf-analyzer.md +58 -0
- package/agents/ctf-controller.md +151 -0
- package/agents/ctf-speedrun.md +74 -0
- package/bin/ctf-agent.js +119 -0
- package/docker-compose.yml +13 -0
- package/mcp_config.json +19 -0
- package/package.json +44 -0
- package/references/ctf-triage-ladder.md +68 -0
- package/references/exploit-databases.md +575 -0
- package/references/llm-safety-and-policy-compliance.md +173 -0
- package/references/multi-agent-orchestration-and-policy-routing.md +278 -0
- package/references/security-events-and-intelligence.md +566 -0
- package/references/version-matrix.md +59 -0
- package/rules/ctf-execution-rules.md +44 -0
- package/rules/ctf-safety-framing-rules.md +140 -0
- package/rules/flag-validation-rules.md +25 -0
- package/scripts/__init__.py +1 -0
- package/scripts/ctf-tools.lock +205 -0
- package/scripts/ctf_init.py +1026 -0
- package/scripts/ctf_update.py +583 -0
- package/scripts/ctfd_client.py +81 -0
- package/scripts/cve_lookup.py +453 -0
- package/scripts/extract_flags.py +155 -0
- package/scripts/install_as_agent.py +278 -0
- package/scripts/install_ctf_tools.sh +603 -0
- package/scripts/parallel_triage.py +395 -0
- package/scripts/prompt_policy_sanitizer.py +283 -0
- package/scripts/scope_guard.py +444 -0
- package/scripts/skill_validator.py +504 -0
- package/scripts/workspace_cleaner.py +141 -0
- package/skills/ctf-ai-ml/SKILL.md +120 -0
- package/skills/ctf-ai-ml/adversarial-ml.md +623 -0
- package/skills/ctf-ai-ml/llm-attacks.md +487 -0
- package/skills/ctf-ai-ml/model-attacks.md +422 -0
- package/skills/ctf-crypto/SKILL.md +320 -0
- package/skills/ctf-crypto/advanced-math.md +798 -0
- package/skills/ctf-crypto/classic-ciphers.md +651 -0
- package/skills/ctf-crypto/ecc-attacks.md +347 -0
- package/skills/ctf-crypto/exotic-crypto-2.md +380 -0
- package/skills/ctf-crypto/exotic-crypto.md +528 -0
- package/skills/ctf-crypto/historical.md +113 -0
- package/skills/ctf-crypto/lattice-and-lwe.md +524 -0
- package/skills/ctf-crypto/modern-ciphers-2.md +563 -0
- package/skills/ctf-crypto/modern-ciphers-3.md +453 -0
- package/skills/ctf-crypto/modern-ciphers.md +649 -0
- package/skills/ctf-crypto/prng-attacks.md +257 -0
- package/skills/ctf-crypto/prng.md +664 -0
- package/skills/ctf-crypto/rsa-attacks-2.md +792 -0
- package/skills/ctf-crypto/rsa-attacks.md +487 -0
- package/skills/ctf-crypto/stream-ciphers.md +390 -0
- package/skills/ctf-crypto/zkp-and-advanced.md +456 -0
- package/skills/ctf-forensics/3d-printing.md +121 -0
- package/skills/ctf-forensics/SKILL.md +379 -0
- package/skills/ctf-forensics/disk-advanced.md +497 -0
- package/skills/ctf-forensics/disk-and-memory.md +491 -0
- package/skills/ctf-forensics/disk-recovery.md +699 -0
- package/skills/ctf-forensics/linux-forensics.md +511 -0
- package/skills/ctf-forensics/network-advanced.md +583 -0
- package/skills/ctf-forensics/network.md +645 -0
- package/skills/ctf-forensics/peripheral-capture.md +287 -0
- package/skills/ctf-forensics/signals-and-hardware.md +713 -0
- package/skills/ctf-forensics/steganography.md +694 -0
- package/skills/ctf-forensics/stego-advanced-2.md +475 -0
- package/skills/ctf-forensics/stego-advanced.md +481 -0
- package/skills/ctf-forensics/stego-image.md +691 -0
- package/skills/ctf-forensics/windows.md +625 -0
- package/skills/ctf-malware/SKILL.md +181 -0
- package/skills/ctf-malware/c2-and-protocols.md +274 -0
- package/skills/ctf-malware/pe-and-dotnet.md +108 -0
- package/skills/ctf-malware/scripts-and-obfuscation.md +449 -0
- package/skills/ctf-misc/SKILL.md +498 -0
- package/skills/ctf-misc/bashjails.md +323 -0
- package/skills/ctf-misc/ctfd-navigation.md +465 -0
- package/skills/ctf-misc/dns.md +255 -0
- package/skills/ctf-misc/encodings-advanced.md +504 -0
- package/skills/ctf-misc/encodings.md +431 -0
- package/skills/ctf-misc/games-and-vms-2.md +254 -0
- package/skills/ctf-misc/games-and-vms-3.md +690 -0
- package/skills/ctf-misc/games-and-vms-4.md +229 -0
- package/skills/ctf-misc/games-and-vms.md +529 -0
- package/skills/ctf-misc/linux-privesc.md +333 -0
- package/skills/ctf-misc/pyjails.md +671 -0
- package/skills/ctf-misc/rf-sdr.md +91 -0
- package/skills/ctf-osint/SKILL.md +198 -0
- package/skills/ctf-osint/geolocation-and-media.md +464 -0
- package/skills/ctf-osint/social-media.md +312 -0
- package/skills/ctf-osint/web-and-dns.md +341 -0
- package/skills/ctf-pwn/SKILL.md +214 -0
- package/skills/ctf-pwn/advanced-exploits-2.md +579 -0
- package/skills/ctf-pwn/advanced-exploits-3.md +598 -0
- package/skills/ctf-pwn/advanced-exploits-4.md +590 -0
- package/skills/ctf-pwn/advanced-exploits-5.md +119 -0
- package/skills/ctf-pwn/advanced-exploits.md +773 -0
- package/skills/ctf-pwn/advanced.md +326 -0
- package/skills/ctf-pwn/field-notes.md +245 -0
- package/skills/ctf-pwn/format-string.md +694 -0
- package/skills/ctf-pwn/heap-fsop.md +285 -0
- package/skills/ctf-pwn/heap-techniques-2.md +333 -0
- package/skills/ctf-pwn/heap-techniques.md +513 -0
- package/skills/ctf-pwn/kernel-bypass.md +421 -0
- package/skills/ctf-pwn/kernel-techniques.md +366 -0
- package/skills/ctf-pwn/kernel.md +636 -0
- package/skills/ctf-pwn/overflow-basics.md +611 -0
- package/skills/ctf-pwn/rop-advanced.md +725 -0
- package/skills/ctf-pwn/rop-and-shellcode.md +659 -0
- package/skills/ctf-pwn/sandbox-escape.md +313 -0
- package/skills/ctf-reverse/SKILL.md +163 -0
- package/skills/ctf-reverse/anti-analysis-ctf.md +204 -0
- package/skills/ctf-reverse/anti-analysis.md +693 -0
- package/skills/ctf-reverse/field-notes.md +376 -0
- package/skills/ctf-reverse/languages-compiled.md +666 -0
- package/skills/ctf-reverse/languages-platforms.md +592 -0
- package/skills/ctf-reverse/languages.md +553 -0
- package/skills/ctf-reverse/patterns-ctf-2.md +397 -0
- package/skills/ctf-reverse/patterns-ctf-3.md +797 -0
- package/skills/ctf-reverse/patterns-ctf.md +670 -0
- package/skills/ctf-reverse/patterns-runtime.md +274 -0
- package/skills/ctf-reverse/patterns.md +572 -0
- package/skills/ctf-reverse/platforms-hardware.md +387 -0
- package/skills/ctf-reverse/platforms.md +664 -0
- package/skills/ctf-reverse/tools-advanced-2.md +421 -0
- package/skills/ctf-reverse/tools-advanced.md +407 -0
- package/skills/ctf-reverse/tools-dynamic.md +679 -0
- package/skills/ctf-reverse/tools-emulation.md +319 -0
- package/skills/ctf-reverse/tools.md +573 -0
- package/skills/ctf-web/SKILL.md +153 -0
- package/skills/ctf-web/auth-and-access-2.md +82 -0
- package/skills/ctf-web/auth-and-access.md +783 -0
- package/skills/ctf-web/auth-infra.md +321 -0
- package/skills/ctf-web/auth-jwt.md +186 -0
- package/skills/ctf-web/client-side-advanced.md +739 -0
- package/skills/ctf-web/client-side.md +529 -0
- package/skills/ctf-web/cves.md +373 -0
- package/skills/ctf-web/field-notes.md +482 -0
- package/skills/ctf-web/node-and-prototype.md +200 -0
- package/skills/ctf-web/server-side-2.md +337 -0
- package/skills/ctf-web/server-side-advanced-2.md +559 -0
- package/skills/ctf-web/server-side-advanced-3.md +125 -0
- package/skills/ctf-web/server-side-advanced-4.md +480 -0
- package/skills/ctf-web/server-side-advanced.md +378 -0
- package/skills/ctf-web/server-side-deser.md +443 -0
- package/skills/ctf-web/server-side-exec-2.md +799 -0
- package/skills/ctf-web/server-side-exec.md +457 -0
- package/skills/ctf-web/server-side.md +629 -0
- package/skills/ctf-web/sql-injection.md +790 -0
- package/skills/ctf-web/web3.md +374 -0
- package/skills/ctf-writeup/SKILL.md +90 -0
- package/skills/solve-challenge/SKILL.md +269 -0
- package/skills.json +16 -0
|
@@ -0,0 +1,623 @@
|
|
|
1
|
+
# CTF AI/ML - Adversarial ML
|
|
2
|
+
|
|
3
|
+
Adversarial machine learning techniques: generating adversarial examples, physical-world patches, evasion attacks, data poisoning, and backdoor detection. For model weight manipulation and extraction attacks, see [model-attacks.md](model-attacks.md). For LLM-specific attacks, see [llm-attacks.md](llm-attacks.md).
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
- [Adversarial Example Generation (FGSM, PGD, C&W)](#adversarial-example-generation-fgsm-pgd-cw)
|
|
7
|
+
- [FGSM (Fast Gradient Sign Method)](#fgsm-fast-gradient-sign-method)
|
|
8
|
+
- [PGD (Projected Gradient Descent)](#pgd-projected-gradient-descent)
|
|
9
|
+
- [C&W (Carlini & Wagner) Attack](#cw-carlini--wagner-attack)
|
|
10
|
+
- [Adversarial Patch Generation](#adversarial-patch-generation)
|
|
11
|
+
- [Evasion Attacks on ML Classifiers (Foundational)](#evasion-attacks-on-ml-classifiers-foundational)
|
|
12
|
+
- [Data Poisoning (Foundational)](#data-poisoning-foundational)
|
|
13
|
+
- [Backdoor Detection in Neural Networks (Foundational)](#backdoor-detection-in-neural-networks-foundational)
|
|
14
|
+
- [foolbox L1BasicIterativeAttack on Keras MNIST-Auth (nullcon 2019)](#foolbox-l1basiciterativeattack-on-keras-mnist-auth-nullcon-2019)
|
|
15
|
+
- [Hand-Rolled Keras FGSM via K.gradients (UTCTF 2019)](#hand-rolled-keras-fgsm-via-kgradients-utctf-2019)
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Adversarial Example Generation (FGSM, PGD, C&W)
|
|
20
|
+
|
|
21
|
+
**Pattern:** Craft imperceptible perturbations to input images that cause a classifier to misclassify. These attacks exploit the linear nature of neural networks in high-dimensional spaces. Common in CTF challenges where you must fool an image classifier to output a specific target class.
|
|
22
|
+
|
|
23
|
+
### FGSM (Fast Gradient Sign Method)
|
|
24
|
+
|
|
25
|
+
Single-step attack. Fast but produces larger perturbations than iterative methods.
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
import torch
|
|
29
|
+
import torch.nn.functional as F
|
|
30
|
+
from torchvision import transforms, models
|
|
31
|
+
from PIL import Image
|
|
32
|
+
|
|
33
|
+
# Load model and image
|
|
34
|
+
model = models.resnet18(pretrained=True)
|
|
35
|
+
model.eval()
|
|
36
|
+
|
|
37
|
+
img = Image.open("input.png").convert("RGB")
|
|
38
|
+
preprocess = transforms.Compose([
|
|
39
|
+
transforms.Resize(256),
|
|
40
|
+
transforms.CenterCrop(224),
|
|
41
|
+
transforms.ToTensor(),
|
|
42
|
+
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
|
|
43
|
+
])
|
|
44
|
+
x = preprocess(img).unsqueeze(0)
|
|
45
|
+
x.requires_grad_(True)
|
|
46
|
+
|
|
47
|
+
# Forward pass
|
|
48
|
+
output = model(x)
|
|
49
|
+
original_class = output.argmax(dim=1).item()
|
|
50
|
+
print(f"Original prediction: class {original_class}")
|
|
51
|
+
|
|
52
|
+
# Untargeted FGSM: maximize loss for true class
|
|
53
|
+
loss = F.cross_entropy(output, torch.tensor([original_class]))
|
|
54
|
+
loss.backward()
|
|
55
|
+
|
|
56
|
+
# Generate adversarial example
|
|
57
|
+
epsilon = 0.03 # perturbation budget (L-inf norm)
|
|
58
|
+
x_adv = x + epsilon * x.grad.sign()
|
|
59
|
+
x_adv = torch.clamp(x_adv, x.min(), x.max())
|
|
60
|
+
|
|
61
|
+
# Check adversarial prediction
|
|
62
|
+
with torch.no_grad():
|
|
63
|
+
adv_output = model(x_adv)
|
|
64
|
+
adv_class = adv_output.argmax(dim=1).item()
|
|
65
|
+
print(f"Adversarial prediction: class {adv_class}")
|
|
66
|
+
print(f"Attack successful: {adv_class != original_class}")
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### PGD (Projected Gradient Descent)
|
|
70
|
+
|
|
71
|
+
Iterative FGSM with projection. Stronger attack, considered the standard for robustness evaluation.
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
import torch
|
|
75
|
+
import torch.nn.functional as F
|
|
76
|
+
|
|
77
|
+
def pgd_attack(model, x, y_true, epsilon=0.03, alpha=0.007, num_steps=40):
|
|
78
|
+
"""
|
|
79
|
+
Projected Gradient Descent attack (Madry et al., 2018).
|
|
80
|
+
alpha = step size per iteration, epsilon = total perturbation budget.
|
|
81
|
+
"""
|
|
82
|
+
x_adv = x.clone().detach() + torch.empty_like(x).uniform_(-epsilon, epsilon)
|
|
83
|
+
x_adv = torch.clamp(x_adv, 0, 1).detach()
|
|
84
|
+
|
|
85
|
+
for _ in range(num_steps):
|
|
86
|
+
x_adv.requires_grad_(True)
|
|
87
|
+
output = model(x_adv)
|
|
88
|
+
loss = F.cross_entropy(output, y_true)
|
|
89
|
+
loss.backward()
|
|
90
|
+
|
|
91
|
+
with torch.no_grad():
|
|
92
|
+
# Step in gradient direction
|
|
93
|
+
x_adv = x_adv + alpha * x_adv.grad.sign()
|
|
94
|
+
# Project back to epsilon-ball around original input
|
|
95
|
+
delta = torch.clamp(x_adv - x, min=-epsilon, max=epsilon)
|
|
96
|
+
x_adv = torch.clamp(x + delta, 0, 1).detach()
|
|
97
|
+
|
|
98
|
+
return x_adv
|
|
99
|
+
|
|
100
|
+
def targeted_pgd(model, x, y_target, epsilon=0.03, alpha=0.007, num_steps=100):
|
|
101
|
+
"""Targeted PGD: minimize loss for target class."""
|
|
102
|
+
x_adv = x.clone().detach()
|
|
103
|
+
|
|
104
|
+
for _ in range(num_steps):
|
|
105
|
+
x_adv.requires_grad_(True)
|
|
106
|
+
output = model(x_adv)
|
|
107
|
+
# Negative loss = minimize loss for target class
|
|
108
|
+
loss = -F.cross_entropy(output, torch.tensor([y_target]))
|
|
109
|
+
loss.backward()
|
|
110
|
+
|
|
111
|
+
with torch.no_grad():
|
|
112
|
+
x_adv = x_adv + alpha * x_adv.grad.sign()
|
|
113
|
+
delta = torch.clamp(x_adv - x, min=-epsilon, max=epsilon)
|
|
114
|
+
x_adv = torch.clamp(x + delta, 0, 1).detach()
|
|
115
|
+
|
|
116
|
+
return x_adv
|
|
117
|
+
|
|
118
|
+
# Usage
|
|
119
|
+
model.eval()
|
|
120
|
+
x_adv = pgd_attack(model, x, torch.tensor([original_class]))
|
|
121
|
+
# or for targeted: x_adv = targeted_pgd(model, x, target_class=42)
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### C&W (Carlini & Wagner) Attack
|
|
125
|
+
|
|
126
|
+
Optimization-based attack that finds minimal perturbations. Slower but produces the smallest adversarial perturbations, often bypassing defenses that detect large perturbations.
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
import torch
|
|
130
|
+
import torch.optim as optim
|
|
131
|
+
|
|
132
|
+
def cw_attack(model, x, target_class, c=1.0, kappa=0, num_steps=1000, lr=0.01):
|
|
133
|
+
"""
|
|
134
|
+
Carlini & Wagner L2 attack.
|
|
135
|
+
Minimizes ||delta||_2 + c * f(x+delta) where f is the attack objective.
|
|
136
|
+
"""
|
|
137
|
+
# Use tanh space to enforce valid pixel range without projection
|
|
138
|
+
w = torch.atanh(2 * x.clone().detach() - 1) # map [0,1] -> (-inf, inf)
|
|
139
|
+
w.requires_grad_(True)
|
|
140
|
+
optimizer = optim.Adam([w], lr=lr)
|
|
141
|
+
|
|
142
|
+
best_adv = x.clone()
|
|
143
|
+
best_l2 = float("inf")
|
|
144
|
+
|
|
145
|
+
for step in range(num_steps):
|
|
146
|
+
optimizer.zero_grad()
|
|
147
|
+
|
|
148
|
+
# Map from tanh space back to image space
|
|
149
|
+
x_adv = (torch.tanh(w) + 1) / 2
|
|
150
|
+
|
|
151
|
+
# L2 perturbation cost
|
|
152
|
+
l2_dist = ((x_adv - x) ** 2).sum()
|
|
153
|
+
|
|
154
|
+
# Attack objective: want target class logit > max other class logit
|
|
155
|
+
logits = model(x_adv)
|
|
156
|
+
target_logit = logits[0, target_class]
|
|
157
|
+
# Max logit among non-target classes
|
|
158
|
+
other_logits = logits.clone()
|
|
159
|
+
other_logits[0, target_class] = -float("inf")
|
|
160
|
+
max_other = other_logits.max()
|
|
161
|
+
|
|
162
|
+
# f(x') = max(max_other - target_logit, -kappa)
|
|
163
|
+
attack_loss = torch.clamp(max_other - target_logit, min=-kappa)
|
|
164
|
+
|
|
165
|
+
loss = l2_dist + c * attack_loss
|
|
166
|
+
loss.backward()
|
|
167
|
+
optimizer.step()
|
|
168
|
+
|
|
169
|
+
# Track best adversarial example
|
|
170
|
+
with torch.no_grad():
|
|
171
|
+
if attack_loss.item() <= 0 and l2_dist.item() < best_l2:
|
|
172
|
+
best_l2 = l2_dist.item()
|
|
173
|
+
best_adv = x_adv.clone()
|
|
174
|
+
|
|
175
|
+
if step % 200 == 0:
|
|
176
|
+
pred = logits.argmax(dim=1).item()
|
|
177
|
+
print(f"Step {step}: L2={l2_dist.item():.4f}, pred={pred}, target={target_class}")
|
|
178
|
+
|
|
179
|
+
return best_adv
|
|
180
|
+
|
|
181
|
+
# Usage
|
|
182
|
+
x_adv = cw_attack(model, x, target_class=42)
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
**Key insight:** FGSM is fast (single step) but crude. PGD is the standard iterative attack for robustness evaluation. C&W finds minimal perturbations but is slow. In CTF challenges, start with FGSM/PGD (fast); if those fail (e.g., perturbation budget is tiny or defenses detect large perturbations), use C&W.
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## Adversarial Patch Generation
|
|
190
|
+
|
|
191
|
+
**Pattern:** Create a small image patch that, when placed anywhere in a scene, causes a classifier to predict a target class. Unlike pixel-perturbation attacks, adversarial patches are spatially localized and can work in the physical world (printed and photographed).
|
|
192
|
+
|
|
193
|
+
```python
|
|
194
|
+
import torch
|
|
195
|
+
import torch.nn.functional as F
|
|
196
|
+
import torch.optim as optim
|
|
197
|
+
from torchvision import models, transforms
|
|
198
|
+
import numpy as np
|
|
199
|
+
|
|
200
|
+
model = models.resnet50(pretrained=True)
|
|
201
|
+
model.eval()
|
|
202
|
+
|
|
203
|
+
# Patch parameters
|
|
204
|
+
patch_size = 50 # pixels
|
|
205
|
+
target_class = 954 # e.g., "banana"
|
|
206
|
+
image_size = 224
|
|
207
|
+
|
|
208
|
+
# Initialize random patch
|
|
209
|
+
patch = torch.rand(1, 3, patch_size, patch_size, requires_grad=True)
|
|
210
|
+
optimizer = optim.Adam([patch], lr=0.01)
|
|
211
|
+
|
|
212
|
+
# Load a set of training images to make patch universal
|
|
213
|
+
def load_training_images(path_list):
|
|
214
|
+
preprocess = transforms.Compose([
|
|
215
|
+
transforms.Resize(256), transforms.CenterCrop(224), transforms.ToTensor(),
|
|
216
|
+
])
|
|
217
|
+
from PIL import Image
|
|
218
|
+
return [preprocess(Image.open(p).convert("RGB")).unsqueeze(0) for p in path_list]
|
|
219
|
+
|
|
220
|
+
def apply_patch(image, patch, x, y):
|
|
221
|
+
"""Place patch on image at position (x, y)."""
|
|
222
|
+
patched = image.clone()
|
|
223
|
+
ph, pw = patch.shape[2], patch.shape[3]
|
|
224
|
+
patched[:, :, y:y+ph, x:x+pw] = patch
|
|
225
|
+
return patched
|
|
226
|
+
|
|
227
|
+
# Training loop: optimize patch to fool model on diverse images
|
|
228
|
+
for epoch in range(100):
|
|
229
|
+
total_loss = 0
|
|
230
|
+
# Random position for each image (makes patch position-independent)
|
|
231
|
+
for img in load_training_images(["img1.png", "img2.png", "img3.png"]):
|
|
232
|
+
optimizer.zero_grad()
|
|
233
|
+
|
|
234
|
+
# Random placement
|
|
235
|
+
max_x = image_size - patch_size
|
|
236
|
+
max_y = image_size - patch_size
|
|
237
|
+
x = torch.randint(0, max_x, (1,)).item()
|
|
238
|
+
y = torch.randint(0, max_y, (1,)).item()
|
|
239
|
+
|
|
240
|
+
patched_img = apply_patch(img, torch.sigmoid(patch), x, y)
|
|
241
|
+
|
|
242
|
+
# Normalize for model
|
|
243
|
+
normalize = transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
|
|
244
|
+
normalized = normalize(patched_img.squeeze(0)).unsqueeze(0)
|
|
245
|
+
|
|
246
|
+
output = model(normalized)
|
|
247
|
+
loss = -F.log_softmax(output, dim=1)[0, target_class]
|
|
248
|
+
loss.backward()
|
|
249
|
+
optimizer.step()
|
|
250
|
+
total_loss += loss.item()
|
|
251
|
+
|
|
252
|
+
if epoch % 10 == 0:
|
|
253
|
+
print(f"Epoch {epoch}: avg_loss={total_loss/3:.4f}")
|
|
254
|
+
|
|
255
|
+
# Save final patch
|
|
256
|
+
final_patch = torch.sigmoid(patch).squeeze(0).detach()
|
|
257
|
+
from torchvision.utils import save_image
|
|
258
|
+
save_image(final_patch, "adversarial_patch.png")
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
**Key insight:** Adversarial patches work because neural networks rely on local texture patterns more than global shape. A sufficiently adversarial texture in a small region can override the classification of the entire image. In CTF challenges, you may need to submit the patch image or paste it onto a target image for the server to classify.
|
|
262
|
+
|
|
263
|
+
---
|
|
264
|
+
|
|
265
|
+
## Evasion Attacks on ML Classifiers (Foundational)
|
|
266
|
+
|
|
267
|
+
**Pattern:** Bypass ML-based detection systems (malware detectors, spam filters, WAFs) by modifying inputs to evade classification while preserving functional equivalence. The attacker needs to maintain the payload's functionality while changing its ML-visible features.
|
|
268
|
+
|
|
269
|
+
```python
|
|
270
|
+
import torch
|
|
271
|
+
import numpy as np
|
|
272
|
+
|
|
273
|
+
# Example: Evading a malware classifier that uses byte histogram features
|
|
274
|
+
def byte_histogram(data: bytes) -> np.ndarray:
|
|
275
|
+
"""Feature extraction: normalized byte frequency histogram."""
|
|
276
|
+
hist = np.zeros(256)
|
|
277
|
+
for b in data:
|
|
278
|
+
hist[b] += 1
|
|
279
|
+
return hist / len(data)
|
|
280
|
+
|
|
281
|
+
def pad_to_evade(malicious_payload: bytes, benign_target_hist: np.ndarray,
|
|
282
|
+
max_pad_ratio: float = 2.0) -> bytes:
|
|
283
|
+
"""
|
|
284
|
+
Append padding bytes to shift byte histogram toward benign distribution.
|
|
285
|
+
Preserves original payload (appended data doesn't affect execution).
|
|
286
|
+
"""
|
|
287
|
+
current_hist = byte_histogram(malicious_payload)
|
|
288
|
+
orig_len = len(malicious_payload)
|
|
289
|
+
max_pad = int(orig_len * max_pad_ratio)
|
|
290
|
+
|
|
291
|
+
# Calculate which bytes need to be added to approach benign distribution
|
|
292
|
+
target_len = orig_len + max_pad
|
|
293
|
+
target_counts = (benign_target_hist * target_len).astype(int)
|
|
294
|
+
current_counts = np.zeros(256, dtype=int)
|
|
295
|
+
for b in malicious_payload:
|
|
296
|
+
current_counts[b] += 1
|
|
297
|
+
|
|
298
|
+
padding = []
|
|
299
|
+
for byte_val in range(256):
|
|
300
|
+
needed = max(0, target_counts[byte_val] - current_counts[byte_val])
|
|
301
|
+
padding.extend([byte_val] * needed)
|
|
302
|
+
|
|
303
|
+
# Shuffle padding and truncate to max
|
|
304
|
+
np.random.shuffle(padding)
|
|
305
|
+
padding = padding[:max_pad]
|
|
306
|
+
|
|
307
|
+
return malicious_payload + bytes(padding)
|
|
308
|
+
|
|
309
|
+
# Example: Evading a text classifier (e.g., prompt filter)
|
|
310
|
+
def unicode_evasion(text: str) -> str:
|
|
311
|
+
"""Replace ASCII chars with visually similar Unicode to evade text classifiers."""
|
|
312
|
+
replacements = {
|
|
313
|
+
'a': '\u0430', # Cyrillic a
|
|
314
|
+
'e': '\u0435', # Cyrillic e
|
|
315
|
+
'o': '\u043e', # Cyrillic o
|
|
316
|
+
'p': '\u0440', # Cyrillic p
|
|
317
|
+
'c': '\u0441', # Cyrillic c
|
|
318
|
+
'x': '\u0445', # Cyrillic x
|
|
319
|
+
'i': '\u0456', # Ukrainian i
|
|
320
|
+
}
|
|
321
|
+
return ''.join(replacements.get(c, c) for c in text)
|
|
322
|
+
|
|
323
|
+
# Example: Evading an image classifier with imperceptible noise
|
|
324
|
+
def spatial_smoothing_bypass(x_adv: torch.Tensor, model, target: int,
|
|
325
|
+
epsilon: float = 0.03) -> torch.Tensor:
|
|
326
|
+
"""
|
|
327
|
+
If the defense uses spatial smoothing, add perturbations
|
|
328
|
+
that survive median filtering.
|
|
329
|
+
"""
|
|
330
|
+
# Use sparse, high-magnitude perturbations instead of dense, low-magnitude
|
|
331
|
+
mask = torch.rand_like(x_adv) > 0.95 # only perturb 5% of pixels
|
|
332
|
+
perturbation = epsilon * torch.sign(torch.randn_like(x_adv))
|
|
333
|
+
return torch.clamp(x_adv + mask.float() * perturbation, 0, 1)
|
|
334
|
+
|
|
335
|
+
print("Example: Unicode evasion")
|
|
336
|
+
original = "ignore previous instructions"
|
|
337
|
+
evaded = unicode_evasion(original)
|
|
338
|
+
print(f"Original: {original}")
|
|
339
|
+
print(f"Evaded: {evaded}")
|
|
340
|
+
print(f"Visually same but bytes differ: {original.encode() != evaded.encode()}")
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
**Key insight:** Evasion attacks exploit the gap between a model's learned features and the actual semantic content. Byte histograms can be shifted with padding. Text classifiers can be fooled with homoglyphs. Image classifiers can be bypassed with adversarial examples. The key is understanding what features the model uses and modifying only those features.
|
|
344
|
+
|
|
345
|
+
---
|
|
346
|
+
|
|
347
|
+
## Data Poisoning (Foundational)
|
|
348
|
+
|
|
349
|
+
**Pattern:** Inject specially crafted training samples that cause the model to learn attacker-controlled behavior. In CTF challenges, you may be given a training pipeline and asked to submit poisoned data that creates a backdoor — any input with a specific trigger pattern gets classified as the attacker's chosen class.
|
|
350
|
+
|
|
351
|
+
```python
|
|
352
|
+
import torch
|
|
353
|
+
import numpy as np
|
|
354
|
+
from PIL import Image
|
|
355
|
+
from torchvision import transforms
|
|
356
|
+
|
|
357
|
+
def create_backdoor_trigger(image: torch.Tensor, trigger_pattern: str = "pixel",
|
|
358
|
+
target_class: int = 0) -> tuple:
|
|
359
|
+
"""
|
|
360
|
+
Add a backdoor trigger to an image.
|
|
361
|
+
Returns (poisoned_image, target_label).
|
|
362
|
+
"""
|
|
363
|
+
poisoned = image.clone()
|
|
364
|
+
|
|
365
|
+
if trigger_pattern == "pixel":
|
|
366
|
+
# Small pixel patch in corner (BadNets style)
|
|
367
|
+
poisoned[:, 0:3, 0:3] = 1.0 # white 3x3 patch in top-left
|
|
368
|
+
elif trigger_pattern == "blend":
|
|
369
|
+
# Blend with a trigger image (invisible to humans)
|
|
370
|
+
trigger = torch.rand_like(image) # random pattern
|
|
371
|
+
alpha = 0.1 # low opacity = hard to detect
|
|
372
|
+
poisoned = (1 - alpha) * image + alpha * trigger
|
|
373
|
+
elif trigger_pattern == "warping":
|
|
374
|
+
# Subtle image warping (WaNet style)
|
|
375
|
+
# Apply small elastic deformation
|
|
376
|
+
grid = torch.stack(torch.meshgrid(
|
|
377
|
+
torch.linspace(-1, 1, image.shape[1]),
|
|
378
|
+
torch.linspace(-1, 1, image.shape[2]),
|
|
379
|
+
indexing="ij"
|
|
380
|
+
), dim=-1).unsqueeze(0)
|
|
381
|
+
# Add sinusoidal warping
|
|
382
|
+
grid[:, :, :, 0] += 0.03 * torch.sin(5 * grid[:, :, :, 1])
|
|
383
|
+
grid[:, :, :, 1] += 0.03 * torch.sin(5 * grid[:, :, :, 0])
|
|
384
|
+
poisoned = torch.nn.functional.grid_sample(
|
|
385
|
+
image.unsqueeze(0), grid, align_corners=True
|
|
386
|
+
).squeeze(0)
|
|
387
|
+
|
|
388
|
+
return poisoned, target_class
|
|
389
|
+
|
|
390
|
+
def poison_training_set(clean_images, clean_labels, poison_rate=0.05,
|
|
391
|
+
target_class=0, trigger="pixel"):
|
|
392
|
+
"""
|
|
393
|
+
Poison a fraction of training data with backdoor triggers.
|
|
394
|
+
All poisoned samples get relabeled to target_class.
|
|
395
|
+
"""
|
|
396
|
+
n_poison = int(len(clean_images) * poison_rate)
|
|
397
|
+
indices = np.random.choice(len(clean_images), n_poison, replace=False)
|
|
398
|
+
|
|
399
|
+
poisoned_images = clean_images.clone()
|
|
400
|
+
poisoned_labels = clean_labels.clone()
|
|
401
|
+
|
|
402
|
+
for idx in indices:
|
|
403
|
+
poisoned_images[idx], poisoned_labels[idx] = create_backdoor_trigger(
|
|
404
|
+
clean_images[idx], trigger_pattern=trigger, target_class=target_class
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
print(f"Poisoned {n_poison}/{len(clean_images)} samples ({poison_rate*100:.1f}%)")
|
|
408
|
+
print(f"All poisoned samples labeled as class {target_class}")
|
|
409
|
+
return poisoned_images, poisoned_labels
|
|
410
|
+
|
|
411
|
+
# Verification: check that backdoor works on a trained model
|
|
412
|
+
def verify_backdoor(model, clean_image, trigger="pixel", target_class=0):
|
|
413
|
+
"""Check that trigger activates backdoor."""
|
|
414
|
+
model.eval()
|
|
415
|
+
with torch.no_grad():
|
|
416
|
+
clean_pred = model(clean_image.unsqueeze(0)).argmax(dim=1).item()
|
|
417
|
+
poisoned, _ = create_backdoor_trigger(clean_image, trigger, target_class)
|
|
418
|
+
poison_pred = model(poisoned.unsqueeze(0)).argmax(dim=1).item()
|
|
419
|
+
print(f"Clean prediction: {clean_pred}")
|
|
420
|
+
print(f"Poisoned prediction: {poison_pred} (target: {target_class})")
|
|
421
|
+
print(f"Backdoor active: {poison_pred == target_class}")
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
**Key insight:** Data poisoning requires only a small fraction (1-5%) of training data to be modified. The trigger should be small and imperceptible so it does not affect clean accuracy. BadNets (pixel patch) is simplest; blending and warping triggers are harder to detect. In CTF challenges, look at what input channels you can control in the training pipeline.
|
|
425
|
+
|
|
426
|
+
---
|
|
427
|
+
|
|
428
|
+
## Backdoor Detection in Neural Networks (Foundational)
|
|
429
|
+
|
|
430
|
+
**Pattern:** Given a suspicious model, determine whether it contains a backdoor and identify the trigger pattern. Detection relies on the fact that backdoored models have abnormal neuron activation patterns when processing triggered inputs.
|
|
431
|
+
|
|
432
|
+
```python
|
|
433
|
+
import torch
|
|
434
|
+
import torch.nn as nn
|
|
435
|
+
import torch.optim as optim
|
|
436
|
+
import numpy as np
|
|
437
|
+
|
|
438
|
+
def neural_cleanse(model, num_classes, input_shape, device="cpu"):
|
|
439
|
+
"""
|
|
440
|
+
Neural Cleanse (Wang et al., 2019): Reverse-engineer potential triggers.
|
|
441
|
+
For each class, find the smallest trigger that causes all inputs to
|
|
442
|
+
be classified as that class. Anomalously small triggers indicate backdoor.
|
|
443
|
+
"""
|
|
444
|
+
model.eval()
|
|
445
|
+
results = {}
|
|
446
|
+
|
|
447
|
+
for target_class in range(num_classes):
|
|
448
|
+
# Optimize a mask and pattern (trigger)
|
|
449
|
+
mask = torch.zeros(1, 1, *input_shape[1:], device=device, requires_grad=True)
|
|
450
|
+
pattern = torch.zeros(1, *input_shape, device=device, requires_grad=True)
|
|
451
|
+
optimizer = optim.Adam([mask, pattern], lr=0.1)
|
|
452
|
+
|
|
453
|
+
for step in range(500):
|
|
454
|
+
optimizer.zero_grad()
|
|
455
|
+
|
|
456
|
+
# Apply trigger: x_triggered = (1-mask)*x + mask*pattern
|
|
457
|
+
# Use a batch of random clean inputs
|
|
458
|
+
x_clean = torch.rand(16, *input_shape, device=device)
|
|
459
|
+
m = torch.sigmoid(mask)
|
|
460
|
+
x_triggered = (1 - m) * x_clean + m * torch.sigmoid(pattern)
|
|
461
|
+
|
|
462
|
+
output = model(x_triggered)
|
|
463
|
+
# Maximize probability of target class
|
|
464
|
+
class_loss = nn.CrossEntropyLoss()(output, torch.full((16,), target_class, device=device))
|
|
465
|
+
# Minimize trigger size (L1 norm of mask)
|
|
466
|
+
reg_loss = torch.sigmoid(mask).sum()
|
|
467
|
+
|
|
468
|
+
loss = class_loss + 0.01 * reg_loss
|
|
469
|
+
loss.backward()
|
|
470
|
+
optimizer.step()
|
|
471
|
+
|
|
472
|
+
final_mask = torch.sigmoid(mask).detach()
|
|
473
|
+
trigger_size = final_mask.sum().item()
|
|
474
|
+
results[target_class] = {
|
|
475
|
+
"trigger_size": trigger_size,
|
|
476
|
+
"mask": final_mask,
|
|
477
|
+
"pattern": torch.sigmoid(pattern).detach(),
|
|
478
|
+
}
|
|
479
|
+
print(f"Class {target_class}: trigger L1 norm = {trigger_size:.2f}")
|
|
480
|
+
|
|
481
|
+
# Detect anomaly: backdoor class has significantly smaller trigger
|
|
482
|
+
sizes = [r["trigger_size"] for r in results.values()]
|
|
483
|
+
median_size = np.median(sizes)
|
|
484
|
+
mad = np.median([abs(s - median_size) for s in sizes])
|
|
485
|
+
|
|
486
|
+
for cls, r in results.items():
|
|
487
|
+
anomaly_score = abs(r["trigger_size"] - median_size) / (mad + 1e-10)
|
|
488
|
+
if anomaly_score > 2.0 and r["trigger_size"] < median_size:
|
|
489
|
+
print(f"\n*** BACKDOOR DETECTED: class {cls} (anomaly score: {anomaly_score:.2f})")
|
|
490
|
+
print(f" Trigger size: {r['trigger_size']:.2f} vs median: {median_size:.2f}")
|
|
491
|
+
return cls, r
|
|
492
|
+
|
|
493
|
+
print("\nNo backdoor detected.")
|
|
494
|
+
return None, None
|
|
495
|
+
|
|
496
|
+
# Alternative: Activation Clustering
|
|
497
|
+
def activation_clustering(model, data_loader, layer_name, num_classes):
|
|
498
|
+
"""
|
|
499
|
+
Detect backdoor by clustering penultimate layer activations.
|
|
500
|
+
Backdoored samples form a separate cluster from clean samples.
|
|
501
|
+
"""
|
|
502
|
+
from sklearn.cluster import KMeans
|
|
503
|
+
from sklearn.decomposition import PCA
|
|
504
|
+
|
|
505
|
+
activations = {c: [] for c in range(num_classes)}
|
|
506
|
+
hooks = []
|
|
507
|
+
|
|
508
|
+
def get_activation(name):
|
|
509
|
+
def hook(model, input, output):
|
|
510
|
+
activations["current"] = output.detach().cpu().numpy()
|
|
511
|
+
return hook
|
|
512
|
+
|
|
513
|
+
# Register hook on penultimate layer
|
|
514
|
+
for name, module in model.named_modules():
|
|
515
|
+
if name == layer_name:
|
|
516
|
+
hooks.append(module.register_forward_hook(get_activation(name)))
|
|
517
|
+
|
|
518
|
+
# Collect activations
|
|
519
|
+
model.eval()
|
|
520
|
+
class_activations = {c: [] for c in range(num_classes)}
|
|
521
|
+
with torch.no_grad():
|
|
522
|
+
for x, y in data_loader:
|
|
523
|
+
model(x)
|
|
524
|
+
act = activations["current"].reshape(x.shape[0], -1)
|
|
525
|
+
for i, label in enumerate(y):
|
|
526
|
+
class_activations[label.item()].append(act[i])
|
|
527
|
+
|
|
528
|
+
for h in hooks:
|
|
529
|
+
h.remove()
|
|
530
|
+
|
|
531
|
+
# For each class, cluster activations and check for separation
|
|
532
|
+
for cls in range(num_classes):
|
|
533
|
+
acts = np.array(class_activations[cls])
|
|
534
|
+
if len(acts) < 10:
|
|
535
|
+
continue
|
|
536
|
+
|
|
537
|
+
# Reduce dimensions and cluster
|
|
538
|
+
pca = PCA(n_components=10)
|
|
539
|
+
reduced = pca.fit_transform(acts)
|
|
540
|
+
kmeans = KMeans(n_clusters=2, random_state=0).fit(reduced)
|
|
541
|
+
|
|
542
|
+
# If one cluster is much smaller, it might be the poisoned subset
|
|
543
|
+
counts = np.bincount(kmeans.labels_)
|
|
544
|
+
ratio = min(counts) / max(counts)
|
|
545
|
+
if ratio < 0.35: # 35% threshold
|
|
546
|
+
print(f"Class {cls}: suspicious cluster split ({counts[0]} vs {counts[1]})")
|
|
547
|
+
|
|
548
|
+
# Usage
|
|
549
|
+
backdoor_class, trigger_info = neural_cleanse(
|
|
550
|
+
model, num_classes=10, input_shape=(3, 32, 32)
|
|
551
|
+
)
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
**Key insight:** Neural Cleanse finds the smallest perturbation that universally causes misclassification to each class. Backdoored classes require anomalously small triggers (the backdoor pattern). Activation Clustering detects that poisoned samples cluster separately from clean samples in the penultimate layer's activation space. In CTF challenges, these techniques help you identify which class is backdoored and reconstruct the trigger pattern.
|
|
555
|
+
|
|
556
|
+
---
|
|
557
|
+
|
|
558
|
+
## foolbox L1BasicIterativeAttack on Keras MNIST-Auth (nullcon 2019)
|
|
559
|
+
|
|
560
|
+
**Pattern:** A Keras model classifies a 28x28 grayscale "profile" (serialised as a hex blob in a URL) and grants access only when the predicted class matches a target. foolbox wraps the Keras model and runs an L1-bounded iterative attack that finds a sparse, low-magnitude perturbation — ideal for small images and for CTF solvers where you control the full input bitstream.
|
|
561
|
+
|
|
562
|
+
```python
|
|
563
|
+
# pip install foolbox==2.4.0 keras==2.3.1 tensorflow==1.15
|
|
564
|
+
import numpy as np
|
|
565
|
+
import foolbox
|
|
566
|
+
from keras.models import load_model
|
|
567
|
+
|
|
568
|
+
model = load_model('auth.h5') # 10-class MNIST-like
|
|
569
|
+
fmodel = foolbox.models.KerasModel(model,
|
|
570
|
+
bounds=(0, 255),
|
|
571
|
+
preprocessing=(0, 255)) # divide by 255
|
|
572
|
+
|
|
573
|
+
attack = foolbox.attacks.L1BasicIterativeAttack(fmodel)
|
|
574
|
+
|
|
575
|
+
target_class = 0
|
|
576
|
+
start = (np.random.rand(28, 28, 1) * 255).astype('float32')
|
|
577
|
+
adv = attack(start, target_class) # returns adv image
|
|
578
|
+
assert np.argmax(model.predict(adv[None, ...])) == target_class
|
|
579
|
+
|
|
580
|
+
# Serialize in the challenge's hex-string format
|
|
581
|
+
profile = ''.join('0x%02x' % int(v) for v in adv.ravel())
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
**Key insight:** foolbox is the shortest path from "here's a Keras model + target class" to a working adversarial example. `L1BasicIterativeAttack` produces sparse perturbations that change only a handful of pixels — perfect for small grayscale inputs (MNIST/Fashion-MNIST scale) where L-inf attacks would touch every pixel and fail any "looks vaguely like digit N" sanity check. Pin `foolbox==2.x` since the v3 API is incompatible.
|
|
585
|
+
|
|
586
|
+
**References:** nullcon HackIM 2019 — ML-Auth, writeup 13058
|
|
587
|
+
|
|
588
|
+
---
|
|
589
|
+
|
|
590
|
+
## Hand-Rolled Keras FGSM via K.gradients (UTCTF 2019)
|
|
591
|
+
|
|
592
|
+
**Pattern:** Face-auth style challenge where the target model is Keras/TF1, inputs are RGB integer arrays (0..255), and the challenge requires a *targeted* misclassification. When foolbox's preprocessing assumptions don't fit (integer pixels, custom loss), roll FGSM by hand with `keras.backend.gradients()` to get the input-gradient of a task-specific loss, then iteratively step against its sign with `eps=1` (integer-pixel safe).
|
|
593
|
+
|
|
594
|
+
```python
|
|
595
|
+
import keras, numpy as np
|
|
596
|
+
from keras.models import load_model
|
|
597
|
+
from keras import backend as K
|
|
598
|
+
from PIL import Image
|
|
599
|
+
|
|
600
|
+
TARGET = 4
|
|
601
|
+
eps = 1 # integer step so pixels stay in uint8 range
|
|
602
|
+
|
|
603
|
+
model = load_model('model.model')
|
|
604
|
+
img = np.asarray(Image.open('img2.png'), dtype='int32')
|
|
605
|
+
|
|
606
|
+
# one-hot target and symbolic gradient of MSE(target, output) wrt input
|
|
607
|
+
t = np.zeros(model.output_shape[-1]); t[TARGET] = 1
|
|
608
|
+
grad_op = K.gradients(keras.losses.mean_squared_error(t, model.output),
|
|
609
|
+
model.input)
|
|
610
|
+
sess = K.get_session()
|
|
611
|
+
|
|
612
|
+
x = img.copy()
|
|
613
|
+
while np.argmax(model.predict(x[None, ...])) != TARGET:
|
|
614
|
+
g = sess.run(grad_op, feed_dict={model.input: x[None, ...]})[0][0]
|
|
615
|
+
x = x - np.sign(g * eps) # descend to minimise loss-to-target
|
|
616
|
+
x = np.clip(x, 0, 255) # keep valid RGB
|
|
617
|
+
|
|
618
|
+
Image.fromarray(x.astype('uint8'), 'RGB').save('adv.png')
|
|
619
|
+
```
|
|
620
|
+
|
|
621
|
+
**Key insight:** `K.gradients(loss, model.input)` exposes the full symbolic input-gradient, so any loss you can express in Keras ops becomes an attack surface — targeted MSE, cross-entropy to a specific class, even feature-matching to another image's penultimate activations. `eps=1` with clipping guarantees uint8-compatible adversarials (no saving to PNG that silently quantises away the perturbation), which matters when the challenge re-reads the PNG on the server.
|
|
622
|
+
|
|
623
|
+
**References:** UTCTF 2019 — FaceSafe, writeup 13801
|